Files
DadaoTask/2.Design/API.md
T
Steven e25a1a4e31
CI / backend (push) Canceled after 0s
feat(task): M1 任务域后端与 Web 列表(状态机/乐观锁/视图/搜索)
- item/tag/item_tag/comment/activity 五表迁移,RLS 全覆盖,pg_trgm 子串索引
- 状态机 domain/task.py:非法流转回 5002 并给 allowed,done 写 completed_at
- /workspaces/{ws}/items 列表/详情/新建/改/状态/软删/评论/标签
- 乐观锁 UPDATE...WHERE version,冲突 4009 + current_version
- Web 任务页:视图 tab、快速新建、勾选完成、开工/阻塞、详情事件流、游标分页
- 测试 43 全绿,ruff 干净,Web 构建通过
- 文档:2.Design/API.md 新增,ROADMAP 按已验证口径重写
2026-10-28 03:48:48 +08:00

7.0 KiB
Raw Blame History

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

只写已经在跑的接口。写完没验证的东西不进这份文档。 前缀 /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 DELETE /items/{item_id}?version=3

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

3.6 评论

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

3.7 标签 /workspaces/{ws_id}/tags

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

4. 健康与版本

方法 路径 说明
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。