> **For AI assistants**: Read this entire file before designing or implementing any API. Contract rules here are mandatory. Do not invent per-module variants unless explicitly allowed. # Fonrey API 契约规范(API_CONTRACT) **版本**: 1.1 **适用范围**: 全模块(account / permission / property / client / complex / org / setting) **关联总纲**: `TECH_STACK/TECH_STACK.md` **最后更新**: 2026-04-28 --- ## 1. 文档定位与原则 本文件定义 Fonrey 全局 API 契约标准,解决跨模块接口风格漂移问题。模块技术方案中的 API 章节必须遵循本文件,不得各自定义冲突规则。 ### 1.1 强制级别 - **MUST**:必须遵守,违反视为缺陷 - **SHOULD**:建议遵守,若不遵守需在模块文档注明原因 - **MAY**:可选能力 ### 1.2 适用接口类型 - JSON API(`/api/**`) - HTMX 片段端点(HTML response) - 文件上传/下载端点 --- ## 2. 请求 / 响应格式规范 ## 2.1 请求体规范(JSON API) - `Content-Type` MUST 为 `application/json` - `charset=utf-8` SHOULD 显式声明 - 写操作(POST/PUT/PATCH)MUST 传业务 payload;禁止空对象写入 推荐请求结构: ```json { "data": { "...": "业务字段" }, "meta": { "request_id": "可选,客户端透传" } } ``` 兼容说明:历史端点已存在 `filters/sort/pagination` 平铺结构时可继续使用,但新接口 SHOULD 迁移到 `data` 容器。 ## 2.2 成功响应规范(JSON API) 成功响应 MUST 使用统一 envelope: ```json { "ok": true, "data": {}, "meta": { "request_id": "uuid", "timestamp": "2026-04-27T16:30:00+08:00" } } ``` 说明: - `ok` MUST 为 `true` - `data` MUST 存在(可为空对象 `{}` 或空数组 `[]`) - `meta` SHOULD 包含 `request_id` 与服务端时间 ## 2.3 失败响应规范(JSON API) 失败响应 MUST 使用统一 envelope: ```json { "ok": false, "error": "权限不足", "code": "PROPERTY_PERMISSION_DENIED", "details": {}, "meta": { "request_id": "uuid", "timestamp": "2026-04-27T16:30:00+08:00" } } ``` 说明: - `error` MUST 为面向用户/调用方可读消息 - `code` MUST 为稳定机器可读码(大写下划线) - `details` MAY 提供字段级错误(如校验失败) ## 2.4 HTMX 响应规范 - 成功:返回 HTML 片段;必要时通过 `HX-Trigger` 触发前端事件 - 失败: - 状态码 MUST 正确(4xx/5xx) - SHOULD 在响应头返回 `HX-Trigger`,例如: - `{"toast:error":"权限不足"}` - `{"toast:error":"请求失败,请重试"}` --- ## 3. 错误码规范 ## 3.1 命名规则 - 错误码 MUST 为 `UPPER_SNAKE_CASE` - 推荐前缀:`_`(如 `PROPERTY_` / `CLIENT_` / `ORG_`) ## 3.2 HTTP 状态码基线 | HTTP | 使用场景 | 示例 code | |---|---|---| | 400 | 参数错误、业务前置条件不满足 | `PROPERTY_VALIDATION_ERROR` | | 401 | 仅用于纯 API Token 鉴权失败(当前 Web 会话模式一般不用) | `AUTH_UNAUTHORIZED` | | 403 | 已登录但无权限 | `*_PERMISSION_DENIED` | | 404 | 资源不存在或不可见 | `*_NOT_FOUND` | | 409 | 状态冲突、任务未就绪 | `*_STATE_CONFLICT` / `*_JOB_NOT_READY` | | 422 | 字段级校验错误(可选) | `*_VALIDATION_FAILED` | | 429 | 频控触发 | `RATE_LIMITED` | | 500 | 未预期异常 | `INTERNAL_ERROR` | ## 3.3 稳定性要求 - `code` MUST 可稳定依赖,不得频繁改名 - 错误文案 `error` 可优化,但不应影响调用方流程判断 --- ## 4. 分页规范 Fonrey 列表查询 MUST 使用 Keyset 分页;禁止 OFFSET 深分页。 ## 4.1 请求格式 ```json { "filters": {}, "sort": {"field": "updated_at", "order": "desc"}, "pagination": {"mode": "keyset", "cursor": null, "limit": 20} } ``` ## 4.2 响应格式 ```json { "ok": true, "data": { "items": [], "next_cursor": "opaque_cursor_2" }, "meta": { "pagination": {"mode": "keyset", "cursor": "opaque_cursor", "limit": 20} } } ``` ## 4.3 约束 - `limit` MUST 有上限(建议 ≤ 100) - `cursor` MUST 为不透明字符串,禁止暴露内部排序字段组合 - 排序字段 MUST 来自白名单,防止 SQL 注入与慢查询 --- ## 5. 搜索 / 筛选规范 ## 5.1 推荐请求结构 ```json { "filters": { "keyword": "保利", "status": ["active", "pending"], "district_id": "uuid" }, "sort": {"field": "updated_at", "order": "desc"}, "pagination": {"mode": "keyset", "cursor": null, "limit": 20} } ``` ## 5.2 语义规范 - `keyword`:模糊检索词(服务端统一做 trim) - 多选条件 MUST 使用数组(如 `status: []`) - 空数组 `[]` 语义:不限制该条件 - `null` 语义:由模块文档明确(默认建议等同“不传”) ## 5.3 安全与性能 - 仅允许白名单字段参与筛选和排序 - LIKE/全文检索字段 SHOULD 建立索引或搜索策略 - 查询快照哈希(用于缓存/导出)SHOULD 对 filters+sort+scope 进行规范化后计算 --- ## 6. 上传规范 Fonrey 优先采用“预签名上传 + 回执提交(commit)”两段式。 ## 6.1 标准流程 1. 客户端请求 upload-token(业务 API) 2. 客户端直传对象存储(R2) 3. 客户端调用 commit API 回写元数据 ## 6.2 合约要求 - upload-token MUST 短时有效(建议 5~15 分钟) - commit MUST 幂等(建议支持 `idempotency_key`) - 上传白名单与大小限制 MUST 在模块文档声明并在服务端校验 - SHOULD 校验 `content_type` 与 `size` - MAY 增加 `sha256` 校验确保完整性 ## 6.3 错误码建议 - `*_UPLOAD_TOKEN_EXPIRED` (409/400) - `*_UPLOAD_FILE_TOO_LARGE` (400) - `*_UPLOAD_FILE_TYPE_NOT_ALLOWED` (400) - `*_UPLOAD_COMMIT_CONFLICT` (409) --- ## 7. 文件下载规范 下载统一采用“导出任务 + 状态查询 + download endpoint”。 ## 7.1 标准流程 1. 创建导出任务 `POST /api/**/export/jobs/` 2. 轮询任务状态 `GET /api/**/export/jobs/{job_id}/` 3. 下载结果 `GET /api/**/export/jobs/{job_id}/download/` ## 7.2 合约要求 - 任务未完成下载 MUST 返回 `409` + `*_EXPORT_JOB_NOT_READY` - 下载链接 SHOULD 为一次性或短时有效 URL - 响应 SHOULD 设置 `Content-Disposition`(附件下载) - 文件名 SHOULD 带模块与日期,便于审计 --- ## 8. 权限拒绝返回规范 ## 8.1 JSON API - 未登录:MUST 返回 `302`(Web Session 场景)或 `401`(纯 API 场景) - 已登录无权限:MUST 返回 `403` - 失败体 MUST 使用统一错误 envelope,`code` 为 `*_PERMISSION_DENIED` ## 8.2 页面路由(SSR/HTMX) - 未登录:302 跳转登录页 - 已登录无权限:403 页面(或 HTMX 403 片段) - HTMX 拒绝 SHOULD 触发 `HX-Trigger` toast 事件 ## 8.3 测试强约束 每个受保护端点 MUST 覆盖三态: - 200(有权限) - 403(已登录无权限) - 302/401(未登录,视端点类型) --- ## 9. 乐观锁(Optimistic Locking)规范 ### 9.1 适用场景 `properties`、`clients`、`complexes` 三张高竞争表的更新操作(`PUT`/`PATCH`)**MUST** 使用乐观锁并发控制,防止"后写覆盖先写"数据丢失。 ### 9.2 请求规范 客户端发起更新时,MUST 在请求体中携带当前资源版本号: ```json { "data": { "sale_price": 180, "version": 3 } } ``` > 兼容说明:当前 Fonrey 为内部 Web / Electron 客户端,采用请求体传递 `version` 字段,无需 `If-Match` Header(避免 HTMX 额外配置复杂度)。未来若提供对外开放 REST API,可补充支持 `If-Match: ` Header 形式。 ### 9.3 服务端执行规范 服务端执行 UPDATE 时 MUST 同时匹配 `version`,并将 `version` +1: ```sql UPDATE properties SET sale_price = :sale_price, version = version + 1, updated_at = NOW(), updated_by = :operator_id WHERE id = :id AND version = :client_version -- 乐观锁匹配 AND deleted_at IS NULL; ``` - 若受影响行数 **= 1**:更新成功,返回 `200` - 若受影响行数 **= 0**:抛 `ConflictError`,返回 `409` + code `*_VERSION_CONFLICT` ### 9.4 冲突响应规范 ```json { "ok": false, "error": "已被他人修改,请刷新重试", "code": "PROPERTY_VERSION_CONFLICT", "details": { "field": "version", "your_version": 3, "hint": "请重新获取最新数据后再提交" }, "meta": { "request_id": "uuid", "timestamp": "2026-04-28T10:00:00+08:00" } } ``` - HTTP 状态码 MUST 为 `409` - `code` 格式:`_VERSION_CONFLICT`(如 `PROPERTY_VERSION_CONFLICT` / `CLIENT_VERSION_CONFLICT` / `COMPLEX_VERSION_CONFLICT`) - 前端 SHOULD 展示 Toast:**「已被他人修改,请刷新重试」**,并自动触发资源重新加载 ### 9.5 Check List - [ ] `version` 字段在 GET 响应中 MUST 返回(供后续 PUT/PATCH 携带) - [ ] 服务层 update 方法 MUST 校验受影响行数,0 行时抛 `ConflictError` - [ ] 前端表单 MUST 在隐藏域中保存 `version`,随 PUT/PATCH 提交 - [ ] HTMX 场景:冲突时后端 MUST 返回 `HX-Trigger: {"toast:error":"已被他人修改,请刷新重试"}` - [ ] 测试 MUST 覆盖:并发两次更新同版本,第二次 MUST 返回 `409` --- ## 10. 与模块文档的衔接规则 - 各模块技术方案中的“四、API 设计原则”“六、关键 API 规范”“十二、错误码建议”必须引用本文件 - 模块文档可补充模块特有 code 与字段,但不得与本规范冲突 - 冲突时以本文件为准;若需例外,必须在模块文档显式记录 ADR 链接 --- ## 10. 落地检查清单(Review Checklist) - [ ] 是否使用统一成功/失败 envelope - [ ] 错误码是否为稳定 `UPPER_SNAKE_CASE` - [ ] 列表接口是否全部 Keyset 分页 - [ ] filters/sort 字段是否白名单化 - [ ] 上传是否采用 token+commit 且具备幂等保障 - [ ] 下载是否采用 job 流程并处理未就绪 409 - [ ] 权限拒绝是否遵循 200/403/302(401) 三态 - [ ] 测试是否覆盖契约关键路径 - [ ] 所有视图是否附加 `@extend_schema`(或 `@extend_schema_view`)注解 - [ ] 枚举字段是否通过 `OpenApiTypes` 或 `ChoiceField` 在 Schema 中完整暴露所有值 - [ ] 生成的 `openapi.json` 是否已提交 / 与代码同步更新 - [ ] `schemathesis` 契约测试是否纳入 CI(至少覆盖 Positive 用例) --- ## 11. OpenAPI 落地规范(机器可读契约) > **For AI assistants**: §11 是实现层强约定。生成视图代码时必须同步写 @extend_schema;生成测试代码时必须包含契约断言。 ### 11.1 工具链(MUST) | 角色 | 工具 | 说明 | |---|---|---| | Schema 生成 | `drf-spectacular` | 唯一授权的 OpenAPI 生成库;禁止 drf-yasg | | Schema 文件 | `openapi.json`(根目录) | 每次 CI 必须重新生成并 diff 检查 | | 契约测试 | `schemathesis` | 基于生成 Schema 做 Positive + Negative 测试 | | 文档 UI | Swagger UI(`/api/docs/`) | 开发环境默认开启,生产环境按需 | 安装: ```bash pip install drf-spectacular schemathesis ``` `settings.py` 最低配置: ```python INSTALLED_APPS += ["drf_spectacular"] SPECTACULAR_SETTINGS = { "TITLE": "Fonrey API", "VERSION": "1.1.0", "SERVE_INCLUDE_SCHEMA": False, "SCHEMA_PATH_PREFIX": r"/api/", # 枚举值直接展开(不折叠成 $ref),便于 AI agent 直接读值 "ENUM_GENERATE_CHOICE_DESCRIPTION": True, "COMPONENT_SPLIT_REQUEST": True, } ``` `urls.py`: ```python from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView urlpatterns += [ path("api/schema/", SpectacularAPIView.as_view(), name="schema"), path("api/docs/", SpectacularSwaggerView.as_view(url_name="schema"), name="swagger-ui"), ] ``` ### 11.2 视图注解规范(MUST) 每个 `APIView` / `ViewSet` 动作 MUST 携带 `@extend_schema`,最低包含: ```python from drf_spectacular.utils import extend_schema, OpenApiParameter, OpenApiExample from drf_spectacular.types import OpenApiTypes @extend_schema( summary="获取房源详情", # 简短操作名(中文 OK) tags=["property"], # 模块 tag,与路由前缀一致 responses={200: PropertyDetailSerializer}, # 失败响应也要声明,给 AI agent 提供完整错误路径 responses={ 200: PropertyDetailSerializer, 403: OpenApiTypes.OBJECT, # 统一 error envelope 404: OpenApiTypes.OBJECT, }, ) def retrieve(self, request, pk=None): ... ``` 枚举字段 MUST 在 Serializer 中使用 `ChoiceField` 并指定 `choices`,`drf-spectacular` 会自动生成 `enum` 约束: ```python from ENUMS import PropertyType # 取自项目枚举常量 class PropertySerializer(serializers.Serializer): property_type = serializers.ChoiceField( choices=PropertyType.choices, help_text="房源类型(详见 DATA_MODEL/ENUMS.md § property.property_type)" ) ``` 分页/筛选端点额外声明 `parameters`: ```python @extend_schema( parameters=[ OpenApiParameter("cursor", OpenApiTypes.STR, description="Keyset 游标,首页传 null"), OpenApiParameter("limit", OpenApiTypes.INT, description="每页条数,最大 100"), OpenApiParameter("status", OpenApiTypes.STR, description="状态筛选,多选用逗号分隔"), ] ) ``` ### 11.3 Schema 文件管理(MUST) CI pipeline MUST 包含以下步骤,防止 Schema 与代码漂移: ```bash # 生成最新 Schema python manage.py spectacular --color --file openapi.json # diff 检查(有变更时 CI 提醒,但不阻断 — 由开发者 review 后提交) git diff --exit-code openapi.json || echo "⚠️ openapi.json has changed — please review and commit" ``` - `openapi.json` MUST 纳入版本控制(不加入 `.gitignore`) - 合并 PR 时若 `openapi.json` 有非预期变更,MUST 作为 Review 阻断项 ### 11.4 契约测试规范(MUST) 使用 `schemathesis` 对每个模块做 Positive 路径覆盖,最低要求: ```bash # 对本地运行的 dev server 跑契约测试 schemathesis run openapi.json \ --base-url http://localhost:8000 \ --auth-header "Authorization: Bearer $TEST_TOKEN" \ --checks status_code_conformance response_schema_conformance \ --tag property # 可按模块 tag 分批跑 ``` CI 集成示例(GitHub Actions): ```yaml - name: Contract Tests run: | python manage.py spectacular --file openapi.json schemathesis run openapi.json \ --base-url http://localhost:8000 \ --checks status_code_conformance response_schema_conformance \ --exitfirst # 首个失败即停止 ``` **AI Agent 验收词(Acceptance Criteria)**:实现任意 API 端点后,须能通过以下验证: ``` GIVEN openapi.json 已重新生成 WHEN schemathesis 对该端点执行 Positive 测试 THEN status_code_conformance PASS(响应码与 Schema 声明一致) AND response_schema_conformance PASS(响应体结构与 Serializer 一致) AND 所有枚举字段值落在 ENUMS.md 定义的合法值集合内 ``` ### 11.5 枚举值契约一致性(MUST) `ENUMS.md` 是枚举的单一事实来源(Source of Truth)。项目 MUST 维护一个 `enums.py`(或按模块拆分),与 `ENUMS.md` 保持同步: ```python # fonrey/core/enums.py — 机器可读枚举常量,与 ENUMS.md 严格对齐 from django.db import models class PropertyType(models.TextChoices): RESIDENTIAL = "residential", "住宅" COMMERCIAL = "commercial", "商业" OFFICE = "office", "办公" INDUSTRIAL = "industrial", "工业" LAND = "land", "土地" OTHER = "other", "其他" ``` AI agent 实现时验证词: ``` GIVEN enums.py 中某枚举类的所有 value WHEN 与 ENUMS.md 对应域的值列表对比 THEN 两侧完全一致(无多余值,无缺失值,大小写相同) ``` ### 11.6 分阶段落地路线(参考) | 阶段 | 目标 | 完成标志 | |---|---|---| | **P0 接入** | 安装 drf-spectacular,生成首版 `openapi.json`,Swagger UI 可访问 | `/api/docs/` 正常渲染,无 import 报错 | | **P1 注解补全** | 所有现有视图加 `@extend_schema`,枚举字段用 ChoiceField | `openapi.json` 无 `{}` 空 Schema;所有端点有 `summary` 和 `tags` | | **P2 契约测试** | schemathesis 纳入 CI,Positive 用例全绿 | CI status_code + response_schema 两项检查全 PASS | | **P3 持续守护** | openapi.json diff 纳入 PR Review;枚举值变更同步 enums.py | PR checklist 自动提醒 Schema 变更 |