diff --git a/changelogs-v2/2026-08/29_6654_供应商合同字段可空与编辑信息入口-修改接口-管理后台.md b/changelogs-v2/2026-08/29_6654_供应商合同字段可空与编辑信息入口-修改接口-管理后台.md new file mode 100644 index 00000000..adb1b1b7 --- /dev/null +++ b/changelogs-v2/2026-08/29_6654_供应商合同字段可空与编辑信息入口-修改接口-管理后台.md @@ -0,0 +1,389 @@ +--- +schema: "hl-changelog/v2" +ticket: "6654" +title: "供应商合同字段可空与编辑信息入口" +consumer: "admin" +author: "lc(GIT)" +change_type: "修改接口" +backend_status: "deployed" +gateway_status: "verified" +frontend_status: "pending" +frontend_owner: "" +frontend_ref: "" +target_release: "" +verified_at: "" +status_note: "PR #6665 已合并 dev-v3;hl-resource-service 已通过 Deploy Panel 任务 3dde08b9 部署提交 65ac86e9312b22dd2fc7313049a693fdb0ccad59。真实 TEST Gateway 已验证合同业务字段全空登记、补全、清空、删除,changeReason 必填失败零写入,expectedUpdateTime 省略成功及过期值拒绝;测试草稿已清理。合同与结算 table 的页面顺序和消费映射待前端处理。" +updated_at: "2026-08-29" +base: "dev-v3" +--- + +# 供应商合同字段可空与编辑信息入口 + +供应商合同继续走独立登记接口,不并入供应商档案聚合。合同的 12 个业务字段现在全部可空,`changeReason` 继续必填;合同更新、删除的 `expectedUpdateTime` 改为可选,但一旦提供,过期版本仍会被拒绝。 + +管理端新建供应商时可完全不登记合同,取得 `supplierId` 后再补录;编辑页通过基本信息详情读取合同,通过既有账户接口读取和维护结算信息。页面按“资质证照 → 合同信息 → 结算信息”排列属于前端消费工作,当前状态为待前端处理。 + +## 二、变更接口清单 + +| # | 接口 | 方法 | 路径 | 变更类型 | 说明 | +|---:|---|---|---|---|---| +| 1 | 独立登记供应商合同 | POST | `/admin/supplier/items/{supplierId}/contracts/add` | 修改请求 | 12 个合同业务字段全部可空,`changeReason` 仍必填 | +| 2 | 独立更新供应商合同 | PUT | `/admin/supplier/items/{supplierId}/contracts/{contractId}/update` | 修改请求 | 合同业务字段与新增一致且可清空,`expectedUpdateTime` 可选 | +| 3 | 独立删除供应商合同 | DELETE | `/admin/supplier/items/{supplierId}/contracts/{contractId}/del` | 修改请求 | `expectedUpdateTime` 可选,`changeReason` 仍必填 | +| 4 | 查询供应商基本信息 | GET | `/admin/supplier/items/{supplierId}/basic-info/view` | 修改响应语义 | `contracts[]` 的全部业务字段允许返回 `null` | + +## 三、接口详情 + +### 1. 独立登记供应商合同 `POST /admin/supplier/items/{supplierId}/contracts/add` + +**VO**: `SupplierContractCreateReqVO / SupplierContractRespVO` + +#### 使用场景 + +供应商已经创建但合同资料尚未齐全时,可先登记一条纯合同记录,之后再补充。若暂时不需要合同记录,供应商新建请求直接省略废弃的 `contracts` 字段即可。 + +#### 入参 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|---|---|---|---:|---|---| +| `supplierId` | Path | String | 是 | 正整数 ID 字符串 | 目标供应商 | +| `contractName` | Body | String | 否 | 最长 500 字符 | 合同名称 | +| `contractNo` | Body | String | 否 | 最长 100 字符 | 合同编号 | +| `contractType` | Body | String | 否 | `FRAME` / `SINGLE_TRIP` / `PURCHASE` | 合同类型 | +| `signDate` | Body | String | 否 | `yyyy-MM-dd` | 签署日期 | +| `startDate` | Body | String | 否 | `yyyy-MM-dd` | 有效期开始;仅与同时提供的结束日期做区间校验 | +| `endDate` | Body | String | 否 | `yyyy-MM-dd` | 有效期结束;两端都提供时不得早于开始日期 | +| `amount` | Body | String | 否 | 非负,最多 10 位整数和 2 位小数 | 合同金额 | +| `pricingMode` | Body | String | 否 | 最长 100 字符 | 计价方式 | +| `settleCycle` | Body | String | 否 | 最长 32 字符 | 结算周期说明 | +| `status` | Body | String | 否 | `DRAFT` / `ACTIVE` / `EXPIRED` | 合同状态 | +| `scanFileUrl` | Body | String | 否 | 最长 1000 字符 | 扫描件永久地址 | +| `remark` | Body | String | 否 | 最长 500 字符 | 合同备注 | +| `changeReason` | Body | String | 是 | 去空白后非空,最长 500 字符 | 审计原因,不属于可空业务字段 | + +#### 出参 `Result` + +| 字段 | 类型 | 说明 | +|---|---|---| +| `data.contractId` | String | 新合同雪花 ID | +| `data.contractName` 等 12 个业务字段 | 对应类型/null | 未填写的字段返回 `null` | +| `data.updateTime` | String | 服务端合同版本,格式 `yyyy-MM-dd HH:mm:ss` | + +#### 请求示例 + +```json +{ + "changeReason": "合同资料暂缺,先登记记录" +} +``` + +#### 响应示例 + +```json +{ + "code": 200, + "message": "成功", + "success": true, + "data": { + "contractId": "2094000000000000001", + "contractName": null, + "contractNo": null, + "contractType": null, + "signDate": null, + "startDate": null, + "endDate": null, + "amount": null, + "pricingMode": null, + "settleCycle": null, + "status": null, + "scanFileUrl": null, + "remark": null, + "updateTime": "2026-08-29 16:10:00" + } +} +``` + +#### 空数据 / 降级响应 + +请求体除 `changeReason` 外可以不包含任何字段;成功后返回合同对象,业务字段全部为 `null`。不要把 `null` 自动替换为默认合同类型、状态、日期或金额。 + +#### 错误响应 + +缺少审计原因时失败且不新增合同: + +```json +{ "code": 400, "message": "变更原因不能为空", "success": false, "data": null } +``` + +#### 业务边界 + +- 要求可信管理员、`FINANCE`/`SUPER_ADMIN` 写角色和 `supplier:update` 平台权限。 +- 合同登记不进入供应商审批,不推进供应商主体 `updateTime`。 +- `POST /admin/supplier/items/add` 和供应商资料更新中的废弃 `contracts` 字段仍被忽略;前端必须先获得 `supplierId`,再按需调用本接口。 + +### 2. 独立更新供应商合同 `PUT /admin/supplier/items/{supplierId}/contracts/{contractId}/update` + +**VO**: `SupplierContractUpdateReqVO / SupplierContractRespVO` + +#### 使用场景 + +补齐、修改或清空一条已登记合同。该接口执行完整替换:未传或传 `null` 的业务字段会保存为 `null`,不是“保持原值”。 + +#### 入参 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|---|---|---|---:|---|---| +| `supplierId` | Path | String | 是 | 正整数 ID 字符串 | 目标供应商 | +| `contractId` | Path | String | 是 | 正整数 ID 字符串 | 目标合同 | +| 新增接口的 12 个业务字段 | Body | 对应类型 | 否 | 与新增一致 | 完整替换;省略即清空对应字段 | +| `changeReason` | Body | String | 是 | 去空白后非空,最长 500 字符 | 审计原因 | +| `expectedUpdateTime` | Body | String | 否 | `yyyy-MM-dd HH:mm:ss` | 提供时必须等于当前合同版本;省略时由服务端锁串行更新 | + +#### 出参 `Result` + +| 字段 | 类型 | 说明 | +|---|---|---| +| `data.contractId` | String | 合同雪花 ID | +| `data.contractName` 等 12 个业务字段 | 对应类型/null | 完整替换后的值,允许为 `null` | +| `data.updateTime` | String | 更新后的合同版本 | + +#### 请求示例 + +```json +{ + "contractName": "2026 年度框架合同", + "contractType": "FRAME", + "startDate": "2026-09-01", + "endDate": "2027-08-31", + "status": "ACTIVE", + "changeReason": "补齐已签署合同" +} +``` + +#### 响应示例 + +```json +{ + "code": 200, + "message": "成功", + "success": true, + "data": { + "contractId": "2094000000000000001", + "contractName": "2026 年度框架合同", + "contractType": "FRAME", + "startDate": "2026-09-01", + "endDate": "2027-08-31", + "status": "ACTIVE", + "updateTime": "2026-08-29 16:10:01" + } +} +``` + +#### 空数据 / 降级响应 + +仅发送 `changeReason` 可把 12 个业务字段全部清空。若替换后的业务载荷与当前记录完全相同,返回“未检测到合同实际变化”,不会伪造新版本。 + +#### 错误响应 + +提供过期版本时继续失败且零写入: + +```json +{ "code": 395014, "message": "数据已被修改,请刷新后重试", "success": false, "data": null } +``` + +#### 业务边界 + +- `expectedUpdateTime` 省略不等于关闭并发保护;分布式合同锁和数据库行锁仍串行化同一合同写入。 +- 客户端若选择发送版本,必须使用最近一次详情或写响应中的 `updateTime`。 +- `changeReason` 始终必填,失败时合同和审计均不写入。 + +### 3. 独立删除供应商合同 `DELETE /admin/supplier/items/{supplierId}/contracts/{contractId}/del` + +**VO**: `SupplierContractDeleteReqVO / Void` + +#### 使用场景 + +删除不再保留的合同登记。删除为软删除,并记录完整审计原因。 + +#### 入参 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|---|---|---|---:|---|---| +| `supplierId` | Path | String | 是 | 正整数 ID 字符串 | 目标供应商 | +| `contractId` | Path | String | 是 | 正整数 ID 字符串 | 目标合同 | +| `expectedUpdateTime` | Body | String | 否 | `yyyy-MM-dd HH:mm:ss` | 提供时执行版本匹配 | +| `changeReason` | Body | String | 是 | 去空白后非空,最长 500 字符 | 删除审计原因 | + +#### 出参 `Result` + +| 字段 | 类型 | 说明 | +|---|---|---| +| `code` | Integer | 成功时为 `200` | +| `success` | Boolean | 成功时为 `true` | +| `data` | null | 删除成功不返回业务对象 | + +#### 请求示例 + +```json +{ + "changeReason": "合同登记作废" +} +``` + +#### 响应示例 + +```json +{ "code": 200, "message": "成功", "success": true, "data": null } +``` + +#### 空数据 / 降级响应 + +`expectedUpdateTime` 可省略,但请求体不能省略,且必须包含非空 `changeReason`。 + +#### 错误响应 + +```json +{ "code": 400, "message": "变更原因不能为空", "success": false, "data": null } +``` + +#### 业务边界 + +- 提供的过期 `expectedUpdateTime` 仍返回 `395014`,不删除、不写审计。 +- 删除成功后基本信息详情的 `contracts[]` 不再返回该记录。 +- 权限、锁、幂等、审计和软删除边界均保持原有实现。 + +### 4. 查询供应商基本信息 `GET /admin/supplier/items/{supplierId}/basic-info/view` + +**VO**: `SupplierBasicInfoRespVO` + +#### 使用场景 + +进入供应商编辑页时读取主体、资质和合同。前端将 `data.contracts` 绑定到“合同信息”table。 + +#### 入参 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|---|---|---|---:|---|---| +| `supplierId` | Path | String | 是 | 正整数 ID 字符串 | 目标供应商 | + +无 Query 参数,无请求体。 + +#### 出参 `Result` + +| 字段 | 类型 | 说明 | +|---|---|---| +| `data.contracts` | Array | 当前未软删除合同,按 `contractId` 升序 | +| `data.contracts[].contractId` | String | 合同雪花 ID | +| `data.contracts[]` 的 12 个业务字段 | 对应类型/null | 合同登记未填写时返回 `null` | +| `data.contracts[].updateTime` | String | 合同当前版本 | + +#### 请求示例 + +```http +GET /admin/supplier/items/2094000000000000000/basic-info/view +Authorization: Bearer +``` + +#### 响应示例 + +```json +{ + "code": 200, + "message": "成功", + "success": true, + "data": { + "supplierId": "2094000000000000000", + "contracts": [ + { + "contractId": "2094000000000000001", + "contractName": null, + "contractType": null, + "startDate": null, + "endDate": null, + "status": null, + "updateTime": "2026-08-29 16:10:00" + } + ] + } +} +``` + +#### 空数据 / 降级响应 + +没有合同登记时 `data.contracts` 返回空数组;单份合同没有填写的业务字段返回 `null`,二者含义不同。 + +#### 错误响应 + +```json +{ "code": 395001, "message": "供应商不存在", "success": false, "data": null } +``` + +#### 业务边界 + +- 只读接口要求可信管理员、可读角色和 `supplier:view` 平台权限。 +- 查询不推进主体或合同版本,不触发审批和写副作用。 +- 合同 table 的新增、编辑、删除分别调用前三个独立写接口,不把 `contracts` 回传给供应商聚合更新接口。 + +## 四、契约约束与正确调用方式 + +- 新建供应商可完全省略合同;若需登记,先调用 `POST /admin/supplier/items/add` 获取字符串 `supplierId`,再调用合同新增接口。 +- 编辑页合同读取使用 `GET /admin/supplier/items/{supplierId}/basic-info/view` 的 `data.contracts`。 +- 编辑页结算读取继续使用 `GET /admin/supplier/items/{supplierId}/account-info/list`;新增账户继续使用 `POST /admin/supplier/items/{supplierId}/bank-accounts/add`,其审批和主体状态门禁不变。 +- 新建的 `initialAccounts[]` 与独立账户新增的 `accounts[]` 继续复用同一 `SupplierBankAccountReqVO`,可填写项一致:`accountType`、`bankName`、`bankBranch`、`accountNo`、`proofFileUrls`、`settleMode`、`accountPeriod`、`invoiceType`、`taxRate`。新建供应商最多携带 1 项初始账户。 +- `supplierId`、`contractId`、`accountId` 和 `amount` 按字符串处理,禁止转为 JavaScript Number。 + +## 五、数据库行为 + +- Flyway migration `V20260829_002` 将 `supplier_contract.contract_name`、`contract_type`、`start_date`、`end_date`、`status` 从 `NOT NULL` 调整为可空。 +- 合同创建、更新、删除继续在本服务 schema 内完成;审计与业务写同事务提交或回滚。 +- 没有跨 schema 写入,没有新增 Redis、MQ、配置或路由变更。 + +## 六、边界行为 + +- 业务字段为空不等于审计字段可空:合同新增、更新、删除均必须有 `changeReason`。 +- `expectedUpdateTime` 仅在合同更新和删除中可省略;提供时仍执行秒级版本比较。 +- 合同日期只在 `startDate` 与 `endDate` 同时存在时校验先后顺序。 +- 合同枚举字段为空时不校验;非空时仍只接受既有枚举值。 +- 账户查询和新增的既有状态、审批、权限及敏感附件读取规则未改变。 + +## 六.6、修改前后对比 + +| 项目 | 修改前 | 修改后 | +|---|---|---| +| 合同名称、类型、开始日、结束日、状态 | 必填 | 可空 | +| 其余 7 个合同业务字段 | 可空 | 仍可空 | +| `changeReason` | 新增、更新、删除均必填 | 继续必填 | +| `expectedUpdateTime` | 更新、删除必填 | 更新、删除可选;提供过期值仍拒绝 | +| 结算信息字段模型 | 新建与独立账户新增复用同一 VO | 不变,编辑页继续复用既有账户接口 | + +## 六.7、影响评估 + +- **是否破坏向后兼容**: 否;原先完整载荷继续有效,旧客户端继续发送版本也有效。 +- **前端是否必须同步上线**: 是;需要增加合同/结算 table、允许合同业务控件为空,并保留 `changeReason` 必填。 +- **前端 workaround 清理点**: 删除合同业务字段的前端强制必填;不要删除 `changeReason` 校验。 + +## 七、不影响范围 + +- 不修改供应商新建、更新、提交请求中的废弃 `contracts` 聚合字段语义。 +- 不修改结算账户字段、审批、状态机、默认账户、权限或接口路径。 +- 不修改供应商主体的 `changeReason`、`expectedUpdateTime` 必填规则。 +- 不新增业务错误码、Gateway 路由、Redis、MQ 或 Nacos 配置。 + +## 八、测试环境已验证 + +- 合同信息整体省略不影响供应商草稿创建;空业务字段合同可登记并由详情读回。 +- 合同可在不传 `expectedUpdateTime` 时补全、清空和删除;传入过期版本返回并发失败且零写入。 +- 新增、更新、删除缺少 `changeReason` 均失败且零写入。 +- 编辑供应商后合同和初始结算账户摘要保持;结算读取入口可正常访问。 +- TEST 验收产生的供应商草稿、合同和初始账户已软删除,并通过详情与结算入口读回确认不可见。 + +## 十、相关文档 + +- Issue: [#6654](https://git.1814.love:8443/wx/HL/issues/6654) +- PR: [#6665](https://git.1814.love:8443/wx/HL/pulls/6665) +- 合并提交: `65ac86e9312b22dd2fc7313049a693fdb0ccad59` + +## 关联 / 联系人 + +- **后端负责人**: @lc +- **前端状态**: 待前端处理