# 【契约核对·司机 H5】司机自助录入（新招/续签/OCR/init）字段名权威清单 — 逐字核对回执

> 端：司机 H5（自助录入）| 服务：hl-fleet-service `com.hulalv.fleet.h5` | 后端**零改动**（仅核对回执）| 2026-06-15
>
> 起因：前端反馈「硬必填字段已确定不会 400，但选填字段名是推断的」，请后端拿真实 VO 逐字核对。本文以**当前 dev-v3 代码实物**为准（非文档推断），逐项确认/纠正下列 5 个待核项。

## ⚠️ 关键说明（先看这 3 条，最容易踩）

1. **VO 类名两处纠正**：前端用的 `OcrRespVO` 实际叫 **`OcrResultVO`**；`InitRespVO` 实际叫 **`H5InitRespVO`**。`SubmitNewReqVO` / `SubmitRenewReqVO` 名字正确。
2. **驾龄字段是 `drivingYears`（Integer），不是 `years`**。
3. **🔴 准驾车型字段名「写读不一致」（后端真实如此，前端必须分别对待）**：
   - **提交**（submit/new）和 **OCR 返回**（OcrResultVO）里叫 **`licenseClass`**；
   - **续签预填**（init 的 prefill）里叫 **`licenseType`**。
   - 即：续签场景前端读 `prefill.licenseType` 回显，但若进 submit 入参得用 `licenseClass` 字段名。这是两个不同 VO、历史命名差异，后端本次不动（动它要前后端联调，且你们已在按它编码）。续签提交本身**不收准驾车型**（见 §3），所以实际只影响「续签页准驾车型回显读 `licenseType`」一处。

---

## 1. submit/new 新招提交 — `POST /app/h5/driver-onboard/submit/new`

入参 `SubmitNewReqVO`（`@RequestBody`，application/json）。**硬必填只有 2 个**：`token`、`licenseNo`；**其余全选填**（不传不会 400）。

### 1.1 顶层字段（逐字）

| 字段名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `token` | String | ✅ @NotBlank | H5 邀请凭证（URL 透传） |
| `name` | String | 选填 | 姓名 |
| `phone` | String | 选填 | 手机号 11 位 |
| `gender` | String | 选填 | **性别编码字符串**：`1`=男 / `2`=女 / `0`=未知（取值来自字典 gender，提交时字典校验，非法报 **100001**） |
| `nation` | String | 选填 | 民族 |
| `drivingYears` | Integer | 选填 | **驾龄（年）— 注意不是 `years`** |
| `idCard` | String | 选填 | 身份证号 18 位 |
| `idCardFrontUrl` | String | 选填 | 身份证正面 OSS URL |
| `idCardBackUrl` | String | 选填 | 身份证背面 OSS URL ← **前端推断的名字正确** |
| `licenseNo` | String | ✅ @NotBlank | 驾照号（#3781 与表单创建口径对齐，缺失返 400） |
| `licenseClass` | String | 选填 | 准驾车型 A1/A2/B1/B2 等 |
| `licenseExpire` | String(date) | 选填 | 驾照有效期 `yyyy-MM-dd`（LocalDate） |
| `licenseFrontUrl` | String | 选填 | 驾照正页 OSS URL |
| `licenseBackUrl` | String | 选填 | 驾照副页 OSS URL ← **前端推断的名字正确** |
| `hasOwnVehicle` | Boolean | 选填 | 是否自带车 |
| `vehicle` | object | 选填 | 自带车信息（hasOwnVehicle=true 时填，见 §1.2） |
| `preferredCategory` | String | 选填 | 常开大类 key（如 suv/mpv/bus，取自 init 的 `vehicleTypeOptions[].typeKey`） |
| `preferredModel` | String | 选填 | 常开型号（取自 `vehicleTypeOptions[].models[].modelName`） |
| `vehicleSource` | String | 选填 | 车源：仅 `own` / `company`（@Pattern，其它值 400） |
| `vehicleNotes` | String | 选填 | 备注 |
| `emergency` | object | 选填 | 紧急联系人嵌套块（见 §1.3） |
| `portraitUrl` | String | 选填 | 本人正脸照 OSS URL（头像） |

### 1.2 `vehicle` 嵌套块（`SubmitVehicleVO`）

