文件
hl-api-changelog/changelogs-v2/2026-09/30_8576_团期配车提交与确认响应新增车辆规格提醒清单-修改接口-管理后台.md
T
API Changelog Bot和Claude Opus 5 903e30b4d1
changelog-filename-gate / validate (push) Failing after 1s
docs(changelog): 订正 #8576 的 frontend_status 误判,并给 #8597/#8603 补 not_required 的限定
#8576:我在 2026-09-30 把它从 not_required 改成 pending 是错的,本次改回。
错因是读了落后 693 个提交的 hl-ui 本地工作树——#8464(提交 d7e932ac,2026-09-28)
已整体删除 src/views/fleet/group-dispatch/,src/api/fleet/group-dispatch.js 随之
收缩到只剩 getGroupDispatchPendingBatches,reconfigure / confirm 在前端已无消费方。
对 origin/v2.1 第三次复核:specWarnings 在 src/ 下 0 命中(唯一命中在 .claude 备忘文件),
reconfigure 的 src/ 命中全是注释或 order-v2 同词异义;阳性对照 13 个文件有 export function、
matrix.js 有活跃消费方,证明检索本身有分辨力。

#8597:点明「房务控制台」整个域在前端尚不存在(9 个关键词 0 命中,阳性对照 house-allocation
活跃),故此处的 not_required 是「没有可改的代码」而非「对现有页面透明」,需与 #8491 一并核对。

#8603:点名前端真实渲染点 Step3PickupDropoff.vue:178-179 读的是 props.order.pickupDropoffGate
(未动的那个对象),与本次删字段的 ConfirmRequirementRespVO 不是同一载体,避免下一个人
按字段名 grep 得出相反结论。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-30 15:55:39 +08:00

624 行
35 KiB
Markdown
原始文件 Blame 文件历史

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
---
schema: "hl-changelog/v2"
ticket: "8576"
title: "团期配车提交 / 确认响应新增 specWarnings 车辆规格提醒清单(非错误)"
consumer: "admin"
author: "wx(GIT)"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "not_required"
frontend_owner: ""
frontend_ref: ""
target_release: ""
verified_at: ""
status_note: "PR #8602 已 squash 合并 dev-v3(cfefe04a83),hl-fleet-service dev-v3 分支已滚测试服。纯新增字段,两个端点的既有字段、错误码、HTTP 形态全部未变。【frontend_status 取 not_required 的依据,2026-09-30 对 hl-ui origin/v2.1 第三次复核】两个写口在前端已无消费方:#8464(2026-09-28,提交 d7e932ac)整体删除了 src/views/fleet/group-dispatch/ 页面模块,src/api/fleet/group-dispatch.js 随之收缩到只剩 getGroupDispatchPendingBatches 一个出口(该文件头部注释明写 reconfigure / confirm / readiness / share-groups / share-member-candidates 已移除,页面恢复时从 git 历史找回)。复核读数:specWarnings 全仓 1 命中且落在 .claude/agents/memory/ 的备忘文件里、src/ 下 0;reconfigure 在 src/ 下的命中全部是注释或 order-v2「受控重开窗口」令牌机制的同词异义。阳性对照:同目录 13 个文件有 export function、matrix.js 有活跃消费方,故检索本身有分辨力。⚠️ 本条 2026-09-30 一度被我改成 pending,依据是「group-dispatch.js 消费 reconfigure / confirm」—— 那是读了落后 693 个提交的本地工作树得出的,该判断作废;后端端点仍全部保留可用,日后恢复页面时按 specWarnings[].code 分支弹提醒即可。"
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<GroupDispatchSpecWarningRespVO>`)。**没有删字段、没有改名、没有改类型**,既有字段与错误码一个都没动。
- 🔴 **`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<GroupDispatchReconfigureRespVO>`
| 字段 | 类型 | 说明 |
|------|------|------|
| `groupBatchId` | String | 团期主订单 ID(雪花,字符串) |
| `requirementId` | String | 正式团级用车需求 ID(雪花,字符串) |
| `requirementVersion` | Integer | 正式团级用车需求版本 |
| `planVersion` | Long | 团期计划版本 |
| `addedCount` | Integer | 新增派车记录数 |
| `removedCount` | Integer | 软删派车记录数 |
| `keptCount` | Integer | 保留未变派车记录数 |
| `updatedCount` | Integer | 就地更新派车记录数 |
| `aliveCount` | Integer | 存活派车记录总数 |
| `addedDispatchIds` | Array\<String\> | 新增派车记录主键列表(雪花,字符串) |
| `idempotentShortCircuit` | Boolean | 本次是否被计划去重短路;`true` 是**幂等成功**,不是失败 |
| `coverage` | Object | 按乘车分组的覆盖明细,见下 |
| `coverage.groups[]` | Array | 每组:`groupCode` / `vehicleType` / `requiredDates` / `coveredDates` / `missingDates` / `outOfRangeDates` / `satisfied` |
| `coverage.missingGroupCodes` | Array\<String\> | 整组未提交的组码 |
| `coverage.wholeBatchSatisfied` | Boolean | 全团行程日整体覆盖是否成立 |
| `legacyGroupRowCount` | Integer | 无分组键的历史派车行数(非错误,仅留痕) |
| `releasedShareGroupIds` | Array\<String\> | 本次连带解除的共用关系 ID 清单 |
| `keptSourceIds` | Array\<String\> | 保留占用的 claim 来源 ID 清单 |
| `releasedSourceIds` | Array\<String\> | 占用已被真正释放的派单 ID 清单 |
| `pendingReassignSourceIds` | Array\<String\> | 待人工改派的派单 ID 清单(占用已释放,当前无车) |
| `ignoredDemandDays` | Array\<String\> | 因 `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\<String\> | 实际座位合计低于声明总容量的服务日,升序;**仅 `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 会产出一个不存在的缺口)。
#### 错误响应
既有错误码一条都没变。示例(分组不在本团需求内,消息模板 `乘车分组不存在于本团正式需求: {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<GroupDispatchConfirmRespVO>`
| 字段 | 类型 | 说明 |
|------|------|------|
| `groupBatchId` | String | 团期主订单 ID(雪花,字符串) |
| `confirmedCount` | Integer | 本次由「已派车」转为「已确认」的配车行数;**重复确认为 0,属幂等成功** |
| `alreadyConfirmedCount` | Integer | 确认前就已是「已确认」的配车行数(重复确认时全部落在这里) |
| `requirementId` | String | 本次确认所依据的正式团级用车需求 ID(雪花,字符串) |
| `requirementVersion` | Integer | 本次确认所依据的需求版本 |
| `planVersion` | 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\<String\> | 不达标的服务日(升序);仅 `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 / 某(组+日)格里有一辆车座位取不到 → 对应提醒不产出。
#### 错误响应
既有错误码一条都没变。示例(现存配车对当前需求仍不完整,消息模板 `配车尚未覆盖完整, 不能确认: {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<GroupDispatchSpecWarningRespVO>`,无提醒为空数组 |
| `GroupDispatchConfirmRespVO.specWarnings` | 不存在 | 🆕 `List<GroupDispatchSpecWarningRespVO>`,无提醒为空数组 |
| 两个响应体的其余全部字段 | — | 未变(无删除、无改名、无类型变化) |
| 两个请求体 | — | 未变(一个字段都没动) |
| 两个端点的错误码集合 | — | 未变 |
### 行为级对比
| 行为 | 改前 | 改后 |
|------|------|------|
| 派错车型(声明大巴、实际 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