文件
hl-api-changelog/changelogs-v2/2026-09/06_7067_派单去槽位化按行程日配车-接送机独立配置-修改接口-管理后台.md
T
2026-09-07 21:29:21 +08:00

1684 行
88 KiB
Markdown
原始文件 Blame 文件历史

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
---
schema: "hl-changelog/v2"
ticket: "7067"
title: "派单去槽位化:按行程日配车 + 接送机独立配置(管理后台四步向导)"
consumer: "admin"
author: "wx(GIT)"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "verified"
frontend_owner: "mmg"
frontend_ref: "af4ad08c"
target_release: ""
verified_at: "2026-09-07"
status_note: "前端 U1-U7 全部交付并验证。U1(死端点下线+auto-recommend 改路径+updatePickupDropoffConfig)、U2(看板/矩阵读取面切日行粒度,44dafb4d)、U3(canonical 快照 retainedSlotIds→retainedGroupIds,c4e63bf3)、U4(/batch 提交体改稀疏 dailyPlan 去槽位坐标,6cef244a)、U5(向导四步化+第③步接送机,并入 #7203 整段不用车,7d21f35a)、U6(confirm 605914/605915 接送机覆盖门禁分支,透原文+跳第③步,先于幂等回执)、U7(退役字段全量清扫:AssignModal 展示档/基线键去 assignmentSlotId/fleetItemIndex,死函数 validateFleetSlotSelections 修 NaN+误报;保留疑似活契约 singleton create/stale 快照/failedFleetItemIndex 横幅,因 activeAssignments 明示未变更,已加注待后端确认)——U6/U7 合 commit af4ad08c,各步 checkpoint 全绿(含 Vitest 全量+生产构建),收尾 724/724 绿,对抗 review PASS。"
updated_at: "2026-09-07"
base: "dev-v3"
---
# 车务派单:去槽位化按行程日配车与接送机独立配置(管理后台)
> **服务**: hl-fleet-service (8087)
> **PR**: #7194、#7218(返工二轮)、#7223(端到端实测暴露的三处阻断)
> **Issue**: #7067
> **日期**: 2026-09-06
> **影响范围**: 管理后台车务派单模块——派单向导四步流程(订单详情/排车/接送机/确认执行)、派单看板列表与详情、矩阵派单甘特图与未派订单清单
---
## ⚠️ 关键变化
本次是**破坏性契约变更,没有兼容期**:旧字段携带即 400,不做灰度双读。
- **前端派单向导从三步改四步**:①订单详情 → ②排车 → ③接送机 → ④确认执行。以前排车(第②步)里顺带勾选接机;现在接送机是独立的第③步,写口径也是独立接口。
- **槽位概念整体退役**:以前 `assignmentSlotId` + `fleetItemIndex` 是派车的身份;现在唯一操作身份是 `assignmentGroupId`(历史行回退 `assignmentId`,对任何真实行恒非空)。前端不得再使用 `slotId` / `fleetItemIndex` 做任何身份判定或请求参数。
- **排车请求体重构**:以前提交 `items[]`(槽位矩阵,逐槽 `used`/`pickupParticipant`);现在提交 `dailyPlan[]`(服务日期×车辆×司机,同日可多车)。以前"某天不配车"要提交一条 `used=false` 的占位项;**现在不需要**——该日不出现在 `dailyPlan[]` 里即可,但需求日期窗内存在这类空配车日时必须显式 `confirmNoVehicleServiceDates=true` 二次确认。
- **虚拟待派条目**:以前看板/矩阵只呈现库里真实存在的派车行;现在零派车行的新声明用车需求订单,由 order-v3 当前有效用车需求投射出「虚拟待派条目」并入看板列表与矩阵未派清单,`virtualPending=true`,`assignmentId`/`assignmentGroupId` 为 null。**前端不能再用「`dailyAssignments` 为空」推断待派,必须读 `virtualPending`**。
- **候选查询字段改名**:`retainedSlotIds` → `retainedGroupIds`,`cells[].slotId` → `cells[].groupId`。
- **司机 H5 短链旧 token 直接失效**:token v2 claim 由 `assignmentSlotId` 改为 `assignmentGroupId`,签名输入口径变化使旧 token 报 605306(fail-closed 拒绝,不是容忍旧值),司机侧需重新获取短链。
- **两个错误码作废**:605012、605911(码位保留不复用,不会再出现在任何响应里)。
- **(2026-09-07 返工补充)`progressSteps` 由 3 步变 4 步**,`CONFIRM_EXECUTE` 的 step 号 3 → 4:按下标或按数组长度取值的前端会直接破,改为按 `code` 取值。详见「九之二」B1。
- **(2026-09-07 返工补充)`POST /batch` 提交成功不再等于订单车控完成**:大交通声明要接/送机而第③步未配齐时,最终方案不发布、订单停在处理中。前端必须读响应新增的 `finalPlanPublished`。详见「九之二」B2。
- **(2026-09-07 返工补充)看板详情 `pickupDropoffGate` 为 `null` 表示「门禁未知」(order-v3 降级),不是「无接送机声明」**。详见「九之二」B3。
---
## 一、背景
去槽位化前,"车辆槽位"是排车的固定拓扑单位:一个槽位对应需求声明的一个车辆项,逐日切片必须挂在某个槽位下,且排车与接送机勾选耦合在同一个提交动作里。这带来两个问题:一是同一天需要多辆车时无法表达(一槽一天一行的限制);二是新声明用车需求但车务尚未处理的订单,必须先靠"需求展开"预建 `unassigned` 占位行才能被看板/矩阵发现,一旦占位行缺失(历史遗留、并发竞态等)车务就看不到这张待派订单。
本单把排车模型从"槽位×日期"改为"服务日期×车辆×司机"的直接列表,取消预建占位行、改为读时按 order-v3 当前有效需求投射「虚拟待派条目」,并把接送机勾选从排车动作中拆出,独立成第③步。
| 维度 | 证据 A(测试环境实测画像,2026-09-06 只读连接) | 说明 |
|------|--------|--------|
| `fleet_assignment` 总行数 | 584 | 现状规模 |
| 零派车行但需清理的旧占位行(`assignment_status='unassigned'` 且车/司机全 NULL) | 75(涉 64 个 requirement) | 去槽位化后不再需要这类占位行,随本单一并清理(见 `docs/tasks/7067-contract-review.md`「测试环境历史数据处置」) |
| `assignment_group_id IS NULL` 的存量行 | 224,且各自 `assignment_slot_id` 互不相同 | 历史行按 `assignmentGroupId ?? assignmentId` 回退,等价于旧的按 slotId 分组,不需要回填 |
---
## 二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 批量派车(车务最终实派方案原子提交) | POST | `/admin/fleet/assignments/batch` | 修改(请求体重构) | `dailyPlan[]` 替代 `items[]`,旧字段携带即 400 |
| 2 | 接送机配置(按派车行整批幂等覆盖) | PUT | `/admin/fleet/assignments/pickup-dropoff-config` | 新增接口 | 四步向导第③步独立写口径 |
| 3 | 按当前派车方案原子确认全部执行段 | POST | `/admin/fleet/assignments/requirements/{requirementId}/confirm` | 修改(新增门禁错误码) | 大交通接送机未配置阻断 605914/605915 |
| 4 | 查询派单候选资源 | POST | `/admin/fleet/assignments/candidates` | 修改(响应改名+入参新增) | `retainedSlotIds`→`retainedGroupIds`、`cells[].slotId`→`cells[].groupId`,新增 `assignmentGroupId` 入参 |
| 5 | 看板列表 | GET | `/admin/fleet/board/orders` | 修改(结构重构+虚拟条目) | `assignmentSlots[]`→`dailyAssignments[]`,新增 `virtualPending` |
| 6 | 矩阵未派订单清单 | GET | `/admin/fleet/matrix/unassigned-orders` | 修改(并入虚拟条目) | 新增 `virtualPending`,排序追加末位兜底键 |
| 7 | 一键重派推荐(只读) | POST | `/admin/fleet/assignments/{assignmentId}/auto-recommend` | 修改(路径变更) | 原 `/slots/{slotId}/auto-recommend`,锚点由槽位 ID 改派单行 ID |
| 8 | 新增车辆槽位 | POST | `/admin/fleet/assignments/slots` | 删除接口 | 无替代;改在 `POST /batch` 的 `dailyPlan[]` 里多提交一项 |
| 9 | 删除车辆槽位 | DELETE | `/admin/fleet/assignments/slots/{slotId}` | 删除接口 | 无替代;改在 `POST /batch` 的 `dailyPlan[]` 里不提交该车该日 |
| 10 | 按日期删除改期残留配车数据 | POST | `/admin/fleet/assignments/slots/{slotId}/clear-residue-dates` | 删除接口 | 无替代;残留由 `POST /batch` 整批 diff 精确取消 |
| 11 | 逐日取消改期残留派车 | POST | `/admin/fleet/assignments/{assignmentId}/cancel-residue` | 删除接口 | 无替代;同上 |
| 12 | 看板订单详情 | GET | `/admin/fleet/board/orders/{orderId}` | 修改(vehicleSlots 改日行粒度) | 删除 `assignmentSlotId`/`fleetItemIndex`,新增 `serviceDate`/`dropoffParticipant` |
| 13 | 矩阵主数据(月视图甘特) | GET | `/admin/fleet/matrix/grid` | 修改(并行组字段删除) | `assignments[].parallelAssignments[].fleetItemIndex` 删除 |
| 14 | 矩阵当天订单清单 | GET | `/admin/fleet/matrix/day-orders` | 修改(排序键变更) | `assignments[].fleetItemIndex` 删除,排序键改 `assignmentId`;不并入虚拟条目 |
---
## 三、接口详情
### 1. 批量派车(车务最终实派方案原子提交) `POST /admin/fleet/assignments/batch`
**VO**: `BatchCreateAssignmentReqVO → BatchAssignmentWriteRespVO`
#### 使用场景
四步向导第②步"排车"点击"保存排车方案"时调用。车务对同一用车需求一次性提交全部服务日×车辆×司机的配车计划,后端按(服务日×车辆)diff 原子重写该需求当前全部派车行:精确匹配的现行行保留不动,仅调价原因差异走轻量更新,其余在途行取消、其余提交项新建。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| orderId | Body | Long | ✅ | - | 订单 ID |
| orderNo | Body | String | ❌ | - | 订单号冗余 |
| requirementId | Body | Long | ✅ | - | 当前生效用车需求 ID |
| startDate | Body | Date | ✅ | - | 用车开始日期 |
| endDate | Body | Date | ✅ | - | 用车结束日期 |
| pickupAt | Body | String | ❌ | - | 接客地 |
| dropoffAt | Body | String | ❌ | - | 送客地 |
| headcount | Body | Integer | ❌ | - | 乘客人数 |
| confirmNoVehicleServiceDates | Body | Boolean | ❌ | 需求窗内存在不配车日期时必须为 true,否则 400 | 空配车日二次确认 |
| sendItinerarySms | Body | Boolean | ❌ | 不传按 false | 是否向本批各车师傅发行程短信(整批统一决策) |
| skipCityJunctionException | Body | Boolean | ❌ | - | 跳过城市衔接例外 |
| fromEntry | Body | String | ❌ | - | 操作来源 |
| requestId | Body | String | ✅ | ≤64 | 批次级幂等请求标识 |
| dailyPlan[] | Body | Array | ✅ | ≤4000 项 | 按行程日的完整配车列表 |
| dailyPlan[].serviceDate | Body | Date | ✅ | 须属于当前需求日期窗 | 服务日期 |
| dailyPlan[].vehicleId | Body | Long | ✅ | 同一服务日不能重复使用同一车辆 | 车辆 ID |
| dailyPlan[].driverId | Body | Long | ✅ | 同一服务日不能重复使用同一司机 | 司机 ID |
| dailyPlan[].assignmentPrice | Body | BigDecimal | ❌ | ≥0.00,整数最多10位/小数最多2位 | 本车当天价格;不传按车型价格日历参考价兜底 |
| dailyPlan[].priceAdjustmentReason | Body | String | ❌ | ≤256 | 实际价格与日历参考价不一致时的调整原因 |
| dailyPlan[].confirmCrossResident | Body | Boolean | ❌ | - | 跨常驻车显式确认 |
| ~~items~~ | Body | - | ❌ | 携带非 null 值即 400 | 已移除:旧槽位序号矩阵 |
| ~~chargeableServiceDates~~ | Body | - | ❌ | 携带非 null 值即 400 | 已移除:旧收费日期字段 |
| ~~vehicleFeeWaiverReason~~ | Body | - | ❌ | 携带非 null 值即 400 | 已移除 |
| ~~confirmAllServiceDatesFree~~ | Body | - | ❌ | 携带非 null 值即 400 | 已移除 |
| ~~holdMode~~ | Body | - | ❌ | 携带非 null 值即 400 | 已移除:#5827 起一步派定 |
| ~~dailyPlan[].fleetItemIndex~~ | Body | - | ❌ | 携带非 null 值即 400 | 已移除:稳定车辆槽位序号 |
| ~~dailyPlan[].used~~ | Body | - | ❌ | 携带非 null 值即 400 | 已移除:用车开关(不配车=无该项) |
| ~~dailyPlan[].pickupParticipant~~ | Body | - | ❌ | 携带非 null 值即 400 | 已移除:接机标志改由接送机配置步骤写入 |
#### 出参 `Result<BatchAssignmentWriteRespVO>`
| 字段 | 类型 | 说明 |
|------|------|------|
| assignments[] | Array | 按创建顺序返回的派单结果 |
| assignments[].fleetItemIndex | Integer | 历史遗留字段(去槽位化后不再有槽位序号可归因),本单起恒为 null |
| assignments[].assignment | Object | 复用单车派单响应(AssignmentWriteRespVO) |
| assignments[].assignment.id | String | 新建/命中的派单 ID(雪花) |
| assignments[].assignment.assignmentGroupId | String | 派车组 ID,任何真实行恒非空 |
| assignments[].assignment.assignmentSlotId | String | 稳定车辆槽位 ID;去槽位化后新建行恒为 null(字段保留兼容,不再落值) |
| assignments[].assignment.assignmentStatus | String | 派单状态,提交即派定恒为 `assigned` |
| assignments[].assignment.protocolPrice | String | 协议价日单价快照 |
| assignments[].assignment.vehicleFeeTotal | String | 该行最终总车费 |
| assignments[].assignment.confirmedAt | String | 派定确认时间 |
| assignments[].assignment.sideEffects | Object | 副作用执行结果(车/司机占用反算) |
| failedFleetItemIndex | Integer | 基线不一致失败时的车辆槽位序号;去槽位化后无法归因,恒为 null |
| dailyDifferences[] | Array | 基线不一致时的逐日差异 |
#### 请求示例
```json
{
"orderId": "70123456789",
"requirementId": "70123456790",
"startDate": "2026-08-20",
"endDate": "2026-08-22",
"requestId": "batch-7067-0001",
"sendItinerarySms": false,
"confirmNoVehicleServiceDates": true,
"dailyPlan": [
{ "serviceDate": "2026-08-20", "vehicleId": "2079857983403024385", "driverId": "2079857983403024387" },
{ "serviceDate": "2026-08-21", "vehicleId": "2079857983403024385", "driverId": "2079857983403024387" }
]
}
```
(2026-08-22 未出现在 `dailyPlan` 中 = 该日不配车;需求窗内存在空配车日,故必须带 `confirmNoVehicleServiceDates=true`,否则 400)
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"data": {
"assignments": [
{
"fleetItemIndex": null,
"assignment": {
"id": "2086270160359878658",
"assignmentGroupId": "2086270160359878658",
"assignmentSlotId": null,
"assignmentStatus": "assigned",
"protocolPrice": "1300.00",
"vehicleFeeTotal": "1300.00",
"confirmedAt": "2026-08-19 10:00:00"
}
}
],
"failedFleetItemIndex": null,
"dailyDifferences": null
},
"success": true
}
```
#### 空数据 / 降级响应
需求日期窗内所有服务日均不配车(车务确认本次无需用车)时,提交空 `dailyPlan: []` + `confirmNoVehicleServiceDates: true`,diff 会取消全部在途行,`data.assignments` 返回空数组 `[]`。本接口是同步写操作,无下游降级路径——依赖资源不可用时直接抛错,不做静默降级。
#### 错误响应
```json
{
"code": 605062,
"message": "存在派车日期与当前用车需求不符的槽位,请先调整或取消后再继续",
"success": false,
"data": null
}
```
#### 业务边界
- 携带任一旧字段(`items`/`chargeableServiceDates`/`vehicleFeeWaiverReason`/`confirmAllServiceDatesFree`/`holdMode`/`dailyPlan[].fleetItemIndex`/`used`/`pickupParticipant`)一律 400,不会静默忽略。
- 同一服务日允许多车,但同日不能重复使用同一车辆或同一司机(各自触发 400)。
- 需求日期窗内存在未出现在 `dailyPlan[]` 的服务日(且该日无 completed 历史行覆盖)时,必须 `confirmNoVehicleServiceDates=true`,否则 400。
- 已完结(completed)日期的派车冻结:提交项与完结行不一致返回 400「该日已完结不可改派」。
- 接送机不在本接口配置;本接口新建行的 `pickupParticipant`/`dropoffParticipant` 恒落 0,需再调 `PUT /pickup-dropoff-config` 独立配置。
- 幂等:同一 `requestId` 300 秒窗口内重复提交直接拒绝「批量派单创建处理中,请勿重复提交」。
- 提交即执行最终基线复核;不一致返回 605041 及 `data.dailyDifferences`。
---
### 2. 接送机配置(按派车行整批幂等覆盖) `PUT /admin/fleet/assignments/pickup-dropoff-config`
**VO**: `PickupDropoffConfigReqVO → Void`
#### 使用场景
四步向导第③步"接送机配置"页面保存时调用。车务对第②步已排车的行逐一勾选是否参与接机/送机;本接口按订单+当前需求整批幂等覆盖,是去槽位化后接机/送机标志的唯一写路径。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| orderId | Body | Long | ✅ | - | 订单 ID |
| requirementId | Body | Long | ✅ | - | 当前生效用车需求 ID |
| requestId | Body | String | ✅ | ≤64 | 幂等请求标识 |
| items[] | Body | Array | ✅(可传空数组) | ≤4000 项 | 要标记接送机参与的派车行集合 |
| items[].assignmentId | Body | Long | ✅ | 须是当前需求下生效派车行 | 派车行 ID |
| items[].pickupParticipant | Body | Boolean | ✅ | - | 当日是否参与 ARRIVAL 接机 |
| items[].dropoffParticipant | Body | Boolean | ✅ | 与 pickupParticipant 不能同为 false | 当日是否参与 DEPARTURE 送机 |
#### 出参 `Result<Void>`
| 字段 | 类型 | 说明 |
|------|------|------|
| data | null | 本接口无响应体,成功仅 `code=200` |
#### 请求示例
```json
{
"orderId": "70123456789",
"requirementId": "70123456790",
"requestId": "pdc-7067-0001",
"items": [
{ "assignmentId": "2086270160359878658", "pickupParticipant": true, "dropoffParticipant": false },
{ "assignmentId": "2086270160359878659", "pickupParticipant": false, "dropoffParticipant": true }
]
}
```
#### 响应示例
```json
{ "code": 200, "message": "成功", "data": null, "success": true }
```
#### 空数据 / 降级响应
`items` 字段只校验非 null(无 `@NotEmpty`),传空数组 `[]` 是合法请求,语义为「清空该订单当前需求下全部生效派车行的接送机标志」(两方向都归 0)。本接口无读侧降级路径。
#### 错误响应
```json
{
"code": 605913,
"message": "接送机配置无效:派单行不存在、不属于当前需求或已是终态",
"success": false,
"data": null
}
```
#### 业务边界
- **整批幂等覆盖**:该订单+当前需求下未出现在 `items[]` 中的生效派车行,两个方向标志一律归 0(不是增量 diff)。
- 同一行两方向标志不能全为 false(`@AssertTrue` 拦截);不参与的行应直接从列表移除,而不是传两个 false。
- 候选限定「该订单+当前需求+`serviceDate` 非空+车辆司机已定+状态 ∈ {holding, assigned}」;completed/canceled/exception 终态行不可配置,命中即整批 605913 拒绝(不部分生效)。
- 大交通未要求接送机时也允许手工配置(读侧只作提示,不阻断写入)。
- `items[]` 传空数组合法,用于一键清空。
---
### 3. 按当前派车方案原子确认全部执行段 `POST /admin/fleet/assignments/requirements/{requirementId}/confirm`
**VO**: `ConfirmRequirementReqVO → ConfirmRequirementRespVO`
#### 使用场景
四步向导第④步"确认执行"点击提交时调用。车务对当前用车需求的全部有效执行段做原子最终确认,需精确携带当前全部派车组及各组行程短信选择;服务端在确认前新增大交通接送机覆盖门禁。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| requirementId | Path | Long | ✅ | - | 当前用车需求 ID |
| orderId | Body | Long | ✅ | - | 订单 ID |
| requestId | Body | String | ✅ | ≤64 | 幂等请求标识 |
| expectedRequirementVersion | Body | Integer | ✅ | - | 预期当前有效用车需求版本 |
| expectedRequirementSha256 | Body | String | ✅ | 64位小写十六进制 | Board 返回的当前用车需求 canonical SHA-256 |
| expectedPlanGeneration | Body | Long | ✅ | - | 预期当前最终派车方案代际 |
| groups[] | Body | Array | ✅ | ≤50 项 | 执行段确认选择 |
| groups[].assignmentGroupId | Body | Long | ✅ | - | 当前有效派车组 ID |
| groups[].sendItinerarySms | Body | Boolean | ✅ | - | 是否向本执行段司机发送行程短信 |
#### 出参 `Result<ConfirmRequirementRespVO>`
| 字段 | 类型 | 说明 |
|------|------|------|
| requirementId | String | 用车需求 ID |
| dispatchPlanGeneration | String | 已确认的最终派车方案代际 |
| confirmed | Boolean | 整组是否原子确认成功 |
| groups[] | Array | 各执行段确认结果 |
| groups[].assignmentId | String | 代表派单 ID |
| groups[].assignmentGroupId | String | 派车组 ID,历史行回退 assignmentId |
| groups[].assignmentStatus | String | 派单状态 |
| groups[].confirmedAt | String | 车务最终确认时间 |
| groups[].sendItinerarySms | Boolean | 是否选择发送本段行程短信 |
| groups[].itinerarySmsEventId | String | 行程短信 Outbox 事件 ID;未发送为 null |
| groups[].itinerarySmsStatus | String | 行程短信状态 |
| groups[].itineraryUrl | String | 本段电子行程单 H5 链接;签发不可用时为 null |
#### 请求示例
```json
{
"orderId": "70123456789",
"requestId": "confirm-req-7067-0001",
"expectedRequirementVersion": 3,
"expectedRequirementSha256": "a1b2c3d4e5f6000000000000000000000000000000000000000000000000",
"expectedPlanGeneration": "1934500000000000001",
"groups": [
{ "assignmentGroupId": "2086270160359878658", "sendItinerarySms": true }
]
}
```
(路径参数 `requirementId=70123456790`)
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"data": {
"requirementId": "70123456790",
"dispatchPlanGeneration": "1934500000000000002",
"confirmed": true,
"groups": [
{
"assignmentId": "2086270160359878658",
"assignmentGroupId": "2086270160359878658",
"assignmentStatus": "assigned",
"confirmedAt": "2026-08-19 10:05:00",
"sendItinerarySms": true,
"itinerarySmsEventId": "3086270160359878700",
"itinerarySmsStatus": "PENDING",
"itineraryUrl": "https://h5.example.com/itinerary/xxx"
}
]
},
"success": true
}
```
#### 空数据 / 降级响应
本接口是同步写操作,不存在空数据形态;下游行程单短链签发失败时 `itineraryUrl` 回退为 null,不阻断本次确认。
#### 错误响应
```json
{
"code": 605914,
"message": "大交通要求接机,以下日期未配置接机车辆:2026-08-20,2026-08-21",
"success": false,
"data": null
}
```
#### 业务边界
- 大交通接送机覆盖门禁先于幂等回执读取判定——即使是重复提交的 `requestId`,只要接送机未配齐仍会先报 605914/605915。
- 605914(ARRIVAL 接机)与 605915(DEPARTURE 送机)互相独立判定,各自返回缺失日期列表(`{0}` 为逗号分隔的 `yyyy-MM-dd`)。
- 大交通无接送需求的订单,两个门禁均不触发,直接放行,不做任何断言。
- 接/送机要求日若已被 completed 历史行覆盖(行程中换版平移),缺席豁免,不重复要求配置。
- `groups[]` 必须精确等于当前全部有效执行段;同一 `requestId` 用于不同业务载荷返回 605059;`requestId` 命中历史损坏回执返回 605063(不可自愈终态,前端不得自动重试)。
---
### 4. 查询派单候选资源 `POST /admin/fleet/assignments/candidates`
**VO**: `AssignmentCandidateReqVO → AssignmentCandidateRespVO`
#### 使用场景
四步向导第②步"排车"点击某个日期格子选车/选司机时调用;同时驱动车辆和司机两个独立分页候选列表,以及 Step2 canonical 快照(`cells[]` group×day 笛卡尔积,供排车矩阵渲染)。
#### 入参
字段众多(完整 28 个字段见源码 `AssignmentCandidateReqVO.java`),下表为本单相关及核心字段:
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| orderId | Body | Long | ❌ | 改派排除自身时必填 | 当前订单 ID |
| requirementId | Body | Long | ❌ | 改派排除自身时必填 | 当前用车需求 ID |
| assignmentGroupId | Body | Long | ❌ | 本单新增 | 改派时被排除派单所属的派车组 ID,替代 `fleetItemIndex` 做归属校验 |
| ~~fleetItemIndex~~ | Body | Integer | ❌ | `@Deprecated`,不拒收 | 已降级:不再参与归属校验,仅剩「按需求车型项回退推断车型/座位提示」用途 |
| startDate | Body | Date | ✅ | - | 用车开始日期 |
| endDate | Body | Date | ✅ | - | 用车结束日期 |
| selectedVehicleId | Body | Long | ❌ | - | 已选车辆 ID(支持先选车) |
| selectedDriverId | Body | Long | ❌ | - | 已选司机 ID(支持先选司机) |
| excludeAssignmentId | Body | Long | ❌ | 必须属于当前订单和当前用车需求 | 改派时排除的当前派单 ID |
| expectedPlanGeneration | Body | Long | ❌ | 与当前 canonical 快照不符即拒绝 | Step2 幂等/同代校验期望代际 |
| expectedSnapshotVersion | Body | Long | ❌ | 同上 | Step2 期望快照版本 |
| driverAvailability | Body | String | ❌(默认 ALL) | `ALL`\|`AVAILABLE` | 司机可用性筛选 |
| driverSort | Body | String | ❌(默认 SMART) | `SMART`\|`RATING`\|`YEARS`\|`RECENT_ORDER` | 司机排序 |
| vehiclePage / vehiclePageSize | Body | Integer | ✅(有默认 1/20) | ≥1,≤100 | 车辆候选分页 |
| driverPage / driverPageSize | Body | Integer | ✅(有默认 1/20) | ≥1,≤100 | 司机候选分页 |
#### 出参 `Result<AssignmentCandidateRespVO>`
完整字段见源码 `AssignmentCandidateRespVO.java`,下表为本单变更及核心字段:
| 字段 | 类型 | 说明 |
|------|------|------|
| vehicles | Object | 车辆候选独立分页(`CandidatePageVO`) |
| drivers | Object | 司机候选独立分页(`CandidatePageVO`) |
| suggestedDriverId | String | 已选车辆自动代入司机 ID |
| canonicalSnapshot | Object | Step2 canonical 快照;无 `requirementId` 或需求上下文不可用时为 null |
| canonicalSnapshot.retainedGroupIds | Array\<String\> | 本单由 `retainedSlotIds` 改名;有序稳定派车组 ID |
| canonicalSnapshot.cells[] | Array | 每个 group×day 唯一 cell |
| canonicalSnapshot.cells[].groupId | String | 本单由 `slotId` 改名;稳定派车组 ID,历史行回退 assignmentId |
| canonicalSnapshot.cells[].serviceDate | Date | 服务日期 |
| canonicalSnapshot.cells[].assignmentId | String | 当天逐日派车行 ID;无行为 null |
| canonicalSnapshot.cells[].used | String | `USED`/`UNUSED`/null 三态 |
| canonicalSnapshot.cells[].vehicleId / driverId | String | 当天车辆/司机 ID;未派或不用车为 null |
#### 请求示例
```json
{
"requirementId": "70123456790",
"startDate": "2026-08-20",
"endDate": "2026-08-22",
"headcount": 5,
"assignmentGroupId": "2086270160359878658",
"vehiclePage": 1,
"vehiclePageSize": 20,
"driverPage": 1,
"driverPageSize": 20
}
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"data": {
"vehicles": { "records": [], "total": 0, "page": 1, "pageSize": 20 },
"drivers": { "records": [], "total": 0, "page": 1, "pageSize": 20 },
"canonicalSnapshot": {
"planGeneration": "1934500000000000001",
"snapshotVersion": "1",
"retainedGroupIds": ["2086270160359878658"],
"editableServiceDates": ["2026-08-20", "2026-08-21", "2026-08-22"],
"cells": [
{
"groupId": "2086270160359878658",
"serviceDate": "2026-08-20",
"assignmentId": "2086270160359878658",
"used": "USED",
"readOnly": false,
"vehicleId": "2079857983403024385",
"driverId": "2079857983403024387"
}
]
}
},
"success": true
}
```
#### 空数据 / 降级响应
无 `requirementId` 或需求上下文不可用时 `canonicalSnapshot` 整体为 null;某 group×day 无对应行时该 cell 的 `assignmentId`/车辆/司机/价格为 null,但 cell 本身仍存在(笛卡尔积不缺格)。
#### 错误响应
```json
{
"code": 100001,
"message": "excludeAssignmentId 不属于当前订单或当前用车需求",
"success": false,
"data": null
}
```
(本接口候选查询本身恒 200 承载业务提示;此错误码来自「仅跨订单或排除派单不存在时报参数非法」的入参校验路径,非派车锁内校验)
#### 业务边界
- 归属校验统一改用 `assignmentGroupId`:`fleetItemIndex` 参与归属校验的旧口径已删除,仅保留其在「需求车型项回退推断车型/座位提示」上的非判定用途,不拒收旧值。
- 排除换版保留派单或未派车占位行时不校验 `fleetItemIndex`,座位按被排除行自身快照。
- 同订单但槽位上下文不一致的排除不报错,忽略排除继续查询(被误传行的占用如实展示);仅跨订单或排除派单不存在才报参数非法。
- `canonicalSnapshot` 每个 group×day 唯一 cell,显式携带 `USED`/`UNUSED`/`readOnly`/null,同代(`planGeneration`+`snapshotVersion`)内重复读取/幂等重试不重编号。
---
### 5. 看板列表 `GET /admin/fleet/board/orders`
**VO**: `BoardOrderPageReqVO → BoardOrderPageRespVO`
#### 使用场景
派单看板列表页首次加载、筛选和翻页时调用;四步向导的订单入口卡片来源。
#### 入参
完整字段见源码 `BoardOrderPageReqVO.java`,下表为核心字段:
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| statuses[] | Query | String[] | ❌ | 见六.5 枚举 | 多状态筛选,任一命中即返 |
| status | Query | String | ❌ | 兼容单值/逗号分隔 | 状态筛选别名 |
| startDayFrom / startDayTo | Query | Date | ❌ | 与行程区间重叠 | 日期区间 |
| startDate / endDate | Query | Date | ❌ | 兼容别名 | 日期区间别名 |
| vehicleTypeKeys[] / typeKeys[] | Query | String[] | ❌ | `suv`/`mpv`/`bus`/`sedan` | 车型大类多选 |
| keyword | Query | String | ❌ | - | 统一文字搜索 |
| consultantId | Query | Long | ❌ | - | 定制师精确筛选 |
| variant | Query | String | ❌(不传按 list) | `list`\|`grid`,其余值 100001 | 视图切换 |
| page / pageSize | Query | Integer | ❌(默认 1/20) | ≥1,≤100 | 分页 |
#### 出参 `Result<BoardOrderPageRespVO>`
完整字段见源码 `BoardOrderRecordVO.java`,下表为核心及本单变更字段:
| 字段 | 类型 | 说明 |
|------|------|------|
| records[] | Array | 当前页记录(确定性排序) |
| total / page / pageSize | Long/Integer | 分页信息 |
| records[].assignmentId | String | 代表日行派单 ID |
| records[].assignmentGroupId | String | 派车组 ID;真实记录恒非空 |
| records[].requirementId | String | 当前用车需求 ID |
| records[].dailyAssignments[] | Array | 本单由 `assignmentSlots[]` 改名;全部日行派车项 |
| records[].dailyAssignments[].assignmentId | String | 派单 ID |
| records[].dailyAssignments[].assignmentGroupId | String | 派车组 ID |
| records[].dailyAssignments[].serviceDate | Date | 服务日期 |
| records[].dailyAssignments[].dayDisplayNo | Integer | 当日组内展示序号(纯展示,不作身份) |
| records[].dailyAssignments[].pickupParticipant | Boolean | 本单新增,当天是否参与接机 |
| records[].dailyAssignments[].dropoffParticipant | Boolean | 本单新增,当天是否参与送机 |
| records[].assignmentProgress | Object | 日行维度四计数 |
| records[].assignmentProgress.totalDailyItems | Integer | 日行总数 |
| records[].assignmentProgress.dispatchedDailyItems | Integer | 已派出行数 |
| records[].assignmentProgress.completedDailyItems | Integer | 已完结日数 |
| records[].assignmentProgress.canceledDailyItems | Integer | 已取消日数 |
| records[].virtualPending | Boolean | 本单新增,是否虚拟待派条目;真实记录恒显式 false |
| records[].assignmentStatus | String | 当前派单状态(含派生态) |
#### 请求示例
```http
GET /admin/fleet/board/orders?statuses=unassigned_urgent&statuses=holding&page=1&pageSize=20
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"data": {
"records": [
{
"orderNo": "HL202607010001",
"orderId": "70123456789",
"assignmentId": "2086270160359878658",
"assignmentGroupId": "2086270160359878658",
"requirementId": "70123456790",
"dailyAssignments": [
{
"assignmentId": "2086270160359878658",
"assignmentGroupId": "2086270160359878658",
"serviceDate": "2026-08-20",
"dayDisplayNo": 1,
"assignmentStatus": "assigned",
"pickupParticipant": true,
"dropoffParticipant": false
}
],
"assignmentProgress": {
"totalDailyItems": 3,
"finalizedByFleet": true,
"dispatchedDailyItems": 3,
"completedDailyItems": 0,
"canceledDailyItems": 0
},
"virtualPending": false,
"assignmentStatus": "assigned"
}
],
"total": 21,
"page": 1,
"pageSize": 20
},
"success": true
}
```
#### 空数据 / 降级响应
该需求既无派车行、又不在 order-v3 有效需求窗内时整张卡不出现;`records` 为空数组 `[]` 时 `total=0`;order-v3 整体不可达时读侧回退派单快照,不抛错不 500。虚拟待派条目示例:
```json
{ "assignmentId": null, "assignmentGroupId": null, "dailyAssignments": [], "virtualPending": true }
```
#### 错误响应
```json
{
"code": 100001,
"message": "variant 仅支持 list、grid",
"success": false,
"data": null
}
```
#### 业务边界
- 顶层不再返回 `assignmentSlots`/`fleetItemIndex`/`assignmentSlotId`(已删除字段)。
- 同一 `requirementId` 只返回一条记录;前端不得把 `dailyAssignments[]` 拆成多张"用车需求"卡。
- 同一订单绝不同时出现真实记录与虚拟条目(服务端按 orderId 去重)。
- `virtualPending` 真实记录恒显式 `false`(不是 null),前端不得用「`dailyAssignments` 为空」推断待派,必须读 `virtualPending`。
- `dayDisplayNo` 只是展示位次,不可作身份或去重键;唯一操作身份是 `assignmentGroupId`(历史行回退 `assignmentId`)。
- `variant` 传非 `list`/`grid` 返回 100001。
---
### 6. 矩阵未派订单清单 `GET /admin/fleet/matrix/unassigned-orders`
**VO**: `MatrixUnassignedReqVO → List<MatrixUnassignedOrderVO>`
#### 使用场景
矩阵派单页右侧"未派订单"面板加载时调用;车务从此处把订单拖拽到左侧甘特图某车某日发起派车。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| year | Query | Integer | ✅ | 1970-9999 | 年份 |
| month | Query | Integer | ✅ | 1-12 | 月份 |
| typeKeys[] | Query | String[] | ❌ | `suv`/`mpv`/`bus`/`sedan`,空=全部 | 车型大类多选 |
#### 出参 `Result<List<MatrixUnassignedOrderVO>>`
完整字段见源码 `MatrixUnassignedOrderVO.java`,下表为核心及本单变更字段:
| 字段 | 类型 | 说明 |
|------|------|------|
| orderId | String | 订单号(兼容字段) |
| orderNumericId | String | 订单数值 ID(雪花) |
| requirementId | String | 当前生效用车需求 ID |
| virtualPending | Boolean | 本单新增,是否虚拟待派条目 |
| assignmentId | String | 派单 ID;虚拟条目为 null |
| assignmentGroupId | String | 派车组 ID;虚拟条目为 null |
| vehicleCategory | String | 规范小写车型 key;虚拟条目取需求车型明细首项 |
| categoryLabel | String | 车型中文标签,恒非 null |
| startDay / endDay | Integer | 月内起止日(跨月已裁剪) |
| serviceDateSegments[] | Array | 连续有效服务日期段 |
| urgentBadge | String | 紧急徽章 `T-N`;非紧急为 null |
| parallelAssignments[] | Array | 同订单全部并行派车组;虚拟条目恒空数组 |
#### 请求示例
```http
GET /admin/fleet/matrix/unassigned-orders?year=2026&month=8&typeKeys=suv&typeKeys=mpv
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"data": [
{
"orderId": "26-0503",
"orderNumericId": "1234567890",
"orderNo": "26-0503",
"requirementId": "70123456790",
"virtualPending": true,
"assignmentId": null,
"assignmentGroupId": null,
"vehicleCategory": "suv",
"categoryLabel": "SUV",
"startDay": 6,
"endDay": 11,
"urgentBadge": "T-2",
"parallelAssignments": []
}
],
"success": true
}
```
#### 空数据 / 降级响应
当月无未派订单时返回空数组 `[]`;Nacos `fleet.board.virtual-candidates-enabled=false` 或 order-v3 候选不可用时只丢虚拟条目,真实条目原样返回(fail-open,不让未派窗口整体打不开);`vehicleAdvice` 暂恒为 null(数据源未建)。
#### 错误响应
```json
{
"code": 100001,
"message": "月份超出范围",
"success": false,
"data": null
}
```
#### 业务边界
- 虚拟条目一单一条(不按车型拆),真实条目一单多车型时同订单出现多行(按 `assignmentId`+`vehicleCategory` 区分)。
- 同一订单已有任何真实候选行时不再补虚拟条目(不会双出)。
- 排序键 `urgentBadge → startDay → assignmentId → orderNumericId`(末位兜底键保证虚拟条目顺序确定)。
- 前端拖拽 key 用 `assignmentId ?? ('virtual:' + orderNumericId + ':' + requirementId)`。
- 虚拟条目 `assignmentId`/`assignmentGroupId` 为 null,不得对其发起行级拖拽/改派/取消,只能按 `requirementId` 走整单派车入口。
- 清单条数与 `GET /admin/fleet/matrix/grid` 未派计数在同一 `typeKeys` 过滤下必须相等。
---
### 7. 一键重派推荐(只读,不产生写操作) `POST /admin/fleet/assignments/{assignmentId}/auto-recommend`
**VO**: `(PathVariable assignmentId) → SlotAutoRecommendRespVO`
#### 使用场景
需求变更后车务手动删除旧派车,对空缺派车行调用本接口获取系统推荐车辆+司机;车务确认后仍走 `POST /batch` 手动提交。#7067 前锚点是槽位 ID,本单改为派单行 ID。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| assignmentId | Path | Long | ✅ | 须为空缺待派车派单行 | 派单行 ID(本单起取代原 `slotId`) |
#### 出参 `Result<SlotAutoRecommendRespVO>`
| 字段 | 类型 | 说明 |
|------|------|------|
| assignmentId | String | 锚点派单行 ID(本单起取代槽位 ID) |
| requirementId | String | 归属需求 ID |
| orderId | String | 归属订单 ID |
| recommendedVehicle | Object | 推荐车辆;无推荐为 null |
| recommendedDriver | Object | 推荐司机;无推荐为 null |
| recommendNote | String | 推荐说明 |
| vehicleAlternatives[] | Array | 车辆备选(前 5) |
| driverAlternatives[] | Array | 司机备选(前 5) |
| noRecommendation | Boolean | 是否无可用推荐 |
#### 请求示例
```http
POST /admin/fleet/assignments/2086270160359878658/auto-recommend
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"data": {
"assignmentId": "2086270160359878658",
"requirementId": "70123456790",
"orderId": "70123456789",
"recommendedVehicle": { "vehicleId": "2079857983403024385", "plate": "蒙A-88888", "modelName": "别克GL8", "seats": 7 },
"recommendedDriver": { "driverId": "2079857983403024387", "name": "王师傅", "driverStatus": "idle" },
"recommendNote": "车型匹配·座位充足·档期可用",
"vehicleAlternatives": [],
"driverAlternatives": [],
"noRecommendation": false
},
"success": true
}
```
#### 空数据 / 降级响应
无可用车辆或司机推荐时 `recommendedVehicle`/`recommendedDriver` 为 null、`noRecommendation=true`,`vehicleAlternatives`/`driverAlternatives` 为空数组,前端提示人工选配;本接口只读不产生写操作,无其他降级路径。
#### 错误响应
```json
{
"code": 605009,
"message": "派单不存在",
"success": false,
"data": null
}
```
#### 业务边界
- 路径参数语义变化:旧 `slotId`(稳定车辆槽位 ID)不再被接受,必须传当前空缺派车行的 `assignmentId`;旧路径 `POST /slots/{slotId}/auto-recommend` 直接 404。
- 只读接口,调用不产生任何写操作,也不影响派单状态。
- 车务确认推荐结果后仍需另调 `POST /batch` 手动提交,本接口不自动落库。
---
### 8. 新增车辆槽位(已删除) `POST /admin/fleet/assignments/slots`
**VO**: `(已删除,无 VO)`
#### 使用场景
**已删除**。原用途:改派时增加一个待派车辆槽位(多派一辆),新增槽位挂空占位派单行等车务后续选车。去槽位化后不再存在"槽位"这个身份实体,多派一辆车改为直接在 `POST /batch` 的 `dailyPlan[]` 里多提交一项(服务日×车辆×司机)。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| (无) | - | - | - | - | 本接口已删除,路径不存在,调用任何入参均返回 404 |
(原入参历史参考,仅供归档:`requirementId`/`vehicleType`/`seats`/`reason`)
#### 出参 `Result<Void>`
| 字段 | 类型 | 说明 |
|------|------|------|
| (无) | - | 本接口已删除,不再返回任何响应体 |
#### 请求示例
```json
{ "_deprecated": "本接口已删除,以下为历史请求体结构,仅供归档参考", "requirementId": "70123456789", "vehicleType": "suv", "seats": 5, "reason": "改派时多派一辆" }
```
#### 响应示例
```json
{ "_deprecated": "本接口已删除,调用将得到 404,不会再有下列历史响应体", "slotId": "1934567890123456789", "assignmentId": "1934567890123456790", "fleetItemIndex": 1 }
```
#### 空数据 / 降级响应
无。路径已整体移除,网关/服务对该路径的任何请求返回 404,不做业务层降级。
#### 错误响应
```json
{
"code": 404,
"message": "Not Found",
"success": false,
"data": null
}
```
(历史版本本接口曾用 605012「槽位不存在」拒绝非法 `requirementId`;该错误码已随槽位概念一并撤销,不会再出现)
#### 业务边界
- **为什么删**:去槽位化后"车辆槽位"不再是持久身份,新增一辆车不需要预先建一个空槽位再等车务选车,直接在批量派车里提交这一天这辆车即可。
- **前端改调什么**:不再调用本接口;四步向导第②步"排车"里"新增一辆车"操作,改为在本地待提交的 `dailyPlan[]` 里追加一项(服务日期+车辆+司机),随下一次 `POST /batch` 一并提交。
---
### 9. 删除车辆槽位(已删除) `DELETE /admin/fleet/assignments/slots/{slotId}`
**VO**: `(已删除,无 VO)`
#### 使用场景
**已删除**。原用途:车务删除某个车辆槽位(含已派车/待确认的),联动释放车辆/司机占用。去槽位化后"删除某天某车"改为在 `POST /batch` 提交时,该服务日对应的车辆不出现在 `dailyPlan[]` 里,由整批 diff 自动取消。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| (无) | - | - | - | - | 本接口已删除,路径不存在,调用任何入参均返回 404 |
(原入参历史参考,仅供归档:PathVar `slotId` + Body `reason`)
#### 出参 `Result<Void>`
| 字段 | 类型 | 说明 |
|------|------|------|
| (无) | - | 本接口已删除,不再返回任何响应体 |
#### 请求示例
```json
{ "_deprecated": "本接口已删除,以下为历史请求体结构,仅供归档参考", "reason": "定制师傅建议的槽位不需要" }
```
#### 响应示例
```json
{ "_deprecated": "本接口已删除,调用将得到 404,不会再有下列历史响应体", "slotId": "1934567890123456789", "removedRowCount": 1, "cancelledAssignmentCount": 0 }
```
#### 空数据 / 降级响应
无。路径已整体移除,网关/服务对该路径的任何请求返回 404,不做业务层降级。
#### 错误响应
```json
{
"code": 404,
"message": "Not Found",
"success": false,
"data": null
}
```
(历史版本本接口曾用 605012「槽位不存在」/605047「行程已结束不可操作配车」拒绝;两码随槽位概念一并撤销/失去载体,不会再出现在本路径)
#### 业务边界
- **为什么删**:槽位不再是持久身份,"删除某个槽位"这个动作本身失去操作对象;对某天不再配车不需要单独一个删除动作。
- **前端改调什么**:不再调用本接口;四步向导第②步"移除某天某车"操作,改为在本地待提交的 `dailyPlan[]` 里去掉该项,随下一次 `POST /batch` 一并提交(该日不出现即视为不配车)。
---
### 10. 按日期删除改期残留配车数据(已删除) `POST /admin/fleet/assignments/slots/{slotId}/clear-residue-dates`
**VO**: `(已删除,无 VO)`
#### 使用场景
**已删除**。原用途:订单改期后,按指定服务日期子集精确清理旧服务日在途(holding/assigned)配车残留,不影响其余日期。去槽位化后残留清理不再是独立动作,由 `POST /batch` 整批按(服务日×车辆)diff 精确取消旧日期在途行完成。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| (无) | - | - | - | - | 本接口已删除,路径不存在,调用任何入参均返回 404 |
(原入参历史参考,仅供归档:PathVar `slotId` + Body `serviceDates`(必填,YYYY-MM-DD 数组) + `reason`)
#### 出参 `Result<Void>`
| 字段 | 类型 | 说明 |
|------|------|------|
| (无) | - | 本接口已删除,不再返回任何响应体 |
#### 请求示例
```json
{ "_deprecated": "本接口已删除,以下为历史请求体结构,仅供归档参考", "serviceDates": ["2026-08-01", "2026-08-02"], "reason": "订单改期后旧日期残留" }
```
#### 响应示例
```json
{ "_deprecated": "本接口已删除,调用将得到 404,不会再有下列历史响应体", "clearedServiceDates": ["2026-08-01"], "cancelledRowCount": 1 }
```
#### 空数据 / 降级响应
无。路径已整体移除,网关/服务对该路径的任何请求返回 404,不做业务层降级。
#### 错误响应
```json
{
"code": 404,
"message": "Not Found",
"success": false,
"data": null
}
```
(历史版本本接口曾用 605012/605047/605054/605071/605600 拒绝;随槽位概念撤销,不会再出现在本路径)
#### 业务边界
- **为什么删**:去槽位化后重写排车方案本身就是"按日期 diff",不再需要一个专门的"按日期清理残留"轻动作,`POST /batch` 一次提交即完成同等效果。
- **前端改调什么**:不再调用本接口;改期后重新提交 `POST /batch`,未出现在新 `dailyPlan[]` 里的旧日期行由整批 diff 自动取消。
---
### 11. 逐日取消改期残留派车(已删除) `POST /admin/fleet/assignments/{assignmentId}/cancel-residue`
**VO**: `(已删除,无 VO)`
#### 使用场景
**已删除**。原用途:仅取消指定 `assignmentId` 对应的窗外逐日残留行,不删除槽位、不影响同槽其他日期。语义与「按日期删除改期残留配车数据」相近,同随槽位化一起退役,替代方式相同。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| (无) | - | - | - | - | 本接口已删除,路径不存在,调用任何入参均返回 404 |
(原入参历史参考,仅供归档:PathVar `assignmentId` + Body `requestId`(必填,≤64) + `cancelReason`)
#### 出参 `Result<Void>`
| 字段 | 类型 | 说明 |
|------|------|------|
| (无) | - | 本接口已删除,不再返回任何响应体 |
#### 请求示例
```json
{ "_deprecated": "本接口已删除,以下为历史请求体结构,仅供归档参考", "requestId": "residue-cancel-20260812-001", "cancelReason": "改期残留逐日取消" }
```
#### 响应示例
```json
{ "_deprecated": "本接口已删除,调用将得到 404,不会再有下列历史响应体(历史响应体为内部结果对象,字段与创建派单响应相同)" }
```
#### 空数据 / 降级响应
无。路径已整体移除,网关/服务对该路径的任何请求返回 404,不做业务层降级。
#### 错误响应
```json
{
"code": 404,
"message": "Not Found",
"success": false,
"data": null
}
```
#### 业务边界
- **为什么删**:同「按日期删除改期残留配车数据」,残留清理由 `POST /batch` 整批 diff 覆盖,不再需要按单个派单行单独取消残留。
- **前端改调什么**:不再调用本接口;改期后重新提交 `POST /batch` 即完成残留清理。
---
### 12. 看板订单详情 `GET /admin/fleet/board/orders/{orderId}`
**VO**: `(PathVariable orderId) → BoardOrderDetailVO`
#### 使用场景
派单弹窗 Step1 当前订单详情页加载时调用,展示当前订单摘要、逐日行程、大交通与当前有效用车需求的车辆日行执行情况。本单变更集中在 `vehicleSlots[]`(由"按车型聚合的全程槽位"改为"日行粒度")与 `dailyVehiclePlan[]`(删除槽位序号字段、新增送机标志)。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| orderId | Path | Long | ✅ | - | 订单 ID |
#### 出参 `Result<BoardOrderDetailVO>`
完整字段见源码 `BoardOrderDetailVO.java`(含 `activeAssignments`、`travelers`、`transport`、`progressSteps`、`operationLog` 等本单未变更字段);下表为本单变更的两个子结构:
| 字段 | 类型 | 说明 |
|------|------|------|
| vehicleSlots[] | Array | 本单由"按车型聚合的全程槽位"改为「车辆日行云」:一行=一个行程日的一条派车 |
| vehicleSlots[].serviceDate | Date | 本单新增,本行服务日期;历史全程行为 null |
| vehicleSlots[].displayNo | Integer | 本单由 `slotDisplayNo` 改名;按 (serviceDate, assignmentId) 升序的展示位次,纯展示不作身份 |
| vehicleSlots[].assignmentGroupId | String | 派车组 ID,同车同司机连续日行共用;历史行无该值时回退 assignmentId |
| vehicleSlots[].pickupParticipant | Boolean | 本单新增,当天是否参与接机 |
| vehicleSlots[].dropoffParticipant | Boolean | 本单新增,当天是否参与送机 |
| ~~vehicleSlots[].assignmentSlotId~~ | - | 本单删除:稳定车辆槽位 ID |
| ~~vehicleSlots[].fleetItemIndex~~ | - | 本单删除:需求车辆槽位序号 |
| dailyVehiclePlan[].dropoffParticipant | Boolean | 本单新增,当天是否参与送机(S3 接送机独立配置) |
| ~~dailyVehiclePlan[].fleetItemIndex~~ | - | 本单删除:稳定车辆槽位序号 |
| ~~dailyVehiclePlan[].assignmentSlotId~~ | - | 本单删除:稳定车辆槽位 ID |
#### 请求示例
```http
GET /admin/fleet/board/orders/70123456789
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"data": {
"id": "HL20260708144554930",
"orderNo": "HL20260708144554930",
"requirementId": "70123456790",
"vehicleSlots": [
{
"serviceDate": "2026-08-28",
"displayNo": 1,
"assignmentGroupId": "1934567890123456789",
"pickupParticipant": true,
"dropoffParticipant": false,
"slotStatus": "assigned",
"assignmentId": "1934567890123456789"
}
],
"dailyVehiclePlan": [
{
"serviceDate": "2026-08-28",
"assignmentId": "1934567890123456789",
"assignmentGroupId": "1934567890123456789",
"used": true,
"pickupParticipant": true,
"dropoffParticipant": false
}
]
},
"success": true
}
```
#### 空数据 / 降级响应
order-v3 不可达时回退 fleet 本地快照并将 `relatedDetailReady=false`(前端据此对 `plannerNote`/`itinerary` 等占位字段显示占位文案,不报错);无行程返回空天列表,不生成伪数据。
#### 错误响应
```json
{
"code": 401,
"message": "未登录",
"success": false,
"data": null
}
```
#### 业务边界
- `vehicleSlots[]` 不再按车型聚合展示"全程槽位",改为按 (serviceDate, assignmentId) 逐行展开;前端如仍按"槽位卡片"渲染需重构为按日期分组。
- `displayNo` 与看板列表的 `dayDisplayNo` 同口径,均为纯展示位次,不可作身份或请求参数。
- `vehicleSlots[].assignmentGroupId`、`dailyVehiclePlan[].assignmentGroupId` 历史行无值时统一回退 `assignmentId`,对任何真实行恒非空。
---
### 13. 矩阵主数据(月视图甘特) `GET /admin/fleet/matrix/grid`
**VO**: `MatrixGridReqVO → MatrixGridRespVO`
#### 使用场景
矩阵派单页月视图甘特图加载时调用(行=车、列=日期)。本单变更范围小:车段内 `parallelAssignments[].fleetItemIndex` 字段删除,`assignmentGroupId` 的"回退 assignmentId"语义在文档层面统一澄清(取值口径不变)。
#### 入参
完整字段见源码 `MatrixGridReqVO.java`(fleetTeamIds/typeKeys/season/status 等,本单未变更),不在此重复列出。
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| year | Query | Integer | ✅ | - | 年份 |
| month | Query | Integer | ✅ | 1-12 | 月份 |
#### 出参 `Result<MatrixGridRespVO>`
完整字段见源码 `MatrixGridRespVO.java`(`vehicles[]`/`statusCounts`/`fleetTeamCounts` 等本单未变更);下表为本单变更字段:
| 字段 | 类型 | 说明 |
|------|------|------|
| vehicles[].assignments[].parallelAssignments[] | Array | 同订单当前全部并行派车组 |
| ~~vehicles[].assignments[].parallelAssignments[].fleetItemIndex~~ | - | 本单删除:需求车型项序号 |
| vehicles[].assignments[].assignmentGroupId | String | 派车组 ID;历史行无该值时回退 assignmentId(本单起口径统一) |
#### 请求示例
```http
GET /admin/fleet/matrix/grid?year=2026&month=8
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"data": {
"year": 2026,
"month": 8,
"daysInMonth": 31,
"vehicles": [
{
"id": "1234567890",
"plate": "蒙A-88888",
"assignments": [
{
"id": "1234567890",
"assignmentGroupId": "1234567890",
"parallelAssignments": [
{ "assignmentGroupId": "1234567891", "vehicleCategory": "suv", "assignmentStatus": "assigned" }
]
}
]
}
]
},
"success": true
}
```
#### 空数据 / 降级响应
该车本月无派单则 `assignments` 为空数组;无并行派车组时 `parallelAssignments` 为空数组,不是 null。
#### 错误响应
```json
{
"code": 605010,
"message": "月份超出范围",
"success": false,
"data": null
}
```
#### 业务边界
- `parallelAssignments[]` 不再携带 `fleetItemIndex`;前端若曾用它做并行组去重键,须改用 `assignmentGroupId`。
- `assignmentGroupId` 历史行无值时统一回退 `assignmentId`,对任何真实行恒非空(本单起在全部矩阵响应中口径一致)。
---
### 14. 矩阵当天订单清单 `GET /admin/fleet/matrix/day-orders`
**VO**: `(RequestParam date) → List<MatrixDayOrderVO>`
#### 使用场景
点矩阵日期列头弹出当天所有订单(含已派+未派)时调用。本单变更:`assignments[].fleetItemIndex` 字段删除,排序键由 `fleetItemIndex` 改为 `assignmentId`;**本接口明确不并入虚拟待派条目**(它是「每辆车一条」的执行视图,虚拟条目无行可列)。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| date | Query | String | ✅ | 格式 `YYYY-MM-DD` | 日期 |
#### 出参 `Result<List<MatrixDayOrderVO>>`
完整字段见源码 `MatrixDayOrderVO.java`;下表为本单变更字段:
| 字段 | 类型 | 说明 |
|------|------|------|
| assignments[] | Array | 该订单展开的派单列表;本单起按 `assignmentId` 排序(原按 `fleetItemIndex` 排序) |
| assignments[].assignmentGroupId | String | 派车组 ID;历史行无该值时回退 assignmentId |
| ~~assignments[].fleetItemIndex~~ | - | 本单删除:同订单内派单序号 |
#### 请求示例
```http
GET /admin/fleet/matrix/day-orders?date=2026-05-04
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"data": [
{
"orderId": "26-0501",
"orderNumericId": "1234567890",
"orderAssignStatus": "partial",
"assignments": [
{ "assignmentId": "1234567890", "assignmentGroupId": "1234567890", "vehiclePlate": "蒙A-88888", "driverName": "王师傅", "assignmentStatus": "assigned" },
{ "assignmentId": "1234567891", "assignmentGroupId": "1234567891", "vehiclePlate": null, "driverName": null, "assignmentStatus": "unassigned" }
]
}
],
"success": true
}
```
#### 空数据 / 降级响应
当天无订单覆盖返回空数组 `[]`。**本接口不并入虚拟待派条目**——它是「每辆车一条」的执行视图,零派车行的订单在这里无行可列,不会出现在本响应中;待派发现请走看板列表或矩阵未派清单。
#### 错误响应
```json
{
"code": 100001,
"message": "date 缺失或格式非 YYYY-MM-DD",
"success": false,
"data": null
}
```
#### 业务边界
- `assignments[]` 排序键从 `fleetItemIndex` 改为 `assignmentId`;前端若依赖旧排序位次做展示或去重,需改用 `assignmentId`。
- 明确不含虚拟待派条目;零派车行订单不会出现在本清单,需要发现这类订单请用看板列表(`GET /admin/fleet/board/orders`)或矩阵未派清单(`GET /admin/fleet/matrix/unassigned-orders`)。
- 未派车的 `vehiclePlate`/`driverName` 为 null,`assignmentStatus=unassigned`;`orderAssignStatus` 按 `assignments` 聚合(全未派/部分/全派)。
---
## 四、契约约束与正确调用方式
> 本节只写**后端接受/拒绝 payload 的规则**,不写 UI 渲染建议。
### ✅ 正确 / ❌ 错误 payload 对照
| 场景 | payload |
|------|---------|
| ✅ 某天不配车 | 该日不出现在 `dailyPlan[]` 中 |
| ❌ 某天不配车(旧写法) | `{ "serviceDate": "2026-08-22", "used": false }` → 400(`used` 字段已移除) |
| ✅ 需求窗内存在空配车日 | 提交时带 `"confirmNoVehicleServiceDates": true` |
| ❌ 需求窗内存在空配车日未确认 | 不带 `confirmNoVehicleServiceDates` → 400「存在未配置车辆的服务日期时必须二次确认」 |
| ✅ 接送机配置 | 独立调 `PUT /pickup-dropoff-config`,不在 `/batch` 里传接送机字段 |
| ❌ 排车时携带接送机标志 | `dailyPlan[].pickupParticipant=true` → 400 |
| ✅ 接送机整批覆盖 | `items[]` 传"本次要参与的全部行",未出现的行自动归 0 |
| ❌ 接送机增量更新(误解) | 只传新增的一行,误以为其余行标志维持不变 → 实际会被清零 |
| ✅ 候选归属校验 | 传 `assignmentGroupId` 排除当前操作的派车组 |
| ❌ 候选归属校验(旧写法) | 传 `fleetItemIndex` 试图排除某槽位 → 不再生效(仅回退推断车型用) |
| ✅ 一键重派推荐传参 | PathVar `assignmentId`(空缺待派车行),旧 `slotId` 直接 404 |
### 切换状态时的必要动作
- 从"旧槽位排车"切到"按行程日配车":前端必须整段替换请求体构造逻辑,逐槽位 `items[]` 改造为逐日 `dailyPlan[]`,不能只是改字段名。
- 从"排车时勾选接机"切到"接送机独立步骤":前端向导必须新增第③步页面与独立提交动作,不能在第②步表单里继续携带接送机字段(会被 400 拒绝)。
- 候选查询排除派车组:前端所有传 `fleetItemIndex` 做排除的调用点必须切换为传 `assignmentGroupId`,否则改派时看不到真实冲突(被排除行没有真正排除)。
---
## 五、数据库行为
- 同一服务日可以存在多条派车记录(同日多车),不再受"一槽一天一行"的限制。
- 某天不配车时,该日不会产生任何派车记录(不是一条 `used=false` 的占位记录)。
- 提交批量派车后,未在本次提交中出现的原有派车记录会被取消或替换,不会残留旧记录与新记录并存的同日重复占用。
- 接送机两个方向的标志只有"是/否"两态,不存在"未设置"的第三态;未被本次接送机配置提交覆盖的记录,两个方向都会被置为"否"。
- 已完结(completed)状态的派车记录在服务日期层面只读,不可再被批量派车覆盖或取消。
- 新建的派车记录不再携带旧的"槽位序号"或"稳定车辆槽位"标识,这两个历史字段在新记录上恒为空。
- 派单确认执行不产生任何派车记录的增删,仅推进状态与写入确认时间。
- 删除的槽位新增/删除/残留清理相关写操作不再存在;同等效果改由批量派车一次提交的整批重写完成,不再有单独的“新增一个槽位”“删除一个槽位”“按日期清理残留”这些独立写动作。
---
## 六、边界行为
- 未登录 → 401(网关拦截)
- 请求参数非法(如 `variant` 传非 `list`/`grid`,`year`/`month` 缺失或越界)→ 100001
- 资源不存在(派单/需求)→ 语义化错误码(如 605009),不是裸 404
- 下游 order-v3 不可达时看板/矩阵读侧回退派单快照或跳过虚拟条目,不 500 不阻断页面(fail-open)
- 老数据兼容:历史行无 `assignmentGroupId` 时统一回退 `assignmentId` 展示,前端按此字段判定身份;两处例外须判空——看板详情操作日志项与虚拟待派条目
- 已取消/已完结的派车记录进入只读态,写接口对其操作返回状态类错误码而非静默忽略
- 大交通无接送需求时,确认执行不做任何接送机相关断言,直接放行
## 六.5、枚举 / 数据字典
### `assignmentStatus`(多接口出参共用,落库态+派生态)
**所属字段**: `BoardOrderRecordVO.assignmentStatus` / `MatrixUnassignedOrderVO.assignmentStatus` 等 | **类型**: `String`
| 值 | 中文 | 说明 |
|----|------|------|
| `unassigned` | 待派 | 无派车行/需求刚展开(去槽位化后由虚拟条目呈现) |
| `unassigned_urgent` | 待派·临近出团 | 派生态,出团临近 |
| `holding` | 排车锁定 | 存量待确认执行 |
| `holding_urgent` | 排车锁定·超时 | 派生态 |
| `assigned` | 已派 | 已最终确认 |
| `canceled` | 已取消 | - |
| `completed` | 已完成 | - |
### `used`(Step2 快照 cell 三态)
**所属字段**: `AssignmentCandidateRespVO.CanonicalSnapshotVO.SnapshotCellVO.used` | **类型**: `String`
| 值 | 中文 | 说明 |
|----|------|------|
| `USED` | 实际用车 | 该 group×day 有对应派车行 |
| `UNUSED` | 显式不用车 | 该 group×day 明确不派车 |
| `null` | 未编辑 | 尚未编辑到该格(不缺格,只是三态之一) |
### `driverAvailability` / `driverSort`(候选查询排序筛选)
**所属字段**: `AssignmentCandidateReqVO.driverAvailability` / `driverSort` | **类型**: `String`
| 值 | 中文 | 说明 |
|----|------|------|
| `ALL` | 全部司机 | `driverAvailability` 默认值 |
| `AVAILABLE` | 仅空闲司机 | - |
| `SMART` | 智能排序 | `driverSort` 默认值 |
| `RATING` | 按评分排序 | - |
| `YEARS` | 按驾龄排序 | - |
| `RECENT_ORDER` | 按最近派单排序 | - |
### `vehicleCategory` / `typeKeys`(车型大类规范 key,看板/矩阵/候选共用)
**所属字段**: `MatrixUnassignedOrderVO.vehicleCategory` / `BoardOrderPageReqVO.typeKeys` / `MatrixUnassignedReqVO.typeKeys` | **类型**: `String`
| 值 | 中文 | 说明 |
|----|------|------|
| `suv` | 越野 | - |
| `mpv` | 商务车 | - |
| `bus` | 大巴 | - |
| `sedan` | 轿车 | - |
## 六.6、修改前后对比
### 字段级对比
| 字段 | 改前 | 改后 |
|------|------|------|
| `POST /batch` 请求体 | `items[]`(槽位矩阵,含 `fleetItemIndex`/`used`/`pickupParticipant`) | `dailyPlan[]`(服务日×车辆×司机,无槽位序号/用车开关/接机勾选) |
| `POST /batch` 响应 | `assignments[].fleetItemIndex` 是有意义的槽位序号 | `assignments[].fleetItemIndex` 恒为 null(字段保留兼容) |
| 接送机写入位置 | 随 `POST /batch` 的 `items[].pickupParticipant` 一起提交 | 独立 `PUT /pickup-dropoff-config`,两方向各自 `pickupParticipant`/`dropoffParticipant` |
| `POST /candidates` 请求 | 用 `fleetItemIndex` 做归属校验 | 新增 `assignmentGroupId` 做归属校验,`fleetItemIndex` 降级仅供车型/座位回退推断 |
| `POST /candidates` 响应 | `canonicalSnapshot.retainedSlotIds` | `canonicalSnapshot.retainedGroupIds` |
| `POST /candidates` 响应 | `cells[].slotId` | `cells[].groupId` |
| `GET /board/orders` 响应 | 顶层 `assignmentSlots[]`(含 `assignmentSlotId`/`fleetItemIndex`) | 顶层 `dailyAssignments[]`(含 `assignmentGroupId`/`dayDisplayNo`),删除 `assignmentSlots`/`fleetItemIndex`/`assignmentSlotId` |
| `GET /board/orders` 进度 | 按车辆槽位数估算缺口 | 按日行维度四计数(`totalDailyItems` 等),不再按需求声明车数估算缺口 |
| `GET /matrix/unassigned-orders` | 只有真实未派行 | 新增虚拟待派条目(`virtualPending=true`),零派车行订单也会出现 |
| `GET /matrix/unassigned-orders` 排序 | `urgentBadge → startDay → assignmentId` | 追加末位兜底键 `orderNumericId` |
| `POST /requirements/{id}/confirm` | 无接送机门禁 | 新增 605914(接机未配置)/605915(送机未配置)门禁 |
| 错误码 | 605012(槽位不存在)/605911(索引越界)可能触发 | 两码撤销,码位保留不复用 |
### 行为级对比
| 行为 | 改前 | 改后 |
|------|------|------|
| 某天不配车 | 提交一条 `used=false` 的占位项 | 该日不出现在 `dailyPlan[]` 中,无需占位项 |
| 新声明用车需求的订单在看板/矩阵的呈现 | 预建 `unassigned` 占位行才能被发现 | 零派车行时由虚拟待派条目投射呈现,不预建占位行 |
| 接送机配置提交方式 | 排车时逐槽勾选,未勾选的行视为不变 | 独立整批提交,未出现在 `items[]` 的生效行强制归零(非增量 diff) |
| 前端派单向导步数 | 三步(订单详情→排车→确认执行) | 四步(订单详情→排车→接送机→确认执行) |
| 司机 H5 短链身份 | 基于 `assignmentSlotId` 签名 | 基于 `assignmentGroupId` 签名,旧 token 直接 605306 失效(无兼容期) |
| `POST /slots`、`DELETE /slots/{slotId}`、`POST /slots/{slotId}/clear-residue-dates`、`POST /{assignmentId}/cancel-residue` | 四个独立槽位增删/残留清理端点 | 四个端点全部删除,无替代端点;等价操作并入 `POST /batch` 的整批 diff |
| `POST /slots/{slotId}/auto-recommend` | 路径参数为稳定车辆槽位 `slotId` | 路径改为 `POST /{assignmentId}/auto-recommend`,参数改为派单行 `assignmentId` |
| `GET /board/orders/{orderId}` 响应 | `vehicleSlots[]` 按车型聚合为"全程槽位",含 `assignmentSlotId`/`fleetItemIndex`/`slotDisplayNo` | `vehicleSlots[]` 改为日行粒度,字段改 `serviceDate`/`displayNo`,新增 `dropoffParticipant`,删除 `assignmentSlotId`/`fleetItemIndex` |
| `GET /board/orders/{orderId}` 响应 | `dailyVehiclePlan[]` 含 `fleetItemIndex`/`assignmentSlotId` | 两字段删除,新增 `dropoffParticipant` |
| `GET /matrix/grid` 响应 | `assignments[].parallelAssignments[].fleetItemIndex` 存在 | 该字段删除 |
| `GET /matrix/day-orders` 响应 | `assignments[]` 按 `fleetItemIndex` 排序,含该字段 | 该字段删除,排序键改为 `assignmentId` |
## 六.7、影响评估
- **是否破坏向后兼容**: 是。旧字段(`items`/`fleetItemIndex`/`used`/`pickupParticipant` 等)携带即 400,无灰度兼容期。
- **前端是否必须同步上线**: 是。派单向导(第②③④步)、看板列表/详情、矩阵未派清单/甘特图必须同批改造,否则排车/接送机/确认执行三个写接口全部 400 无法使用。
- **前端 workaround 清理点**:
- 拖拽/操作身份从 `assignmentSlotId` 全部切到 `assignmentGroupId`(回退 `assignmentId`);
- "`dailyAssignments` 为空即待派"的推断逻辑必须删除,改判 `virtualPending`;
- 候选归属排除从传 `fleetItemIndex` 切到传 `assignmentGroupId`;
- 司机 H5 短链失效后的旧链接不可用,需引导重新获取。
## 七、不影响范围
- **仅影响**: 管理后台车务派单模块(派单向导四步、派单看板列表/详情、矩阵甘特与未派清单)
- **零影响**:
- C 端小程序/H5 除司机行程短链身份切换外的其余功能
- 订单创建、订单详情、产品/资源模块
- 近期出团 dashboard(`GET /internal/fleet/dashboard-summary`,consumer=internal,由 user-service 调用,管理后台前端不直连):按 `virtualPending` 过滤掉虚拟待派条目,保持既有 `assignmentId` 非空契约;响应 VO `FleetUpcomingTripRespVO` 本轮只更新了注释说明,字段未改名/未删列,不在本文档逐接口详情范围内
- 历史已取消/已完结派车记录的既有数据(不迁移,读侧兼容展示)
- 网关路由:`Path=/admin/fleet/**` 通配路由已覆盖新增端点 `PUT /pickup-dropoff-config`,无需新增路由配置
---
## 八、测试环境已验证
> 环境:`https://api.test.1814.love:9443`(网关)。部署:`hl-fleet-service` 8087+8187、`hl-order-service-v3` 8086+8186 双实例滚动更新,均 UP(2026-09-06 17:38 / 17:39,dev-v3 含 `9130624`)。
> 账号:`admin` 登录后 `POST /admin/auth/switch-role {"roleId":5}` 切到 `VEHICLE_MANAGER`——网关 `JwtAuthFilter` 对 `/admin/fleet/**` 强制车务角色,非车务角色一律 403,**前端联调必须先切角色**。
- [x] `POST /admin/fleet/assignments/batch` 携带 `items[]` 或 `holdMode` → `code=400`,报文:`请求包含已移除的旧字段(items/chargeableServiceDates/vehicleFeeWaiverReason/confirmAllServiceDatesFree/holdMode),请按新契约仅提交 dailyPlan`
- [x] `PUT /admin/fleet/assignments/pickup-dropoff-config` 端点存在,空 `items[]` → `code=200`(整批幂等覆盖语义下「全部取消勾选」是合法请求)
- [x] `POST /admin/fleet/assignments/{assignmentId}/auto-recommend`(改名后)存在 → 传不存在的 ID 返回业务码 `605009 派单不存在`
- [x] 旧路径 `POST /admin/fleet/assignments/slots/{slotId}/auto-recommend` → `code=404 接口不存在`
- [x] 已删除端点 `DELETE /slots/{slotId}`、`POST /slots/{slotId}/clear-residue-dates`、`POST /{assignmentId}/cancel-residue` → 均 `code=404 接口不存在`
- [x] 已删除端点 `POST /admin/fleet/assignments/slots` → `code=405 请求方法不支持: POST`(**不是 404**:`slots` 被 `DELETE /{assignmentId}` 的路径变量匹配走,Spring 判方法不支持。端点确已移除,前端按 404/405 都当作「已下线」处理)
- [x] `POST /admin/fleet/assignments/candidates` → `canonicalSnapshot.retainedGroupIds = ["2087157119630389249"]`、`cells[].groupId` 存在;`retainedSlotIds` / `cells[].slotId` 均已消失;1 组 × 3 个可编辑日 = 3 个 cell(笛卡尔积不缺格),`used` 为三态字符串(实测 `"USED"`)
- [x] `GET /admin/fleet/board/orders` 87 条记录:顶层无 `assignmentSlots` / `fleetItemIndex` / `assignmentSlotId`;每条都带 `dailyAssignments[]` 与 `virtualPending`;日行携带 `assignmentGroupId` / `serviceDate` / `pickupParticipant` / `dropoffParticipant`,无退役字段;`assignmentProgress` 为日行维度四计数(`totalDailyItems` / `dispatchedDailyItems` / `completedDailyItems` / `canceledDailyItems`)
- [x] `GET /admin/fleet/board/orders/{orderId}` `vehicleSlots[]` 已是日行粒度:新增 `serviceDate` / `displayNo` / `dropoffParticipant`,`assignmentSlotId` / `fleetItemIndex` / `slotDisplayNo` 均已移除
- [x] `GET /admin/fleet/matrix/unassigned-orders` 出现虚拟待派条目:`virtualPending=true`、`assignmentId=null`、`assignmentGroupId=null`、`parallelAssignments=[]`
- [x] 不变量:矩阵未派清单条数(1)== `GET /admin/fleet/matrix/grid` 的 `statusCounts.unassignedOrders`(1)
- [x] `GET /admin/fleet/matrix/grid` `parallelAssignments[]` 已无 `fleetItemIndex`,改带 `assignmentGroupId`
- [x] `GET /admin/fleet/matrix/day-orders?date=YYYY-MM-DD` `assignments[]` 已无 `fleetItemIndex`、带 `assignmentGroupId`,且**不并入**虚拟条目(实测无 `virtualPending=true` 记录)
- [x] 存量占位行清理:按已批准的 `hl-data-cleanup/v1` manifest `7067-unassigned-cleanup.json` 删除 `fleet_assignment` 中 75 条纯占位 `unassigned` 行(`vehicle_id`/`driver_id` 全为 NULL)及关联 17 条操作日志;执行前四道闸门计数与 manifest 逐项一致,92 行已完整备份至 `7067-unassigned-cleanup.backup.json`;删后总行 584→509、剩余 `unassigned`=0
**以下两条未取得线上证据,如实登记:**
- [ ] 看板列表出现 `virtualPending=true` 的虚拟条目 —— **本次未观察到,非缺陷**。看板的虚拟候选窗是 `[今天, 今天+lookahead]`,而清理后测试库里零派车行的需求出发日全部早于今天(最晚 2026-09-01,实测当天为 2026-09-06),按设计「行程已结束的零派车行订单不进看板」被正确排除。矩阵侧(按年月取数)已实测到虚拟条目,读侧投影逻辑本身已验证。该分支由单测覆盖。
- [ ] `POST /admin/fleet/assignments/requirements/{requirementId}/confirm` 触发 605914 / 605915 —— 需要「大交通声明需接/送机 + 对应日期未配置车辆」的特定数据组合,测试库当前无此样本,未构造(构造需改动他人测试数据)。该门禁由单测覆盖。
## 九之二、2026-09-07 第二轮返工的契约增量(前端必读)
> #7067 于 2026-09-06 被验收打回三项 P1,本节是返工后的**契约增量**,PR #7218 + #7223(均合 dev-v3)。
> 前三条是**会破坏现有渲染**的变化,请优先处理。
### ⚠️ B1(破坏性)`progressSteps` 由 3 步变 4 步,`CONFIRM_EXECUTE` 的 step 号 3 → 4
上一版本节把 `progressSteps` 列为「本单未变更字段」,**这是错的**,本次更正。
| step | code | name | 说明 |
|------|------|------|------|
| 1 | ORDER_DETAIL | 订单详情 | 不变 |
| 2 | DISPATCH | 排车 | 不变 |
| 3 | **PICKUP_DROPOFF** | **接送机** | **本次新增的一步** |
| 4 | CONFIRM_EXECUTE | 确认执行 | **step 由 3 变 4** |
**按数组下标或按数组长度取值的前端会直接破**,请改为按 `code` 取值。
第③步的状态取自接送机门禁:
- 门禁已满足(无接送机声明,或已配齐)→ `DONE`,不置 `active`;
- 有声明但未配齐 → `PROCESSING` 且 `active=true`,同时把第④步压回 `WAITING`;
- **门禁未知(见 B3)→ `WAITING` 且不置 `active`**,第④步同样 `WAITING`。
### ⚠️ B2(破坏性)`POST /admin/fleet/assignments/batch` 提交成功不再等于订单车控完成
第②步排车照常落库并推进 `assigned`,但**大交通声明要接/送机而第③步尚未配齐时,DAILY_V3 最终方案被压住不发**,order-v3 侧 `vehicle_control_status` 停在 `PROCESSING`。
响应 `Result<BatchAssignmentWriteRespVO>` 新增两个字段:
| 字段 | 类型 | 说明 |
|------|------|------|
| finalPlanPublished | Boolean | 本次是否已发布最终方案。**false = 排车已落库但接送机未配齐**,必须继续走第③步 |
| pickupDropoffGate | Object | 接送机门禁状态,结构见 B4 |
**前端必须读 `finalPlanPublished`**:为 false 时不能提示「派单完成」,应引导用户去第③步接送机;为 true 时才是完成态。第③步配齐后服务端会**立即补发**最终方案,无需回到第②步重提交。
### ⚠️ B3(破坏性)`pickupDropoffGate` 整体为 `null` 表示「门禁未知」,不是「无声明」
`GET /admin/fleet/board/orders/{orderId}` 的顶层 `pickupDropoffGate` 在 order-v3 详情上下文降级(拉取失败)时返回 `null`。此时大交通拿不到权威值,服务端**不给出乐观判定**。
前端按「未知」渲染:不展示「接送机已完成」,也不展示缺口日期条;第③④步按 B1 停在 `WAITING`。
**不要把 `null` 当作「该订单没有接送机要求」**——写侧用的是 strict 拉取,真实状态可能是「有声明未配齐、订单仍在处理中」,两侧结论会正好相反。
### B4 接送机门禁结构 `PickupDropoffGateVO`
出现在三处:`POST /batch` 响应、`PUT /pickup-dropoff-config` 响应、`GET /board/orders/{orderId}` 顶层。三处同一份结构、同一套判据。
| 字段 | 类型 | 必返 | 说明 |
|------|------|------|------|
| arrivalRequiredDates | Array\<Date\> | ✅ | 大交通声明需要**接机**的服务日(升序) |
| departureRequiredDates | Array\<Date\> | ✅ | 大交通声明需要**送机**的服务日(升序) |
| missingPickupDates | Array\<Date\> | ✅ | 其中尚未配置接机车辆的服务日(升序) |
| missingDropoffDates | Array\<Date\> | ✅ | 其中尚未配置送机车辆的服务日(升序) |
| declared | Boolean | ✅ | 该订单大交通是否有任一方向的接送机声明 |
| satisfied | Boolean | ✅ | 门禁是否已满足(无声明或已配齐)。**false 时最终方案不发布、订单车控停在处理中** |
`declared=false` 时四个数组均为空、`satisfied=true`——门禁对无接送机声明的订单整体让路,允许车务手工勾选但不强制。
已完结(`completed`)的服务日豁免:行程中换版平移后该日事实已随历史派车固化,不会再要求重配。
### B5 `PUT /admin/fleet/assignments/pickup-dropoff-config` 响应类型变更
由原来的空响应改为 `Result<PickupDropoffConfigRespVO>`:
| 字段 | 类型 | 必返 | 说明 |
|------|------|------|------|
| finalPlanPublished | Boolean | ✅ | 本次保存是否**补发**了最终方案快照(配齐即发)。只在门禁由「不满足」跨到「满足」的那一次为 true;重复保存同样已满足的状态不会重复发版 |
| requirementReopened | Boolean | ✅ | 本次是否把**已完成**的订单拉回处理中(见 B6) |
| reopenBlockedReason | String | 否 | 本该拉回处理中却没拉回的原因;`null`=不适用或已成功拉回。当前唯一取值 `REQUIREMENT_LIFECYCLE_DISABLED`(服务端需求级生命周期开关未启用),见 B6 |
| pickupDropoffGate | Object | ✅ | 写入后的门禁状态,结构见 B4 |
保存成功后订单状态到底变没变,前端从这两个布尔值当场就能知道,不需要再拉一次看板详情。
### B6 在已完成的订单上清掉要求日的接送机勾选 → 订单被拉回处理中
车务改接送机安排是正常业务动作,服务端不拒绝提交;但订单车控已经是完成态时,清掉要求日的勾选会写一条「重开」意图把用车需求拉回 `PROCESSING`,响应里 `requirementReopened=true`。前端应据此把订单状态从「已完成」改回「处理中」,并提示需要重新配齐接送机。
**例外**:服务端配置 `fleet.assign.requirement-lifecycle-enabled=false`(application.yml 默认值;测试服 Nacos `hl-fleet-service-test.yml` 已显式置 `true`,故测试环境走的是正常重开路径)时,重开意图不会被投递。此时接口**不拒绝、照常落库**,但返回 `requirementReopened=false` + `reopenBlockedReason="REQUIREMENT_LIFECYCLE_DISABLED"`——含义是「接送机勾选已保存,但订单状态没能拉回处理中」。
前端在拿到 `reopenBlockedReason` 时应提示用户「保存成功,但订单仍显示已完成,请联系运维开启需求级生命周期开关后重新保存一次」,不要把它当成失败,也不要把订单画成已回到处理中。
(为什么不整笔拒绝:该开关默认关闭,硬拒等于「改接送机安排」这条正常业务动作在默认配置下永远不可用,还会连带丢弃同一次提交里其它合法的勾选修改。)
### B7 逐日 `dropoffRequired`(读侧新增,与 `pickupRequired` 对称)
`GET /admin/fleet/board/orders/{orderId}` 的 `dailyVehiclePlan[]` 每一项新增 `dropoffRequired`(Boolean):当天大交通是否要求**送机**。原先只有 `pickupRequired`(要求接机),设计文档声称有 `dropoffRequired` 而代码里没有,本次补齐。
注意区分同名的两组字段:
- `pickupRequired` / `dropoffRequired` = **大交通要求**当天接/送机(只读,来自订单)
- `pickupParticipant` / `dropoffParticipant` = 该派车行**实际被勾为**当天的接/送机车(可写,第③步配置)
### B8 服务端配置(不影响契约,供排障参考)
| Nacos 键 | 默认 | 作用 |
|---|---|---|
| `fleet.assign.pickup-dropoff-gate-enabled` | `true` | 接送机门禁总开关。关闭即整体退回返工前行为(无条件发布最终方案、不做 605914/605915 断言),作为线上回滚阀 |
| `fleet.assign.requirement-lifecycle-enabled` | `false` | 需求级生命周期 Outbox 开关,见 B6 的例外 |
### B9 后端内部修复(无契约变更,但影响可观察行为)
- **改派后订单不再卡在处理中**:去槽位化后改派生成的替换行带全新派车组 ID 且不继承槽位,旧实现认不出「墓碑↔替换行」是同一身份,导致方案被永久判为不完整、最终方案不发布、订单卡 `PROCESSING` 且无自动恢复路径。已改为改派时在同一事务里作废墓碑的定稿身份。**前端无需改动**,但此前观察到的「改派完订单一直显示处理中」现象随之消失。
- 同源修复:改派之后不能再通过「撤销取消」把旧车复活成同日第二辆在途车。
- 「撤销取消」重建最终方案时也会先过接送机门禁,不再绕过。
**以下两条是 2026-09-07 测试服端到端实测才暴露的缺陷,均已随本次修复上线;前端无需改动,但此前的异常现象会随之消失:**
- **改派必现 `400 字段【assignment_slot_id】未填写`**,且派单操作时间线自 #7067 上线起停止记录。
操作日志表 `fleet_assignment_operation_log.assignment_slot_id` 仍是 NOT NULL 且无默认值,
而去槽位化后写入侧是**无条件**落 NULL(与锚点行是不是存量行无关),MySQL 严格模式直接
打断整笔事务。实测边界:改派 400(会写日志),确认执行 200(不写日志,不受影响);
`fleet_assignment_operation_log` 最后一条记录停在 2026-08-31、此后零新增。
已由 Flyway 把该列放开为可空,改派与操作时间线随之恢复。
- **手工调价的订单车控可能永久停在「处理中」**。派车行的调价时间列是秒级精度,
MySQL 落库时对亚秒部分四舍五入,可能比同一笔快照的完成时间晚零点几秒,
order-v3 据此判定「调价晚于完成」并整份拒收 DAILY_V3 快照(约一半概率必现,
自动计价的行不受影响)。已两侧修复:写入侧秒级下取整,校验侧按列精度给 1 秒容差。
修复前已被隔离的快照事件需要运维走 `POST /internal/fleet/jobs/vehicle-assignment-snapshot-outbox/{eventId}/requeue` 重排一次。
---
## 九之三、2026-09-07 第三轮返工(独立复审缺口)的契约增量
第二轮返工合并后的独立复审发现 2 处逻辑缺口,均属历史数据兼容与「承诺与实现不符」。
本轮对前端**没有破坏性变更**:没有新增/删除字段,没有改字段名或类型。
只有一个字段的**触发面变宽**,和一个原因码取值新增——两条都会让前端少踩坑,不用改代码也能跑,
但按下面的口径调提示语会更准确。
### C1 `requirementReopened` 的触发条件从「跃迁」变成「状态补偿」
`PUT /admin/fleet/assignments/pickup-dropoff-config` 响应里的 `requirementReopened`:
- **以前**:只有「本次保存恰好把门禁从满足变成不满足」才为 `true`。
- **现在**:只要「订单车务仍是完成态、而写入后的门禁不满足」就为 `true`,不要求本次保存发生跃迁。
为什么改:旧判据有个进不去的死角。当运维把 `fleet.assign.requirement-lifecycle-enabled`
关着的时候清空接送机勾选,第一次保存会如实返回
`reopenBlockedReason=REQUIREMENT_LIFECYCLE_DISABLED`(订单没被拉回);
随后运维开启开关、车务重新保存一次时,保存前后门禁**都已经是不满足**,跃迁不成立,
旧代码永远进不到重开分支——`requirementReopened` 恒 `false`、`reopenBlockedReason` 还退化成 `null`,
订单永久停在「已完成但门禁不满足」的不一致态。这与九之二 B6 里写的
「开关打开后重存一次即可收敛」直接矛盾。改成补偿判据后那句承诺才真正成立。
**对前端的影响**:字段名与类型不变,原有「`true` 就提示订单已回到处理中」的处理逻辑仍然成立,
只是会在更多场景下为 `true`(例如车务只是改了另一天的勾选、而某个要求日本来就没配齐)。
另外重复保存是**幂等**的:同一需求已有未消费的重开意图时不会再写第二条,此时仍返回 `true`
(含义是「已被拉回/重开意图已在途」)。
### C2 `reopenBlockedReason` 新增取值 `NO_DISPATCH_ANCHOR`
| 取值 | 含义 | 前端提示建议 |
|---|---|---|
| `null` | 不适用,或已成功拉回处理中 | 无需提示 |
| `REQUIREMENT_LIFECYCLE_DISABLED` | 需求级生命周期开关未启用;勾选已落库但订单仍是完成态 | 提示「配置已保存,订单状态需运维开启开关后重存一次收敛」 |
| **`NO_DISPATCH_ANCHOR`**(新增) | 该需求下已没有可作锚点的派车行(只剩已取消/异常/未派占位行),本该拉回却拉不回 | 提示「配置已保存,但该需求已无有效派车,请回第②步重新排车」 |
以前这种情况会返回 `requirementReopened=false` + `reopenBlockedReason=null`,
按契约会被读成「不适用」,而实际是「本该拉回却没拉回」——是个沉默失败。现在显式回报。
### C3 「重存一次即可收敛」的前提(B6 的补充说明)
九之二 B6 说「开关打开后重存一次即可收敛」,这句话有个前提:**要求日上得有可配置的派车行**。
如果某个接送机要求日根本没有排车(该日不配车),门禁在第③步怎么存都满足不了,
重存只会反复把订单拉回处理中——这种情况要回**第②步**先给该日排车,不是第③步能自愈的。
前端在 `pickupDropoffGate.missingPickupDates` / `missingDropoffDates` 命中的日期上,
若该日在排车结果里没有任何车,应引导回第②步而不是让用户在第③步反复点保存。
### C4 后端内部修复(无契约变更,但影响可观察行为)
- **存量订单二次改派后,最终方案不再被误判为不完整**。
旧模型下已经从 A 改派到 B 的订单(两者共享历史车位血缘),再把 B 改派成新模型的 C 之后,
更早的取消记录 A 会失去同血缘的有效行、重新被计入完成条件,导致改派后的最终方案发不出去、
订单车控卡在「处理中」且没有自动回到「已完成」的路径。
已修复为:改派时把「因本次改派而失去同血缘有效行」的整条历史链路一并退休。
「整槽取消待恢复」的记录不受影响,仍照旧计入完成条件(该防线未被削弱)。
---
## 十、相关文档
- 展示口径权威源(HL 仓库路径):`docs/tasks/7067-display-matrix.md`(看板/详情/四步向导/矩阵的数据源、状态范围、空态、守恒规则)
- 契约审查(HL 仓库路径):`docs/tasks/7067-contract-review.md`(变更面、Internal Feign 路由核对、序列化与空值约束、错误码变更清单)
- 关联 Issue: [wx/HL#7067](https://git.1814.love:8443/wx/HL/issues/7067)
## 关联 / 联系人
### 链接
- **Issue**: [#7067](https://git.1814.love:8443/wx/HL/issues/7067)
- **PR**: #(待回填)
### 联系人
- **后端负责人**: @wx