| 字段名 | 类型 | 说明 |
|---|---|---|
| `plate` | String | 车牌号 |
| `model` | String | 车型名称 |
| `seats` | Integer | 核定座位数（含驾驶员） |
| `annualInspectDate` | String(date) | 下次年检日期 `yyyy-MM-dd` |
| `regFrontUrl` | String | **行驶证**正面 OSS URL（注意是 `reg` 不是 license） |
| `regBackUrl` | String | **行驶证**副页 OSS URL |
| `exteriorUrls` | List\<String\> | 车外观照片 URL 列表 |
| `interiorUrls` | List\<String\> | 车内饰照片 URL 列表 |

### 1.3 `emergency` 嵌套块（`SubmitEmergencyVO`）

| 字段名 | 类型 | 说明 |
|---|---|---|
| `name` | String | 紧急联系人姓名 |
| `phone` | String | 紧急联系人手机号 11 位 |
| `relation` | String | 与司机的关系（如「父亲」，自由文本） |

### 1.4 返回 `SubmitRespVO`

```json
{ "pendingId": "1234567890123456789", "estimatedReviewHours": 24, "season": null }
```
- `pendingId`：雪花 ID，**字符串格式**（已 ToStringSerializer，按字符串收）；`estimatedReviewHours`：新招=24；`season`：新招恒为 null。
- 错误码：400 参数校验 / **600300** token 无效 / **600301** token 已过期 / **600302** token 已使用 / **600200** 身份证已存在。

---

## 2. OCR 识别 — `POST /app/h5/driver-onboard/ocr/{step}`

### 2.1 step 步骤标识（路径参数，仅这 3 个合法值，其它返 400）

| step 值 | 用途 | 驾照即 §前端问的「ocr/license」 |
|---|---|---|
| `id-card` | 身份证 | |
| `license` | **驾照 ← 前端用的 `ocr/license` 路径正确** | ✅ |
| `reg-cert` | 行驶证 | |

### 2.2 入参（multipart/form-data，不是 JSON）

| 参数 | 必填 | 说明 |
|---|---|---|
| `token` | ✅ | form 参数 |
| `file` | ✅ | 图片文件，仅图片类型 ≤10MB（违反报 **100001**） |
| `side` | 选填 | `front` / `back`，默认 `front` |

### 2.3 返回 `OcrResultVO`（统一 VO，按 step 填对应字段，其余为 null）

| step | 该步会填的字段 |
|---|---|
| `id-card` | `name` / `idCard` / `gender`(1=男/2=女) / `nation` / `issuedBy`(签发机关) |
| `license` | `licenseNo` / `licenseClass` / `licenseExpire`(`yyyy-MM-dd`) |
| `reg-cert` | `plate` / `model` / `seats`(Integer) / `annualInspectDate`(`yyyy-MM-dd`) |

> 注意①：OCR 驾照返回的是 **`licenseClass`**（与 submit 一致，非 prefill 的 licenseType）。
> 注意②：**当前 OCR 是 stub**（`OcrStubService` 返回固定假数据，工单 #2797 真实 OCR 未接）；字段名/契约已稳定，可照此编码，但识别值现在是假的。
> 错误码：400 step 非法 / **100001** 文件空/非图片/超 10MB / **600300** token 无效 / **600301** token 已过期。

---

## 3. submit/renew 续签提交 — `POST /app/h5/driver-onboard/submit/renew`

入参 `SubmitRenewReqVO`。**只填变更项**。

| 字段名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `token` | String | ✅ @NotBlank | H5 邀请凭证 |
| `continueSeason` | **Boolean** | ✅ @NotNull | **是否继续合作：`true`=继续 / `false`=今年休息 ← 前端按 boolean 传，正确** |
| `phoneChanged` | Boolean | 选填 | 手机号是否已变更 |
| `phone` | String | 选填 | 新手机号（phoneChanged=true 时填） |
| `primaryVehiclePlateChanged` | Boolean | 选填 | 常驻车是否已变更 |
| `primaryVehiclePlate` | String | 选填 | 新常驻车牌（…Changed=true 时填） |
| `licenseRenewed` | Boolean | 选填 | 驾照是否已续期 |
| `licenseExpire` | String(date) | 选填 | 新驾照有效期 `yyyy-MM-dd`（licenseRenewed=true 时填） |
| `licenseFrontUrl` | String | 选填 | 新驾照正页 URL |
| `licenseBackUrl` | String | 选填 | 新驾照副页 URL |

- 续签**不收准驾车型**字段（无 licenseClass/licenseType 入参），所以 §0 那条不一致只影响「预填回显」不影响续签提交。
- 返回 `SubmitRespVO`：`{ pendingId, estimatedReviewHours: 12, season: "pending" }`。
- 错误码：400 / **600300** / **600301** / **600302**。

