docs(api): 发布供应商资质有效期契约
changelog-filename-gate / validate (push) Successful in 2s
changelog-filename-gate / validate (pull_request) Successful in 2s

这个提交包含在:
lc
2026-08-25 16:23:36 +08:00
父节点 c3acb6e822
当前提交 e1b0a667cd
共修改 2 个文件,包含 444 行新增和 23 行删除
@@ -0,0 +1,419 @@
---
schema: "hl-changelog/v2"
ticket: "6304"
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 #6342 与补充修复 PR #6349 均已合并 dev-v3,TEST 最终部署提交为 af3a7bae5。真实 SUPER_ADMIN 经 Gateway 已验证显式永久有效、395042 冲突零写入、历史省略兼容和数据清理;未认证请求仍返回标准 401 业务响应。管理端尚待适配,frontend_status 保持 pending。"
updated_at: "2026-08-25"
base: "dev-v3"
---
# 供应商资质增加永久有效与到期剩余天数
供应商资质的有效期输入改为显式的“永久有效”契约;非永久有效资质必须提交到期日期。详情按上海时区自然日返回有符号剩余天数,便于页面直接展示“剩余 N 天”“今日到期”或“已过期 N 天”。
> **服务**: `hl-resource-service`
> **PR**: #6342、补充修复 #6349
> **Issue**: #6304、补充修复 #6347
> **日期**: 2026-08-25
> **影响范围**: 管理端供应商新建、编辑、提交审批和基本信息详情的资质证照区域
## ⚠️ 关键变化
- 资质写入字段新增 `permanentValid`;`true` 时不得提交 `expiryDate`,`false` 时必须提交 `expiryDate`。
- 详情资质项新增 `daysUntilExpiry`;永久有效时返回 `null`,有限期按 `Asia/Shanghai` 自然日返回正数、零或负数。
- 既有只读字段 `isRequired` 仍表示“该供应商类型是否要求此资质”,没有改义,也不能作为永久有效标识。
## 一、变更接口清单
| # | 接口 | 方法 | 路径 | 变化 |
|---|---|---|---|---|
| 1 | 创建供应商注册草稿 | POST | `/admin/supplier/items/add` | `qualifications[].permanentValid` 可写 |
| 2 | 更新供应商 | PUT | `/admin/supplier/items/{supplierId}/update` | `qualifications[].permanentValid` 可写,并支持增量保留语义 |
| 3 | 提交供应商注册审批 | POST | `/admin/supplier/items/{supplierId}/submit` | `qualifications[].permanentValid` 可写 |
| 4 | 查询供应商基本信息 | GET | `/admin/supplier/items/{supplierId}/basic-info/view` | `qualifications[]` 新增 `daysUntilExpiry` |
统一响应仍为 `Result<T>`。业务校验失败可能仍返回 HTTP 200,调用方必须同时判断 `code`、`success` 和 `data`。
## 二、字段契约
### 2.1 写入字段
| 字段 | 位置 | JSON 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `permanentValid` | `qualifications[]` | Boolean | 条件必填 | `true` 表示永久有效,`false` 表示存在到期日;历史调用方可省略并触发兼容推导 |
| `expiryDate` | `qualifications[]` | String / null | 条件必填 | 格式 `yyyy-MM-dd`;仅 `permanentValid=false` 时必须有值 |
合法组合:
| 场景 | `permanentValid` | `expiryDate` | 结果 |
|---|---:|---|---|
| 永久有效 | `true` | 省略或 `null` | 接受,到期日为空 |
| 有到期日 | `false` | `yyyy-MM-dd` | 接受,保存该到期日 |
| 历史永久请求 | 省略 | 省略或 `null` | 接受,兼容推导为 `true` |
| 历史有限期请求 | 省略 | `yyyy-MM-dd` | 接受,兼容推导为 `false` |
| 矛盾组合 | `true` | `yyyy-MM-dd` | 拒绝,业务码 `395042` |
| 缺少到期日 | `false` | 省略或 `null` | 拒绝,业务码 `395042` |
更新既有资质时还有以下增量规则:
- `permanentValid` 与 `expiryDate` 均省略:保留当前有效期,不做修改。
- 仅提交非空 `expiryDate`:兼容推导 `permanentValid=false`。
- 显式提交 `permanentValid=true` 且不提交到期日:切换为永久有效并清空原到期日。
- 新增资质且两字段均省略:按历史兼容口径创建为永久有效。
### 2.2 详情响应字段
| 字段 | JSON 类型 | 空值 | 说明 |
|---|---|---|---|
| `permanentValid` | Boolean | 不为空 | 由 `expiryDate` 是否为空动态推导 |
| `daysUntilExpiry` | Integer / null | 永久有效时为 `null` | 到期日相对上海业务当日的有符号自然日差 |
| `expired` | Boolean | 不为空 | 仅当 `daysUntilExpiry < 0` 时为 `true` |
| `isRequired` | Boolean | 不为空 | 历史只读兼容字段;继续表示供应商类型规则是否要求该资质 |
以 2026-08-25(上海日期)为例:
| `expiryDate` | `permanentValid` | `daysUntilExpiry` | `expired` |
|---|---:|---:|---:|
| `null` | `true` | `null` | `false` |
| `2026-08-26` | `false` | `1` | `false` |
| `2026-08-25` | `false` | `0` | `false` |
| `2026-08-24` | `false` | `-1` | `true` |
## 三、接口详情
### 3.1 创建供应商注册草稿 `POST /admin/supplier/items/add`
使用场景:新建供应商草稿时保存资质证照。`permanentValid` 位于每个 `qualifications[]` 项内。
典型请求:
```json
{
"fullName": "示例酒店管理有限公司",
"shortName": "示例酒店",
"taxNo": "91110108MA00000001",
"types": [
{
"typeCode": "HOTEL"
}
],
"mainCooperation": "酒店住宿服务",
"qualifications": [
{
"qualType": "BUSINESS_LICENSE",
"certNo": "LIC-2026-0001",
"imageUrl": "https://files.example.test/supplier/license-1.jpg",
"permanentValid": false,
"expiryDate": "2027-08-25"
}
]
}
```
成功响应:
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": {
"supplierId": "2092152294748352514",
"supplierNo": null,
"status": "DRAFT",
"onboardingStage": "PROFILE_DRAFT",
"initialAccounts": [],
"updateTime": "2026-08-25 15:21:00"
}
}
```
永久有效请求只需将资质项改为:
```json
{
"qualType": "BUSINESS_LICENSE",
"certNo": "LIC-2026-PERM",
"imageUrl": "https://files.example.test/supplier/license-perm.jpg",
"permanentValid": true,
"expiryDate": null
}
```
### 3.2 更新供应商 `PUT /admin/supplier/items/{supplierId}/update`
使用场景:编辑既有资质或切换永久有效状态。`qualifications` 一旦传入,仍代表资质集合的完整当前快照;既有项必须携带 `qualificationId` 和该项的 `expectedUpdateTime`。
将有限期资质切换为永久有效:
```json
{
"qualifications": [
{
"qualificationId": "2092152300000000001",
"qualType": "BUSINESS_LICENSE",
"certNo": "LIC-2026-0001",
"imageUrl": "https://files.example.test/supplier/license-1.jpg",
"permanentValid": true,
"expiryDate": null,
"expectedUpdateTime": "2026-08-25 15:21:00"
}
],
"changeReason": "营业执照变更为永久有效",
"expectedUpdateTime": "2026-08-25 15:21:00"
}
```
成功响应:
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": {
"supplierId": "2092152294748352514",
"supplierNo": null,
"status": "DRAFT",
"onboardingStage": "PROFILE_DRAFT",
"initialAccounts": [],
"updateTime": "2026-08-25 15:35:00"
}
}
```
若本次不修改既有资质的有效期,可同时省略该项的 `permanentValid` 和 `expiryDate`;不要回传未经确认的默认值。
### 3.3 提交供应商注册审批 `POST /admin/supplier/items/{supplierId}/submit`
使用场景:提交完整供应商表单。资质有效期组合规则与创建草稿完全一致。
典型请求:
```json
{
"fullName": "示例酒店管理有限公司",
"shortName": "示例酒店",
"taxNo": "91110108MA00000001",
"types": [
{
"typeCode": "HOTEL"
}
],
"mainCooperation": "酒店住宿服务",
"licenseImageUrl": "https://files.example.test/supplier/license-1.jpg",
"qualifications": [
{
"qualificationId": "2092152300000000001",
"qualType": "BUSINESS_LICENSE",
"certNo": "LIC-2026-0001",
"imageUrl": "https://files.example.test/supplier/license-1.jpg",
"permanentValid": false,
"expiryDate": "2027-08-25"
}
],
"expectedUpdateTime": "2026-08-25 15:35:00"
}
```
成功响应:
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": {
"approvalLogId": "2092152400000000001",
"requestNo": "SUP-PROFILE-20260825-0001",
"provider": "LOCAL_AUTO",
"approvalStatus": "APPROVED",
"spNo": null,
"spStatus": null,
"syncStatus": "APPLIED",
"submittedAt": "2026-08-25 15:36:00",
"finishedAt": "2026-08-25 15:36:00"
}
}
```
### 3.4 查询供应商基本信息 `GET /admin/supplier/items/{supplierId}/basic-info/view`
使用场景:展示供应商详情的资质证照。请求需要有效的管理端 `Authorization`,无请求体。
```http
GET /admin/supplier/items/2092152294748352514/basic-info/view
Authorization: Bearer <有效管理端令牌>
```
有限期资质成功响应(假设上海日期为 2026-08-25):
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": {
"supplierId": "2092152294748352514",
"supplierNo": null,
"fullName": "示例酒店管理有限公司",
"shortName": "示例酒店",
"tax_no": "9111********0001",
"legalRepresentative": null,
"contactPhoneMask": null,
"establishDate": null,
"registeredCapital": null,
"businessScope": null,
"staffScale": null,
"mainCooperation": "酒店住宿服务",
"status": "DRAFT",
"creditLevel": "B",
"totalScore": null,
"types": [
{
"typeCode": "HOTEL",
"typeName": "酒店"
}
],
"contacts": [],
"qualifications": [
{
"qualificationId": "2092152300000000001",
"qualType": "BUSINESS_LICENSE",
"qualTypeName": "营业执照",
"certNoMask": "LIC****0001",
"imageUrl": "https://files.example.test/supplier/license-1.jpg",
"expiryDate": "2026-08-26",
"permanentValid": false,
"daysUntilExpiry": 1,
"validityStatus": "VALID",
"validityStatusName": "有效",
"isRequired": true,
"expired": false,
"updateTime": "2026-08-25 15:21:00"
}
],
"updateTime": "2026-08-25 15:21:00"
}
}
```
永久有效资质的差异字段为:
```json
{
"expiryDate": null,
"permanentValid": true,
"daysUntilExpiry": null,
"validityStatus": "VALID",
"validityStatusName": "有效",
"expired": false
}
```
## 四、错误响应
新增业务错误码:
| 业务码 | 消息 | 触发条件 | 写入结果 |
|---:|---|---|---|
| `395042` | 永久有效标记与到期日期不一致 | `permanentValid=true` 且到期日非空,或 `permanentValid=false` 且到期日为空 | 整笔请求失败,零写入 |
异常请求片段:
```json
{
"qualifications": [
{
"qualType": "BUSINESS_LICENSE",
"permanentValid": true,
"expiryDate": "2027-08-25"
}
]
}
```
错误响应:
```json
{
"code": 395042,
"message": "永久有效标记与到期日期不一致",
"success": false,
"data": null
}
```
既有错误码继续有效,包括资质类型无效、证照编号无效、必备资质缺失、资质过期和并发版本冲突。
## 五、修改前后对比
### 字段级对比
| 字段 | 修改前 | 修改后 |
|---|---|---|
| 写入 `qualifications[].permanentValid` | 无显式写入契约,只能由到期日是否为空隐式表达 | 可显式提交;省略时仍兼容历史请求 |
| 响应 `qualifications[].daysUntilExpiry` | 无 | 有限期返回有符号整数,永久有效返回 `null` |
| 响应 `qualifications[].isRequired` | 供应商类型是否要求此资质 | 语义不变,继续只读返回 |
### 行为级对比
| 行为 | 修改前 | 修改后 |
|---|---|---|
| 永久有效输入 | 仅通过不传到期日表达 | 优先显式提交 `permanentValid=true`;旧请求仍兼容 |
| 非永久有效输入 | 提交到期日 | 提交 `permanentValid=false` 与到期日 |
| 到期提示 | 调用方自行按日期计算 | 直接读取 `daysUntilExpiry`,无需自行处理时区和跨日 |
## 六、管理端接入事项
1. 将资质证照区域的“是否必备”编辑控件改为“永久有效”;提交字段必须使用 `permanentValid`,不得复用或改写 `isRequired`。
2. `permanentValid=true` 时清空并禁用 `expiryDate`;`false` 时要求用户选择到期日。
3. 非永久有效时在到期日期附近显示后端返回的 `daysUntilExpiry`:正数表示剩余天数,零表示今日到期,负数表示已过期天数。
4. 永久有效时不显示到期天数。
5. 编辑既有数据以详情返回的 `permanentValid` 回显;不要从 `isRequired` 推导。
## 七、兼容性与影响评估
- **是否破坏向后兼容**: 否。旧客户端省略 `permanentValid` 时仍按 `expiryDate` 兼容推导。
- **前端是否必须同步上线**: 是。当前界面尚未提交新字段,也未展示剩余天数。
- **前端 workaround 清理点**: 若页面存在本地到期天数计算,改为直接消费 `daysUntilExpiry`。
- 不新增接口、Gateway 路由、权限点、数据库迁移、配置、Redis 或 MQ 契约。
- 不改变供应商状态机、审批语义、资质必备规则、证照编号脱敏、软删除和数据范围。
## 八、TEST 验证证据
- 原实现 PR #6342 与补充修复 PR #6349 均已合并;远端精确部署分支 `deploy/6347-supplier-qualification-null-expiry` 的 HEAD 与最终 `dev-v3` 合并提交 `af3a7bae5f3d19b97a7ad52daf31bb8fd18d5676` 完全一致,TEST 双实例健康。
- 真实 `SUPER_ADMIN` 经 Gateway 将既有有限期资质切换为永久有效后,更新成功;重新查询返回 `expiryDate=null`、`permanentValid=true`、`daysUntilExpiry=null`、`expired=false`、`validityStatus=VALID`。
- 同一资质提交 `permanentValid=true` 与非空 `expiryDate` 时返回业务码 `395042`、消息“永久有效标记与到期日期不一致”;失败前后供应商和资质的 `updateTime` 均保持 `2026-08-25 16:19:20`,证明零写入。
- 历史客户端省略 `permanentValid`、仅提交 `expiryDate=2026-08-26` 时更新成功;详情兼容返回 `permanentValid=false`、`daysUntilExpiry=1`、`expired=false`。
- 本工单创建的 DRAFT 测试供应商已通过业务删除接口清理;按精确测试名称分页查询 `total=0`,按原 ID 查询返回 `395001 供应商不存在`。
- 未认证访问供应商入口返回业务码 `401`、`success=false`、`data=null`,证明请求仍经 Gateway 认证门禁。
- 原实现定向 80 项零失败、Resource 全量 2058 项零失败(38 项条件跳过);补充修复定向 46 项零失败、Resource 全量 2059 项零失败(38 项条件跳过)。离线 HTML 自检通过,文档仓库 `npm test` 46 项零失败。
- 管理端适配不属于本后端工单,`frontend_status` 保持 `pending`,不把后端契约验证误记为前端已完成。
## 九、撤回
1. 从最新 `dev-v3` 创建回退分支,先执行 `git revert -m 1 --no-edit af3a7bae5f3d19b97a7ad52daf31bb8fd18d5676` 撤回补充修复,再执行 `git revert -m 1 --no-edit c5b1a84aea6b1c7beb7a626131376355008e662c` 撤回原实现;分别审查并经独立 PR 合入。
2. 重新构建并滚动部署 `hl-resource-service`;本次无数据库、配置、Redis 或 MQ 变更,无需执行 DDL、DML 或数据恢复。
3. 回退前确认管理端不再提交 `permanentValid` 或读取 `daysUntilExpiry`;旧字段 `expiryDate`、`expired` 和 `isRequired` 以及存量数据无需迁移。
4. 回退后经 Gateway 复测草稿创建、更新、提交、详情、未认证和永久/有限期边界,并确认资质必备规则与证照脱敏保持原行为。
## 关联 / 联系人
- **Issue**: [#6304](https://git.1814.love:8443/wx/HL/issues/6304)
- **PR**: [#6342](https://git.1814.love:8443/wx/HL/pulls/6342)
- **合并提交**: [c5b1a84ae](https://git.1814.love:8443/wx/HL/commit/c5b1a84aea6b1c7beb7a626131376355008e662c)
- **补充 Issue**: [#6347](https://git.1814.love:8443/wx/HL/issues/6347)
- **补充 PR**: [#6349](https://git.1814.love:8443/wx/HL/pulls/6349)
- **最终部署提交**: [af3a7bae5](https://git.1814.love:8443/wx/HL/commit/af3a7bae5f3d19b97a7ad52daf31bb8fd18d5676)
- **后端负责人**: @lc