---
schema: "hl-changelog/v2"
ticket: "5824"
title: "换版后看板缺口消失 / 订单车控误标 DONE 修复：assignmentProgress 新增 staleFinalizedPlan，finalizedByFleet 语义收窄为「已对当前用车需求定稿」"
consumer: "admin"
change_type: "修改接口"
author: "wx(GIT)"
backend_status: "deployed"
gateway_status: "not_required"
frontend_status: "implemented"
frontend_owner: "mmg"
frontend_ref: "aa9c087b"
target_release: ""
verified_at: "2026-08-11"
status_note: "后端 PR #5843 已合并 dev-v3 并部署测试服，26-5067 双出口实测通过。前端已实现(aa9c087b)：BoardSlotSummary 在 assignmentProgress.staleFinalizedPlan===true 时头部挂「需求已换版 · 请核对车型与数量」warning 轻提示(严格 === true,旧契约缺省不误亮,不做按钮禁用)。其余 checklist 实证零改动——finalizedByFleet 前端零消费；「已派几台车」boardSummary 已走 assignmentProgress.assignedSlots/totalSlots 不数 assignmentSlots;CANCELED 槽位渲染层已按 assignmentStatus 区分(display.js canceled 态样式 + OrderDrawer 着色与短信/费用编辑排除);待派车 tab 记录级状态由后端驱动。测试:board-slot-summary 10/10(新增 3 分支用例),fleet 全域 66 文件 619 用例全绿,checkpoint light 全过。"
updated_at: "2026-08-11"
base: "dev-v3"
---

# 车务看板：换版后缺口重新露出 + 订单车控不再误标完成（#5824）

> **服务**: hl-fleet-service
> **日期**: 2026-08-11
> **含 Flyway**: `20260811.005`（`fleet_assignment` 新增 `plan_finalized_requirement_id` 定稿归属列）
> **无新增端点 · 无字段删除 · 无请求参数变化 · 无需改网关路由**

---

## 背景（一句话）

`#5810` 换版整槽平移只改派单行的 `requirement_id`，「已定稿」标记原样保留，于是**上一版需求的定稿被当成当前版需求的定稿**：
看板把缺口整体抹平（多要的车看不见），订单侧还会据此回发最终方案把车控标成已完成（实测 26-5067 少 1 台车仍 DONE）。
本次给定稿加上「归属哪一版需求」的证据，读侧据此区分「当代定稿」与「换版平移来的陈旧定稿」，修两个出口：
①看板缺口重新露出（下文「变更接口」）；②订单车控不再误标完成（下文「订单车控状态（order-v3 侧）」）。

---

## 变更接口

### `GET /admin/fleet/board/orders` —— `assignmentProgress` 结构变化

#### 1. 新增字段 `staleFinalizedPlan`（Boolean，恒非 null）

| 取值 | 含义 |
|---|---|
| `true` | 该需求名下**存在任一非终态派单行携带「上一版需求的定稿」**：换版后旧派单被整槽平移过来，车务尚未对这些行按当前需求重新定稿。判据是**析取**——当代定稿行与陈旧定稿行并存（例如部分槽已按新需求重新确认、部分槽仍是平移过来的旧定稿）时同样为 `true`，不要求「只剩」旧定稿 |
| `false` | 全部定稿行都归属当前需求（当代定稿）/ 未定稿 / 历史存量行无归属证据（新列上线前的行，按兼容口径视同当代）/ **携带陈旧归属的行已是终态**（`canceled`、`completed` 一律豁免，不判 `true`）|

> 终态豁免的原因：改派与按天改派只把行置为 `canceled`、不清定稿标记，完结行同理不会再被刷进新方案；
> 不豁免就会让这些永远动不了的历史行把需求**永久**钉成「陈旧」（看板永久缺口提示、订单车控永久卡处理中）。

**它只是提示，不一定改判**：只有 `staleFinalizedPlan=true` **且确有缺口**（`suggestedSlots` > 现有槽位数）时才撤销定稿权威。
换版只改车型或人数、车数没变时（例：V1 商务车×1 → V2 SUV×1），本字段仍为 `true`，但计数与卡片状态**一律不动**——
车型/数量不符按 `#5810` 已定口径只做提示、不做门禁。

**前端建议**：`staleFinalizedPlan=true` 时在卡片上挂一个「需求已换版，请核对车型与数量」的轻提示，配合已有的
`requirementFleetText`（当前需求车队组成语句）让车管一眼比对。不要用它做任何按钮禁用。

#### 2. `finalizedByFleet` 语义收窄

