# 产品 Step1-5 保存接口 JSR-303 校验真实生效

**日期**: 2026-04-23
**PR**: #1270 (Closes #1269) — PR #1259 后续补丁
**服务**: hl-product-service-v2 / hl-order-service-v2
**类型**: fix（修复校验漏补）

---

## 背景

PR #1259（装备建议字段改条目列表）里 SaveReqVO 加了 JSR-303 校验注解（`@NotBlank` / `@Size(max=20)` / `@Size(min=1, max=50)`），但 `AdminProductController` 的 Controller 方法未加 `@Valid` 修饰符，**校验注解实际未触发**。非法数据仍能 200 入库。

本次补丁把 Step1-5 全部保存接口及一个订单接口补齐 `@Valid`，校验现在真实生效。

---

## 影响接口（9 处）

| 接口 | Method |
|------|--------|
| `POST /admin/product/item/basic` | saveBasic |
| `POST /admin/product/item/{id}/itinerary` | saveItinerary |
| `POST /admin/product/item/{id}/route` | saveRoute |
| `POST /admin/product/item/{id}/price-calendar/batch` | savePriceCalendar |
| `POST /admin/product/item/{id}/schedule` | saveSchedule（2 个）|
| `POST /admin/product/item/{id}/schedule/batch` | batchCreateSchedule |
| `PUT /admin/product/item/{id}/supplement` | saveSupplement |
| `PUT /admin/product/item/{id}/detail-blocks` | saveDetailBlocks |
| `DELETE /admin/order/{orderId}/itinerary/pending/{editId}` | withdrawPendingEdit |

---

## 行为变化（前端必读）

### 之前（PR #1270 前）
```http
PUT /admin/product/item/{id}/supplement
{
  "equipmentList": [
    {"text": ""}          // 非法：空 text
  ]
}
→ 200 OK（错误地通过！数据存入 DB）
```

### 之后（PR #1270 后）
```http
PUT /admin/product/item/{id}/supplement
{
  "equipmentList": [
    {"text": ""}
  ]
}
→ 400 {
    "code": 400,
    "success": false,
    "message": "equipmentList[0].text: 装备文案不能为空"
  }
```

---

## 装备建议校验规则（生效中）

| 规则 | 违反时错误消息 |
|------|---------------|
| `equipmentList` 条目数 ≤ 20 | `equipmentList: 装备建议条目数不能超过20条` |
| 单条 `text` 非空非空白 | `equipmentList[0].text: 装备文案不能为空` |
| 单条 `text` 长度 1-50 字 | `equipmentList[0].text: 装备文案长度需在 1-50 字符之间` |

**注意错误消息格式**：`{字段路径}: {消息}`，含数组索引（`[0]`、`[1]` 等），前端可精确定位哪条条目出错。

---

## 前端建议

### 1. 前置校验（推荐）
admin 编辑器在提交前自行 validate 一次：
- 条目数 > 20 → 禁止提交 + toast
- 单条 text 空 → 禁止提交 + 红色高亮
- 单条 text 长度 > 50 → 禁止提交 + 高亮

避免走到后端才发现，体验更好。

### 2. 后端报错展示（兜底）
如果绕过了前置校验，后端 400 返回的 `message` 字段可直接 toast：

```javascript
const resp = await api.saveSupplement(req);
if (resp.code !== 200) {
  ElMessage.error(resp.message || '保存失败');
  return;
}
```

**错误消息已中文化**，直接展示即可。

### 3. 数组索引提示
如果要做精细错误提示，可解析 `equipmentList[N].text` 中的 N，滚动聚焦到对应条目输入框。

---

## 其他接口的校验

`basic / itinerary / route / price-calendar / schedule` 等其他 Step 接口的 SaveReqVO 本来就有 JSR-303 注解，但同样因为缺 `@Valid` 没有生效。**本次补丁一起修复**。

常见校验约束（以 `ProductBasicSaveReqVO` 为例）:
- `productType` 非空
- `name` 长度 ≤ 30 字
- `tripDays` ≥ 1
- 具体以 Knife4j `@ApiModelProperty` 中的 `required`/`allowableValues` 为准

如果前端之前传错数据能 200，现在可能 400，请以实际接口返回为准调整。

---

## 向后兼容

- **合法请求**：行为不变，仍 200
- **非法请求**：从"假成功（200 入脏数据）"变为"正确失败（400 + 可读消息）"
- 属于**修复性变化**，非破坏性

---

## 验证

测试服网关已 curl 4 场景全过：

```bash
# 合法 3 条 → 200
# 21 条 → 400 equipmentList: 装备建议条目数不能超过20条
# 空 text → 400 equipmentList[0].text: 装备文案不能为空
# 51 字 text → 400 equipmentList[0].text: 装备文案长度需在 1-50 字符之间
```
