CI / backend (push) Canceled after 0s
- ROADMAP:勾选与实际证据一一对应,未做的写明原因与去向(通知推迟到 M2、子任务与 @人 合并进 item_link) - DEVELOPMENT:新增向量库、e2e、性能实测三节 - API:补批量状态与看板接口,重排小节编号 - deploy:docker-compose.vector.yml + vector-init(独立 PG+pgvector,5434,不碰 fnsvr-01) - .gitignore:注释乱码修正,补 *.tsbuildinfo / e2e 产物
236 lines
8.3 KiB
Markdown
236 lines
8.3 KiB
Markdown
# 接口约定(M0–M1 已实现)
|
||
|
||
> 只写**已经在跑**的接口。写完没验证的东西不进这份文档。
|
||
> 前缀 `/api/v1`。所有响应都是同一个信封,**HTTP 200 不代表业务成功**。
|
||
|
||
## 0. 通用
|
||
|
||
### 0.1 信封
|
||
|
||
```json
|
||
{ "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`
|
||
|
||
```json
|
||
{ "phone": "13800000000" }
|
||
```
|
||
|
||
响应 `data: { dev_code?: "123456" }`。`ENV=dev` 时才回显验证码,生产没有这个字段。
|
||
60 秒内重复请求 → `2010`。
|
||
|
||
### `POST /auth/login`
|
||
|
||
```json
|
||
{ "phone": "13800000000", "code": "123456" }
|
||
```
|
||
|
||
```json
|
||
{ "user": { "id": "…", "phone": "138****0000", "nickname": "用户0000" },
|
||
"tokens": { "access_token": "…", "refresh_token": "…", "token_type": "bearer", "expires_in": 1800 } }
|
||
```
|
||
|
||
手机号**首次登录即注册**(昵称取"用户" + 后四位)。手机号回显永远是打码的。
|
||
|
||
### `POST /auth/refresh`
|
||
|
||
```json
|
||
{ "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`
|
||
|
||
```json
|
||
{ "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`
|
||
|
||
```json
|
||
{ "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`(看板多选拖拽)
|
||
|
||
```json
|
||
{ "status": "done",
|
||
"items": [{ "id": "…", "version": 3 }, { "id": "…", "version": 7 }] }
|
||
```
|
||
|
||
```json
|
||
{ "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`(看板一屏数据)
|
||
|
||
```json
|
||
{ "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. 健康与版本
|
||
|
||
| 方法 | 路径 | 说明 |
|
||
|---|---|---|
|
||
| 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`。
|