unis_manager/README.md

42 KiB
Raw Blame History

DMS管理平台

基于《软件开发部管理工作执行表.xlsx》构建的 DMS 管理平台。

侧栏是四个业务模块 + 三个支撑页:

模块 适用 关心什么 数据表
① 工作看板 每天进来的第一屏 三类工作本周期谁更新了、谁没动,点一条从一览看到详情 只读汇总(/api/weekly + /api/focus)
② 项目管理 NEX云桌面、AI Agent、智飞无人机平台、内部运营… 长期重点项目的迭代执行、子任务进展、风险阻塞 projects / tasks / updates
③ 定制项目 各省市客户定制子项目 商务阶段流转(签单→开发→交付→验收→计收)+ 按进度确认收入 custom_projects / custom_updates
④ 每周重点工作 不值得单独建项目档案的短期活儿 一条就是一件事,面板里录,归在录入周这一周,跟到完成 weekly_focus

支撑页三个:人力资源(staff,全系统「负责人」的唯一来源)、更新录入(执行 / 交付记录的唯一写入口:粘贴原文 → AI 整理 → 确认写入)、设置(周期管理、AI 配置)。

②③ 是两套彼此独立的项目体系(数据表也完全分开),共用一个人力资源库:项目与定制项目的「负责人」都从花名册里选,人员进出只维护一处。④ 的条目不是项目,不挂子任务与执行记录。① 只做汇总与跳转,自己不落数据。

周期管理

新增周期

三个入口,都会自动接续年份:

入口 操作
顶部周期下拉 选「+ 新建周期…」
更新录入页(常规 / 定制都有) 周期下拉选「+ 新建周期…」
设置页 → 周期管理 「+ 新建周期」按钮

输入格式 9月第1周(也可写 2027年1月第1周 显式指定年份),年份按以下规则自动推断:

  • 月份 大于等于 最新周期的月份 → 用最新周期的年份(9月已有第4周时补录第1周,仍留在同年)
  • 月份 小于 最新周期的月份 → 进入下一年(8月之后建 1月第1周 → 次年)

重复创建同一周期会直接返回已存在的那个,不会产生重复数据。

删除周期

设置页 → 周期管理,表格里列出所有周期及各自的记录数。只有不含任何执行记录的空周期才能删除,已有记录的会置灰并说明原因,接口层面也会二次校验。

年份整体平移

历史数据的年份口径需要整体调整时:

python scripts/shift_year.py --dry-run      # 先预演,看清楚了再执行
python scripts/shift_year.py                # 全部 +1 年
python scripts/shift_year.py --years -1     # 反向回滚

内部按年份从大到小更新,避免 2024→2025 撞上尚未更新的 2025;执行后会校验 sort_key 无重复,有重复则整体回滚。 执行前建议手动备份:cp data/board.db data/board.db.bak

快速开始

cd /Users/jiliu/WorkSpace/unis_manager
./run.sh

打开 http://127.0.0.1:8770

首次运行 run.sh 会自动完成:选择带依赖的 Python 解释器 → 缺依赖则安装 → 建库 → 导入历史 Excel → 拆分定制项目,然后启动并打开浏览器。

启动方式与排错

场景 命令
正常启动 ./run.sh
报 permission denied chmod +x run.sh 后重试(不要用 sudo)
不想改权限 bash run.sh
换端口 PORT=8771 ./run.sh
关闭热加载 DEV=0 ./run.sh(默认是开着的)
不自动开浏览器 NO_OPEN=1 ./run.sh
提示"端口已被占用" 原窗口 Ctrl+C,或 lsof -ti:8770 | xargs kill 后重跑
用 Docker 部署(不装 Python) ./deploy.sh up,见下面「Docker 部署」

⚠️ 不要用 sudo 启动:数据库文件会变成 root 属主,后续写入会失败。

