--- schema: "hl-changelog/v2" ticket: "8226" title: "车型大类中文名空白校验收紧(全角空格等 Unicode 空白改为拒绝)+ 空名大类不再被挤出车型字典白名单" consumer: "admin" author: "jw(GIT)" change_type: "修改接口" backend_status: "deployed" gateway_status: "verified" frontend_status: "verified" frontend_owner: "mmg" frontend_ref: "9e0897c2934a6720fb9cbab05b1e14394c74c8f6" target_release: "" verified_at: "2026-09-23" status_note: "PR #8232 已合并 dev-v3(9231b8d8c),2026-09-23 14:30 滚动部署 TEST 双实例(检出 7a876ec57)。真实网关实测:新增/编辑车型大类 typeName 传全角空格、半角空格、空串、null 均返回 400「大类中文名不能为空」,库内零写入;读侧临时把 mpv 名称置空后,order-v3 车型白名单仍含 mpv(展示名回落为 mpv),验毕已原样还原。前端待办:车型大类表单的 typeName 必填校验需同样把全角空格视为空。前端已交付(2026-09-23):CategoryEditModal typeName 必填规则改 trim() 判空 validator,纯全角/半角空白提交前拦下(与后端 400 同口径),首尾带空格正常名放行且提交不 trim 原样发;新建组件 spec 4 例全绿。" updated_at: "2026-09-23" base: "dev-v3" --- # 车队车型管理: 车型大类中文名空白校验收紧 + 空名大类不再被挤出字典白名单 > **存放目录**: 二期(v3) → `changelogs-v2/2026-09/` > > **服务**: hl-fleet-service > **PR**: #8232 > **Issue**: #8226 > **日期**: 2026-09-23 > **影响范围**: 管理后台「车型管理」新增/编辑车型大类表单的 typeName 校验;order-v3 用车需求车型字典白名单(582032)的数据源 --- ## ⚠️ 关键变化(非必须,本版与上版行为不同 / 纠错 / 撤销时必写) - 本次变化:新增/编辑车型大类时,`typeName` 如果全部由**全角空格(U+3000)等 Unicode 空白**组成,现在返回 `400「大类中文名不能为空」`。 - 前端以前以为的:`typeName` 只要不是空串、不全是半角空格,后端就会接受。 - 实际新行为:以前后端用 `@NotBlank` 校验,它按 `String.trim()` 判空,只去掉半角空白,所以全角空格能写进库。可车队给 order-v3 的车型白名单按 `isBlank()` 判空名,会把这种名字当成空的、把整个大类从白名单里丢掉,结果这个大类下的用车需求被 `582032「不存在或已下线」` 误拒。现在两处同时收口:写侧拒绝这种名字;读侧即使库里出现空名,也不再把该大类挤出白名单。 --- ## 二、变更接口清单 | # | 接口 | 方法 | 路径 | 变更类型 | 说明 | |---|------|------|------|----------|------| | 1 | 新增车型大类 | POST | `/admin/fleet/vehicle-types` | 入参校验收紧 | `typeName` 全为 Unicode 空白(含全角空格)改为 400 | | 2 | 编辑车型大类 | PUT | `/admin/fleet/vehicle-types/{typeId}` | 入参校验收紧 | 同上 | --- ## 三、接口详情 ### 1. 新增车型大类 `POST /admin/fleet/vehicle-types` **VO**: `VehicleTypeSaveReqVO → VehicleTypeRespVO` #### 使用场景 管理后台「车队 → 车型管理」新增一个车型大类(如 SUV系列 / 商务车)时调用。 #### 入参 | 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | |------|------|------|------|------|------| | Authorization | Header | String | ✅ | - | 管理端登录令牌 | | typeKey | Body | String | ✅ | 非空白,≤16,全局唯一(600101) | 大类标识 key,如 `suv` | | typeName | Body | String | ✅ | **至少含一个非空白字符(空白按 Unicode 判定,全角空格也算空白,本次收紧)**,≤64 | 大类中文名 | | icon | Body | String | ❌ | ≤64 | 图标组件名或 emoji | | description | Body | String | ❌ | ≤256 | 大类描述 | | sortOrder | Body | Integer | ✅ | 非 null | 排序权重,升序 | #### 出参 `Result` | 字段 | 类型 | 说明 | |------|------|------| | id | Long | 大类 ID | | typeKey | String | 大类 key | | typeName | String | 大类中文名 | | icon | String | 图标 | | description | String | 描述 | | sortOrder | Integer | 排序权重 | | seatOptions | Array\ | 座位数选项,新建恒为 `[]` | | modelCount | Integer | 型号数,新建恒为 0 | | inUseCount | Integer | 在役车辆数,新建恒为 0 | #### 请求示例 ```json { "typeKey": "mpv", "typeName": "商务车", "icon": "", "description": "", "sortOrder": 2 } ``` #### 响应示例 ```json { "code": 200, "message": "成功", "data": { "id": "2064609312168083458", "typeKey": "mpv", "typeName": "商务车", "icon": "", "description": "", "sortOrder": 2, "seatOptions": [], "modelCount": 0, "inUseCount": 0 }, "success": true } ``` #### 空数据 / 降级响应 写接口没有空数据场景,也没有降级分支;校验不通过时零写入。 ```json { "code": 400, "message": "大类中文名不能为空", "data": null, "success": false } ``` #### 错误响应 `typeName` 为 null、空串、纯半角空格、纯全角空格,一律返回同一条错误,且只报一条: ```json { "code": 400, "message": "大类中文名不能为空", "data": null, "traceId": null, "success": false } ``` ```json { "code": 600101, "message": "车型大类标识已存在", "data": null, "success": false } ``` #### 业务边界 - 鉴权:未登录 → 业务码 `401`(「缺少有效的 Authorization 头」)。 - 只拒绝**全部**由空白组成的名字;首尾带空格的正常名(如 `" 商务车 "`)照常放行,后端不做 trim。 - 以前只有全角空格这一类会被放行,现在也拒绝;null、空串、半角空格在改前就被拒,行为不变。 --- ### 2. 编辑车型大类 `PUT /admin/fleet/vehicle-types/{typeId}` **VO**: `VehicleTypeSaveReqVO → VehicleTypeRespVO` #### 使用场景 管理后台「车型管理」编辑已有大类的名称、图标、描述或排序。`typeKey` 不可改,后端忽略入参中的 `typeKey`。 #### 入参 | 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | |------|------|------|------|------|------| | Authorization | Header | String | ✅ | - | 管理端登录令牌 | | typeId | Path | Long | ✅ | 大类须存在(600102) | 大类 ID | | typeName | Body | String | ✅ | **至少含一个非空白字符(空白按 Unicode 判定,全角空格也算空白,本次收紧)**,≤64 | 大类中文名 | | icon | Body | String | ❌ | ≤64;不传则不改 | 图标 | | description | Body | String | ❌ | ≤256;不传则不改 | 描述 | | sortOrder | Body | Integer | ✅ | 非 null | 排序权重 | #### 出参 `Result` | 字段 | 类型 | 说明 | |------|------|------| | id | Long | 大类 ID | | typeKey | String | 大类 key(不可改) | | typeName | String | 大类中文名 | | icon | String | 图标 | | description | String | 描述 | | sortOrder | Integer | 排序权重 | | seatOptions | Array\ | 该大类下型号座位数选项,去重升序 | | modelCount | Integer | 型号数 | | inUseCount | Integer | 在役车辆数 | #### 请求示例 ```json { "typeName": "商务车", "icon": "", "description": "", "sortOrder": 2 } ``` #### 响应示例 ```json { "code": 200, "message": "成功", "data": { "id": "2064609312168083458", "typeKey": "mpv", "typeName": "商务车", "sortOrder": 2, "seatOptions": [7], "modelCount": 1, "inUseCount": 0 }, "success": true } ``` #### 空数据 / 降级响应 写接口没有空数据场景,也没有降级分支;校验不通过时零写入,`update_time` 也不变。 ```json { "code": 400, "message": "大类中文名不能为空", "data": null, "success": false } ``` #### 错误响应 ```json { "code": 400, "message": "大类中文名不能为空", "data": null, "traceId": null, "success": false } ``` ```json { "code": 600102, "message": "车型大类不存在", "data": null, "success": false } ``` #### 业务边界 - 鉴权:未登录 → 业务码 `401`。 - 参数校验先于「大类是否存在」:`typeName` 为空白时,即使 `typeId` 不存在也返回 400,而不是 600102。 - 编辑时把名字清空是被拒绝的操作,不会把一个在营大类变成空名大类。 --- ## 四、契约约束与正确调用方式(接口类必写) > 本节只写**后端接受/拒绝 payload 的规则**,不写 UI 渲染建议。 ### ✅ 正确 / ❌ 错误 payload 对照 | 场景 | payload | 结果 | |------|---------|------| | ✅ 正常中文名 | `typeName: "商务车"` | 通过 | | ✅ 首尾带空格 | `typeName: " 商务车 "` | 通过,原样落库 | | ❌ 纯全角空格 | `typeName: "  "` | 400(**本次新增拒绝**) | | ❌ 纯半角空格 / 空串 / null | `typeName: " "` / `""` / 不传 | 400(改前已拒) | - 前端表单的必填校验应与后端一致,按「去掉**全部 Unicode 空白**后是否为空」判定。JS 的 `String.prototype.trim()` 会去掉全角空格(U+3000),可以直接用 `value.trim() === ''` 判空。 --- ## 五、数据库行为(涉及写操作时必写) - 无 Flyway migration、无 DDL、无表结构变更。`fleet_vehicle_type.type_name` 原本就是 `VARCHAR(64) NOT NULL`。 - 校验失败的请求零写入:TEST 实测 4 次 POST、2 次 PUT 被拒后,`fleet_vehicle_type` 全表逐字段(含 `update_time`)与请求前一致,也没有生成测试 key 的行。 - 存量处理成本为 0:TEST 上在架大类 4 行,`type_name` 都非空白。 --- ## 六、边界行为 - 未登录 → 业务码 `401`。 - `typeName` 只要含一个非空白字符就放行;是否是「像中文的名字」后端不判断。 - 读侧兜底(内部接口,前端不直接调用,写在这里方便排查 582032):如果库里仍出现空名大类(例如被直接改库),车队给 order-v3 的车型白名单(`GET /internal/fleet/vehicle-types/category-names`)**照样包含这个大类**,只是展示名回落为规范 key(如 `mpv`),同时车队服务打一条 WARN「车型大类名称为空,展示名回落到规范 key」。改前这个大类会从白名单里整个消失,导致用车需求报 `582032「车型 mpv 不在车型字典内(不存在或已下线)」`,而座位数校验照常通过。 - 同一规范 key 下有多行时,展示名取排序最前、且名称非空白的那一行。 --- ## 六.6、修改前后对比(修改/删除类接口必写,新增跳过) ### 字段级对比 | 字段 | 改前 | 改后 | |------|------|------| | `typeName` 校验 | `@NotBlank`:按 `trim()` 判空,全角空格放行 | `@NotNull` + Unicode 空白判定:全角空格等一律拒 | ### 行为级对比 | 行为 | 改前 | 改后 | |------|------|------| | 新增/编辑时 `typeName` 传全角空格 | 200,落库 | 400「大类中文名不能为空」,零写入 | | `typeName` 传 null / 空串 / 半角空格 | 400 | 400(同一条文案,只报一条) | | 库里存在空名在营大类时,order-v3 提交该大类的用车需求 | 582032 误拒(座位校验却通过) | 正常通过字典校验 | --- ## 六.7、影响评估(修改/删除类必写) - **是否破坏向后兼容**:只收紧了一种此前被接受的取值(`typeName` 全为全角空格等 Unicode 空白),这种名字本来就没有业务意义;请求/响应结构不变。 - **前端是否必须同步上线**:否。前端不改的话,用户输入全角空格会在提交后收到后端 400 文案,不会写坏数据。建议前端表单必填校验同步用 `trim()` 判空,在提交前就提示。 - **前端 workaround 清理点**:无。 --- ## 七、不影响范围(显式声明, 帮前端/QA 缩小排查面) - **仅影响**:新增/编辑车型大类的 `typeName` 校验;内部接口 `category-names` 对空名大类的取舍。 - **零影响**: - `typeKey`、`icon`、`description`、`sortOrder` 的校验规则。 - `GET /admin/fleet/vehicle-types`(树)、`/page`、`/list` 的响应结构与内容。 - 车型型号(`/admin/fleet/vehicle-types/{typeId}/models` 等)相关接口。 - order-v3 的 582032 / 582033 错误码与文案本身;名称正常的大类在白名单里的名称与顺序不变(TEST 实测 4 个在架大类改前改后一致)。 - 座位数选项接口 `seat-options` 的行为(本来就不看名称)。 --- ## 八、测试环境已验证 部署:PR #8232 合并 `dev-v3`(`9231b8d8c`),2026-09-23 14:30 滚动部署 TEST 双实例(检出 `7a876ec57`,含本单)。 构建身份(零写入判据):直连 8087 / 8187 两个实例,`PUT /admin/fleet/vehicle-types/999999999`(不存在的 id):`typeName` 传全角空格返回 400(旧代码会放行到 600102);同一 id 传正常名返回 600102(阴性对照)。 真实网关(`https://api.test.1814.love`,管理端 token): ``` POST /admin/fleet/vehicle-types 无 token → 401 缺少有效的 Authorization 头 ✓ POST /admin/fleet/vehicle-types typeName="  " → 400 大类中文名不能为空 ✓ POST /admin/fleet/vehicle-types typeName=" " → 400 大类中文名不能为空 ✓ POST /admin/fleet/vehicle-types typeName="" → 400 大类中文名不能为空 ✓ POST /admin/fleet/vehicle-types typeName=null → 400 大类中文名不能为空 ✓ PUT /admin/fleet/vehicle-types/{suv2 的 id} typeName=" " → 400 ✓ PUT /admin/fleet/vehicle-types/{suv2 的 id} typeName="" → 400 ✓ fleet_vehicle_type 全表前后逐字段一致(含 update_time)✓ ``` 读侧(内部接口,两个实例):把 `mpv` 的 `type_name` 临时依次改为空串、半角空格、全角空格,`category-names` 均返回 `mpv → "mpv"`,其余 3 个大类名称与顺序不变,`seat-options?vehicleType=mpv` 仍返回 `[7]`;验毕 `type_name` 与 `update_time` 均恢复原值(逐字段一致)。 单元测试:`VehicleTypeServiceTest` 28、`VehicleTypeSaveReqVOValidationTest` 5、`VehicleTypeControllerTest` 17、`VehicleTypeControllerIdempotentTest` 6、`FleetRedLineArchTest` 18,共 74 例 0 失败。 --- ## 十、相关文档 - 关联 Issue: [wx/HL#8226](https://git.1814.love/wx/HL/issues/8226) - 关联 PR: [wx/HL#8232](https://git.1814.love/wx/HL/pulls/8232) - 上游来源:#8202(子订单行程用车车型大类接入车队字典校验,582032) ## 关联 / 联系人 ### 链接 - **Issue**: [#8226](https://git.1814.love/wx/HL/issues/8226) - **PR**: [#8232](https://git.1814.love/wx/HL/pulls/8232) - **Merge commit**: [9231b8d8c](https://git.1814.love/wx/HL/commit/9231b8d8c) ### 联系人 - **后端负责人**: @jw - **前端负责人**: @mmg