---
schema: "hl-changelog/v2"
ticket: "5279"
title: "车务派单全链路 E2E 前端缺陷交接"
consumer: "admin"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "implemented"
frontend_owner: "hl-admin"
frontend_ref: "2ee0988c235c0483a5b51f8cc0deb195d7fd8573"
target_release: ""
verified_at: ""
status_note: "本次没有新增或修改后端字段、类型、必填性、路径和错误码；测试环境当前契约已验证。前端需修正动作码、部分日期、HOLD 费用草稿、刷新重置和冲突日期等消费逻辑。"
updated_at: "2026-07-27"
base: "dev-v3"
---

# 车务：派单全链路 E2E 前端缺陷交接

> **服务**: `hl-fleet-service`、`hl-order-service-v3`
>
> **工单**: [wx/HL#5279](https://git.1814.love:8443/wx/HL/issues/5279)
>
> **前端基线**: `mmg/hl-ui v2.1@5e6718f952ed156c2e71bc38d08c3482e72ba744`
>
> **影响范围**: 车务管理 → 派单看板、矩阵派单、派车弹窗、派单详情；订单详情 → 用车需求调整

## 结论

这是一份**当前契约消费纠错和前端缺陷交接**，不是后端接口变更：

- 后端 #5275 已保证部分日期派车只消费目标切片，剩余日期和稳定槽位守恒；
- 后端 #5277 已恢复完整已派需求 `canAssign=true + CHANGE_ASSIGNMENT`；
- 测试环境通过真实后台 UI 完成 HOLD、DIRECT、司机确认、最终确认、司机拒接、改车、改司机、需求驳回/换版/重派、逐日费用、免费服务日、冲突和重复提交；
- 当前剩余阻断均可在最新前端稳定复现，后端不应增加旧动作码别名或重复状态来迁就页面。

## 变更接口

本次不新增接口、字段或错误码，仅纠正以下现有管理后台接口的前端消费：

| 方法 | 路径 | 当前契约用途 |
| --- | --- | --- |
| `GET` | `/admin/fleet/board/orders` | 看板代表行、能力字段、稳定槽位和未派切片上下文 |
| `GET` | `/admin/fleet/board/orders/<orderId>` | 当前有效派车组、逐日费用和独立生命周期 |
| `POST` | `/admin/fleet/assignments/candidates` | 车辆/司机可用窗口与真实冲突明细 |
| `POST` | `/admin/fleet/assignments` | HOLD/DIRECT 首次派车 |
| `POST` | `/admin/fleet/assignments/<assignmentId>/change` | 按稳定槽位改车、改司机或同时改派 |
| `POST` | `/admin/fleet/assignments/<assignmentId>/driver-confirmation` | 登记司机回复 |
| `POST` | `/admin/fleet/assignments/<assignmentId>/confirm` | 最终确认执行 |
| `POST` | `/admin/fleet/assignments/<assignmentId>/driver-reject` | 司机拒接并退回未派 |
| `DELETE` | `/admin/fleet/assignments/<assignmentId>` | 取消派单 |
| `POST` | `/admin/fleet/assignments/<assignmentId>/early-complete` | 提前完结 |

## 一、动作码必须消费后端当前值

权威字段来自：

```http
GET /admin/fleet/board/orders
GET /admin/fleet/board/orders/<orderId>
```

前端按 `canAssign` 和 `availableActionCodes` 渲染，不按中文状态或本地别名推断。

| 生命周期 | `currentStep` | 当前动作码 | 正确页面行为 |
| --- | ---: | --- | --- |
| `unassigned` | 2 | `ASSIGN`, `REJECT_REQUIREMENT` | 派车派人、驳回需求 |
| `holding_wait_driver` | 3 | `CHANGE_ASSIGNMENT`, `RECORD_DRIVER_CONFIRMATION`, `DRIVER_REJECT`，出团前/当天另有 `CANCEL_ASSIGNMENT` | 继续派车、司机拒接、改派、取消 |
| `driver_confirmed` | 4 | `CHANGE_ASSIGNMENT`, `CONFIRM_EXECUTION`，出团前/当天另有 `CANCEL_ASSIGNMENT` | 恢复第 4 步确认执行；不得只剩改派 |
| `assigned` 且未过出团日 | 4 | `CHANGE_ASSIGNMENT`, `CANCEL_ASSIGNMENT` | 改派、取消派单 |
| `assigned` 且已过出团日 | 4 | `CHANGE_ASSIGNMENT`, `EARLY_COMPLETE` | 改派、提前完结 |
| `completed/canceled` | 终态 | 无可写动作 | 只读 |

当前前端存在三处精确错位：

1. 检查 `CANCEL`，而后端下发 `CANCEL_ASSIGNMENT`；
2. 检查 `COMPLETE_EARLY`，而后端下发 `EARLY_COMPLETE`；
3. `driver_confirmed` 已下发 `CONFIRM_EXECUTION`，但卡片和详情没有恢复确认执行入口。

`DRIVER_REJECT` 与 `RECORD_DRIVER_CONFIRMATION` 是现行值，继续沿用。不要让后端同时下发新旧两套动作码。

## 二、部分日期稳定槽必须保留用户点击上下文

### 复现

测试单 `26-6263` 的同一 `assignmentSlotId`：

- 07-30、07-31：已有 HOLD；
- 08-01：唯一剩余 `unassigned`；
- 看板卡片正确显示“用车 08-01 · 1天”；
- 8 月矩阵未派池也只显示 08-01。

但当前前端：

- 从看板进入 Step 2 后改成编辑 07-30/07-31 的 active HOLD；
- 从矩阵车辆 08-01 空闲格进入时，顶部虽显示“有效服务日期：08-01”，Step 2 却显示 `已选 0/0 天`，继续复用 07-30/07-31 费用并返回 0 个候选。

### 正确规则

- 首次派车 `mode=assign` 必须保留用户点击的看板代表行：`assignmentId/assignmentSlotId/fleetItemIndex/startDate/endDate`；详情异步返回后不能被 `currentAssignment` 覆盖；
- 矩阵入口以 `entryContext.clickedDate + startDate + endDate` 与订单合法服务段求交集；本例结果固定为 08-01；
- `currentAssignment/activeAssignments[]` 只用于改派和确认已有有效派车组，不代表未派切片；
- 实际计费服务日为空时，应从已经校验通过的 `assignmentDateRange` 回退生成，不得读取另一 active 组的逐日费用；
- 提交仍使用原稳定 `assignmentSlotId`，只消费 08-01，不得重建第二槽位或覆盖 07-30/07-31。

## 三、HOLD 手工补价不能在司机确认后丢失

### 复现

`26-9140` 改派时：

- 07-30/07-31 使用日历价 860；
- 08-01 日历缺价，页面提交覆盖价 860 和调价原因；
- `/change` 返回并落库总价 2580；
- 派单详情也正确展示 08-01 覆盖价和原因。

登记司机确认后，Step 4 又从候选价格日历重建费用，08-01 变为“待补价”，总价变为“价格不完整”，`validateVehicleFeeDraft` 阻断最终确认。

### 正确规则

- HOLD 已创建后，`activeAssignments[]/currentAssignment` 的 `dailyVehicleFees`、`vehicleFeeTotal`、`vehicleFeeAdjustmentReason`、`chargeableServiceDates` 和 `vehicleFeeWaiverReason` 是当前派车组权威快照；
- 候选接口的价格只用于新选车草稿或日历参考，不得覆盖已经保存的 HOLD 费用；
- 第 3 步登记司机回复和第 4 步确认执行都不能清空已保存覆盖价；
- 详情重拉后应按 `assignmentId/assignmentGroupId` 恢复同一派车组快照。

## 四、后台刷新不得重置正在编辑的派车草稿

实测派车弹窗打开后，后台订单刷新会触发整套初始化：

- 候选接口已返回 19 辆车和 11 名司机，但两个列表被清空，计数/筛选项仍保留；
- 用户已切换 DIRECT，提交前又恢复为 HOLD；
- 再点一次筛选会重新请求并恢复候选。

前端应做到：

- `props.order` 仅因列表/SSE 刷新生成新对象时，不重置当前弹窗；
- 只有订单 ID、目标需求 ID、目标稳定槽位、模式或用户主动关闭/重开变化时才重新初始化；
- 已编辑车辆、司机、日期、收费日、逐日价、原因、跨常驻确认和 HOLD/DIRECT 模式全部保留；
- 若服务端基线真的变化，使用现有 baseline-difference 强提示并让用户决定，不静默改写草稿。

## 五、多槽 HOLD 必须按槽位推进

`26-5256` 有两个稳定车辆槽位。槽位 1 登记司机确认后：

- 详情真值为槽位 1 `driver_confirmed`、槽位 2 `holding_wait_driver`；
- 卡片却仍把代表司机显示为“待回复”；
- “继续派车”入口消失，槽位 2 无法登记司机回复。

正确行为：

- 订单只要任一 active 槽位包含 `RECORD_DRIVER_CONFIRMATION`，卡片和详情保留“继续派车”；
- 打开后默认选中最早待回复槽位，也允许切换其它槽位；
- 一个槽位确认不得覆盖或隐藏另一个槽位状态；
- 订单聚合步骤取最慢槽位，代表卡文案必须明确“代表槽位”，不能冒充全单状态。

## 六、冲突窗口字段不能混用

候选响应同时包含：

| 字段 | 含义 | 页面用途 |
| --- | --- | --- |
| `conflicts[]` | 真正占用冲突；`blocking=true` 的日期禁止选择 | 展示冲突订单与冲突日期 |
| `availabilityWindows[]` | 当前查询区间内仍可用的连续窗口 | 仅在资源可用/部分可用提示中展示 |
| `availabilityReasonCode` | `AVAILABLE`、`ASSIGNMENT_CONFLICT` 等判定码 | 决定禁用和提示类型 |
| `availabilityReasonMessage` | 后端判定文案 | 展示主提示 |

实测后端冲突是 08-01，`availabilityWindows` 是 08-02..08-04；页面却显示“存在派单冲突 · 08-02 至 08-04”。禁用结果正确，日期解释相反。

冲突文案应从 `conflicts[].startDate/endDate` 汇总；`availabilityWindows` 不得拼在冲突文案后。

## 七、提交与刷新反馈

### 1. 防重复提交

真实 UI 同一时刻双击 DIRECT 发出两个不同 `requestId`：

- 第一笔成功并只生成一个有效派车；
- 第二笔被后端守恒门禁拒绝，返回 `605033`：`用车需求完成回写处理中，请稍后重试`。

前端应在第一笔进入提交函数时立即短路后续点击，并在按钮、快捷键和程序触发路径共用同一 `submitting` 门禁。同一草稿的网络重试应复用 requestId，不应每次生成新值。

### 2. 司机拒接后的详情

司机拒接成功后，卡片立即变为未派，但当前详情抽屉仍保留旧 HOLD 车辆、费用和动作。关闭重开才正确。成功后需用最新服务端详情整体替换旧对象，不得继续把 mutation 前的行快照合并回来。

### 3. 车务角色快捷入口

车务角色下顶部可见“订单列表”，点击却进入未注册 `/housekeeper/orders` 并显示 404。未注册/无权限时不要展示快捷入口；有权限时应确保动态路由已注册再导航。

## 八、前端验收清单

- [ ] `CANCEL_ASSIGNMENT` 显示并执行取消；不再检查 `CANCEL`。
- [ ] `EARLY_COMPLETE` 显示并执行提前完结；不再检查 `COMPLETE_EARLY`。
- [ ] `driver_confirmed + CONFIRM_EXECUTION` 可从卡片和详情恢复第 4 步。
- [ ] 多槽 HOLD 任一槽位待回复时保留“继续派车”，逐槽推进且代表状态不串槽。
- [ ] `26-6263` 从看板和 8 月矩阵都只编辑/提交 08-01，07-30/07-31 不变。
- [ ] 已保存 HOLD 的手工逐日价、调价原因和免费日配置在司机确认、详情刷新、最终确认之间保持不变。
- [ ] SSE/列表刷新不清空候选、车辆司机草稿或切换 HOLD/DIRECT 模式。
- [ ] 冲突日期取 `conflicts[]`，可用窗口取 `availabilityWindows[]`，两者文案不混用。
- [ ] DIRECT/HOLD 第一击后立即禁用所有重复提交路径；同草稿重试复用 requestId。
- [ ] 司机拒接后当前卡片和详情同时刷新到 unassigned。
- [ ] 车务角色的订单快捷入口不再进入 404。

## 验证证据

- 后端 `dev-v3@79297c838b62af2426cc83ad0e78cffbd961096b` 已部署，Fleet 8087/8187 两实例健康；
- #5277 Fleet reactor 2,427 项通过，0 失败、0 错误、1 跳过；Spotless 通过；
- 网关验证：完整 assigned 单槽和三槽均为 `canAssign=true + CHANGE_ASSIGNMENT`；
- 真实 UI 写入通过：HOLD、DIRECT、司机确认、最终确认（不发送短信）、司机拒接、只换车、只换司机、同时换车司机、需求驳回、V2/V3 换版重派、收费/免费日、重复提交；
- 后端守恒：重复 DIRECT 只生成一笔有效派车；需求 `DONE_ADJUST` 返回 `assignmentDeletedCount=1`，旧资源随后可重新选择；
- 部分日期后端真值：07-30/07-31 为 HOLD，08-01 唯一 unassigned，三天共用稳定槽位，无重复/孤儿；
- 关键页面截图：`fleet-partial-continuation-broken.png`、`5277-assigned-reassign-restored.png`；
- 完整逐步证据由 #5279 工单评论和测试记录保留。

## 十、契约审查

- 本文件不对应新的 Controller、VO、字段、类型、必填性、动作码或错误码变更；
- OpenAPI/oasdiff：`not_required`，没有新的 `frontend_api` diff；
- Consumer Contract：`not_required`，没有 Feign/shared Java 变化；
- 当前动作码由 `AssignmentLifecycleResolver` 和现有服务测试确认；部署网关响应与源码一致；
- 前端代码审查确认当前仍检查 `CANCEL`、`COMPLETE_EARLY`，且没有消费 `CONFIRM_EXECUTION`。

## 不影响范围

- 不修改 `hl-ui`，不在 `mmg/hl-ui` 建工单；本文件 `frontend_status` 保持 `pending`；
- 不要求后端新增兼容动作码、重复字段、特殊前端分支或放松资源/幂等守恒；
- 不修改派车状态机、计费公式、库存/占用、保险、通知、Outbox 或数据库结构；
- 不把前端实现、发布、页面验收纳入后端工单关闭条件。