| | 旧语义 | 新语义 |
|---|---|---|
| `finalizedByFleet=true` | 车务已提交最终实派方案（不区分是哪一版需求的方案） | 车务**已对当前用车需求**提交最终实派方案 |

只有当「陈旧定稿 + 确有缺口」同时成立时，`finalizedByFleet` 才由旧口径的 `true` 变成 `false`。
其余场景取值不变（含少派：车务对当前需求定稿后少派也仍是 `true`，缺口按实派守恒不再报）。

#### 3. `staleFinalizedPlan=true` 且有缺口时的具体变化

以「V1 商务车×1 已定稿并派车 → 换版为 V2 SUV×2」为例（`suggestedSlots=2`，现有 1 个槽位）：

| 字段 | 修复前 | 修复后 |
|---|---|---|
| `finalizedByFleet` | `true`（按上一版定稿） | `false` |
| `staleFinalizedPlan` | 无此字段 | `true` |
| `totalSlots` | `1`（按实派守恒） | `2`（按当前需求估算） |
| `unassignedSlots` | `0`（缺口被抹平） | `1`（缺口露出） |
| `canceledSlots` | `0`（定稿分支恒 0） | 按实际取消槽位数返回（可 > 0） |
| 记录级 `assignmentStatus` | `assigned` | `unassigned`（当天/临近出发则 `unassigned_urgent`） |

**卡片会换 tab**：这类订单从「已派车」跳到「待派车」（临近出发进急单组）。这是本次修复的目的——
车管必须在待派车里看到它，否则新需求多要的那台车永远没人补。

**`statusCounts` 与列表条数同源一致**：`summary.statusCounts`、`statusOptions` 与 `/orders` 列表用的是同一份记录级派生态，
「tab 上有 N 条 → 点进去就是 N 条」这一保证在本次改动后仍然成立（已加回归用例锁定）。

#### 4. ⚠️ 破坏性提醒：`assignmentSlots` 会包含 `CANCELED` 槽位

`assignmentSlots` 原本在 `finalizedByFleet=true` 时会过滤掉已取消的槽位。
由于本次「陈旧定稿 + 有缺口」会把 `finalizedByFleet` 置为 `false`，**这类卡片的 `assignmentSlots` 里会重新出现
`baseAssignmentStatus=canceled` / `assignmentStatus=canceled` 的槽位**（该需求下整槽被取消的历史槽位；
该需求没有整槽取消历史时列表内容不变）。

**前端必须按槽位状态区分渲染**：不要假设 `assignmentSlots` 只含活跃槽位；已取消槽位应置灰或折叠，
不要计入「已派几台车」的展示口径（改用 `assignmentProgress` 的 `assignedSlots`/`totalSlots`）。
这类槽位的 `availableActionCodes` 为空、`canAssign=false`，点不动，但会占版面。

---

## `GET /admin/fleet/board/summary` —— 口径变化（字段结构不变）

| 字段 | 变化 |
|---|---|
| `todayDepartCount` | 「今日出团」只数「今天出发 **且记录级已派车**」。陈旧定稿且缺口的卡片记录级已被改判为待派车，**不再计入本数** |
| `pendingCount` / `pendingUrgentCount` | 上述卡片改由这两个数体现（今天出发必然落在 `unassigned_urgent`） |
| `statusCounts.unassigned` / `.assigned` | 随记录级派生态同步变化，与 `/orders` 列表同源 |

口径理由：车还没配齐，算作「已派车出团」会让当天出团数虚高、车管漏配车。

---

## 订单车控状态（order-v3 侧，定制师页面可见）

本工单修的是两个出口，上面是看板（出口①），这里是订单车控（出口②）。**无接口结构变化**：
fleet 不新增/修改端点，变的是 order-v3 既有字段 `vehicle_control_status` 的推进时机。

**触发条件**（两个条件必须同时成立，与看板同一份「陈旧」行级判据）：

1. 该需求下存在任一非终态派单行携带上一版需求的定稿（终态行同样豁免）；
2. **当前需求车数 > 现有非取消槽位数**（订单侧刻意排除已取消槽位——取消掉的槽位不是交付出去的车，
   与看板按槽位视图计数略有差异，这里统一取更保守的一侧，宁可判未完成也不误报完成）。

**结果对比**：

| | 修复前 | 修复后 |
|---|---|---|
| 车务确认后的满派判定 | 按「上一版定稿即权威」判已满派（该分支刻意不比对需求车数） | 退回常规拓扑校验，按当前需求的车数 × 服务日核对，缺车即未满派 |
| 是否回发 `DAILY_V3` 最终方案 | 回发 | **不回发** |
| 订单 `vehicle_control_status` | 被标 `DONE`（定制师页面显示派车已完成，实际少一台车，实测 26-5067） | 停在处理中（`PROCESSING`），定制师页面继续显示派车进行中 |