热更新(默认开启):uvicorn 除 *.py 外还会监听 web/** 的 js/css/html 与 .env,改动即重启; 页面里的 web/livereload.js 每秒读一次 /health 的启动代际,发现服务重启后自动刷新浏览器。 也就是说改完代码不用手动重启、也不用手动刷新。DEV=0 ./run.sh 可关掉(关闭后该脚本首轮即自行停止)。 data/*.db 的写入不会触发重启。

手动分步执行(等价于 run.sh 干的事)

pip install -r requirements.txt
python scripts/init_db.py
python scripts/import_excel.py "/Users/jiliu/WorkSpace/定开管理/软件开发部管理工作执行表.xlsx"
python scripts/migrate_custom.py
python -m uvicorn app.main:app --port 8770

重新导入:python scripts/import_excel.py <xlsx> --reset 之后再跑一次 python scripts/migrate_custom.py (迁移脚本会删除常规表里的「定制开发」容器,重复执行前请先 reset)

Docker 部署(换机器 / 上服务器)

不想装 Python,或者要放到服务器上常开,用 Docker:

cd /path/to/unis_manager
./deploy.sh up            # 构建镜像 + 后台启动 + 等健康检查

打开 http://127.0.0.1:8770。首次启动自动建表、预置分类,并创建内置角色与 admin 账号(密码见 .env)。

文件 作用
Dockerfile 运行镜像:装依赖 + 拷 app/ web/ scripts/;启动时先跑 init_db.py(幂等)再起 uvicorn
docker-compose.yml 编排:./data 挂进容器存数据库、HOST_PORT 控对外端口、restart: unless-stopped 随 Docker 自启
deploy.sh 命令封装:up / update / restart / down / logs / status / backup / mirror,顺带检查 docker 是否在跑、.env 与 data/ 是否存在,并在构建前探测国内可用源
.dockerignore 保证 data/、.env、.git 不会被烤进镜像
场景 命令
改完代码上线 ./deploy.sh update(重建镜像并替换容器)
改了 .env(AI Key / 端口) ./deploy.sh restart
看日志排错 ./deploy.sh logs
看状态与健康检查 ./deploy.sh status
停止(数据保留) ./deploy.sh down
换对外端口 .env 里 HOST_PORT=8080,再 ./deploy.sh up
裸 compose 命令 docker compose up -d --build / docker compose down
构建时基础镜像 / pip 下载超时 ./deploy.sh mirror 重探,或手工指定 PIP_INDEX_URL=... BASE_IMAGE=... ./deploy.sh up(见下「国内网络加速」)
把现有数据带到新机器 ./deploy.sh backup → 拷到目标机 data/board.db → ./deploy.sh up

国内网络加速(docker.io / pypi.org 连不上时)

国内构建会撞两道互不相干的墙,得分开治,只治一道必然还剩一道:

卡在哪 典型报错 治法
① 守护进程拉基础镜像 failed to resolve source metadata for docker.io/library/python:3.12-slim: ... TLS handshake timeout 换仓库前缀 BASE_IMAGE(走加速站,或给守护进程配 registry-mirrors)
② 构建容器里装依赖 [4/8] RUN pip install ... -i https://pypi.org/simple 非零退出 换 pip 源 PIP_INDEX_URL(清华 / 阿里 / 腾讯)

./deploy.sh up 在构建前会自动把两道源都探出来(判据是真的 docker pull、是在容器里 urlopen,不信宿主机 curl,也不信 docker manifest inspect 的假 denied),结果写进 .env,下次以及裸跑 compose 都复用:

./deploy.sh up                  # 自动探测 → 写 .env → 构建 → 等健康检查
./deploy.sh mirror              # 只想重探一遍源(不会冲掉 .env 里手工钉死的 BASE_IMAGE)

不想用脚本,等价的两条 build-arg:

docker compose build \
  --build-arg BASE_IMAGE=docker.1ms.run/library/python:3.12-slim \
  --build-arg PIP_INDEX_URL=https://pypi.tuna.tsinghua.edu.cn/simple
docker compose up -d

几个必须知道的点:

  • 基础镜像拉成功 ≠ 构建能过:docker pull 只治①,容器里的 pip install 照旧撞 pypi.org,必须另外给②换源。
  • shell 里的 HTTP(S)_PROXY 对构建没用:Docker 守护进程和构建容器都不读宿主机的代理变量。要走代理得在 Docker Desktop → Settings → Resources → Proxies 里配 http://host.docker.internal:7897(端口换成你本机代理的)。
  • 境外机器 / 内网有自建源:CN_MIRROR=0 ./deploy.sh up 关掉探测,源完全由 .env 说了算。
  • 目标机完全不能上网:走离线路。docker save -o unis-board.tar unis-board:latest → 拷过去 docker load -i unis-board.tar → NO_BUILD=1 ./deploy.sh up(跳过构建直接用本地镜像)。
  • 构建失败时脚本会自己打印一份排查清单(deploy.sh 的 build_hint),照抄命令即可。

几个坑:

  • 数据只落在 ./data:重建镜像、down 再 up 都不动数据;备份依旧是拷走 data/board.db。
  • 时区必须设:容器默认 UTC,而周期/周次/入库时间都按本地日期算,所以 compose 固定传 TZ=Asia/Shanghai,别删。
  • 容器内端口固定 8770:.env 的 PORT 是本机 run.sh 用的,compose 里覆盖成 8770;要换端口只改 HOST_PORT。
  • 别和 run.sh 同时跑:一是 SQLite 是文件库,同一份 board.db 不要同时被本机进程和容器读写;二是本机进程绑的是 127.0.0.1:8770,比 Docker 发布的 *:8770 更精确,浏览器和 curl 127.0.0.1:8770 会全打到本机服务上(症状:/health 里 dev 是 true,而容器是 false)。两者要共存就给容器换端口:HOST_PORT=8899 ./deploy.sh up。
  • 换 MySQL:放开 Dockerfile 里 pymysql 那行重新 build,再把 .env 的 DATABASE_URL 指过去(见「迁移到 MySQL」)。

带着现有数据部署

镜像里故意不含数据(.dockerignore 排除了 data/),容器读的是宿主机挂载进来的 ./data/board.db,所以:

  • 就在本机这个目录部署 → 什么都不用做,./deploy.sh up 起来读的就是当前这份库;连 user_sessions 里的登录态都延续,别人不用重新登录。
  • 换一台机器部署 → 先把数据取出来,落到目标机的 ./data/board.db:
./deploy.sh backup                                    # 一致性快照 → data/backups/board-YYYYmmdd_HHMMSS.db
scp data/backups/board-*.db user@host:/srv/unis_manager/data/board.db
ssh user@host 'cd /srv/unis_manager && ./deploy.sh up'
  • data/board.db 本身在 git 里,git clone 也会带一份,但那是最后一次提交的状态;要「当前最新」请用 ./deploy.sh backup 的快照(走 SQLite 备份 API,本机服务正在写也能拿到一致的库)。
  • 启动时的 init_db.py 和 ensure_seed 只会补缺失的表 / 分类 / 内置角色,users 非空就不再创建 admin,不会覆盖或清空任何数据。实测:拿当前生产库快照起容器,启动前后逐表行数完全一致,123 个定制项目、22 名人员、老 token 全部照常可用。
  • 目标机上的 ./data 属主要和运行容器的人对得上(compose 以 root 跑则无此顾虑);SQLite 是文件库,同一份库别让两个实例同时写。

功能

1. 工作看板

进来第一屏:三类工作在这一页从「一览」点到「详情」,左栏挑一条,右栏跟着联动。

区域 内容
顶部 KPI 重点项目(含本期已更新数)/ 重点项目未更新 / 本周重点工作(本周到期的重点工作条数,脚注「逾期 N 条 · 已完成 N 条」)/ 执行记录 / 定制合同额(含项目数与本期计收)/ 风险阻塞
左栏 三组清单,顺序固定:① 本周工作重点(列表,逾期的置顶)② 重点项目(列表:is_key 的项目,另含本期补了执行记录的非重点项目,条目前用 ★ 区分)③ 定制项目——只给一个入口「本周定制项目动态」,左栏不铺清单(100 多个项目会淹没看板),点开才在右栏列本周有动态的。组头显示条数与「几个已更新 / 几条逾期 / 本周动态几个」;每条带负责人、状态或阶段、本期摘要、风险条数,本期有记录的点亮标记
右栏 点左栏任意一条即可联动。重点项目 → 本期执行记录 / 进度与状态(含子任务)/ 近 8 期趋势;定制项目入口 → 本周动态总览(顶部标签:本周新增 / 本周验收 / 有交付动态 / 合同额合计 / 本期计收,下面分「本周新增」「本周验收」「本周有变化」三段,每条带阶段、负责人、金额、本期计收与摘要),点某一条再下钻到该项目 → 交付环节五段条 / 财务跟踪四联卡 + 开票·回款行内输入 / 本期交付记录 / 更早的交付记录,改开票、回款、环节日期即时保存(改日期会自动推进商务阶段);每周重点工作 → 只读的说明与状态,要改点「修改」开抽屉
工具栏 「负责人」筛选、「导出本周期记录」,提示里带本期项目数与定制动态数;顶栏可切周 / 月视图(月视图自动汇总该月所有周)。「列入全部在途」开关(all_custom)不在工具栏,挪到了右栏定制总览的头部
统计概览 页面最底部的折叠区(原「执行面板」,已不占主导航位):周期活跃度柱状图、分类环形图、状态分布、进度 Top10、低进度预警、风险清单,数据来自 GET /api/stats;加载失败也不影响看板主体

数据分两路:GET /api/weekly?period_id=|year&month&owner=&all_custom= 一次返回重点项目清单 + 定制项目清单 + 顶部 KPI(含 custom_new 本期新入库数、custom_updated 本期有交付记录数);「本周工作重点」不在 /api/weekly 里,前端并发取 GET /api/focus 后合并,分堆也在这一步判定(按条目的录入周:本周录入的 + 更早周次标了「延期」又没做完的结转条目)。

定制项目的口径:/api/weekly 返回本期有交付记录、被标为重点、本周期新入库(is_new:入库日期落在本周期首末日之内)、本周期新下单(sign_date 落在本周期)或本周期已验收(update_date 落在本周期且项目状态为「已验收」)的定制项目;右栏总览默认列「本周新增」「本周验收」「本周有变化」三段,其中前两段与「定制项目 → 本周进展」的「新增项目 / 验收情况」口径完全一致(前者按「下单时间」、后者按「更新时间 + 状态=已验收」),其余只在组头计数里出现,并在底部提示「另有 N 个在途定制项目本周没有动态」。勾选总览头部的「列入全部在途」后,后端补上处于在途阶段(商机 → 已验收)的项目,总览多出一段「其他在途项目」。头部几枚标签与「合同额合计」只加总当前列出来的行(同一项目同时落在两段时只算一次),勾与不勾数字会跟着清单变。

2. 项目管理

长期重点项目(projects + tasks + updates)。执行记录的写入逻辑没变,仍旧统一走「更新录入」。

模块 说明
项目管理 表格展示分类、负责人、状态、进度、更新情况(标签只在详情抽屉里维护,列表不放,避免挤占名称列);列表只读,行内不放任何输入框和下拉,每行只有 详情 / 修改 / 归档 三个按钮(操作列吸附在表格右侧,横向滚动也不会被挤出可视区);负责人筛选下拉来自人力资源库;支持新建与「含归档」筛选。详情抽屉里的周期反馈可切「按周 / 按月」与「本周 · 近 4 周 · 近 12 周 · 全部」(按月为「本月 · 近 3 月 · 近 6 月 · 全部」),用 ‹ › 翻页看更早的周期,默认跟随顶栏选中的周期

「执行面板」不再单列模块,已折进工作看板底部的「统计概览」折叠区。

项目维护操作

操作 说明
面板分区 抽屉用标准对话框式标题栏:标题在左(下面一行副标题),基础信息的操作按钮成排在右,✕ 在最右,按钮条嵌在标题栏里(不悬浮、不吸顶,滚动时跟着标题栏常驻)。详情档是「删除 · 归档/恢复 · 标记重点 · 修改」,修改与新增档是「取消 · 保存/添加」,主按钮永远在最右、危险动作在最左,按钮文字不再重复对象名(标题栏已经写了是哪个项目/条目)。附表的写入另放一处:绿虚线 = 附表 · 更新记录条仍留在它自己的附表标题下面,只有「+ 录一笔「某周期」执行记录 / 交付记录」,点它跳到「更新录入」并自动带上该项目与顶栏当前选中的周期。附表的写入统一走更新录入(粘贴原文 → AI 整理 → 确认写入),抽屉里不另做一套表单,避免两条路写出两份数据。基础信息的按钮在标题栏、附表的更新按钮在附表上方,两类按钮不在同一个位置混着放;编辑与新增档的报错信息显示在正文最上方;周期反馈上方的 按周/按月 + 本周/近 4 周/… 标成「查看范围(只读筛选)」,只翻看不改数据
改名 改名就是改基本信息:在详情面板点「修改」,表单第一行即项目名称(项目管理与定制项目同一套逻辑)。名称非空校验 + 重名检测,失败信息显示在面板正文最上方,不弹提示跑路
负责人与团队 多负责人只适用于重点项目:重点项目可以指定多个负责人与团队成员,定制项目只有一个交付负责人(单人下拉)外加团队成员。人名一律从人力资源库下拉里选。界面上负责人不分主次:名单横向排开自动换行,每一位都带 × 可删(没有「主」标记、没有不给删的首行),「+ 添加负责人 / + 添加成员」跟在名单末尾。名单只在详情抽屉的「修改」档维护;只读处一律一排对等的小标签:重点项目的列表与详情把负责人摆成对等的一排,定制项目的列表与详情只显示一个交付负责人加团队成员标签。存储与统计口径不变:重点项目 owner 字段仍是 owners[0](删掉第一位就由后一位顶上),看板、导出、人力负载与 KPI 按它归属,修改档的提示行写明了这点;重点项目所有负责人都算命中,定制项目按交付负责人命中;团队成员不参与该筛选
归档 项目从看板与列表默认视图中隐藏,数据完整保留;勾选「含归档」可查看,随时可恢复
删除 高危操作:弹窗列出将连带删除的子任务数与执行记录数,并且必须逐字输入项目名称,「永久删除」按钮才会点亮。删除后不可恢复,仅想隐藏请用归档

定制项目的「项目明细」表同理:列表只读,操作列只有 详情 / 修改 / 归档,改名 / 名单 / 删除都在抽屉里做。

项目合并

把两个项目合并成一个(例如把「智空无人机平台」并入「智飞无人机平台」):

python scripts/merge_project.py --from "智空无人机平台" --to "智飞无人机平台" --dry-run
python scripts/merge_project.py --from "智空无人机平台" --to "智飞无人机平台"
  • 子任务同名 → 归并到目标项目的同名子任务,执行记录一并转挂
  • 子任务仅源项目有 → 直接转到目标项目名下(保留原名)
  • 分类 / 负责人 / 描述 / 标签仅在目标缺失时继承
  • 合并后按最新记录回算目标项目的进度与状态
  • 执行前建议备份:cp data/board.db data/board.db.bak

3. 定制项目

模块 说明
交付流水线(默认页签) 卡头给「在库 N 个 · 重点 N 个 · 本期更新 N 个」,漏斗本身按环节给项目数与金额(页签上不再叠一条全局 KPI 带,金额与项目数按环节累计);「入库 → 签单 → 交付 → 验收 → 计收」五段漏斗,每段给项目数与金额;口径为商务阶段 + 已录环节日期取先到哪一步算哪一步(环节日期还没补全时由商务阶段兜底)。下方「卡点项目」列出最后一个有日期的环节停留超过 CUSTOM_STEP_AGING_DAYS(默认 45)天且尚未计收的项目,可一键「处理」跳到详情抽屉补录
阶段看板 按「商机 → 已签单 → 开发中 → 交付完成 → 已验收 → 已计收 → 暂停/异常」分列展示,卡片带金额、区域、开发进度、可确认收入;列头显示项目数与金额合计。卡片只是入口,点卡片开详情抽屉;卡上的 ★ 只读(重点标记在详情面板的基础信息区改),外层不做任何直接写入
财务跟踪 页签内一排 KPI:合同总额 / 按进度可确认收入 / 已计收 / 待计收 / 已开票 / 已回款 / 待开票 / 待回款(各带比例脚注);再往下是阶段金额分布、周期活跃度、按负责人与区域汇总、待回款排行。财务口径的汇总只在这一页出现,环节与账期一屏对齐,方便财务逐月对账
项目明细 表头一行给出当前列表的合同额 / 可确认 / 已计收 / 开票 / 回款合计;按入库时间倒序的只读总表:区域 / 负责人 / 入库 · 签单 · 交付 · 验收日期 / 阶段 / 合同额 / 进度 / 可确认 / 已计收 / 开票 / 回款(金额列统一以「万」计)/ 备注;改数据点行尾「修改」
详情抽屉 分「详情 / 修改」两档:详情只读,修改才给表单(含项目名称、手动确认比例、交付环节五个日期、区域下拉、开票与回款金额)+ 收入确认卡 + 周期执行记录 + 阶段流转时间线。负责人从人力资源库里选。基础信息的动作在标题栏按钮条上(修改/保存,正文里标明不会动下面的交付记录),开票与回款金额跟着标题栏的「保存」一起提交;附表的绿虚线条(「+ 录一笔」跳更新录入 · 定制项目并带上项目与周期)留在附表标题下

4. 每周重点工作

不值得建项目档案的短期活儿:一条就是一个活儿,不用先建项目,录进来就能按周跟踪。

区块 说明
新增 工具栏「+ 新增重点工作」开独立面板(列表顶部那张内嵌录入卡已撤掉):工作内容(必填,一句话说清干什么)/ 说明(可选)/ 录入周(只读=顶栏当前选中的那一周,切周再录就归到那一周)/ 预计完成时间(日历日期,只是目标日期,不决定条目归到哪一周)/ 执行状态(取 constants.STATUSES)/ 结果反馈 / 是否延期(人工开关)
列表 工作内容 / 说明(截断预览)/ 预计完成时间(下面小字标「录入 X月第N周」)/ 状态(标了延期且没完成的挂红色「延期」徽章)/ 更新日期 / 操作 详情 · 修改 · 归档 · 删除——和两张项目列表同一套规矩:列表只读,行内不放输入框
筛选 本周 / 延期 / 已完成 / 全部 四个堆(chip 上实时显示各堆条数)+「含归档条目」;顶栏切周即换基准周,工具栏标明「本周/延期按录入周统计」;顶部全局搜索同时匹配工作内容与说明
详情抽屉 同样分「详情 / 修改」两档,基础信息的按钮全在抽屉标题栏右侧的按钮条上:详情档是「删除 · 归档 · 修改」,修改与新增档是「取消 · 添加/保存」,报错信息显示在正文最上方。点「修改」才出表单,录入周只读(改预计完成时间不会把条目搬去别的周)。重点工作条目没有附表,所以抽屉里不出现绿的更新条
删除 列表行与抽屉顶部都能删,走同一个确认弹窗,并提示「只是这周不再跟进?请改用归档」。条目不挂子任务与执行记录,不要求逐字输入名称——逐字输入项目名称只用在项目管理与定制项目的项目删除上
导出 右上角「导出 Excel」→ GET /api/export/focus,带上当前筛选条件
联动 工作看板左栏第一组「本周工作重点」与 KPI「本周重点工作」读的就是这张表;看板右栏点条目给只读详情,改内容走抽屉

分堆口径(基准周 = 顶栏选中的那一周,一律按录入周算,和预计完成时间无关):录入周等于基准周 → 本周;录入周早于基准周且「是否延期」打开且未完成 → 延期(上周及更早没做完的活儿结转进来);状态为「已完成」→ 已完成(交叉视图,条目仍留在自己那一周)。 「更早周次录入、没标延期又没完成」的条目不进当周视图,只在全部里查得到——所以每周收尾时,没做完的记得勾上「延期」,它才会结转进下周。库里不存「逾期」字段,延期是人工开关。

人力资源库

侧栏「人力资源」,是全系统负责人姓名的唯一来源(staff 表)。

区块 说明
顶部 KPI 在册人数与带项目人数、各归属小组人数、人员归属(汇智 / 云数 / 外服)分布,以及花名册外的负责人核对(历史数据里对不上花名册的人名,一个都不藏)
花名册 姓名 / 归属小组 / 人员归属 / 说明 + 该人的在管项目数、定制项目数、定制合同额、名下待计收、最近更新;小组与归属可行内改(下拉),说明行内编辑失焦即存
筛选与导出 在编 / 已归档 / 全部 + 按小组筛选;「导出 Excel」按当前筛选导出
新增 弹窗录入:姓名必填且全库唯一,小组与归属下拉取自 constants.STAFF_GROUPS / STAFF_AFFILIATIONS
改名 走「编辑」改姓名时,名下项目与定制项目的负责人、其他负责人、团队成员一并同步改掉,历史数据不会断链
归档 / 恢复 归档后不再出现在「负责人」下拉里,但历史项目仍完整保留其负责人;随时可恢复。离职人员用归档,不要用删除
删除 名下还挂着项目或定制项目时接口直接返回 400 并说明原因,防止把在管项目变成无主项目

导入名单(表头按名称匹配:序号 / 姓名 / 归属小组 / 人员归属 / 说明):

python scripts/import_staff.py --dry-run   # 只看解析结果
python scripts/import_staff.py             # 增量导入(同名跳过,不覆盖页面上的编辑)
python scripts/import_staff.py --reset     # 清空后重新导入
python scripts/import_staff.py <xlsx>      # 指定别的文件;首次运行 ./run.sh 会自动导入

负责人下拉(ownerSelectHtml)在项目新建/详情、定制项目新建/详情、工作看板等位置统一使用,选项来自花名册: 已归档的人不再作为可选项出现,只有他本来就是这条记录的负责人时才带「(已归档)」标注出现一次; 历史数据里花名册没有的人名同样补一个「(不在花名册)」选项,保证打开老项目不会把负责人改没。 多负责人与团队成员复用同一个下拉(peopleEditorHtml,现在只有重点项目的抽屉用它):人名下拉横向排开、自动换行,选项同样只有花名册里的人,所以新增成员不需要另外维护一份名单;名单里的每一位都带 × 可删,界面上不区分主次。定制项目的负责人是普通的单个 ownerSelectHtml 下拉,只有团队成员用这套名单编辑器。

入库时间

定制项目新增「入库时间」字段,当前口径为 该项目最早记录所在周期的第一天(周一):

  • 优先取原始《项目明细表》台账中该项目所在的周(该列是稀疏填写的,导入时按周分组向下继承)
  • 台账没有的,取《周工作情况总结》「定制开发」块中该项目首次出现的周
  • 再没有,依次退回 custom_updates 最早记录周期 → 项目所属周期 → 记录创建日期

周期换算规则:以该月第一个周一所在周为第 1 周,第 N 周顺延 7×(N-1) 天。 例如 2026年8月第4周 → 2026-08-24(2026-08-01 是周六,第一个周一是 08-03)。

字段可在明细表中直接改,也可在抽屉里改;重新计算用:

python scripts/add_entry_date.py --dry-run   # 预演
python scripts/add_entry_date.py             # 回填(跳过已有值)
python scripts/add_entry_date.py --force     # 全部重算

区域(省级行政区)

  • 编辑时是规范下拉,34 个省级行政区按大区分组(华北 / 东北 / 华东 / 华中 / 华南 / 西南 / 西北)
  • 列表筛选下拉同样使用规范名称,只列出实际有项目的区域
  • 历史数据里若出现「内蒙古自治区」这类非规范写法,保存时会自动归一为「内蒙古」
  • 早期从项目名推断的区域(infer_region)与标准列表口径一致,无需迁移

界面设计语言

前端是零构建的原生 JS(web/index.html + web/styles.css + web/app.js + web/icons.js),设计口径固定如下,新增页面请沿用:

  • 禁用「彩色描边」:卡片、列表、步骤条、表格一律中性灰边框(--border / --border-strong)或无边框,不用圆角多色边框表达状态。
  • 重点色只出现在三处:图标、关键数字/文本、小面积 tinted 底板(.ic-badge、.tag-mini、.kpi.accent-*)。状态色(statusMeta)与阶段色(stageMeta)是唯一配色来源,避免各页面自己调色。
  • 图标系统:web/icons.js 内置 50 个 24×24 线性 SVG,stroke=currentColor,颜色跟着文字走。对外 API: svg(name) 裸图标;badge(name) 图标 + tinted 底板(KPI 用);status() / stage() / pipe() 按状态·阶段·交付环节取图标; nameOfStatus() / nameOfStage() / statusBadge() 出文案与徽标;hydrate() 填 [data-icon];auto() 自动补前导图标;names() 列全部图标名。
  • 自动图标:auto() + MutationObserver 在每次渲染后按标题关键词给 .card-head h4 / .sec-title / .kcol-title / .wk-group-head / .notice / .empty 加前导图标,并填充 [data-icon];用 data-ico 属性防重复,标题里自带 <svg class="ic">(如阶段看板按阶段取图标)时跳过自动补图,不会出现两个图标。新增区块只要标题里带对应关键词就自动带图标,不用手写。
  • 阶段看板列头不带色条:列头用 Ico.stage(code) + --c 注入阶段色(.kcol-title .ic { color: var(--c) }),早期那种 border-top:3px solid 阶段色 的彩条已去掉。
  • 交付环节图标按 code 取:后端 CUSTOM_PIPELINE 下发的字段是 code(不是 key),前端一律 Ico.pipe(s.code);写成 s.key 会静默退化成通用圆点。
  • 表格防竖排:table.tbl th / .pill / .tag-mini 统一 white-space:nowrap,容易挤窄的列(区域、负责人)在 <td> 上加 class="nowrap";实在挤不下交给 .tbl-wrap { overflow:auto } 横向滚动。
  • 抽屉不被内容撑破:.form-row > div { min-width: 0 }(grid 子项默认 min-width:auto,会被交付环节步骤条撑出整表横向滚动),步骤条本身 flex:1 1 132px(132px = 日期输入框含日历图标的最小可用宽度)。

备注:styles.css 里的 .pcard、.board-grid 当前没有任何 JS 引用(历史遗留样式),此处只做记录,未删除。

数据导出

各列表页右上角均有 导出 Excel,导出内容会带上页面当前的筛选条件:

位置 导出内容 接口
定制项目 定制项目明细(含区域、金额、进度、收入确认、入库时间、备注 + 合计行) GET /api/export/custom
定制项目 定制项目周期记录 GET /api/export/custom-updates
项目管理 项目管理清单(分类、负责人、状态、标签、子任务数等) GET /api/export/projects
工作看板 本周期(或本月)的执行记录明细 GET /api/export/updates
每周重点工作 重点工作清单(工作内容 / 说明 / 预计完成时间 / 录入周 / 执行状态 / 是否延期 / 结果反馈 / 更新) GET /api/export/focus
人力资源库 花名册(含在管 / 定制项目数、合同额、待计收) GET /api/export/staff

导出的 xlsx 带表头样式、冻结首行、自动筛选,金额列按千分位格式;文件名含导出日期。

收入确认口径

定制项目按完工百分比法确认收入:

阶段 确认比例
商机/报备 0%
已签单/下单 10%
开发中 10% + 40% × 开发进度
交付完成 70%
已验收 90%
已计收 100%
暂停/异常 30%
已关闭 100%
  • 可确认收入 = 合同额 × 上表比例
  • 待计收 = 可确认收入 − 已计收
  • 待开票 = 合同额 − 已开票、待回款 = 合同额 − 已回款(均不小于 0,随开票 / 回款金额自动更新)
  • 开票与回款金额在项目详情抽屉、定制项目详情抽屉、工作看板右栏都能直接填
  • 项目上可填「手动确认比例」覆盖阶段推算(用于预付、分批收款等特殊情形)
  • 比例定义见 app/constants.py::CUSTOM_STAGES 与 custom_revenue_ratio(),改一处即可全局生效

数据模型

# 项目管理(长期重点项目)
Period(周期: 年/月/周, sort_key)
   └── Project(分类, 负责人 owners[0], 其他负责人, 团队成员, 状态, 优先级, 重点标记, 进度, 标签)
          ├── Task(子任务)
          └── Update(执行情况: 正文, 摘要, 进度, 状态, 风险, 下一步, 来源, 原文)

# 定制项目(独立)
CustomProject(名称, 客户, 区域, 合同额, 阶段, 开发进度, 手动确认比例,
              已计收金额, 交付负责人(单人), 团队成员, 入库/签单/交付/验收/计收日期, 开票/回款金额, 重点, 归档)
              # 定制项目不做多负责人:other_owners 列保留在库里但不再读写
   └── CustomUpdate(周期, 阶段, 内容, 进度, 本期新增下单, 本期新增计收, 风险, 下一步)

# 每周重点工作(独立:条目不是项目,不挂子任务与执行记录)
WeeklyFocus(工作内容, 说明, 录入周→Period, 预计完成时间, 执行状态, 是否延期, 结果反馈, 归档)

Staff(姓名, 归属小组, 人员归属, 说明, 排序, 归档)   # 花名册:所有「负责人 / 团队成员」下拉的唯一来源
Category / Tag / Setting / ImportLog
LedgerItem(客户项目台账)   # 只剩模型与 Excel 导入解析,界面无入口、接口已下线

原表映射关系:

  • 周工作情况总结:时间 → Period;工作内容 → Project;内容说明 → Task;完成情况 → Update (三列均为稀疏填充,导入时逐级向下继承;仅填「完成情况」的行作为上一条记录的续行合并)
  • 原「定制开发」的 92 个子任务 → custom_projects(名称尾部的金额如「(39,400.00)」会被解析成合同额)
  • 项目明细表 39 条台账 → custom_projects(区域 / 金额 / 类别→阶段 / 状态说明),与子任务同名时自动合并

AI 整理

两套体系各有独立提示词:

  • 项目管理(常规执行记录):抽取 摘要 / 进度 / 状态 / 子任务列表 / 风险 / 下一步

  • 定制项目类:抽取 商务阶段 / 开发进度 / 合同额 / 累计计收 / 本期新增计收 / 摘要 / 风险 / 下一步 / 本期明细

  • 每周重点工作不过 AI:在独立面板里直接填(工作内容 / 说明 / 预计完成时间填日历日期 / 执行状态)

  • 默认走 OpenAI 兼容协议,支持 DeepSeek、通义千问(兼容模式)、Moonshot、智谱、OpenAI、本地 Ollama 等。

    • Ollama 示例:Base URL http://localhost:11434/v1,模型 qwen2.5:14b,Key 任意填。
  • 未配置 API Key 时自动降级为本地规则解析:按编号切分事项、正则提取百分比与金额、 关键词判定阶段 —— 无需 Key 也能完成录入。

    • 阶段判定做了特殊处理:文本里同时出现「已计收」和「进度 60%」时判为开发中,计收金额由 revenue_delta 承载。
  • 解析结果写入前始终提供可编辑预览,确认后才落库,并留痕到 import_logs。

迁移到 MySQL

  1. 安装驱动:pip install pymysql cryptography
  2. 修改 .env:
    DATABASE_URL=mysql+pymysql://user:password@127.0.0.1:3306/unis_board?charset=utf8mb4
    
  3. 重启服务(表结构自动创建),再依次执行 import_excel.py --reset 与 migrate_custom.py。

数据访问层全部使用 SQLAlchemy ORM,无原生 SQL、无 SQLite 专有语法,切换方言无需改业务代码。

目录结构

app/
  main.py            FastAPI 入口(API 挂载在 /api,静态页面在 /)
  config.py          配置与 .env 加载(DATABASE_URL、AI 参数)
  db.py              engine / Session(按 URL 自动适配方言)
  models.py          ORM 模型(项目 + 定制项目 + 每周重点工作 + 花名册)
  constants.py       状态/阶段常量、收入确认比例、区域与阶段识别
  ai_parser.py       LLM 解析 + 本地规则兜底(通用 / 定制两套)
  routers/           board(周期/元数据/统计) · weekly(工作看板数据) · projects(项目管理) ·
                     custom(定制项目) · focus(每周重点工作) · staff(人力) · ai · export
web/                 index.html · styles.css · app.js(零依赖,无构建步骤)
scripts/             init_db.py · import_excel.py · migrate_custom.py · import_staff.py · add_entry_date.py · merge_project.py · shift_year.py
data/board.db        SQLite 数据库
Dockerfile           运行镜像(依赖 + 代码,启动时自动补表/预置分类)
docker-compose.yml   部署编排(./data 持久化、HOST_PORT、健康检查)
deploy.sh            部署命令封装:up / update / restart / down / logs / status
.dockerignore        排除 data/ 与 .env,不把数据和密钥打进镜像

API 速览

# 通用
GET  /api/meta                     状态字典 / 优先级 / AI 配置状态
GET  /api/periods                  周列表 + 按月聚合(含各周期记录数;today_id = 今天所在的周,顶栏默认落它)
POST /api/periods                  新增周期(传 label 如「9月第1周」,年份自动接续)
DELETE /api/periods/{id}           删除空周期(有记录则拒绝)
GET  /api/board?period_id=|year&month                             周期汇总(页面主链路不走它,统计走 /api/stats)
GET  /api/stats?period_id=|year&month
GET  /api/weekly?period_id=|year&month&owner=&all_custom=   工作看板:重点项目 + 定制项目本周动态 + KPI
                                     (custom_new 本期新入库 / custom_updated 本期有交付记录;行上带 is_new)
                                     (左栏第一组「本周工作重点」前端另取 /api/focus 合并)
GET  /api/projects                 (?archived=false|true|all)
POST /api/projects   PATCH /api/projects/{id}   DELETE /api/projects/{id}
                                     (owners/members 传人名数组,整体替换:不传=不改,传 []=清空;owners[0] 回写 owner 列供归属统计,界面不再区分主次)
GET  /api/categories   POST /api/categories   PATCH /api/categories/{id}   DELETE /api/categories/{id}

# 定制项目
GET  /api/custom/meta              阶段字典 + 区域列表 + 交付环节(pipeline) + 卡点阈值(aging_days)
GET  /api/custom/projects?stage=&region=&keyword=
POST /api/custom/projects   PATCH /api/custom/projects/{id}   DELETE /api/custom/projects/{id}
                                     (owner 是单个人名;members 传人名数组,整体替换:不传=不改,传 []=清空。不再接受 owners,多负责人只属于重点项目)
GET  /api/custom/board?period_id=|year&month    阶段看板
GET  /api/custom/stats?period_id=|year&month    收入统计
POST /api/custom/updates
POST /api/custom/ai/parse          粘贴文本 → 结构化预览
POST /api/custom/ai/commit         确认写入

# 每周重点工作
GET  /api/focus?archived=false|true|all&status=&keyword=   条目清单(period_id/period_label/sort_key 都是**录入周**,分堆由调用方判定)
POST /api/focus   PATCH /api/focus/{id}   DELETE /api/focus/{id}
                                     单条增改删;工作内容非空。period_id=录入周(前端传顶栏当前周,
                                     不传后端按今天所在的周补;due_date 只是目标日期,不改归属周)
                                     或直接给 period_id(必须存在);两者都不给 → 400

# 人力资源库
GET  /api/staff?archived=false|true|all&group=&keyword=   花名册 + 各自工作量 + 花名册外负责人 + 小组/归属字典
POST /api/staff   PATCH /api/staff/{id}   DELETE /api/staff/{id}(名下有项目时拒绝,离职请归档)

# 其他
GET  /api/settings   PUT /api/settings

# 导出(均返回 xlsx)
GET  /api/export/custom            ?region=&stage=&keyword=&archived=
GET  /api/export/custom-updates    ?period_id=|year&month
GET  /api/export/projects          ?category_id=&keyword=&archived=
GET  /api/export/staff             ?archived=false|true|all
GET  /api/export/focus             ?archived=false|true|all&status=&keyword=
GET  /api/export/updates           ?period_id=|year&month&project_id=