文件
hl-api-changelog/changelogs-v2/2026-08/29_6544_供应商合同独立登记-新增接口-管理后台.md
T
Mimingguang b54c4cc8d7
changelog-filename-gate / validate (push) Successful in 2s
chore(changelog): 回写 #6544/#6620/#6643 前端消费状态 verified
修改原因:三条 changelog 前端已交付并推送 hl-admin v2.1,回写 frontmatter 消费证据。
- #6544 供应商合同独立登记 → frontend_ref 55a3a056
- #6620 主体证件号 taxNo 回显(DRAFT 可改非 DRAFT 锁定) → frontend_ref 9d5dcbab
- #6643 营业执照 licenseImageUrl 顶层权威回显 → frontend_ref bab3673b
2026-08-29 15:07:39 +08:00

22 KiB
原始文件 Blame 文件历史

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 6544 供应商合同独立登记 admin lc(GIT) 新增接口 deployed verified verified mmg 55a3a056 v2.1 2026-08-29 PR #6559、#6591、#6606 已合并 dev-v3;hl-resource-service 已通过 Deploy Panel 任务 ca2fe4d7 部署最终提交 c579c87014f56452fea2fce5075b31ae6fd12a35。#6591 的写后回读、395052–395058、金额 scale 等值与草稿合同存在门禁均已进入 TEST。#6544 基线曾完成独立合同写链路实证;本次最终提交因运行时身份缺少 supplier:update,按用户确认冻结权限依赖的真实写复测,未配置权限、未发业务写请求,不把缺失权限记为通过。 2026-08-29 dev-v3

供应商管理:合同独立登记

供应商合同从注册聚合中拆出,改为独立登记、独立更新、独立软删除的维护边界。合同不再随供应商建档审批提交,审批通过与否都不影响合同登记;已登记合同在详情回显中继续随供应商返回。

一、背景

