--- schema: "hl-changelog/v2" ticket: "7290" title: "供应商合同十项必填并移除登记原因" consumer: "admin" author: "lc(GIT)" change_type: "修改接口" backend_status: "deployed" gateway_status: "verified" frontend_status: "verified" frontend_owner: "mmg" frontend_ref: "87cf8ba8" target_release: "" verified_at: "2026-09-07" status_note: "前端已交付并验证(commit 87cf8ba8):SupplierContractEditModal 十项转必填(文本 trim 非空/枚举日期判空/autoRenew 按 ===true-false 不把 false 当空,endDate 合并必填+不早于开始日期单条 rule),contractNo label 补合同编号→合同编号;删登记/修改原因输入+changeReasonRule+buildBody 不再组装 changeReason;必填恒携带、可选(contractType/amount/pricingMode/settleCycle/remark)保持新增省略/编辑整份替换,expectedUpdateTime 围栏与 395014/395051 提示不变;api add/update JSDoc 改新口径,delete 与删除审计流程零改动。目标 spec 24/24 绿+supplier 域 183/183 绿+checkpoint 全绿(Vitest 全量+生产构建)。" updated_at: "2026-09-07" base: "dev-v3" --- # 供应商合同:十项必填并移除登记原因 ## 一、关键变化 - 新增、编辑合同均要求提交:合同名称、合同编号、合同状态、业务线、关联主合同、签署日期、自动续约、有效期开始、有效期结束、合同附件。 - 页面字段 `contractNo` 的文案改为“合同编号”,不再显示“补合同编号”。 - 新增、编辑不再要求登记原因,请移除原因输入框并停止发送 `changeReason`;旧调用方多传该字段仍兼容忽略。 - 新增、编辑成功后不再产生供应商变更审计。合同删除接口及其必填原因、删除审计均不变。 本文覆盖此前 #6654、#6842 中“合同业务字段可空、新增或编辑原因必填”的旧口径。 ## 二、变更接口清单 | # | 接口 | 方法 | 路径 | 变更类型 | 说明 | |---|---|---|---|---|---| | 1 | 新增供应商合同 | POST | `/admin/supplier/items/{supplierId}/contracts/add` | 请求校验与行为修改 | 十项必填,移除登记原因与新增审计 | | 2 | 编辑供应商合同 | PUT | `/admin/supplier/items/{supplierId}/contracts/{contractId}/update` | 请求校验与行为修改 | 十项必填,移除登记原因与编辑审计 | ## 三、接口详情 ### 1. 新增供应商合同 `POST /admin/supplier/items/{supplierId}/contracts/add` **VO**: `SupplierContractCreateReqVO / SupplierContractRespVO` #### 使用场景 在已有供应商下新增一份完整合同,不再填写登记原因。 #### 入参 | 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | |---|---|---|---|---|---| | `supplierId` | Path | String | 是 | 正整数 ID | 所属供应商 | | `contractName` | Body | String | 是 | 非空,最长 500 | 合同名称 | | `contractNo` | Body | String | 是 | 非空,最长 100 | 合同编号;页面不得再写“补合同编号” | | `status` | Body | String | 是 | `DRAFT` / `ACTIVE` / `SIGNED` / `EXPIRED` | 合同状态 | | `businessLine` | Body | String | 是 | 非空,最长 100 | 业务线 | | `relatedMainContract` | Body | String | 是 | 非空,最长 100 | 关联主合同引用 | | `signDate` | Body | String | 是 | `yyyy-MM-dd` | 签署日期 | | `autoRenew` | Body | Boolean | 是 | `true` / `false` | 自动续约;`false` 是有效值 | | `startDate` | Body | String | 是 | `yyyy-MM-dd` | 有效期开始 | | `endDate` | Body | String | 是 | 不早于 `startDate` | 有效期结束 | | `scanFileUrl` | Body | String | 是 | 非空,最长 1000 | 合同附件永久地址 | | `contractType` | Body | String/null | 否 | `FRAME` / `SINGLE_TRIP` / `PURCHASE` | 合同类型 | | `amount` | Body | String/null | 否 | 非负,最多 10 位整数和 2 位小数 | 合同金额 | | `pricingMode` | Body | String/null | 否 | 最长 100 | 计价方式 | | `settleCycle` | Body | String/null | 否 | 最长 32 | 结算周期 | | `remark` | Body | String/null | 否 | 最长 500 | 备注 | `changeReason` 已从请求模型移除,不要再由新增表单组装。 #### 出参 | 字段 | 类型 | 说明 | |---|---|---| | `code` / `message` / `success` | Integer / String / Boolean | 成功为 `200` / `成功` / `true` | | `data.contractId` | String | 合同 ID | | `data.contractName` / `contractNo` | String | 合同名称 / 合同编号 | | `data.status` / `businessLine` | String | 合同状态 / 业务线 | | `data.relatedMainContract` | String | 关联主合同 | | `data.signDate` / `startDate` / `endDate` | String | `yyyy-MM-dd` | | `data.autoRenew` | Boolean | 自动续约 | | `data.scanFileUrl` | String | 合同附件地址 | | `data.contractType` / `amount` / `pricingMode` / `settleCycle` / `remark` | 对应类型/null | 可选字段,响应结构不变 | | `data.updateTime` | String | 合同版本,`yyyy-MM-dd HH:mm:ss` | #### 请求示例 ```json { "contractName": "年度服务合同", "contractNo": "HT-2026-001", "status": "SIGNED", "businessLine": "旅行服务", "relatedMainContract": "MAIN-2026-001", "signDate": "2026-09-07", "autoRenew": false, "startDate": "2026-09-07", "endDate": "2027-09-06", "scanFileUrl": "https://files.example.com/contracts/demo.pdf", "contractType": "FRAME", "amount": "1200.50", "pricingMode": "按团结算", "settleCycle": "MONTHLY", "remark": null } ``` #### 响应示例 ```json { "code": 200, "message": "成功", "data": { "contractId": "2097000000000000001", "contractName": "年度服务合同", "contractNo": "HT-2026-001", "contractType": "FRAME", "signDate": "2026-09-07", "startDate": "2026-09-07", "endDate": "2027-09-06", "businessLine": "旅行服务", "relatedMainContract": "MAIN-2026-001", "autoRenew": false, "amount": "1200.50", "pricingMode": "按团结算", "settleCycle": "MONTHLY", "status": "SIGNED", "scanFileUrl": "https://files.example.com/contracts/demo.pdf", "remark": null, "updateTime": "2026-09-07 20:00:00" }, "success": true } ``` #### 空数据 / 降级响应 没有空成功数据。十项必填字段缺少、为 `null` 或文本为空白时均失败,`data` 为 `null`。 #### 错误响应 ```json {"code":400,"message":"合同编号不能为空","data":null,"success":false} ``` #### 业务边界 - 前端提交前应同时校验十项必填;业务错误可能仍使用 HTTP 200,必须判断响应体 `code` 和 `success`。 - `autoRenew=false` 不得被空值过滤器删除;`startDate` 不得晚于 `endDate`。 - 权限、供应商状态、审批冻结、幂等和错误码均保持原规则。 ### 2. 编辑供应商合同 `PUT /admin/supplier/items/{supplierId}/contracts/{contractId}/update` **VO**: `SupplierContractUpdateReqVO / SupplierContractRespVO` #### 使用场景 完整编辑一份已有合同,不再填写编辑原因。 #### 入参 | 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | |---|---|---|---|---|---| | `supplierId` | Path | String | 是 | 正整数 ID | 所属供应商 | | `contractId` | Path | String | 是 | 正整数 ID | 目标合同 | | `contractName` | Body | String | 是 | 非空,最长 500 | 合同名称 | | `contractNo` | Body | String | 是 | 非空,最长 100 | 合同编号 | | `status` | Body | String | 是 | `DRAFT` / `ACTIVE` / `SIGNED` / `EXPIRED` | 合同状态 | | `businessLine` | Body | String | 是 | 非空,最长 100 | 业务线 | | `relatedMainContract` | Body | String | 是 | 非空,最长 100 | 关联主合同引用 | | `signDate` | Body | String | 是 | `yyyy-MM-dd` | 签署日期 | | `autoRenew` | Body | Boolean | 是 | `true` / `false` | 自动续约 | | `startDate` | Body | String | 是 | `yyyy-MM-dd` | 有效期开始 | | `endDate` | Body | String | 是 | 不早于 `startDate` | 有效期结束 | | `scanFileUrl` | Body | String | 是 | 非空,最长 1000 | 合同附件永久地址 | | `expectedUpdateTime` | Body | String/null | 否 | `yyyy-MM-dd HH:mm:ss` | 传入时必须匹配合同当前版本 | | `contractType` / `amount` / `pricingMode` / `settleCycle` / `remark` | Body | 对应类型/null | 否 | 与新增接口一致 | 完整替换时原样回传需保留的可选值 | `changeReason` 已从请求模型移除,不要再由编辑表单组装。 #### 出参 | 字段 | 类型 | 说明 | |---|---|---| | `code` / `message` / `success` | Integer / String / Boolean | 成功为 `200` / `成功` / `true` | | `data.contractId` | String | 原合同 ID | | `data.contractName` / `contractNo` / `status` / `businessLine` | String | 编辑后的必填文本字段 | | `data.relatedMainContract` / `scanFileUrl` | String | 关联主合同 / 合同附件地址 | | `data.signDate` / `startDate` / `endDate` | String | 编辑后的日期 | | `data.autoRenew` | Boolean | 编辑后的自动续约值 | | `data.contractType` / `amount` / `pricingMode` / `settleCycle` / `remark` | 对应类型/null | 编辑后的可选字段 | | `data.updateTime` | String | 新合同版本,供下一次编辑或删除使用 | #### 请求示例 ```json { "contractName": "年度服务合同(续签)", "contractNo": "HT-2026-001-A", "status": "ACTIVE", "businessLine": "旅行服务", "relatedMainContract": "MAIN-2026-001", "signDate": "2026-09-07", "autoRenew": true, "startDate": "2026-09-07", "endDate": "2028-09-06", "scanFileUrl": "https://files.example.com/contracts/demo-a.pdf", "contractType": "FRAME", "amount": "1500.00", "pricingMode": "按团结算", "settleCycle": "MONTHLY", "remark": "续签", "expectedUpdateTime": "2026-09-07 20:00:00" } ``` #### 响应示例 ```json { "code": 200, "message": "成功", "data": { "contractId": "2097000000000000001", "contractName": "年度服务合同(续签)", "contractNo": "HT-2026-001-A", "contractType": "FRAME", "signDate": "2026-09-07", "startDate": "2026-09-07", "endDate": "2028-09-06", "businessLine": "旅行服务", "relatedMainContract": "MAIN-2026-001", "autoRenew": true, "amount": "1500.00", "pricingMode": "按团结算", "settleCycle": "MONTHLY", "status": "ACTIVE", "scanFileUrl": "https://files.example.com/contracts/demo-a.pdf", "remark": "续签", "updateTime": "2026-09-07 20:00:01" }, "success": true } ``` #### 空数据 / 降级响应 没有空成功数据。历史合同仍可读取;编辑历史合同前须先补齐十项必填字段。 #### 错误响应 ```json {"code":400,"message":"合同附件不能为空","data":null,"success":false} ``` #### 业务边界 - 编辑仍是完整替换;十项必填字段必须全部提交,可选字段需按是否保留正确回传。 - `expectedUpdateTime` 可省略,提交时应使用详情返回的合同版本;版本过期返回 `395014`。 - 删除接口不在本次变化内,删除仍必须提交 `changeReason` 并产生删除审计。 ## 四、契约约束与正确调用方式 1. 新增、编辑表单将十项字段全部标为必填,尤其不得把 `autoRenew=false` 当空值。 2. 将 `contractNo` 的页面标签统一改为“合同编号”。 3. 移除新增、编辑的登记原因输入与校验,请求体不再组装 `changeReason`。 4. 编辑时从详情的 `contracts[]` 完整回填并提交;隐藏的可选字段继续原样保留。 5. 删除合同继续沿用原因弹框和原 DELETE 请求体,不要一并移除。 ## 五、数据库行为 新增、编辑成功后保存完整合同值,但不新增供应商变更审计;校验失败时合同与审计均不变化。删除仍为软删除并保留原删除审计。本次无表结构或历史数据迁移。 ## 六、边界行为 - 缺少任一必填字段:`code=400`、`success=false`,不会写入或修改合同。 - 未登录:`code=401`;无写权限:`395002`;合同不存在或归属错误:`395051`;日期倒置:`395054`;无实际变化:`395057`。 - 历史合同可能含空字段,读取不受影响;再次编辑时须补齐十项。 - 旧请求额外携带 `changeReason` 不会失败,但后端不保存该值,也不产生新增、编辑审计。 ## 六.6、修改前后对比 | 字段 / 行为 | 修改前 | 修改后 | |---|---|---| | 十项业务字段 | 均可省略或清空 | 新增、编辑全部必填 | | `contractNo` 页面文案 | 补合同编号 | 合同编号 | | 新增、编辑 `changeReason` | 必填 | 不需要;旧请求多传兼容忽略 | | 新增、编辑供应商变更审计 | 产生审计 | 不产生审计 | | 删除原因与审计 | 原因必填并产生审计 | 不变 | ## 六.7、影响评估 - **是否破坏向后兼容**:对缺少十项中任一字段的旧新增、编辑请求是收紧变化;旧请求多传 `changeReason` 仍兼容。 - **前端是否必须同步上线**:是。必须调整必填、文案和新增/编辑原因交互。 - **前端 workaround 清理点**:删除新增、编辑原因弹框及 `changeReason` 组装;保留删除原因流程。 ## 七、不影响范围 - 仅影响管理后台供应商合同新增、编辑。 - 不修改合同删除、供应商主体、收款账户、小程序接口、响应字段、数据库结构及历史合同读取。 ## 八、测试环境已验证 - POST 与 PUT 各对十项字段逐项省略,共 20 个请求均返回 `code=400`,失败路径零写入。 - 不带 `changeReason` 的新增、编辑均成功;`autoRenew=false` 正常保存,旧请求多传 `changeReason` 仍兼容。 - 新增、编辑对应的供应商变更审计均为 0;临时合同已删除,原删除审计保持存在。 **当前状态:后端已部署并验证;待前端处理。** ## 十、相关文档 - [Issue #7290](https://git.1814.love:8443/wx/HL/issues/7290) - [PR #7298](https://git.1814.love:8443/wx/HL/pulls/7298) - [被本文覆盖的合同旧口径 #6842](https://git.1814.love:8443/wx/HL/issues/6842) ## 关联 / 联系人 ### 链接 - **Issue**: [#7290](https://git.1814.love:8443/wx/HL/issues/7290) - **PR**: [#7298](https://git.1814.love:8443/wx/HL/pulls/7298) - **Merge commit**: [29ee134f330ca9524d6e74090a80aea3594b334e](https://git.1814.love:8443/wx/HL/commit/29ee134f330ca9524d6e74090a80aea3594b334e) ### 联系人 - **后端负责人**: @lc