docs(changelog): #8278 团级用车分组 809116 改为扣司机座,取代 22_8152 旧判据与文案
changelog-filename-gate / validate (push) Failing after 1s
changelog-filename-gate / validate (push) Failing after 1s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cfipfut7pN3pLCRibrYygP
这个提交包含在:
@@ -0,0 +1,499 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "8278"
|
||||
title: "团级用车分组座位充足性校验(809116)改为扣司机座,与子订单级/fleet 单车派车口径统一"
|
||||
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 #8296 已合并 dev-v3(a0973867f)。部署:hl-order-service-v3 dev-v3 @ a0973867f,2026-09-23 21:09:02 部署,deploy-status 读数 BEHIND=0 STATE=ok,8086/8186 两实例 Nacos 健康。测试服网关真实 PUT 实测(groupBatchId=2102749115823919105,20 座大巴 × 2 辆,两次请求均晚于部署时刻):headcount=40 时返回 809116(扣司机座后可载 38 人,不足 40);headcount=38 时返回 200,remainingPassengerSeats=0。定向单测 mvn -o -pl hl-order-service-v3 -am test:17 个外层类 148/0/0(含 9 个 @ArchTest 载体),BUILD SUCCESS。存量影响核查(测试服只读 SELECT,2026-09-23 20:14:20):活跃团级用车需求 131 个,其中带完整规格(seats/count 均非空)的分组 3 组,按新口径重算全部仍满足要求,按新口径会被拒的活跃分组数为 0;生产环境二期尚未开放,无生产存量。gateway_status: verified —— 路径本就在既有 /v3/admin/** order-service-v3 路由下,本次零路由改动,且已通过真实网关实测。"
|
||||
updated_at: "2026-09-23"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# order-v3 团期需求: 团级用车分组座位充足性校验改为扣司机座
|
||||
|
||||
> **存放目录**: 二期(v3) → `changelogs-v2/2026-09/`
|
||||
>
|
||||
> **服务**: hl-order-service-v3(团期需求域)
|
||||
> **PR**: #8296
|
||||
> **Issue**: #8278
|
||||
> **日期**: 2026-09-23
|
||||
> **影响范围**: 管理后台「团期详情 → 查看需求」Tab 的团级正式用车需求编辑弹窗、用车需求汇总草稿预览
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 关键变化(非必须,本版与上版行为不同 / 纠错 / 撤销时必写)
|
||||
|
||||
- 本次变化:`PUT /v3/admin/order/group-batch/{groupBatchId}/vehicle-requirement` 的 809116 座位充足性校验,判据从「座位数 × 车辆数 < 该组最大单日乘车人数」改为「(座位数 − 1) × 车辆数 < 该组最大单日乘车人数」——每辆车扣 1 个司机座,与子订单级用车需求校验、fleet 单车派车(`AssignmentService`)口径统一。
|
||||
- 前端以前以为的(对应 `22_8152_...` changelog 描述的旧行为):座位数 × 车辆数刚好等于该组最大单日人数的分组能保存成功,例如 19 座 × 1 辆、最大日人数 19 能保存;同一响应体里 `remainingPassengerSeats`(余座回显)却是扣了司机座算出来的,可能已经是负数,前端只需原样展示负值提示缺口,不作为提交是否成功的依据。
|
||||
- 实际现在的行为:同样 19 座 × 1 辆、最大日人数 19 这组输入,现在直接被 809116 拒绝。「拦截口径」与「回显口径」现在共用同一份计算(`VehicleSeatCalculator.SeatSummary`),不再互相矛盾——凡是回显会算出 `remainingPassengerSeats` 为负的组合,保存时就先被拒绝,不会写入库。
|
||||
- 809116 报文占位符从 5 个变成 6 个,新模板与两个换算样例见「三、接口详情 → 1 → 业务边界」。
|
||||
- 本条**取代** `changelogs-v2/2026-09/22_8152_团期用车需求补车辆规格与接送机汇总-修改接口-管理后台.md` 中关于 809116 判据与文案的描述,具体被取代的位置见「十、相关文档」。
|
||||
|
||||
---
|
||||
|
||||
## 一、背景(选填)
|
||||
|
||||
#8278 AC-1 定案:旧口径下「拦截不扣司机座、回显扣司机座」是对称子域的口径漂移(CODE_RULES §15.7)——子订单级容量校验与 fleet 单车派车早已扣司机座,只有团级拦截没扣,导致「刚好坐满」的组合能保存成功,但回显立刻显示余座为负。本单让团级拦截口径向已有的、更严格的口径看齐(扣司机座),而不是放松回显。
|
||||
|
||||
最强反例(评审已确认,留档):「刚好坐满、司机另开一辆车」这类特殊排法在新口径下会被拒绝——这是业务上刻意收紧,因为司机座不能卖给乘客,运营应当据此把车辆规格填成真实的乘客运力,而不是靠"整车都算乘客座"的旧口径蒙混过关。
|
||||
|
||||
---
|
||||
|
||||
## 二、变更接口清单
|
||||
|
||||
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||
|---|------|------|------|----------|------|
|
||||
| 1 | 保存团期正式用车需求 | PUT | `/v3/admin/order/group-batch/{groupBatchId}/vehicle-requirement` | 校验判据变更 + 错误报文格式变更 | 809116 扣司机座 |
|
||||
| 2 | 用车需求汇总草稿 | GET | `/v3/admin/order/group-batch/{groupBatchId}/vehicle-requirement/aggregate-draft` | 响应字段语义说明 | `violations[]` 里的 809116 条目共用同一份新判据/新报文 |
|
||||
|
||||
---
|
||||
|
||||
## 三、接口详情
|
||||
|
||||
### 1. 保存团期正式用车需求 `PUT /v3/admin/order/group-batch/{groupBatchId}/vehicle-requirement`
|
||||
|
||||
**VO**: `GroupVehicleRequirementSaveReqVO → GroupVehicleRequirementRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
团期详情「查看需求 → 团级正式用车需求」编辑弹窗点保存。语义仍是整份全量替换:未出现在本次提交里的分组会被移出当前版本。请求体与响应体结构均不变,本条只改 809116 的判据与报文。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| groupBatchId | Path | Long | ✅ | - | 团期聚合主键 |
|
||||
| version | Body | Integer | ❌ | 首次保存传 null,其后回传上次拿到的值 | 乐观锁;不一致抛 809102 |
|
||||
| remark | Body | String | ❌ | ≤500 | 整份需求备注 |
|
||||
| groups | Body | Array | ✅ | 可以是空数组(`@NotNull` 非 `@NotEmpty`) | 全部乘车分组;有在团需车户却零分组抛 809103 |
|
||||
| groups[].groupId | Body | Long | ❌ | 新增分组传 null | 带上它即声明「这就是库里那一组」,此时 groupCode 不得变更(否则 809104) |
|
||||
| groups[].groupCode | Body | String | ✅ | ≤32,同一份内不得重复 | 直接作为车费 alloc_group |
|
||||
| groups[].vehicleType | Body | String | ✅ | ≤64 | 车型文本/字典值 |
|
||||
| groups[].serviceStartDate | Body | LocalDate | ✅ | `yyyy-MM-dd` | 本组服务开始日 |
|
||||
| groups[].serviceEndDate | Body | LocalDate | ✅ | 不早于开始日 | 本组服务结束日 |
|
||||
| groups[].seats | Body | Integer | ❌ | `@Min(1)`;与 count 同填或同空 | 单车座位数(含驾驶位);**本次改动后参与扣司机座的容量判据**,存量分组可不传 |
|
||||
| groups[].count | Body | Integer | ❌ | `@Min(1)`;与 seats 同填或同空 | 车辆数量;只填一半抛 809118 |
|
||||
| groups[].specialTags | Body | Array<String> | ❌ | 取值须在字典 `vehicle_special_demand` 内 | 特殊诉求标签编码数组;含字典外编码整份拒绝(809117) |
|
||||
| groups[].remark | Body | String | ❌ | ≤500 | 该组备注 / 其他诉求 |
|
||||
| groups[].days | Body | Array | ✅ | 非空,且正好铺满本组服务日范围 | 逐日用车人数与成员 |
|
||||
| groups[].days[].tripDate | Body | LocalDate | ✅ | 落在本组服务日范围内、不重复、不缺日 | 越界或重复抛 809105,缺日抛 809106 |
|
||||
| groups[].days[].headcount | Body | Integer | ✅ | `@Min(1)`,且 ≥ 当日成员户数 | 该组该日**乘车人数**;本次改动后这个值与「(seats − 1) × count」比较,决定是否触发 809116 |
|
||||
| groups[].days[].memberOrderIds | Body | Array<Long> | ✅ | 非空,须全属本团在团户 | 该组该日实际乘车的子订单集合 |
|
||||
|
||||
#### 出参 `Result<GroupVehicleRequirementRespVO>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| requirementId | String | 正式需求主键(Long 序列化为字符串) |
|
||||
| groupBatchId | String | 团期聚合主键(Long 序列化为字符串) |
|
||||
| status | String | DRAFT / CONFIRMED / DISPATCHED / DONE / PENDING_RECONFIRM / CANCELLED;PUT 后为 DRAFT |
|
||||
| version | Integer | 版本号,下次提交须回传 |
|
||||
| remark | String | 整份备注 |
|
||||
| confirmedBy / confirmedAt | String / LocalDateTime | 整份确认人/时间;DRAFT 时为 null |
|
||||
| planRefreshState / planRefreshReplayCount / blockedStage / planRefreshStalled / planRefreshStalledReason / planRefreshTimeoutAt / planRefreshReplayExhausted | - | 配车刷新状态相关字段,本次改动未涉及 |
|
||||
| groups | Array | 全部乘车分组;整团免车态为空数组 |
|
||||
| groups[].groupId | String | 分组主键(Long 序列化为字符串) |
|
||||
| groups[].groupCode | String | 分组键 = 车费 alloc_group |
|
||||
| groups[].vehicleType / vehicleTypeName | String | 车型文本/字典值 / 车型中文名 |
|
||||
| groups[].serviceStartDate / serviceEndDate | LocalDate | 本组服务日范围 |
|
||||
| groups[].seats | Integer | 单车座位数(含驾驶位);存量分组为 null |
|
||||
| groups[].count | Integer | 车辆数量;存量分组为 null |
|
||||
| groups[].specialTags | Array<SpecialTagItem> | 特殊诉求标签(code + name) |
|
||||
| groups[].remark | String | 该组备注 |
|
||||
| groups[].totalSeatCount | Integer | 总座位数 = seats × count(含驾驶位);座位或数量缺一即为 null |
|
||||
| groups[].maxHeadcount | Integer | 该组 days 里的最大用车人数 |
|
||||
| **groups[].remainingPassengerSeats** | Integer | 余座 = 扣司机座后的可乘座位 − maxHeadcount;**可为负**;计算公式本次未变,但**新提交**里凡是会算出负值的组合,保存时已被 809116 拦在前面,不会写入库——负值目前只可能出现在改动前已保存、尚未被下一次提交重新校验的存量分组上 |
|
||||
| groups[].days[].tripDate / headcount / memberOrderIds / memberOrderCount | - | 逐日行程与成员,未变 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```json
|
||||
{
|
||||
"version": null,
|
||||
"remark": "#8278-AC6",
|
||||
"groups": [
|
||||
{
|
||||
"groupId": null,
|
||||
"groupCode": "AC6BUS",
|
||||
"vehicleType": "bus",
|
||||
"serviceStartDate": "2027-03-24",
|
||||
"serviceEndDate": "2027-03-25",
|
||||
"seats": 20,
|
||||
"count": 2,
|
||||
"specialTags": [],
|
||||
"remark": "#8278-AC6",
|
||||
"days": [
|
||||
{ "tripDate": "2027-03-24", "headcount": 38, "memberOrderIds": [2102749115559677953] },
|
||||
{ "tripDate": "2027-03-25", "headcount": 38, "memberOrderIds": [2102749115559677953] }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
真实网关实测响应(20 座 × 2 辆,headcount=38,`(20−1)×2=38` 恰好用完):
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"requirementId": "2102750063359135746",
|
||||
"groupBatchId": "2102749115823919105",
|
||||
"status": "DRAFT",
|
||||
"version": 1,
|
||||
"remark": "#8278-AC6",
|
||||
"confirmedBy": null,
|
||||
"confirmedAt": null,
|
||||
"planRefreshState": null,
|
||||
"planRefreshReplayCount": 0,
|
||||
"blockedStage": null,
|
||||
"planRefreshStalled": false,
|
||||
"planRefreshStalledReason": null,
|
||||
"planRefreshTimeoutAt": null,
|
||||
"planRefreshReplayExhausted": false,
|
||||
"groups": [
|
||||
{
|
||||
"groupId": "2102750063363330049",
|
||||
"groupCode": "AC6BUS",
|
||||
"vehicleType": "bus",
|
||||
"vehicleTypeName": "大巴系列",
|
||||
"serviceStartDate": "2027-03-24",
|
||||
"serviceEndDate": "2027-03-25",
|
||||
"seats": 20,
|
||||
"count": 2,
|
||||
"specialTags": [],
|
||||
"remark": "#8278-AC6",
|
||||
"totalSeatCount": 40,
|
||||
"maxHeadcount": 38,
|
||||
"remainingPassengerSeats": 0,
|
||||
"days": [
|
||||
{ "tripDate": "2027-03-24", "headcount": 38, "memberOrderIds": ["2102749115559677953"], "memberOrderCount": 1 },
|
||||
{ "tripDate": "2027-03-25", "headcount": 38, "memberOrderIds": ["2102749115559677953"], "memberOrderCount": 1 }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
- 整团没有在团需车户时,`groups: []` 是合法提交,响应 `groups` 为空数组;不受本次改动影响。
|
||||
- 存量分组(本次改动之前已保存的组)在数据库里原样保留旧值,不会因为本次上线被批量重算或清退;只有当它下次出现在某次 PUT 提交的 `groups` 数组里(哪怕自身字段一个没改,只是跟其他组一起整份提交)时,才会按新口径重新校验——如果它本身坐不下(扣司机座后不够),这次整份保存会被 809116 拒绝、全部字段零写入。
|
||||
- 车型中文名依赖车队侧车型库,降级行为不变:查不到编码或车队不可用时 `vehicleTypeName` 为 null,接口仍 200,不阻断页面。
|
||||
|
||||
```json
|
||||
{ "code": 200, "message": "成功", "data": { "groups": [] }, "success": true }
|
||||
```
|
||||
|
||||
#### 错误响应
|
||||
|
||||
真实网关实测响应(20 座 × 2 辆,headcount=40,`(20−1)×2=38 < 40`):
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 809116,
|
||||
"message": "第 AC6BUS 组座位数不足:20 座 × 2 辆,扣除 2 个司机座后可载客 38 人,少于该组最大乘车人数 40 人",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 报文里「第 {0} 组」的 {0} 仍是 `groupCode`,不是序号(不变);一次只抛一条,按校验遍历顺序收集(不变)。
|
||||
- **新模板与占位符**:`第 {0} 组座位数不足:{1} 座 × {2} 辆,扣除 {3} 个司机座后可载客 {4} 人,少于该组最大乘车人数 {5} 人`。{1}=单车座位数(含司机座),{2}=车辆数,{3}=扣除的司机座数(恒等于 {2}),{4}=`(seats−1)×count` 算出的可载客座位,{5}=该组最大单日乘车人数。**占位符从旧模板的 5 个({0}~{4})变成 6 个({0}~{5}),其中两个位置的语义变了**:旧模板 `第 {0} 组座位数不足:{1} 座 × {2} 辆 = {3} 座,少于该组最大乘车人数 {4} 人` 里,{3} 是总座位数(`seats × count`),{4} 是该组最大乘车人数;新模板里 {3} 改为扣除的司机座数,{4} 改为可载客座位数,最大乘车人数挪到 {5}。{0}/{1}/{2} 语义不变。前端如果曾按位置/正则解析这句报文(而不是原样展示整句 `message`),必须同步改;如果只是原样 toast 整句 `message` 字符串,零改动即可。
|
||||
- 判据公式:`passengerSeatCapacity = max(0, seats × count − count)`,`passengerSeatCapacity < maxHeadcount` 时拒绝。两个换算样例(公式自算,均为「改前放行、改后拒绝」的收窄场景):
|
||||
- **19 座 × 1 辆、该组最大日人数 19**:`passengerSeatCapacity = max(0, 19×1 − 1) = 18`,`18 < 19` → 拒绝,`809116`:「第 {groupCode} 组座位数不足:19 座 × 1 辆,扣除 1 个司机座后可载客 18 人,少于该组最大乘车人数 19 人」。
|
||||
- **7 座 × 2 辆、该组最大日人数 13**:`passengerSeatCapacity = max(0, 7×2 − 2) = 12`,`12 < 13` → 拒绝,`809116`:「第 {groupCode} 组座位数不足:7 座 × 2 辆,扣除 2 个司机座后可载客 12 人,少于该组最大乘车人数 13 人」。
|
||||
- seats 与 count 同生同死规则不变:只填一个抛 809118(报文字面文本未变,仍是「第 {0} 组的座位数与车辆数必须同时填写,或同时留空」);两个都不填 = 存量形态,座位校验整体跳过。
|
||||
- 乐观锁 `version` 不一致抛 809102(不变);特殊诉求标签字典校验(809117)、逐字段约束均不变。
|
||||
|
||||
---
|
||||
|
||||
### 2. 用车需求汇总草稿 `GET /v3/admin/order/group-batch/{groupBatchId}/vehicle-requirement/aggregate-draft`
|
||||
|
||||
**VO**: `GroupVehicleAggregateDraftRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
团期详情「查看需求 → 团级正式用车需求」为空/未确认时,前端用它拉一份系统按子订单在团情况自动配出的推荐草稿(`seats` 取组内最大单车座位、`count = ceil(最大日人数 / (seats − 1))`),供运营参考后再决定是否提交。本次改动不影响这个推荐公式本身(`GroupVehicleDraftAggregator.java:345`,已用 `git show a0973867f -- <该文件>` 核对,本次合并只改了该文件的 javadoc 注释,公式代码未动),只影响 `violations[]` 数组里 809116 条目的判据与报文——它与保存端点共用同一份校验函数(`collectFleetSpecViolations`)。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| groupBatchId | Path | Long | ✅ | - | 团期聚合主键 |
|
||||
|
||||
#### 出参 `Result<GroupVehicleAggregateDraftRespVO>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| groupBatchId | String | 团期聚合主键(Long 序列化为字符串) |
|
||||
| currentStatus | String | 当前正式需求状态;未建过正式需求为 null |
|
||||
| draft | Object | 结构同 `GroupVehicleRequirementSaveReqVO`;`version` 为当前生效版本号或 null;`draft.groups[].groupId` 恒为 null(草稿未落库) |
|
||||
| droppedFleetItems | Array | 因非主车型/车型字典不可用被剔除的子订单车队行;字段 orderId/orderNo/vehicleType/seats/count/keptVehicleType/reason |
|
||||
| staleHeadcountOrders | Array | 冻结人数与当前实际人数不一致的子订单;字段 orderId/orderNo/frozenHeadcount/liveHeadcount |
|
||||
| paddedOrderDays | Array | 被自动补天的子订单;字段 orderId/orderNo/dates |
|
||||
| violations | Array | 草稿自身触发的校验违规;恒非 null,无违规为空数组 |
|
||||
| violations[].code | Integer | 错误码,本次改动相关的是 809116 |
|
||||
| violations[].reason | String | 原因码,见「六.5」 |
|
||||
| violations[].detail | String | 渲染文案,与「三 → 1 → 错误响应」里同码条目的 `message` 逐字相同 |
|
||||
| violations[].groupCode / tripDate / orderId | String / LocalDate / String | 定位到具体分组/日期/子订单,均可为 null |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
GET /v3/admin/order/group-batch/2102749115823919105/vehicle-requirement/aggregate-draft
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"groupBatchId": "2102749115823919105",
|
||||
"currentStatus": null,
|
||||
"draft": {
|
||||
"version": null,
|
||||
"remark": null,
|
||||
"groups": [
|
||||
{
|
||||
"groupId": null,
|
||||
"groupCode": "AC6BUS",
|
||||
"vehicleType": "bus",
|
||||
"serviceStartDate": "2027-03-24",
|
||||
"serviceEndDate": "2027-03-25",
|
||||
"seats": 20,
|
||||
"count": 2,
|
||||
"specialTags": [],
|
||||
"remark": null,
|
||||
"days": [
|
||||
{ "tripDate": "2027-03-24", "headcount": 38, "memberOrderIds": ["2102749115559677953"] },
|
||||
{ "tripDate": "2027-03-25", "headcount": 38, "memberOrderIds": ["2102749115559677953"] }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"droppedFleetItems": [],
|
||||
"staleHeadcountOrders": [],
|
||||
"paddedOrderDays": [],
|
||||
"violations": []
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
- 整团没有可汇总内容时,`draft.groups` 为空数组,`droppedFleetItems`/`staleHeadcountOrders`/`paddedOrderDays`/`violations` 均为空数组(javadoc 原文:恒非 null)。
|
||||
- 有需车户缺少可汇总的行程用车需求时报 809121(本次改动未涉及此判据)。
|
||||
- 车型字典不可用时报 809120(本次改动未涉及此判据)。
|
||||
|
||||
```json
|
||||
{ "code": 200, "message": "成功", "data": { "draft": { "groups": [] }, "droppedFleetItems": [], "staleHeadcountOrders": [], "paddedOrderDays": [], "violations": [] }, "success": true }
|
||||
```
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 809121,
|
||||
"message": "团期 XXX 有 2 户缺少可汇总的行程用车需求,暂不能自动汇总:ORD001、ORD002",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 推荐公式 `count = ceil(最大日人数 / (seats − 1))` 保证草稿自己生成的分组,扣司机座后的可载客座位数恒 ≥ 该组最大日人数,所以由这个端点直接产出的草稿**正常情况下不会**在自己的 `violations[]` 里出现 809116。
|
||||
- `violations[]` 里若确实出现 809116 条目(例如运营手工改过草稿后再调这个端点做二次校验),其 `detail` 文案与占位符结构(6 段)与「三 → 1 → 错误响应」逐字相同,前端复用同一份渲染/解析逻辑即可,不需要为本端点单独适配。
|
||||
- 本端点只读,不落库,多次调用互不影响、无并发/幂等问题。
|
||||
|
||||
---
|
||||
|
||||
## 四、契约约束与正确调用方式(接口类必写)
|
||||
|
||||
> 本节只写**后端接受/拒绝 payload 的规则**,不写 UI 渲染建议。
|
||||
|
||||
### ✅ 正确 / ❌ 错误 payload 对照(`PUT .../vehicle-requirement` 分组元素,`seats`/`count`/该组最大单日人数三者的关系)
|
||||
|
||||
| 场景 | seats × count | (seats−1) × count | 最大单日人数 | 判定 |
|
||||
|------|----------------|---------------------|----------------|------|
|
||||
| ✅ 明显够坐 | 35 | 34 | 20 | 放行(改前改后均放行) |
|
||||
| ✅ 恰好用完(边界) | 40 | 38 | 38 | 放行,`remainingPassengerSeats=0`(20 座×2 辆/38 人,已用真实网关实测) |
|
||||
| ❌ 不扣司机座够坐、扣了不够(新增拒绝区间) | 19 | 18 | 19 | 809116 拒绝——**改前放行、改后拒绝** |
|
||||
| ❌ 不扣司机座够坐、扣了不够(新增拒绝区间) | 14 | 12 | 13 | 809116 拒绝——**改前放行、改后拒绝**(7 座×2 辆/13 人) |
|
||||
| ❌ 两版本均拒绝 | 10 | 9 | 15 | 809116 拒绝(改前改后均拒绝,不扣司机座也不够坐) |
|
||||
|
||||
### 切换状态时的必要动作
|
||||
|
||||
`seats`/`count` 仍是「同填同空」的互斥对(不变):只提交其中一个会被 809118 拒绝;两个都不提交等价于存量形态,座位校验整体跳过。提交时不要依赖"隐藏输入框"的 UI 行为,后端只看 payload 里这两个字段是否同时为非 null。
|
||||
|
||||
---
|
||||
|
||||
## 五、数据库行为(涉及写操作时必写)
|
||||
|
||||
本次改动**不涉及表结构变化**,`groups.seats`/`groups.count`/`groups_day.headcount` 三列的落库口径与列值语义均未变——放行的提交,落库值仍是前端提交的原始 `seats`/`count`,后端不做任何扣减存储;变的只是"放不放行"这一步的判定发生在写库之前。
|
||||
|
||||
| 前端提交 | 改前落库结果 | 改后落库结果 |
|
||||
|----------|--------------|--------------|
|
||||
| 19 座 × 1 辆、最大日人数 19(该组) | 校验通过,`group.seats=19, group.count=1` 落库 | 809116 拒绝,**整份提交零写入**(不含该分组之外的其他改动) |
|
||||
| 20 座 × 2 辆、最大日人数 38(该组) | 校验通过,`group.seats=20, group.count=2` 落库 | 校验通过(边界恰好用完),落库同值 |
|
||||
|
||||
**零写入范围**:`PUT` 语义是整份全量替换,任一分组触发 809116 会导致这次提交整体失败,不只是该分组,其余分组在本次提交里的改动也不会落库(与改前逻辑一致,不是本次新增行为)。
|
||||
|
||||
---
|
||||
|
||||
## 六、边界行为
|
||||
|
||||
- 未登录 → 401(网关拦截,不变)。
|
||||
- `groupBatchId` 不存在 → 团期聚合层报错(不变,未在本次改动范围)。
|
||||
- 车队/字典服务降级 → 809120(`vehicleType` 字典不可用)判据与报文不变,仍与 809116 相互独立、互不影响。
|
||||
- 老数据兼容 → 存量分组的 `seats`/`count` 为 null 时 809116 判据整体跳过(不变);已有 `seats`/`count` 但未随本次改动重新保存的组,回显仍按旧值展示,可能带负的 `remainingPassengerSeats`(见「三 → 1 → 空数据 / 降级响应」)。
|
||||
- 生产环境 → 二期功能尚未在生产开放,本次改动的行为差异目前不会被任何生产流量触发。
|
||||
|
||||
---
|
||||
|
||||
## 六.5、枚举 / 数据字典(接口出现枚举时必写)
|
||||
|
||||
### status(正式用车需求状态,`GroupVehicleRequirementRespVO.status`)
|
||||
|
||||
**所属字段**: `GroupVehicleRequirementRespVO.status` | **类型**: `String`
|
||||
|
||||
| 值 | 中文 | 说明 |
|
||||
|----|------|------|
|
||||
| `DRAFT` | 草稿 | PUT 保存成功后的默认状态;本次改动后,坐不下的组合会在到达这一步之前就被 809116 拒绝 |
|
||||
| `CONFIRMED` | 已确认 | 整份确认后 |
|
||||
| `DISPATCHED` | 已派车 | fleet 已排车 |
|
||||
| `DONE` | 已完成 | 车务完成 |
|
||||
| `PENDING_RECONFIRM` | 待重新确认 | 确认后又被撤回/变更,需重新确认 |
|
||||
| `CANCELLED` | 已取消 | 整团取消 |
|
||||
|
||||
本次改动未涉及状态机迁移逻辑本身,取值与含义均不变。
|
||||
|
||||
### violations[].reason(`GroupVehicleAggregateDraftRespVO.Violation.reason`,本次改动相关的两个取值)
|
||||
|
||||
**所属字段**: `GroupVehicleAggregateDraftRespVO.Violation.reason` | **类型**: `String`
|
||||
|
||||
| 值 | 中文 | 说明 |
|
||||
|----|------|------|
|
||||
| `GROUP_SEATS_INSUFFICIENT` | 座位数不足 | 对应 809116;本次改动后判据改为扣司机座 |
|
||||
| `GROUP_SPEC_INCOMPLETE` | 车辆规格不完整 | 对应 809118;判据与报文字面文本均未变 |
|
||||
|
||||
---
|
||||
|
||||
## 六.6、修改前后对比(修改/删除类接口必写,新增跳过)
|
||||
|
||||
### 字段级对比
|
||||
|
||||
| 字段 | 改前 | 改后 |
|
||||
|------|------|------|
|
||||
| 809116 报文占位符数量 | 5 个({0}~{4}) | 6 个({0}~{5}):{3} 从「总座位数(`seats × count`)」改为「扣除的司机座数(=车辆数)」;{4} 从「最大乘车人数」改为「可载客座位数」;「最大乘车人数」后移到新增的 {5} |
|
||||
| 809116 判据表达式 | `seats × count < 该组最大单日乘车人数` | `max(0, seats × count − count) < 该组最大单日乘车人数`(即扣司机座后判断) |
|
||||
| `remainingPassengerSeats`(响应字段) | 计算公式含扣司机座,可能为负;负值组合仍能保存成功(拦截口径更宽松) | 计算公式不变;但**新提交**里会算出负值的组合,已先被 809116 拒绝,不会写入库 |
|
||||
|
||||
### 行为级对比
|
||||
|
||||
| 行为 | 改前 | 改后 |
|
||||
|------|------|------|
|
||||
| 提交 `seats × count == 该组最大单日人数`(如 19 座×1 辆/19 人) | 放行 | 809116 拒绝 |
|
||||
| 提交 `(seats−1) × count == 该组最大单日人数`(如 20 座×2 辆/38 人) | 放行 | 放行(余座为 0,恰好用完) |
|
||||
| 提交 `(seats−1)×count < 该组最大单日人数 ≤ seats×count`(如 19×1/19、7×2/13) | 放行(不扣司机座时够坐) | **809116 拒绝**(扣司机座后不够坐,本次改动新增的拒绝区间) |
|
||||
| 拦截口径 vs 回显口径是否一致 | 刻意相差 1 个司机座/车,可能「保存成功但 remainingPassengerSeats 为负」 | 两口径共用同一份 `VehicleSeatCalculator` 计算,新提交不会再出现这种矛盾 |
|
||||
| 存量分组(改动前已保存、字段未变) | — | 不主动重算,原样保留在库里;只有它下次出现在某次 PUT 的 `groups` 数组里才会被新口径重新校验 |
|
||||
| fleet 单车派车(`AssignmentService`) | 已扣司机座 | 不变,本次改动是团级向它对齐 |
|
||||
| fleet 团级就绪检查黄牌(`GroupDispatchReadinessService#seatShortageWarnings`) | 不扣司机座 | 不变,仍不扣司机座,跟踪于 #8294,不在本次改动范围内 |
|
||||
|
||||
---
|
||||
|
||||
## 六.7、影响评估(修改/删除类必写)
|
||||
|
||||
- **是否破坏向后兼容**: 部分是——请求体/响应体字段结构未变,但同一份 payload 在「恰好等于旧口径边界、不足新口径边界」的场景下,会从改前的 200 变成改后的 809116(809116 是 HTTP 200 下的业务失败,不是传输层错误)。
|
||||
- **前端是否必须同步上线**: 视前端现有实现而定。若前端只是把 809116 的 `message` 整句原样 toast 展示,不改也能正常显示新文案,零改动。若前端曾按占位符位置/正则解析这句报文(例如截取「× {2} 辆」后面的数字单独展示),必须同步改成新的 6 段结构,否则会把 {3}(司机座数)或 {4}(可载客座位)错位显示。
|
||||
- **前端 workaround 清理点**: 若前端为「刚好坐满」这类输入写过专门的“应该能保存”预期用例,需要把预期改成会撞 809116;`remainingPassengerSeats` 为负仍是合法信号(不应做 `Math.max(0, x)` clamp,也不应据此拦截提交,这条 22_8152 的既有结论继续有效),但不能再假设"负值一定对应一个刚保存成功的新组合"——新提交里的负值组合已经在保存时被拒绝,负值目前只可能来自尚未被下一次保存重新校验的存量分组。
|
||||
|
||||
---
|
||||
|
||||
## 七、不影响范围(显式声明, 帮前端/QA 缩小排查面)
|
||||
|
||||
- **仅影响**: `PUT .../vehicle-requirement` 的 809116 触发条件与报文;`GET .../vehicle-requirement/aggregate-draft` 响应体 `violations[]` 数组里 809116 条目的判据与报文(若出现)。
|
||||
- **零影响**:
|
||||
- 两个接口的请求体/响应体字段结构(无新增、无删除字段)。
|
||||
- 错误码码值本身(仍是 809116/809118,未变);809117(特殊诉求标签字典)、809102(乐观锁)、809103~809115、809119~809122 等其余错误码的判据与报文。
|
||||
- `GET /v3/admin/order/group-batch/{groupBatchId}/vehicle-requirement`(读取回显):不触发任何校验,原样返回库里已落值。
|
||||
- `GET .../requirement/vehicle-households`、`GET .../requirement-summary`、`GET .../requirement/confirm-check`:均未改动。
|
||||
- `aggregate-draft` 自身的推荐车辆数公式 `count = ceil(最大日人数 / (seats − 1))`(`GroupVehicleDraftAggregator.java:345`):本次合并只改了该文件的 javadoc,公式代码本身未动。
|
||||
- fleet 单车派车口径(`AssignmentService`):本就扣司机座,不受影响。
|
||||
- fleet 团级就绪检查黄牌口径(`GroupDispatchReadinessService#seatShortageWarnings`):仍不扣司机座,未随本次改动对齐,另开 #8294 跟进。
|
||||
- 生产环境:二期功能尚未在生产开放,本条改动不影响任何生产流量。
|
||||
- 历史数据:存量分组的 `seats`/`count`/`headcount` 不做批量重算或迁移,见「六.6」。
|
||||
|
||||
---
|
||||
|
||||
## 八、测试环境已验证
|
||||
|
||||
真实网关实测(`https://api.test.1814.love`,hl-order-service-v3 dev-v3 @ `a0973867f`,2026-09-23 21:09:02 部署,`deploy-status` 读数 `BEHIND=0 STATE=ok`,两次 PUT 请求均晚于部署时刻):
|
||||
|
||||
```
|
||||
PUT /v3/admin/order/group-batch/2102749115823919105/vehicle-requirement
|
||||
20 座 × 2 辆,headcount=40(两天) → code=809116
|
||||
message=第 AC6BUS 组座位数不足:20 座 × 2 辆,扣除 2 个司机座后可载客 38 人,少于该组最大乘车人数 40 人 ✓
|
||||
GET 同一团期同一端点回读 → code=200, data=null(校验在写库前抛出,零写入)✓
|
||||
|
||||
PUT /v3/admin/order/group-batch/2102749115823919105/vehicle-requirement
|
||||
同一分组,headcount 改为 38(两天) → code=200, message=成功
|
||||
data.groups[0]: seats=20, count=2, totalSeatCount=40, maxHeadcount=38, remainingPassengerSeats=0 ✓
|
||||
GET 同一团期同一端点回读 → code=200, requirementId=2102750063359135746, status=DRAFT, version=1 ✓
|
||||
```
|
||||
|
||||
验证团期:`groupBatchId=2102749115823919105`(本轮自建夹具,未影响任何既有排期/团期)。两次 PUT 之间唯一变量是 `headcount`(40→38),`groupCode`/`vehicleType`/`seats`/`count`/日期范围完全相同,809116 与 200 的分野只能来自本次判据变更。
|
||||
|
||||
定向单测(`mvn -o -pl hl-order-service-v3 -am test -Dtest='GroupVehicleRequirementSaveTest*,GroupVehicleDraftAggregatorTest*,...'`,起跑 2026-09-23 20:31:52):17 个外层类 / 148 用例,Failures/Errors/Skipped 全 0,`BUILD SUCCESS`;含 `VehicleSeatCalculatorTest`(6)、`GroupVehicleRequirementSaveTest`(25,含新旧边界两条判据)、`GroupVehicleDraftAggregatorTest`(19)与 9 个 `@ArchTest` 载体。仅改注释的返工提交后,定向复跑受影响 3 类:50/0/0。
|
||||
|
||||
存量影响核查(测试服只读 SELECT,2026-09-23 20:14:20):活跃团级用车需求 131 个,其中带完整规格(`seats`/`count` 均非空)的分组 3 组;按新口径重算,这 3 组全部仍满足要求(新口径可载客座位 4~15 人不等,均 ≥ 该组最大日人数 2~5 人),按新口径会被拒的活跃分组数为 **0**。生产环境二期尚未开放,无生产存量。
|
||||
|
||||
限定:本次真实网关实测只覆盖了「一辆 20 座大巴 × 2 辆」这一种具体座位组合的团级 PUT 入口;其它座位数组合、子订单级校验、`aggregate-draft` 端点自身,由上述单测覆盖(`aggregate-draft` 与保存端点共享同一份校验代码,但本次未针对该端点单独发起真实 HTTP 调用)。存量核查的分母只有 3 组带规格(团级用车规格字段是 #8152 刚上线的新字段,前端入口尚未普及),0 组的读数只说明此刻不会新拒任何已存活跃版本,对未来接入更多分组后是否仍为 0 没有分辨力。
|
||||
|
||||
---
|
||||
|
||||
## 十、相关文档
|
||||
|
||||
- 关联 Issue: [wx/HL#8278](https://git.1814.love/wx/HL/issues/8278)
|
||||
- 关联 PR: [wx/HL#8296](https://git.1814.love/wx/HL/pulls/8296)
|
||||
- 已知缺口(不在本次改动范围内): [wx/HL#8294](https://git.1814.love/wx/HL/issues/8294) —— fleet 团级就绪检查黄牌口径尚未对齐扣司机座
|
||||
- **订正声明(取代旧描述,不改动原文件)**:本条取代 `changelogs-v2/2026-09/22_8152_团期用车需求补车辆规格与接送机汇总-修改接口-管理后台.md` 中以下位置关于 809116 判据与文案的描述:
|
||||
- 第 36-38 行「⚠️ 关键变化」块(「拦截口径不扣司机位」「回显口径扣司机位」「20 座 × 1 辆 / 20 人能保存成功,而返回的 `remainingPassengerSeats` 是 -1」):描述的是本次改动前的行为,改动后 20×1/20 这组输入已改为 809116 拒绝,两口径不再矛盾。
|
||||
- 第 40、42、44 行「展示侧」「提交侧」「响应体里没有任何字段表示…」三段:其中「是否允许保存由后端 809116 判定(口径:`seats × count` 与该组最大乘车人数比较,不扣司机位)」这句已过时,判据已改为扣司机位。
|
||||
- 第 240 行错误响应示例 `"第 BUS 组座位数不足:19 座 × 1 辆 = 19 座,少于该组最大乘车人数 20 人"`:这是旧模板(5 个占位符)的渲染结果,新模板见本条「三 → 1 → 错误响应」与「三 → 1 → 业务边界」。
|
||||
- 第 268 行业务边界「座位充足性判据 = `seats × count < 该组最大单日乘车人数`,不扣司机位」:判据已变,见本条「六.6」。
|
||||
- 第 354 行「20 座 × 1 辆载 20 人保存成功,余座 `-1`」:该结论已不成立,这组输入现在被拒绝。
|
||||
- 第 944 行行为级对比表格「提交坐不下的规格 | 接受(无此字段) | 809116 拒绝(判据不扣司机位)」一行,末列「判据不扣司机位」已过时。
|
||||
- 第 958-959 行「若此前把 `remainingPassengerSeats` 之类余座数做过 `Math.max(0, x)` 处理,必须撤掉」「不要新增『余座为负则禁止保存』的前端校验」:这两条建议本身**依然有效**,但其背景「后端放行的组合」已收窄——新提交里会被后端放行的组合,扣司机座后已经够坐,不会再出现负值;负值目前只可能来自尚未被下一次保存重新校验的存量分组。
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
### 链接
|
||||
|
||||
- **Issue**: [#8278](https://git.1814.love/wx/HL/issues/8278)
|
||||
- **PR**: [#8296](https://git.1814.love/wx/HL/pulls/8296)
|
||||
- **Merge commit**: [a0973867f](https://git.1814.love/wx/HL/commit/a0973867f)
|
||||
|
||||
### 联系人
|
||||
|
||||
- **后端负责人**: @wx
|
||||
在新工单中引用
屏蔽一个用户