Files
DadaoTask/2.Design/API.md
T
dadao 600131dcf5
CI / backend (push) Canceled after 0s
docs(m2): 补工单接口契约、M2 清单与状态表
- API.md 新增第 4 节:升级/动作/列表/关系/通知与 6xxx 错误码
- ROADMAP 补 M2 全部条目与 DOD,验收用例加自验与回流两条
- DEVELOPMENT 记两条新坑:PowerShell 批量改写会毁中文、npm.ps1 被执行策略拦
- README 状态表把工单从待做改成已通,测试数 57
2026-10-28 13:20:31 +08:00

12 KiB
Raw Blame History

接口约定(M0–M2 已实现)

只写已经在跑的接口。写完没验证的东西不进这份文档。 前缀 /api/v1。所有响应都是同一个信封,HTTP 200 不代表业务成功。

0. 通用

0.1 信封

{ "code": 0, "message": "ok", "data": { }, "request_id": "c4a8…" }
  • code == 0 才是成功;非 0 时 data 里放结构化细节(例如冲突时的 current_version)。
  • request_id 出现在响应头 X-Request-Id 与日志里,报障时带上它。
  • 分页统一 {items, next_cursor, has_more};next_cursor 为空即到底。

0.2 错误码

码 HTTP 含义 客户端该怎么处理
1001 400 参数不合法 提示并标红字段
1002 401 未认证 清 token → 登录页
1004 404 资源不存在 —
1009 409 唯一冲突(如 slug 已存在) 换个名字
2001 401 缺少访问令牌 登录页
2002 401 令牌无效/过期 先试 refresh,失败再登录
2003 403 令牌类型不符(refresh 当 access 用) 程序 bug
2010 429 验证码请求太频繁 倒计时 60 秒
2011 400 验证码错误/已过期 重发
2012 403 账号被禁用 联系管理员
3001 404 工作区不存在 —
3003 403 不是该工作区成员 不要提示"工作区不存在"
3004 403 角色权限不足 隐藏对应按钮
3005 409 已经是成员 直接进工作区
3010 400 邀请码无效/过期 提示重新索取
4009 409 版本冲突(乐观锁) 用 data.current_version 重试或刷新
5001 404 任务不存在 刷新列表
5002 409 非法状态流转 用 data.allowed 决定下一步给什么按钮
5003 404 标签不存在 —

错误码发布后不许改号,客户端靠它做分支。

0.3 认证

Authorization: Bearer <access_token>。access 30 分钟,refresh 14 天。 refresh_token 只能用于 /auth/refresh(服务端校验 typ)。

0.4 幂等

所有 POST/PATCH/DELETE 都可带 Idempotency-Key: <uuid>。 24 小时内同用户 + 同 key 返回首次结果,并带响应头 X-Idempotent-Replay: 1。 只缓存 2xx/3xx,失败不缓存,所以重试是安全的。

0.5 并发

改动类接口(PATCH /items/{id}、POST /items/{id}/status、DELETE)必须带 version。 拿不到 version 就别改——先 GET 一次。


1. 认证 /auth

POST /auth/sms/send

{ "phone": "13800000000" }

响应 data: { dev_code?: "123456" }。ENV=dev 时才回显验证码,生产没有这个字段。 60 秒内重复请求 → 2010。

POST /auth/login

{ "phone": "13800000000", "code": "123456" }
{ "user": { "id": "…", "phone": "138****0000", "nickname": "用户0000" },
  "tokens": { "access_token": "…", "refresh_token": "…", "token_type": "bearer", "expires_in": 1800 } }

手机号首次登录即注册(昵称取"用户" + 后四位)。手机号回显永远是打码的。

POST /auth/refresh

{ "refresh_token": "…" }

返回同形状的 tokens。无状态 JWT,服务端不维护黑名单——token 泄露只能等过期,这是 M0 已知取舍。

GET /auth/me

返回 { user, workspaces[] },workspaces[].role 是当前用户在该工作区的角色。


2. 工作区 /workspaces

方法 路径 权限 说明
POST /workspaces 登录 建工作区,创建者自动成为 owner
GET /workspaces 登录 我加入的工作区
GET /workspaces/{ws_id} 成员 详情
GET /workspaces/{ws_id}/members 成员 游标分页
POST /workspaces/{ws_id}/invitations admin+ 生成邀请码,{role};只有 owner 能邀请 admin
POST /workspaces/{ws_id}/join 登录 {code} 凭码加入,邀请码一次性

角色等级:owner(4) > admin(3) > member(2) > guest(1)。


