Files
shanxia2.0/openspec/changes/zkxw-engineering-standards/specs/api-conventions/spec.md
T
2026-08-21 16:50:47 +08:00

26 KiB
Raw Blame History

ADDED Requirements

Requirement: 统一 POST 接口

除认证类接口(登录 RSA 公钥获取等系统级 GET 接口)和文件上传/下载接口外,新增的业务 API 端点 SHALL 统一使用 HTTP POST 方法。SHALL NOT 使用 GET、PUT、DELETE 方法定义新的业务路由。

存量代码不追溯:本规范仅约束新增模块(如 CRM、报销、地磅等迁移模块)。app/api/admin.py、app/api/public.py、app/api/menu.py 等既有模块保持现有 GET/POST/PUT/DELETE 混用风格,SHALL NOT 为符合新规范而重构已有代码。新旧模块风格并存是正常的过渡状态。

Scenario: 新增业务路由

  • WHEN 开发者在 app/api/ 中新增业务接口
  • THEN 路由装饰器 SHALL 使用 @router.post(),SHALL NOT 使用 @router.get() / @router.put() / @router.delete()

Scenario: 存量模块不修改

  • WHEN app/api/admin.py 等既有模块使用 @router.get() / @router.put() / @router.delete()
  • THEN SHALL NOT 为符合新规范而修改这些文件,保持原样

Scenario: 查询类接口

  • WHEN 实现列表查询、详情查询、下拉选项等只读接口
  • THEN 依然使用 POST 方法,查询条件通过请求体(JSON body)传入

Scenario: 文件上传下载例外

  • WHEN 接口涉及文件上传(如附件上传、Excel 导入)
  • THEN 可使用 POST + multipart/form-data,请求体使用 FormData 而非 JSON
  • WHEN 接口涉及文件下载(如 Excel 导出、模板下载)
  • THEN 可使用 POST,返回 StreamingResponse 或 FileResponse,Content-Type 为对应文件类型

Requirement: 统一请求体格式

所有 POST 业务接口的请求体 SHALL 使用 JSON 格式,并遵循以下分类规范:

列表查询请求体

{
  "page": 1,
  "pageSize": 10,
  "<筛选字段1>": "<值>",
  "<筛选字段2>": "<值>"
}
  • page:页码,默认 1,从 1 开始
  • pageSize:每页条数,默认 10,最大 500
  • 筛选字段为可选,按业务需要定义
  • 排序字段暂不在请求体中定义,使用后端默认排序(create_time DESC),后续按需扩展

全量查询请求体(不分页)

当接口需要返回全量数据(如树形列表、下拉选项、字典数据、所有标签等)时,请求体 SHALL NOT 包含 page 和 pageSize 字段。后端 SHALL 返回全量数据,不进行分页。

{
  "<筛选字段1>": "<值>"
}

关键约束:SHALL NOT 通过设置 pageSize: 999、pageSize: 9999 等大数值来模拟全量查询。这会导致后端参数校验 422 错误(pageSize 上限为 500)。需要全量数据时,必须调用不分页接口。

Scenario: 产品列表分页查询

  • WHEN 前端调用产品列表接口
  • THEN 请求体 SHALL 为 { "page": 1, "pageSize": 10, "productName": "关键词", "categoryId": "xxx", "status": "1" }

Scenario: 产品分类树形全量查询

  • WHEN 前端调用产品分类树形接口(不分页)
  • THEN 请求体 SHALL NOT 包含 page 和 pageSize 字段,后端返回全量树形数据

Scenario: 下拉选项全量查询

  • WHEN 前端调用下拉选项接口(如产品选择器、分类选择器)
  • THEN 请求体 SHALL NOT 包含 page 和 pageSize 字段,后端返回全量选项列表

Scenario: 禁止超大 pageSize 模拟全量

  • WHEN 开发者需要获取全量数据
  • THEN SHALL NOT 设置 pageSize: 999 / pageSize: 9999 / pageSize: 1000 等超过 500 的值
  • THEN SHALL 使用对应的不分页接口(如 /options、/tree)

