--- schema: "hl-changelog/v2" ticket: "6312" title: "供应商敏感字段与数值格式校验收紧" consumer: "admin" author: "lc(GIT)" change_type: "修改接口" backend_status: "deployed" gateway_status: "verified" frontend_status: "verified" frontend_owner: "mmg" frontend_ref: "187b7c3b" target_release: "" verified_at: "2026-08-25" 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 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 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`: ```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 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 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