--- schema: "hl-changelog/v2" ticket: "6474" title: "供应商详情分离变更记录与审批流水" consumer: "admin" author: "lc(GIT)" change_type: "新增接口" backend_status: "deployed" gateway_status: "verified" frontend_status: "verified" frontend_owner: "mmg" frontend_ref: "199147de" target_release: "v2.1" verified_at: "2026-08-27" status_note: "PR #6519 已合并 dev-v3;补充缺陷 PR #6534 已恢复旧兼容查询。hl-resource-service 已由 Deploy Panel 任务 9e69fd2c 精确发布提交 aec1de2d 至 TEST,并以真实 SUPER_ADMIN、ADMIN 和受限 CUSTOMIZER 身份完成 Gateway 只读验收。" updated_at: "2026-08-27" base: "dev-v3" --- # 供应商管理:详情分离变更记录与审批流水 供应商详情新增两个相互独立的只读分页接口:“变更记录”只返回供应商主体发生过的业务变化,“审批记录”只返回供应商主体审批事实。两个列表独立计数、独立筛选、独立排序,收款账户记录不会混入。 旧 `/admin/supplier/items/{supplierId}/approval-records/page` 继续保留,用于仍需主体与账户变更混合列表的兼容场景;本次不删除、不改名,也不要求现有调用方同步切换。 ## 一、背景 旧审批记录接口承载的是主体与收款账户变更的兼容混合列表,不能同时满足详情页“业务变更”和“审批过程”两种独立展示语义。若管理端在本地拆分或二次计数,会出现分页总数不准确、账户记录混入主体历史、审批技术字段误展示等问题。 本次由后端直接提供两个稳定的业务白名单视图: - “变更记录”展示操作类型、变更前后业务摘要、原因、生命周期状态、操作人、历史角色和发生时间。 - “审批记录”展示审批业务、申请人、状态、动作、审批人安全展示值、意见、提交时间和完成时间。 - 两个接口都在查询前校验可信管理身份、角色和 `supplier:approval:read` 权限。 ## 二、变更接口清单 | # | 接口 | 方法 | 路径 | 变更类型 | 说明 | |---:|---|---|---|---|---| | 1 | 查询供应商主体变更记录 | GET | `/admin/supplier/items/{supplierId}/change-records/page` | 新增只读接口 | 独立分页返回主体变更业务摘要 | | 2 | 查询供应商主体审批流水 | GET | `/admin/supplier/items/{supplierId}/approval-history/page` | 新增只读接口 | 独立分页返回主体审批过程与结果 | ## 三、接口详情 ### 1. 查询供应商主体变更记录 `GET /admin/supplier/items/{supplierId}/change-records/page` **VO**: `SupplierChangeRecordPageReqVO` / `SupplierChangeRecordRespVO` #### 使用场景 管理端进入供应商详情的“变更记录”页签时调用。服务端完成主体记录筛选、分页和业务摘要投影;前端不要从旧混合列表中再次筛选主体记录,也不要自行拼接前后值摘要。 #### 入参 | 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | |---|---|---|---:|---|---| | `supplierId` | Path | String | 是 | 正整数 ID 字符串 | 目标供应商;不得转为 JavaScript Number | | `page` | Query | Integer | 否 | 默认 `1`,最小 `1` | 当前页;兼容别名 `pageNo` | | `pageSize` | Query | Integer | 否 | 默认 `20`,范围 `1..100` | 每页条数 | | `operationType` | Query | String | 否 | `CREATE`、`UPDATE`、`ENABLE`、`DISABLE`、`DELETE` | 操作类型精确筛选 | | `fieldName` | Query | String | 否 | 非空白,最长 64 字符 | 发生变化的业务字段精确筛选 | | `status` | Query | String | 否 | 生命周期编码 | 按变更后的供应商状态筛选 | | `from` | Query | String | 条件必填 | `yyyy-MM-dd HH:mm:ss` | 与 `to` 成对传入,按发生时间筛选,含边界 | | `to` | Query | String | 条件必填 | `yyyy-MM-dd HH:mm:ss`,不得早于 `from` | 与 `from` 成对传入,含边界 | | `sortBy` | Query | String | 否 | `occurredAt` 或 `changeLogId`,默认 `occurredAt` | 服务端白名单排序字段 | | `sortDirection` | Query | String | 否 | `ASC` 或 `DESC`,不区分大小写,默认 `DESC` | 排序方向 | #### 出参 `Result>` 分页对象固定包含 `records`、`total`、`page`、`pageSize`。 | 字段 | 类型 | 说明 | |---|---|---| | `records` | Array | 当前页主体变更记录;无数据时为 `[]` | | `total` | Integer | 符合筛选条件的主体变更总数,不包含账户记录 | | `page` | Integer | 当前页码 | | `pageSize` | Integer | 当前页容量 | | `records[].operationType` | String | 操作类型编码 | | `records[].oldValue` | String | 变更前中文业务摘要;空值使用明确占位 | | `records[].newValue` | String | 变更后中文业务摘要;空值使用明确占位 | | `records[].valueAvailability` | String | `FULL` 或 `LEGACY_MASKED_UNRECOVERABLE` | | `records[].changeReason` | String | 业务变更原因或稳定占位 | | `records[].status` | String/null | 变更后的供应商生命周期中文名 | | `records[].operatorName` | String | 操作人展示名;系统操作为“系统”,依赖降级为“未知管理员” | | `records[].operatorRole` | String | 操作发生时的角色快照中文名 | | `records[].occurredAt` | String | 变更发生时间,格式 `yyyy-MM-dd HH:mm:ss` | 响应不会返回变更日志 ID、管理员内部 ID、原始审计 JSON、TraceId、账户 ID、证明附件或其他主体敏感快照字段。 #### 请求示例 ```http GET /admin/supplier/items/2092800000000000001/change-records/page?page=1&pageSize=20&operationType=UPDATE&sortBy=occurredAt&sortDirection=DESC Authorization: Bearer ``` GET 请求无请求体。 #### 响应示例 ```json { "code": 200, "message": "成功", "success": true, "data": { "records": [ { "operationType": "UPDATE", "oldValue": "供应商全称:示例旅行服务公司", "newValue": "供应商全称:示例旅行服务有限公司", "valueAvailability": "FULL", "changeReason": "修正供应商主体名称", "status": "合作中", "operatorName": "示例管理员", "operatorRole": "超级管理员", "occurredAt": "2026-08-27 15:20:00" } ], "total": 1, "page": 1, "pageSize": 20 } } ``` #### 空数据 / 降级响应 筛选结果为空仍返回成功分页,不回退到旧混合列表: ```json { "code": 200, "message": "成功", "success": true, "data": { "records": [], "total": 0, "page": 1, "pageSize": 20 } } ``` 历史记录无法恢复完整原值时,仍返回已有安全摘要,并明确标记: ```json { "operationType": "UPDATE", "oldValue": "138****8000", "newValue": "139****9000", "valueAvailability": "LEGACY_MASKED_UNRECOVERABLE", "changeReason": "历史记录", "status": "合作中", "operatorName": "未知管理员", "operatorRole": "管理员", "occurredAt": "2026-07-01 10:00:00" } ``` #### 错误响应 非法筛选、分页、排序或时间范围返回参数错误,例如只传 `from`: ```json { "code": 400, "message": "from和to必须同时传入", "success": false, "data": null } ``` 供应商不存在或已删除: ```json { "code": 395001, "message": "供应商不存在", "success": false, "data": null } ``` #### 业务边界 - 要求 Gateway 登录态、可信管理员身份、允许的读角色以及 `supplier:approval:read` 平台权限;任一门禁失败时不查询历史数据。 - 代码角色矩阵沿用 `ADMIN`、`FINANCE`、`SUPER_ADMIN`;角色允许不等于拥有平台权限,两项必须同时满足。 - 只读取供应商主体变更,不包含收款账户变更或审批流水。 - `oldValue`、`newValue` 是后端形成的中文业务摘要,不是可回填编辑表单的结构化快照。 - User 姓名依赖异常只影响 `operatorName`,分页仍成功且不会以管理员内部 ID 降级。 - 接口只读,成功、空数据和失败场景均不修改供应商、审批、缓存或消息状态。 ### 2. 查询供应商主体审批流水 `GET /admin/supplier/items/{supplierId}/approval-history/page` **VO**: `SupplierApprovalHistoryPageReqVO` / `SupplierApprovalHistoryRespVO` #### 使用场景 管理端进入供应商详情的“审批记录”页签时调用。该列表只表达主体审批申请、过程和结果,不包含主体字段变更摘要,也不包含收款账户审批。 #### 入参 | 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | |---|---|---|---:|---|---| | `supplierId` | Path | String | 是 | 正整数 ID 字符串 | 目标供应商;不得转为 JavaScript Number | | `page` | Query | Integer | 否 | 默认 `1`,最小 `1` | 当前页;兼容别名 `pageNo` | | `pageSize` | Query | Integer | 否 | 默认 `20`,范围 `1..100` | 每页条数 | | `bizType` | Query | String | 否 | 大写字母开头,仅大写字母、数字、下划线,最长 32 字符 | 审批业务类型精确筛选 | | `approvalStatus` | Query | String | 否 | 大写字母开头,仅大写字母、数字、下划线,最长 20 字符 | 审批状态精确筛选 | | `action` | Query | String | 否 | 大写字母开头,仅大写字母、数字、下划线,最长 32 字符 | 审批动作或结果精确筛选 | | `from` | Query | String | 条件必填 | `yyyy-MM-dd HH:mm:ss` | 与 `to` 成对传入,按提交时间筛选,含边界 | | `to` | Query | String | 条件必填 | `yyyy-MM-dd HH:mm:ss`,不得早于 `from` | 与 `from` 成对传入,含边界 | | `sortBy` | Query | String | 否 | `submittedAt` 或 `finishedAt`,默认 `submittedAt` | 服务端白名单排序字段 | | `sortDirection` | Query | String | 否 | `ASC` 或 `DESC`,不区分大小写,默认 `DESC` | 排序方向 | #### 出参 `Result>` 分页对象固定包含 `records`、`total`、`page`、`pageSize`。 | 字段 | 类型 | 说明 | |---|---|---| | `records` | Array | 当前页主体审批记录;无数据时为 `[]` | | `total` | Integer | 符合筛选条件的主体审批总数,不包含账户审批 | | `page` | Integer | 当前页码 | | `pageSize` | Integer | 当前页容量 | | `records[].bizType` | String | 审批业务类型编码 | | `records[].bizTypeName` | String | 审批业务类型中文名;未知编码显示“其他供应商审批” | | `records[].applicantName` | String | 申请人展示名;系统申请为“系统”,依赖降级为“未知申请人” | | `records[].approvalStatus` | String | 审批状态编码 | | `records[].approvalStatusName` | String | 审批状态中文名;未知编码显示“未知状态” | | `records[].action` | String/null | 审批动作或结果编码 | | `records[].actionName` | String | 审批动作中文名;未知编码显示“其他动作” | | `records[].approverName` | String | 安全展示值:“系统”“待审批”或“外部审批人” | | `records[].opinion` | String | 审批意见;无意见时为 `—` | | `records[].submittedAt` | String | 提交时间;历史缺失时使用该审批事实的创建时间,格式 `yyyy-MM-dd HH:mm:ss` | | `records[].finishedAt` | String/null | 完成时间;审批中为 `null`,格式 `yyyy-MM-dd HH:mm:ss` | 响应不会返回审批日志 ID、申请人内部 ID、requestNo、spNo、审批模板 ID、企微用户 ID、候选或详情摘要、候选或详情 JSON/密文、apply/sync 技术状态。 #### 请求示例 ```http GET /admin/supplier/items/2092800000000000001/approval-history/page?page=1&pageSize=20&approvalStatus=APPROVED&sortBy=submittedAt&sortDirection=DESC Authorization: Bearer ``` GET 请求无请求体。 #### 响应示例 ```json { "code": 200, "message": "成功", "success": true, "data": { "records": [ { "bizType": "PROFILE_CREATE", "bizTypeName": "供应商建档审批", "applicantName": "示例管理员", "approvalStatus": "APPROVED", "approvalStatusName": "已通过", "action": "SYSTEM_AUTO_APPROVE", "actionName": "系统自动通过", "approverName": "系统", "opinion": "—", "submittedAt": "2026-08-27 14:00:00", "finishedAt": "2026-08-27 14:00:01" } ], "total": 1, "page": 1, "pageSize": 20 } } ``` #### 空数据 / 降级响应 不存在符合条件的审批事实时返回成功空分页: ```json { "code": 200, "message": "成功", "success": true, "data": { "records": [], "total": 0, "page": 1, "pageSize": 20 } } ``` 申请人姓名依赖不可用时,该记录仍正常返回,`applicantName` 为 `未知申请人`;不会回退为内部管理员 ID。 #### 错误响应 非法枚举、分页、排序或反向时间范围返回参数错误,例如: ```json { "code": 400, "message": "from不能晚于to", "success": false, "data": null } ``` 无业务读取权限时返回拒绝结果,且不查询审批历史: ```json { "code": 403, "message": "无权访问供应商数据", "success": false, "data": null } ``` #### 业务边界 - 权限条件与变更记录接口相同:可信管理身份、`ADMIN`/`FINANCE`/`SUPER_ADMIN` 读角色和 `supplier:approval:read` 平台权限缺一不可。 - 只读取供应商主体审批,不包含收款账户审批或主体字段变更记录。 - 时间筛选基于提交时间;历史提交时间为空时使用该审批事实的创建时间。 - 申请人名称每页最多批量补全一次;User 服务异常、空响应或缺失用户时安全降级,整页不返回 500。 - 审批人只返回业务安全展示值,不暴露企微或外部审批系统标识。 - 接口只读,不推进审批状态,不触发审批回调,也不产生缓存、消息或配置副作用。 ## 四、契约约束与正确调用方式 ### 详情页接入映射 | 页面区域 | 正确接口 | 数据范围 | |---|---|---| | 变更记录 | `GET /admin/supplier/items/{supplierId}/change-records/page` | 仅主体变更 | | 审批记录 | `GET /admin/supplier/items/{supplierId}/approval-history/page` | 仅主体审批 | | 旧兼容混合列表 | `GET /admin/supplier/items/{supplierId}/approval-records/page` | 主体与账户变更,契约不变 | ### ✅ 正确 / ❌ 错误调用对照 | 场景 | 调用 / 结果 | |---|---| | ✅ 分别加载两个页签 | 两条新接口分别维护自己的 `page`、`pageSize`、筛选和 `total` | | ✅ 查询完整时间区间 | 同时传 `from=2026-08-01 00:00:00` 与 `to=2026-08-31 23:59:59` | | ✅ 兼容旧页面 | 继续调用旧 `approval-records/page`,无需因本次新增接口修改 | | ❌ 在前端拆旧混合列表 | 分页后再过滤会得到错误总数,也不能生成审批流水 | | ❌ 只传一侧时间 | 返回 `400`,不执行历史查询 | | ❌ 把 `supplierId` 转为 Number | 可能丢失精度;ID 必须始终按 String 传输和比较 | 调用方必须同时检查 `code`、`message`、`success` 和 `data`,不能只用 HTTP 状态判断业务成功。 ## 六、边界行为 - 未登录或登录态失效:统一结果业务码 `401`,不进入供应商查询。 - 已登录但角色或平台权限不满足:业务码 `403`,不读取历史。 - `supplierId<=0`、`page<1`、`pageSize` 不在 `1..100`、非法筛选或排序:业务码 `400`。 - 供应商不存在或已软删除:业务码 `395001`。 - `from`、`to` 必须同时传入,且 `from<=to`;时间边界包含起止时刻。 - 请求超过最后一页:返回原请求页码、空 `records` 和真实 `total`,不自动改页。 - 两个新接口的 `total` 分别统计自己的数据集,互不借用;账户变更和账户审批都不进入。 - 旧 `approval-records/page` 的路径、请求、响应及主体/账户混合语义保持兼容。 ## 六.5、枚举 / 数据字典 ### `operationType`(变更记录操作类型) **所属字段**: `SupplierChangeRecordPageReqVO.operationType` / `SupplierChangeRecordRespVO.operationType` | **类型**: `String` | 值 | 中文 | 说明 | |---|---|---| | `CREATE` | 创建 | 创建供应商主体 | | `UPDATE` | 更新 | 更新主体业务字段 | | `ENABLE` | 启用 | 恢复可用状态 | | `DISABLE` | 停用 | 暂停或禁用合作 | | `DELETE` | 删除 | 软删除业务事实 | ### `status`(变更后的供应商生命周期) **所属字段**: `SupplierChangeRecordPageReqVO.status` | **类型**: `String` | 值 | 中文展示 | |---|---| | `DRAFT` | 草稿 | | `VETTING` | 注册审核中 | | `ACTIVE` | 合作中 | | `SUSPENDED` | 暂停合作 | | `FROZEN` | 已冻结 | | `BLACKLIST` | 黑名单 | | `ARCHIVED` | 已归档 | ### `valueAvailability`(变更摘要可用性) **所属字段**: `SupplierChangeRecordRespVO.valueAvailability` | **类型**: `String` | 值 | 中文 | 调用方处理 | |---|---|---| | `FULL` | 完整业务摘要可用 | 正常展示 `oldValue` / `newValue` | | `LEGACY_MASKED_UNRECOVERABLE` | 历史原值不可恢复 | 展示现有摘要,并标注其为历史脱敏值;不要提示用户重试 | ### `bizType`(主体审批业务类型) **所属字段**: `SupplierApprovalHistoryPageReqVO.bizType` / `SupplierApprovalHistoryRespVO.bizType` | **类型**: `String` | 值 | 中文展示 | 说明 | |---|---|---| | `PROFILE_CREATE` | 供应商建档审批 | 注册或建档审批 | | `STATUS_CHANGE` | 供应商状态变更审批 | 生命周期状态变更审批 | | 其他合法大写编码 | 其他供应商审批 | 为历史及后续业务保留兼容 | ### `approvalStatus`(主体审批状态) **所属字段**: `SupplierApprovalHistoryPageReqVO.approvalStatus` / `SupplierApprovalHistoryRespVO.approvalStatus` | **类型**: `String` | 值 | 中文展示 | |---|---| | `PENDING` | 审批中 | | `APPROVED` | 已通过 | | `REJECTED` | 已驳回 | | `CANCELED` / `CANCELLED` | 已撤销 | | `FAILED` | 失败 | | 其他合法大写编码 | 未知状态 | ### `action`(主体审批动作或结果) **所属字段**: `SupplierApprovalHistoryPageReqVO.action` / `SupplierApprovalHistoryRespVO.action` | **类型**: `String` | 值 | 中文展示 | |---|---| | `SUBMIT` | 已提交 | | `SYSTEM_AUTO_APPROVE` | 系统自动通过 | | `APPLY_FAIL` | 结果应用失败 | | `APPROVE` | 通过 | | `REJECT` | 驳回 | | `REVOKE` | 撤销 | | `CANCEL` | 取消 | | 其他合法大写编码 | 其他动作 | ## 七、不影响范围 - **仅新增**:供应商详情的两个管理端只读分页契约。 - **保持兼容**:旧 `approval-records/page` 继续返回主体与账户变更混合列表。 - **零影响**:供应商详情、创建、更新、提交、归档、暂停合作、拉黑、删除及收款账户管理接口。 - **零影响**:供应商写权限、数据范围、状态机、审批提交/结果应用、事务、锁、幂等、审计与软删除语义。 - **零影响**:数据库结构与历史数据;本次无 migration、数据回填或破坏性数据操作。 - **零影响**:Gateway 顶级路由、Nacos 配置、Redis、MQ 和跨服务写链路。 - 后端仓库未修改任何管理端前端源码;管理端接入状态保持 `pending`。 ## 八、测试环境已验证 - 本地自动化:主功能定向测试 79 项零失败;补充空上下文兼容测试套件 51 项零失败;最终 `hl-resource-service` 全量 2158 项零失败、零错误(38 项条件跳过);`GatewayRouteAuditTest` 4 项零失败。 - 合并:主 PR #6519 合并提交 `3dc80ad69630127d91fa973a682051b2ef5c41d8`;验收发现旧兼容接口空上下文缺陷后,补充工单 #6532 / PR #6534 以最小修复合入,最新合并提交为 `aec1de2db2b4ed07d78877c1ccd4e532919bdaf3`。 - TEST 精确部署:Deploy Panel 任务 `9e69fd2c` 成功发布 `aec1de2db2b4ed07d78877c1ccd4e532919bdaf3`;构建退出码 0,任务期 10 次有效采样均至少 2 个运行进程、2 个健康启用 Nacos 实例,零不可用采样,部署窗日志无失败标记。 - 真实 Gateway:使用现有真实 `SUPER_ADMIN` 与同账号可切换的 `ADMIN` 身份,两条新接口的成功分页、字段白名单、独立总数、筛选、排序、边界参数和旧接口兼容均通过;`CUSTOMIZER` 返回 `403`,未认证返回 `401`。 - TEST 数据覆盖:目标供应商存在 18 条主体变更事实和 1 条主体审批事实;对应账户变更、账户审批均为 0,验证两个新接口没有跨域混页。审批样本为 `APPROVED` / `SYSTEM_AUTO_APPROVE`;环境没有外部审批人样本,未伪造业务数据。 - 身份说明:代码角色矩阵仍包含 `ADMIN`、`FINANCE`、`SUPER_ADMIN`。TEST 角色目录存在 `FINANCE`,但当前获批真实账号不能切换到该角色;按工单确认使用现有 `SUPER_ADMIN`(并覆盖 `ADMIN`)替代 FINANCE 实测,不创建、不修改、不伪造财务账号。 - 清理:验收全程只读,供应商数据指纹前后一致,业务测试数据创建数为 0;真实会话均已失效处理,无数据库、Redis、MQ 或临时配置需要清理。 ## 九、相关历史 PR | PR | Issue | 说明 | 是否仍有效 | |---|---|---|---| | #6486 | #6436 | 冻结供应商授权完整值与历史摘要语义 | ✅ 有效,本次沿用 | | #6519 | #6474 | 新增主体变更记录与主体审批流水独立分页 | ✅ 本次主功能 | | #6534 | #6532 | 修复旧兼容接口投影空上下文时的空指针 | ✅ 有效,保障历史兼容 | ## 十、相关文档 - 关联 Issue:[#6474](https://git.1814.love:8443/wx/HL/issues/6474) - 关联 PR:[#6519](https://git.1814.love:8443/wx/HL/pulls/6519) - 补充缺陷 Issue:[#6532](https://git.1814.love:8443/wx/HL/issues/6532) - 补充缺陷 PR:[#6534](https://git.1814.love:8443/wx/HL/pulls/6534) - 管理端接入:将“变更记录”“审批记录”分别切换到两条新接口;前端引用待回填。 ## 撤回 1. 管理端先停止请求两个新路径,并恢复使用旧 `approval-records/page`,避免代码回退窗口产生请求失败。 2. 从最新 `dev-v3` 创建独立回退分支,对主功能合并执行 `git revert -m 1 --no-edit 3dc80ad69630127d91fa973a682051b2ef5c41d8`,验证后经独立 PR 合入。 3. #6532 的空上下文修复可独立保留,它修复的是旧兼容接口且不依赖两个新路径。若明确要求连同该修复一起撤回,再按从新到旧顺序执行 `git revert -m 1 --no-edit aec1de2db2b4ed07d78877c1ccd4e532919bdaf3`;这样会重新引入旧兼容接口在特定记录上的 500 风险,不作为推荐方案。 4. 使用 Deploy Panel 两阶段客户端,仅滚动部署 `hl-resource-service`;本次无数据库、配置、Redis 或 MQ 恢复步骤,也无不可逆数据影响。 5. 撤回后经 Gateway 验证两个新路径不可用、旧兼容接口按选定回退范围正常,复测未认证和越权,并确认 Resource 双实例、Nacos、日志及零写入。 6. 同步发布本 Changelog 的撤回说明;不得仅改文件名表达状态。 ## 关联 / 联系人 ### 链接 - **Issue**: [#6474](https://git.1814.love:8443/wx/HL/issues/6474) - **PR**: [#6519](https://git.1814.love:8443/wx/HL/pulls/6519) - **Merge commit**: [`3dc80ad6`](https://git.1814.love:8443/wx/HL/commit/3dc80ad69630127d91fa973a682051b2ef5c41d8) - **补充 PR**: [#6534](https://git.1814.love:8443/wx/HL/pulls/6534) - **TEST 目标提交**: [`aec1de2d`](https://git.1814.love:8443/wx/HL/commit/aec1de2db2b4ed07d78877c1ccd4e532919bdaf3) ### 联系人 - **后端负责人**: @lc