供应商敏感字段校验收紧交付(#6312)
changelog-filename-gate / validate (push) Successful in 2s

这个提交包含在:
lc
2026-08-25 12:39:57 +08:00
父节点 119c459808
当前提交 ded9289d98
@@ -0,0 +1,454 @@
---
schema: "hl-changelog/v2"
ticket: "6312"
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 #6322 已合并 dev-v3,合并提交 52a61a88 已由部署任务 b5d27d2b 发布到 TEST。供应商新增、修改、注册提交和独立新增收款账户现统一校验税号、电话、证照编号、银行账号、税率、金额、评分、枚举及业务 ID;接口路径、字段名和成功响应结构不变。"
updated_at: "2026-08-25"
base: "dev-v3"
---
# 供应商敏感字段与数值格式校验收紧
供应商新增、修改、注册提交和独立新增收款账户的输入契约现统一收紧。管理端必须在提交前按本文校验税号、电话、证照编号、银行账号、税率、金额、评分和枚举,并将供应商子表的 `Long` ID 作为规范正整数字符串发送。
本次没有新增、删除或重命名字段,也没有改变成功响应结构;但以前可能被接受的错误格式现在会返回参数错误,因此属于请求兼容性收紧。
## 🔧 关键变化
| 项目 | 修改前 | 修改后 |
|---|---|---|
| 敏感字段格式 | 部分入口仅校验非空或宽松长度,Controller 与 Service 口径可能不一致 | Controller 与 Service 使用同一业务口径,绕过 Controller 也不能写入非法值 |
| 银行账号 | 展示格式和规范化后的数字长度缺少完整门禁 | 去除空格、连字符后必须为 8 至 32 位数字 |
| 税率、金额、评分 | 个别入口可进入后续流程后才失败 | 请求入口即校验范围和小数位 |
| 子表业务 ID | `null`、数字令牌、零或溢出值可能被错误解释为新增或类型错误 | 只有字段省略表示新增;显式 ID 必须是规范正整数字符串 |
| 枚举 | DTO 与直接 Service 调用的口径可能不一致 | 人员规模、账户、结算、发票、合同类型和合同状态均双层校验 |
业务失败可能仍返回 HTTP 200。管理端必须同时判断统一响应中的 `code`、`success`、`message` 和 `data`。
## 变更接口清单
| # | 方法 | 路径 | 权限 | 变化 |
|---:|---|---|---|---|
| 1 | POST | `/admin/supplier/items/add` | `supplier:create` | 收紧主体、联系人、资质和初始账户输入 |
| 2 | PUT | `/admin/supplier/items/{supplierId}/update` | `supplier:update` | 收紧主体、子表 ID、合同和评价输入 |
| 3 | POST | `/admin/supplier/items/{supplierId}/submit` | `supplier:update` + `supplier:approval:submit` | 完整注册表单复用与新增一致的字段规则 |
| 4 | POST | `/admin/supplier/items/{supplierId}/bank-accounts/add` | `supplier:account:manage` + `supplier:approval:submit` | 独立账户复用初始账户的账号、结算和开票规则 |
## 通用字段规则
### 主体、联系人和资质
| 字段 | JSON 类型 | 空值语义 | 当前约束 |
|---|---|---|---|
| `taxNo` | String | 新增、提交必填;修改时省略表示不改 | 原始文本 6 至 64 位,只允许字母、数字、空格和连字符;服务端移除空格、连字符并转大写后仍须为 6 至 64 位字母或数字;可识别的统一社会信用代码、居民身份证同时校验日期或校验位 |
| `contactPhone` | String / null | 公司电话可省略 | 原始文本 7 至 20 位;去除空格、括号、连字符及 `+86`/`0086` 后,须为中国大陆手机号、带 `0` 的座机或 400/800 服务号 |
| `contacts[].contactPhone` | String | 新增、完整提交的联系人必填;修改既有联系人时省略表示不改 | 与公司电话使用同一规范化和号码类型规则 |
| `qualifications[].certNo` | String / null | 草稿可空;提交是否必填继续按资质规则判断 | 最大 128 个 UTF-8 字节;超过长度直接拒绝,读取仍只返回脱敏值 |
| `staffScale` | String / null | 省略表示不改或不设置 | 仅允许 `LT50`、`R50_200`、`R200_500`、`GT500`,区分大小写 |
电话示例:
| 输入 | 结果 |
|---|---|
| `+86 138-0013-8000` | 接受,按境内手机号规范化 |
| `010-12345678` | 接受,按境内座机规范化 |
| `1234567` | 拒绝,消息为 `公司联系电话格式不合法` 或 `联系人电话格式不合法` |
| `13800ABC000` | 拒绝,电话不能包含字母 |
### 收款账户、结算和开票
`initialAccounts[]` 与独立账户接口 `accounts[]` 使用同一规则:
| 字段 | 必填 | 当前约束 |
|---|---|---|
| `accountType` | 是 | `CORPORATE`(对公)或 `PERSONAL`(对私) |
| `bankName` | 是 | 非空,最大 500 字符 |
| `bankBranch` | 否 | 最大 500 字符 |
| `accountNo` | 是 | 只允许数字、空格和连字符;去除展示分隔符后必须为 8 至 32 位数字 |
| `settleMode` | 否 | `PREPAY`、`MONTHLY`、`SINGLE` |
| `accountPeriod` | 条件必填 | 只有 `MONTHLY` 必须填写;其他结算方式必须为空 |
| `invoiceType` | 否 | `SPECIAL`、`NORMAL`、`NONE` |
| `taxRate` | 条件必填 | `SPECIAL`/`NORMAL` 必填,`NONE` 必须为空;格式为 `0%` 至 `100%`,最多两位小数 |
合法税率包括 `0%`、`6%`、`6.5%`、`6.50%`、`100%`、`100.00%`。`5.123%`、`100.01%`、缺少 `%` 或负数均拒绝。
### 合同、评价和枚举
| 字段 | 当前约束 |
|---|---|
| `contracts[].amount` | `0` 至 `9999999999.99`,最多两位小数 |
| `contracts[].contractType` | `FRAME`、`SINGLE_TRIP`、`PURCHASE` |
| `contracts[].status` | `DRAFT`、`ACTIVE`、`EXPIRED` |
| `evaluations[].score` | `0` 至 `5`,最多两位小数 |
枚举均区分大小写。空值是否表示“不修改”继续由对应修改字段语义决定;显式提交未知值一律拒绝。
### 路径和子表业务 ID
路径中的 `{supplierId}` 必须大于 0。JSON 中以下可选子表 ID 使用同一严格规则:
- `contacts[].contactId`
- `qualifications[].qualificationId`
- `contracts[].contractId`
- `evaluations[].evaluationId`
- `evaluations[].orderId`
- `evaluations[].resourceId`
- `evaluations[].evaluator`
| 发送值 | 结果 |
|---|---|
| 字段省略 | 接受,表示新增子项或未建立可选关联 |
| `"1"` 至 `"9223372036854775807"` | 接受,必须是 1 至 19 位且在 Java `Long` 正整数范围内 |
| `null`、`""`、`"0"`、`"-1"` | 拒绝 |
| `"01"`、`" 1"` | 拒绝,不允许前导零或空白 |
| `1` | 拒绝,数字 JSON 令牌不能替代字符串 |
| `"9223372036854775808"` 或 20 位以上数字 | 拒绝,超出范围或长度 |
管理端更新已有子项时,应原样回传详情接口返回的字符串 ID;新增子项必须省略 ID 字段,不能传 `null`。
## 接口详情
### 1. 创建供应商草稿 `POST /admin/supplier/items/add`
请求头:
```text
Authorization: Bearer <accessToken>
Content-Type: application/json
```
典型合法请求:
```json
{
"fullName": "示例旅行服务有限公司",
"shortName": "示例旅行",
"taxNo": "91350211M000100Y46",
"contactPhone": "010-12345678",
"staffScale": "R50_200",
"mainCooperation": "住宿与地接服务",
"types": [
{ "typeCode": "HOTEL" }
],
"contacts": [
{
"contactName": "示例联系人",
"contactPhone": "+86 138-0013-8000",
"contactRole": "contentBus",
"isPrimary": true
}
],
"initialAccounts": [
{
"accountType": "CORPORATE",
"bankName": "示例银行",
"accountNo": "6222 0212 3456 7890",
"settleMode": "SINGLE",
"invoiceType": "SPECIAL",
"taxRate": "6.00%"
}
]
}
```
成功响应结构不变:
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": {
"supplierId": "2092000000000000001",
"supplierNo": null,
"status": "DRAFT",
"onboardingStage": "PROFILE_DRAFT",
"initialAccounts": [
{
"accountId": "2092000000000000002",
"accountNoMask": "6222********7890",
"status": "DRAFT"
}
],
"updateTime": "2026-08-25 12:35:00"
}
}
```
无效税号、电话、证照编号或初始账户会在供应商、联系人、账户和审批写入前失败。
### 2. 修改供应商 `PUT /admin/supplier/items/{supplierId}/update`
请求头:
```text
Authorization: Bearer <accessToken>
Content-Type: application/json
```
更新已有联系人时,`contactId` 必须使用字符串;新增联系人则省略该字段:
```json
{
"contactPhone": "+86 138-0013-8000",
"staffScale": "R200_500",
"contacts": [
{
"contactId": "2092000000000000010",
"contactPhone": "010-87654321",
"expectedUpdateTime": "2026-08-25 12:35:00"
},
{
"contactName": "新增联系人",
"contactPhone": "13900139000",
"contactRole": "contentMoney",
"isPrimary": false
}
],
"contracts": [
{
"contractName": "年度合作框架",
"contractType": "FRAME",
"amount": 1000000.50,
"status": "DRAFT"
}
],
"evaluations": [
{
"orderId": "2092000000000000020",
"score": 4.75,
"content": "履约正常"
}
],
"changeReason": "更新联系方式与合作资料",
"expectedUpdateTime": "2026-08-25 12:35:00"
}
```
成功响应仍为 `Result<SupplierWriteRespVO>`:
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": {
"supplierId": "2092000000000000001",
"supplierNo": null,
"status": "DRAFT",
"onboardingStage": "PROFILE_DRAFT",
"initialAccounts": [],
"updateTime": "2026-08-25 12:40:00"
}
}
```
任一子项 ID、金额、评分或枚举非法时,整个更新失败,既有主体与子表快照不变。
### 3. 提交供应商注册 `POST /admin/supplier/items/{supplierId}/submit`
请求头:
```text
Authorization: Bearer <accessToken>
Content-Type: application/json
```
提交接口接收完整注册表单,主体、联系人、资质和 `initialAccounts` 复用创建接口的全部规则,并要求 `expectedUpdateTime`:
```json
{
"fullName": "示例旅行服务有限公司",
"taxNo": "91350211M000100Y46",
"contactPhone": "010-12345678",
"mainCooperation": "住宿与地接服务",
"types": [
{ "typeCode": "HOTEL" }
],
"contacts": [
{
"contactName": "示例联系人",
"contactPhone": "13800138000",
"contactRole": "contentBus",
"isPrimary": true
}
],
"qualifications": [
{
"qualType": "BUSINESS_LICENSE",
"certNo": "LIC-2026-000001",
"imageUrl": "https://files.example.com/supplier/license.png"
}
],
"expectedUpdateTime": "2026-08-25 12:40:00"
}
```
成功响应结构不变:
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": {
"approvalLogId": "2092000000000000030",
"requestNo": "SUPPLIER-PROFILE-EXAMPLE",
"provider": "LOCAL_AUTO",
"approvalStatus": "APPROVED",
"spNo": null,
"spStatus": null,
"syncStatus": "APPLIED",
"submittedAt": "2026-08-25 12:41:00",
"finishedAt": "2026-08-25 12:41:00"
}
}
```
格式错误会在创建审批事实前失败;必备资质、并发版本、重复主体和状态机规则保持既有语义。
### 4. 独立新增收款账户 `POST /admin/supplier/items/{supplierId}/bank-accounts/add`
请求头:
```text
Authorization: Bearer <accessToken>
Content-Type: application/json
```
请求示例:
```json
{
"accounts": [
{
"accountType": "CORPORATE",
"bankName": "示例银行",
"bankBranch": "示例支行",
"accountNo": "6222-0212-3456-7890",
"settleMode": "MONTHLY",
"accountPeriod": "月结30天",
"invoiceType": "NORMAL",
"taxRate": "6%",
"proofFileUrls": [
"https://files.example.com/supplier/account-proof.png"
]
}
]
}
```
成功响应仍为按请求顺序返回的数组:
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": [
{
"accountId": "2092000000000000040",
"approvalLogId": "2092000000000000041",
"requestNo": "SUPPLIER-ACCOUNT-EXAMPLE",
"provider": "LOCAL_AUTO",
"approvalStatus": "APPROVED",
"spNo": null,
"spStatus": null,
"syncStatus": "APPLIED",
"accountStatus": "ACTIVE",
"isDefault": "NO",
"submittedAt": "2026-08-25 12:45:00",
"finishedAt": "2026-08-25 12:45:00"
}
]
}
```
账号、结算或开票组合非法时整批在账户和审批写入前失败。供应商尚未完成注册时,合法格式仍返回既有业务错误 `395010`(`请先完成供应商注册`)。
## 典型错误响应
字段格式失败统一使用业务码 `400`,例如:
```json
{
"code": 400,
"message": "收款账号规范化后必须为8到32位数字",
"success": false,
"data": null
}
```
| 场景 | `code` | 典型消息 |
|---|---:|---|
| `{supplierId}` 为 0 | `400` | `供应商ID必须为正数` |
| 路径 ID 超出 `Long` 范围 | `400` | `参数 supplierId 格式错误,请检查后重试` |
| 可识别的统一社会信用代码校验位错误 | `400` | `主体证件号的统一社会信用代码校验位不合法` |
| 公司或联系人电话不符合境内号码规则 | `400` | `公司联系电话格式不合法` / `联系人电话格式不合法` |
| 子表 ID 显式为 `null`、空、数字令牌、零或溢出 | `400` | `请求数据格式错误:字段 [...] 格式错误` |
| 账号规范化后少于 8 位或超过 32 位 | `400` | `收款账号规范化后必须为8到32位数字` |
| 税率超过 100% 或多于两位小数 | `400` | `税率必须为0%至100%且最多两位小数` |
| 合同金额超过范围或多于两位小数 | `400` | `合同金额最多10位整数和2位小数` |
| 评价分数超过 5 或多于两位小数 | `400` | `评分不能大于5` / `评分最多1位整数和2位小数` |
| 未认证 | `401` | 统一认证失败;HTTP 状态可能仍为 200 |
本次没有新增业务错误码;既有权限、供应商不存在、未注册、必备资质、乐观锁和审批状态错误继续使用原错误码。
## 管理端接入事项
1. 新建、编辑和提交表单按本文在前端同步限制长度、数字格式、范围、小数位和枚举,避免用户提交后才看到后端错误。
2. 银行账号输入可保留空格或连字符用于展示,但提交前应确认去除分隔符后为 8 至 32 位数字;不要使用 JavaScript `Number` 保存账号。
3. 税率必须保留 `%`;根据 `invoiceType` 联动必填和清空,根据 `settleMode` 联动 `accountPeriod`。
4. 所有子表业务 ID 使用字符串保存和提交。新增项省略 ID,不能传 `null`;禁止用 `Number(...)` 转换雪花 ID。
5. 合同金额和评分使用十进制定点输入,分别限制两位小数和对应范围。
6. 接口失败时保留用户表单,展示后端 `message`;不能只判断 HTTP 状态。
## 影响评估与未变化范围
- **是否破坏向后兼容**:对合法请求兼容;对以前可能被接受的非法格式不兼容。
- **前端是否需要同步**:需要。请求字段和响应结构未变,但表单校验、ID 序列化及结算/开票联动应对齐。
- **历史数据**:不迁移、不批量重写;存量数据读取不受影响,下次显式修改相关字段时按新规则校验。
- **权限与状态机**:不变,仍使用既有管理端角色、平台权限、乐观版本、聚合锁、幂等、审批、审计和软删除规则。
- **基础设施**:无数据库 migration、配置、Redis Key、MQ Topic、Feign 或 Gateway 路由变化。
- **响应与脱敏**:成功响应结构不变;税号、电话、证照号和账号读取仍只返回脱敏值。
## 验证证据
- 自动化:12 个供应商聚焦测试类全部通过;Resource 及依赖模块全量 2018 项测试零失败、零错误,38 项仓库既有条件跳过,13 个 Maven 模块构建成功。
- 独立审查:合并提交的补丁等价、字段边界、Controller/Service 入口一致性和失败零副作用测试均通过,未发现确定性缺口。
- TEST 部署:部署任务 `b5d27d2b` 成功,`hl-resource-service` 的 8182、8082 双实例依次启动;服务器运行提交确认包含合并提交 `52a61a88c`,Nacos 两实例均为 healthy、enabled。
- 真实 Gateway:使用真实有效 SUPER_ADMIN 登录完成 35 项验收,覆盖未认证、路径 ID、税号、公司电话、联系人电话、证照长度、初始/独立账户、税率、子表 ID、合同金额、评价分数、枚举、合法创建、脱敏读取、合法更新和业务状态失败。
- 零写入:合法草稿建立后的数据库快照为 `1` 个主体、`1` 个类型、`1` 个联系人、`1` 个初始账户、`0` 个审批;全部失败用例执行后快照完全一致。
- 清理:合成草稿通过既有删除接口成功清理,活动主体、类型、联系人、账户、评价和审批均为 0;按审计要求保留 3 条主体变更事实和 2 条账户变更事实,不含真实个人或企业数据。
- 运行健康:验收后 8082、8182 仍健康,Gateway 可达,两实例本次启动日志 `ERROR=0`;未触发文件、配置或 MQ 副作用,未生成需要清理的重复确认令牌。
## 撤回
1. 从最新 `dev-v3` 创建回退分支,执行 `git revert -m 1 --no-edit 52a61a88c906395d5cafd72cbda2390ad31a02a3`,经新的独立 PR 合入。
2. 重新构建并滚动部署 `hl-resource-service`,确认部署提交不再包含本工单变更。
3. 无数据库 migration、数据改写、配置、Redis、MQ 或跨服务契约变更,不需要执行 DDL、DML、缓存清理、消息补偿或配置恢复;现有业务和审计数据保持不变。
4. 回退后接口路径、字段名和成功响应结构仍不变,但输入接受范围恢复为本工单前的宽松口径;管理端若已上线严格校验可继续保持,避免重新接受明显非法值。
5. 经 Gateway 重跑合法新增、修改、提交、独立账户、未认证、失败零写入和脱敏读取用例,并确认 Nacos 双实例健康、服务日志无新增错误。
6. 若撤回本 Changelog,以新的文档提交删除本文件或标记撤回,不重写已发布历史。
## 关联 / 联系人
### 链接
- **Issue**: [#6312](https://git.1814.love:8443/wx/HL/issues/6312)
- **PR**: [#6322](https://git.1814.love:8443/wx/HL/pulls/6322)
- **合并提交**: [52a61a88c](https://git.1814.love:8443/wx/HL/commit/52a61a88c906395d5cafd72cbda2390ad31a02a3)
### 联系人
- **后端负责人**: @lc