---

## 4. init 初始化（含续签预填档案结构）— `GET /app/h5/driver-onboard/init?token={token}`

返回 `H5InitRespVO`（前端用名 `InitRespVO`，真实类名 `H5InitRespVO`）。

### 4.1 顶层字段

| 字段名 | 类型 | 说明 |
|---|---|---|
| `mode` | String | `new`=新招 / `renew`=续签（决定走 8 步还是 4 步） |
| `tokenValid` | Boolean | =`tokenState∈{fresh,editable,rejected_editable}`；false 时按 tokenState 渲染失效文案 |
| `tokenState` | String | 7 态：`fresh` / `editable` / `rejected_editable` / `expired` / `voided` / `consumed` / `invalid` |
| `reviewStatus` | String | `pending` / `approved` / `rejected`（token 不存在时 null） |
| `rejectReason` | String | 驳回原因（仅 rejected 相关态有值） |
| `lastSubmittedAt` | String(datetime) | 上次提交时间（未提交过 null） |
| `expireAt` | String(datetime) | token 过期时间 |
| `stepConfig` | object | `{ totalSteps: new=8/renew=4, stepNames: [...] }` |
| `prefill` | object | **续签预填档案（仅 mode=renew 有值，新招 null）见 §4.2** |
| `vehicleTypeOptions` | List\<object\> | 常开车型字典（**仅 mode=new 且 token 可编辑**，renew 为 null）；第 5 步「常开车型」唯一合法数据源 |

### 4.2 `prefill` 续签预填结构（`RenewPrefillVO`）← 前端待核项「续签 init 预填档案结构」

| 字段名 | 类型 | 说明 |
|---|---|---|
| `name` | String | 司机姓名 |
| `phone` | String | 手机号**脱敏展示**（`138****1234`，仅展示，提交变更走 §3 的 phone） |
| `primaryVehiclePlate` | String | 常驻车车牌 |
| `licenseNo` | String | 驾照号**脱敏展示**（`150102******1234`） |
| `licenseType` | String | **准驾车型 ← 注意此处字段名是 `licenseType`，不是 licenseClass（见 §0 关键说明 3）** |
| `licenseExpire` | String(date) | 驾照有效期 `yyyy-MM-dd` |
| `licenseDaysLeft` | Long | 驾照距到期天数（前端展示续期提醒用） |
| `activeYears` | List\<Integer\> | 历次在册年份列表，如 `[2023,2024,2025]` |

### 4.3 `vehicleTypeOptions` 结构（仅新招用，`VehicleTypeOptionVO`）

```
[{ typeKey, typeName, icon, models: [{ modelName, seats }] }]
```
- 严禁带价格（basePrice 物理上不在该 VO，司机端不透出基准价）；前端禁写死内置 typeKey（运营动态字典）。

---

## 5. 图片上传补充（前端可能也要）— `POST /app/h5/driver-onboard/upload`

multipart：`token`(必填) / `file`(图片≤10MB) / `groupKey`(可选，默认 fleet-driver)；返回 `UploadRespVO` = `{ "url": "OSS公开URL" }`。所有 *Url 字段（idCardFrontUrl/licenseFrontUrl/portraitUrl/vehicle.regFrontUrl…）都回填这个 url。

---

## 核对结论汇总（对应前端 5 个待核项）

| 前端待核项 | 结论 |
|---|---|
| ① submit/new 选填字段名（drivingYears vs years、紧急联系人块） | 是 **`drivingYears`**；紧急联系人块字段名 `emergency.{name,phone,relation}`，全部按 §1.1/§1.3 |
| ② 照片 idCardBackUrl/licenseBackUrl | **推断正确**；另注意自带车行驶证是 `vehicle.regFrontUrl/regBackUrl`（reg 非 license） |
| ③ submit/renew continueSeason 类型 | **Boolean，正确**（@NotNull 必填） |
| ④ OCR ocr/license 步骤标识及返回字段名 | step=**`license`** 正确；返回字段见 §2.3（驾照填 licenseNo/licenseClass/licenseExpire）；类名是 **OcrResultVO** |
| ⑤ 续签 init 预填档案结构 | 见 §4.2；**唯一坑：准驾车型在 prefill 里叫 `licenseType`** |

> 后端本次零代码改动；以上为 dev-v3 当前代码实物核对。若真机测试某选填项「没落库」，优先核对字段名是否与本清单一致（尤其 drivingYears、vehicle 内 reg* 命名、prefill.licenseType）。
