---
schema: "hl-changelog/v2"
ticket: "5284"
title: "建议车辆槽位由车务自由删减并按最终实派方案提交"
consumer: "admin"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "not_required"
frontend_status: "implemented"
frontend_owner: "hl-ui-pi"
frontend_ref: "d459b946"
target_release: ""
verified_at: ""
status_note: "最终实派方案行为已由 #5262 部署；#5284 收口既有接口语义并纠正 #5245 旧前端口径；管理后台已由提交 d459b946 完成槽位自由删减和最终方案 batch 提交并通过验证。"
updated_at: "2026-07-27"
base: "dev-v3"
---

# 车务：建议车辆槽位由车务自由删减

> **服务**: `hl-fleet-service`
>
> **工单**: [wx/HL#5284](https://git.1814.love:8443/wx/HL/issues/5284)
>
> **影响范围**: 车务管理 → 派单看板 → 订单派车弹窗的车辆槽位编辑与批量提交

## ⚠️ 关键纠错

截图中默认出现但没有删除入口的“车辆槽位 1”，是页面依据订单当前用车需求
`requiredVehicles[]` 展开的**定制师/订单建议槽位**，不是车务必须保留的最终槽位。

#5245 曾写“只删除新增且未提交的草稿槽位”。该表述只是在强调不能用本地删除撤销已有派车，
但被页面实现成了“建议生成的初始槽位不可删除”。**这个实现口径需要纠正：所有尚未提交的本地槽位都可删除，
包括建议生成的初始槽位和车务后加的槽位。最终配几辆、配什么车型由车务决定。**

边界保持不变：

- 编辑态允许暂时删至 0 个槽位，并保留“添加车辆槽位”入口；
- 最终提交时 `items[]` 仍必须包含 1–20 个完整槽位；
- 已经存在的 `holding/assigned/completed` 有效派车不是本地草稿，不能直接删除，继续走取消或改派流程。

## 变更接口

本次不新增 JSON 字段、路径或错误码，只明确现有接口的权威语义：

| 方法 | 路径 | 当前权威语义 |
| --- | --- | --- |
| `GET` | `/admin/fleet/board/orders` | `requiredVehicles[]` 和 `assignmentProgress.suggestedSlots` 是订单建议，仅作参考 |
| `GET` | `/admin/fleet/board/orders/<orderId>` | `suggestedVehicleCount` 是建议数量，`actualVehicleCount` 是车务当前实派数量 |
| `POST` | `/admin/fleet/assignments/batch` | `items[]` 是车务本次保留的**完整最终实派方案**，无需覆盖全部建议槽位 |

## 批量提交契约

### `items[]` 是完整最终方案

假设订单建议 2 辆 SUV，页面可以删除两个建议槽位后重新添加 1 个槽位，并提交：

```json
{
  "orderId": "2080000000000000001",
  "requirementId": "2080000000000000002",
  "startDate": "2026-08-04",
  "endDate": "2026-08-07",
  "holdMode": 1,
  "requestId": "fleet-final-plan-example",
  "items": [
    {
      "fleetItemIndex": 5,
      "vehicleId": "2080000000000000101",
      "driverId": "2080000000000000201"
    }
  ]
}
```

规则：

- `items[]` 数量可以少于、等于或多于建议数量，但必须为 1–20；
- `fleetItemIndex` 可沿用建议索引，也可使用未占用的新索引，不要求连续；
- 批内 `fleetItemIndex`、车辆和司机各自唯一；
- 未被最终方案保留的 `unassigned` 建议占位会退出待派和完成条件；
- 若漏传已有 `holding/assigned` 等在途槽位，后端拒绝整批提交并提示先取消，不会静默删除已有派车；
- 任一槽位失败仍整批回滚，前端不得循环调用单派接口替代批量提交。

### 看板建议数与实派数分离

最终方案提交后可能出现：

```json
{
  "assignmentProgress": {
    "suggestedSlots": 2,
    "finalizedByFleet": true,
    "totalSlots": 1,
    "unassignedSlots": 0,
    "holdingSlots": 1,
    "assignedSlots": 0,
    "completedSlots": 0,
    "canceledSlots": 0
  }
}
```

此时 `suggestedSlots=2` 只保留建议事实，页面进度、完成条件和最终车辆数都按 `totalSlots=1` 计算。

## 前端展示矩阵

| 场景 | 数据源 | 页面行为 | 守恒规则 |
| --- | --- | --- | --- |
| 订单建议 | `requiredVehicles[]`、`suggestedSlots` | 标注“建议”，只作为车型/座位/数量参考 | 不决定最终槽位数 |
| 建议生成的初始槽位 | 前端未提交草稿 | 与新增草稿相同，显示删除入口 | 可见草稿槽位均可删除 |
| 车务新增槽位 | 前端未提交草稿 | 显示删除入口 | 可见草稿槽位均可删除 |
| 删除到 0 个 | 本地草稿为空 | 展示空态和“添加车辆槽位”，禁用下一步/提交 | 编辑态可为 0，提交态最少 1 |
| 最终批量提交 | `items[]` | 只提交车务最终保留的槽位，不补回已删除建议 | 可见最终槽位与 `items[]` 一一对应 |
| 已有有效派车 | `activeAssignments[]` | 不显示本地“删除草稿”；使用取消/改派动作 | 不静默撤销 `holding/assigned` |
| 最终进度 | `assignmentProgress` | `finalizedByFleet=true` 后按 `totalSlots` 展示 | 状态数量之和等于最终 `totalSlots` |

## 前端处理清单

- [ ] 移除 `slot.isNewDraft === true` 对删除按钮的唯一门控；建议生成的未提交初始槽位也必须可删除。
- [ ] 删除任一未提交槽位后同步清理该槽位的车辆、司机、逐日车费和跨常驻确认草稿，不能残留参与提交。
- [ ] 删除当前槽位后切到相邻槽位；删至 0 个时进入明确空态，不读取已删除槽位的 picker 状态。
- [ ] 0 槽位时保留“添加车辆槽位”，并禁用下一步/最终提交；不要向后端发送空 `items[]`。
- [ ] 最终 payload 只由当前可见槽位生成；不得按 `requiredVehicles[]` 数量补回已删除建议槽位。
- [ ] `requiredVehicles[]`、`suggestedVehicleCount`、`suggestedSlots` 的文案统一为“建议/参考”，不得展示为车务必配数量。
- [ ] 已有 `activeAssignments[]` 不走本地草稿删除；继续使用后端下发的取消/改派动作。
- [ ] 雪花 ID 继续按字符串消费，`fleetItemIndex` 不要求连续且不得因删除后重排而串到已有稳定槽位。

## 不影响范围

- 本工单不修改 `hl-ui`，不在 `mmg/hl-ui` 建工单；`frontend_status` 保持 `pending`。
- 不修改派单状态机、车辆/司机占用、通知、保险、对账、逐日车费、Outbox 或数据库结构。
- 不放宽空批次：编辑态可删至 0，不等于后端接受空 `items[]`。
- 不允许通过漏传已有有效派车实现静默删除。

## 验证证据

- 后端最终方案能力来自已部署的 #5262；#5284 未新增运行时行为，因此 `gateway_status=not_required`。
- 定向回归 5 项通过：建议数与最终数可不同、遗漏建议占位不阻塞完成、已有在途槽位漏传阻断、看板建议/实派分离、空 `items[]` 拒绝。
- Fleet 全量 `test`：2431 项，0 失败、0 错误、1 跳过。
- Fleet 模块 `spotless:check`：通过。
- `mvn -pl hl-fleet-service -am verify`：2431 项，0 失败、0 错误、1 跳过，JAR 构建成功。
- OpenAPI/oasdiff：`not_configured`；已人工比对路径、字段、类型和必填性，确认只有 Swagger/Javadoc 语义收口。
- Consumer Contract：`not_required`；没有 internal Feign 或 shared Java 变化。
- 前端状态：`pending`；领取后按 `pending → claimed → implemented → released → verified` 真实流转。
