文件
hl-api-changelog/changelogs-v2/2026-08/26_6397_供应商注册合同聚合信息-修改接口-管理后台.md
T

18 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 6397 供应商注册合同聚合信息 admin lc(GIT) 修改接口 deployed verified pending mmg f2a4200d 2026-08-26 PR #6405 已合并 dev-v3;Deploy Panel 任务 af3f205b 成功发布 Resource 双实例,合同接口已完成 23 项真实 Gateway 验收。TEST 工作副本存在服务器本地提交导致精确提交回读仍被阻塞,后端工单保持开启;本条只交接已经实测存在的接口契约。2026-08-26 晚 PR #6450 修正:响应 contracts[].amount 由 Number 改为 String(金额全站统一字符串输出),前端 f2a4200d 按 Number 集成处需改为按字符串解析,改完请回填 frontend_status。 2026-08-26 dev-v3

🔧 供应商注册合同聚合信息

供应商创建草稿、提交注册和基础信息详情现统一支持合同完整快照。管理端应把“合同信息”放在“资质证照”之后、现有账户区域之前,并把原“初始账户”展示标题改为“结算信息”。

展示名调整不改变接口字段:原请求字段仍为 initialAccounts,不得改成 settlementInfo 或其他名称。

二、变更接口清单

# 接口 方法 路径 变化
1 创建供应商注册草稿 POST /admin/supplier/items/add 请求可选增加 contracts 完整集合
2 提交供应商注册 POST /admin/supplier/items/{supplierId}/submit 请求可选增加 contracts 完整快照
3 查询供应商基本信息 GET /admin/supplier/items/{supplierId}/basic-info/view 响应增加 contracts 列表

统一响应均为 Result<T>。业务失败可能仍为 HTTP 200,调用方必须同时判断 code、success、message 和 data。

