---
name: jwb-api-review
description: Use when 审查或新增求职工作台后端 API 时，需要按本仓库的错误码契约、状态码语义、两段式写入与安全边界逐条核对。English triggers：API review, REST design, error codes, AIP-193, Zalando API guidelines, two-phase write.
compatibility: 面向「开发 / 扩展本仓库后端」的场景；仓库在侧（jobws 指仓库内的 python tools/jobws.py）；Python 3.12+。
license: MIT
metadata:
  version: 26.9.15
---

# 后端 API 审查清单

外部参考：Google AIP-193（错误）、AIP-158（分页）、Zalando RESTful API Guidelines。
本文件是本仓库的**落地口径**——与既有代码 / 测试冲突时以测试为准。

## 错误：加法而非替换

- 每个错误必须同时带 `detail`（人读中文原文）与 `error_code`（稳定语义标识），
  用户可见的动态值进 `error_params`；
- 命名 `<域>.<语义>`，域与路由对齐（ws / app / status / approval / sys / …）；
  **同一语义跨路由必须复用同一个 code**；
- 前端语言包按 code 出一份文案——改 code 等于破坏前端文案，要当契约对待；
- 新增 code 必须同步：两个语言包 + 自动校验（判定脚本会扫全部 ApiError 调用，
  校验 code ↔ 文案 ↔ 参数三者的对齐）。

## 状态码语义（本仓库口径）

| 码 | 用于 |
|---|---|
| 400 | 值层面拒绝：未知键、越界的目标、非法路径 |
| 404 | 记录 / 工作区不存在 |
| 409 | 冲突：预览后数据变了、乐观并发失败（重试前必须重新读取） |
| 422 | 语义校验失败（格式、枚举、终态不回退）；令牌不可用 |
| 200 + ok:false | MCP 工具层的「可读拒绝」形态（宿主不当异常处理） |

## 写入：两段式与安全边界

- 写端点先预览（不落盘）→ 一次性令牌 → apply；「要写什么、写到哪里」在令牌里，
  **不在请求里**——apply 不接收路径 / 载荷参数；
- apply 的落盘段必须持资源锁（「读最新 → 重校验 → 写」整段互斥；锁在领域层的
  落盘函数里，不在 web 层单点加——CLI / MCP 走同一条路）；
- 工作区参数只收相对名，`realpath` 后校验在允许根之内；越界拒绝而非警告；
- 路径经安全拼接；错误响应不得泄露未授权位置的信息。

## 形状与分页

- 资源名用复数、动作由 HTTP 方法承载；响应字段 camelCase；与 CSV 列名对齐的
  数据字段保持原样（中文列名不翻，展示层再映射）；
- 列表响应：`items` + `total`；大集合设上限并提示「另有 N 条」（返回体不随
  数据量膨胀）；
- 时间用 ISO 8601 字符串；「未提供」用 null，不用空字符串表达。

## 审查动作（逐条走）

1. 列出本 PR 的全部写端点：每个都有预览 + 令牌吗？落盘锁在哪一层？
2. 扫 ApiError 调用：code 是否与既有同义 code 重复定义？参数是否齐全？
3. 扫前端 `err.<code>`：新增 code 两侧语言包都补齐了吗？
4. 路径与工作区边界：有没有绕过既有校验的新入口？
5. 兼容面：默认行为是否不变？破坏性变更是否走了版本流程？

## 常见错误

| 错误 | 后果 | 正确做法 |
|---|---|---|
| 新错误只给 detail 不给 code | 英文界面露出中文原文 | 同步双语言包 |
| 同义错误用了两个 code | 前端文案漂移 | 复用同一个 code |
| 写端点直接落盘 | 用户失去确认权 | 两段式 + 令牌 |
| 锁只加在 web 层 | CLI / MCP 路径并发丢写 | 落盘函数自持锁 |
| 列表接口不设上限 | 大数据撑爆响应 | items 上限 + total |