3. 任务 /workspaces/{ws_id}/items

所有接口都要求是该工作区成员;跨租户一律 3003,任务不存在一律 5001(不区分"没有"和"不是你的")。

3.1 GET /items

参数 取值 说明
view inbox active today week done canceled all deleted 默认 active(todo+doing+blocked)
status 可重复 与 view 叠加时取更严的
assignee me 或 uuid
tag 标签名
q 关键词 标题 + 正文,ILIKE %q%
sort updated(默认) due created due 不分页
include_deleted 布尔 与 view=all 一起用
limit 1–100,默认 20
cursor 上一页的 next_cursor

today / week 的边界按 settings.default_timezone(默认 Asia/Shanghai)算, 用半开区间 [今天 00:00, 明天 00:00),不用 UTC 切日。

3.2 POST /items

{ "title": "今天要交的周报", "body_md": "", "due_at": "2026-10-28T18:00:00+08:00",
  "priority": 1, "assignee_id": null, "status": "inbox", "tag_names": ["周报"] }

status 默认 inbox(进收件箱,先归类再排期)。返回完整 item(含 tags[]、version)。

3.3 PATCH /items/{item_id}

必须带 version。null 与"没传"含义不同:

  • 传 due_at: null → 表示"清空"要用 "clear_due_at": true
  • 同理 clear_assignee、clear_tags
  • tag_names 一旦传就是整体替换

冲突回 4009 且 data.current_version 给最新版本号。

3.4 POST /items/{item_id}/status

{ "status": "doing", "version": 3 }

状态机(backend/app/domain/task.py):

inbox   → todo, canceled
todo    → inbox, doing, done, canceled
doing   → todo, done, blocked, canceled
blocked → doing, canceled
done    → todo            (重开)
canceled→ todo            (恢复)

非法流转回 5002,data = { from, to, allowed }。 前端可以据此直接渲染"下一步能点什么",不用自己维护一份规则。 目标状态与当前相同:直接返回,版本号不变。

3.5 POST /items/batch-status(看板多选拖拽)

{ "status": "done",
  "items": [{ "id": "…", "version": 3 }, { "id": "…", "version": 7 }] }
{ "updated": [{ "id": "…", "version": 4, "unchanged": false }],
  "failed":  [{ "id": "…", "code": 4009, "message": "…", "current_version": 8 }] }
  • 不是全成功或全回滚:拖 10 条里有 1 条被同事改过,其余 9 条照样生效。 全回滚会让用户白干一次,而且更难解释"为什么什么都没变"。
  • 单次最多 50 条;failed[].code 只会是 4009(版本冲突)、5002(状态不允许)、5001(不存在)。
  • 已经是目标状态的条目返回 unchanged: true,计入 updated 但不产生事件。

3.6 GET /items/board(看板一屏数据)

{ "columns": { "todo": [...], "doing": [...], "blocked": [...], "done": [...] },
  "order": ["todo", "doing", "blocked", "done"],
  "labels": { "todo": "待办", "doing": "进行中", "blocked": "被阻塞", "done": "已完成" } }
  • 每列默认 30 条(per_column 1–100),按"截止早的在前、无截止靠后",再按更新时间。
  • 收件箱不在看板上:没归类的东西不该占着"正在做的事"的位置。
  • 一次请求拿全,避免拖拽时四列数据不同步。

3.7 DELETE /items/{item_id}?version=3

软删(写 deleted_at),只在 view=deleted / include_deleted=true 时可见。

3.8 评论

方法 路径 说明
GET /items/{item_id}/comments 时间正序,游标分页
POST /items/{item_id}/comments {body_md}

3.9 标签 /workspaces/{ws_id}/tags

方法 路径 说明
GET /tags 工作区内标签,{id,name,color}
POST /tags {name, color?};同名直接返回已有的(幂等,不报错)

4. 工单 /workspaces/{ws_id}

工单不是另一张表里的另一种东西,它是一条 kind='ticket' 的 item 加一行 ticket_ext。 所以详情仍然走 GET /items/{item_id},响应里多一个 ticket 字段。

4.1 POST /items/{item_id}/promote(任务升级成工单)

{ "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 与任务列表一致
{ "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. 健康与版本

方法 路径 说明
GET /health {status, db},DB 探活用 SELECT 1
GET /version {commit, build_time, db_role}

db_role 里的 is_super / bypass_rls / read_all 全为 false 才说明 RLS 真的生效—— 这是部署后的第一道检查,比"能登录"重要得多。

部署校验:curl /version 的 commit 必须等于 git rev-parse --short HEAD。