--- 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` | 字段 | 类型 | 说明 | |------|------|------| | 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` | 字段 | 类型 | 说明 | |------|------|------| | 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` | 字段 | 类型 | 说明 | |------|------|------| | 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.java`,下表为本单变更及核心字段: | 字段 | 类型 | 说明 | |------|------|------| | vehicles | Object | 车辆候选独立分页(`CandidatePageVO`) | | drivers | Object | 司机候选独立分页(`CandidatePageVO`) | | suggestedDriverId | String | 已选车辆自动代入司机 ID | | canonicalSnapshot | Object | Step2 canonical 快照;无 `requirementId` 或需求上下文不可用时为 null | | canonicalSnapshot.retainedGroupIds | Array\ | 本单由 `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` 完整字段见源码 `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` #### 使用场景 矩阵派单页右侧"未派订单"面板加载时调用;车务从此处把订单拖拽到左侧甘特图某车某日发起派车。 #### 入参 | 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | |------|------|------|------|------|------| | year | Query | Integer | ✅ | 1970-9999 | 年份 | | month | Query | Integer | ✅ | 1-12 | 月份 | | typeKeys[] | Query | String[] | ❌ | `suv`/`mpv`/`bus`/`sedan`,空=全部 | 车型大类多选 | #### 出参 `Result>` 完整字段见源码 `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` | 字段 | 类型 | 说明 | |------|------|------| | 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` | 字段 | 类型 | 说明 | |------|------|------| | (无) | - | 本接口已删除,不再返回任何响应体 | #### 请求示例 ```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` | 字段 | 类型 | 说明 | |------|------|------| | (无) | - | 本接口已删除,不再返回任何响应体 | #### 请求示例 ```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` | 字段 | 类型 | 说明 | |------|------|------| | (无) | - | 本接口已删除,不再返回任何响应体 | #### 请求示例 ```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` | 字段 | 类型 | 说明 | |------|------|------| | (无) | - | 本接口已删除,不再返回任何响应体 | #### 请求示例 ```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.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.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` #### 使用场景 点矩阵日期列头弹出当天所有订单(含已派+未派)时调用。本单变更:`assignments[].fleetItemIndex` 字段删除,排序键由 `fleetItemIndex` 改为 `assignmentId`;**本接口明确不并入虚拟待派条目**(它是「每辆车一条」的执行视图,虚拟条目无行可列)。 #### 入参 | 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | |------|------|------|------|------|------| | date | Query | String | ✅ | 格式 `YYYY-MM-DD` | 日期 | #### 出参 `Result>` 完整字段见源码 `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` 新增两个字段: | 字段 | 类型 | 说明 | |------|------|------| | 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\ | ✅ | 大交通声明需要**接机**的服务日(升序) | | departureRequiredDates | Array\ | ✅ | 大交通声明需要**送机**的服务日(升序) | | missingPickupDates | Array\ | ✅ | 其中尚未配置接机车辆的服务日(升序) | | missingDropoffDates | Array\ | ✅ | 其中尚未配置送机车辆的服务日(升序) | | declared | Boolean | ✅ | 该订单大交通是否有任一方向的接送机声明 | | satisfied | Boolean | ✅ | 门禁是否已满足(无声明或已配齐)。**false 时最终方案不发布、订单车控停在处理中** | `declared=false` 时四个数组均为空、`satisfied=true`——门禁对无接送机声明的订单整体让路,允许车务手工勾选但不强制。 已完结(`completed`)的服务日豁免:行程中换版平移后该日事实已随历史派车固化,不会再要求重配。 ### B5 `PUT /admin/fleet/assignments/pickup-dropoff-config` 响应类型变更 由原来的空响应改为 `Result`: | 字段 | 类型 | 必返 | 说明 | |------|------|------|------| | 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