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

14 KiB
Raw Blame History

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禁止空对象写入

推荐请求结构:

{
  "data": {
    "...": "业务字段"
  },
  "meta": {
    "request_id": "可选,客户端透传"
  }
}

兼容说明:历史端点已存在 filters/sort/pagination 平铺结构时可继续使用,但新接口 SHOULD 迁移到 data 容器。

2.2 成功响应规范JSON API

成功响应 MUST 使用统一 envelope

{
  "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

{
  "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 请求格式

{
  "filters": {},
  "sort": {"field": "updated_at", "order": "desc"},
  "pagination": {"mode": "keyset", "cursor": null, "limit": 20}
}

4.2 响应格式

{
  "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 推荐请求结构

{
  "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_typesize
  • 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 返回 302Web Session 场景)或 401(纯 API 场景)
  • 已登录无权限MUST 返回 403
  • 失败体 MUST 使用统一错误 envelopecode*_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)注解
  • 枚举字段是否通过 OpenApiTypesChoiceField 在 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/ 开发环境默认开启,生产环境按需

安装:

pip install drf-spectacular schemathesis

settings.py 最低配置:

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

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,最低包含:

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 并指定 choicesdrf-spectacular 会自动生成 enum 约束:

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

@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 与代码漂移:

# 生成最新 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 路径覆盖,最低要求:

# 对本地运行的 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

- 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 保持同步:

# 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.jsonSwagger UI 可访问 /api/docs/ 正常渲染,无 import 报错
P1 注解补全 所有现有视图加 @extend_schema,枚举字段用 ChoiceField openapi.json{} 空 Schema所有端点有 summarytags
P2 契约测试 schemathesis 纳入 CIPositive 用例全绿 CI status_code + response_schema 两项检查全 PASS
P3 持续守护 openapi.json diff 纳入 PR Review枚举值变更同步 enums.py PR checklist 自动提醒 Schema 变更