Files
nexus/Project/fonrey/TECH_STACK/API_CONTRACT.md
2026-04-28 16:39:52 +08:00

468 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
> **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/PATCHMUST 传业务 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`
- 推荐前缀:`<MODULE>_`(如 `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. 与模块文档的衔接规则
- 各模块技术方案中的“四、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 纳入 CIPositive 用例全绿 | CI status_code + response_schema 两项检查全 PASS |
| **P3 持续守护** | openapi.json diff 纳入 PR Review枚举值变更同步 enums.py | PR checklist 自动提醒 Schema 变更 |