前端 hl-admin 按本条完成破坏性表单重构并验证,回写 frontmatter: frontend_status=verified + owner mmg + ref dde41d86ffa2a6bd2cd0ae10b96e0ab1d78821fa + verified_at 2026-09-18 + status_note 追加实现与验证说明。
21 KiB
schema, ticket, title, consumer, author, change_type, backend_status, gateway_status, frontend_status, frontend_owner, frontend_ref, target_release, verified_at, status_note, updated_at, base
| schema | ticket | title | consumer | author | change_type | backend_status | gateway_status | frontend_status | frontend_owner | frontend_ref | target_release | verified_at | status_note | updated_at | base |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| hl-changelog/v2 | 7919 | 供应商合同改为主合同/补充合同,业务线改为资源并按资源选择主合同 | admin | lc(GIT) | 修改接口 | deployed | verified | verified | mmg | dde41d86ffa2a6bd2cd0ae10b96e0ab1d78821fa | 2026-09-18 | 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+生产构建)。 | 2026-09-18 | 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<SupplierContractRespVO>
使用场景
在供应商详情「合同信息」新增一份合同。资源从该供应商「资源信息」(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<SupplierContractRespVO>
| 字段 | 类型 | 说明 |
|---|---|---|
| contractType | String | MAIN / SUPPLEMENT |
| resourceModule | String | 资源模块 |
| resourceId | String | 资源 ID |
| resourceName | String | 资源当前名称;资源已删除或暂时读不到时为 null |
| relatedMainContractId | String | 关联主合同 ID;主合同为 null |
| relatedMainContractNo | String | 关联主合同的合同编号;主合同为 null |
| amount | String | 签约金额 |
| 其余字段 | - | 与原来相同;businessLine、relatedMainContract 不再返回 |
请求示例
{
"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"
}
响应示例
{
"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。
错误响应
{
"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<SupplierContractRespVO>
使用场景
编辑一份合同。整份替换,入参与接口 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<SupplierContractRespVO>
| 字段 | 类型 | 说明 |
|---|---|---|
| 同接口 1 | - | 同接口 1 |
请求示例
{
"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"
}
响应示例
{
"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。
错误响应
{
"code": 395038,
"message": "供应商资源关联不存在",
"success": false,
"data": null
}
业务边界
- 错误码与接口 1 相同;另有原有的
395014并发修改、395051合同不存在、395057无变化。 - 主合同改动时不检查是否已被补充合同关联。
3. 查询可选主合同 GET /admin/supplier/items/{supplierId}/contracts/main-options
VO: SupplierMainContractOptionReqVO → Result<List<SupplierMainContractOptionRespVO>>
使用场景
新增或编辑补充合同、选定资源后,取该供应商在该资源下的主合同,作为「关联主合同」的可选项。
入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---|---|---|
| supplierId | Path | String(Long) | ✅ | 正整数 | 供应商 ID |
| resourceModule | Query | String | ✅ | 资源模块编码 | 补充合同所选资源的模块 |
| resourceId | Query | String(Long) | ✅ | 正整数 | 补充合同所选资源的 ID |
出参 Result<List<SupplierMainContractOptionRespVO>>
| 字段 | 类型 | 说明 |
|---|---|---|
| contractId | String | 主合同 ID,保存补充合同时作为 relatedMainContractId 传回 |
| contractNo | String | 合同编号 |
| contractName | String | 合同名称 |
请求示例
GET /admin/supplier/items/2091381643661983746/contracts/main-options?resourceModule=HOTEL&resourceId=3001000000000000010
响应示例
{
"code": 200,
"data": [
{
"contractId": "2100783918801174530",
"contractName": "HL7919-TEST-M1",
"contractNo": "HL7919-TEST-M1"
}
],
"message": "成功",
"success": true,
"traceId": null
}
空数据 / 降级响应
{ "code": 200, "message": "成功", "data": [], "success": true }
错误响应
{ "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<SupplierBasicInfoRespVO>
| 字段 | 类型 | 说明 |
|---|---|---|
| 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 | - | 不再返回 |
请求示例
GET /admin/supplier/items/2091381643661983746/basic-info/view
响应示例
{
"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,详情其余部分正常返回。
错误响应
{ "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
- 关联 PR: wx/HL#7924
- 资源选择来源:
GET /admin/supplier/items/{supplierId}/resource-info/page(已有接口,不变)
关联 / 联系人
链接
联系人
- 后端负责人: @lc