From 0119ffca673e49371f2ba1a7c7ed4f3ec02742a0 Mon Sep 17 00:00:00 2001 From: lc Date: Fri, 18 Sep 2026 11:14:07 +0800 Subject: [PATCH] =?UTF-8?q?docs(changelog):=20#7919=20=E4=BE=9B=E5=BA=94?= =?UTF-8?q?=E5=95=86=E5=90=88=E5=90=8C=E6=94=B9=E4=B8=BA=E4=B8=BB=E5=90=88?= =?UTF-8?q?=E5=90=8C/=E8=A1=A5=E5=85=85=E5=90=88=E5=90=8C=EF=BC=8C?= =?UTF-8?q?=E4=B8=9A=E5=8A=A1=E7=BA=BF=E6=94=B9=E4=B8=BA=E8=B5=84=E6=BA=90?= =?UTF-8?q?=E5=B9=B6=E6=8C=89=E8=B5=84=E6=BA=90=E9=80=89=E6=8B=A9=E4=B8=BB?= =?UTF-8?q?=E5=90=88=E5=90=8C=EF=BC=88=E4=BF=AE=E6=94=B9=E6=8E=A5=E5=8F=A3?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5 (1M context) --- ...�ˆ同补充合同并按资源选择主合同-修改接口-管理后台.md | 532 ++++++++++++++++++ 1 file changed, 532 insertions(+) create mode 100644 changelogs-v2/2026-09/18_7919_供应商合同改为主合同补充合同并按资源选择主合同-修改接口-管理后台.md diff --git a/changelogs-v2/2026-09/18_7919_供应商合同改为主合同补充合同并按资源选择主合同-修改接口-管理后台.md b/changelogs-v2/2026-09/18_7919_供应商合同改为主合同补充合同并按资源选择主合同-修改接口-管理后台.md new file mode 100644 index 00000000..ec73fec3 --- /dev/null +++ b/changelogs-v2/2026-09/18_7919_供应商合同改为主合同补充合同并按资源选择主合同-修改接口-管理后台.md @@ -0,0 +1,532 @@ +--- +schema: "hl-changelog/v2" +ticket: "7919" +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 #7924 已合并 dev-v3,合并提交 ac6b9a3b1 由部署任务 b958fc83 发布到 TEST,并经真实 Gateway 验收 30/30 通过;前端需按本条改表单与取数。" +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