# 【行为变更·错误码·管理后台 + 司机H5】车务审计修复批次（PR #3818 / #3819）

> **PR**: #3818（审计修复）、#3819（分层重构 + 唯一键并发锁）
> **服务**: hl-fleet-service | **更新时间**: 2026-06-15
>
> **存放目录**: `changelogs-v2/2026-06/`
> **影响范围**: 管理后台「司机审核 / 司机档案编辑 / 价格日历」 + 司机自助 H5 入职（免登录端）

---

## ⚠️ 关键说明

本批是车务服务一次**全量代码质量审计修复**，绝大部分是**内部重构**（分层归位 / 并发锁 / 事务合并 / N+1 查询优化 / 注释口径），**对前端无契约变化**——**无新增/删除端点，无字段结构变化，无字段改名**。

本 changelog **只列前端可感知的变化**，分三类：
1. **新增 3 个错误码** + 既有错误码的**新触发场景** —— 前端需补提示文案。
2. **司机 H5 入职端点输入校验加固** —— 前端 H5 需补客户端校验 + 限流后重试提示。
3. **个别编辑/删除的行为修复** —— 修掉前端原本会踩到的误拦/误清。

⚠️ 不需要前端改字段映射，只需按下表补错误码文案 + H5 端补几处校验。

---

## 1. 新增错误码（前端需补提示文案）

| code | 名称 | 触发端点 | 触发场景 | 建议前端文案 |
|---|---|---|---|---|
| **600404** | RENEW_TARGET_SEASON_ILLEGAL | `POST /admin/fleet/drivers/pending/{pendingId}/approve` | 审核**续签**记录时，目标司机当前处于**黑名单**状态（防止通过续签审核绕过授权静默解封黑名单司机） | 「该司机已被拉黑，不能通过续签审核解封，请走解除黑名单流程」 |
| **600310** | APPROVED_CANNOT_VOID | `POST /admin/fleet/h5/tokens/{pendingId}/void` | 对**已通过审核**的入职链接点「作废」 | 「该链接对应的司机已审核通过，不可作废」 |
| **600311** | ONBOARD_MODE_MISMATCH | `POST /app/h5/driver-onboard/submit/new`、`POST /app/h5/driver-onboard/submit/renew` | 用**新招** token 打到「续签提交」端点，或用**续签** token 打到「新招提交」端点 | 「入职链接类型不匹配，请使用正确的链接重新进入」 |

**既有错误码的新触发场景**（前端文案可能已有，确认覆盖即可）：

| code | 名称 | 端点 | 新增触发 |
|---|---|---|---|
| 600203 | PHONE_DUPLICATED | `…/pending/{pendingId}/approve` | 续签审核里司机改了手机号，且新手机号与另一在册司机重复 |
| 600503 | MODEL_NOT_FOUND | `DELETE /admin/fleet/pricing-calendar`（清除价格） | 传入不存在的车型 ID（之前静默成功删 0 行，现在明确报错） |
| 600501 | DATE_RANGE_INVALID | `DELETE /admin/fleet/pricing-calendar`（清除价格） | startDate 晚于 endDate（区间反转，之前静默无效，现在明确报错） |

---

## 2. 错误码语义修正（管理后台）

**端点**：`POST /admin/fleet/h5/tokens/{pendingId}/void`（作废入职链接）

- **旧行为**：对「已审核通过」的链接作废，返回 `600304`，消息「链接已作废」——与事实矛盾（该链接并未作废）。
- **新行为**：返回 **`600310`「已通过的链接不可作废」**。
- `600304`（LINK_VOIDED「链接已作废」）现在**只**用于校验真正已作废的链接。

> 前端：把「已通过不可作废」场景的错误码判断由 `600304` 改为 `600310`。

---

## 3. 司机 H5 入职端点输入校验加固（司机H5）

免登录 H5 端新增以下后端校验，前端最好同步补客户端校验提升体验：

### 3.1 证件 OCR 端点加文件校验 + 限流

**端点**：`POST /app/h5/driver-onboard/ocr/{step}`（multipart 上传证件照识别）

- **新增文件校验**：文件**非空** + **仅图片**（contentType `image/*`） + **≤ 10 MB**，不满足返 `100001`「参数非法: …」。
- **新增限流**：`@RateLimiter` **30 次 / 60 秒**（与短链端点一致），超限返限流提示。

```
# 反例：上传非图片或超大文件
POST /app/h5/driver-onboard/ocr/idcard  (multipart, file=xxx.pdf)
→ { "code": 100001, "message": "参数非法: 仅支持图片格式（jpg/png/webp）" }
```

