26 KiB
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>": "<值>"
}
idSHALL NOT 出现在新增请求体中(由后端自动生成 UUID)tenant_noSHALL 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 同时满足:
model_config = ConfigDict(populate_by_name=True)- 多词字段定义 Python 名为 snake_case,并通过
Field(..., alias="camelCase名")声明 JSON 别名 - 路由内调用 CRUD 时优先使用
body.model_dump(exclude_unset=True)(默认按字段名输出 snake_case),SHALL NOT 要求前端改为 snake_case - 路由层读取字段时 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
CarrierCreateSHALL 含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/**中新增或修改对 Pydanticbody的字段访问 - 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,messageSHALL 为操作结果描述(如 "删除成功")
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_noSHALL 等于当前登录用户租户,且不得信任客户端单独提交的租户覆盖服务端策略(除非经安全评审的多租户管理接口)
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。
上传接口
- 方法与编码:
POST+multipart/form-data(文件上传例外,见上文「文件上传下载例外」)。 - 表单字段:文件字段名 SHALL 为
file;业务目录字段 SHALL 为folder(Form字符串,如delivery-evidence、avatars)。 - 鉴权:SHALL 要求登录态(Bearer JWT 或与小程序一致的现有鉴权);未登录 SHALL 返回 401。
- 成功响应:统一信封下
dataSHALL 至少包含url(对象存储可访问地址);MAY 含filename、contentType、size。前端以data.url为唯一回填依据。 - 类型与大小:SHALL 仅接受图片 MIME(至少覆盖
image/jpeg、image/png、image/webp;建议兼容image/heic/image/heif);单文件大小上限建议 10MB(与叁强一致,业务另有约定时在接口文档写明)。 - 路径落点:山峡落地时 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 包装。
业务关联
- 业务确认类接口(如取货、送达、备货完成)请求体 SHALL 使用 JSON,图片以 URL 数组 传递(建议字段名
imageUrls: string[])。 - 服务端收到 URL 列表后 SHALL 校验归属与数量上限,再更新业务状态;需要附件台账时 SHALL 写入附件表(或等价持久化),
file_url存完整 URL;多 URL 落单字段时 SHALL 使用英文逗号分隔(见architecture-conventions「附件与对象存储」)。 - 业务接口 SHALL NOT 再接收同一确认动作中的 multipart 文件流。
Scenario: 小程序上传一张凭证图
- WHEN 已登录小程序用户对图片上传接口提交
file+folder - THEN 响应
codeSHALL 为成功码且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 遵循以下约定:
- 列定义集中管理:各模块导出列 SHALL 声明于
backend/app/utils/crm_export_columns.py,使用ExportColumn(title, field, dict_type=...);dict_type须与原 CRM 对应*Vo.java中@ExcelDictFormat一致。 - 统一导出入口:路由 SHALL 调用
backend/app/utils/excel_export.py的export_rows_to_xlsx_response,SHALL NOT 在app/api/crm.py等路由内重复 openpyxl 表头/行循环。 - 字典预加载:导出前 SHALL 一次查询
sys_dict_data构建 value→label 映射(load_dict_label_maps),SHALL NOT 按行 N+1 查字典。 - 租户隔离:字典查询 SHALL 过滤当前用户
tenant_no(与列表接口一致)。 - 响应类型:成功时 SHALL 返回
application/vnd.openxmlformats-officedocument.spreadsheetml.sheet二进制流;失败时 MAY 返回 JSON{ code, message }(前端 blob 拦截器须能识别,见coding-conventions)。 - 依赖:运行环境 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并对照 JavaCrmXxxVo,路由 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