⚠️ 关键变化(2026-08-26 晚,PR #6450 同日修正)

  • 响应 contracts[].amount 由 Number 改为 String:此前(f2a4200d 验收时)响应示例为 "amount": 1200.50,现实际输出 "amount": "1200.50",与全站金额字段(contractId 之外的金额一律字符串)对齐,防 JavaScript 浮点精度丢失。
  • 请求侧不变:contracts[].amount 请求仍按 Number 传(字符串同值也可被兼容解析),无需改表单提交。
  • 前端处理:详情/列表展示处把 amount 当字符串渲染即可,参与运算前 Number(...) 转换。

三、接口详情

三个接口的合同字段完全一致,统一在「公共合同字段」约定;各接口的使用场景、请求/响应示例与错误码分节详述。

公共合同字段

请求字段 contracts[]

字段 类型 创建必填 提交既有项必填 约束与说明
contractId String 否,且禁止传入 是 正整数 ID 字符串;必须属于当前供应商,同一快照不得重复
contractName String 是 是 非空白,最长 500 字符
contractNo String 否 否 最长 100 字符
contractType String 是 是 FRAME、SINGLE_TRIP、PURCHASE
signDate String 否 否 yyyy-MM-dd
startDate String 是 是 yyyy-MM-dd
endDate String 是 是 yyyy-MM-dd,不得早于 startDate
amount Number 否 否 大于等于 0,最多 10 位整数和 2 位小数
pricingMode String 否 否 计价方式说明,最长 100 字符
settleCycle String 否 否 结算周期说明,最长 32 字符
status String 是 是 写接口允许 DRAFT、ACTIVE、EXPIRED
scanFileUrl String 否 否 合同扫描件永久地址,最长 1000 字符
remark String 否 否 最长 500 字符

响应字段 contracts[]

详情返回上述全部业务字段,并额外返回:

字段 类型 说明
contractId String 合同 ID,始终按字符串处理,不得转 JavaScript Number
updateTime String 合同当前版本,格式 yyyy-MM-dd HH:mm:ss

响应中的 amount 为 String(如 "1200.50",PR #6450 起),请求中的 amount 仍按 Number 传。

历史数据可能返回只读状态 TERMINATED;创建和提交请求不得发送该状态。

1. 创建供应商注册草稿 POST /admin/supplier/items/add

POST /admin/supplier/items/add

使用场景与边界

  • contracts 可省略、为 null 或空数组,旧客户端行为不变。
  • 非空时最多 100 项,合同与供应商主体、资质和 initialAccounts 一起成功或一起失败。
  • 创建请求中的每个合同都是新合同,禁止携带 contractId。
  • 写入仍要求现有供应商创建权限;仅可信 FINANCE、SUPER_ADMIN 且拥有对应平台权限的身份可执行。

典型请求

POST /admin/supplier/items/add
Authorization: Bearer <admin-token>
Content-Type: application/json
{
  "fullName": "示例供应商有限公司",
  "shortName": "示例供应商",
  "taxNo": "91350211M000100Y46",
  "types": [
    {
      "typeCode": "SCENIC"
    }
  ],
  "mainCooperation": "景区资源合作",
  "licenseImageUrl": "https://files.example.com/license.png",
  "qualifications": [
    {
      "qualType": "BUSINESS_LICENSE",
      "certNo": "LIC-2026-001",
      "imageUrl": "https://files.example.com/license.png",
      "permanentValid": true
    }
  ],
  "contracts": [
    {
      "contractName": "2026 年度框架合同",
      "contractNo": "HT-2026-001",
      "contractType": "FRAME",
      "signDate": "2026-08-26",
      "startDate": "2026-09-01",
      "endDate": "2027-08-31",
      "amount": 1200.50,
      "pricingMode": "按团结算",
      "settleCycle": "MONTHLY",
      "status": "DRAFT",
      "scanFileUrl": "https://files.example.com/contracts/HT-2026-001.pdf",
      "remark": "年度合作"
    }
  ],
  "initialAccounts": []
}

成功响应

{
  "code": 200,
  "message": "成功",
  "data": {
    "supplierId": "2090300000000063970",
    "supplierNo": null,
    "status": "DRAFT",
    "onboardingStage": "PROFILE_DRAFT",
    "initialAccounts": [],
    "updateTime": "2026-08-26 11:10:00"
  },
  "success": true
}

失败响应:创建携带合同 ID

{
  "code": 400,
  "message": "创建草稿不能携带合同ID",
  "data": null,
  "success": false
}

失败时不会留下供应商主体或部分合同。

2. 提交供应商注册 POST /admin/supplier/items/{supplierId}/submit

POST /admin/supplier/items/{supplierId}/submit

使用场景与快照语义

  • contracts 省略或为 null:本次不处理合同,保留草稿当前合同。
  • contracts: []:明确清空当前全部合同。
  • 非空数组:作为完整快照;带 contractId 的项覆盖当前合同,不带 ID 的项新增,当前已有但数组中遗漏的合同删除。
  • 带 ID 的合同必须属于路径中的供应商;不属于当前供应商、重复 ID、非法枚举、负金额或日期逆序均失败。
  • expectedUpdateTime 仍是供应商聚合并发版本;发生并发修改时调用方应刷新详情后重新组装完整表单。
  • 合同快照会进入本次审批资料,但提交注册不会自动改写合同自身的 status。

典型请求

POST /admin/supplier/items/2090300000000063970/submit
Authorization: Bearer <admin-token>
Content-Type: application/json
{
  "fullName": "示例供应商有限公司",
  "shortName": "示例供应商",
  "taxNo": "91350211M000100Y46",
  "types": [
    {
      "typeCode": "SCENIC"
    }
  ],
  "mainCooperation": "景区资源合作",
  "licenseImageUrl": "https://files.example.com/license.png",
  "qualifications": [
    {
      "qualType": "BUSINESS_LICENSE",
      "certNo": "LIC-2026-001",
      "imageUrl": "https://files.example.com/license.png",
      "permanentValid": true
    }
  ],
  "contracts": [
    {
      "contractId": "2090300000000063971",
      "contractName": "2026 年度框架合同",
      "contractNo": "HT-2026-001",
      "contractType": "FRAME",
      "signDate": "2026-08-26",
      "startDate": "2026-09-01",
      "endDate": "2027-08-31",
      "amount": "1200.50",
      "pricingMode": "按团结算",
      "settleCycle": "MONTHLY",
      "status": "ACTIVE",
      "scanFileUrl": "https://files.example.com/contracts/HT-2026-001.pdf",
      "remark": "提交审批"
    }
  ],
  "initialAccounts": [],
  "expectedUpdateTime": "2026-08-26 11:10:00"
}

成功响应

{
  "code": 200,
  "message": "成功",
  "data": {
    "approvalLogId": "2090300000000063972",
    "requestNo": "SUP-REQ-7b8c9d00112233445566778899aabbccddeeff00112233445566778899aabbcc",
    "provider": "LOCAL_AUTO",
    "approvalStatus": "APPROVED",
    "spNo": null,
    "spStatus": null,
    "syncStatus": "APPLIED",
    "submittedAt": "2026-08-26 11:11:00",
    "finishedAt": "2026-08-26 11:11:00"
  },
  "success": true
}

失败响应:合同不属于当前供应商

{
  "code": 400,
  "message": "合同不属于当前供应商",
  "data": null,
  "success": false
}

该失败会回滚本次提交表单中的主体、资质、合同和审计变化,供应商仍保持原状态和原版本。

3. 查询供应商基本信息 GET /admin/supplier/items/{supplierId}/basic-info/view

GET /admin/supplier/items/{supplierId}/basic-info/view

请求

GET /admin/supplier/items/2090300000000063970/basic-info/view
Authorization: Bearer <admin-token>

无请求体。读取继续要求可信读角色和 supplier:view 平台权限。

成功响应

{
  "code": 200,
  "message": "成功",
  "data": {
    "supplierId": "2090300000000063970",
    "supplierNo": null,
    "fullName": "示例供应商有限公司",
    "shortName": "示例供应商",
    "tax_no": "9135**********0Y46",
    "legalRepresentative": null,
    "legalRepresentativeIdNoMask": null,
    "legalRepresentativeIdCardFrontUrl": null,
    "legalRepresentativeIdCardBackUrl": null,
    "contactPhoneMask": null,
    "establishDate": null,
    "registeredCapital": null,
    "businessScope": null,
    "staffScale": null,
    "mainCooperation": "景区资源合作",
    "status": "DRAFT",
    "creditLevel": "B",
    "totalScore": null,
    "types": [
      {
        "typeCode": "SCENIC",
        "typeName": "景区"
      }
    ],
    "contacts": [],
    "qualifications": [
      {
        "qualificationId": "2090300000000063973",
        "qualType": "BUSINESS_LICENSE",
        "qualTypeName": "营业执照",
        "certNoMask": "LI*********01",
        "imageUrl": "https://files.example.com/license.png",
        "expiryDate": null,
        "permanentValid": true,
        "daysUntilExpiry": null,
        "validityStatus": "VALID",
        "validityStatusName": "有效",
        "isRequired": false,
        "expired": false,
        "updateTime": "2026-08-26 11:10:00"
      }
    ],
    "contracts": [
      {
        "contractId": "2090300000000063971",
        "contractName": "2026 年度框架合同",
        "contractNo": "HT-2026-001",
        "contractType": "FRAME",
        "signDate": "2026-08-26",
        "startDate": "2026-09-01",
        "endDate": "2027-08-31",
        "amount": "1200.50",
        "pricingMode": "按团结算",
        "settleCycle": "MONTHLY",
        "status": "DRAFT",
        "scanFileUrl": "https://files.example.com/contracts/HT-2026-001.pdf",
        "remark": "年度合作",
        "updateTime": "2026-08-26 11:10:00"
      }
    ],
    "updateTime": "2026-08-26 11:10:00"
  },
  "success": true
}

合同按 contractId 升序返回,只包含当前有效合同。没有合同时返回空数组 [],不返回 null。

常见失败

场景 code 前端处理
未登录或 Token 失效 401 跳转登录,不展示空详情
可信角色或 supplier:view 平台权限不足 403 展示无权限状态
供应商不存在或已删除 395001 返回列表并刷新

六、边界行为

  • contracts 省略、null 与空数组语义不同:省略/传 null = 不处理合同(旧客户端兼容);[] = 明确清空全部合同。
  • 合同非空时单请求最多 100 项;合同与供应商主体、资质、initialAccounts 同事务,一起成功或一起失败。
  • 创建草稿禁止携带 contractId(每个合同都是新项);提交既有合同必须原样带回字符串 contractId,外部/他人合同 ID 触发整体回滚零写入。
  • endDate 早于 startDate 直接校验失败,零写入。
  • amount 边界:大于等于 0,最多 10 位整数和 2 位小数;响应按字符串输出(PR #6450 起)。
  • 历史只读状态 TERMINATED 仅可返回,创建/提交发送该状态会被拒绝。
  • 越权:仅可信 FINANCE、SUPER_ADMIN 且拥有对应平台权限可写;普通 ADMIN 调用写接口返回越权错误。

六.6、修改前后对比

场景 修改前 修改后
创建草稿 请求不能携带合同 可选携带完整 contracts,与草稿一起成功或失败
提交注册 提交表单不能维护合同 可省略保留、空数组清空或提交完整合同快照
基础信息 不返回合同列表 返回完整 contracts[] 及字符串 ID、版本
账户区域标题 页面显示“初始账户” 页面应显示“结算信息”,接口字段仍为 initialAccounts
页面区块顺序 资质后直接进入账户区域 资质证照 → 合同信息 → 结算信息

六.7、影响评估

  • 管理端(admin):供应商注册/编辑页需新增「合同信息」表格并调整区块顺序;既有合同必须缓存并原样回传字符串 contractId,否则提交会整单回滚。
  • 同日修正(PR #6450):响应 contracts[].amount 由 Number 改为 String,前端 f2a4200d 按 Number 集成的解析处需改为字符串处理(展示直接渲染、运算前 Number(...)),影响面限于合同金额展示/计算处,请求提交不受影响。
  • C 端(mp):不涉及,无影响。
  • QA 排查面:问题定位优先看「契约约束与正确调用方式」第 3、4 条(快照回传与空数组语义)与「关键变化」(amount 字符串化)。

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

  1. 在“资质证照”区域之后新增“合同信息”表格,在合同之后显示原账户表格。
  2. 原账户表格标题改为“结算信息”;所有请求和响应继续使用 initialAccounts,不要改字段名。
  3. 创建草稿时合同为完整新项,不发送 contractId;编辑后提交时,既有合同必须原样带回字符串 contractId。
  4. 提交表单是完整快照。用户明确删除全部合同时发送 contracts: [];未加载合同或不处理合同时省略字段,不要误发空数组。
  5. 所有 ID 均作为字符串保存、比较和回传,不经过 Number 转换。
  6. 本次不新增接口路径、权限点或业务错误码;旧客户端省略 contracts 时继续可用。
  7. 管理端源码不在本后端工单中修改,前端状态保持 pending,直至完成页签、标题和表格接入并提供前端引用。

七、不影响范围

  • 不新增接口路径、权限点或业务错误码;旧客户端省略 contracts 时行为完全不变。
  • initialAccounts 字段名、类型与语义不变(仅页面展示标题由「初始账户」改「结算信息」,属前端文案)。
  • 请求侧 contracts[].amount 仍按 Number 传,PR #6450 只改响应输出,不改请求解析。
  • 供应商其余模块(资源信息、审批记录、账户证明)的接口与字段不受影响。
  • 数据库结构无变更(复用既有快照列),无 Redis/MQ 行为变化。

八、测试环境已验证

  • 自动化:供应商定向测试 115 项通过;hl-resource-service 全量 2,111 项,0 失败、0 错误,38 项条件跳过;hl-verify 与差异检查通过。
  • 部署:Deploy Panel API 任务 af3f205b 终态 success、退出码 0、has_build_error=false,未发现 Maven、编译或滚动发布错误。
  • 健康:hl-resource-service 的 8082、8182 双实例运行;Nacos test 命名空间两实例均 healthy=true、enabled=true。
  • 真实 Gateway:23 项断言通过,覆盖合同随草稿创建、详情完整回显、字符串 ID、创建携带 ID 失败、普通 ADMIN 越权、提交外部合同 ID 完整回滚、日期逆序零写入和旧客户端省略 contracts。
  • 清理:两个临时 DRAFT 均通过业务删除接口软删除并回读为不存在;仅保留不可逆的 CREATE/DELETE 操作审计。
  • 环境限制:部署前锁定 origin/dev-v3=1a16a5aec7d0b95ec87e6fb222581060a3135984,但面板 Git API 回读到服务器本地短提交 6f7d3ca78,Gitea 无法解析该对象。接口行为已真实验证,后端工单仍等待测试环境恢复精确远端提交后复验,不能据此宣称最终交付完成。

撤回

  1. 从最新 dev-v3 创建回退分支,执行 git revert -m 1 --no-edit 1a16a5aec7d0b95ec87e6fb222581060a3135984,经独立 PR 合入。
  2. 重新滚动部署 hl-resource-service;无需执行数据库结构、配置、Redis 或 MQ 恢复。
  3. 管理端停止发送和读取 contracts,恢复原页面结构;initialAccounts 契约始终不变。
  4. 已保存的合同资料保留,不做破坏性批量清理;回退后旧客户端继续按省略 contracts 的路径工作。
  5. 经 Gateway 复测创建、提交、基础信息、未认证、越权、非法合同 ID、失败零写入和旧客户端兼容,并确认双实例与 Nacos 健康。

十、相关文档

  • 设计/API 说明:仓库 docs/supplier/API-CHANGE-6397.html
  • 同日修正 PR:#6450(响应 contracts[].amount Number→String,每日审查红线修复)
  • 供应商暂停/拉黑原因必填(同属供应商状态域):changelogs-v2/2026-08/26_6392_供应商暂停合作与拉黑原因必填接口-新增接口-管理后台.md

关联 / 联系人