--- schema: "hl-changelog/v2" ticket: "5257" title: "车务看板按用车需求聚合多车型槽位" consumer: "admin" change_type: "修改接口" backend_status: "deployed" gateway_status: "verified" frontend_status: "implemented" frontend_owner: "hl-ui-codex" frontend_ref: "mmg/hl-ui@594dfa44b497b40a1e75c81b3e60abba8deb6137" target_release: "" verified_at: "2026-07-26T11:13:51+08:00" status_note: "后端 PR #5259 已合并为 dev-v3@01bf9c627,并重新部署测试环境及通过网关复验;本契约明确替代 #5216 的按槽位卡片维度。前端尚未认领,需按需求卡消费 assignmentSlots。" updated_at: "2026-07-26T03:40:29.152Z" base: "dev-v3" --- # 车务看板按用车需求聚合多车型槽位 ## 关联 - Issue: [wx/HL#5257](https://git.1814.love:8443/wx/HL/issues/5257) - 服务: `hl-fleet-service` - 前端仓库/分支: `mmg/hl-ui` / `v2.1` - 影响范围: 车务管理 → 派车看板列表、需求卡和需求详情 - 被本契约替代的旧口径: [#5216 派车看板补充槽位接送路线与就绪摘要](./24_5216_派车看板补充槽位接送路线与就绪摘要-修改接口-管理后台.md) 中“每张卡对应一个稳定车辆槽位”的维度说明 ## 关键变化 同一订单的一条当前有效用车需求可能同时需要多种车型、多个车辆槽位。例如 `SUV×1 + 商务车×1` 是 **1 条用车需求、合计 2 辆车**,不能显示成 2 条“用车需求”。 `GET /admin/fleet/board/orders` 的 `data.records[]` 从派车槽位粒度调整为当前 active `requirementId` 粒度: - 同一 `requirementId` 只返回一条 record。 - `requiredVehicles[]` 展示全部车型组及数量。 - `assignmentProgress.totalSlots` 展示合计需要的车辆数。 - 新增 `assignmentSlots[]`,完整保留逐辆派车身份、状态、车辆和司机。 - 原顶层单槽位字段兼容保留,统一表示当前最需要处理的代表槽位。 - 状态、车型或司机筛选命中需求内任一槽位时,需求只返回一次。 - 后端先按需求聚合,再排序和分页;`total` 与汇总接口不再按槽位重复计数。 请求参数、路径、HTTP method、响应包络、错误码、数据库和派车执行语义均不变。 ## 变更接口 ```http GET /admin/fleet/board/orders ``` ### 响应示例 ```json { "code": 200, "message": "成功", "data": { "records": [ { "id": "26-5256", "orderId": "2080200000000000000", "requirementId": "2080200000000000900", "requiredVehicles": [ { "vehicleType": "suv", "categoryLabel": "SUV", "seats": 5, "count": 1 }, { "vehicleType": "mpv", "categoryLabel": "商务车", "seats": 7, "count": 1 } ], "assignmentProgress": { "totalSlots": 2, "unassignedSlots": 2, "holdingSlots": 0, "assignedSlots": 0, "completedSlots": 0, "canceledSlots": 0 }, "assignmentStatus": "unassigned", "assignmentId": "2080200000000000001", "assignmentSlotId": "2080200000000000101", "fleetItemIndex": 0, "canAssign": true, "canRejectRequirement": true, "assignmentSlots": [ { "assignmentId": "2080200000000000001", "assignmentGroupId": "2080200000000000201", "assignmentSlotId": "2080200000000000101", "fleetItemIndex": 0, "slotSummary": { "requiredVehicleType": "suv", "requiredVehicleTypeLabel": "SUV", "requiredSeats": 5 }, "baseAssignmentStatus": "unassigned", "assignmentStatus": "unassigned", "assignmentStatusLabel": "待派车", "availableActionCodes": ["ASSIGN"], "vehiclePlate": null, "vehicleModel": null, "vehicleSeats": null, "driverName": null, "driverPhone": null, "urgentBadge": null, "canAssign": true }, { "assignmentId": "2080200000000000002", "assignmentGroupId": "2080200000000000202", "assignmentSlotId": "2080200000000000102", "fleetItemIndex": 1, "slotSummary": { "requiredVehicleType": "mpv", "requiredVehicleTypeLabel": "商务车", "requiredSeats": 7 }, "baseAssignmentStatus": "unassigned", "assignmentStatus": "unassigned", "assignmentStatusLabel": "待派车", "availableActionCodes": ["ASSIGN"], "vehiclePlate": null, "vehicleModel": null, "vehicleSeats": null, "driverName": null, "driverPhone": null, "urgentBadge": null, "canAssign": true } ] } ], "total": 1, "page": 1, "pageSize": 20 }, "success": true } ``` ## 新增字段 `assignmentSlots[]` 所有雪花 ID 必须按字符串消费。 | 字段 | 类型 | 空值 | 说明 | |---|---|---|---| | `assignmentId` | `String` | 否 | 逐槽位派车/改派操作使用的派单 ID | | `assignmentGroupId` | `String` | 否 | 同一派车组 ID | | `assignmentSlotId` | `String` | 否 | 跨逐日切片稳定的车辆槽位 ID | | `fleetItemIndex` | `Integer` | 否 | 当前需求 `fleet[]` 展开后的槽位序号,0 起 | | `slotSummary` | `Object` | 否 | 该槽位车型、座位、服务范围和整单槽位数 | | `baseAssignmentStatus` | `String` | 否 | 基础落库态 | | `assignmentStatus` | `String` | 否 | 当前有效状态,可含紧急派生态 | | `assignmentStatusLabel` | `String` | 否 | 后端中文状态标签 | | `lifecycleStageCode/lifecycleStageLabel` | `String` | 否 | 该槽位生命周期阶段 | | `currentStep` | `Integer` | 否 | 该槽位当前步骤 | | `availableActionCodes` | `String[]` | 否 | 该槽位可用动作;需求级驳回不在此数组 | | `vehiclePlate/vehicleModel` | `String` | 是 | 已派车辆信息 | | `vehicleSeats` | `Integer` | 是 | 已派车辆座位数 | | `vehicleFleetTeamId` | `String` | 是 | 已派车辆所属车队 ID | | `vehicleFleetTeamName/vehicleFleetTeamType/vehicleFleetTeamSettleType` | `String` | 是 | 已派车辆所属车队信息 | | `driverName` | `String` | 是 | 已派司机姓名 | | `driverPhone` | `String` | 是 | 已脱敏司机手机号 | | `urgentBadge` | `String` | 是 | `T-N` 或 `Nh`;非紧急为 `null` | | `canAssign` | `Boolean` | 否 | 该槽位是否可进入派车/改派流程 | ## 顶层兼容字段 以下既有字段没有删除,旧前端继续读取不会报错: - `assignmentId/assignmentGroupId/assignmentSlotId/fleetItemIndex` - `slotSummary` - `assignmentStatus/assignmentStatusLabel` - `lifecycleStageCode/lifecycleStageLabel/currentStep/availableActionCodes` - `currentVehicle*` - `currentDriver*` - `urgentBadge/canAssign/canRejectRequirement` 它们统一指向代表槽位,选择优先级为: ```text 未派 → 排车中 → 已派 → 已完成 → 已取消 ``` 同级按 `fleetItemIndex/assignmentSlotId/assignmentId` 稳定排序。部分已派时,顶层仍指向 未派槽位,保证原“派车派人”入口可继续派下一辆;所有槽位的真值以 `assignmentSlots[]` 和 `assignmentProgress` 为准。 `canRejectRequirement` 与需求级驳回动作只读外层 record。任一槽位已进入 `holding/assigned` 时,外层不会错误开放驳回;槽位级 `availableActionCodes` 不包含需求级驳回。 ## 筛选、分页和汇总 - 状态多选:任一槽位命中即返回该需求一次。 - 车型多选:任一需求槽位命中即返回该需求一次,`requiredVehicles[]` 仍保留全部车型。 - 司机筛选/关键词:任一槽位司机命中即返回该需求一次。 - 混合状态:顶层状态取代表槽位状态;完整状态分布读取 `assignmentProgress` 和 `assignmentSlots[]`。 - 空态:无当前有效需求或无 fleet 看板候选时返回 `records=[]/total=0`,不按人数合成虚假槽位。 - 顺序:先聚合为唯一 requirement record,再确定性排序和分页。 - `data.total`:查询范围内唯一 active `requirementId` 数,不是车辆槽位数。 - `GET /admin/fleet/board/summary`:状态和今日出团数使用相同需求粒度,不重复计数。 守恒关系: ```text assignmentProgress.totalSlots == unassignedSlots + holdingSlots + assignedSlots + completedSlots + canceledSlots assignmentProgress.totalSlots == sum(requiredVehicles[].count) ``` ## 前端处理清单 - [ ] 看板列表对每个 `records[]` 只渲染一张需求卡,不再按 `fleetItemIndex/assignmentSlotId` 拆卡,也不得展开 `assignmentSlots[]` 生成额外卡片。 - [ ] 列表 row key 优先使用 `requirementId`;仅兼容历史空值时回退 `id/orderId`。 - [ ] 需求卡展示 `requiredVehicles[]` 的全部车型组,并显示 `assignmentProgress.totalSlots` 为“需要 N 辆车”。 - [ ] 需求详情遍历 `assignmentSlots[]` 展示全部车辆槽位、各自状态和已派车辆/司机。 - [ ] 顶层按钮可继续使用代表槽位字段;逐辆操作必须使用所选 `assignmentSlots[i].assignmentId/fleetItemIndex/assignmentSlotId`,不得复用其他槽位 ID。 - [ ] 需求级驳回只使用外层 `canRejectRequirement/availableActionCodes`,不从槽位数组推断。 - [ ] 混合状态显示以 `assignmentProgress` 为准,不用顶层单一状态覆盖所有槽位。 - [ ] 继续按既有稳定状态 token 映射颜色,不按中文文案判断色值。 - [ ] 覆盖单车型×1、多车型各×1、同车型×2、部分已派、全已派、完结/取消和空列表场景。 ## 不影响范围 - 不修改用车需求 `fleet[]` 的业务语义。 - 不合并、删除或改写真实 `fleet_assignment`;派车仍逐辆、逐槽位执行。 - 不修改派车、确认、取消、改派、需求驳回接口的请求结构。 - 不修改 §7 矩阵派单的车辆槽位维度。 - 不修改数据库、网关路由、错误码、车辆/司机占用、保险或费用。 - 本交接不代表已修改、发布或验证 `mmg/hl-ui`。 ## 后端验证 - 精确复现测试:同一 `requirementId` 下 `SUV×1 + 商务车×1` 返回 `total=1/records=1`、`requiredVehicles=2` 组、`assignmentSlots=2`、 `assignmentProgress.totalSlots=2`。 - 同车型×2、部分已派、全已派、混合完结/取消、状态多选、聚合后分页和汇总去重均有自动化覆盖。 - 相关定向测试 64 项通过,0 failures/errors。 - OpenAPI diff 状态为 `not_configured`:当前环境没有 `oasdiff`,仓库也没有可复现的 Swagger 2 → OpenAPI 3 导出链;契约审查保存了 Controller/VO 字段对比和自动化测试 作为人工 fallback 证据。 ## 验证证据 - 后端 commit:`fd641651144ab429ec1b371e1902a70bf3e7f025` - 后端 PR:[wx/HL#5259](https://git.1814.love:8443/wx/HL/pulls/5259) - 合并 commit:`dev-v3@01bf9c6274d5c40e00e65b84c5224189b0fd06b8` - 合并后测试部署:现有部署 API 已滚动发布 `dev-v3`, `hl-fleet-service:8087/8187` 均健康;构建、发布成功,部署日志尾部的 `ERROR/FATAL/Exception` 命中数为 0。 - 网关复现订单 `26-5256`:HTTP/业务码均为 200,`records=1`、`total=1`、 `requiredVehicles=SUV×1+MPV×1`、`assignmentSlots=2` 且逐槽位 ID 唯一、 `assignmentProgress.totalSlots=2`、状态数量之和为 2;脱敏证据 SHA-256 为 `7ba2b05d8bdf335207771c59e089357dbe25d1fe0ac4aa0db1c91f910da563f5`。 - fleet 定向测试 64 项通过;`mvn -pl hl-fleet-service spotless:check` 611 个文件通过;`mvn -pl hl-fleet-service -am verify` 中 fleet 2386 项测试 0 failures/errors、skipped 1,完整 reactor `BUILD SUCCESS`。 - OpenAPI diff 为 `not_configured`,已保存 Controller/VO 字段对比、兼容语义、 自动化测试和脱敏网关结构作为人工 fallback 证据。 - `frontend_status=pending`:本次未修改、部署或验证 `mmg/hl-ui`; 页面仍需按“1 条需求卡 + 2 个槽位详情”的新契约完成消费。