旧版把合同作为供应商注册聚合的一部分随审批走(#6397),审批候选 v2 起不再携带 contracts。为支持签约后可随时登记线下合同、按业务状态独立更新/作废,本次新增三个独立合同写接口:登记(add)、完整替换更新(update)、软删除(del)。合同不参与供应商生命周期状态机,不受建档/审批/归档状态推进约束;删除草稿供应商时若仍有未删除合同会被拒绝(需先独立删合同)。

二、变更接口清单

# 接口 方法 路径 变更类型 说明
1 独立登记供应商合同 POST /admin/supplier/items/{supplierId}/contracts/add 新增写接口 在既有供应商下登记一份线下合同,不进入建档审批
2 独立更新供应商合同 PUT /admin/supplier/items/{supplierId}/contracts/{contractId}/update 新增写接口 带乐观版本整份替换合同全部业务字段
3 独立删除供应商合同 DELETE /admin/supplier/items/{supplierId}/contracts/{contractId}/del 新增写接口 带乐观版本软删除,审计原因必填

三、接口详情

1. 独立登记供应商合同 POST /admin/supplier/items/{supplierId}/contracts/add

VO: SupplierContractCreateReqVO / SupplierContractRespVO

使用场景

管理端在供应商详情「合同」区域新增一条线下合同。合同登记与供应商审批解耦,登记成功立即在详情合同列表随时间返回 updateTime 并发版本。

入参

字段 位置 类型 必填 约束 说明
supplierId Path String 是 正整数 ID 字符串 目标供应商;不得转为 JavaScript Number
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,不得早于 startDate 有效期结束日期
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<SupplierContractRespVO>

字段 类型 说明
contractId String 合同 ID(雪花,保证转字符串传输)
contractName String 合同名称
contractNo String/null 合同编号
contractType String 合同类型
signDate String/null 合同签署日期
startDate String 有效期开始日期
endDate String 有效期结束日期
amount String 合同金额(保证转字符串传输,保留两位小数场景由后端规范)
pricingMode String/null 计价方式
settleCycle String/null 结算周期
status String 合同状态;历史终止合同可能返回 TERMINATED
scanFileUrl String/null 合同扫描件地址
remark String/null 合同备注
updateTime String 当前并发版本,格式 yyyy-MM-dd HH:mm:ss;请原样用于下一次 update/del

请求示例

POST /admin/supplier/items/2091381911266967553/contracts/add
Authorization: Bearer <admin-token>
Content-Type: application/json

{
  "contractName": "2026年度框架合同",
  "contractNo": "HT-2026-001",
  "contractType": "FRAME",
  "signDate": "2026-08-29",
  "startDate": "2026-09-01",
  "endDate": "2027-08-31",
  "amount": "1200.50",
  "pricingMode": "按团结算",
  "settleCycle": "MONTHLY",
  "status": "ACTIVE",
  "scanFileUrl": "https://files.example.com/contracts/1.pdf",
  "remark": "线下签署后登记",
  "changeReason": "线下签署后登记"
}

响应示例

{
  "code": 200,
  "message": "成功",
  "success": true,
  "data": {
    "contractId": "2093378327078154241",
    "contractName": "2026年度框架合同",
    "contractNo": "HT-2026-001",
    "contractType": "FRAME",
    "signDate": "2026-08-29",
    "startDate": "2026-09-01",
    "endDate": "2027-08-31",
    "amount": "1200.50",
    "pricingMode": "按团结算",
    "settleCycle": "MONTHLY",
    "status": "ACTIVE",
    "scanFileUrl": "https://files.example.com/contracts/1.pdf",
    "remark": "线下签署后登记",
    "updateTime": "2026-08-29 00:41:00"
  },
  "traceId": null
}

空数据 / 降级响应

登记成功后 data 恒为完整合同对象(不会返回 null)。金额为空时 amount 返回 null,前端按无金额展示,不要把 null 当 "0.00":

{
  "code": 200,
  "message": "成功",
  "success": true,
  "data": {
    "contractId": "2093378327078154241",
    "contractName": "无金额合同",
    "contractNo": null,
    "contractType": "PURCHASE",
    "signDate": null,
    "startDate": "2026-09-01",
    "endDate": "2027-08-31",
    "amount": null,
    "pricingMode": null,
    "settleCycle": null,
    "status": "DRAFT",
    "scanFileUrl": null,
    "remark": null,
    "updateTime": "2026-08-29 00:41:00"
  }
}

错误响应

供应商不存在或已软删除:

{
  "code": 395001,
  "message": "供应商不存在",
  "success": false,
  "data": null
}

已归档供应商拒绝合同写入(不查询、不锁行、不落审计):

{
  "code": 395031,
  "message": "已归档供应商仅允许查看",
  "success": false,
  "data": null
}

正常 HTTP 请求先经过 Bean Validation;缺必填字段或长度超限仍返回 400 和中文字段文案。395052–395056 是 Service/事务层防旁路校验的冻结错误码,不替代 Controller 的 400:

{
  "code": 400,
  "message": "合同名称不能为空",
  "success": false,
  "data": null
}

业务边界

  • 权限:可信管理员身份 + FINANCE/SUPER_ADMIN 写角色 + supplier:update 平台权限;ADMIN 或任一平台权限门禁失败均不产生数据库写。
  • 合同登记只写 supplier_contract 单表与一条 CREATE 审计事实,不推进供应商主体版本,不进审批。
  • 幂等:同一管理员 + 同一供应商 + 同一规范化载荷 5 秒窗口内重复提交只执行一次;锁键按供应商分区。
  • 事务内先锁供应商行(FOR UPDATE),改合同必在事务中完成,失败整体回滚。
  • supplierId、contractId、amount 必须按字符串处理,禁止转 JavaScript Number。

2. 独立更新供应商合同 PUT /admin/supplier/items/{supplierId}/contracts/{contractId}/update

VO: SupplierContractUpdateReqVO(继承 Create 全字段 + expectedUpdateTime) / SupplierContractRespVO

使用场景

管理端对已登记合同做整份替换更新。请求体必须携带目标合同当前 updateTime 作为乐观版本围栏;服务端版本不匹配时整体拒绝且不产生任何写。

入参

字段 位置 类型 必填 约束 说明
supplierId Path String 是 正整数 ID 字符串 目标供应商;不得转为 Number
contractId Path String 是 正整数 ID 字符串 目标合同;不得转为 Number
(Create 全部字段) Body String 同新增 同新增 整份替换所需全字段
expectedUpdateTime Body String 是 yyyy-MM-dd HH:mm:ss 目标合同当前并发版本,来自详情或上次写响应

出参 Result<SupplierContractRespVO>

与新增相同,updateTime 返回新版本(严格晚于旧版本),请用该值继续后续围栏。

字段 类型 说明
contractId String 合同 ID(字符串)
(其余字段) - 同新增出参,updateTime 为新并发版本

请求示例

PUT /admin/supplier/items/2091381911266967553/contracts/2093378327078154241/update
Authorization: Bearer <admin-token>
Content-Type: application/json

{
  "contractName": "2026年度框架合同(变更)",
  "contractNo": "HT-2026-001",
  "contractType": "FRAME",
  "signDate": "2026-08-29",
  "startDate": "2026-10-01",
  "endDate": "2027-08-31",
  "amount": "1500.00",
  "pricingMode": "按团结算",
  "settleCycle": "MONTHLY",
  "status": "ACTIVE",
  "scanFileUrl": "https://files.example.com/contracts/1.pdf",
  "remark": "续约调整",
  "changeReason": "续约调整",
  "expectedUpdateTime": "2026-08-29 00:41:00"
}

响应示例

{
  "code": 200,
  "message": "成功",
  "success": true,
  "data": {
    "contractId": "2093378327078154241",
    "contractName": "2026年度框架合同(变更)",
    "contractNo": "HT-2026-001",
    "contractType": "FRAME",
    "signDate": "2026-08-29",
    "startDate": "2026-10-01",
    "endDate": "2027-08-31",
    "amount": "1500.00",
    "pricingMode": "按团结算",
    "settleCycle": "MONTHLY",
    "status": "ACTIVE",
    "scanFileUrl": "https://files.example.com/contracts/1.pdf",
    "remark": "续约调整",
    "updateTime": "2026-08-29 00:41:01"
  }
}

空数据 / 降级响应

更新接口无空数据场景。请求体携带的整份字段(含空值)会覆盖原值:contractNo、signDate、pricingMode、settleCycle、scanFileUrl、remark 传 null 或空串会被清空;amount 传 null 会清除金额。清空能力只在更新接口生效,新增不触发。

错误响应

版本不匹配/已过期(前端收到后请重新拉详情取最新 updateTime 再重试):

{
  "code": 395014,
  "message": "数据已被他人修改,请刷新后重试",
  "success": false,
  "data": null
}

合同不存在、已删除或不属于路径供应商(跨供应商同码隐藏):

{
  "code": 395051,
  "message": "供应商合同不存在",
  "success": false,
  "data": null
}

请求与当前内容完全一致(无实际变化,不推进版本):

{
  "code": 400,
  "message": "未检测到实际变化",
  "success": false,
  "data": null
}

业务边界

  • 同一供应商下同一合同的 update 与 del 共享锁键,串行化后版本围栏在事务内校验。
  • payloadChanged 用值比较(金额忽略小数位 scale),纯金额 1200.5 与 1200.50 等价,不会误判为变更。
  • 更新推进合同自身 updateTime(秒级严格递增),不推进供应商主体版本。
  • 5 秒幂等窗口按双 ID + 载荷摘要去重;锁键串行化同行写。
  • 审计记录完整前后快照差异(contracts 字段组)。

3. 独立删除供应商合同 DELETE /admin/supplier/items/{supplierId}/contracts/{contractId}/del

VO: SupplierContractDeleteReqVO / 无返回体

使用场景

对已登记合同作废软删除。删除后 deletedAt 写入当前时间,查询与详情不再返回;与同一合同 update 共享锁键和版本围栏。

入参

字段 位置 类型 必填 约束 说明
supplierId Path String 是 正整数 ID 字符串 目标供应商
contractId Path String 是 正整数 ID 字符串 目标合同
expectedUpdateTime Body String 是 yyyy-MM-dd HH:mm:ss 目标合同当前并发版本
changeReason Body String 是 最长 500 字符 本次删除原因,写入审计

出参 Result<Void>

成功时 data 为 null:

字段 类型 说明
code Integer 恒为 200 表示成功
data null 删除接口无返回体

请求示例

DELETE /admin/supplier/items/2091381911266967553/contracts/2093378327078154241/del
Authorization: Bearer <admin-token>
Content-Type: application/json

{
  "expectedUpdateTime": "2026-08-29 00:41:01",
  "changeReason": "线下合同作废"
}

响应示例

{
  "code": 200,
  "message": "成功",
  "success": true,
  "data": null
}

空数据 / 降级响应

删除成功恒返回上述结构;无空数据分支。删除后合同立即从查询/详情消失,如需保留展示请走更新改状态而非删除。

错误响应

版本不匹配:

{
  "code": 395014,
  "message": "数据已被他人修改,请刷新后重试",
  "success": false,
  "data": null
}

合同不存在或已删除:

{
  "code": 395051,
  "message": "供应商合同不存在",
  "success": false,
  "data": null
}

业务边界

  • 删除是软删除,不物理清理业务合同数据;删除与完整删除审计同事务提交或回滚。
  • 删除草稿供应商时若存在未删除合同,档案删除会被拒绝(提示先独立删合同,错误码见「六、边界行为」)。
  • 已删除合同的 expectedUpdateTime 不再有效,误传原版本返回合同不存在。

四、契约约束与正确调用方式

✅ 正确 / ❌ 错误 payload 对照

场景 调用 / 结果
✅ 新增后立即保存返回值 保存 updateTime 用于后续 update/del 版本围栏
✅ 更新时整份提交 传全字段;想清空的字段显式传 null 或空串
✅ 金额按字符串 "amount": "1200.50",contractId/supplierId 恒为字符串
✅ 并发被拒后重试 重新 GET 详情取最新 updateTime,再带新版本重试
❌ 把 ID/金额转 Number 可能丢精度;必须按字符串传输与比较
❌ 只传变更字段 update 是整份替换语义;缺字段会被清空
❌ 依赖审批候选带合同 审批候选 v2 起不携带 contracts;合同一律走独立接口维护

关键提示(当前 TEST 构建)

  • 已登记的合同通过详情接口随供应商返回的 contracts 数组回显,前端不必重复维护列表状态。
  • B1 已修复并部署:登记(add)插入后按合同 ID 回读数据库持久化实体,响应与 CREATE 审计使用同一真实秒级 updateTime;该值可直接用于下一次 update/del 版本围栏。

五、数据库行为

  • 写操作仅影响 supplier_contract 一行(新增 insert / 更新 update / 删除软删),以及 supplier_change_log 一条对应合同审计事实(fieldName=contract,前后完整快照差异)。
  • 三个写接口均不修改 supplier_main(供应商主体版本不变),不进审批流,不写缓存、MQ 或跨服务数据。
  • 更新与删除仅在版本围栏通过后产生写;版本不匹配时不产生任何数据库副作用。
  • 软删除使用统一 deleted_at 逻辑删除,物理行保留;查询与详情自动过滤。

六、边界行为

  • 未登录/登录失效:业务码 401;角色或平台权限不足:403,不查询不写入。
  • supplierId/contractId 非法(非数字、超长、<=0):400;供应商不存在或已软删除:395001。
  • 已归档供应商:395031(写入一律拒绝)。
  • 未知或非生命周期状态供应商:395005(状态机门禁先行失败)。
  • 版本不匹配:395014;请求无实际变化:395057。
  • 删除草稿供应商仍有未删除合同:395058「已有合同登记,请先独立删除合同」。
  • Controller 的 Bean Validation/绑定失败仍返回 400;Service/事务层防旁路校验使用 395052–395056,其中缺失合同并发版本文案为「预期更新时间不能为空」。

六.5、枚举 / 数据字典

contractType(合同类型)

所属字段: SupplierContractCreateReqVO.contractType / SupplierContractRespVO.contractType | 类型: String

值 说明
FRAME 框架合同
SINGLE_TRIP 单团单合同
PURCHASE 采购合同

status(合同状态)

所属字段: SupplierContractCreateReqVO.status / SupplierContractRespVO.status | 类型: String

值 说明
DRAFT 草稿
ACTIVE 生效中(可维护)
EXPIRED 已过期(可维护)
TERMINATED 已终止(仅历史回显旧值,写接口不允许设置)

七、不影响范围

  • 仅新增:三个独立合同写接口;合同登记与审批、状态机、归档完全解耦。
  • 保持兼容:供应商建档/更新/提交/审批/归档/暂停/拉黑/删除接口契约不变;详情回显 contracts 数组不变。
  • 零影响:审批候选 v2 起不含 contracts(v1 候选中的 contracts 字段被兼容忽略);不阻断既有审批流。
  • 零影响:收款账户、资质、资源关联、历史/审批流水等供应商子域。
  • 零影响:Gateway 路由(沿用 /admin/supplier/**)、数据库结构(无迁移)、Redis、MQ、Feign 契约。

八、测试环境已验证

  • 本地自动化:合同命令/事务/校验器定向测试(SupplierContractTransactionServiceTest、SupplierContractValidatorTest、SupplierContractServiceTest、SupplierAggregateWriterTest、SupplierErrorCodeContractTest)51 项零失败;每日审查修复后 hl-resource-service 全量 2181 项零失败、零错误(38 项条件跳过)。
  • 主 PR #6559、审查修复 PR #6591 与中文版本文案补充 PR #6606 均已合入 dev-v3;最终 Resource 合并提交为 d7e455492648b085445691ccde4a13198d4cd6c6。
  • TEST 部署:Deploy Panel 任务 ca2fe4d7 成功,预期/实际提交均为 c579c87014f56452fea2fce5075b31ae6fd12a35;双进程、Nacos 两实例、6 个任务期采样和两类日志门禁通过,零观测不可用采样。
  • 最终自动化:合同定向 73 项、供应商真实 MySQL Testcontainers 398 项、Resource 全量 2181 项均为 0 失败/0 错误;全量保留 38 项既有条件跳过。
  • Gateway 边界:#6544 基线写链路已有真实 TEST 实证;最终部署回读身份为 SUPER_ADMIN,但当前平台权限不含 supplier:update。用户明确冻结该权限依赖的写复测,本轮没有配置/绕过权限,也没有发合同业务写请求;权限与事务成功/拒绝路径以最终自动化证据补充,不宣称运行时缺失权限已通过。
  • 数据清理:本轮最终部署和冻结验收未创建供应商、合同、缓存、MQ 或审批合成数据;TEST 身份已登出。

九、相关历史 PR

PR Issue 说明 是否仍有效
#6559 #6544 独立合同登记三写接口 + 边界拆分 ✅ 本次主功能
#6591 #6604 合同模块每日审查修复 7 项(写后读回/错误码/VO 文档/金额比较) ✅ 已部署
#6606 #6604 合同并发版本校验中文文案与全局错误码审计修复 ✅ 已部署
#6519 #6474 供应商变更/审批分离(前置演进) ✅ 不受影响

十、相关文档

  • 关联 Issue:#6544
  • 关联 PR:#6559 / #6591 / #6606
  • 管理端接入:详情页「合同」区改为直接调 add/update/del 三接口;提交审批的候选不再携带 contracts。

撤回

  1. 管理端停止请求三个 contract 写路径,并清除详情页合同编辑入口。
  2. 从最新 dev-v3 建独立回退分支,按依赖逆序回退 d7e455492648b085445691ccde4a13198d4cd6c6、9eb9c96c09975a3ccf4966a257cc77bdf9f743b9 与 be0fe8125197b759459c975d211319e10cad150c,验证后经独立 PR 合入。
  3. 仅需回退写接口时可先保留回显逻辑,只移除 add/update/del 路由与前端入口。
  4. 使用 Deploy Panel 两阶段客户端仅滚动部署 hl-resource-service;本次无数据库、配置、Redis 或 MQ 恢复步骤。
  5. 撤回后经 Gateway 验证三个写路径不可用、详情 contracts 回显按选定回退范围正常,并确认零写入。
  6. 同步发布本 Changelog 的撤回说明;不得仅改文件名表达状态。

关联 / 联系人

链接

联系人

  • 后端负责人: @lc