--- schema: "hl-changelog/v2" ticket: "8576" title: "团期配车提交 / 确认响应新增 specWarnings 车辆规格提醒清单(非错误)" consumer: "admin" author: "wx(GIT)" change_type: "修改接口" backend_status: "deployed" gateway_status: "verified" frontend_status: "pending" frontend_owner: "" frontend_ref: "" target_release: "" verified_at: "" status_note: "PR #8602 已 squash 合并 dev-v3(cfefe04a83),hl-fleet-service dev-v3 分支已滚测试服。纯新增字段,两个端点的既有字段、错误码、HTTP 形态全部未变。" updated_at: "2026-09-30" base: "dev-v3" --- # hl-fleet-service: 团期配车提交 / 确认响应新增 `specWarnings` 车辆规格提醒清单 > **存放目录**: `changelogs-v2/2026-09/` > **服务**: hl-fleet-service (端口 8089) > **PR**: #8602 > **Issue**: #8576 > **日期**: 2026-09-30 > **影响范围**: 团期配车写口两个端点 `POST /admin/fleet/group-dispatch/batches/{groupBatchId}/reconfigure` 与 `POST /admin/fleet/group-dispatch/batches/{groupBatchId}/confirm` 的响应体各新增一个 `specWarnings` 数组 --- ## ⚠️ 关键变化 - 两个端点的响应体**各新增一个字段** `specWarnings`(`List`)。**没有删字段、没有改名、没有改类型**,既有字段与错误码一个都没动。 - 🔴 **`specWarnings` 不是错误**:它出现在 **HTTP 200 + 业务成功**的响应里。提交照常成功、确认照常成功、`coverage.satisfied` 照常按「排没排满」给结论。它回答的是另一个问题——**排上的那辆车,是不是这个分组当初要的那一类、那么多座**。前端不要把它当失败处理,也不要因为它非空就回滚本地状态。 - 之前团期配车这条路**完全不读车辆实体的车型与座位**:声明 16 座大巴、实际派进 5 座 SUV,一路能确认到终态且零信号。本次补的就是这个信号。 - 提醒分两类,**字段分两组、互不相干**,前端按 `code` 分支取值即可(另一组字段在各自提醒里恒为 `null`): - `WARN_VEHICLE_TYPE_MISMATCH`(**车级**):点名到某一辆车,带 `vehicleId` / `vehiclePlate` / `declaredVehicleType` / `actualVehicleType`。 - `WARN_GROUP_SEATS_BELOW_SPEC`(**组级**,一个分组最多一条):点名到组和不达标的服务日,带 `declaredSeats` / `declaredVehicleCount` / `declaredSeatTotal` / `actualSeatTotal` / `shortageDates`。 - 两端的作用域不同,别混读: - **reconfigure**:只覆盖**本次提交新增或就地改过**的「分组 + 车辆」组合,不含本次没动的存活行(否则一次只改司机的提交会把历史遗留的不符行一起刷出来)。 - **confirm**:只覆盖**本次由「已派车」推进为「已确认」**的那些行。所以**重复确认(幂等重放)时恒为空列表**——那一次没有任何行被推进,清单的分母是空的。 - **无提醒时是空数组 `[]`,不会是 `null`**,可直接 `v-for`。 --- ## 一、背景 团期配车的「声明」来自正式团级用车需求的乘车分组(组码、车型、单车座位数 `seats`、每日车辆数 `vehicleCount`),「实际」来自车辆档案(车型 `typeKey`、座位数)。改前这两侧从来没被比对过,派错车型 / 座位不够在整条链路上零信号。 本次做成**提醒而不是硬拒**有两条已定口径的原因: | 维度 | 为什么不做成错误码 | |------|-------------------| | 座位不足 | 就绪判定里它已经是 wx 在 #7444 D9 拍板的「只提醒」档(`GroupDispatchReadinessService.WARN_SEAT_SHORTAGE`),硬拒会与该定案冲突 | | 车型不符 | 两侧处在同一字典的不同归一层级,且两侧都合法地存在取不到值的行(车辆大类行缺失 / 存量需求的历史自由文本),硬拒会把现在能正常干活的分派拦下来 | 与既有的 `GroupDispatchReadinessItemVO` 分工不同:那一份比的是「已扣司机座的可载客数 vs 该日实际用车人数」,回答「坐不坐得下」;本份比的是「名义座位合计 vs 需求方声明的计划容量 `seats × vehicleCount`」,回答「派的车是不是按计划来的」。右值不同源,不是重复。 --- ## 二、变更接口清单 | # | 接口 | 方法 | 路径 | 变更类型 | 说明 | |---|------|------|------|----------|------| | 1 | 整团逐日配车提交 | POST | `/admin/fleet/group-dispatch/batches/{groupBatchId}/reconfigure` | 响应新增字段 | 新增 `specWarnings`,覆盖本次新增或就地改过的组+车组合 | | 2 | 确认整团配车 | POST | `/admin/fleet/group-dispatch/batches/{groupBatchId}/confirm` | 响应新增字段 | 新增 `specWarnings`,覆盖本次被推进为「已确认」的行;重复确认恒为空 | --- ## 三、接口详情 ### 1. 整团逐日配车提交 `POST /admin/fleet/group-dispatch/batches/{groupBatchId}/reconfigure` **VO**: `GroupDispatchReconfigureReqVO → GroupDispatchReconfigureRespVO` #### 使用场景 团期配车页点「提交」时调用,按乘车分组提交整团逐日配车计划,服务端与现状差量比对(多删少补,旧记录软删留痕)。权限点 `fleet:group-dispatch:write`。本次改动只在响应里多加一个提醒清单,提交本身的行为与校验一条都没变。 #### 入参 | 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | |------|------|------|------|------|------| | `groupBatchId` | Path | Long | ✅ | - | 团期主订单 ID | | `requirementId` | Body | Long | ✅ | `@NotNull` | 正式团级用车需求 ID,必须等于基线当前活跃需求,落后抛 602005 | | `requirementVersion` | Body | Integer | ✅ | `@NotNull` | 正式团级用车需求版本,同上 | | `clearAll` | Body | Boolean | ❌ | - | 显式整团清零标志;为 `true` 时 `demands` 只当待清日用,不会写入任何配车行 | | `reconfigureWindowToken` | Body | String | ❌ | 团期已过资源准备阶段时必填 | 受控重开窗口令牌 | | `survivorPolicy` | Body | String | ❌ | `clearAll=true` 且存在 active 共用关系时必填 | 幸存共用派单处置策略 | | `demands` | Body | Array | ❌ | `@Valid`;`clearAll=false` 时必填 | 逐日配车需求列表 | | `demands[].tripDate` | Body | String(`yyyy-MM-dd`) | ✅ | `@NotNull` | 行程日期 | | `demands[].assignments` | Body | Array | ✅ | `@Valid` | 当日排车项列表 | | `demands[].assignments[].groupId` | Body | String | ✅ | `@NotBlank`,`@Size(max=64)` | 乘车分组键(= 需求侧 `group_code`),空值返 HTTP 业务 400「乘车分组不能为空」 | | `demands[].assignments[].vehicleId` | Body | Long | ✅ | `@NotNull` | 派出车辆 ID | | `demands[].assignments[].driverId` | Body | Long | ❌ | - | 派出司机 ID;可空 = 仅排车未排司机 | | `demands[].assignments[].remark` | Body | String | ❌ | `@Size(max=200)` | 备注 | #### 出参 `Result` | 字段 | 类型 | 说明 | |------|------|------| | `groupBatchId` | String | 团期主订单 ID(雪花,字符串) | | `requirementId` | String | 正式团级用车需求 ID(雪花,字符串) | | `requirementVersion` | Integer | 正式团级用车需求版本 | | `planVersion` | Integer/Long | 团期计划版本 | | `addedCount` | Integer | 新增派车记录数 | | `removedCount` | Integer | 软删派车记录数 | | `keptCount` | Integer | 保留未变派车记录数 | | `updatedCount` | Integer | 就地更新派车记录数 | | `aliveCount` | Integer | 存活派车记录总数 | | `addedDispatchIds` | Array\ | 新增派车记录主键列表(雪花,字符串) | | `idempotentShortCircuit` | Boolean | 本次是否被计划去重短路;`true` 是**幂等成功**,不是失败 | | `coverage` | Object | 按乘车分组的覆盖明细,见下 | | `coverage.groups[]` | Array | 每组:`groupCode` / `vehicleType` / `requiredDates` / `coveredDates` / `missingDates` / `outOfRangeDates` / `satisfied` | | `coverage.missingGroupCodes` | Array\ | 整组未提交的组码 | | `coverage.wholeBatchSatisfied` | Boolean | 全团行程日整体覆盖是否成立 | | `legacyGroupRowCount` | Integer | 无分组键的历史派车行数(非错误,仅留痕) | | `releasedShareGroupIds` | Array\ | 本次连带解除的共用关系 ID 清单 | | `keptSourceIds` | Array\ | 保留占用的 claim 来源 ID 清单 | | `releasedSourceIds` | Array\ | 占用已被真正释放的派单 ID 清单 | | `pendingReassignSourceIds` | Array\ | 待人工改派的派单 ID 清单(占用已释放,当前无车) | | `ignoredDemandDays` | Array\ | 因 `clearAll=true` 未被写入的行程日清单;`clearAll=false` 时为空列表 | | `specWarnings` | Array | 🆕 所派车辆与分组声明不符的提醒清单(**非错误,不影响提交成败**);无提醒为空数组 | | `specWarnings[].code` | String | `WARN_VEHICLE_TYPE_MISMATCH` / `WARN_GROUP_SEATS_BELOW_SPEC` | | `specWarnings[].message` | String | 中文描述,已点名到组与车牌 / 服务日,可直接展示 | | `specWarnings[].groupCode` | String | 相关乘车分组码;**两类提醒都有值** | | `specWarnings[].vehicleId` | String | 相关车辆 ID(雪花,字符串);**仅 `WARN_VEHICLE_TYPE_MISMATCH`** | | `specWarnings[].vehiclePlate` | String | 相关车牌;车辆档案未录车牌时为 null(`message` 里已退回 `ID=车辆ID`) | | `specWarnings[].declaredVehicleType` | String | 分组声明车型(归一后的规范大类 key,如 `bus`);**仅 `WARN_VEHICLE_TYPE_MISMATCH`** | | `specWarnings[].actualVehicleType` | String | 车辆实际车型(归一后的规范大类 key,如 `suv`);**仅 `WARN_VEHICLE_TYPE_MISMATCH`** | | `specWarnings[].declaredSeats` | Integer | 分组声明的**单车**座位数(含司机座);**仅 `WARN_GROUP_SEATS_BELOW_SPEC`** | | `specWarnings[].declaredVehicleCount` | Integer | 分组声明的**每日**车辆数;**仅 `WARN_GROUP_SEATS_BELOW_SPEC`** | | `specWarnings[].declaredSeatTotal` | Integer | 每日总容量 = `declaredSeats × declaredVehicleCount`,**后端算好回传,前端不要自己乘**;**仅 `WARN_GROUP_SEATS_BELOW_SPEC`** | | `specWarnings[].actualSeatTotal` | Integer | 不达标服务日里**最低**那一天的实际座位合计;**仅 `WARN_GROUP_SEATS_BELOW_SPEC`** | | `specWarnings[].shortageDates` | Array\ | 实际座位合计低于声明总容量的服务日,升序;**仅 `WARN_GROUP_SEATS_BELOW_SPEC`** | #### 请求示例 ```json { "requirementId": 5501, "requirementVersion": 3, "clearAll": false, "demands": [ { "tripDate": "2026-09-13", "assignments": [ { "groupId": "BUS", "vehicleId": 1001, "driverId": 2001, "remark": "AA 团 7 座商务" } ] }, { "tripDate": "2026-09-14", "assignments": [ { "groupId": "BUS", "vehicleId": 1001, "driverId": 2001, "remark": null } ] } ] } ``` #### 响应示例 ```json { "code": 200, "message": "成功", "success": true, "data": { "groupBatchId": "8801", "requirementId": "5501", "requirementVersion": 3, "planVersion": 7, "addedCount": 2, "removedCount": 0, "keptCount": 3, "updatedCount": 0, "aliveCount": 5, "addedDispatchIds": ["9001", "9002"], "idempotentShortCircuit": false, "coverage": { "groups": [ { "groupCode": "BUS", "vehicleType": "bus", "requiredDates": ["2026-09-13", "2026-09-14"], "coveredDates": ["2026-09-13", "2026-09-14"], "missingDates": [], "outOfRangeDates": [], "satisfied": true } ], "missingGroupCodes": [], "wholeBatchSatisfied": true }, "legacyGroupRowCount": 0, "releasedShareGroupIds": [], "keptSourceIds": [], "releasedSourceIds": [], "pendingReassignSourceIds": [], "ignoredDemandDays": [], "specWarnings": [ { "code": "WARN_VEHICLE_TYPE_MISMATCH", "message": "分组 BUS 声明车型 大巴客车,所派车辆 蒙P318A 实际为 SUV", "groupCode": "BUS", "vehicleId": "1001", "vehiclePlate": "蒙P318A", "declaredVehicleType": "bus", "actualVehicleType": "suv", "declaredSeats": null, "declaredVehicleCount": null, "declaredSeatTotal": null, "actualSeatTotal": null, "shortageDates": null }, { "code": "WARN_GROUP_SEATS_BELOW_SPEC", "message": "分组 BUS 声明每日总容量 16 座(单车 16 座 × 1 辆),实际座位合计最低仅 5 座,涉及 2 个服务日:[2026-09-13, 2026-09-14]", "groupCode": "BUS", "vehicleId": null, "vehiclePlate": null, "declaredVehicleType": null, "actualVehicleType": null, "declaredSeats": 16, "declaredVehicleCount": 1, "declaredSeatTotal": 16, "actualSeatTotal": 5, "shortageDates": ["2026-09-13", "2026-09-14"] } ] } } ``` #### 空数据 / 降级响应 无提醒时 `specWarnings` 是**空数组**,不是 `null`: ```json { "code": 200, "message": "成功", "success": true, "data": { "groupBatchId": "8801", "addedCount": 0, "removedCount": 0, "keptCount": 5, "updatedCount": 0, "aliveCount": 5, "idempotentShortCircuit": true, "specWarnings": [] } } ``` **降级(fail-open)规则** —— 下列情况提醒**不产出**,`specWarnings` 少一条或为空,这是设计行为不是丢数据: - 分组不在基线的权威分组清单里 → 该组整组跳过。 - 车辆在本次的车辆档案快照里取不到 → 该车跳过。 - 声明侧或实际侧任一方的车型归一不出规范 key(车辆大类行缺失 / 存量需求的历史自由文本)→ 不报车型不符。 - 分组的 `seats` 或 `vehicleCount` 为 null 或 ≤ 0 → 不做容量判定。 - 某个(分组 + 服务日)格里**只要有一辆车**的座位数取不到或 ≤ 0 → **整格不判**(不把缺值折成 0,折 0 会产出一个不存在的缺口)。 #### 错误响应 既有错误码一条都没变。示例(分组不在本团需求内): ```json { "code": 602001, "message": "分组 VAN 不在本团正式用车需求的分组清单内", "success": false, "data": null } ``` 完整错误码:602000(排车项缺分组,服务层兜底;admin 口由入参校验先拦下返 400「乘车分组不能为空」)/ 602001 分组不在本团需求内 / 602002 整组未排车 / 602003 该组服务日未排满 / 602004 该组排了本组服务范围外的日期 / 602005 需求身份或版本已变 / 602006 需求状态不允许 / 602009 取不到权威分组清单 / 600003 重复行程日 / 600004 单日排车为空 / 600005 缺车辆 ID / 600006 车辆被占 / 600007 司机被占 / 600008 并发修改 / 600009 基线不可用 / 600010 团期状态不可配 / 600011 全团服务日未覆盖满 / 605037 车辆维保或停用不可派 / 605038 司机休假或待激活不可派 / 605006 司机已黑名单 / 605013 司机非在册赛季不可派单。 #### 业务边界 - **鉴权**:权限点 `fleet:group-dispatch:write`(与读口 `fleet:group-dispatch:view` 分开);未登录由网关拦截返 401。 - **`specWarnings` 非错误**:HTTP 200 + `success=true` 的响应里出现,提交已成功落库。不要据此回滚本地状态或阻断后续动作。 - **作用域**:只覆盖**本次提交新增或就地改过**的「分组 + 车辆」组合;本次没动的存活行不重判(一次只改司机的提交不会把历史遗留的不符行刷出来)。 - **两类提醒字段分组互斥**:按 `code` 分支取值,另一组字段恒 `null`。 - **`declaredSeatTotal` 由后端算好**:口径(含不含司机座、按不按日)只有一份权威,前端不要复算。 - **`actualSeatTotal` 是最低值不是明细**:它与 `declaredSeatTotal` 一起答完「最坏差多少」,不需要拿 `shortageDates` 反查每一天。 - **防重提交与幂等是两件事**:10 秒内对同一份计划重复提交会被防重窗口**拒绝**(返「团期配车重配处理中,请勿重复提交」);窗口之外重复提交同一份计划会正常受理并返回 `idempotentShortCircuit=true`,**那是成功**。 - **`clearAll=true` 时必须读 `ignoredDemandDays`**:否则「清完并按新计划重排」与「只清空」在响应里长得一模一样(两者 `addedCount` 都是 0)。 - **雪花 ID 一律是字符串**:`groupBatchId` / `requirementId` / `addedDispatchIds[]` / `specWarnings[].vehicleId` 等都以字符串下发。 --- ### 2. 确认整团配车 `POST /admin/fleet/group-dispatch/batches/{groupBatchId}/confirm` **VO**: `GroupDispatchConfirmReqVO → GroupDispatchConfirmRespVO` #### 使用场景 团期配车页点「确认」时调用,把该团全部「已派车」的配车行转为「已确认」,并登记一条把正式用车需求推进到「已发车务」的异步回写意图。权限点与提交写口同一个 `fleet:group-dispatch:write`。本次改动只在响应里多加一个提醒清单。 #### 入参 | 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | |------|------|------|------|------|------| | `groupBatchId` | Path | Long | ✅ | - | 团期主订单 ID | | `requirementId` | Body | Long | ✅ | `@NotNull` | 正式团级用车需求 ID | | `requirementVersion` | Body | Integer | ✅ | `@NotNull` | 正式团级用车需求版本 | | `remark` | Body | String | ❌ | `@Size(max=200)` | 确认备注,**仅留痕**,不写入配车行 | #### 出参 `Result` | 字段 | 类型 | 说明 | |------|------|------| | `groupBatchId` | String | 团期主订单 ID(雪花,字符串) | | `confirmedCount` | Integer | 本次由「已派车」转为「已确认」的配车行数;**重复确认为 0,属幂等成功** | | `alreadyConfirmedCount` | Integer | 确认前就已是「已确认」的配车行数(重复确认时全部落在这里) | | `requirementId` | String | 本次确认所依据的正式团级用车需求 ID(雪花,字符串) | | `requirementVersion` | Integer | 本次确认所依据的需求版本 | | `planVersion` | Integer/Long | 当前团期计划版本(确认不改计划,故不递增) | | `requirementAdvanceIntent` | String | 已登记的需求回写意图方向,恒为 `CONFIRMED_TO_DISPATCHED` | | `coverage` | Object | 按乘车分组的覆盖明细(确认前对库里现存配车行重判一次的结果),结构同 reconfigure | | `legacyGroupRowCount` | Integer | 本团存活派车行里没有乘车分组键的历史行数;非零时 602008 的缺口很可能正是它们造成的 | | `specWarnings` | Array | 🆕 本次被推进为「已确认」的行里,所派车辆与分组声明不符的提醒清单(**非错误,确认已成功**);无提醒为空数组 | | `specWarnings[].code` | String | `WARN_VEHICLE_TYPE_MISMATCH` / `WARN_GROUP_SEATS_BELOW_SPEC` | | `specWarnings[].message` | String | 中文描述,已点名到组与车牌 / 服务日,可直接展示 | | `specWarnings[].groupCode` | String | 相关乘车分组码;两类提醒都有值 | | `specWarnings[].vehicleId` | String | 相关车辆 ID(雪花,字符串);仅 `WARN_VEHICLE_TYPE_MISMATCH` | | `specWarnings[].vehiclePlate` | String | 相关车牌;未录车牌时为 null | | `specWarnings[].declaredVehicleType` | String | 分组声明车型(规范 key);仅 `WARN_VEHICLE_TYPE_MISMATCH` | | `specWarnings[].actualVehicleType` | String | 车辆实际车型(规范 key);仅 `WARN_VEHICLE_TYPE_MISMATCH` | | `specWarnings[].declaredSeats` | Integer | 分组声明单车座位数;仅 `WARN_GROUP_SEATS_BELOW_SPEC` | | `specWarnings[].declaredVehicleCount` | Integer | 分组声明每日车辆数;仅 `WARN_GROUP_SEATS_BELOW_SPEC` | | `specWarnings[].declaredSeatTotal` | Integer | 每日总座位数(后端算好);仅 `WARN_GROUP_SEATS_BELOW_SPEC` | | `specWarnings[].actualSeatTotal` | Integer | 不达标日中的最低实际座位合计;仅 `WARN_GROUP_SEATS_BELOW_SPEC` | | `specWarnings[].shortageDates` | Array\ | 不达标的服务日(升序);仅 `WARN_GROUP_SEATS_BELOW_SPEC` | #### 请求示例 ```json { "requirementId": 5501, "requirementVersion": 3, "remark": "与地接确认车辆无误" } ``` #### 响应示例 ```json { "code": 200, "message": "成功", "success": true, "data": { "groupBatchId": "8801", "confirmedCount": 8, "alreadyConfirmedCount": 0, "requirementId": "5501", "requirementVersion": 3, "planVersion": 7, "requirementAdvanceIntent": "CONFIRMED_TO_DISPATCHED", "coverage": { "groups": [ { "groupCode": "BUS", "vehicleType": "bus", "requiredDates": ["2026-09-13", "2026-09-14"], "coveredDates": ["2026-09-13", "2026-09-14"], "missingDates": [], "outOfRangeDates": [], "satisfied": true } ], "missingGroupCodes": [], "wholeBatchSatisfied": true }, "legacyGroupRowCount": 0, "specWarnings": [ { "code": "WARN_VEHICLE_TYPE_MISMATCH", "message": "分组 BUS 声明车型 大巴客车,所派车辆 蒙P318A 实际为 SUV", "groupCode": "BUS", "vehicleId": "1001", "vehiclePlate": "蒙P318A", "declaredVehicleType": "bus", "actualVehicleType": "suv", "declaredSeats": null, "declaredVehicleCount": null, "declaredSeatTotal": null, "actualSeatTotal": null, "shortageDates": null } ] } } ``` #### 空数据 / 降级响应 **重复确认(幂等重放)**:`confirmedCount=0`、`alreadyConfirmedCount=N`、`specWarnings` 恒为**空数组**(本次没有任何行被推进,清单的分母是空的)。HTTP 仍是 200,**这是成功不是失败**: ```json { "code": 200, "message": "成功", "success": true, "data": { "groupBatchId": "8801", "confirmedCount": 0, "alreadyConfirmedCount": 8, "requirementId": "5501", "requirementVersion": 3, "planVersion": 7, "requirementAdvanceIntent": "CONFIRMED_TO_DISPATCHED", "legacyGroupRowCount": 0, "specWarnings": [] } } ``` **降级(fail-open)规则**与 reconfigure 端点逐条相同:分组不在基线 / 车辆取不到 / 任一侧车型归一不出规范 key / `seats` 或 `vehicleCount` 为 null 或 ≤0 / 某(组+日)格里有一辆车座位取不到 → 对应提醒不产出。 #### 错误响应 既有错误码一条都没变。示例(现存配车对当前需求仍不完整): ```json { "code": 602008, "message": "现存配车对当前需求仍不完整:分组 BUS 缺 2026-09-15", "success": false, "data": null } ``` 完整错误码:602007 本团无可确认的配车行 / 602008 现存配车对当前需求仍不完整 / 602005 需求身份或版本已变 / 602006 需求状态不允许 / 602009 取不到权威分组清单 / 600008 并发修改 / 600009 基线不可用 / 605037 车辆维保或停用不可派 / 605038 司机休假或待激活不可派 / 605006 司机已黑名单 / 605013 司机非在册赛季不可派单。 #### 业务边界 - **鉴权**:权限点 `fleet:group-dispatch:write`(与提交写口同一个);未登录由网关拦截返 401。 - **`specWarnings` 非错误**:确认已经成功。它是终态前的最后一次复核——重配与确认之间车辆档案可能被改过,也可能有行绕过重配直接进来。 - **作用域**:只覆盖**本次由「已派车」推进为「已确认」**的那些行;已是「已确认」的行不重判。 - **重复确认时恒为空列表**:不要把「第二次点确认没有提醒」理解成「问题已经消失」。 - **本端点没有防重提交时间窗**:连点多少次都是 `confirmedCount=0 / alreadyConfirmedCount=N` 这个形态,不会出现「请勿重复提交」这类错误码;同团的并发调用由服务端串行化。 - **异步回写**:响应成功只代表车务侧已确认并已把回写意图可靠登记,正式用车需求的状态可能稍后才变成「已发车务」,需求页需自行刷新。 - **回写会推进需求版本但不会让重复确认变成错误**:首次确认成功后正式用车需求被推进一版(status 转 DISPATCHED),此时本端点跳过需求版本与状态的严格校验,仍返回 200 + 两个计数。 - **不校验团期是否可配**:那道门禁管的是「还能不能改车」,确认不改车。 - **雪花 ID 一律是字符串**。 --- ## 四、契约约束与正确调用方式 > 本节只写后端接受 / 拒绝 payload 的规则,不写 UI 渲染建议。 ### ✅ 正确 / ❌ 错误 payload 对照 | 场景 | payload | |------|---------| | ✅ 正常提交两天配车 | `{ "requirementId": 5501, "requirementVersion": 3, "clearAll": false, "demands": [ { "tripDate": "2026-09-13", "assignments": [ { "groupId": "BUS", "vehicleId": 1001 } ] } ] }` | | ✅ 整团清零 | `{ "requirementId": 5501, "requirementVersion": 3, "clearAll": true, "demands": [] }` | | ✅ 确认(带留痕备注) | `{ "requirementId": 5501, "requirementVersion": 3, "remark": "与地接确认车辆无误" }` | | ✅ 确认(不带备注) | `{ "requirementId": 5501, "requirementVersion": 3 }` | | ❌ 排车项缺分组键 | `{ ..., "assignments": [ { "vehicleId": 1001 } ] }` → 400「乘车分组不能为空」 | | ❌ 缺需求版本 | `{ "requirementId": 5501, "demands": [...] }` → 400「正式团级用车需求版本不能为空」 | | ❌ 确认备注超 200 字 | `{ ..., "remark": "<201 字>" }` → 400「确认备注长度不能超过 200」 | ### 处理 `specWarnings` 的必要动作 - 两个端点的成功分支里都要读 `specWarnings`:非空时就地展示(`message` 已经是完整中文句子,可直接渲染),**不要**把它接到错误处理分支上。 - 按 `code` 分支取字段,不要对全部字段做非空假设——另一类提醒的字段组恒为 `null`。 - `declaredSeatTotal` 直接用后端回传的值,不要用 `declaredSeats × declaredVehicleCount` 自己算。 - reconfigure 的 `specWarnings` 只反映本次动过的行:`specWarnings` 为空**不等于**全团没有不符行,只等于「本次动的这些行没有不符」。 --- ## 五、数据库行为 两个端点都是写端点,但**本次改动零写入变化**——`specWarnings` 完全由内存中的比对产出(`GroupDispatchVehicleSpecInspector` 是纯静态、无 IO),不新建表、不加列、不落任何提醒记录。 | 前端提交 | 配车行的写入 | 提醒的持久化 | |----------|--------------|--------------| | `reconfigure` 差量提交 | 多删少补,旧记录软删留痕(本次未变) | **不落库**,仅随本次响应下发 | | `reconfigure` `clearAll=true` | 清空存活行,`demands` 不写入 | **不落库** | | `confirm` 首次确认 | 「已派车」行 CAS 推进为「已确认」,登记回写意图(本次未变) | **不落库** | | `confirm` 重复确认 | 一个字段都不动 | **不落库**,且恒为空数组 | 因此**刷新页面或重新拉取不会再拿到同一批提醒**——提醒是本次动作的返回值,不是可查询的状态。 --- ## 六、边界行为 - 未登录 → 401(网关拦截)。 - 权限点 `fleet:group-dispatch:write` 缺失 → 权限校验失败。 - 团期不存在 / 基线不可用 → 600009。 - 需求身份或版本落后 → 602005(fail-closed,不接受「反正车没变」)。 - 10 秒内重复提交同一份 reconfigure 计划 → 被防重窗口拒绝,提示「团期配车重配处理中,请勿重复提交」。 - 窗口外重复提交同一份计划 → 200 + `idempotentShortCircuit=true`(成功)。 - 重复 confirm → 200 + `confirmedCount=0`、`specWarnings=[]`(成功)。 - 车辆档案未录车牌 → `specWarnings[].vehiclePlate` 为 null,但 `message` 里退回 `ID=车辆ID`,不留空白。 - 老数据兼容:历史派车行没有乘车分组键时不计入任何组的覆盖,计入 `legacyGroupRowCount`,也不进 `specWarnings`。 --- ## 六.5、枚举 / 数据字典 ### code(车辆规格提醒项代码) **所属字段**: `specWarnings[].code` | **类型**: `String` | 值 | 中文 | 说明 | |----|------|------| | `WARN_VEHICLE_TYPE_MISMATCH` | 车型与分组声明不符 | **车级**提醒。两侧车型各自归一成规范大类 key 后不相等时产出。带 `vehicleId` / `vehiclePlate` / `declaredVehicleType` / `actualVehicleType`;其余字段为 null | | `WARN_GROUP_SEATS_BELOW_SPEC` | 分组座位低于声明容量 | **组级**提醒,一个分组最多一条。判据:`该组该日实际座位合计 < seats × vehicleCount`。带 `declaredSeats` / `declaredVehicleCount` / `declaredSeatTotal` / `actualSeatTotal` / `shortageDates`;其余字段为 null | ### declaredVehicleType / actualVehicleType(归一后的车型规范大类 key) **所属字段**: `specWarnings[].declaredVehicleType`、`specWarnings[].actualVehicleType` | **类型**: `String` 取值是车型字典归一后的**规范大类 key**(如 `bus` / `suv`),**不是**车辆档案里的原值——车辆档案侧存的是开集原值(例如测试环境 SUV 大类的 `type_key` 实际是 `suv2`),后端归一后才比。前端如需展示中文名,用 `message` 里已经拼好的中文,不要自己拿 key 去查字典。 ### requirementAdvanceIntent(需求回写意图方向) **所属字段**: `GroupDispatchConfirmRespVO.requirementAdvanceIntent` | **类型**: `String` | 值 | 中文 | 说明 | |----|------|------| | `CONFIRMED_TO_DISPATCHED` | 已确认 → 已发车务 | 当前**恒为此值**;表示确认成功后还有一步异步回写,需求列表页的状态可能稍后才变 | --- ## 六.6、修改前后对比 ### 字段级对比 | 字段 | 改前 | 改后 | |------|------|------| | `GroupDispatchReconfigureRespVO.specWarnings` | 不存在 | 🆕 `List`,无提醒为空数组 | | `GroupDispatchConfirmRespVO.specWarnings` | 不存在 | 🆕 `List`,无提醒为空数组 | | 两个响应体的其余全部字段 | — | 未变(无删除、无改名、无类型变化) | | 两个请求体 | — | 未变(一个字段都没动) | | 两个端点的错误码集合 | — | 未变 | ### 行为级对比 | 行为 | 改前 | 改后 | |------|------|------| | 派错车型(声明大巴、实际 SUV) | 全链路零信号,一路能确认到终态 | 提交与确认的响应里各出一条 `WARN_VEHICLE_TYPE_MISMATCH` | | 某服务日实际座位合计低于声明容量 | 全链路零信号 | 出一条 `WARN_GROUP_SEATS_BELOW_SPEC`,带最低值与不达标日清单 | | 提交 / 确认的成败判定 | 按覆盖与资源可派性 | 未变——`specWarnings` 不参与成败判定 | | 重复确认 | `confirmedCount=0`、`alreadyConfirmedCount=N` | 未变,额外 `specWarnings=[]` | | 只改司机的提交 | — | 不会把历史遗留的不符行刷出来(作用域限本次动过的组+车组合) | --- ## 六.7、影响评估 - **是否破坏向后兼容**: 否(纯新增字段,既有字段与错误码零变化;老前端忽略新字段即可正常工作) - **前端是否必须同步上线**: 否(不读新字段不会报错,只是拿不到提醒) - **前端 workaround 清理点**: 无(此前没有任何前端侧的车型 / 座位比对,不存在需要撤掉的本地实现) --- ## 七、不影响范围 - **仅影响**: `POST /admin/fleet/group-dispatch/batches/{groupBatchId}/reconfigure` 与 `POST /admin/fleet/group-dispatch/batches/{groupBatchId}/confirm` 两个响应体各新增一个数组字段。 - **零影响**: - 团期配车所有读口(概览、就绪判定、矩阵、看板) - `GroupDispatchReadinessItemVO` 的座位就绪判定(右值不同源,本次未动) - 单车派单、改派、取消链路 - order-v3 侧的正式团级用车需求存 / 读 / 撤回 / 免车 - 车辆档案、司机档案的任何端点 - 历史数据:提醒不落库,不做任何数据迁移 --- ## 八、测试环境已验证 - **代码事实**(对 `origin/dev-v3` 逐一查证): - 合并提交 `cfefe04a83`(PR #8602 squash 合并进 `dev-v3`)。 - 新增 VO `hl-fleet-service/.../dispatch/vo/GroupDispatchSpecWarningRespVO.java`(12 个字段)与对位 Feign DTO `GroupDispatchSpecWarningDTO`。 - 新增纯静态无 IO 的 `GroupDispatchVehicleSpecInspector`;两条提醒的消息拼装、fail-open 跳过条件、`seatTotalOrNull` 的「一辆车取不到座位就整格不判」逻辑均已逐行核对。 - `GroupDispatchReconfigureRespVO` 与 `GroupDispatchConfirmRespVO` 各新增 `specWarnings` 字段,javadoc 分别写明作用域(本次新增/就地改过 vs 本次被推进)与「重复确认恒为空列表」。 - **部署**:`hl-fleet-service` 的 `dev-v3` 分支已滚到测试服,两个端点走管理端网关 `/admin/fleet/**` 既有路由,无新增路由。 ``` POST /admin/fleet/group-dispatch/batches/{groupBatchId}/reconfigure → 200 + data.specWarnings 存在(无提醒时为 [])✓ POST /admin/fleet/group-dispatch/batches/{groupBatchId}/confirm → 200 + data.specWarnings 存在(无提醒时为 [])✓ POST .../confirm 重复调用 → 200 + confirmedCount=0 + specWarnings=[] ✓ ``` --- ## 九、相关历史 PR | PR | Issue | 说明 | 是否仍有效 | |----|-------|------|------------| | — | #7442 | 团期配车写口(reconfigure / confirm)首次落地 | ✅ 有效 | | — | #7444 D9 | wx 拍板座位不足在就绪判定里只提醒不硬拒 | ✅ 有效(本单沿用该口径) | | — | #8195 | 需求侧车型归一后回写规范 key | ✅ 有效(本单的比对依赖它) | | — | #8528 | reconfigure / confirm 资源可派性硬校验(605037/605038/605006/605013) | ✅ 有效 | | **本 PR #8602** | **#8576** | 两个写口响应新增 `specWarnings` | ✅ 最新 | --- ## 十、相关文档 - 关联 Issue: [wx/HL#8576](https://git.1814.love:8443/wx/HL/issues/8576) - 关联 PR: [wx/HL#8602](https://git.1814.love:8443/wx/HL/pulls/8602) ## 关联 / 联系人 ### 链接 - **Issue**: [#8576](https://git.1814.love:8443/wx/HL/issues/8576) - **PR**: [#8602](https://git.1814.love:8443/wx/HL/pulls/8602) - **Merge commit**: [cfefe04a83](https://git.1814.love:8443/wx/HL/commit/cfefe04a83) ### 联系人 - **后端负责人**: @wx