Scenario: 后端 pageSize 校验

  • WHEN 后端分页接口接收到 pageSize > 500
  • THEN SHALL 返回 422 参数校验错误,SHALL NOT 静默截断或放行

详情查询请求体

{
  "id": "<主键ID>"
}

新增请求体

{
  "<业务字段1>": "<值>",
  "<业务字段2>": "<值>"
}
  • id SHALL NOT 出现在新增请求体中(由后端自动生成 UUID)
  • tenant_no SHALL NOT 出现在请求体中(由后端从当前用户上下文注入)
  • 公共字段(create_by、create_time、del_flag 等)SHALL NOT 出现在请求体中

更新请求体

{
  "id": "<主键ID>",
  "<业务字段1>": "<值>",
  "<业务字段2>": "<值>"
}
  • id 为必填,标识要更新的记录
  • 其余字段为可选(只传需要更新的字段)

删除请求体

{
  "id": "<主键ID>"
}
  • 单条删除使用单个 id
  • 批量删除使用 "ids": ["id1", "id2", ...]

Scenario: 禁止客户端传递系统字段

  • WHEN 前端调用新增或更新接口
  • THEN 请求体 SHALL NOT 包含 id(新增时)、tenant_no、create_by、create_time、update_by、update_time、del_flag 等系统字段

Requirement: JSON 字段命名(前后端 camelCase 对齐)

Web 管理端(web-admin)与对外 JSON 接口中,请求体与响应 data 内的业务字段 SHALL 使用 camelCase(如 categoryName、pageSize、createTime)。Python 服务层(Pydantic 模型属性名、CRUD 函数参数、model_dump() 传给 ORM 的键)SHALL 继续使用 snake_case(如 category_name)。

