--- schema: "hl-changelog/v2" ticket: "5187" title: "多车辆槽位原子批量派车与价格日历带价" consumer: "admin" change_type: "新增接口" backend_status: "deployed" gateway_status: "verified" frontend_status: "implemented" frontend_owner: "hl-ui-codex" frontend_ref: "mmg/hl-ui@6479adf1caf2a5caeea08a24a42bacecbaaabd6a" target_release: "hl-ui/v2.1" verified_at: "" status_note: "前端 v2.1 已实现按 fleetItemIndex 的多槽位选择、批量提交和重复车辆/司机禁选;测试环境 9527 已提供对应源码,尚待登录态页面实操验收。" updated_at: "2026-07-24" base: "dev-v3" generated: "2026-07-23T15:38:00+08:00" --- # 【新增接口·前端待处理·管理后台】多车辆槽位原子批量派车与价格日历带价 ## 目标前端 - 端类型:管理后台(Web) - 目标仓库:`mmg/hl-ui` - 目标分支:`v2.1` - 页面:车务管理 → 派车看板 → 派车派人弹窗、派单详情 - 前端交接:仅以本 `hl-api-changelog` 文档为准,不另建前端仓库工单。 - 小程序:无需处理 > **后端工单**: [wx/HL#5187](https://git.1814.love:8443/wx/HL/issues/5187) > > **后端 PR**: [wx/HL#5189](https://git.1814.love:8443/wx/HL/pulls/5189) > > **兼容性**: 既有单槽位 `POST /admin/fleet/assignments` 不变;多车订单必须改用本次批量接口, > 前端不得循环调用单派接口。 ## 一、业务口径 一条用车需求可能展开出多个车辆槽位。派车弹窗应按 `fleetItemIndex` 维护多组 “车辆 + 司机 + 协议价”,允许一次选择多辆车并一次提交。整批任一槽位失败时不得留下前面 已成功、后面失败的半批派单。 - 同一批内 `fleetItemIndex`、`vehicleId`、`driverId` 分别不可重复。 - 前端只允许选择当前需求实际展开出的待派槽位,不得自行增加超过需求数量的车辆。 - 雪花 ID 全程按字符串保存和提交。 - 同一次提交及其网络重试必须复用同一个 `requestId`;用户修改选择后主动再次提交应生成新值。 - `holdMode=1` 表示排车中等待司机确认,`holdMode=0` 表示直接派定;整批模式必须一致。 ## 变更接口 ```http POST /admin/fleet/assignments/batch Content-Type: application/json ``` 请求示例: ```json { "orderId": "2046800000000000001", "orderNo": "26-4165", "requirementId": "2046800000000000101", "startDate": "2026-07-28", "endDate": "2026-07-30", "pickupAt": "海拉尔", "dropoffAt": "满洲里", "headcount": 8, "chargeableServiceDates": [ "2026-07-28", "2026-07-29", "2026-07-30" ], "holdMode": 1, "skipCityJunctionException": false, "fromEntry": "from-board", "requestId": "fleet-batch-7fe5c3a8", "items": [ { "fleetItemIndex": 0, "vehicleId": "2046800000000000201", "driverId": "2046800000000000301", "protocolPrice": "520.00", "confirmCrossResident": false }, { "fleetItemIndex": 1, "vehicleId": "2046800000000000202", "driverId": "2046800000000000302", "protocolPrice": "860.00", "confirmCrossResident": false } ] } ``` ### 公共字段 | 字段 | 类型 | 必填 | 说明 | | --- | --- | ---: | --- | | `orderId` | String(Long) | 是 | 订单 ID | | `orderNo` | String | 否 | 订单号冗余 | | `requirementId` | String(Long) | 是 | 当前生效用车需求 ID | | `startDate` / `endDate` | LocalDate | 是 | 整批服务日期闭区间 | | `pickupAt` / `dropoffAt` | String | 否 | 接送地 | | `headcount` | Integer | 否 | 乘客人数 | | `chargeableServiceDates` | LocalDate[] | 否 | 不传=全部计费;空数组=全部免费 | | `vehicleFeeWaiverReason` | String | 条件必填 | 存在免费服务日时填写 | | `confirmAllServiceDatesFree` | Boolean | 条件必填 | 全部免费时必须为 `true` | | `holdMode` | Integer | 是 | `1=排车中`,`0=直接派定` | | `skipCityJunctionException` | Boolean | 否 | 与单派接口同义 | | `fromEntry` | String | 否 | 操作来源 | | `requestId` | String | 是 | 批次幂等键,最大 64 字符 | | `items` | Object[] | 是 | 1-20 个车辆槽位 | ### `items[]` | 字段 | 类型 | 必填 | 说明 | | --- | --- | ---: | --- | | `fleetItemIndex` | Integer | 是 | 当前需求展开后的槽位序号,0 起 | | `vehicleId` | String(Long) | 是 | 所选车辆 ID,批内不可重复 | | `driverId` | String(Long) | 是 | 所选司机 ID,批内不可重复 | | `protocolPrice` | String(BigDecimal) | 否 | 元/车天;不传时后端按车型价格日历兜底 | | `messageTemplateId` | String(Long) | 否 | `holdMode=1` 的通知模板 | | `customBody` | String | 否 | `holdMode=1` 的本次自定义通知正文 | | `confirmCrossResident` | Boolean | 否 | 跨常驻车辆组合的显式确认 | 成功响应按 `fleetItemIndex` 升序返回: ```json { "code": 200, "data": { "assignments": [ { "fleetItemIndex": 0, "assignment": { "id": "2046800000000000401", "assignmentGroupId": "2046800000000000501", "assignmentSlotId": "2046800000000000601", "assignmentStatus": "holding", "stageCode": "holding_wait_driver", "stageLabel": "排车中·等待司机确认", "currentStep": 2, "skippedStepCodes": [], "protocolPrice": "520.00", "holdSentAt": null, "confirmedAt": null, "sideEffects": null, "dailyDifferences": null } } ], "failedFleetItemIndex": null, "dailyDifferences": null }, "message": "成功", "success": true } ``` 直接派定发生订单/行程/需求冻结基线不一致时返回既有业务码 `605041`,并额外指出失败槽位: ```json { "code": 605041, "data": { "assignments": [], "failedFleetItemIndex": 1, "dailyDifferences": [ { "serviceDate": "2026-07-29", "differenceType": "CAPACITY_INSUFFICIENT", "message": "逐日车辆可用座位不足" } ] } } ``` 无论返回哪一种失败,整批均不产生部分成功数据。前端失败后保留用户当前选择并展示后端文案; `605041` 可同时高亮 `failedFleetItemIndex` 对应槽位及逐日差异。 ## 三、派车弹窗前端修改 ### 3.1 多车辆选择 当前实现只有全局单值 `selVehicle/selDriver`,再次选择会覆盖上一辆车。需改成按 `fleetItemIndex` 保存的槽位数组或 Map: ```text selectedSlots[fleetItemIndex] = { vehicle, driver, protocolPrice, confirmCrossResident } ``` - 点击某个候选车辆只修改当前待选槽位,不清空其他已选槽位。 - 已选摘要、取消车辆、司机选择和常驻组合提示均必须作用于对应槽位。 - 提交前校验所有本次待派槽位都有车辆和司机,然后一次调用批量接口。 - 禁止用 `for` 循环调用旧单派接口;那会在中途失败时留下半批状态。 - 成功后一次关闭弹窗并刷新看板;不得每成功一辆刷新一次。 #### 当前消费差距 - `useVehicleDriverPicker.js` 仍只维护一组 `selVehicle/selDriver`。 - `AssignModalFooter.vue` 仍只展示一组车辆和司机,并按这一组决定按钮是否可用。 - `useAssignFlow.js` 仍只调用 `createAssignment`,没有构造 `items[]`。 - `src/api/fleet/board.js` 尚未封装 `POST /fleet/assignments/batch`。 #### 展示矩阵 | 场景 | “已选车辆”区域 | 候选/司机联动 | 主操作 | | --- | --- | --- | --- | | 尚未选择 | 显示 `已选车辆 0/N` 和 N 个待选槽位 | 提示先选择车辆 | 禁用,显示未完成组数 | | 已选一辆 | 槽位 01 显示车牌、车型、司机和移除操作,并成为当前编辑槽位 | 已选车辆标记不可重复;司机只写入当前槽位 | 未完成全部槽位时保持禁用 | | 继续多选 | 新车辆进入下一个待选 `fleetItemIndex`;其他已选槽位保持不变 | 已被其他槽位使用的车辆和司机不可重复选择 | 全部槽位完整后启用 | | 切换槽位 | 高亮当前编辑槽位;允许单独更换车辆、司机和价格 | 候选与司机面板切换到该槽位上下文 | 完整度实时更新 | | 搜索/筛选/翻页 | 已选区域固定可见,集合不丢失 | 只改变候选列表 | 状态保持 | | HOLD 完整 | 显示 `已选择 N/N 辆,司机 N/N` | 每槽位独立司机 | `下一步 · 发送给 N 名司机` | | DIRECT 完整 | 显示 `已选择 N/N 辆,司机 N/N` | 每槽位独立司机 | `直接派定 N 辆车` | | 批量失败 | 保留全部选择;高亮 `failedFleetItemIndex` | 允许修正失败槽位 | 原批次不产生部分成功 | | 批量成功 | 清空选择并关闭弹窗 | 看板只统一刷新一次 | 仅发送一次批量请求 | “已选车辆”应作为车辆筛选与候选列表之间持续可见的紧凑区域,不得只在底栏显示最后一辆。 选择数量不得超过当前需求的待派车辆槽位数;移除某一槽位不得重排或清空其他槽位。 ### 3.2 车型价格日历自动带价 候选接口 `vehicles[].protocolPrice` 已返回所选车辆车型在服务开始日的价格日历单价。当前页面 只从订单级 `props.order.protocolPrice` 初始化输入框,导致价格日历明明有值仍显示空。 - 选中车辆时,把该车辆的 `protocolPrice` 写入对应槽位价格框。 - 每辆车独立显示、独立可编辑,提交到 `items[].protocolPrice`。 - 切换车辆时改为新车辆的价格日历值;不能沿用上一辆车的价格。 - 候选值为空时输入框可留空,后端仍会在最终保存时按所选车辆车型 + `startDate` 再兜底一次。 - 不得把一个全局价格复制给所有不同车型。 ### 3.3 联系定制师 派车看板卡片已有“联系定制师”,派单详情第 1 步和后续派车弹窗也应与房务详情保持一致: - 在详情可见区域补“联系定制师”按钮,复用现有 `open-fleet` 会话流程。 - 订单 ID 使用数字雪花字符串,不能传 `HL...` 展示号或团号。 - 按钮位置、图标、禁用态、加载态和聊天抽屉交互复用房务模块,不另做一套样式。 - 首次打开真实会话和实时未读角标仍按 [#5180 前端交接](./23_5180_订单详情联系车务独立未读红点-修改接口-管理后台.md)处理。 ## 四、派车看板默认状态筛选 这是前端初始化逻辑修复,不需要后端接口变更: - `statusSel` 初始值必须为 `[]`,页面首次进入状态框显示空/不限。 - 首次列表请求不得携带 `statuses=unassigned`,默认展示全部状态。 - 点击“重置”后的值和首次进入完全一致。 - 用户主动选择“待派车”后才传对应状态;刷新筛选结果时不得偷偷恢复默认待派车。 ## 五、前端验收清单 - [ ] 一条需求展开 2 个车辆槽位时,可同时选择 2 辆不同车辆和 2 名不同司机,第一辆不会被第二辆覆盖。 - [ ] 提交只发送 1 次 `/admin/fleet/assignments/batch`,不循环调用旧单派接口。 - [ ] 第二槽位失败时页面提示失败,刷新后两个槽位都没有半批残留。 - [ ] 价格日历有值时,选择每辆车后各自价格框立即带出对应 `protocolPrice`。 - [ ] 修改某辆车价格只影响该槽位,成功响应按槽位回显冻结价格。 - [ ] 派单详情第 1 步和派车流程均能直接“联系定制师”,交互与房务一致。 - [ ] 派车看板首次进入状态筛选为空,首次请求不传 `statuses`,默认可见全部状态。 - [ ] 主动筛选“待派车”及重置行为正确。 - [ ] 增加多槽位状态管理、批量请求映射、车型切换带价和默认空筛选的组件/组合式函数测试。 ## 验证证据 - `mvn -pl hl-fleet-service spotless:check` 通过。 - `AssignmentControllerTest + AssignmentServiceTest`:281 项通过。 - `mvn -pl hl-fleet-service -am verify` 通过:fleet 2334 项,0 failure / 0 error,1 skipped。 - 批量成功、空明细校验、重复槽位校验、字符串雪花 ID、直接派定基线差异及事务/幂等注解均有测试覆盖。 - 测试环境部署任务 `28f9048a` 成功,`hl-fleet-service` 两个滚动实例均恢复健康。 - 经测试环境网关验证: - 派车看板列表请求返回 HTTP 200 / 业务码 200。 - 批量接口空明细返回业务码 400,文案为“派单车辆槽位不能为空”。 - 批量接口重复 `fleetItemIndex` 返回业务码 100001,且未产生写入。 - 自建并标记测试订单,使用 SUV + MPV 两个槽位执行失败探针:第一槽位合法、第二槽位车辆不存在, 接口返回 605001;随后详情仍为 0 个有效派单,证明第一槽位及副作用意图随整批回滚。 - 同一测试订单使用两个合法槽位执行成功探针:接口返回 200,结果按 `fleetItemIndex=[0,1]` 排序,价格快照分别为 `700.00`、`860.00`,详情恰有 2 个 `assigned` 派单。 - 使用相同 `requestId` 重放返回业务码 100502;重放后详情仍恰有 2 个有效派单,无重复写入。 - 验收后已通过订单取消 API 精确清理自建测试订单,订单状态为 `CANCELLED`,详情有效派单恢复为 0。 - 部署后 fleet 服务与网关日志未发现 ERROR;重复槽位探针只产生预期的业务校验 WARN。 > 本文是前端接入通知,不代表已修改或发布 `mmg/hl-ui`。