--- schema: "hl-changelog/v2" ticket: "7919" title: "供应商合同改为主合同/补充合同,业务线改为资源并按资源选择主合同" consumer: "admin" author: "lc(GIT)" change_type: "修改接口" backend_status: "deployed" gateway_status: "verified" frontend_status: "verified" frontend_owner: "mmg" frontend_ref: "dde41d86ffa2a6bd2cd0ae10b96e0ab1d78821fa" target_release: "" verified_at: "2026-09-18" status_note: "PR #7924 已合并 dev-v3,合并提交 ac6b9a3b1 由部署任务 b958fc83 发布到 TEST,并经真实 Gateway 验收 30/30 通过;前端需按本条改表单与取数。[mmg 2026-09-18 已实现并验证] 供应商合同登记/编辑弹窗业务线文本块改「合同类型」可见必填(MAIN/SUPPLEMENT)+「资源」filterable 下拉(composite key module|id 同设两字段,buildBody split 拆分),relatedMainContract 文本块改 relatedMainContractId 下拉仅 SUPPLEMENT 渲染;级联走显式 @update:value 非 watch(换资源/切类型清空已选主合同并按新资源重拉 main-options,fillForm 编程回填直接 loadMainOptions 不清已选值防 fill-race);历史非新枚举类型回显归 null 强制补齐;列表列改业务线→资源 resourceName/关联主合同→relatedMainContractNo 并新增合同类型列;#6842 隐藏字段语义不变整份回传;新增 getSupplierContractMainOptions 封装,395034/395038/395052/395064 透 message;两弹窗 spec 重写/对齐定向 35/35+scoped checkpoint 全绿(全量 Vitest+生产构建)。" updated_at: "2026-09-18" base: "dev-v3" --- # 供应商合同: 合同类型改为主合同/补充合同,业务线改为资源,补充合同按资源选择主合同 > **服务**: hl-resource-service (端口 8082) > **PR**: #7924 > **Issue**: #7919 > **日期**: 2026-09-18 > **影响范围**: 管理后台供应商详情「合同信息」的新增/编辑合同表单与合同列表 --- ## ⚠️ 关键变化 - 合同新增、编辑**不再接收** `businessLine`(业务线)和 `relatedMainContract`(手填的关联主合同文字),改为 `resourceModule` + `resourceId`(资源)和 `relatedMainContractId`(关联主合同 ID)。 - `contractType` 由可选的 `FRAME/SINGLE_TRIP/PURCHASE` 改为**必填**的 `MAIN`(主合同)/ `SUPPLEMENT`(补充合同),旧三个值会被拒绝。 - 前端不改就无法保存合同(缺合同类型、缺资源都会报 400),**需要前端同步上线**。 --- ## 二、变更接口清单 | # | 接口 | 方法 | 路径 | 变更类型 | 说明 | |---|------|------|------|----------|------| | 1 | 独立登记供应商合同 | POST | `/admin/supplier/items/{supplierId}/contracts/add` | 请求体与响应体字段变更 | 合同类型必填主/补充;资源替代业务线;关联主合同改为 ID | | 2 | 独立更新供应商合同 | PUT | `/admin/supplier/items/{supplierId}/contracts/{contractId}/update` | 请求体与响应体字段变更 | 同上 | | 3 | 查询可选主合同 | GET | `/admin/supplier/items/{supplierId}/contracts/main-options` | 新增接口 | 按供应商 + 资源列出补充合同可关联的主合同 | | 4 | 查询供应商基本信息 | GET | `/admin/supplier/items/{supplierId}/basic-info/view` | 响应体字段变更 | `contracts[]` 去掉业务线/手填关联主合同,新增资源与关联主合同字段 | --- ## 三、接口详情 ### 1. 独立登记供应商合同 `POST /admin/supplier/items/{supplierId}/contracts/add` **VO**: `SupplierContractCreateReqVO` → `Result` #### 使用场景 在供应商详情「合同信息」新增一份合同。资源从该供应商「资源信息」(`GET /admin/supplier/items/{supplierId}/resource-info/page` 返回的 `resourceModule` + `resourceId`)中选;合同类型为补充合同时,关联主合同从接口 3 的返回里选。 #### 入参 | 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | |------|------|------|------|------|------| | supplierId | Path | String(Long) | ✅ | 正整数 | 供应商 ID | | contractType | Body | String | ✅ | `MAIN` / `SUPPLEMENT` | 合同类型:主合同 / 补充合同(新) | | resourceModule | Body | String | ✅ | 资源信息的资源模块编码,如 `HOTEL` | 资源模块(新,替代 `businessLine`) | | resourceId | Body | String(Long) | ✅ | 正整数;须为该供应商当前绑定的资源 | 资源 ID(新,替代 `businessLine`) | | relatedMainContractId | Body | String(Long) | 补充合同✅ / 主合同不传 | 正整数;须为同一供应商、同一资源下的主合同 | 关联主合同 ID(新,替代 `relatedMainContract`);主合同传了也不保存 | | amount | Body | Number | ❌ | 0~9999999999.99,最多两位小数 | 签约金额(字段不变,名称改为签约金额) | | contractName / contractNo / signDate / startDate / endDate / autoRenew / status / scanFileUrl | Body | - | ✅ | 与原来相同 | 其余必填项不变 | | pricingMode / settleCycle / remark | Body | String | ❌ | 与原来相同 | 不变 | | businessLine / relatedMainContract | Body | - | - | **已移除** | 传了会被忽略 | #### 出参 `Result` | 字段 | 类型 | 说明 | |------|------|------| | contractType | String | `MAIN` / `SUPPLEMENT` | | resourceModule | String | 资源模块 | | resourceId | String | 资源 ID | | resourceName | String | 资源当前名称;资源已删除或暂时读不到时为 null | | relatedMainContractId | String | 关联主合同 ID;主合同为 null | | relatedMainContractNo | String | 关联主合同的合同编号;主合同为 null | | amount | String | 签约金额 | | 其余字段 | - | 与原来相同;`businessLine`、`relatedMainContract` 不再返回 | #### 请求示例 ```json { "contractName": "满洲里饭店补充协议", "contractNo": "HT-2026-009", "contractType": "SUPPLEMENT", "resourceModule": "HOTEL", "resourceId": "3001000000000000010", "relatedMainContractId": "2100783918801174530", "amount": 20000.00, "signDate": "2026-09-18", "startDate": "2026-09-18", "endDate": "2027-09-17", "autoRenew": false, "status": "SIGNED", "scanFileUrl": "https://hlgl-test.oss-cn-beijing.aliyuncs.com/test/other/xxx.pdf" } ``` #### 响应示例 ```json { "code": 200, "message": "成功", "data": { "amount": null, "autoRenew": false, "contractId": "2100784143032860674", "contractName": "HL7919-TEST-S1", "contractNo": "HL7919-TEST-S1", "contractType": "SUPPLEMENT", "endDate": "2027-09-17", "pricingMode": null, "relatedMainContractId": "2100783918801174530", "relatedMainContractNo": "HL7919-TEST-M1", "remark": null, "resourceId": "3001000000000000010", "resourceModule": "HOTEL", "resourceName": "满洲里饭店(百年俄式)", "scanFileUrl": "https://hlgl-test.oss-cn-beijing.aliyuncs.com/test/other/xxx.pdf", "settleCycle": null, "signDate": "2026-09-18", "startDate": "2026-09-18", "status": "SIGNED", "updateTime": "2026-09-18 11:09:05" }, "success": true } ``` #### 空数据 / 降级响应 资源名称读取失败(如车队服务暂不可用)时合同照常保存,`resourceName` 为 null。 #### 错误响应 ```json { "code": 395064, "message": "关联主合同须为同一供应商、同一资源下的主合同", "success": false, "data": null } ``` #### 业务边界 - 权限与原来相同(`supplier:update`,FINANCE / SUPER_ADMIN),供应商暂停合作、黑名单、归档或企微审批中时仍拒绝写合同。 - 资源不是该供应商当前绑定的资源 → `395038 供应商资源关联不存在`;资源模块非法 → `395034`。 - 补充合同未传 `relatedMainContractId` → `395052 关联主合同不能为空`;关联的不是同一供应商、同一资源下的主合同(含关联补充合同、其他资源或其他供应商的合同、自己)→ `395064 关联主合同须为同一供应商、同一资源下的主合同`。 - 失败请求不写库。 ### 2. 独立更新供应商合同 `PUT /admin/supplier/items/{supplierId}/contracts/{contractId}/update` **VO**: `SupplierContractUpdateReqVO` → `Result` #### 使用场景 编辑一份合同。整份替换,入参与接口 1 相同,另可带 `expectedUpdateTime`(取详情里该合同的 `updateTime`)。历史合同(类型为空或框架/单团/采购、没有资源)编辑保存时必须按新规则补齐合同类型与资源。 #### 入参 | 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | |------|------|------|------|------|------| | supplierId / contractId | Path | String(Long) | ✅ | 正整数 | 供应商 ID / 合同 ID | | contractType | Body | String | ✅ | `MAIN` / `SUPPLEMENT` | 同接口 1 | | resourceModule / resourceId | Body | String | ✅ | 同接口 1 | 同接口 1 | | relatedMainContractId | Body | String(Long) | 补充合同✅ | 同接口 1;不能是本合同自己 | 同接口 1 | | expectedUpdateTime | Body | String | ❌ | `yyyy-MM-dd HH:mm:ss` | 乐观版本,不变 | | 其余字段 | Body | - | - | 与接口 1 相同 | - | #### 出参 `Result` | 字段 | 类型 | 说明 | |------|------|------| | 同接口 1 | - | 同接口 1 | #### 请求示例 ```json { "contractName": "满洲里饭店主合同", "contractNo": "HT-2026-008", "contractType": "MAIN", "resourceModule": "HOTEL", "resourceId": "3001000000000000010", "amount": 20000.00, "signDate": "2026-09-18", "startDate": "2026-09-18", "endDate": "2027-09-17", "autoRenew": false, "status": "SIGNED", "scanFileUrl": "https://hlgl-test.oss-cn-beijing.aliyuncs.com/test/other/xxx.pdf", "expectedUpdateTime": "2026-09-18 15:00:00" } ``` #### 响应示例 ```json { "code": 200, "message": "成功", "data": { "amount": "20000.00", "autoRenew": false, "contractId": "2100783918801174530", "contractName": "HL7919-TEST-M1", "contractNo": "HL7919-TEST-M1", "contractType": "MAIN", "endDate": "2027-09-17", "pricingMode": null, "relatedMainContractId": null, "relatedMainContractNo": null, "remark": "主合同带关联主合同ID", "resourceId": "3001000000000000010", "resourceModule": "HOTEL", "resourceName": "满洲里饭店(百年俄式)", "scanFileUrl": "https://hlgl-test.oss-cn-beijing.aliyuncs.com/test/other/xxx.pdf", "settleCycle": null, "signDate": "2026-09-18", "startDate": "2026-09-18", "status": "SIGNED", "updateTime": "2026-09-18 11:08:50" }, "success": true } ``` #### 空数据 / 降级响应 同接口 1,资源名称读取失败时 `resourceName` 为 null。 #### 错误响应 ```json { "code": 395038, "message": "供应商资源关联不存在", "success": false, "data": null } ``` #### 业务边界 - 错误码与接口 1 相同;另有原有的 `395014` 并发修改、`395051` 合同不存在、`395057` 无变化。 - 主合同改动时不检查是否已被补充合同关联。 ### 3. 查询可选主合同 `GET /admin/supplier/items/{supplierId}/contracts/main-options` **VO**: `SupplierMainContractOptionReqVO` → `Result>` #### 使用场景 新增或编辑补充合同、选定资源后,取该供应商在该资源下的主合同,作为「关联主合同」的可选项。 #### 入参 | 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | |------|------|------|------|------|------| | supplierId | Path | String(Long) | ✅ | 正整数 | 供应商 ID | | resourceModule | Query | String | ✅ | 资源模块编码 | 补充合同所选资源的模块 | | resourceId | Query | String(Long) | ✅ | 正整数 | 补充合同所选资源的 ID | #### 出参 `Result>` | 字段 | 类型 | 说明 | |------|------|------| | contractId | String | 主合同 ID,保存补充合同时作为 `relatedMainContractId` 传回 | | contractNo | String | 合同编号 | | contractName | String | 合同名称 | #### 请求示例 ```http GET /admin/supplier/items/2091381643661983746/contracts/main-options?resourceModule=HOTEL&resourceId=3001000000000000010 ``` #### 响应示例 ```json { "code": 200, "data": [ { "contractId": "2100783918801174530", "contractName": "HL7919-TEST-M1", "contractNo": "HL7919-TEST-M1" } ], "message": "成功", "success": true, "traceId": null } ``` #### 空数据 / 降级响应 ```json { "code": 200, "message": "成功", "data": [], "success": true } ``` #### 错误响应 ```json { "code": 395034, "message": "不支持的资源模块", "success": false, "data": null } ``` #### 业务边界 - 权限同供应商详情(`supplier:view`,ADMIN / FINANCE / SUPER_ADMIN)。 - 只返回未删除、类型为主合同、资源完全相同的合同,按合同 ID 升序;补充合同不会出现在结果中。 ### 4. 查询供应商基本信息 `GET /admin/supplier/items/{supplierId}/basic-info/view` **VO**: `SupplierBasicInfoRespVO.contracts[]` → `SupplierContractRespVO` #### 使用场景 供应商详情「合同信息」列表与编辑回显。 #### 入参 | 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | |------|------|------|------|------|------| | supplierId | Path | String(Long) | ✅ | 正整数 | 不变 | #### 出参 `Result` | 字段 | 类型 | 说明 | |------|------|------| | contracts[].contractType | String | `MAIN` / `SUPPLEMENT`;历史合同可能为 null 或 `FRAME`/`SINGLE_TRIP`/`PURCHASE` | | contracts[].resourceModule / resourceId / resourceName | String | 资源;历史合同为 null | | contracts[].relatedMainContractId / relatedMainContractNo | String | 关联主合同;主合同与历史合同为 null,主合同被删除后编号为 null | | contracts[].amount | String | 签约金额 | | contracts[].businessLine / relatedMainContract | - | **不再返回** | #### 请求示例 ```http GET /admin/supplier/items/2091381643661983746/basic-info/view ``` #### 响应示例 ```json { "code": 200, "message": "成功", "data": { "supplierId": "2091381643661983746", "...": "其余字段不变", "contracts": [ { "amount": "20000.00", "autoRenew": false, "contractId": "2100783918801174530", "contractName": "HL7919-TEST-M1", "contractNo": "HL7919-TEST-M1", "contractType": "MAIN", "endDate": "2027-09-17", "pricingMode": null, "relatedMainContractId": null, "relatedMainContractNo": null, "remark": "主合同带关联主合同ID", "resourceId": "3001000000000000010", "resourceModule": "HOTEL", "resourceName": "满洲里饭店(百年俄式)", "scanFileUrl": "https://hlgl-test.oss-cn-beijing.aliyuncs.com/test/other/xxx.pdf", "settleCycle": null, "signDate": "2026-09-18", "startDate": "2026-09-18", "status": "SIGNED", "updateTime": "2026-09-18 11:08:50" }, { "amount": null, "autoRenew": false, "contractId": "2100784143032860674", "contractName": "HL7919-TEST-S1", "contractNo": "HL7919-TEST-S1", "contractType": "SUPPLEMENT", "endDate": "2027-09-17", "pricingMode": null, "relatedMainContractId": "2100783918801174530", "relatedMainContractNo": "HL7919-TEST-M1", "remark": null, "resourceId": "3001000000000000010", "resourceModule": "HOTEL", "resourceName": "满洲里饭店(百年俄式)", "scanFileUrl": "https://hlgl-test.oss-cn-beijing.aliyuncs.com/test/other/xxx.pdf", "settleCycle": null, "signDate": "2026-09-18", "startDate": "2026-09-18", "status": "SIGNED", "updateTime": "2026-09-18 11:09:05" } ] }, "success": true } ``` #### 空数据 / 降级响应 没有合同时 `contracts` 为 `[]`;资源名称读取失败时对应合同的 `resourceName` 为 null,详情其余部分正常返回。 #### 错误响应 ```json { "code": 395001, "message": "供应商不存在", "success": false, "data": null } ``` #### 业务边界 - 其余字段不变。历史合同原有的业务线、手填关联主合同文字仍留在库中,但接口不再返回。 --- ## 四、契约约束与正确调用方式 | 场景 | payload | |------|---------| | ✅ 主合同 | `{ "contractType": "MAIN", "resourceModule": "HOTEL", "resourceId": "3001000000000000010", ... }`(不传 `relatedMainContractId`) | | ✅ 补充合同 | `{ "contractType": "SUPPLEMENT", "resourceModule": "HOTEL", "resourceId": "3001000000000000010", "relatedMainContractId": "<接口 3 返回的 contractId>", ... }` | | ❌ 旧合同类型 | `{ "contractType": "FRAME", ... }` → 400 合同类型不合法 | | ❌ 补充合同不带主合同 | `{ "contractType": "SUPPLEMENT", ... }` → 395052 | | ❌ 选了另一家酒店的主合同 | `relatedMainContractId` 属于其他资源 → 395064 | | ❌ 资源不属于该供应商 | `resourceId` 未绑定在该供应商下 → 395038 | --- ## 五、数据库行为 | 前端提交 | 合同类型 | 资源 | 关联主合同 | |----------|----------|------|------------| | 主合同,带或不带 `relatedMainContractId` | `MAIN` | 按提交保存 | 保存为空 | | 补充合同 + 合法主合同 | `SUPPLEMENT` | 按提交保存 | 保存所选主合同 ID | | 任一校验失败 | - | - | 不写库 | --- ## 六、边界行为 - 未登录 → 401(网关拦截)。 - 资源名称读取失败 → `resourceName` 为 null,不影响保存和详情。 - 历史合同 → 类型保持原值、资源与关联主合同为 null,不报错;编辑保存时须补齐新必填项。 --- ## 六.5、枚举 / 数据字典 ### contractType **所属字段**: `contractType` | **类型**: `String` | 值 | 中文 | 说明 | |----|------|------| | `MAIN` | 主合同 | 不关联其他合同 | | `SUPPLEMENT` | 补充合同 | 必须关联同一供应商、同一资源下的主合同 | ### resourceModule **所属字段**: `resourceModule` | **类型**: `String` 取值与供应商「资源信息」`resourceModule` 相同:`SCENIC` 景区、`RESTAURANT` 餐厅、`SUPPLIES` 备品、`SUPPLIES_COMBO` 组合配品、`ACTIVITY` 游玩项目、`HOTEL` 酒店、`SERVICE` 服务、`COST_ITEM` 额外成本、`STAFF` 服务人员、`VEHICLE` 车辆。 ## 六.6、修改前后对比 ### 字段级对比 | 字段 | 改前 | 改后 | |------|------|------| | contractType | 可选,`FRAME`/`SINGLE_TRIP`/`PURCHASE` | 必填,`MAIN`/`SUPPLEMENT` | | businessLine | 必填文字 | 移除,改为 `resourceModule` + `resourceId`(必填) | | relatedMainContract | 必填文字 | 移除,改为 `relatedMainContractId`(补充合同必填) | | amount | 合同金额,可选 | 签约金额,可选,规则不变 | | 响应 | 返回 `businessLine`、`relatedMainContract` | 返回 `resourceModule`、`resourceId`、`resourceName`、`relatedMainContractId`、`relatedMainContractNo` | ### 行为级对比 | 行为 | 改前 | 改后 | |------|------|------| | 关联主合同 | 所有合同都要填一段文字 | 主合同不填;补充合同从同一供应商、同一资源的主合同中选 | | 可选主合同 | 无 | 新增接口 3 | ## 六.7、影响评估 - **是否破坏向后兼容**: 是(请求体必填字段变化) - **前端是否必须同步上线**: 是 - **前端 workaround 清理点**: 业务线、关联主合同两个文本输入框及其必填校验 ## 七、不影响范围 - **仅影响**: 供应商详情「合同信息」的新增、编辑与列表显示 - **零影响**: 合同删除接口、供应商档案/审批/资源绑定等其他接口 ## 八、测试环境已验证 部署:`dev-v3@ac6b9a3b1`(合并 PR #7924)经部署任务 `b958fc83` 发布到 TEST,`hl-resource-service` 双实例滚动完成。验证供应商:HL6195-TEST-A-20260823122514(`2091381643661983746`)。 - 后端:hl-resource-service 全模块回归 2997 个测试,0 失败,0 错误;数据库迁移 V20260918_002 已在 TEST 执行成功。 - TEST 网关(部署提交 ac6b9a3b1):主合同新增/改签约金额回读一致;资源名称回读「满洲里饭店(百年俄式)」;主合同传关联主合同 ID 后回读为空;可选主合同按资源只返回对应主合同;补充合同关联主合同后回读编号;未绑定资源 395038、补充合同缺主合同 395052、关联其他资源/补充合同/其他供应商合同 395064、旧类型 FRAME 400,失败请求均未写库;历史 FRAME 合同照常返回。 --- ## 十、相关文档 - 关联 Issue: [wx/HL#7919](https://git.1814.love:8443/wx/HL/issues/7919) - 关联 PR: [wx/HL#7924](https://git.1814.love:8443/wx/HL/pulls/7924) - 资源选择来源:`GET /admin/supplier/items/{supplierId}/resource-info/page`(已有接口,不变) ## 关联 / 联系人 ### 链接 - **Issue**: [#7919](https://git.1814.love:8443/wx/HL/issues/7919) - **PR**: [#7924](https://git.1814.love:8443/wx/HL/pulls/7924) - **Merge commit**: [ac6b9a3b1](https://git.1814.love:8443/wx/HL/commit/ac6b9a3b1987753c0dd1eda03960ffb30063f401) ### 联系人 - **后端负责人**: @lc