新增或迁移模块的 Pydantic 请求体(app/schemas/** 或路由内联 BaseModel)SHALL 同时满足:

  1. model_config = ConfigDict(populate_by_name=True)
  2. 多词字段定义 Python 名为 snake_case,并通过 Field(..., alias="camelCase名") 声明 JSON 别名
  3. 路由内调用 CRUD 时优先使用 body.model_dump(exclude_unset=True)(默认按字段名输出 snake_case),SHALL NOT 要求前端改为 snake_case
  4. 路由层读取字段时 SHALL 使用 Schema 上的 Python 属性名(见下表),SHALL NOT 使用 JSON alias 名作为 body.xxx 访问(alias 仅参与反序列化,不会在模型上生成 camelCase 属性)
前端 JSON(camelCase) Python 属性(路由内访问) 说明
createTimeRange body.create_time_range 常见列表筛选
viewMode body.view_mode 如 pool / mine / all
customerName body.customer_name
customerIds body.customer_ids 批量公海操作
pageSize body.pageSize 例外:部分 Schema 将字段名直接定义为 pageSize(无 snake_case、无 alias),须以源码为准

列表/详情等读接口的 _row_to_dict(或等价转换)SHALL 将 ORM 列名转为 camelCase 键名再返回前端。

参考实现:app/api/admin.py 中 TenantCreateBody;CRM:app/schemas/crm/customer.py、app/schemas/crm/product.py;路由:app/api/crm.py(客户列表/公海池);运输计划:app/schemas/pl/**(openspec/changes/transport-plan-management/specs/pl-api-json/spec.md);地磅:app/schemas/db/weighing.py。

FastAPI 路由 Query 参数若 Python 参数名为 page_size,SHALL 声明 Query(..., alias="pageSize"),与前端列表分页一致。

Scenario: 运输计划承运商新增 422 回归

  • WHEN 前端 POST /api/transport/carrier 发送 { "carrierName": "测试" }
  • THEN CarrierCreate SHALL 含 carrier_name = Field(..., alias="carrierName") 且校验通过
  • AND SHALL NOT 因缺少 alias 返回 422 carrier_name Field required

Scenario: 运输计划列表 pageSize Query

  • WHEN 前端 GET /api/transport/plan/?page=2&pageSize=50
  • THEN 路由 SHALL 使用 page_size: int = Query(..., alias="pageSize") 解析为 50
  • AND SHALL NOT 忽略 pageSize 而始终使用默认 20

Scenario: 前端提交新增分类

  • WHEN 前端调用 POST /web/crm/product-categories/add,请求体为 { "categoryName": "有机原料", "categoryCode": "yjyl", "orderNum": 0 }
  • THEN 后端 Pydantic 模型 SHALL 校验通过,且 model_dump() 得到 { "category_name": "有机原料", "category_code": "yjyl", "order_num": 0, ... } 供 CRUD 使用

Scenario: 仅定义 snake_case 字段名导致 422

  • WHEN 请求体 JSON 使用 camelCase,但 Schema 仅声明 category_name 且无 alias / populate_by_name
  • THEN 属于规范违反;修复方式为补全 alias,SHALL NOT 将 web-admin 改为发送 snake_case 作为长期方案

Scenario: 响应列表字段

  • WHEN 接口返回单条或列表业务记录
  • THEN data 内字典键 SHALL 为 camelCase(如 parentId、createTime),与前端 TypeScript 字段一致

Scenario: 存量 admin 内联 Schema

  • WHEN app/api/admin.py 等既有模块已使用 populate_by_name + alias
  • THEN 新增模块 SHALL 复用同一模式;SHALL NOT 为对齐规范而批量重构存量文件,除非该文件正在因其他需求修改

Scenario: 路由层按 alias 名访问请求体导致 500

  • GIVEN CustomerListBody 定义 create_time_range: Optional[List[str]] = Field(..., alias="createTimeRange")
  • WHEN 路由中编写 if body.createTimeRange: 或 view_mode=body.viewMode
  • THEN 运行时将抛出 AttributeError(如 'CustomerListBody' object has no attribute 'createTimeRange'),HTTP 常表现为 code: 500;修复 SHALL 改为 body.create_time_range、body.view_mode 等 Python 属性名,或改为 dump = body.model_dump(exclude_unset=True) 后从 dump 取值

Scenario: CRM 公海池列表 viewMode

  • WHEN 前端 POST /web/crm/customer/list 请求体为 { "page": 1, "pageSize": 10, "viewMode": "pool" }
  • THEN 路由 SHALL 使用 body.view_mode(值为 pool)传入 CRUD,SHALL NOT 使用 body.viewMode;page / pageSize 按 Schema 定义使用 body.page、body.pageSize

Scenario: 代码评审 — 路由读 body 字段

  • WHEN PR 在 app/api/** 中新增或修改对 Pydantic body 的字段访问
  • THEN 审查者 SHALL 对照对应 app/schemas/** 中的属性名(非 alias 字符串);若出现 body.[a-z]+[A-Z] 形式且 Schema 为 snake_case + alias,SHALL 要求修正

Requirement: 统一响应格式

所有业务接口 SHALL 返回统一的 JSON 响应结构:

{
  "code": 200,
  "message": "成功",
  "data": <任意类型或null>,
  "timestamp": 1748572800000
}

Scenario: 成功响应

  • WHEN 接口执行成功
  • THEN 响应体 SHALL 为 { "code": 200, "message": "成功", "data": <业务数据>, "timestamp": <毫秒时间戳> }

Scenario: 失败响应

  • WHEN 接口执行失败(校验不通过、业务异常等)
  • THEN 响应体 SHALL 为 { "code": <错误码>, "message": "<可读错误描述>", "data": null, "timestamp": <毫秒时间戳> }

Scenario: 分页列表响应

  • WHEN 接口返回分页数据
  • THEN data 字段 SHALL 为 { "list": [...], "total": 100, "page": 1, "pageSize": 10, "totalPages": 10 }

Scenario: 单条记录响应

  • WHEN 接口返回详情或新增/更新后的记录
  • THEN data 字段 SHALL 为该记录的字典对象(通过 _row_to_dict 转换)

Scenario: 无数据响应

  • WHEN 接口执行删除等无需返回数据的操作
  • THEN data 字段 SHALL 为 null,message SHALL 为操作结果描述(如 "删除成功")

Requirement: 统一返回码定义

所有业务接口 SHALL 使用以下返回码体系:

返回码 含义 使用场景
200 成功 所有成功响应
400 请求参数错误 必填字段缺失、格式不合法、业务校验失败
401 未认证 Token 缺失、Token 过期、Token 无效
403 无权限 当前用户无权访问该资源或操作
404 资源不存在 根据 ID 查询的记录不存在(或已删除)
409 数据冲突 唯一性校验失败(如编号重复)
500 服务器内部错误 未捕获的异常、数据库错误

Scenario: 参数校验失败

  • WHEN 请求体缺少必填字段或字段格式不合法
  • THEN SHALL 返回 { "code": 400, "message": "<具体校验失败原因>", "data": null }

Scenario: 唯一性冲突

  • WHEN 新增或更新时违反唯一性约束(如产品编号重复)
  • THEN SHALL 返回 { "code": 409, "message": "产品编号已存在", "data": null }

Scenario: 记录不存在

  • WHEN 按 ID 查询或操作的记录不存在或已删除
  • THEN SHALL 返回 { "code": 404, "message": "记录不存在", "data": null }

Scenario: 未登录

  • WHEN 请求未携带有效 Token
  • THEN SHALL 返回 { "code": 401, "message": "未登录或登录已过期", "data": null }

Scenario: Web 登录账号或密码错误

  • WHEN POST /web/auth/login 校验失败(用户不存在、密码错误等)
  • THEN SHALL 返回 { "code": 400, "message": "用户名或密码错误", "data": null },SHALL NOT 使用 401(避免前端将业务码误判为 Token 失效并触发登出)

Scenario: Web 登录查用户

  • WHEN Web 登录按 user_no 解析 sys_user 且未传租户
  • THEN SHALL 优先使用 DEFAULT_TENANT_NO(shanxia2.0);未命中时可按 user_no 回退查询以兼容未完成租户迁移的库,并建议尽快执行 docs/sql/20260530_migrate_tenant_no_to_shanxia2.0.sql

Scenario: 无权限

  • WHEN 当前用户角色无权执行该操作
  • THEN SHALL 返回 { "code": 403, "message": "无操作权限", "data": null }

Scenario: 服务器异常

  • WHEN 发生未预期的异常(数据库错误、代码 bug 等)
  • THEN SHALL 返回 { "code": 500, "message": "服务器内部错误", "data": null },SHALL NOT 将堆栈信息暴露给前端

Requirement: URL 路径设计规范

统一 POST 后,URL 路径 SHALL 通过路径语义区分操作类型:

操作 URL 模式 示例
列表查询 /web/<module>/<resource>/list /web/crm/products/list
详情查询 /web/<module>/<resource>/detail /web/crm/products/detail
新增 /web/<module>/<resource>/add /web/crm/products/add
更新 /web/<module>/<resource>/update /web/crm/products/update
删除 /web/<module>/<resource>/delete /web/crm/products/delete
下拉选项 /web/<module>/<resource>/options /web/crm/products/options
导入 /web/<module>/<resource>/import /web/crm/products/import
导出 /web/<module>/<resource>/export /web/crm/products/export
下载模板 /web/<module>/<resource>/import-template /web/crm/products/import-template

Scenario: 新增资源路由

  • WHEN 为 CRM 产品模块定义路由
  • THEN 路由 SHALL 为 @router.post("/products/list"), @router.post("/products/add") 等,SHALL NOT 使用 RESTful 风格的 @router.get("/products") 或 @router.put("/products/{id}")

Scenario: 避免路径参数

  • WHEN 操作需要指定某条记录
  • THEN 记录标识 SHALL 通过请求体 { "id": "xxx" } 传入,SHALL NOT 通过 URL 路径参数 /products/{id} 传入

Requirement: 租户隔离

所有面向业务数据的查询接口 SHALL 仅返回 tenant_no 等于当前登录用户租户编号的数据;所有写入或更新类接口 SHALL 将目标业务行的 tenant_no 设置为当前登录用户的 tenant_no(与《shanxia2.0需求 V0.3》§3 一致)。

本仓库门户默认租户编号 SHALL 为 shanxia2.0(配置项 DEFAULT_TENANT_NO、DDL/ORM 默认值见 database-conventions 与 coding-conventions)。无 JWT 的公共入口(如 Web 登录按 user_no 查 sys_user)SHALL 使用该默认租户,SHALL NOT 使用历史编码 zkxw。

Scenario: 列表查询

  • WHEN 调用任意业务数据列表或详情接口
  • THEN 后端 SHALL 在查询条件中强制包含当前用户的 tenant_no

Scenario: 新建业务主表

  • WHEN 创建新的业务主表记录
  • THEN 持久化的 tenant_no SHALL 等于当前登录用户租户,且不得信任客户端单独提交的租户覆盖服务端策略(除非经安全评审的多租户管理接口)

Requirement: 数据权限过滤

除超级管理员等需求明确豁免的场景外,所有业务查询接口 SHALL 叠加当前登录用户全部角色合并后的数据权限范围进行过滤(与需求 §2.1.5「多角色取权限之和」一致)。

Scenario: 用户绑定多角色

  • WHEN 用户拥有角色 A 与角色 B
  • THEN 可访问数据范围 SHALL 为两角色数据权限的并集(菜单与数据权限均适用并集语义)

Requirement: 列表默认排序

在无单独产品排序要求的前提下,列表类接口返回的数据集合 SHALL 按 create_time 降序排列(与需求 §3 一致)。

Scenario: 默认分页列表

  • WHEN 接口未声明自定义排序参数且业务无例外说明
  • THEN 默认排序 SHALL 为 create_time DESC

Requirement: API 前缀与工程约定

对外 HTTP API SHALL 使用统一前缀(当前仓库为 /api 及版本/子路径,见 AGENTS.md);新增路由 SHALL 注册到既有路由聚合机制,避免绕过中间件(鉴权、加解密、审计等)。

Scenario: 新增 admin 路由

  • WHEN 在 app/api/ 增加新的路由模块
  • THEN 该模块 SHALL 在 app/api/__init__.py 与 app/main.py 中完成注册,并符合现有认证与租户上下文注入模式

Requirement: 小程序拍照凭证上传接口

miniprogram 拍照凭证(仅图片,见 ui-conventions「小程序拍照凭证上传(基线)」)相关接口 SHALL 遵循下列约定;权威行为对齐叁强鲜配 POST /api/upload/image + 业务接口携带 imageUrls。

上传接口

  1. 方法与编码:POST + multipart/form-data(文件上传例外,见上文「文件上传下载例外」)。
  2. 表单字段:文件字段名 SHALL 为 file;业务目录字段 SHALL 为 folder(Form 字符串,如 delivery-evidence、avatars)。
  3. 鉴权:SHALL 要求登录态(Bearer JWT 或与小程序一致的现有鉴权);未登录 SHALL 返回 401。
  4. 成功响应:统一信封下 data SHALL 至少包含 url(对象存储可访问地址);MAY 含 filename、contentType、size。前端以 data.url 为唯一回填依据。
  5. 类型与大小:SHALL 仅接受图片 MIME(至少覆盖 image/jpeg、image/png、image/webp;建议兼容 image/heic/image/heif);单文件大小上限建议 10MB(与叁强一致,业务另有约定时在接口文档写明)。
  6. 路径落点:山峡落地时 MAY 复用/对齐 POST /api/upload/image,或提供 POST /api/mini/.../upload/image;无论路径如何,小程序 uni.uploadFile 的 url 与后端注册路径 SHALL 一致,并完成路由注册(app/api/__init__.py、app/main.py)。

加解密排除(强制)

multipart 上传路径 SHALL 列入服务端 API_ENCRYPT_EXCLUDE_PATHS(及前端若启用 API 加密时的排除清单)。微信小程序端若因运行时不保证 WebCrypto 而关闭客户端加密(叁强 sqxp-wx/utils/request.js 在 MP-WEIXIN 下 API_ENCRYPT_ENABLED = false),上传仍 SHALL 依赖 HTTPS + Bearer,SHALL NOT 对文件字节做 JSON AES 包装。

业务关联

  1. 业务确认类接口(如取货、送达、备货完成)请求体 SHALL 使用 JSON,图片以 URL 数组 传递(建议字段名 imageUrls: string[])。
  2. 服务端收到 URL 列表后 SHALL 校验归属与数量上限,再更新业务状态;需要附件台账时 SHALL 写入附件表(或等价持久化),file_url 存完整 URL;多 URL 落单字段时 SHALL 使用英文逗号分隔(见 architecture-conventions「附件与对象存储」)。
  3. 业务接口 SHALL NOT 再接收同一确认动作中的 multipart 文件流。

Scenario: 小程序上传一张凭证图

  • WHEN 已登录小程序用户对图片上传接口提交 file + folder
  • THEN 响应 code SHALL 为成功码且 data.url 非空,对象存储中可按该 URL 访问图片

Scenario: 业务提交携带 imageUrls

  • WHEN 用户确认取货/送达等动作且本地已有已上传 URL
  • THEN 业务 POST 体 SHALL 含 imageUrls 数组,服务端 SHALL 据此落库/关联,SHALL NOT 要求客户端再次上传二进制

Scenario: 上传路径未排除加密

  • WHEN 上传路径未加入 API_ENCRYPT_EXCLUDE_PATHS 且全局 API 加密开启
  • THEN 视为规范违规;实现与评审 SHALL 在合入前补齐排除项

Requirement: 细胞存储样本跨表关联键

细胞存储场景下,将协议样本行(biz_protocol_sample)与下游业务(样本接收 biz_sample_reception、质检工单 biz_qc_work_order 等)进行业务匹配时,关联键 SHALL 仅为 tenant_no + sample_code(租户内样本编码唯一且不变;接收/工单表须持久化 sample_code 快照)。不得使用 collection_protocol_id 或 protocol_sample_id 作为查询、列表排除、详情回显的关联依据(协议单可换号/转科研、编辑保存会重建样本行 id,但样本编码不变)。

Scenario: 协议单查看弹窗展示样本接收

  • WHEN 用户打开采集协议单详情,且租户内某 sample_code 已登记接收
  • THEN 详情接口 SHALL 仅按样本编码返回对应 sampleReception(及同规则的 qcInfo),与当前协议单 ID、样本行 id 是否变更无关

Scenario: 待接收列表排除已接收样本

  • WHEN 租户内某 sample_code 已存在有效接收记录(del_flag=0)
  • THEN 该编码的协议样本 SHALL NOT 出现在待接收列表

Scenario: 登记接收防重复

  • WHEN 用户对某协议样本提交接收登记
  • THEN 服务端 SHALL 按 tenant_no + sample_code 校验是否已接收,已存在则拒绝重复登记

Requirement: CRM Excel 导出字典 label 转换

CRM 模块(及后续从山峡 CRM 迁移的业务模块)列表 Excel 导出 SHALL 在服务端将字典 code(dict_value) 转为 label(dict_label) 后再写入单元格,行为对齐 Java RuoYi ExcelDictConvert + @ExcelDictFormat(dictType=...)。

实现 SHALL 遵循以下约定:

  1. 列定义集中管理:各模块导出列 SHALL 声明于 backend/app/utils/crm_export_columns.py,使用 ExportColumn(title, field, dict_type=...);dict_type 须与原 CRM 对应 *Vo.java 中 @ExcelDictFormat 一致。
  2. 统一导出入口:路由 SHALL 调用 backend/app/utils/excel_export.py 的 export_rows_to_xlsx_response,SHALL NOT 在 app/api/crm.py 等路由内重复 openpyxl 表头/行循环。
  3. 字典预加载:导出前 SHALL 一次查询 sys_dict_data 构建 value→label 映射(load_dict_label_maps),SHALL NOT 按行 N+1 查字典。
  4. 租户隔离:字典查询 SHALL 过滤当前用户 tenant_no(与列表接口一致)。
  5. 响应类型:成功时 SHALL 返回 application/vnd.openxmlformats-officedocument.spreadsheetml.sheet 二进制流;失败时 MAY 返回 JSON { code, message }(前端 blob 拦截器须能识别,见 coding-conventions)。
  6. 依赖:运行环境 SHALL 安装 openpyxl(backend/requirements.txt)。

Scenario: 线索列表导出

  • WHEN 用户调用 POST /web/crm/clue/export 且线索 source 存为 website
  • THEN Excel「线索来源」列 SHALL 展示 sys_dict_data 中 dict_type=crm_clue_source 且 dict_value=website 的 dict_label,SHALL NOT 直接输出 website

Scenario: 新增 CRM 模块导出

  • WHEN 移植新 CRM 模块并增加 /export 路由
  • THEN 开发者 SHALL 先在 crm_export_columns.py 增加 XXX_EXPORT_COLUMNS 并对照 Java CrmXxxVo,路由 SHALL 仅调用 export_rows_to_xlsx_response

Scenario: 非字典列与格式化列

  • WHEN 导出列无 @ExcelDictFormat(如姓名、手机号)或为布尔语义(如是否主联系人)
  • THEN MAY 使用无 dict_type 的 ExportColumn,或使用 formatter(参考联系人导出 _yes_no)

常见错误案例(API / Pydantic)

以下案例供迁移与 Code Review 对照;编号 EC-API-* 与 ui-conventions 中 EC-UI-* 并列,不替代上文 Requirement 的 SHALL。

错误案例 EC-API-001:路由层用 JSON alias 访问 Pydantic 请求体

项 说明
现象 接口 HTTP 200 但 data 为 null,message 如 'CustomerListBody' object has no attribute 'createTimeRange';前端表现为 HttpError / 公海池或客户列表白屏
根因 Schema 使用 create_time_range = Field(..., alias="createTimeRange"),路由误写 body.createTimeRange。Pydantic v2 的 alias 只用于解析入参,Python 属性名仍为 create_time_range
错误写法 body.createTimeRange、body.viewMode、body.customerName、body.customerIds、body.poolReason 等(与 Schema 属性名不一致时)
正确写法 body.create_time_range、body.view_mode、body.customer_name、body.customer_ids、body.pool_reason;或 kwargs = body.model_dump(exclude_unset=True) 再传给 CRUD
注意 少数字段在 Schema 中直接命名为 pageSize(无 alias),此时 SHALL 使用 body.pageSize,不可臆测为 body.page_size
参考 app/schemas/crm/customer.py(CustomerListBody);修复记录:app/api/crm.py 客户列表/导出/公海池相关路由(2026-05)

Scenario: EC-API-001 复现与验证

  • WHEN 修改 CRM 客户列表路由后做冒烟
  • THEN POST /web/crm/customer/list 且 {"page":1,"pageSize":10,"viewMode":"pool"} SHALL 返回 code: 200 且 data.list 为数组,SHALL NOT 再出现 createTimeRange 相关 AttributeError