docs: 交付 #6436 供应商完整字段契约
changelog-filename-gate / validate (push) Successful in 3s

这个提交包含在:
lc
2026-08-27 11:42:09 +08:00
父节点 e91101c037
当前提交 cf3fe30a0f
@@ -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 <admin-token>
```
#### 响应示例
```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 <admin-token>
```
#### 响应示例
```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 <admin-token>
```
#### 响应示例
```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<SupplierApprovalRecordRespVO>`
#### 使用场景
统一查看供应商主体与收款账户的变更历史,并区分完整新记录和不可恢复的历史掩码记录。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---:|---|---|
| `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 <admin-token>
```
#### 响应示例
```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