unis_manager/README.md

510 lines
42 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 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周` → 次年)
重复创建同一周期会直接返回已存在的那个,不会产生重复数据。
### 删除周期
设置页 → 周期管理,表格里列出所有周期及各自的记录数。**只有不含任何执行记录的空周期才能删除**,已有记录的会置灰并说明原因,接口层面也会二次校验。
### 年份整体平移
历史数据的年份口径需要整体调整时:
```bash
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`
## 快速开始
```bash
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 干的事)
```bash
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:
```bash
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/` 是否存在,并在构建前定好基础镜像与 pip 源 |
| `.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=` 指定),或先 `docker pull` / 给守护进程配 registry-mirrors |
| ② 构建容器里装依赖 | `[4/8] RUN pip install ... -i https://pypi.org/simple` 非零退出 | 换 pip 源 `PIP_INDEX_URL`(清华 / 阿里 / 腾讯) |
`./deploy.sh up` 构建前把这两道都定下来,原则是**能本地解决就不联网**:
- 基础镜像只看本地有没有:`BASE_IMAGE` 指定的镜像在本地 → 直接用;只有同名 tag(如 `python:3.12-slim`)→ 也用(仓库前缀只是当初从哪儿拉的,镜像是同一份)。本地确实没有才试拉一次(守护进程配的 registry-mirrors 在这一步生效),还拉不到就**直接报错停下**并打印补救命令——不去逐个试加速站,那种循环只会白等几分钟。
- pip 源:在构建用的容器里真 `urlopen` 一次逐个试(容器网络 = 构建网络,比宿主机 curl 准),默认清华源。
- 定下来的值只 export 给本次构建,**不写回 `.env`**(`.env` 在 git 里,把某台机器的地址带到另一台机器正是故障源)。
```bash
./deploy.sh up # 定源(本地有镜像就不联网)→ 构建 → 等健康检查
./deploy.sh mirror # 重探一次 pip 源,并打印可写进 .env 的两行
```
> **别把某台机器上的 `BASE_IMAGE` 前缀提交进仓库。** 换机器后那个前缀对应的镜像本地不存在,构建就会卡在 `load metadata`;这行留空或写裸 `python:3.12-slim`,让每台机器自己的镜像说了算。
不想用脚本,等价的两条 build-arg:
```bash
docker compose build \
--build-arg BASE_IMAGE=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` 跳过 pip 探测,源完全由 `.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`:
```bash
./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 按它归属,修改档的提示行写明了这点;重点项目**所有负责人都算命中**,定制项目按交付负责人命中;团队成员不参与该筛选 |
| **归档** | 项目从看板与列表默认视图中隐藏,数据完整保留;勾选「含归档」可查看,随时可恢复 |
| **删除** | 高危操作:弹窗列出将连带删除的子任务数与执行记录数,并且必须**逐字输入项目名称**,「永久删除」按钮才会点亮。**删除后不可恢复**,仅想隐藏请用归档 |
> 定制项目的「项目明细」表同理:列表只读,操作列只有 详情 / 修改 / 归档,改名 / 名单 / 删除都在抽屉里做。
#### 项目合并
把两个项目合并成一个(例如把「智空无人机平台」并入「智飞无人机平台」):
```bash
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 并说明原因,防止把在管项目变成无主项目 |
导入名单(表头按名称匹配:序号 / 姓名 / 归属小组 / 人员归属 / 说明):
```bash
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)。
字段可在明细表中直接改,也可在抽屉里改;重新计算用:
```bash
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=
```