349 行
14 KiB
Markdown
349 行
14 KiB
Markdown
---
|
||
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<VehicleTypeRespVO>`
|
||
|
||
| 字段 | 类型 | 说明 |
|
||
|------|------|------|
|
||
| id | Long | 大类 ID |
|
||
| typeKey | String | 大类 key |
|
||
| typeName | String | 大类中文名 |
|
||
| icon | String | 图标 |
|
||
| description | String | 描述 |
|
||
| sortOrder | Integer | 排序权重 |
|
||
| seatOptions | Array\<Integer\> | 座位数选项,新建恒为 `[]` |
|
||
| 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<VehicleTypeRespVO>`
|
||
|
||
| 字段 | 类型 | 说明 |
|
||
|------|------|------|
|
||
| id | Long | 大类 ID |
|
||
| typeKey | String | 大类 key(不可改) |
|
||
| typeName | String | 大类中文名 |
|
||
| icon | String | 图标 |
|
||
| description | String | 描述 |
|
||
| sortOrder | Integer | 排序权重 |
|
||
| seatOptions | Array\<Integer\> | 该大类下型号座位数选项,去重升序 |
|
||
| 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
|