--- schema: "hl-changelog/v2" ticket: "6304" title: "供应商资质增加永久有效与到期剩余天数" consumer: "admin" author: "lc(GIT)" change_type: "修改接口" backend_status: "deployed" gateway_status: "verified" frontend_status: "verified" frontend_owner: "mmg" frontend_ref: "25fb4b27" target_release: "" verified_at: "2026-08-25" 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`。业务校验失败可能仍返回 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