**恢复方式**：车务在看板对**当前需求**重新提交/确认最终方案即可自动恢复——确认时定稿归属会被盖章成当前需求 ID，
陈旧判据随即失效，满派后照常回发最终方案、车控回到 `DONE`。无需人工改库，也不需要运维介入。

**只陈旧不缺车不改判**：车数没变的换版（只改车型/人数）仍按定稿权威判已满派，车控照常推进到 `DONE`，
与看板「只提示不改判」保持同一口径。

---

## 详情端点说明（无变化）

`GET /admin/fleet/board/orders/{orderId}` **不返回** `assignmentProgress`，因此也没有 `staleFinalizedPlan`。
详情页已有等价且更可操作的信息：`requirementFleetText`（当前需求车队组成）+ `suggestedVehicleCount`（需求车数）
+ `actualVehicleCount`（实派槽位数）。由于本次改判只在「确有缺口」时发生，凡列表把卡片挪进待派车的订单，
详情页必然满足 `suggestedVehicleCount > actualVehicleCount`，两处不会互相矛盾。

---


---

## 验证证据

测试服（`hl-fleet-service` 部署于 2026-08-11 14:44，含 PR #5843 合并提交 `6789811ea`），实测订单 **26-5067**（`HL20260809095538772`）。

该单需求 V1 `mpv×1`（已派 1 台并定稿）→ V2 `suv×2`，属于典型「换版后车数变多」场景。

### 存量回填

Flyway `20260811.005` 执行后，该单派单行 `plan_finalized_requirement_id` 精确回填为 **V1 的 requirement_id**，与行上当前 `requirement_id` 不等 → 判定为陈旧定稿。真库统计：17 行活跃定稿中 12 行可由确认回执精确反查，其中仅 1 行为真陈旧（即本单），其余存量归属 = 当前需求或留 NULL（兼容档），状态无跳变。

### 出口①：看板缺口重新露出

`GET /admin/fleet/board/orders?keyword=26-5067` 实际返回：

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

- 记录级状态 `unassigned` / 「待派车」（修复前为 `assigned`「已派车」）
- tab 计数与列表条数一致：`statuses=unassigned` → 1 条；`statuses=assigned` → 0 条
- 与订单侧「部分回配 (1/2)」口径对齐

### 出口②：订单车控不再误标完成

在测试服对该单再提交一次换版（V3 `suv×2`，车数不变仍缺 1 台）实测：

| 项 | 修复前（V2 换版时） | 修复后（V3 换版时） |
|---|---|---|
| fleet 行 `requirement_id` | 平移到新需求 | 平移到新需求（一致） |
| `plan_finalized_requirement_id` | 无此列 | 仍为 V1 → 陈旧 |
| 是否回发 DAILY_V3 最终方案 | 是 | 否 |
| `order_main.vehicle_control_status` | **DONE**（缺 1 台车却判完成） | **PENDING** ✅ |

### 门禁与测试

- 受影响 6 个测试类 666/666 全绿；`FleetRedLineArchTest` 12/12；`spotless:check` 通过
- 全量 3505 用例，剩余失败均为环境相关且已在干净 `origin/dev-v3` 基线复现（Testcontainers 并发起容器失败、`ReleaseE` 缺 `-Dfleet.mysql.releaseE.finalMigrationSha256` 系统属性），低负载单独重跑 32/32 通过

## 已知取舍

带 `statuses=[unassigned]`（含 `unassigned_urgent`）筛选时，后端为了不漏掉「落库态是 assigned/holding、
但记录级会被改判成待派车」的卡片，会把 DB 候选集扩到 `unassigned + holding + assigned` 再在内存精筛。
副作用：**该筛选条件下扫描量变大，更容易触发 5000 条安全上限**（触发后走游标扫描，或提示「请增加日期或关键词条件」）。
不带状态筛选的默认首屏不受影响（本来就查全状态）。

---

## 前端 checklist

- [ ] `assignmentProgress.staleFinalizedPlan=true` 时展示换版核对提示（不做按钮禁用）
- [ ] 不再假设 `finalizedByFleet=true` 等于「有过最终方案」，语义已收窄为「对当前需求定稿」
- [ ] `assignmentSlots` 按 `assignmentStatus` 区分渲染，容忍 `canceled` 槽位出现
- [ ] 「已派几台车」一律取 `assignmentProgress`，不要自己数 `assignmentSlots` 长度
- [ ] 待派车 tab 会新增这类换版订单，确认列表/角标/急单分组渲染正常
