docs(m2): 补工单接口契约、M2 清单与状态表
CI / backend (push) Canceled after 0s

- API.md 新增第 4 节:升级/动作/列表/关系/通知与 6xxx 错误码
- ROADMAP 补 M2 全部条目与 DOD,验收用例加自验与回流两条
- DEVELOPMENT 记两条新坑:PowerShell 批量改写会毁中文、npm.ps1 被执行策略拦
- README 状态表把工单从待做改成已通,测试数 57
This commit is contained in:
dadao
2026-10-28 13:20:31 +08:00
parent fd18b2a403
commit 600131dcf5
4 changed files with 147 additions and 12 deletions
+81 -2
View File
@@ -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. 健康与版本
| 方法 | 路径 | 说明 |
|---|---|---|
+3 -1
View File
@@ -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 包)/
+59 -7
View File
@@ -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 条黄金用例回归
```
+4 -2
View File
@@ -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 |
## 快速开始