diff --git a/changelogs-v2/2026-08/27_6476_供应商详情补齐地址备注并统一表单校验-修改接口-管理后台.md b/changelogs-v2/2026-08/27_6476_供应商详情补齐地址备注并统一表单校验-修改接口-管理后台.md new file mode 100644 index 00000000..b21a797e --- /dev/null +++ b/changelogs-v2/2026-08/27_6476_供应商详情补齐地址备注并统一表单校验-修改接口-管理后台.md @@ -0,0 +1,347 @@ +--- +schema: "hl-changelog/v2" +ticket: "6476" +title: "供应商详情补齐地址备注并统一表单校验" +consumer: "admin" +author: "lc(GIT)" +change_type: "修改接口" +backend_status: "deployed" +gateway_status: "verified" +frontend_status: "pending" +frontend_owner: "mmg" +frontend_ref: "" +target_release: "v2.1" +verified_at: "2026-08-27" +status_note: "PR #6517 已合并 dev-v3(合并提交 a952a04c);hl-resource-service 已随 dev-v3 精确提交 b269e5fd 由 Deploy Panel 任务 938ca09e 发布 TEST。真实 TEST 身份已验证 address/remark 创建、详情、更新回显,创建/更新同口径校验、权限失败零写入及测试数据清理。" +updated_at: "2026-08-27" +base: "dev-v3" +--- + +# 供应商管理:详情补齐地址备注并统一表单校验 + +供应商详情接口现在完整返回主体的 `address` 和 `remark`,管理端可用同一份详情快照初始化查看页与编辑表单。创建、提交和更新原本已有的业务字段与校验未增加新的必填项;本工单用契约测试和真实 TEST 验收固定三条写链路的一致口径。 + +本次没有新增接口路径、权限点或业务错误码。 + +## 一、背景 + +创建和更新请求已经接受地址、备注等主体字段,但基础信息详情此前没有回传 `address`、`remark`,导致管理端打开编辑页时无法完整还原已保存表单。同时,创建、提交和更新分属不同请求模型,消费方需要一个明确、可验证的共同校验口径。 + +本次处理后: + +- 详情响应补齐主体地址与内部备注。 +- 创建、提交、更新对共同主体字段、类型、联系人、资质和合同继续执行同一业务规则。 +- 更新只额外要求变更原因、主体并发版本,以及被修改子项的 ID/版本。 +- #6436/#6499 已交付的授权完整值语义保持不变,不重新引入脱敏值或“留空表示保留旧敏感值”的旧约定。 + +## 二、变更接口清单 + +| # | 接口 | 方法 | 路径 | 变更类型 | 说明 | +|---:|---|---|---|---|---| +| 1 | 查询供应商基本信息 | GET | `/admin/supplier/items/{supplierId}/basic-info/view` | 响应新增字段 | 根对象新增 `address`、`remark`,用于完整初始化查看与编辑表单 | + +创建草稿、提交注册和更新资料的路径、请求字段及错误码没有结构性变化,因此不作为新增接口列入上表;其稳定调用规则在第四节集中说明。 + +## 三、接口详情 + +### 1. 查询供应商基本信息 `GET /admin/supplier/items/{supplierId}/basic-info/view` + +**VO**: `SupplierBasicInfoRespVO` + +#### 使用场景 + +管理端进入供应商详情或编辑页时调用。响应是主体、类型、联系人、资质、合同及并发版本的完整快照;编辑页应直接使用其中的 `address`、`remark` 和 `updateTime`,不得用本地空值覆盖。 + +#### 入参 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|---|---|---|---:|---|---| +| `supplierId` | Path | String | 是 | 正整数 ID 字符串 | 目标供应商;不得转为 JavaScript Number | + +#### 出参 `Result` + +| 字段 | 类型 | 说明 | +|---|---|---| +| `supplierId` | String | 供应商 ID | +| `fullName` / `shortName` | String/null | 供应商全称与简称 | +| `tax_no` | String | 完整主体证件号;JSON 字段名保持 `tax_no` | +| `legalRepresentative` | String/null | 法定代表人姓名 | +| `legalRepresentativeIdNo` | String/null | 完整法人居民身份证号 | +| `legalRepresentativeIdCardFrontUrl` / `legalRepresentativeIdCardBackUrl` | String/null | 法人证件正反面永久文件地址 | +| `contactPhone` | String/null | 完整公司联系电话 | +| `establishDate` | String/null | 成立日期,格式 `yyyy-MM-dd` | +| `registeredCapital` / `businessScope` | String/null | 注册资本与经营范围 | +| `address` | String/null | 本次新增回显:注册地址或经营地址 | +| `staffScale` | String/null | 人员规模字典值 | +| `mainCooperation` | String | 主要合作内容 | +| `remark` | String/null | 本次新增回显:供应商主体内部备注 | +| `types` | Array | 类型完整集合;每项包含 `isPrimary` | +| `primaryTypeCode` / `primaryTypeName` | String/null | 当前主类型 | +| `contacts` / `qualifications` / `contracts` | Array | 联系人、资质和合同完整集合;现有项包含字符串 ID 与 `updateTime` | +| `status` | String | 当前供应商状态 | +| `updateTime` | String | 主体并发版本,格式 `yyyy-MM-dd HH:mm:ss` | + +#### 请求示例 + +```http +GET /admin/supplier/items/2092800000000000001/basic-info/view +Authorization: Bearer +``` + +#### 响应示例 + +```json +{ + "code": 200, + "message": "成功", + "success": true, + "data": { + "supplierId": "2092800000000000001", + "fullName": "示例旅行服务有限公司", + "shortName": "示例旅行", + "tax_no": "91350211M000100Y46", + "legalRepresentative": "示例法人", + "legalRepresentativeIdNo": "11010519491231002X", + "legalRepresentativeIdNoMask": "11010519491231002X", + "legalRepresentativeIdCardFrontUrl": "https://files.example.com/supplier/id-front.jpg", + "legalRepresentativeIdCardBackUrl": "https://files.example.com/supplier/id-back.jpg", + "contactPhone": "13800138000", + "contactPhoneMask": "13800138000", + "establishDate": "2020-01-01", + "registeredCapital": "100万元", + "businessScope": "境内旅游服务", + "address": "福建省厦门市示例路 1 号", + "staffScale": "LT50", + "mainCooperation": "酒店与景区资源合作", + "remark": "重点合作供应商", + "types": [{"typeCode": "HOTEL", "typeName": "酒店", "isPrimary": true}], + "primaryTypeCode": "HOTEL", + "primaryTypeName": "酒店", + "contacts": [], + "qualifications": [], + "contracts": [], + "status": "DRAFT", + "updateTime": "2026-08-27 15:30:00" + } +} +``` + +#### 空数据 / 降级响应 + +历史供应商未填写地址或备注时,字段明确返回 `null`;无类型、联系人、资质或合同时,相应集合返回空数组,不把缺失数据当成接口异常: + +```json +{ + "code": 200, + "message": "成功", + "success": true, + "data": { + "supplierId": "2092800000000000001", + "address": null, + "remark": null, + "types": [], + "primaryTypeCode": null, + "primaryTypeName": null, + "contacts": [], + "qualifications": [], + "contracts": [], + "updateTime": "2026-08-27 15:30:00" + } +} +``` + +#### 错误响应 + +供应商不存在、已删除或超出数据范围时不返回空详情: + +```json +{ + "code": 395001, + "message": "供应商不存在", + "success": false, + "data": null +} +``` + +#### 业务边界 + +- 要求可信管理身份、`supplier:view` 平台权限和供应商数据范围;登录态不能代替业务权限。 +- `address`、`remark` 属于响应的向后兼容增加;旧客户端忽略未知字段即可继续运行。 +- `legalRepresentativeIdNoMask`、`contactPhoneMask` 等废弃兼容别名继续与完整值字段同值;新代码使用无 `Mask` 字段。 +- Long ID 一律按字符串保存、比较和传输。 +- 本接口只读,不修改主体版本、缓存或审批状态。 + +## 四、契约约束与正确调用方式 + +以下三个既有写接口没有新增路径或字段,但共同业务口径由本工单固定: + +- 创建草稿:`POST /admin/supplier/items/add` +- 提交注册:`POST /admin/supplier/items/{supplierId}/submit` +- 更新资料:`PUT /admin/supplier/items/{supplierId}/update` + +### 共同主体字段规则 + +| 字段 | 创建/提交 | 更新 | 统一约束 | +|---|---|---|---| +| `fullName` | 必填 | 可选 | 最长 500 字符 | +| `shortName` | 可选 | 可选 | 最长 300 字符 | +| `taxNo` | 必填 | 可选,且仅允许在状态规则内修改 | 6 至 64 位字母、数字或展示分隔符;身份证/统一社会信用代码还校验日期或校验位 | +| `legalRepresentative` | 可选 | 可选 | 最长 500 字符 | +| `legalRepresentativeIdNo` | 可选 | 可选 | 18 位合法居民身份证号;与正反面地址按完整三字段组合校验 | +| `legalRepresentativeIdCardFrontUrl` / `legalRepresentativeIdCardBackUrl` | 可选 | 可选 | 公网 HTTPS 永久文件地址,单项最长 1000 字符 | +| `contactPhone` | 可选 | 可选 | 7 至 20 位合法电话字符 | +| `establishDate` | 可选 | 可选 | `yyyy-MM-dd`,不得晚于当前日期 | +| `registeredCapital` | 可选 | 可选 | 最长 50 字符 | +| `businessScope` | 可选 | 可选 | 最长 500 字符 | +| `address` | 可选 | 可选 | 最长 500 字符;创建和更新同口径 | +| `staffScale` | 可选 | 可选 | `LT50`、`R50_200`、`R200_500`、`GT500` | +| `mainCooperation` | 必填 | 可选 | 创建/提交不能为空白;更新传值时按同一业务含义保存 | +| `licenseImageUrl` | 可选 | 可选 | 最长 500 字符,并与营业执照资质归一化 | +| `remark` | 可选 | 可选 | 供应商主体内部备注,不等同于审批意见或 `changeReason` | + +集合上限与语义保持不变:`types` 最多 15 项;`contacts`、`qualifications`、`contracts` 各最多 100 项;`initialAccounts` 最多 1 项。非空类型必须来自生效字典且不得重复,非空联系人集合必须形成唯一默认联系人,资质和合同继续执行类型、日期、金额、归属和完整快照校验。 + +更新场景额外要求: + +- `changeReason` 必填、非空白、最长 500 字符,并进入业务审计。 +- `expectedUpdateTime` 必须等于详情最新 `updateTime`。 +- 已有联系人、资质、合同随完整快照更新时,必须带回对应字符串 ID 和子项 `updateTime`。 + +### ✅ 正确 / ❌ 错误 payload 对照 + +| 场景 | payload / 结果 | +|---|---| +| ✅ 更新地址和备注 | `{"address":"福建省厦门市示例路 2 号","remark":"地址已确认","changeReason":"更新地址和备注","expectedUpdateTime":"2026-08-27 15:30:00"}` | +| ✅ 只更新备注 | `{"remark":"新的内部备注","changeReason":"补充内部备注","expectedUpdateTime":"2026-08-27 15:30:00"}` | +| ❌ 地址超过 500 字符 | 创建、更新均返回 `400`,零写入 | +| ❌ 成立日期晚于当前日期 | 创建、更新均返回 `400`,零写入 | +| ❌ 更新缺少 `changeReason` | 返回 `400`,零写入 | +| ❌ 使用旧 `expectedUpdateTime` | 返回 `395014`,零写入;必须刷新详情后由用户确认 | + +### 正确更新顺序 + +1. 先查询详情,保存字符串 ID、子项版本和根对象 `updateTime`。 +2. 用户提交时只发送实际变更字段,并附非空 `changeReason`、最新 `expectedUpdateTime`。 +3. 更新成功后采用响应里的新版本;需要完整表单时重新查询详情。 +4. 遇到 `395014` 先刷新,不得自动用旧表单覆盖他人修改。 + +## 五、数据库行为 + +- 成功创建、提交或更新时,主体与本次携带的类型、联系人、资质、合同及业务审计按聚合事务一起成功。 +- 地址和备注保存后,详情读取与变更审计使用同一业务值;`remark` 不会被提升为审批意见或变更原因。 +- 请求校验、权限、状态、归属或并发版本失败时零写入,主体 `updateTime` 不变化。 +- 本次没有数据库结构变更、Flyway migration 或历史数据回填;旧记录的地址/备注原值保持不变。 +- 不新增 Redis、MQ、Feign 或跨服务副作用。 + +## 六、边界行为 + +- 未登录或 Token 失效:`401`,不把无认证响应当作正向验收。 +- ADMIN 等无写权限角色调用创建、提交或更新:`395002`,零写入;已有详情仍按读取权限返回。 +- 供应商不存在或已删除:`395001`。 +- 并发版本过期:`395014`,调用方刷新详情后重提。 +- 地址超过 500 字符、未来成立日期、必填字段缺失或组合不完整:`400`,零写入。 +- 历史 `address`、`remark` 为空:详情返回 `null`,不报错、不猜测默认值。 +- `ARCHIVED` 等不可修改状态继续由既有状态门禁拒绝更新。 +- 业务失败可能仍使用 HTTP 200,调用方必须同时判断响应体 `code`、`success`、`data`。 + +## 六.5、枚举 / 数据字典 + +### `staffScale`(供应商人员规模稳定值) + +**所属字段**: `SupplierDraftSaveReqVO.staffScale / SupplierUpdateReqVO.staffScale / SupplierBasicInfoRespVO.staffScale` | **类型**: `String` + +| 值 | 中文 | 说明 | +|---|---|---| +| `LT50` | 50 人以下 | 小于 50 人 | +| `R50_200` | 50 至 200 人 | 50 至 200 人档 | +| `R200_500` | 200 至 500 人 | 200 至 500 人档 | +| `GT500` | 500 人以上 | 大于 500 人 | + +`types[].typeCode`、`contacts[].contactRole`、`qualifications[].qualType` 继续从对应生效数据字典读取,不在客户端硬编码展示名。 + +## 六.6、修改前后对比 + +### 字段级对比 + +| 字段 | 改前 | 改后 | +|---|---|---| +| 详情根对象 `address` | 未返回,已保存值无法用于表单回填 | 返回保存的地址;未填写为 `null` | +| 详情根对象 `remark` | 未返回,已保存值无法用于表单回填 | 返回主体内部备注;未填写为 `null` | +| 创建/提交/更新请求字段 | 已存在 | 无新增字段、无新增必填项 | + +### 行为级对比 + +| 行为 | 改前 | 改后 | +|---|---|---| +| 打开编辑页 | 详情缺少地址和备注,表单初始化不完整 | 一次详情调用可完整初始化主体字段 | +| 共同字段校验 | 实现存在,但缺少跨创建/更新契约锁定 | 自动化和 TEST 验收固定创建/更新同口径 | +| 完整敏感值回显 | 按 #6436/#6499 返回授权完整值 | 保持不变 | + +## 六.7、影响评估 + +- **是否破坏向后兼容**: 否。响应新增两个可空字段,旧客户端可忽略;请求无新增必填项。 +- **前端是否必须同步上线**: 为闭合编辑体验需要接入 `address`、`remark`,但不构成旧客户端继续运行的硬门禁。 +- **前端 workaround 清理点**: 删除编辑页对地址、备注的本地空值兜底,改为使用详情真实值。 +- **QA 重点**: 新建后打开详情、更新后刷新详情、地址长度 500/501、今天/未来成立日期、ADMIN 越权、旧版本并发冲突。 + +## 七、不影响范围 + +- **仅影响**: 管理后台供应商详情/编辑表单的主体地址、内部备注回填,以及已有写接口校验口径的契约固定。 +- **零影响**: + - Gateway 路由和认证级别。 + - 供应商生命周期状态机、审批、收款账户、证明附件、资源关系及归档语义。 + - 数据库结构、历史数据、Nacos 配置、Redis、MQ、Feign 和其他服务。 + - 小程序、C 端、Web、H5、桌面端接口。 + - #6436/#6499 的授权完整值和废弃 `*Mask` 兼容语义。 + +## 八、测试环境已验证 + +- 本地自动化:供应商定向测试 49 项通过;最新 `dev-v3` 上 `hl-resource-service` Reactor 全量 2,153 项,0 失败、0 错误、38 项条件跳过。 +- 部署:Deploy Panel API 任务 `938ca09e` 终态 `success`;目标与实际提交均为 `b269e5fdca2e1d29e0336314a909885e9d1f9d16`,包含本工单合并提交 `a952a04c`。 +- 采样:部署任务期 8 个有效采样;每个采样至少 2 个运行进程、2 个健康启用 Nacos 实例,零观测不可用采样。该证据只证明采样点,不代表采样间绝对连续。 +- 真实 Gateway:SUPER_ADMIN 创建完整表单后,详情精确回显 `address`、`remark`、完整授权字段、主类型及联系人/资质/合同 ID 和版本;更新后 API、数据库主体与更新审计一致。 +- 失败路径:创建/更新的地址超长均返回 `400`;创建/更新的未来成立日期均返回 `400`;ADMIN 更新返回 `395002`;所有失败均验证零写入、版本不变。 +- 清理:临时供应商先经业务删除接口软删除,确认主体已删除且活动子项为 0;随后按本次唯一标记精确清理 8 行测试数据,17 张关联表回读,剩余供应商 0 行;测试会话已注销并验证失效。 + +## 九、相关历史 PR + +| PR | Issue | 说明 | 是否仍有效 | +|---|---|---|---| +| #6478 / #6483 / #6486 | #6436 | 供应商授权完整值、主类型及同日修正 | ✅ 有效,本次保持 | +| #6517 | #6476 | 详情补齐地址备注并固定表单校验契约 | ✅ 最新 | + +## 十、相关文档 + +- 关联 Issue:[#6476](https://git.1814.love:8443/wx/HL/issues/6476) +- 关联 PR:[#6517](https://git.1814.love:8443/wx/HL/pulls/6517) +- 相关完整值契约:`changelogs-v2/2026-08/27_6436_供应商敏感字段完整回显与主类型-修改接口-管理后台.md` +- 管理端接入:读取详情 `address`、`remark`,更新时保留最新 `expectedUpdateTime`;前端引用待回填。 + +## 撤回 + +1. 从最新 `dev-v3` 创建独立回退分支,执行: + + ```bash + git revert -m 1 --no-edit a952a04cf80f24d6c50cbec4b992c82f8580beba + ``` + +2. 运行供应商定向测试、`hl-resource-service` 全量测试和差异检查,经独立 PR 合入。 +3. 使用同一 Deploy Panel 两阶段客户端,仅滚动部署 `hl-resource-service`,并绑定批准回退分支的精确提交。 +4. 管理端停止依赖详情响应中的 `address`、`remark`,再撤回本 Changelog;既有请求字段和完整值契约保持不变。 +5. 无数据库、配置、Redis 或 MQ 恢复步骤;已保存的地址和备注数据保留,不做破坏性回填或删除。 +6. 撤回后经 Gateway 复测详情、创建、更新、未认证、越权、并发冲突和失败零写入,并确认 Resource 双实例与 Nacos 健康。 + +## 关联 / 联系人 + +### 链接 + +- **Issue**: [#6476](https://git.1814.love:8443/wx/HL/issues/6476) +- **PR**: [#6517](https://git.1814.love:8443/wx/HL/pulls/6517) +- **实现提交**: [`3cd07d27`](https://git.1814.love:8443/wx/HL/commit/3cd07d27dc876f17af1751ec7ef9de2ddbd71949) +- **Merge commit**: [`a952a04c`](https://git.1814.love:8443/wx/HL/commit/a952a04cf80f24d6c50cbec4b992c82f8580beba) +- **TEST 目标提交**: [`b269e5fd`](https://git.1814.love:8443/wx/HL/commit/b269e5fdca2e1d29e0336314a909885e9d1f9d16) + +### 联系人 + +- **后端负责人**: @lc +- **管理端负责人**: @mmg(`frontend_status: pending`)