# @Valid 校验失败 toast 去掉字段名前缀,提示更友好

> **服务**: hl-common-log (共享 module,影响全部 6 个后端服务)
> **PR**: #1916
> **Issue**: 无(从前端 BUG 通知 `09_frontend_notice_admin_service-edit-categorycode-required.md` 衍生的后端体验改善)
> **日期**: 2026-05-09
> **影响范围**: 全部 admin / mp / 内部接口的 `@Valid + @RequestBody` 触发的 400 校验响应文案

---

## ⚠️ 关键变化

后端全局 `MethodArgumentNotValidException` / `BindException` 处理器返回的 `Result.message` 文案格式变了:

| | 改前 | 改后 |
|---|---|---|
| **单字段错误** | `categoryCode: 分类编码不能为空` | `分类编码不能为空` |
| **多字段错误** | `name: 姓名不能为空; age: 必须为正数` | `姓名不能为空; 必须为正数` |
| **defaultMessage 为 null 时** | 字段名: null | 被过滤掉(filter `Objects::nonNull`) |

`Result.code` 仍是 `400`,HTTP status 仍是 `200`(项目惯例),只是 `message` 字符串格式不再带 Java 字段名前缀。

---

## 一、背景

正式环境 admin 后台,用户编辑服务时 toast 显示 `categoryCode: 分类编码不能为空`(同日 changelog `09_frontend_notice_admin_service-edit-categorycode-required.md` 已确认前端误调 POST 创建接口为根因)。即使前端修了误调接口,**`categoryCode:` 这种字段名前缀对最终用户来说是技术噪音 — 用户不该看到 Java 字段名**,所以后端这次顺手做了体验改善。

`hl-common-log` 是 6 个服务的共享异常处理 module,改一处全部受益。

---

## 二、改动接口清单

**所有 `@Valid + @RequestBody` 接口都受影响**(数量约 200+,无法一一列举)。常见高频:

- `POST /admin/*/save` / `POST /admin/*/create`
- `PUT /admin/*/update`
- `POST /mp/*/submit`
- 几乎所有带 `@RequestBody` 的写入接口

不影响:
- `@Validated` query / path 参数(走 `ConstraintViolationException`,本次未改)
- 业务异常(`BizException` 等)
- 系统异常(NPE / 序列化错误等)

---

## 三、对前端 (mmg) 的影响

### 1. 必看:有没有 `message` 解析逻辑

如果 hl-ui / 小程序里有以下代码,**会失效**:

```js
// 类似这样的 message 解析逻辑都需要排查
if (response.message.startsWith('categoryCode:')) { ... }
const [field, msg] = response.message.split(':')
const fieldName = response.message.match(/^(\w+):/)?.[1]
```

请 mmg 全局搜以下关键字检查:
- `\.message\.startsWith\(`
- `\.message\.split\(':?'\)`
- `\.message\.match`
- `\.message\.indexOf\(':'\)`

### 2. 实际更友好

绝大多数情况下,前端只是 `Toast(response.message)` 直接显示,本次改动让用户看到的提示**直接更友好**,**不需要任何前端改动**。

### 3. 多字段错误仍可读

多字段错误时仍以 `;空格` 分隔,例如:

```
姓名不能为空; 手机号格式不正确; 年龄必须为正数
```

足以让用户看清所有问题。

---

## 四、技术细节

### 改动前

```java
// hl-common-log/.../GlobalExceptionHandler.java
String message = e.getBindingResult().getFieldErrors().stream()
        .map(f -> f.getField() + ": " + f.getDefaultMessage())  // 拼字段名
        .reduce((a, b) -> a + "; " + b)
        .orElse("参数校验失败");
```

### 改动后

```java
String message = e.getBindingResult().getFieldErrors().stream()
        .map(FieldError::getDefaultMessage)
        .filter(Objects::nonNull)        // null 兜底,避免 toast 出 "null"
        .reduce((a, b) -> a + "; " + b)
        .orElse("参数校验失败");
```

`BindException` 处理同款改法。

---

## 五、为什么不顺手优化单个 message 文案

有人会想:`分类编码不能为空` 里"分类编码"对用户也不够友好,改成"请选择分类"更好。**本次不动**,理由:

- 改全局 1 处,200+ 接口统一受益,B 方案逐字段改 message 工作量翻 100 倍且新加字段会复发
- `defaultMessage` 文案优化属于 UX polish,可后续单独治理(并不紧急)
- 主目标是**去掉字段名前缀这个技术噪音**,主目标已达成

---

## 六、未覆盖

- `ConstraintViolationException`(`@Validated` 修饰 Controller + query/path 参数路径,如 `@RequestParam @Min(1)`)未改 — line 191 `e.getMessage()` 自带 `arg0.fieldName: 消息` 格式。本次主线 toast 不在此路径,风险/收益偏低,留待下次治理。
- 业务异常 `BizException`、自定义异常文案不动。

---

## 验收

无前端改动情况下,任何 `@Valid + @RequestBody` 接口校验失败时,前端 `response.message` 应不再带 `字段名: ` 前缀。
