---
schema: "hl-changelog/v2"
ticket: "5850"
title: "派车候选接口显式返回 selectable：统一选车弹窗空白复发的后端根治（四条组装路径全覆盖）"
consumer: "admin"
change_type: "修改接口"
author: "wx(GIT)"
backend_status: "deployed"
gateway_status: "not_required"
frontend_status: "not_required"
frontend_owner: ""
frontend_ref: ""
target_release: ""
verified_at: ""
status_note: "PR #5861 已合并 dev-v3 并部署测试服，网关 9443 实测：候选车辆 20 行 / 司机 20 行 selectable 全部非 null（19/20 车 true，1 行因阻断性冲突为 false）。前端 not_required——mmg 已于 313609db 把 normalizeCandidateVehicle 的缺省口径改成「undefined 视为可选、显式 false 才禁用」，与本次后端显式下发兼容；本条属于把前端的兜底假设升级成后端契约保证，前端零改动即受益。"
updated_at: "2026-08-11"
base: "dev-v3"
---

# 车务：派车候选接口显式返回 `selectable`（#5850）

> **服务**: hl-fleet-service
> **端点**: `POST /admin/fleet/assignments/candidates`
> **性质**: 新增响应字段（向后兼容，无破坏性变更）

---

## 一、为什么改

「统一选择车辆/司机」弹窗**反复出现整片空白**（后端返回 200、数据齐全，列表却渲染不出来）。

根因在前后端口径断层：

- 前端统一选车入口（`scope=slot` → `availableOnly=true`）用 `isFullyAvailableCandidate()` 过滤候选，该函数**要求 `selectable === true`**；
- 但后端从来**没有下发过 `selectable` 字段**（全仓零命中），前端 `normalize` 出来是 `undefined`；
- `undefined === true` 为 false → **整列被过滤成空**。单日入口 `availableOnly=false` 不走该过滤，所以只在统一选车弹窗复发。

前端已在 `313609db` 做了兜底（缺省视为可选）。但「弹窗空白」这个故障此前已复发多次，只靠前端约定"没有字段就当可选"是脆的——本次由后端把这个字段变成**显式契约**，从源头消除断层。

---

## 二、变更接口

| 方法 | 路径 | 变更 |
|------|------|------|
| `POST` | `/admin/fleet/assignments/candidates` | 响应新增 `selectable` 字段（车辆候选项 + 司机候选项，共 4 条组装路径） |

## 三、契约变更

`AssignmentCandidateRespVO` 的车辆候选项与司机候选项各**新增一个字段**：

| 字段 | 类型 | 说明 |
|------|------|------|
| `selectable` | `Boolean` | 本次选车场景下该候选是否可被选中。**恒非 null** |

### 与已有 `available` 的区别（务必分清）

| 字段 | 口径 |
|------|------|
| `available` | **纯资源维度**：该车/该司机在所选服务日期窗内是否空闲 |
| `selectable` | 在 `available` 为真的基础上，**再排除阻断性冲突** |

后端判定式：

```
selectable = available && conflicts 中不存在阻断项
```

其中「阻断项」= `conflicts[].blocking` **不是显式 `false`** 的项一律按阻断处理（null/缺省从严）。

> ⚠️ 「已被本弹窗其他槽位选中」属于**前端选车上下文**，后端不感知，**不纳入** `selectable` 口径。前端如需这层排除，仍按自己的选中态叠加过滤。

### 覆盖范围

四条候选组装路径**全部**下发该字段，不存在只有部分路径带字段的情况：

1. `vehicles.records[]` —— 车辆候选分页列表
2. `drivers.records[]` —— 司机候选分页列表
3. `selectedDriverResidentVehicles[]` —— 选定司机的常驻车辆
4. `selectedVehicleResidentDriver` —— 选定车辆的常驻司机

---

## 四、前端影响

**无破坏性变更，前端可零改动。** 但建议按下面的口径收敛：

- 渲染禁用态时**直接读 `selectable`**，不要再自己用 `available` + `conflicts` 二次推导——推导口径与后端不一致正是这次故障的来源。
- 保留「`undefined` 视为可选」的兜底（`313609db` 已做）作为防御，但正常情况下该字段恒有值。
- `selectable === false` 的行建议**置灰并展示 `availabilityReason`**，而不是从列表里过滤掉——车管需要知道"这辆车为什么不能选"。

---

## 五、验证证据（测试环境实测，2026-08-11，网关 9443，车务管理员 token）

```
POST /admin/fleet/assignments/candidates
```

| 项 | 实测 |
|----|------|
| `vehicles.records[]` | 20 行，`selectable` **全部非 null**；19 行 `true`，1 行 `false`（存在阻断性冲突） |
| `drivers.records[]` | 20 行，`selectable` 全部非 null |
| 兼容性 | `available` / `availabilityReasonCode` / `availabilityReason` / `conflicts` 语义与取值**均未变化** |

---

## 六、关联

- 工单 #5850、PR #5861
- 前端条目：`11_frontend_派单Step2需求展示区与统一选车弹窗空白-前端缺陷-管理后台.md`（同一故障的前端侧修复 `313609db`）
