--- schema: "hl-changelog/v2" ticket: "6643" 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 #6645 已合并 dev-v3;hl-resource-service 已通过 Deploy Panel 任务 954419f9 部署提交 d206d1d577a3e3f8cec228e73e139fcb36c00fd5。真实 TEST Gateway 已验证 address、licenseImageUrl、remark 原值回显、更新后回读、空地址保留、营业执照顶层字段权威读取、395014 零写入及测试草稿清理。" updated_at: "2026-08-29" base: "dev-v3" --- # 🔧 供应商编辑页地址、营业执照与备注回显 供应商编辑页应直接使用详情根对象的 `address`、`licenseImageUrl`、`remark` 初始化表单。其中 `licenseImageUrl` 是营业执照上传控件的权威字段,不得再从 `qualifications[].imageUrl` 推导或覆盖。 本次没有新增接口路径、请求必填项、权限点或业务错误码。 ## 一、关键变化 | 字段 | 修改前 | 修改后 | |---|---|---| | `data.address` | 已有详情字段,但编辑和空值保留规则缺少本次回归锁定 | 返回已保存地址;提交非空新值后可回读,更新时留空保持原值 | | `data.licenseImageUrl` | 详情根对象没有稳定的营业执照权威回显,调用方可能从资质数组取值 | 详情根对象稳定返回营业执照;只读取顶层权威值,不再从资质数组反向派生 | | `data.remark` | 已有详情字段,但编辑回显和更新规则缺少本次回归锁定 | 返回已保存内部备注;提交非空新值后可回读 | ## 二、变更接口清单 | # | 接口 | 方法 | 路径 | 变更类型 | 说明 | |---:|---|---|---|---|---| | 1 | 查询供应商基本信息 | GET | `/admin/supplier/items/{supplierId}/basic-info/view` | 响应修改 | 根对象稳定返回 `address`、`licenseImageUrl`、`remark` | | 2 | 更新供应商资料 | PUT | `/admin/supplier/items/{supplierId}/update` | 行为修改 | 三字段按增量更新和并发版本规则保存;写后重新查询可得到新值 | ## 三、接口详情 ### 1. 查询供应商基本信息 `GET /admin/supplier/items/{supplierId}/basic-info/view` **VO**: `SupplierBasicInfoRespVO` #### 使用场景 管理端进入供应商详情或编辑页时获取完整表单快照。营业执照上传控件必须绑定根对象 `data.licenseImageUrl`;`qualifications[]` 继续用于独立资质列表,不是该控件的取值来源。 #### 入参 | 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | |---|---|---|---:|---|---| | `supplierId` | Path | String | 是 | 正整数 ID 字符串 | 目标供应商;不得转为 JavaScript Number | 无 Query 参数,无请求体。 #### 出参 `Result` | 字段 | 类型 | 说明 | |---|---|---| | `data.address` | String/null | 注册地址或经营地址;未填写时为 `null` | | `data.licenseImageUrl` | String/null | 营业执照影像权威地址;未填写时为 `null` | | `data.remark` | String/null | 供应商主体内部备注;不等同于审批意见或变更原因 | | `data.qualifications[].imageUrl` | String/null | 单项资质影像;与根对象营业执照字段分别读取 | | `data.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", "supplierNo": "SUP2092800000000000001", "fullName": "示例旅行服务有限公司", "shortName": "示例旅行", "tax_no": "91350211M000100Y46", "legalRepresentative": "示例法人", "legalRepresentativeIdNo": "11010519491231002X", "legalRepresentativeIdNoMask": "11010519491231002X", "legalRepresentativeIdCardFrontUrl": null, "legalRepresentativeIdCardBackUrl": null, "contactPhone": "13800138000", "contactPhoneMask": "13800138000", "establishDate": "2020-01-01", "registeredCapital": "100万元", "businessScope": "境内旅游服务", "address": "福建省厦门市示例路 2 号", "staffScale": "LT50", "mainCooperation": "酒店与景区资源合作", "licenseImageUrl": "https://files.example.com/supplier/business-license-new.jpg", "remark": "地址与营业执照已复核", "status": "DRAFT", "creditLevel": "B", "totalScore": null, "types": [], "primaryTypeCode": null, "primaryTypeName": null, "contacts": [], "qualifications": [ { "qualificationId": "2092800000000000011", "qualType": "BUSINESS_LICENSE", "qualTypeName": "营业执照", "certNo": null, "certNoMask": null, "imageUrl": "https://files.example.com/supplier/business-license-new.jpg", "expiryDate": null, "permanentValid": true, "daysUntilExpiry": null, "validityStatus": "VALID", "validityStatusName": "有效", "isRequired": false, "expired": false, "updateTime": "2026-08-29 14:40:01" } ], "contracts": [], "updateTime": "2026-08-29 14:40:02" } } ``` #### 空数据 / 降级响应 未填写三字段时返回明确的 `null`,前端显示空控件即可,不要用资质数组补写营业执照: ```json { "code": 200, "message": "成功", "success": true, "data": { "supplierId": "2092800000000000001", "address": null, "licenseImageUrl": null, "remark": null, "qualifications": [], "updateTime": "2026-08-29 14:40:02" } } ``` #### 错误响应 供应商不存在、已删除或超出可见范围: ```json { "code": 395001, "message": "供应商不存在", "success": false, "data": null } ``` #### 业务边界 - 要求可信管理身份、可读角色、`supplier:view` 平台权限和既有数据范围。 - 根对象 `licenseImageUrl` 是编辑页营业执照的唯一权威回显;即使 `qualifications[]` 中同类型影像不同,也不得覆盖根对象值。 - 历史记录没有可用营业执照时返回 `null`,不猜测默认图片。 - 本接口只读,不推进版本、不触发审批或其他业务副作用。 ### 2. 更新供应商资料 `PUT /admin/supplier/items/{supplierId}/update` **VO**: `SupplierUpdateReqVO / SupplierWriteRespVO` #### 使用场景 管理端保存供应商编辑表单。请求只发送实际修改字段,并携带详情中的最新 `updateTime` 和本次 `changeReason`。 #### 入参 | 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | |---|---|---|---:|---|---| | `supplierId` | Path | String | 是 | 正整数 ID 字符串 | 目标供应商 | | `address` | Body | String | 否 | 最长 500 字符 | 非空时更新;省略、`null`、空串或纯空格时保留原值 | | `licenseImageUrl` | Body | String | 否 | 最长 500 字符 | 省略时保留;传非空值时更新权威营业执照;传空串时清空 | | `remark` | Body | String | 否 | - | 非空时更新;省略、`null`、空串或纯空格时保留原值 | | `changeReason` | Body | String | 是 | 去空白后非空,最长 500 字符 | 本次资料变更原因 | | `expectedUpdateTime` | Body | String | 是 | `yyyy-MM-dd HH:mm:ss` | 详情最新主体并发版本 | 其他既有可选字段和集合快照规则保持不变。 #### 出参 `Result` | 字段 | 类型 | 说明 | |---|---|---| | `data.supplierId` | String | 供应商 ID | | `data.supplierNo` | String | 供应商业务编号 | | `data.status` | String | 当前生命周期状态 | | `data.onboardingStage` | String | 当前注册阶段 | | `data.initialAccounts` | Array | 初始收款账户摘要 | | `data.updateTime` | String | 写入后的新主体并发版本 | #### 请求示例 ```http PUT /admin/supplier/items/2092800000000000001/update Authorization: Bearer Content-Type: application/json { "address": "福建省厦门市示例路 2 号", "licenseImageUrl": "https://files.example.com/supplier/business-license-new.jpg", "remark": "地址与营业执照已复核", "changeReason": "更新供应商编辑资料", "expectedUpdateTime": "2026-08-29 14:40:00" } ``` #### 响应示例 ```json { "code": 200, "message": "成功", "success": true, "data": { "supplierId": "2092800000000000001", "supplierNo": "SUP2092800000000000001", "status": "DRAFT", "onboardingStage": "PROFILE_DRAFT", "initialAccounts": [], "updateTime": "2026-08-29 14:40:02" } } ``` 写响应不重复返回 `address`、`licenseImageUrl`、`remark`。前端需要完整表单时,应使用新 `updateTime` 重新查询详情。 #### 空数据 / 降级响应 成功更新时 `data` 恒为写入结果对象,不返回 `null`。当前请求没有实际变化时不会伪造成功或推进版本,而是返回业务失败: ```json { "code": 400, "message": "未检测到实际变化", "success": false, "data": null } ``` #### 错误响应 ```json { "code": 395014, "message": "数据已被他人修改,请刷新后重试", "success": false, "data": null } ``` 收到 `395014` 后先重新获取详情,由用户确认后再提交;不得用旧表单自动覆盖。 #### 业务边界 - 要求可信管理身份、`FINANCE` 或 `SUPER_ADMIN` 写角色及 `supplier:update` 平台权限。 - 成功写入后推进主体 `updateTime`;权限、参数、状态或并发失败时三字段及版本均不变化。 - 顶层 `licenseImageUrl` 更新时会继续兼容同步营业执照资质影像;反向只修改 `qualifications[].imageUrl` 不会改写顶层权威字段。 - `remark` 是主体内部备注;`changeReason` 是本次变更审计原因,两者不得互相替代。 - 统一响应可能以 HTTP 200 承载业务失败,调用方必须同时判断 `code`、`success`、`message` 和 `data`。 ## 四、契约约束与正确调用方式 ### 编辑页初始化 ```text 地址输入框 ← data.address 营业执照上传控件 ← data.licenseImageUrl 内部备注输入框 ← data.remark 并发版本 ← data.updateTime ``` 不得把 `qualifications.find(item => item.qualType === "BUSINESS_LICENSE").imageUrl` 作为营业执照上传控件的回显值或顶层字段兜底。 ### 更新规则对照 | 场景 | 请求 | 结果 | |---|---|---| | 更新三个字段 | 发送三个非空新值 + 原因 + 最新版本 | 更新成功;重新查询返回三个新值 | | 地址留空,修改备注 | `address: " "`,`remark` 为新值 | 地址保持原值,备注更新 | | 省略营业执照 | 不发送 `licenseImageUrl` | 顶层营业执照保持原值 | | 清空营业执照 | `licenseImageUrl: ""` | 顶层营业执照清空,兼容影像同步清空 | | 只改资质数组影像 | 发送 `qualifications[]`,不发送顶层字段 | 资质影像独立变化,顶层营业执照保持原值 | | 使用旧版本 | 发送过期 `expectedUpdateTime` | 返回 `395014`,零写入 | ## 五、数据库行为 本节只描述接口可观察结果,不要求前端感知存储结构: - 三字段更新成功后,再次查询详情返回新值,且主体 `updateTime` 推进。 - `address` 或 `remark` 留空时保存其他字段,重新查询仍返回原地址或原备注。 - 顶层 `licenseImageUrl` 更新成功后,详情根对象与兼容营业执照资质影像均返回新值。 - 只更新资质数组影像时,详情根对象 `licenseImageUrl` 保持原值。 - 权限、参数、状态或 `395014` 并发失败时,三字段和主体版本均不变化。 - 部署时已对可识别的历史营业执照影像完成一次性兼容初始化;之后详情根对象不再从资质数组动态派生。 ## 六、边界行为 - 未登录或 Token 无效由 Gateway 拒绝,不产生业务写入。 - 无 `supplier:view` 时详情读取失败;无 `supplier:update` 或不属于允许写角色时更新返回 `395002`,零写入。 - 供应商不存在、已删除或不可见时返回 `395001`。 - `address` 或 `licenseImageUrl` 超过各自长度上限时返回 `400`,零写入。 - 缺少 `changeReason`、缺少 `expectedUpdateTime` 或没有实际变化时返回 `400`,零写入。 - 版本过期返回 `395014`;客户端必须刷新详情,不能自动覆盖。 - `ARCHIVED` 等不可修改状态继续由既有状态门禁拒绝。 - 业务失败可能仍使用 HTTP 200,客户端必须检查统一响应体。 ## 六.6、修改前后对比 ### 字段级对比 | 字段 | 改前 | 改后 | |---|---|---| | `data.address` | 字段已存在,但本次编辑回归未锁定 | 原值、更新值及留空保留规则均已锁定 | | `data.licenseImageUrl` | 根对象没有稳定权威回显 | 根对象返回可空权威值,不从 `qualifications[]` 反向派生 | | `data.remark` | 字段已存在,但本次编辑回归未锁定 | 原值和更新值均可稳定回读 | ### 行为级对比 | 行为 | 改前 | 改后 | |---|---|---| | 编辑页初始化营业执照 | 可能从资质数组搜索影像 | 固定读取根对象 `licenseImageUrl` | | 更新营业执照 | 根对象回读来源不稳定 | 提交顶层字段后,写后查询返回同一新值 | | 单独修改资质影像 | 可能被误当成主体营业执照 | 不改变根对象营业执照权威值 | ## 六.7、影响评估 - **是否破坏向后兼容**:否。详情根对象增加可空字段并固定既有字段行为;旧客户端可忽略未知字段。 - **前端是否必须同步上线**:需要。营业执照上传控件必须改为读取并提交顶层 `licenseImageUrl`;地址和备注继续直接绑定根对象字段。 - **前端 workaround 清理点**:删除从 `qualifications[]` 搜索营业执照影像并覆盖表单值的逻辑。 - **QA 重点**:已有值回显、三字段更新后刷新、地址留空保留、资质影像与顶层营业执照相互独立、旧版本失败零写入。 ## 七、不影响范围 - **仅影响**:管理后台供应商详情与编辑表单的地址、营业执照和内部备注。 - **零影响**: - 接口路径、Gateway 路由和认证级别。 - 供应商既有角色、平台权限和数据范围。 - 生命周期状态机、审批、合同、收款账户和资源关系。 - 既有业务错误码、Redis、MQ、Feign 和其他服务。 - 小程序、C 端、Web、H5 和桌面端接口。 ## 八、测试环境已验证 - 本地:供应商定向 62 项通过;`hl-resource-service` Reactor 全量 2,186 项通过,0 失败、0 错误,38 项既有条件跳过。 - 合并:后端 PR [#6645](https://git.1814.love:8443/wx/HL/pulls/6645) 已合并,合并提交为 `d206d1d577a3e3f8cec228e73e139fcb36c00fd5`。 - 部署:Deploy Panel API 任务 `954419f9` 成功,TEST 目标与实际提交均为上述合并提交;任务期 6 个采样均至少保持 2 个进程与 2 个健康启用实例匹配,0 个不可用采样。 - 真实 Gateway:唯一测试草稿依次验证原值回显、资质影像独立变化时顶层营业执照不变、三字段更新后回读、空地址保留、旧版本 `395014` 零写入及未认证拒绝。 - 清理:测试草稿已通过业务删除接口软删除并确认详情不可查询;仅保留系统规定的脱敏业务审计事实。 ## 九、撤回 如后端撤回本次契约,前端在回退部署前停止依赖顶层 `licenseImageUrl`,并以同步发布的撤回 Changelog 为准;不得自行恢复未冻结的资质数组反向覆盖逻辑。地址与备注仍按既有 #6476 契约处理。 ## 十、相关文档 - 关联 Issue:[#6643](https://git.1814.love:8443/wx/HL/issues/6643) - 关联 PR:[#6645](https://git.1814.love:8443/wx/HL/pulls/6645) - 既有地址与备注契约:[#6476](./27_6476_供应商详情补齐地址备注并统一表单校验-修改接口-管理后台.md) - 当前前端状态:待前端处理;请按根对象字段完成映射后回写本文件的消费状态。 ## 关联 / 联系人 ### 链接 - **Issue**: [#6643](https://git.1814.love:8443/wx/HL/issues/6643) - **PR**: [#6645](https://git.1814.love:8443/wx/HL/pulls/6645) - **Merge commit**: [`d206d1d5`](https://git.1814.love:8443/wx/HL/commit/d206d1d577a3e3f8cec228e73e139fcb36c00fd5) - **既有地址/备注契约**: [#6476](./27_6476_供应商详情补齐地址备注并统一表单校验-修改接口-管理后台.md) ### 联系人 - **后端负责人**: @lc - **管理端负责人**: 待认领(`frontend_status: pending`)