From 600131dcf5c8d3b321f8fa345f4236976d76baf1 Mon Sep 17 00:00:00 2001 From: dadao Date: Wed, 28 Oct 2026 13:20:31 +0800 Subject: [PATCH] =?UTF-8?q?docs(m2):=20=E8=A1=A5=E5=B7=A5=E5=8D=95?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3=E5=A5=91=E7=BA=A6=E3=80=81M2=20=E6=B8=85?= =?UTF-8?q?=E5=8D=95=E4=B8=8E=E7=8A=B6=E6=80=81=E8=A1=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - API.md 新增第 4 节:升级/动作/列表/关系/通知与 6xxx 错误码 - ROADMAP 补 M2 全部条目与 DOD,验收用例加自验与回流两条 - DEVELOPMENT 记两条新坑:PowerShell 批量改写会毁中文、npm.ps1 被执行策略拦 - README 状态表把工单从待做改成已通,测试数 57 --- 2.Design/API.md | 83 ++++++++++++++++++++++++++++++++++++++- 2.Design/DEVELOPMENT.md | 4 +- 2.Design/ROADMAP-M0-M1.md | 66 +++++++++++++++++++++++++++---- README.md | 6 ++- 4 files changed, 147 insertions(+), 12 deletions(-) diff --git a/2.Design/API.md b/2.Design/API.md index b44b7c7..a2c18a3 100644 --- a/2.Design/API.md +++ b/2.Design/API.md @@ -1,4 +1,4 @@ -# 接口约定(M0–M1 已实现) +# 接口约定(M0–M2 已实现) > 只写**已经在跑**的接口。写完没验证的东西不进这份文档。 > 前缀 `/api/v1`。所有响应都是同一个信封,**HTTP 200 不代表业务成功**。 @@ -222,7 +222,86 @@ canceled→ todo (恢复) --- -## 4. 健康与版本 +## 4. 工单 `/workspaces/{ws_id}` + +工单**不是**另一张表里的另一种东西,它是一条 `kind='ticket'` 的 item 加一行 `ticket_ext`。 +所以详情仍然走 `GET /items/{item_id}`,响应里多一个 `ticket` 字段。 + +### 4.1 `POST /items/{item_id}/promote`(任务升级成工单) + +```json +{ "assignee_id": "uuid", "title": "可选覆盖", "body_md": "可选补充", + "due_at": "2026-10-30T10:00:00+08:00", "priority": 2, "version": 3 } +``` + +返回 `{ "task": {...}, "ticket": {...} }`。 + +- 原任务**留在原地**变成 `blocked`,不搬数据——时间线完整保留。 +- 一个任务只能升级一次,重复升级 → `6002`。 +- `assignee_id` 必须是工作区内 `active` 成员,否则 `1001`。 +- 传 `version` 走乐观锁,`4009` 表示任务刚被别人改过。 +- 被指派人立刻收到 `ticket_assigned` 通知。 + +### 4.2 工单动作 + +| 方法 | 路径 | 谁可以 | 前置状态 → 结果 | +|---|---|---|---| +| POST | `/items/{item_id}/ticket/accept` | 办理人 / admin+ | open → accepted | +| POST | `/items/{item_id}/ticket/start` | 办理人 / admin+ | accepted → in_progress | +| POST | `/items/{item_id}/ticket/submit` | 办理人 / admin+ | in_progress → pending_verify | +| POST | `/items/{item_id}/ticket/verify` | **需求人** / admin+ | pending_verify → done / in_progress | +| POST | `/items/{item_id}/ticket/reassign` | 需求人 / admin+ | 任意未完成 → open(清空受理时间) | +| POST | `/items/{item_id}/ticket/cancel` | 需求人 / 办理人 / admin+ | 未完成 → canceled | + +`verify` 的请求体:`{ "approve": true, "note": "可选说明", "resolution_md": "验收结论" }`。 + +- 驳回时 `approve=false`,回到 `in_progress`(不是 `open`——人还在做,不必重新认领),`reject_count` 加一。 +- **办理人不能验收自己的工单**,否则 `6004`。这是唯一一条"就算 owner 也不通融"的规则之外的角色限制。 +- `resolution_md` 是 M3 知识草稿的原料,尽量写。 +- 前置状态不对 → `6003`,`details` 同时给 `allowed`(当前状态还能去哪)与 `allowed_from`(该动作要求的前置状态)。 + +### 4.3 工单列表 `GET /tickets` + +| 参数 | 说明 | +|---|---| +| `role` | `assigned`(派给我的,默认)/ `requested`(我发起的)/ `all` | +| `status` | 可重复,如 `?status=open&status=accepted` | +| `limit` / `cursor` | 与任务列表一致 | + +### 4.4 事项关系 `GET|POST /items/{item_id}/links` + +```json +{ "to_item_id": "uuid", "link_type": "parent" } +``` + +`GET` 返回 `{ "children": [...], "parents": [...], "related": [...] }`。 +`link_type` 只有 `parent` / `related`,**方向是 from 是 to 的子项**;自环 `5005`、重复挂 `5005`、对方不存在 `5004`、跨租户 `5001`。 + +### 4.5 通知 `/workspaces/{ws_id}/notifications` + +| 方法 | 路径 | 说明 | +|---|---|---| +| GET | `/notifications?unread_only=true` | 我的通知,新的在前 | +| GET | `/notifications/unread-count` | `{ "unread": 3 }`,轮询用 | +| POST | `/notifications/{id}/read` | 标一条 | +| POST | `/notifications/read-all` | 全标已读 | + +`kind`:`ticket_assigned` / `ticket_submitted` / `ticket_approved` / `ticket_rejected` / `task_unblocked`。 +通知只发给当事人,同工作区里其他人看不到——这是"协同"与"广播"的区别。 + +### 4.6 工单的错误码 + +| 码 | HTTP | 含义 | +|---|---|---| +| 6001 | 404 | 工单不存在,或不是工单 | +| 6002 | 409 | 这个任务已经升级过了 | +| 6003 | 409 | 当前状态不允许这个动作 | +| 6004 | 403 | 你没有权限做这个动作(例如自验) | +| 6005 | 404 | 通知不存在,或不是你的 | + +--- + +## 5. 健康与版本 | 方法 | 路径 | 说明 | |---|---|---| diff --git a/2.Design/DEVELOPMENT.md b/2.Design/DEVELOPMENT.md index b054ebd..ceeaf79 100644 --- a/2.Design/DEVELOPMENT.md +++ b/2.Design/DEVELOPMENT.md @@ -135,6 +135,8 @@ VECTOR_DATABASE_URL=postgresql+asyncpg://dadao_app:dadao_dev_pw@127.0.0.1:5434/d | `docker compose` 报 `unknown command` | 只装了 compose v1 | 用 `docker-compose` | | Docker 起不来 | Docker Desktop 未启动 / 需要管理员 | 先启动 Desktop,`docker info` 有输出再继续 | | 迁移报 `cannot insert multiple commands into a prepared statement` | asyncpg 走预备语句,一次只能一条 SQL | 多语句 DDL 拆成每语句一次 `op.execute()` | +| 源文件里的中文变成 `宸ュ崟鍩?` | 用 PowerShell `Get-Content`/`Set-Content` 批量改文件:读时按 GBK 解码,写回 UTF-8,不可逆 | **含中文的源文件只能用编辑器/`edit` 工具改**;真要写脚本,必须显式 `[IO.File]::ReadAllText($p, [Text.Encoding]::UTF8)` + `WriteAllText(..., UTF8Encoding($false))` | +| `npm` 报 `无法加载文件 npm.ps1,因为在此系统上禁止运行脚本` | PowerShell 执行策略 | 用 `npm.cmd`,或直接 `node node_modules/vite/bin/vite.js build` | ### 团队共享开发库(fnsvr-01:8432) @@ -149,7 +151,7 @@ VECTOR_DATABASE_URL=postgresql+asyncpg://dadao_app:dadao_dev_pw@127.0.0.1:5434/d 角色:`dadao_owner`(迁移用,库 owner)与 `dadao_app`(应用用,非 superuser / 无 BYPASSRLS)。 连接串见 `backend/.env.example` 里的注释行,直接拷进 `backend/.env` 即可。 -已验证:在该库上 `alembic upgrade head` 成功,`pytest` **33 passed**(服务端 PG 17.6 vs 本机 16.4,两版都过)。 +已验证:在该库上 `alembic upgrade head`(到 0003)成功,`pytest` **57 passed**(服务端 PG 17.6 vs 本机 16.4,两版都过)。 > ⚠️ **这台服务器没有 pgvector**(`pg_available_extensions` 里只有 `pg_trgm`、`pgcrypto`)。 > M3 知识库要做向量检索的话,必须先解决扩展来源:装 pgvector(需要服务器 root 装 OS 包)/ diff --git a/2.Design/ROADMAP-M0-M1.md b/2.Design/ROADMAP-M0-M1.md index bce60ab..7f70110 100644 --- a/2.Design/ROADMAP-M0-M1.md +++ b/2.Design/ROADMAP-M0-M1.md @@ -1,4 +1,4 @@ -# M0–M1 实施清单 +# 实施清单(M0–M2) > 来源:`2.Design/PRODUCT-PLAN.md` v1.0 第 12 节 > 粒度:每项 0.3–1.5 人日,2 人并行 ≈ 2 周一个里程碑 @@ -211,26 +211,78 @@ --- -## 关键验收用例(M0–M1 必须通过) +## M2 · 工单协同(第 5–6 周,10 人日) + +### 2.1 数据(1d) +- [x] `item.kind='ticket'` 落地;迁移 `0003_tickets.py` **重写 item 的状态约束**为任务∪工单并集 + (旧约束只允许任务六态,工单的 `open/accepted/...` 会被直接拦下) +- [x] `ticket_ext`(`item_id` 既是主键又是外键,天然 1:1):需求人/办理人/原任务指针/受理·提交·验收时间/退回次数/验收结论 +- [x] `item_link`(父子、相关,禁止自环)与 `notification`(只发给当事人) +- [x] 三张表全部 `ENABLE ROW LEVEL SECURITY` + 同租户策略 + GRANT 给应用角色 + +### 2.2 领域(1.5d) +- [x] 工单状态机 `open→accepted→in_progress→pending_verify→done`(可 `canceled`),默认拒绝 +- [x] 动作→角色规则:受理/开工/提交限办理人;**验收限需求人,办理人不能自验**(`6004`);改派/取消限需求人或办理人 +- [x] 驳回回 `in_progress` 而不是 `open`,`reject_count` 累加 +- [x] 升级不搬数据:原任务变 `blocked`,验收通过后自动回流 `todo` + +### 2.3 API(1.5d) +- [x] `POST /items/{id}/promote`、`/ticket/{accept,start,submit,verify,reassign,cancel}` +- [x] `GET /tickets?role=assigned|requested|all&status=` +- [x] `GET|POST /items/{id}/links`、`DELETE /items/{id}/links/{link_id}` +- [x] `GET /notifications`、`/unread-count`、`/read`、`/read-all` +- [x] 抽出 `app/serializers.py`:任务与工单共用一份序列化,**根除"工单响应缺 tags 导致前端白屏"** + +### 2.4 Web(3d) +- [x] 工单页 `/w/:wsId/tickets`:派给我的 / 我发起的 / 全部,状态过滤,列表内联快操作 +- [x] 详情抽屉:按角色显示受理、开工、提交、验收、驳回、改派、取消;工单态顶部显示需求人/办理人/退回次数 +- [x] 任务详情底部"升级成工单"面板(从成员里选人,成功后提示"这条任务现在处于被阻塞") +- [x] 导航加未读角标(30 秒对齐一次,操作后立刻刷新) +- [x] 登录支持 `?next=` 回跳,邀请链接不再"登录完掉回首页" + +### 2.5 安卓(1.5d) +- [ ] 工单列表与动作 —— 只做到任务列表;本机无 JDK/Gradle,仍是**未编译**状态 + +### 2.6 测试与验收(0.7d) +- [x] `backend/tests/test_tickets.py` 11 个用例:升级建单+原任务阻塞+通知、全流程验收回流、驳回、 + 自验 403、非法流转、重复升级、非成员指派、角色作用域、跨租户、关系挂载、通知已读、**响应必带 tags** +- [x] 全量 `pytest -q` → **57 passed**;`ruff check` / `format --check` 全 0 +- [x] `web/e2e/tickets.mjs` 双浏览器上下文双用户 e2e,8 项断言全绿: + 升级 → B 受理/开工/提交 → A 验收通过 → 原任务回流"待办" → 两端控制台零报错 +- [x] `web/e2e/smoke.mjs` 回归复跑(抽屉多了"选人"下拉后修正选择器)7 项全绿 + +### 2.7 通知真实投递 +- [ ] 站内通知已有;微信/飞书推送推迟到 M6(M2 只要"在系统里能看到该我办了") + +### M2 完成定义(DOD) +- [x] 两个人能跑完一次真实协助:升级 → 受理 → 办理 → 提交 → 验收 → 原任务自动解锁 +- [x] 办理人无法自验、跨租户看不到工单、通知只给当事人 +- [ ] 真实用一周:所有"需要别人帮忙"的事都走工单,不再私聊催(需要真实团队) + +--- + +## 关键验收用例(M0–M2 必须通过) | # | 用例 | 期望 | 现状 | |---|---|---|---| | 1 | 租户 A 用户请求租户 B 的 workspace id | 3003 未加入(不泄露存在性) | ✅ 已覆盖 | | 2 | 同一手机号登录两次 | 同一个 user_id,不重复建号 | ✅ 已覆盖 | | 3 | 被移出团队的成员拿旧 token 请求 | 3003;refresh 也失效 | [~] access 已拒,refresh 不可撤销(见 0.6) | -| 4 | 并发两次提交同一任务的不同状态 | 一次成功,一次 409 并可重试 | [ ] M1 | +| 4 | 并发两次提交同一任务的不同状态 | 一次成功,一次 409 并可重试 | ✅ 已覆盖(版本冲突用例) | | 5 | 网络中断后重新提交 | 幂等键保证不产生两条 | ✅ 已覆盖 | | 6 | 删除任务后全局搜索 | 搜不到,且回收站可恢复 | [ ] M3 | | 7 | 停掉 Redis | `/health` 变 503,业务接口给明确错误而非 500 | [ ] dev 用内存实现,未做 | | 8 | 部署后 `/version` | commit 与仓库 HEAD 一致 | [~] 机制已有,等部署验证 | +| 9 | 办理人验收自己的工单 | 403 `6004`,不能自己给自己过 | ✅ 已覆盖 | +| 10 | 工单验收通过后原任务 | 自动从"被阻塞"回到"待办",事件流留痕 | ✅ 已覆盖(含 e2e) | --- ## 下一步排期建议 ``` -已完成 0.1–0.9(骨架、认证、租户隔离、接口规范)+ 0.13 两端壳 -待补 PR 模板 / CI 在 Gitea 实跑 / 0.14 微信 spike / Web 构建验证 -第3周 0.11 部署 + 0.12 契约 + 1.1–1.3 任务域后端 -第4周 1.4 Web 四视图 + 自己开始 dogfooding +已完成 0.1–0.9 骨架与认证 + 1.1–1.7 任务域 + 2.1–2.6 工单协同(含双人 e2e) +待补 0.11 部署 / 0.12 契约 / 0.14 微信 spike / CI 在 Gitea 实跑 / 安卓编译链 +第7–8周 M3 知识:knowledge_doc + chunk、切片与索引、工单关闭生成草稿、带引用问答 +第9–10周 M4 AI:结构化录入、人物工作侧写、主动提醒、20 条黄金用例回归 ``` diff --git a/README.md b/README.md index 0006d61..85c0fc9 100644 --- a/README.md +++ b/README.md @@ -21,11 +21,13 @@ |---|---| | 手机号验证码登录、刷新令牌、幂等请求 | ✅ 已实现并有测试 | | 工作区 / 成员 / 邀请码加入,跨租户隔离(RLS) | ✅ 同上 | -| 任务:收件箱→待办→进行中→完成、截止时间、标签、评论、事件流、乐观锁 | ✅ 后端 + Web 已通,46 个测试全绿 | +| 任务:收件箱→待办→进行中→完成、截止时间、标签、评论、事件流、乐观锁 | ✅ 后端 + Web 已通,57 个测试全绿 | | Web 列表 / 看板拖拽 / 详情侧栏编辑 / 键盘流 / 快速录入语法 | ✅ e2e 7 项断言全绿,构建通过 | | 1000 条任务下的首屏性能 | ✅ 实测 P50 38–55ms(目标 < 300ms) | | 安卓端任务列表与详情 | ⚠️ 代码已写,**本机无 JDK/Gradle,未编译验证** | -| 工单、知识库、LLM 能力 | ⬜ M2–M4 | +| 工单:升级不搬数据、受理→办理→提交→验收、驳回、改派、站内通知 | ✅ 后端 + Web 已通,双人 e2e 8 项全绿 | +| 后端测试 | ✅ 57 passed,ruff 干净 | +| 知识库、LLM 能力 | ⬜ M3–M4 | ## 快速开始