diff --git a/changelogs-v2/2026-08/27_6436_供应商敏感字段完整回显与主类型-修改接口-管理后台.md b/changelogs-v2/2026-08/27_6436_供应商敏感字段完整回显与主类型-修改接口-管理后台.md new file mode 100644 index 00000000..d1e09be1 --- /dev/null +++ b/changelogs-v2/2026-08/27_6436_供应商敏感字段完整回显与主类型-修改接口-管理后台.md @@ -0,0 +1,777 @@ +--- +schema: "hl-changelog/v2" +ticket: "6436" +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 #6478、补充 PR #6483/#6486 已合并 dev-v3,最终提交 aa735fb8 已由 Deploy Panel 任务 7a48028c 发布 TEST;真实 TEST 身份已验证完整值、权限投影、失败零写入及测试数据清理。" +updated_at: "2026-08-27" +base: "dev-v3" +--- + +# 🔧 供应商敏感字段完整回显与主类型 + +供应商管理接口不再用掩码替代已授权管理员需要处理的业务原值,并补齐唯一主类型和注册账户摘要。证明附件仍受独立权限保护,不能因为本次完整值调整而绕过授权或同步读取审计。 + +## 一、背景 + +此前详情、账户和审批记录混用了原值、掩码与历史密文回退,草稿类型也缺少稳定的主类型回显,导致管理端无法可靠编辑、审核或回填表单。本次统一以下消费口径: + +- 通过既有供应商读取权限后,税号、法人证件、电话、证照号和银行账号返回完整业务值。 +- `proofFileUrls` 继续要求 `supplier:account:proof:read`,且只有同步读取审计成功后才返回;无权限时字段不序列化。 +- `types[].isPrimary` 与根对象 `primaryTypeCode`、`primaryTypeName` 成为主类型权威回显。 +- `*Mask` 字段保留兼容但已废弃,兼容期内与对应完整值字段同值;新代码应使用无 `Mask` 的字段。 + +## 二、变更接口清单 + +| # | 接口 | 方法 | 路径 | 变更类型 | 说明 | +|---:|---|---|---|---|---| +| 1 | 查询供应商基本信息 | GET | `/admin/supplier/items/{supplierId}/basic-info/view` | 响应修改 | 返回完整主体、联系人、资质字段及主类型 | +| 2 | 查询供应商账户信息 | GET | `/admin/supplier/items/{supplierId}/account-info/list` | 响应修改 | 返回可读账户完整账号,按权限决定附件字段 | +| 3 | 查询收款账户详情 | GET | `/admin/supplier/bank-accounts/{accountId}/view` | 响应修改 | 返回完整账号,按权限决定附件字段 | +| 4 | 查询供应商审批记录 | GET | `/admin/supplier/items/{supplierId}/approval-records/page` | 请求与响应修改 | 增加目标筛选及完整前后值可用性 | +| 5 | 创建供应商注册草稿 | POST | `/admin/supplier/items/add` | 响应修改 | `initialAccounts` 回显完整账号摘要 | +| 6 | 更新供应商 | PUT | `/admin/supplier/items/{supplierId}/update` | 响应修改 | 稳定回显主类型及完整初始账号摘要 | +| 7 | 提交供应商注册 | POST | `/admin/supplier/items/{supplierId}/submit` | 响应修改 | 审批结果增加完整初始账号摘要 | + +## 三、接口详情 + +### 1. 查询供应商基本信息 `GET /admin/supplier/items/{supplierId}/basic-info/view` + +**VO**: `SupplierBasicInfoRespVO` + +#### 使用场景 + +供应商详情和编辑表单初始化。管理端可直接使用完整字段,并用主类型字段初始化单选控件。 + +#### 入参 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|---|---|---|---:|---|---| +| `supplierId` | Path | String | 是 | 正整数 ID 字符串 | 目标供应商 | + +#### 出参 + +| 字段 | 类型 | 说明 | +|---|---|---| +| `supplierId` | String | 供应商 ID | +| `tax_no` | String | 完整主体证件号,JSON 名保持既有口径 | +| `legalRepresentativeIdNo` | String/null | 完整法人居民身份证号 | +| `legalRepresentativeIdCardFrontUrl` / `legalRepresentativeIdCardBackUrl` | String/null | 完整永久文件地址 | +| `contactPhone` | String/null | 完整公司联系电话 | +| `contacts[].contactPhone` | String | 完整联系人电话 | +| `qualifications[].certNo` | String/null | 完整证照编号 | +| `types[].isPrimary` | Boolean | 当前类型是否为主类型 | +| `primaryTypeCode` / `primaryTypeName` | String/null | 主类型值和展示名称;无类型草稿为 null | +| `legalRepresentativeIdNoMask` / `contactPhoneMask` | String/null | 废弃兼容别名,与完整值字段同值 | + +#### 请求示例 + +```http +GET /admin/supplier/items/2092800000000000001/basic-info/view +Authorization: Bearer +``` + +#### 响应示例 + +```json +{ + "code": 200, + "message": "成功", + "success": true, + "data": { + "supplierId": "2092800000000000001", + "fullName": "示例旅行服务有限公司", + "tax_no": "91350211M000100Y46", + "legalRepresentativeIdNo": "11010519491231002X", + "legalRepresentativeIdNoMask": "11010519491231002X", + "legalRepresentativeIdCardFrontUrl": "https://files.example.com/supplier/front.jpg", + "legalRepresentativeIdCardBackUrl": "https://files.example.com/supplier/back.jpg", + "contactPhone": "13800138000", + "contactPhoneMask": "13800138000", + "types": [{"typeCode": "HOTEL", "typeName": "酒店", "isPrimary": true}], + "primaryTypeCode": "HOTEL", + "primaryTypeName": "酒店", + "contacts": [{"contactName": "示例联系人", "contactPhone": "13900139000", "contactPhoneMask": "13900139000"}], + "qualifications": [{"qualType": "BUSINESS_LICENSE", "certNo": "LIC-2026-001", "certNoMask": "LIC-2026-001"}], + "updateTime": "2026-08-27 11:30:00" + } +} +``` + +#### 空数据 / 降级响应 + +无类型草稿返回 `types: []`、`primaryTypeCode: null`、`primaryTypeName: null`。联系人或资质为空时返回空数组;历史停用类型的名称无法从字典解析时,名称回退为类型值,不阻断详情。 + +#### 错误响应 + +```json +{ + "code": 395001, + "message": "供应商不存在", + "data": null, + "success": false +} +``` + +#### 业务边界 + +- 要求既有 `supplier:view` 和数据范围校验,登录态不能替代业务权限。 +- 普通应用日志、异常和 Trace 不记录上述完整值。 +- `*Mask` 仅用于旧客户端兼容,新代码不得继续依赖掩码语义。 + +### 2. 查询供应商账户信息 `GET /admin/supplier/items/{supplierId}/account-info/list` + +**VO**: `SupplierAccountInfoRespVO` + +#### 使用场景 + +在供应商结算信息区域展示已进入 PENDING、ACTIVE 或 DISABLED 的管理端可读账户。 + +#### 入参 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|---|---|---|---:|---|---| +| `supplierId` | Path | String | 是 | 正整数 ID 字符串 | 目标供应商 | + +#### 出参 + +| 字段 | 类型 | 说明 | +|---|---|---| +| `supplierId` | String | 供应商 ID | +| `bankAccounts` | Array | 可读账户,默认账户优先 | +| `bankAccounts[].accountNo` | String | 完整银行账号 | +| `bankAccounts[].accountNoMask` | String | 废弃兼容别名,与 `accountNo` 同值 | +| `bankAccounts[].proofFileUrls` | Array | 有独立权限且审计成功时出现;无权限时整个字段省略 | +| `bankAccounts[].status` | String | `PENDING`、`ACTIVE` 或 `DISABLED` | + +#### 请求示例 + +```http +GET /admin/supplier/items/2092800000000000001/account-info/list +Authorization: Bearer +``` + +#### 响应示例 + +```json +{ + "code": 200, + "message": "成功", + "success": true, + "data": { + "supplierId": "2092800000000000001", + "bankAccounts": [{ + "accountId": "2092800000000000011", + "accountName": "示例旅行服务有限公司", + "bankName": "示例银行", + "accountNo": "6222021234567890", + "accountNoMask": "6222021234567890", + "proofFileUrls": ["https://files.example.com/supplier/account-proof.pdf"], + "status": "ACTIVE", + "isDefault": "YES" + }], + "updateTime": "2026-08-27 11:31:00" + } +} +``` + +#### 空数据 / 降级响应 + +仅有 DRAFT 账户或没有账户时返回 `bankAccounts: []`。无 `supplier:account:proof:read` 时账号仍为完整值,但每个账户均省略 `proofFileUrls`,不是返回空数组。 + +#### 错误响应 + +证明附件同步审计不可用时失败关闭,不返回任何附件: + +```json +{ + "code": 395039, + "message": "暂时无法校验资源,请稍后重试", + "data": null, + "success": false +} +``` + +#### 业务边界 + +- 要求 `supplier:view`;附件另需 `supplier:account:proof:read`。 +- 每次获准的附件读取都会先同步写安全审计,失败时整个请求失败。 +- DRAFT 初始账户不会出现在该读取接口中,应使用创建/更新响应的 `initialAccounts` 展示草稿摘要。 + +### 3. 查询收款账户详情 `GET /admin/supplier/bank-accounts/{accountId}/view` + +**VO**: `SupplierBankAccountDetailRespVO` + +#### 使用场景 + +查看单个已进入可读状态的收款账户完整信息。 + +#### 入参 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|---|---|---|---:|---|---| +| `accountId` | Path | String | 是 | 正整数 ID 字符串 | 账户 ID | + +#### 出参 + +| 字段 | 类型 | 说明 | +|---|---|---| +| `accountId` | String | 账户 ID | +| `accountName` | String | 收款户名 | +| `accountNo` | String | 完整银行账号 | +| `accountNoMask` | String | 废弃兼容别名,与完整账号同值 | +| `proofFileUrls` | Array | 有独立权限且同步审计成功时出现,否则省略 | +| `status` / `isDefault` | String | 账户状态与默认标记 | + +#### 请求示例 + +```http +GET /admin/supplier/bank-accounts/2092800000000000011/view +Authorization: Bearer +``` + +#### 响应示例 + +```json +{ + "code": 200, + "message": "成功", + "success": true, + "data": { + "accountId": "2092800000000000011", + "accountName": "示例旅行服务有限公司", + "accountType": "CORPORATE", + "bankName": "示例银行", + "accountNo": "6222021234567890", + "accountNoMask": "6222021234567890", + "proofFileUrls": ["https://files.example.com/supplier/account-proof.pdf"], + "settleMode": "PREPAY", + "invoiceType": "NONE", + "status": "ACTIVE", + "isDefault": "YES", + "updateTime": "2026-08-27 11:31:00" + } +} +``` + +#### 空数据 / 降级响应 + +无附件权限时响应省略 `proofFileUrls`;DRAFT、已删除或不存在的账户按不可读处理,不返回草稿详情。 + +#### 错误响应 + +```json +{ + "code": 395001, + "message": "供应商不存在", + "data": null, + "success": false +} +``` + +#### 业务边界 + +- 完整账号沿用基础查看权限,证明附件使用独立权限和同步审计。 +- 账户必须属于未删除供应商且状态为 PENDING、ACTIVE 或 DISABLED。 +- 无附件权限时后端读取阶段即排除附件字段,不是先读取后隐藏。 + +### 4. 查询供应商审批记录 `GET /admin/supplier/items/{supplierId}/approval-records/page` + +**VO**: `SupplierApprovalRecordPageReqVO / PageResult` + +#### 使用场景 + +统一查看供应商主体与收款账户的变更历史,并区分完整新记录和不可恢复的历史掩码记录。 + +#### 入参 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|---|---|---|---:|---|---| +| `supplierId` | Path | String | 是 | 正整数 ID 字符串 | 目标供应商 | +| `page` / `limit` | Query | Integer | 否 | 正整数,`limit` 不超过 200 | 分页参数 | +| `approvalLogId` | Query | String | 否 | 正整数 ID 字符串 | 精确筛选审批 | +| `operationType` | Query | String | 否 | `CREATE/UPDATE/ENABLE/DISABLE/DELETE` | 操作类型 | +| `targetType` | Query | String | 否 | `SUPPLIER/ACCOUNT` | 本次增加的目标筛选 | +| `fieldName` / `status` | Query | String | 否 | 当前接口枚举 | 字段或状态筛选 | +| `from` / `to` | Query | String | 否 | 必须成对,`yyyy-MM-dd HH:mm:ss` | 时间范围 | + +#### 出参 + +| 字段 | 类型 | 说明 | +|---|---|---| +| `records[].targetType` | String | `SUPPLIER` 或 `ACCOUNT` | +| `records[].targetId` | String/null | 主体记录为 null,账户记录为账户 ID | +| `records[].oldValue` / `newValue` | String/null | 完整中文业务摘要,附件仍按独立权限投影 | +| `records[].valueAvailability` | String | `FULL` 或 `LEGACY_MASKED_UNRECOVERABLE` | +| `records[].oldValueMasked` / `newValueMasked` | String/null | 废弃兼容别名 | +| `total` / `page` / `pageSize` | Integer | 分页元数据 | + +#### 请求示例 + +```http +GET /admin/supplier/items/2092800000000000001/approval-records/page?page=1&limit=20&targetType=ACCOUNT +Authorization: Bearer +``` + +#### 响应示例 + +```json +{ + "code": 200, + "message": "成功", + "success": true, + "data": { + "records": [{ + "supplierId": "2092800000000000001", + "operationType": "ENABLE", + "targetType": "ACCOUNT", + "targetId": "2092800000000000011", + "oldValue": "账户状态:待审批;收款账号:6222021234567890", + "newValue": "账户状态:已生效;收款账号:6222021234567890", + "valueAvailability": "FULL", + "status": "已生效", + "operatorName": "测试管理员", + "operatorRole": "超级管理员", + "createTime": "2026-08-27 11:32:00" + }], + "total": 1, + "page": 1, + "pageSize": 20 + } +} +``` + +#### 空数据 / 降级响应 + +无匹配记录返回 `records: []` 和 `total: 0`。历史记录只保存掩码且无法恢复原值时,`valueAvailability` 返回 `LEGACY_MASKED_UNRECOVERABLE`,不会猜测或拼造完整值。 + +#### 错误响应 + +```json +{ + "code": 400, + "message": "目标类型仅支持SUPPLIER或ACCOUNT", + "data": null, + "success": false +} +``` + +#### 业务边界 + +- 要求 `supplier:approval:read` 及供应商数据范围权限。 +- 证明附件只有独立权限存在且同步审计成功时才进入完整摘要。 +- 分页内主体记录和账户记录按创建时间、记录 ID 稳定排序。 + +### 5. 创建供应商注册草稿 `POST /admin/supplier/items/add` + +**VO**: `SupplierDraftSaveReqVO / SupplierWriteRespVO` + +#### 使用场景 + +创建供应商草稿并可同时保存一个初始收款账户;成功响应直接回显该账户的完整账号摘要。 + +#### 入参 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|---|---|---|---:|---|---| +| `fullName` | Body | String | 是 | 非空白,最长 500 字符 | 供应商全称 | +| `taxNo` | Body | String | 是 | 6 至 64 字符 | 主体证件号 | +| `types` | Body | Array | 否 | 草稿可为空,非空不得重复 | 供应商类型,首项成为主类型 | +| `mainCooperation` | Body | String | 是 | 非空白 | 主要合作内容 | +| `initialAccounts` | Body | Array | 否 | 最多 1 项 | 初始收款账户完整输入 | + +#### 出参 + +| 字段 | 类型 | 说明 | +|---|---|---| +| `supplierId` | String | 新供应商 ID | +| `status` | String | `DRAFT` | +| `initialAccounts[].accountId` | String | 初始账户 ID | +| `initialAccounts[].accountNo` | String | 完整银行账号 | +| `initialAccounts[].accountNoMask` | String | 废弃兼容别名,与完整账号同值 | +| `initialAccounts[].status` | String | 创建时为 `DRAFT` | +| `updateTime` | String | 并发版本时间 | + +#### 请求示例 + +```json +{ + "fullName": "示例旅行服务有限公司", + "taxNo": "91350211M000100Y46", + "types": [{"typeCode": "HOTEL"}], + "mainCooperation": "酒店资源合作", + "initialAccounts": [{ + "accountType": "CORPORATE", + "bankName": "示例银行", + "accountNo": "6222021234567890", + "proofFileUrls": ["https://files.example.com/supplier/account-proof.pdf"], + "settleMode": "PREPAY", + "invoiceType": "NONE" + }] +} +``` + +#### 响应示例 + +```json +{ + "code": 200, + "message": "成功", + "success": true, + "data": { + "supplierId": "2092800000000000001", + "supplierNo": null, + "status": "DRAFT", + "onboardingStage": "PROFILE_DRAFT", + "initialAccounts": [{ + "accountId": "2092800000000000011", + "accountNo": "6222021234567890", + "accountNoMask": "6222021234567890", + "status": "DRAFT" + }], + "updateTime": "2026-08-27 11:30:00" + } +} +``` + +#### 空数据 / 降级响应 + +省略 `initialAccounts` 或传空数组时成功创建并返回 `initialAccounts: []`。草稿可传 `types: []`,此时详情的主类型字段为 null。 + +#### 错误响应 + +```json +{ + "code": 395002, + "message": "无权执行该供应商写操作", + "data": null, + "success": false +} +``` + +#### 业务边界 + +- 仅可信 `FINANCE`、`SUPER_ADMIN` 且拥有 `supplier:create` 的身份可写;`ADMIN` 明确拒绝。 +- 初始账户最多一项,完整请求原子成功或失败,失败不留下主体或子项。 +- 新写入和新审计使用完整业务值,但普通日志、异常、Trace 和跨服务消息不得携带这些值。 + +### 6. 更新供应商 `PUT /admin/supplier/items/{supplierId}/update` + +**VO**: `SupplierUpdateReqVO / SupplierWriteRespVO` + +#### 使用场景 + +增量更新供应商草稿或可变字段,并获得稳定的主类型与初始账户摘要回显。 + +#### 入参 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|---|---|---|---:|---|---| +| `supplierId` | Path | String | 是 | 正整数 ID 字符串 | 目标供应商 | +| `types` | Body | Array | 否 | 省略表示不改;空数组表示清空草稿类型 | 类型完整快照 | +| `changeReason` | Body | String | 是 | 非空白 | 变更原因 | +| `expectedUpdateTime` | Body | String | 是 | `yyyy-MM-dd HH:mm:ss` | 乐观并发版本 | + +#### 出参 + +| 字段 | 类型 | 说明 | +|---|---|---| +| `supplierId` / `status` | String | 供应商 ID 和当前状态 | +| `initialAccounts[].accountNo` | String | 既有初始账户完整账号 | +| `initialAccounts[].accountNoMask` | String | 废弃兼容别名 | +| `updateTime` | String | 更新后的并发版本 | + +#### 请求示例 + +```json +{ + "types": [{"typeCode": "HOTEL", "isPrimary": true}], + "changeReason": "调整主合作类型", + "expectedUpdateTime": "2026-08-27 11:30:00" +} +``` + +#### 响应示例 + +```json +{ + "code": 200, + "message": "成功", + "success": true, + "data": { + "supplierId": "2092800000000000001", + "status": "DRAFT", + "onboardingStage": "PROFILE_DRAFT", + "initialAccounts": [{ + "accountId": "2092800000000000011", + "accountNo": "6222021234567890", + "accountNoMask": "6222021234567890", + "status": "DRAFT" + }], + "updateTime": "2026-08-27 11:35:00" + } +} +``` + +#### 空数据 / 降级响应 + +省略 `types` 保持现有类型;草稿传 `types: []` 会清空类型并在详情返回 null 主类型。响应没有初始账户时固定返回空数组。 + +#### 错误响应 + +```json +{ + "code": 395014, + "message": "数据已被他人修改,请刷新后重试", + "data": null, + "success": false +} +``` + +#### 业务边界 + +- 仅可信 `FINANCE`、`SUPER_ADMIN` 且拥有 `supplier:update` 的身份可写。 +- 非空类型集合稳定保留且恰有一个主类型;提交审批时仍要求至少一个类型。 +- 并发版本、状态或权限不满足时失败且零写入。 + +### 7. 提交供应商注册 `POST /admin/supplier/items/{supplierId}/submit` + +**VO**: `SupplierSubmitReqVO / SupplierApprovalCommandRespVO` + +#### 使用场景 + +提交完整注册表单并进入审批;审批响应直接携带本次注册初始账户的完整摘要。 + +#### 入参 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|---|---|---|---:|---|---| +| `supplierId` | Path | String | 是 | 正整数 ID 字符串 | 草稿供应商 | +| `fullName` / `taxNo` | Body | String | 是 | 完整注册表单约束 | 主体信息 | +| `types` | Body | Array | 是 | 至少一项且不得重复 | 第一项为主类型 | +| `initialAccounts` | Body | Array | 否 | 最多一项;省略可保留既有草稿账户 | 初始账户完整快照 | +| `expectedUpdateTime` | Body | String | 是 | 必须等于当前版本 | 并发围栏 | + +#### 出参 + +| 字段 | 类型 | 说明 | +|---|---|---| +| `approvalLogId` / `requestNo` | String | 审批记录与幂等请求号 | +| `provider` / `approvalStatus` / `syncStatus` | String | 审批通道、结果与本地应用状态 | +| `initialAccounts[].accountId` | String | 本次注册关联账户 ID | +| `initialAccounts[].accountNo` | String | 完整银行账号 | +| `initialAccounts[].accountNoMask` | String | 废弃兼容别名 | +| `initialAccounts[].status` | String | 审批通过后为 `ACTIVE` | + +#### 请求示例 + +```json +{ + "fullName": "示例旅行服务有限公司", + "taxNo": "91350211M000100Y46", + "types": [{"typeCode": "HOTEL"}], + "mainCooperation": "酒店资源合作", + "licenseImageUrl": "https://files.example.com/supplier/license.jpg", + "qualifications": [{ + "qualType": "BUSINESS_LICENSE", + "certNo": "LIC-2026-001", + "imageUrl": "https://files.example.com/supplier/license.jpg", + "permanentValid": true + }], + "expectedUpdateTime": "2026-08-27 11:35:00" +} +``` + +#### 响应示例 + +```json +{ + "code": 200, + "message": "成功", + "success": true, + "data": { + "approvalLogId": "2092800000000000021", + "requestNo": "SUP-REQ-EXAMPLE", + "provider": "LOCAL_AUTO", + "approvalStatus": "APPROVED", + "syncStatus": "APPLIED", + "initialAccounts": [{ + "accountId": "2092800000000000011", + "accountNo": "6222021234567890", + "accountNoMask": "6222021234567890", + "status": "ACTIVE" + }], + "submittedAt": "2026-08-27 11:36:00", + "finishedAt": "2026-08-27 11:36:00" + } +} +``` + +#### 空数据 / 降级响应 + +没有初始账户时返回 `initialAccounts: []`。提交仍必须包含非空类型和当前完整注册资料,不因草稿阶段允许空类型而放宽。 + +#### 错误响应 + +```json +{ + "code": 395008, + "message": "请至少选择一个类型并设置唯一主类型", + "data": null, + "success": false +} +``` + +#### 业务边界 + +- 仅可信 `FINANCE`、`SUPER_ADMIN` 且同时拥有更新和提交权限的身份可执行。 +- 审批准备、结果应用、主体状态、账户状态和完整审计保持原有事务与幂等语义。 +- 权限、状态、摘要或并发围栏失败时不允许部分写入。 + +## 四、契约约束与正确调用方式 + +| 场景 | 正确处理 | +|---|---| +| 主类型绑定 | 使用 `primaryTypeCode`;类型列表使用 `types[].isPrimary`,不要自行取第一项猜测 | +| 完整字段 | 使用 `tax_no`、`legalRepresentativeIdNo`、`contactPhone`、`certNo`、`accountNo` | +| 废弃别名 | `legalRepresentativeIdNoMask`、`contactPhoneMask`、`certNoMask`、`accountNoMask` 仅作旧客户端兼容 | +| 证明附件无权限 | `proofFileUrls` 字段不存在;不要把缺字段当接口异常或空附件 | +| 草稿账户 | 从写响应 `initialAccounts` 获取;账户读取接口只返回 PENDING/ACTIVE/DISABLED | +| 业务失败 | HTTP 状态之外必须检查 `code`、`success`、`message` 和 `data` | + +管理端不得把业务请求体中的身份或角色作为授权依据,也不得缓存其他管理员读取到的完整敏感值供当前会话复用。 + +## 五、数据库行为 + +| 外部动作 | 可观察结果 | +|---|---| +| 创建/更新供应商 | 主体、联系人、资质、类型和初始账户在同一业务事务中成功或失败 | +| 注册提交 | 审批结果成功应用后主体和初始账户进入可读生效状态,响应返回完整账户摘要 | +| 新增或变更敏感值 | 后续授权读取返回与提交一致的完整业务值,唯一冲突或非法值整次失败 | +| 失败请求 | 未认证、无权、缺参、非法状态、并发冲突和审计失败均不留下部分业务写入 | +| 历史数据 | 可恢复历史值继续读取;只剩不可逆掩码的审批历史显式标记为不可恢复 | + +这些是接口可观察行为;调用方不依赖具体存储结构,也不应自行维护明文/密文兼容状态。 + +## 六、边界行为 + +- 未登录经 Gateway 返回 `401`。 +- `ADMIN` 可在既有查看权限与数据范围内读取完整业务值,但写入返回 `395002`。 +- `FINANCE`、`SUPER_ADMIN` 仍需同时拥有对应平台权限才能写,角色名称本身不是唯一授权条件。 +- 附件独立权限不足时字段省略;同步审计失败时请求失败,不降级泄露附件。 +- 列表 `limit=201`、缺少必填字段或非法状态返回业务失败,并保持零写入。 +- Snowflake ID 继续按字符串处理;时间格式继续为 `yyyy-MM-dd HH:mm:ss`。 + +## 六.5、枚举 / 数据字典 + +### `targetType` / `valueAvailability` + +| 字段 | 值 | 中文与说明 | +|---|---|---| +| `targetType` | `SUPPLIER` | 供应商主体变更,`targetId` 为 null | +| `targetType` | `ACCOUNT` | 收款账户变更,`targetId` 为账户 ID | +| `valueAvailability` | `FULL` | 完整业务前后值可用 | +| `valueAvailability` | `LEGACY_MASKED_UNRECOVERABLE` | 历史只剩不可逆掩码,不推测原值 | + +### 账户可读状态 + +| 值 | 中文 | 说明 | +|---|---|---| +| `PENDING` | 待审批 | 账户可在管理端读取 | +| `ACTIVE` | 已生效 | 正常可用账户 | +| `DISABLED` | 已停用 | 保留只读历史信息 | +| `DRAFT` | 草稿 | 不进入账户列表和账户详情 | + +## 六.6、修改前后对比 + +### 字段级对比 + +| 字段 | 改前 | 改后 | +|---|---|---| +| 税号、法人证件号、电话、证照号 | 主要返回掩码或混合口径 | 授权读取返回完整值 | +| `accountNo` | 可能为空或仅依赖 `accountNoMask` | 返回完整账号 | +| `*Mask` | 表示掩码 | 废弃兼容别名,暂与完整值同值 | +| `proofFileUrls` | 权限语义分散 | 独立权限 + 同步审计;无权时字段省略 | +| `types[].isPrimary` | 主类型回显不稳定 | 明确 Boolean 标记 | +| `primaryTypeCode/Name` | 不存在 | 根对象直接返回,空类型草稿为 null | +| `initialAccounts` | 写/提交响应摘要不完整 | 返回账户 ID、完整账号和状态 | +| 审批记录 | 主体与账户口径分散 | 统一目标类型、目标 ID、完整值和可用性 | + +### 行为级对比 + +| 行为 | 改前 | 改后 | +|---|---|---| +| 编辑页初始化 | 可能需要用掩码字段或猜测主类型 | 可直接绑定完整值与权威主类型 | +| 附件读取 | 容易把空值与无权混淆 | 无权省略字段,审计失败整体失败 | +| 草稿账户展示 | 读取接口与草稿状态语义不清 | 写响应展示草稿摘要,读取接口只展示可读状态 | +| 历史审批 | 无法区分完整值与不可逆掩码 | 通过 `valueAvailability` 明确区分 | + +## 六.7、影响评估 + +- **是否破坏向后兼容**: 否。既有字段名保留,废弃别名在兼容窗口内继续返回。 +- **前端是否必须同步上线**: 建议尽快。应切换到无 `Mask` 字段、接入主类型字段,并正确处理 `proofFileUrls` 缺失。 +- **前端 workaround 清理点**: 删除自行猜主类型、对 `accountNoMask` 二次掩码、把附件缺字段强制转空数组等兼容逻辑。 +- **敏感展示责任**: 完整值只在已授权业务页面按最小必要范围展示,不写入前端日志、埋点、错误上报或持久缓存。 + +## 七、不影响范围 + +- **仅影响**: 管理后台供应商基本信息、结算账户、注册提交与审批记录消费契约。 +- **零影响**: + - 不新增或修改 Gateway 路由。 + - 不修改角色、菜单、按钮或数据范围定义。 + - 不修改小程序、C 端、订单、产品、车队接口。 + - 不修改文件上传接口、Redis、MQ 或跨服务 DTO。 + - 不允许 DRAFT 账户通过账户查询接口提前暴露。 + +## 八、测试环境已验证 + +最终后端提交 `aa735fb8531a3463b2b460965245e07ed8697775` 已通过 Deploy Panel 任务 `7a48028c` 发布 `hl-resource-service` 双实例,真实 TEST 身份经 Gateway 验证: + +```text +历史兼容:完整值与数据库一致,税号关键字搜索命中 ✓ +SUPER_ADMIN:创建草稿、详情完整值、注册审批、账号及附件审计读取 ✓ +ADMIN:详情与完整账号可读,proofFileUrls 省略,写入拒绝 395002 ✓ +未认证 401、缺参 400、limit=201 为 400、非法状态 400 ✓ +新写主体/联系人/资质/账户及新审计均为完整值,失败请求零写入 ✓ +合成供应商完成精确清理:17 张相关表检查、业务残留 0、失败标记残留 0 ✓ +验收会话主动失效,敏感读取操作审计按审计规则保留 ✓ +``` + +获批真实账号没有 `FINANCE` 角色,因此没有伪造该身份;`FINANCE` 与 `SUPER_ADMIN` 的服务端写权限同构由后端自动化测试覆盖,真实 TEST 写入使用 `SUPER_ADMIN`,并用真实 `ADMIN` 验证拒绝路径。 + +## 九、相关历史 PR + +| PR | Issue | 说明 | 是否仍有效 | +|---|---|---|---| +| #6478 | #6436 | 完整字段、主类型、权限、迁移与兼容读写 | ✅ 主交付 | +| #6483 | #6481 | 修复 TEST 排序规则下回填精确比较 | ✅ 补充修复 | +| #6486 | #6484 | 支持历史 Unicode 税号回填 | ✅ 补充修复 | + +## 十、相关文档 + +- [主工单 #6436](https://git.1814.love:8443/wx/HL/issues/6436) +- [主 PR #6478](https://git.1814.love:8443/wx/HL/pulls/6478) +- [补充工单 #6481](https://git.1814.love:8443/wx/HL/issues/6481) / [PR #6483](https://git.1814.love:8443/wx/HL/pulls/6483) +- [补充工单 #6484](https://git.1814.love:8443/wx/HL/issues/6484) / [PR #6486](https://git.1814.love:8443/wx/HL/pulls/6486) +- 后端详细 API 说明:`docs/supplier/API-CHANGE-6436.html` + +## 关联 / 联系人 + +### 链接 + +- **Issue**: [#6436](https://git.1814.love:8443/wx/HL/issues/6436) +- **PR**: [#6478](https://git.1814.love:8443/wx/HL/pulls/6478) +- **最终 TEST 提交**: [aa735fb8](https://git.1814.love:8443/wx/HL/commit/aa735fb8531a3463b2b460965245e07ed8697775) + +### 联系人 + +- **后端负责人**: @lc