From 20272bb706757eeeca196e4cca2e9e017318568c Mon Sep 17 00:00:00 2001 From: lc Date: Thu, 27 Aug 2026 16:27:45 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E4=B8=8B=E5=8F=91=E4=BE=9B=E5=BA=94?= =?UTF-8?q?=E5=95=86=E8=8D=89=E7=A8=BF=E7=BC=96=E5=8F=B7=E5=A5=91=E7=BA=A6?= =?UTF-8?q?=EF=BC=88#6518=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...应商草稿创建即生成编号-修改接口-管理后台.md | 308 ++++++++++++++++++ 1 file changed, 308 insertions(+) create mode 100644 changelogs-v2/2026-08/27_6518_供应商草稿创建即生成编号-修改接口-管理后台.md diff --git a/changelogs-v2/2026-08/27_6518_供应商草稿创建即生成编号-修改接口-管理后台.md b/changelogs-v2/2026-08/27_6518_供应商草稿创建即生成编号-修改接口-管理后台.md new file mode 100644 index 00000000..e1395eff --- /dev/null +++ b/changelogs-v2/2026-08/27_6518_供应商草稿创建即生成编号-修改接口-管理后台.md @@ -0,0 +1,308 @@ +--- +schema: "hl-changelog/v2" +ticket: "6518" +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 #6531 已合并 dev-v3(合并提交 66aed70a);hl-resource-service 已由 Deploy Panel 任务 e977bf7b 精确发布该提交。真实 TEST 身份完成 47 项 Gateway 断言,验证创建即返回 SUP{supplierId}、列表/详情/更新稳定回显、权限失败零写入、审计事实和业务清理。管理端需停止把新建草稿 supplierNo 当作空值或等待审批后再取。" +updated_at: "2026-08-27" +base: "dev-v3" +--- + +# 🔧 供应商管理:草稿创建即生成编号 + +`POST /admin/supplier/items/add` 成功后,响应中的 `supplierNo` 现在立即为非空字符串,格式固定为 `SUP{supplierId}`。该编号与本次创建的供应商绑定,后续列表、详情、更新、提交及审批流程均保持同一值。 + +请求结构、权限点、状态机和业务错误码没有新增或删除。 + +## 一、背景 + +此前供应商编号在审批通过时才补齐,因此新建草稿阶段的创建响应、列表和详情可能返回 `supplierNo: null`。管理端若需要展示或引用编号,只能等待审批或使用临时占位。 + +本次发布后: + +- 新建草稿成功即取得正式编号,不再存在“先创建、后编号”的等待窗口。 +- 编号格式为字符串 `SUP{supplierId}`;例如 `supplierId="2094000000000000001"` 对应 `supplierNo="SUP2094000000000000001"`。 +- 编号在更新、提交、审批和状态变化中不重算、不覆盖。 +- 草稿删除后原编号不再分配给其他供应商。 +- 发布前已经存在且编号为空的历史记录仍可能返回 `null`;其审批通过时继续兼容补齐。调用方不能自行推导或写入编号。 + +## 二、变更接口清单 + +| # | 接口 | 方法 | 路径 | 变更类型 | 说明 | +|---:|---|---|---|---|---| +| 1 | 创建供应商注册草稿 | POST | `/admin/supplier/items/add` | 响应行为修改 | 成功响应的 `supplierNo` 从草稿期可空改为立即返回 `SUP{supplierId}` | + +列表、详情、更新和审批相关响应中的 `supplierNo` 字段结构未变化;对于本次发布后创建的供应商,它们会稳定返回创建响应中的同一编号。 + +## 三、接口详情 + +### 1. 创建供应商注册草稿 `POST /admin/supplier/items/add` + +**VO**: `SupplierDraftSaveReqVO` → `Result` + +#### 使用场景 + +管理端首次保存供应商注册表单。创建成功后可立即展示、复制或继续传递后端返回的正式 `supplierNo`,无需等待提交审批。 + +#### 入参 + +本次没有新增请求字段,`supplierNo` 也不允许由客户端提交。 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|---|---|---|---:|---|---| +| `fullName` | Body | String | 是 | 非空,最长 500 | 供应商法定全称 | +| `shortName` | Body | String/null | 否 | 最长 300 | 供应商简称 | +| `taxNo` | Body | String | 是 | 6 至 64 位字母、数字或展示分隔符 | 主体证件号 | +| `types` | Body | Array/null | 否 | 最多 15 项;非空时类型值有效且不得重复 | 供应商类型完整集合 | +| `legalRepresentative` | Body | String/null | 否 | 最长 500 | 法定代表人 | +| `legalRepresentativeIdNo` | Body | String/null | 否 | 为空或合法 18 位居民身份证号 | 法人证件号 | +| `legalRepresentativeIdCardFrontUrl` / `legalRepresentativeIdCardBackUrl` | Body | String/null | 否 | 单项最长 1000 | 法人证件正反面永久地址 | +| `contactPhone` | Body | String/null | 否 | 7 至 20 位合法电话字符 | 公司联系电话 | +| `establishDate` | Body | String/null | 否 | `yyyy-MM-dd`,不得晚于当天 | 成立日期 | +| `registeredCapital` | Body | String/null | 否 | 最长 50 | 注册资本文本 | +| `businessScope` / `address` | Body | String/null | 否 | 单项最长 500 | 经营范围与地址 | +| `staffScale` | Body | String/null | 否 | `LT50`、`R50_200`、`R200_500`、`GT500` | 人员规模 | +| `mainCooperation` | Body | String | 是 | 非空 | 主要合作内容 | +| `licenseImageUrl` | Body | String/null | 否 | 最长 500 | 营业执照影像快捷字段 | +| `remark` | Body | String/null | 否 | - | 供应商内部备注 | +| `contacts` | Body | Array/null | 否 | 最多 100 项 | 联系人完整集合;非空时继续执行角色和唯一默认联系人规则 | +| `qualifications` | Body | Array/null | 否 | 最多 100 项 | 资质完整集合;继续执行类型、证号和有效期规则 | +| `contracts` | Body | Array/null | 否 | 最多 100 项 | 合同完整集合;继续执行日期、金额和状态规则 | +| `initialAccounts` | Body | Array/null | 否 | 最多 1 项 | 初始收款账户集合 | +| `duplicateConfirmToken` | Body | String/null | 否 | 最长 256,一次性使用 | 存在近似主体时按后端返回值二次确认 | +| `expectedUpdateTime` | Body | String/null | 否 | 创建草稿时不传 | 仅已有草稿提交审批时使用 | + +#### 出参 `Result` + +| 字段 | 类型 | 说明 | +|---|---|---| +| `supplierId` | String | 新建供应商 ID;必须按字符串保存和传输 | +| `supplierNo` | String | 本次行为变化:创建成功立即返回 `SUP{supplierId}`,非空且全生命周期稳定 | +| `status` | String | 新建草稿固定为 `DRAFT` | +| `onboardingStage` | String | 新建草稿固定为 `PROFILE_DRAFT` | +| `initialAccounts` | Array | 初始收款账户摘要;未提交时通常为空数组 | +| `updateTime` | String | 当前并发版本,格式 `yyyy-MM-dd HH:mm:ss` | + +#### 请求示例 + +```http +POST /admin/supplier/items/add +Authorization: Bearer +Content-Type: application/json +``` + +```json +{ + "fullName": "示例草原旅行服务有限公司", + "shortName": "示例草原旅行", + "taxNo": "91350211M000100Y46", + "mainCooperation": "酒店与景区资源合作", + "types": [], + "contacts": [], + "qualifications": [], + "contracts": [], + "initialAccounts": [] +} +``` + +#### 响应示例 + +```json +{ + "code": 200, + "message": "成功", + "success": true, + "data": { + "supplierId": "2094000000000000001", + "supplierNo": "SUP2094000000000000001", + "status": "DRAFT", + "onboardingStage": "PROFILE_DRAFT", + "initialAccounts": [], + "updateTime": "2026-08-27 16:24:30" + } +} +``` + +#### 空数据 / 降级响应 + +创建接口没有“成功但 data 为空”的降级分支:创建成功必须返回完整 `SupplierWriteRespVO`,失败则返回明确业务错误。编号的历史空数据兼容只出现在发布前遗留记录的列表或详情读取中;这类尚未补齐的记录仍可能出现: + +```json +{ + "supplierId": "2089000000000000001", + "supplierNo": null, + "status": "DRAFT" +} +``` + +管理端可为这种历史空值显示 `—`,但不得本地生成或回写编号。 + +#### 错误响应 + +请求缺少必填字段或字段不合法时,业务失败且不创建草稿: + +```json +{ + "code": 400, + "message": "统一社会信用代码不能为空", + "success": false, + "data": null +} +``` + +真实身份没有 `supplier:create` 写权限时: + +```json +{ + "code": 395002, + "message": "无供应商写入权限", + "success": false, + "data": null +} +``` + +#### 业务边界 + +- 外部请求必须经过 Gateway;未认证返回业务码 `401`,不能把该负向结果当作正向验收。 +- 写操作仅允许既有授权角色并再次校验 `supplier:create`;普通 `ADMIN` 被拒绝且零写入。 +- `supplierNo` 只在整个创建成功后返回;任何字段校验、权限、重复主体确认或持久化失败都不会留下可见的半成品编号。 +- 创建成功后,列表、详情和更新响应必须返回同一 `supplierNo`。 +- 客户端只消费后端响应,不能提交、覆盖、重算、截断或把编号转成数字。 +- 业务失败可能仍使用 HTTP 200,必须同时判断响应体 `code`、`success` 和 `data`。 + +## 四、契约约束与正确调用方式 + +### 正确消费顺序 + +1. 提交创建请求并确认 `code=200`、`success=true`。 +2. 将 `data.supplierId`、`data.supplierNo`、`data.updateTime` 全部按字符串保存。 +3. 页面立即展示 `data.supplierNo`;刷新列表或详情时核对同一字段,不再等待审批。 +4. 后续更新继续传最新 `expectedUpdateTime`,但不得把 `supplierNo` 放进请求体。 + +### ✅ 正确 / ❌ 错误行为对照 + +| 场景 | 正确行为 | +|---|---| +| ✅ 新草稿创建成功 | 直接显示响应 `supplierNo`,例如 `SUP2094000000000000001` | +| ✅ 列表或详情刷新 | 使用接口返回的同一编号,不做本地拼接 | +| ✅ 历史草稿仍为空 | 显示 `—` 并等待后端兼容补齐,不写回猜测值 | +| ❌ 本地生成编号 | 禁止用时间、计数器或客户端缓存生成 | +| ❌ 把编号当数字 | 禁止移除 `SUP`、转数值或做算术运算 | +| ❌ 删除后复用 | 禁止把已删除供应商的旧编号分配给新供应商 | + +## 五、数据库行为 + +- 创建成功时,供应商草稿、正式编号、可选子项和创建审计作为一个业务整体生效;响应中的编号与后续读取一致。 +- 创建失败时,供应商和编号都不产生可见结果,不返回部分成功。 +- 更新、提交或审批不改变已生成编号。 +- 草稿删除后不再出现在正常列表和详情中,但其编号不会重新分配。 +- 本次不要求调用方执行数据迁移,也不改变现有请求字段。 + +## 六、边界行为 + +- 未认证:业务码 `401`,零写入。 +- 无写权限:业务码 `395002`,零写入。 +- 必填字段缺失、格式或组合不合法:业务码 `400`,零写入。 +- 发现近似主体且未完成二次确认:业务码 `395007`,按响应中的一次性确认信息继续既有流程。 +- 发布后新草稿:`supplierNo` 必为非空 `SUP{supplierId}`。 +- 发布前历史空编号:读取时仍允许 `null`;审批通过后按兼容规则补齐。 +- 更新、提交、审批、暂停、拉黑等后续操作:编号保持不变。 +- 软删除后再创建:新供应商获得新的 `supplierId` 和新的 `supplierNo`,不复用旧编号。 + +## 六.5、枚举 / 数据字典 + +本次没有新增枚举或数据字典。`status`、`onboardingStage`、供应商类型、联系人角色、资质类型等继续沿用现有取值;编号格式不是字典值。 + +## 六.6、修改前后对比 + +### 字段级对比 + +| 字段 | 改前 | 改后 | +|---|---|---| +| 创建响应 `data.supplierNo` | 新草稿通常为 `null`,审批通过后才补齐 | 创建成功立即返回非空 `SUP{supplierId}` | +| `supplierId`、`status`、`onboardingStage`、`initialAccounts`、`updateTime` | 已存在 | 字段和类型不变 | +| 创建请求 | 不接受 `supplierNo` | 仍不接受 `supplierNo`,请求无新增字段 | + +### 行为级对比 + +| 行为 | 改前 | 改后 | +|---|---|---| +| 新建后展示编号 | 需要空值占位或等待审批 | 创建成功即可展示正式编号 | +| 列表、详情、更新回显 | 草稿期可能为空 | 对发布后新建草稿稳定返回创建时编号 | +| 提交与审批 | 审批通过时生成编号 | 保留创建时编号;只为历史空值兼容补齐 | +| 删除后再次创建 | 无明确前端口径 | 新供应商取得新编号,旧编号不复用 | + +## 六.7、影响评估 + +- **是否破坏向后兼容**: 否。字段已存在,只把发布后新草稿的值从 `null` 收紧为稳定非空字符串。 +- **前端是否必须同步上线**: 需要适配体验。旧客户端仍可运行,但会继续把已经可用的编号显示为空或等待审批。 +- **前端 workaround 清理点**: 删除“草稿编号为空”“审批通过后再刷新编号”的新建流程兜底,创建成功后直接使用 `data.supplierNo`。 +- **历史兼容**: 仍保留对发布前空编号记录的 `null` 展示兼容,前端不要把所有历史数据强断言为非空。 +- **QA 重点**: 创建响应、立即列表/详情、更新后详情、未认证、ADMIN 越权、非法请求零写入、软删除后新编号不复用。 + +## 七、不影响范围 + +- **仅影响**: 管理后台供应商草稿创建成功后的编号生成时点和后续稳定回显。 +- **零影响**: + - 创建请求字段、供应商类型可选语义、联系人/资质/合同/初始账户规则。 + - Gateway 路由、认证方式、权限码和数据范围。 + - 供应商生命周期状态机、审批级数、暂停、拉黑、归档和资源关系行为。 + - 小程序、C 端、Web、H5、桌面端接口。 + - 其他服务接口、缓存和消息契约。 + +## 八、测试环境已验证 + +- 本地自动化:供应商编号、审批兼容、完整回显、事务边界、权限和迁移契约定向测试 61 项通过;`hl-resource-service` 全量 2,157 项零失败、零错误,38 项仓库既有条件跳过;Gateway 路由审计 4 项通过。 +- 部署:Deploy Panel API 任务 `e977bf7b` 终态 `success`;目标与实际提交均为 `66aed70a2c9919046b9e4362b9db3219858ec6e1`。 +- 可用性采样:任务期 6 个有效采样均至少有 2 个运行进程、2 个健康启用实例,零观测不可用采样;该证据只证明采样点,不代表采样间绝对连续。 +- 真实 Gateway:SUPER_ADMIN 创建草稿后,响应立即满足 `supplierNo=SUP{supplierId}`;列表、详情、更新响应保持同一编号;独立变更记录同时存在 CREATE/UPDATE 事实。 +- 失败路径:未认证请求被 Gateway 拒绝;真实 ADMIN 创建返回 `395002`;非法税号返回 `400`;两类失败均验证零可见写入。TEST 未配置可用 FINANCE 身份,其角色等价性由服务端权限测试覆盖,未伪造身份。 +- 删除与不复用:首个草稿经业务 API 软删除并确认列表、详情不可见;随后创建的新草稿获得不同编号。 +- 清理:两条临时草稿均经业务 API 软删除并确认不可见,测试角色已恢复,会话已注销;未执行物理删除、缓存或 MQ 操作,仅保留正常的创建、更新、删除审计事实。 + +## 九、相关历史 PR + +| PR | Issue | 说明 | 是否仍有效 | +|---|---|---|---| +| #6274 | #6273 | 审批记录响应增加 `supplierNo` 字段 | ✅ 有效,本次收紧新草稿取值时点 | +| #6344 | #6343 | 新建和修改允许供应商类型为空 | ✅ 有效,请求语义不变 | +| #6517 | #6476 | 详情补齐地址备注并统一表单校验 | ✅ 有效,详情继续稳定回显 | +| #6531 | #6518 | 草稿创建即生成唯一供应商编号 | ✅ 最新 | + +## 十、相关文档 + +- 关联 Issue:[#6518](https://git.1814.love:8443/wx/HL/issues/6518) +- 关联 PR:[#6531](https://git.1814.love:8443/wx/HL/pulls/6531) +- 历史编号响应契约:`changelogs-v2/2026-08/24_6273_供应商审批记录补充供应商编号-修改接口-管理后台.md` +- 管理端接入:创建成功立即读取并展示 `data.supplierNo`;历史空值仍保留 `—` 兼容,前端引用待回填。 + +## 撤回 + +1. 管理端先恢复“新草稿编号可能为空”的兼容展示,不再依赖创建响应立即有值。 +2. 后端从最新 `dev-v3` 创建独立回退分支,revert #6518 合并提交 `66aed70a2c9919046b9e4362b9db3219858ec6e1`,验证后经独立 PR 合入并重新发布 `hl-resource-service`。 +3. 无接口字段删除或请求结构回退;发布窗口内已生成的编号默认保留,不执行批量清空或复用。 +4. 撤回后经 Gateway 复测创建响应可空、历史审批补齐、列表/详情/更新兼容、未认证与越权零写入。 +5. 以新的 Changelog 提交标记本契约撤回,不改写已发布提交历史。 + +## 关联 / 联系人 + +### 链接 + +- **Issue**: [#6518](https://git.1814.love:8443/wx/HL/issues/6518) +- **PR**: [#6531](https://git.1814.love:8443/wx/HL/pulls/6531) +- **实现提交**: [`69665d72`](https://git.1814.love:8443/wx/HL/commit/69665d7249b5d269e56c36abbfeafbdbbbe72d45) +- **Merge commit**: [`66aed70a`](https://git.1814.love:8443/wx/HL/commit/66aed70a2c9919046b9e4362b9db3219858ec6e1) + +### 联系人 + +- **后端负责人**: @lc +- **管理端负责人**: 待认领(`frontend_status: pending`)