# 【新增接口+前端回显修正·管理后台】司机编辑「关联车辆」改造——常驻车搜索下拉改绑 + 常开车型走车型库 + 回显修正

> 服务:hl-fleet-service(8087) | 分支:dev-v3 | PR:#3742 | 工单:#3739
> 已部署测试服并端到端实测通过(2026-06-12,过网关 9443 真 admin token,13/13 场景 PASS):绑定/改绑(自动解绑旧车)/解绑/605023 占用拒绝/幂等/600110/onlyResidentUnbound 筛选全部验证,测试数据已还原

## ⚠️ 关键说明(hl-ui 需改 4 处)

司机档案「编辑司机 → 关联车辆」这一块现状有三个问题:常驻车车牌是自由文本框、打开编辑不回显(卡片显示"常驻 蒙A-G8888"但编辑框空白)、常开车型大类 key 不回显。根因 + 改法如下:

1. **常驻车车牌「不回显」是前端读错字段**:司机详情接口**早已返回** `residentVehiclePlate`(权威常驻车,如 `蒙A-G8888`),前端「关联车辆」块当前绑的是另一个字段 `primaryVehiclePlate`(H5 司机自报参考车牌,非权威,多为空)。**请前端把常驻车的回显改读 `residentVehiclePlate`**。

2. **常驻车车牌改成「车辆搜索下拉 + 改绑」**(本次新增后端端点):选车下拉的数据源用 `GET /admin/fleet/vehicles/page?keyword=蒙A&onlyResidentUnbound=true`(只列**无常驻司机**的空闲车);选定后调**新端点** `PUT /admin/fleet/drivers/{driverId}/resident-vehicle` 绑定。**约束(产品拍定):只能绑当前无常驻司机的车;若目标车已有别的常驻司机,后端返 `605023`,提示去「车辆档案」换绑。**

3. **常开车型(大类 key + 车型名)改成「车型库下拉」**:车型字典接口**全部现成**,选型号自动带出 `preferredTypeKey`(大类 key)+ `preferredModel`(车型名),两字段随司机保存接口一起提交即可(后端早已接收)。巴特尔这类"只填了车型名没填大类 key"就是因为现在是手填文本框、历史没填,改下拉后从源头补齐。

4. **`primaryVehiclePlate` 字段保持不变**:它是 H5 司机自报参考车牌,H5/续签仍在用,admin 编辑表单这块**不要再用它绑常驻车**(改用第 2 点的新端点),但也不要删,后端契约不变。

## 1. 新增接口:改绑司机常驻车

`PUT /admin/fleet/drivers/{driverId}/resident-vehicle`

| 入参 | 类型 | 必填 | 说明 |
|---|---|---|---|
| driverId | path Long | 是 | 司机 ID |
| vehicleId | body Long | 否 | 目标车辆 ID(绑定/改绑);**不传或 null = 解绑本司机当前常驻车** |

**语义**

| 场景 | 行为 | 返回 |
|---|---|---|
| 绑定空闲车 | 该车常驻司机 = 本司机 | code 200 |
| 改绑到另一台空闲车 | **本司机原常驻车自动解绑** + 新车绑上 | code 200 |
| 目标车已是本司机常驻 | 幂等,不重复写 | code 200 |
| 目标车被**别的**司机占为常驻 | 拒绝,提示去车辆档案换绑 | **605023** |
| 解绑(vehicleId 不传/null) | 清掉本司机当前常驻车 | code 200 |
| 目标车不存在 | — | **600110** |
| 司机不存在 | — | **600205** |

> 个人自带车(ownerType=personal)绑定后,生命周期 service_status 会按该司机当前赛季自动联动(active→在役 / archived·blacklist→停用),前端无需处理。

**请求/响应示例**

```bash
# 绑定/改绑
curl -X PUT "https://api.test.1814.love:9443/admin/fleet/drivers/2065272148557021186/resident-vehicle" \
  -H "Authorization: Bearer {token}" -H "Content-Type: application/json" \
  -d '{"vehicleId":2064998140821319682}'
# → {"code":200,"message":"成功","data":null,"success":true}

# 目标车已被别人占(605023)
# → {"code":605023,"message":"目标车辆已有常驻司机（那顺），请到车辆档案换绑","success":false}

# 解绑
curl -X PUT ".../drivers/{driverId}/resident-vehicle" -H "Authorization: Bearer {token}" \
  -H "Content-Type: application/json" -d '{"vehicleId":null}'
# → {"code":200,...}
```

## 2. 修改接口:车辆分页新增筛选参数(下拉数据源用)

`GET /admin/fleet/vehicles/page` 新增可选 query 参数:

| 参数 | 类型 | 说明 |
|---|---|---|
| onlyResidentUnbound | Boolean | `true` = 仅返回**无常驻司机**的车(常驻车下拉数据源,只显示可绑的空闲车);**不传 / false = 全部(行为不变)** |

```bash
# 常驻车下拉:按车牌搜 + 只列空闲车
curl "https://api.test.1814.love:9443/admin/fleet/vehicles/page?keyword=蒙A&onlyResidentUnbound=true&pageSize=20" \
  -H "Authorization: Bearer {token}"
```

> 当前已绑的那台常驻车(回显值)由司机详情 `residentVehiclePlate` 单独给出,下拉里只需列空闲车供"换车"。

## 3. 常开车型下拉:用现成车型库接口(无新增)

| 用途 | 接口 | 返回 |
|---|---|---|
| 大类下拉 | `GET /admin/fleet/vehicle-types/list` | 全部大类(typeKey + typeName,不分页) |
| 大类+型号树 | `GET /admin/fleet/vehicle-types` | 大类含 models[](型号名/座位等) |
| 某大类下型号 | `GET /admin/fleet/vehicle-types/models/page?vehicleTypeId={id}` | 型号列表 |

- 「常开车型大类」下拉选项取 `vehicle-types/list` 的 `typeKey`(值)/`typeName`(显示),选定写入司机保存入参 `preferredTypeKey`。
- 「常开车型名」下拉取所选大类下的型号 `modelName`,写入 `preferredModel`。
- 两字段随司机新增/编辑接口(`POST /admin/fleet/drivers`、`PUT /admin/fleet/drivers/{driverId}`)正常提交,后端契约不变。

## 4. 端到端实测记录(测试服,2026-06-12)

```text
腾空 2 台车 → onlyResidentUnbound=true 返 2 台          PASS
绑定 A→蒙A-G8888                                        PASS
改绑 A→蒙A-H7777(蒙A-G8888 自动解绑)                    PASS
巴特尔绑已占的蒙A-H7777 → 605023(那顺占用)              PASS
幂等:A 再绑蒙A-H7777 → 200                              PASS
车不存在 → 600110                                       PASS
解绑 A → 200,resident 变 null                          PASS
还原:巴特尔→G8888 / D2→H7777 / 全 6 车绑回(空闲=0)     PASS
合计 13/13 PASS,测试数据已还原
```
