--- schema: "hl-changelog/v2" ticket: "6436" title: "供应商敏感字段完整回显与主类型" consumer: "admin" author: "lc(GIT)" change_type: "修改接口" backend_status: "deployed" gateway_status: "verified" frontend_status: "verified" frontend_owner: "mmg" frontend_ref: "5957c58c" target_release: "v2.1" verified_at: "2026-08-27" 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