> 前端：上传前先校验类型/大小；遇限流（短时间多次点）给「操作过于频繁，请稍后再试」提示。

### 3.2 提交端点加 mode 守卫

- `POST /app/h5/driver-onboard/submit/new` 现要求 token 是**新招**类型，否则返 `600311`。
- `POST /app/h5/driver-onboard/submit/renew` 现要求 token 是**续签**类型，否则返 `600311`。

> 防止链接被错用到另一端点造成档案污染/数据丢弃。正常流程前端按 init 返回的 `mode` 走对应页面即不会触发。

### 3.3 新招提交「自带车」跨字段必填

**端点**：`POST /app/h5/driver-onboard/submit/new`

- `hasOwnVehicle = true` 时，`vehicle` 对象必须非空，且车牌 `plate`、车型 `model` 关键项非空，否则返 `100001`。
- 之前 `hasOwnVehicle=true` 而 `vehicle` 为空会落库为空白车辆字段，审核侧拿到矛盾数据需人工驳回。

### 3.4 续签提交 `continueSeason` 必填

**端点**：`POST /app/h5/driver-onboard/submit/renew`

- `continueSeason`（是否继续合作）现为 **`@NotNull` 必填**，缺失返 `400`「是否继续合作不能为空」。
- 之前缺失会被后端兜底当「归档」处理，可能误归档司机。

### 3.5 入职链接 URL 为路径参数（仅文档订正，无行为变化）

- 后端产出的入职链接一直是**路径参数**形式 `…/driver-intake/{token}`（对齐前端路由 `/driver-intake/:token`）。
- 本次仅把 Swagger 文档/示例里残留的旧 query 形式 `?t={token}` 订正为 `/{token}`，**实际行为未变**。前端按现有路由即可。

---

## 4. 行为修复（管理后台）

### 4.1 编辑「单次投保」司机不再被误拦

**端点**：`PUT /admin/fleet/drivers/{driverId}`

- **旧问题**：编辑一个 `insuranceType=perTrip` 的司机时，若只回传 `insuranceType` 而**未重发** `insurancePlanId`，会被 `100001` 误拦（即便库里已有合法 planId）。
- **新行为**：库里已有 planId 时，编辑不重发 planId **放行**；仅当库里也没有、入参也没有时才报必填。

### 4.2 编辑司机驾照不再误清有效期

**端点**：`PUT /admin/fleet/drivers/{driverId}`

- 编辑 license 块时，`licenseExpire`（驾照有效期）改为**入参非空才覆盖**（对齐同块其它字段），不再因前端缺省 `expire` 而把库里的有效期清成 NULL。

### 4.3 gender 字段口径统一

- 列表 / 详情的 `gender` 取值统一为 **`0=未知` `1=男` `2=女`**（之前列表文档漏了 `0`）。司机性别可能返回 `0`，前端按「未知」渲染。

---

## 5. 投保 / 退保幂等加固（管理后台）

**端点**：
- `POST /admin/fleet/drivers/{driverId}/insurance/purchase`（投保，真实出单扣费）
- `POST /admin/fleet/drivers/{driverId}/insurance/policies/{insuranceOrderId}/cancel`（退保）

- 这两个端点现已加**幂等 + 分布式锁**：短时间内**重复提交**（双击 / 弱网重试）会被拦截，返回「投保处理中，请勿重复提交」/「退保处理中，请勿重复提交」。
- 前端**无需改动**，但可借此移除自己的临时防双击逻辑（后端已兜底，不会再重复扣费）。

---

## 6. 内部变更（无前端影响，仅备查）

以下为本批的内部重构，**不影响任何接口契约**，列出仅供排查时参考：

- **分层重构（C5-B）**：司机/车辆/车型三子域的跨域数据访问改为 Port/Service 收口（消除循环依赖），读路径返回数据不变。
- **常驻车唯一键（C8）**：车辆 `uk_primary_driver` 单列唯一键改复合 `(primary_driver_id, deleted_at)` + 并发锁，修掉「软删车残留常驻导致新绑定报 500」的潜在问题。绑定/解绑接口行为不变。
- **价格日历并发**：四个价格写端点统一全局写锁，修掉并发「单改+批改」落重复记录致整页 500 的竞态。
- **司机列表标签查询 N+1 优化**：列表返回结构不变。

---

> 有疑问或需要补具体 curl 示例的端点，随时找后端（wx）。
