docs(changelog): #8311 团期分组座位数改档位下拉 + 809124 档位校验(前端座位下拉/组号只读/汇总座位兜底诊断)
changelog-filename-gate / validate (push) Failing after 1s

这个提交包含在:
API Changelog Bot
2026-09-24 12:11:06 +08:00
父节点 47d93b0cf3
当前提交 b865fab5b8
@@ -0,0 +1,518 @@
---
schema: "hl-changelog/v2"
ticket: "8311"
title: "团期正式用车需求分组座位数改由车队字典档位下拉选择(可选可改),新增 809124 档位校验与汇总座位兜底诊断"
consumer: "admin"
author: "wx(GIT)"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "pending"
frontend_owner: "mmg"
frontend_ref: ""
target_release: "v2.1"
verified_at: ""
status_note: "PR #8324 squash 合并 dev-v3(1638faf08)。部署:hl-order-service-v3 dev-v3 @ 1638faf08,2026-09-24 12:06:26 滚动部署两实例,deploy-status 读数 BEHIND=0/N STATE=ok;请求路径上 hl-gateway 与 hl-fleet-service 同为 ok、BEHIND 右侧 N。测试服网关真实 PUT 实测(groupBatchId=2102692937584513025,BUS 16座/SUV 5座 DRAFT):BUS 13 座 → code=809124「第 BUS 组的座位数 13 不在车型 bus 的可选档位 [12, 15, 16, 19] 内,请从下拉项中选择」;SUV 6 座 → code=809124「…不在车型 suv 的可选档位 [5, 7] 内…」;两次拒绝后回读 version 与 seats 均未变(零写入);原值(BUS 16/SUV 5)→ code=200 且落库一致。aggregate-draft 实测回 seatOptionAdjusted=[](当前测试团期没有非法档位子订单)与 draft BUS=16/SUV=5。定向测试 14 个类全绿(SaveTest 31 / DraftAggregatorTest 24 / AggregateDraftTest 12 / ConfirmCheckTest 19 等)。809123 已被同日并入的 #8219 占用,本单让号到 809124。"
updated_at: "2026-09-24"
base: "dev-v3"
---
# order-v3 团期需求: 分组座位数改为字典档位可选 + 809124 档位校验
> **存放目录**: 二期(v3) → `changelogs-v2/2026-09/`
>
> **服务**: hl-order-service-v3(团期需求域,groupbatch 包)
> **PR**: 待建
> **Issue**: #8311
> **日期**: 2026-09-24
> **影响范围**: 管理后台「团期详情 → 查看需求」Tab 的正式用车需求编辑弹窗(含「自动汇总」)
---
## ⚠️ 关键变化(非必须,本版与上版行为不同 / 纠错 / 撤销时必写)
- **新增错误码 809124**:`PUT .../vehicle-requirement` 现在会校验「该组的 `seats` 必须落在该组 `vehicleType` 在车队车型字典里的可选座位档位(seatOptions)内」,不在档位内整份提交被拒。
- **本条与子订单级早已存在的同款校验对齐**:子订单用车需求的 `fleet[].seats` 一直有这条(错误码 **582024**「座位数不在该车型大类可选座位数中,请检查车型库」);团级此前只校验「半填 809118」与「容量不足 809116」,收任意 ≥1 的座位数。本单把团级补齐到同一口径。
- **前端交互随之改变**:座位数不再是自由输入框,改为下拉,选项取自 `GET /admin/fleet/vehicle-types/list` 里该 `typeKey` 的 `seatOptions`。**这条校验是硬约束,不是提示**——手输/绕过下拉提交字典外档位会被 809124 拒绝。
- **`aggregate-draft` 的 `draft.groups[].seats` 语义变更**:#8220 时它取「组内子订单报的最大单车座位」;现在取**该车型大类档位内的值**(子订单报的值在档位内就取组内最大,不在档位内或没报则兜底到该大类**最大档**),被兜底调整的户逐条列在新字段 `seatOptionAdjusted`。**前端必须展示该字段**,否则「子订单报 13 座、草稿变成 19 座」这件事在页面上不可见。
- **组号(`groupCode`)仍由后端生成**(车型大写 + 同车型拆组序号:`BUS`、`BUS2`…),表单不应提供输入框;同车型被拆成多组时,`seatOptionAdjusted[]` 里带 `groupCode` 用于定位是哪一组。
---
## 一、背景(选填)
wx 2026-09-24 定案:正式用车需求编辑弹窗里「单车座位数」应由手输改为**按车队字典档位下拉选择(可选、可改)**。落地时发现团级缺少子订单级早就有的「座位数必须在 seatOptions 内」校验:不补这条,下拉只是页面上的自觉,直接调接口仍能把车队没有的档位(如大巴 13 座)写进正式需求,车务排车时对不上型号。本单同时把「自动汇总」的座位默认值改为落在档位内,并对被调整的户出诊断,避免静默改数。
---
## 二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 保存团期正式用车需求 | PUT | `/v3/admin/order/group-batch/{groupBatchId}/vehicle-requirement` | 新增校验 + 新错误码 | 新增 809124(座位数不在该组车型大类可选档位内);档位不可得复用 809120 |
| 2 | 自动汇总正式用车需求草稿 | GET | `/v3/admin/order/group-batch/{groupBatchId}/vehicle-requirement/aggregate-draft` | 响应新增字段 + 既有字段语义变更 | 新增 `seatOptionAdjusted[]`;`draft.groups[].seats` 改为档位内的值 |
| 3 | 车型大类全量列表(复用,不改) | — | — | 复用不改 | 座位下拉数据源(`seatOptions`)仍走 `GET /admin/fleet/vehicle-types/list`,本单未改它;契约见第八节说明 |
---
## 三、接口详情
### 1. 保存团期正式用车需求 `PUT /v3/admin/order/group-batch/{groupBatchId}/vehicle-requirement`
**VO**: `GroupVehicleRequirementSaveReqVO → GroupVehicleRequirementRespVO`
#### 使用场景
团期详情「查看需求 → 团级正式用车需求」编辑弹窗点保存。语义仍是**整份全量替换**:未出现在本次提交里的分组会被移出当前版本。请求体与响应体结构均不变,本次只新增一条校验与一个错误码。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| groupBatchId | Path | Long | ✅ | - | 团期聚合主键 |
| version | Body | Integer | ❌ | 首次保存传 null,其后回传上次拿到的值 | 乐观锁;不一致抛 809102 |
| remark | Body | String | ❌ | ≤500 | 整份需求备注 |
| groups | Body | Array | ✅ | 可以是空数组 | 全部乘车分组;有在团需车户却零分组抛 809103 |
| groups[].groupId | Body | Long | ❌ | 新增分组传 null | 带上它即声明「这就是库里那一组」,此时 groupCode 不得变更(否则 809104) |
| groups[].groupCode | Body | String | ✅ | ≤32,同一份内不得重复 | 直接作为车费 alloc_group;**表单不应给输入框,用后端生成的只读值** |
| groups[].vehicleType | Body | String | ✅ | ≤64,取值见 `/admin/fleet/vehicle-types/list` | 车型大类;不在字典内抛 809119 |
| groups[].serviceStartDate | Body | LocalDate | ✅ | `yyyy-MM-dd` | 本组服务开始日 |
| groups[].serviceEndDate | Body | LocalDate | ✅ | 不早于开始日 | 本组服务结束日 |
| groups[].seats | Body | Integer | ❌ | `@Min(1)`;与 count 同填或同空;**本次起必须落在该车型大类的 seatOptions 内** | 单车座位数(含驾驶位);下拉取值见 `GET /admin/fleet/vehicle-types/list` 的 `seatOptions`;存量分组(seats 与 count 皆 null)整条跳过本校验 |
| 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)`,且 ≥ 当日成员户数 | 该组该日乘车人数 |
| groups[].days[].memberOrderIds | Body | Array<Long> | ✅ | 非空,须全属本团在团户 | 该组该日实际乘车的子订单集合 |
#### 出参 `Result<GroupVehicleRequirementRespVO>`
| 字段 | 类型 | 说明 |
|------|------|------|
| requirementId | String | 正式需求主键(Long 序列化为字符串) |
| groupBatchId | String | 团期聚合主键 |
| status | String | DRAFT / CONFIRMED / DISPATCHED / DONE / PENDING_RECONFIRM / CANCELLED;PUT 后为 DRAFT |
| version | Integer | 版本号,下次提交须回传 |
| remark | String | 整份备注 |
| confirmedBy / confirmedAt | String / LocalDateTime | 整份确认人/时间;DRAFT 时为 null |
| groups | Array | 全部乘车分组;整团免车态为空数组 |
| groups[].groupCode | String | 分组键 = 车费 alloc_group(后端生成,前端只读回显) |
| groups[].vehicleType / vehicleTypeName | String | 车型大类 key / 中文名 |
| groups[].seats | Integer | 单车座位数(含驾驶位);存量分组为 null |
| groups[].count | Integer | 车辆数量;存量分组为 null |
| groups[].totalSeatCount | Integer | 总座位数 = seats × count;缺一即 null |
| groups[].maxHeadcount | Integer | 该组 days 里的最大用车人数 |
| groups[].remainingPassengerSeats | Integer | 余座 = 扣司机座后的可乘座位 − maxHeadcount;**可为负**,是缺口展示值,**不得据此拦截提交**(见 22_8152) |
| groups[].days[] | Array | 逐日行程与成员(tripDate / headcount / memberOrderIds / memberOrderCount) |
#### 请求示例
```json
{
"version": 3,
"remark": "9/24 换 19 座",
"groups": [
{
"groupId": null,
"groupCode": "BUS",
"vehicleType": "bus",
"serviceStartDate": "2026-10-08",
"serviceEndDate": "2026-10-10",
"seats": 19,
"count": 1,
"specialTags": [],
"remark": "含高速费",
"days": [
{ "tripDate": "2026-10-08", "headcount": 9, "memberOrderIds": ["2102692937378992129"] },
{ "tripDate": "2026-10-09", "headcount": 9, "memberOrderIds": ["2102692937378992129"] },
{ "tripDate": "2026-10-10", "headcount": 9, "memberOrderIds": ["2102692937378992129"] }
]
}
]
}
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"data": {
"requirementId": "2102750063359135746",
"groupBatchId": "2102749115823919105",
"status": "DRAFT",
"version": 4,
"remark": "9/24 换 19 座",
"confirmedBy": null,
"confirmedAt": null,
"groups": [
{
"groupId": "2102750063363330049",
"groupCode": "BUS",
"vehicleType": "bus",
"vehicleTypeName": "大巴系列",
"serviceStartDate": "2026-10-08",
"serviceEndDate": "2026-10-10",
"seats": 19,
"count": 1,
"specialTags": [],
"remark": "含高速费",
"totalSeatCount": 19,
"maxHeadcount": 9,
"remainingPassengerSeats": 9,
"days": [
{ "tripDate": "2026-10-08", "headcount": 9, "memberOrderIds": ["2102692937378992129"], "memberOrderCount": 1 }
]
}
]
},
"success": true
}
```
#### 空数据 / 降级响应
- 整团没有在团需车户时 `groups: []` 是合法提交,响应 `groups` 为空数组。
- 车队车型字典(含座位档位)取不到时,**保存口 fail-closed**:抛 809120「车队车型字典暂不可用,无法校验车型,请稍后重试」,本次提交零写入。
- `vehicleTypeName` 仍可降级为 null(只影响展示),不影响本单校验。
```json
{ "code": 200, "message": "成功", "data": { "groups": [] }, "success": true }
```
#### 错误响应
```json
{
"code": 809124,
"message": "第 BUS 组的座位数 13 不在车型 bus 的可选档位 [12, 15, 16, 19] 内,请从下拉项中选择",
"success": false,
"data": null
}
```
#### 业务边界
- **报文字段**:{0}=组号(`groupCode`,不是序号)、{1}=本次提交的座位数、{2}=车型大类 key、{3}=该大类的可选档位(升序、方括号包裹)。一次只抛一条,按校验遍历顺序收集。
- **校验顺序**:座位档位(809124 / 809120)→ 半填(809118)→ 容量不足(809116)。**档位不可得(报 809120)时不会吞掉同组的半填与容量判据**,仍会继续判下去,避免「先提示稍后重试、补完车辆数才被告知半填」的来回。
- **存量分组(seats 与 count 皆 null)整条跳过**:不查字典、不报 809124/809120/809116。这是为了让「不带 fleet 结构再 PUT 一次」的历史团期不会被一刀拦死。
- **只填了车辆数(seats 为 null)时不判档位**:交给 809118 报半填,不会编出一条「座位数不在档位内」。
- **该车型大类在车队没有任何型号**(`seatOptions` 为空)按「档位不可得」处理 → 809120,需先去车务补型号。
- **注意与子订单级错误码不同**:子订单级同一件事是 582024,团级是 809124;两条链路的报文与码值刻意分开(团级报文必须带组号,一份十几组的整份提交被拒后要能定位是哪一组)。
---
### 2. 自动汇总正式用车需求草稿 `GET /v3/admin/order/group-batch/{groupBatchId}/vehicle-requirement/aggregate-draft`
**VO**: `GroupVehicleAggregateDraftRespVO`
#### 使用场景
编辑弹窗点「自动汇总」时调用,拿到 `draft` 灌进表单、原样或编辑后 PUT 保存;四个(本次起为五个)诊断字段必须一并展示。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| groupBatchId | Path | Long | ✅ | - | 团期聚合主键(不是产品班期 ID) |
#### 出参 `Result<GroupVehicleAggregateDraftRespVO>`
| 字段 | 类型 | 说明 |
|------|------|------|
| groupBatchId | String | 团期聚合主键 |
| currentStatus | String | 当前正式需求状态;未形成时为 null。只有 null 或 DRAFT 能保存 |
| draft | Object | 与保存请求体同形,可原样 PUT;`groupId` 恒为 null |
| draft.groups[].groupCode | String | 后端生成的组号(车型大写 + 拆组序号:BUS、BUS2…),**前端只读展示,不要给输入框** |
| draft.groups[].vehicleType | String | 归一后的车型大类 key(suv2 → suv) |
| draft.groups[].seats | Integer | **本次起为档位内的值**:子订单报的座位在档位内取组内最大,不在档位内或没报则兜底该大类最大档;档位不可得时退回旧规则(取子订单报的最大值) |
| draft.groups[].count | Integer | `ceil(本组最忙那天的人数 / (seats − 1))`,已扣司机座;seats < 2 时与 seats 一起为 null |
| draft.groups[].days[] | Array | 逐日明细,正好铺满本组首日到末日(headcount 为该日实时人数之和) |
| droppedFleetItems[] | Array | 没进草稿的车型项(多车型户只进主车型);**必须提示** |
| staleHeadcountOrders[] | Array | 实时人数 ≠ 子订单冻结人数的户 |
| paddedOrderDays[] | Array | 为过 809109 补进分组的日期 |
| **seatOptionAdjusted[]** | Array | **本次新增**:座位档被兜底调整的户,见下 |
| seatOptionAdjusted[].groupCode | String | 该户所在的草稿分组编码(同户同车型被拆成 BUS/BUS2 时用它区分) |
| seatOptionAdjusted[].orderId / orderNo | String / String | 所属子订单(雪花 ID 按字符串处理) |
| seatOptionAdjusted[].vehicleType | String | 车型大类(草稿里该组的 vehicleType) |
| seatOptionAdjusted[].originalSeats | Integer / null | 子订单报的单车座位数;没报时为 null |
| seatOptionAdjusted[].adoptedSeats | Integer | 草稿实际采用的座位数(该组档位内的值) |
| seatOptionAdjusted[].seatOptions | Integer[] | 该大类的可选档位(升序) |
| seatOptionAdjusted[].reason | String | `SEATS_NOT_IN_OPTIONS` / `SEATS_MISSING` / `SEATS_MISSING_ADOPTED_GROUP_VALUE` |
| violations[] | Array | 草稿预检违规(与保存同一份校验内核):code / reason / detail / groupCode / tripDate / orderId |
#### 请求示例
```http
GET /v3/admin/order/group-batch/2102749115823919105/vehicle-requirement/aggregate-draft
Authorization: Bearer <admin token>
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"data": {
"groupBatchId": "2102749115823919105",
"currentStatus": null,
"draft": {
"version": null,
"remark": null,
"groups": [
{
"groupId": null,
"groupCode": "BUS",
"vehicleType": "bus",
"serviceStartDate": "2026-10-08",
"serviceEndDate": "2026-10-10",
"seats": 19,
"count": 1,
"specialTags": [],
"remark": "HL20260924001: 备注原文",
"days": [
{ "tripDate": "2026-10-08", "headcount": 9, "memberOrderIds": ["2102692937378992129"] }
]
}
]
},
"droppedFleetItems": [],
"staleHeadcountOrders": [],
"paddedOrderDays": [],
"seatOptionAdjusted": [
{
"groupCode": "BUS",
"orderId": "2102692937378992129",
"orderNo": "HL20260924001",
"vehicleType": "bus",
"originalSeats": 13,
"adoptedSeats": 19,
"seatOptions": [12, 15, 16, 19],
"reason": "SEATS_NOT_IN_OPTIONS"
}
],
"violations": []
},
"success": true
}
```
#### 空数据 / 降级响应
- 草稿没有可汇总内容时 `draft.groups` 为空数组,`seatOptionAdjusted` 为空数组,接口仍 200。
- 有需车户还没提交行程用车需求时报 809121 并逐户列出(不是空草稿)。
- **某车型大类在车队没有型号时,汇总口不 fail-closed**:退回旧规则(取子订单报的座位)、不出 `seatOptionAdjusted`,同时在 `violations` 里给出 809120,让页面能提示「这类车型还没在车务建档」。整张字典不可用(Feign 降级)仍直接抛 809120。**保存口对同一情形是 fail-closed**——这条差异是有意的:汇总是只读草稿,卡住它等于管理员连看都看不到。
```json
{ "code": 200, "message": "成功", "data": { "draft": { "groups": [] }, "seatOptionAdjusted": [], "violations": [] }, "success": true }
```
#### 错误响应
```json
{
"code": 809121,
"message": "团期 2102749115823919105 有 2 户缺少可汇总的行程用车需求,暂不能自动汇总:HL20260924001(2102692937378992129):未提交行程用车需求、HL20260924002(2102692937378992130):车型均不在车型字典内",
"success": false,
"data": null
}
```
#### 业务边界
- `reason` 的三种取值对前端提示语不同:`SEATS_NOT_IN_OPTIONS` = 「你报的 N 座这台车没有,已改用 M 座」;`SEATS_MISSING` = 「这户没报座位,跟字典最大档 M 座走」;`SEATS_MISSING_ADOPTED_GROUP_VALUE` = 「这户没报座位,采用的值 M 来自同组其它户报的合法档位(不是字典最大档)」。**不要把后两者渲染成「系统按字典给你选了 M 座」**。
- 座位是**单车**属性,多户报的值不相加(沿用 #8220)。
- 拆组逻辑不变:同车型按连续日期段拆组,第二段起组号加序号;诊断行按所属组带 `groupCode`。
- `draft.groups[].seats` 即使已被兜底到档位内,**保存时仍会按新校验复核**(同一条 809124);本端点不替代保存侧校验。
---
## 四、契约约束与正确调用方式(接口类必写)
- 座位下拉的数据源是 `GET /admin/fleet/vehicle-types/list` 返回项里的 `seatOptions`(该大类下型号 `seats` 去重升序),**按 `typeKey` 取**(SUV 这类在库里的 typeKey 可能是 `suv2`,返回的 `vehicleType` 是归一后的 `suv`,见 23_8221)。**不要在前端写死档位枚举**。
- 切换车型时必须同步换档位并清空旧值,否则旧档位会带着新车型一起提交 → 809124。
### ✅ 正确 / ❌ 错误 payload 对照(分组元素)
```json
// ✅ 大巴档位 12/15/16/19 中取值,seats 与 count 成对
{ "groupCode": "BUS", "vehicleType": "bus", "seats": 19, "count": 1 }
// ❌ 13 不在大巴档位内 → 809124
{ "groupCode": "BUS", "vehicleType": "bus", "seats": 13, "count": 1 }
// ❌ 只填座位数、车辆数漏填 → 809118(不是 809124)
{ "groupCode": "BUS", "vehicleType": "bus", "seats": 19 }
// ✅ 存量分组形态:两列都不传,整条跳过档位与容量校验
{ "groupCode": "BUS", "vehicleType": "bus" }
```
### 切换状态时的必要动作
- 保存成功后响应 `version` 会 +1,下次保存必须回传新值,否则 809102。
- 「自动汇总」会整份覆盖当前编辑内容,前端须二次确认后再灌入。
---
## 五、数据库行为(涉及写操作时必写)
- 无表结构变更、无 Flyway。
- 809124 / 809120 都在写库之前抛出:**整份零写入**(校验失败时响应 `data` 为 null,可回读同一份数据确认未变)。
---
## 六、边界行为
- 存量分组不批量重算、不迁移:只有它下次出现在某次 PUT 的 `groups` 数组里才会按新校验复核(两列皆 null 时仍整条跳过)。
- 同一份提交里多个组同车型时,字典只查一次(不是每组一次 Feign)。
- 车队车型字典服务超时/降级:保存口 fail-closed 报 809120;汇总口退回旧座位规则并在 `violations` 里报 809120。
---
## 六.5、枚举 / 数据字典(接口出现枚举时必写)
### 错误码
| 码 | 触发 | 报文 |
|---|---|---|
| 809124 | 该组 `seats` 不在该组 `vehicleType` 的可选档位内 | 第 {0} 组的座位数 {1} 不在车型 {2} 的可选档位 {3} 内,请从下拉项中选择 |
| 809120(复用) | 车队车型字典不可用 / 该大类在车队没有型号(档位不可得) | 车队车型字典暂不可用,无法校验车型,请稍后重试 |
| 809118(不变) | `seats` 与 `count` 只填了一个 | 第 {0} 组的座位数与车辆数必须同时填写,或同时留空 |
| 809116(不变) | 扣司机座后可载客座位少于该组最大单日人数 | 第 {0} 组座位数不足:{1} 座 × {2} 辆,扣除 {3} 个司机座后可载客 {4} 人,少于该组最大乘车人数 {5} 人 |
### seatOptionAdjusted[].reason
| 取值 | 含义 | 建议提示语 |
|---|---|---|
| `SEATS_NOT_IN_OPTIONS` | 子订单报了座位但不在该大类档位内 | 该户报的 {originalSeats} 座本车型没有,草稿改用 {adoptedSeats} 座(可选 {seatOptions}) |
| `SEATS_MISSING` | 子订单没报座位,草稿按字典最大档兜底 | 该户没报座位,草稿按 {adoptedSeats} 座(字典最大档)计 |
| `SEATS_MISSING_ADOPTED_GROUP_VALUE` | 子订单没报座位,草稿采用的值来自同组其它户报的合法档位 | 该户没报座位,草稿跟随本组 {adoptedSeats} 座计 |
---
## 六.6、修改前后对比(修改/删除类接口必写,新增跳过)
### 字段级对比
| 字段 | 改前 | 改后 |
|------|------|------|
| 保存接口请求/响应字段 | — | 无新增、无删除,结构不变 |
| `aggregate-draft` 响应 | 无 `seatOptionAdjusted` | 新增 `seatOptionAdjusted[]`(8 个子字段) |
| `draft.groups[].seats` | 组内子订单报的最大单车座位 | 该大类档位内的值(在档位内取组内最大、否则兜底最大档;档位不可得退回旧规则) |
### 行为级对比
| 行为 | 改前 | 改后 |
|------|------|------|
| 提交字典内档位(如 bus 19 座) | 放行 | 放行(不变) |
| 提交字典外档位(如 bus 13 座) | **放行** | **809124 拒绝** |
| 提交座位数但车型大类在车队没有型号 | 放行 | 809120 拒绝(保存口);汇总口退回旧规则 + violations 里报 809120 |
| 存量分组(seats/count 皆 null)整份再提交 | 跳过座位相关校验 | 不变,仍整条跳过 |
| 预检/整团确认侧 | 不校档位 | 不变(该入口读的是库里已存值,且被 `doConfirm` 在事务内调用,见「七、不影响范围」) |
---
## 六.7、影响评估(修改/删除类必写)
- **是否破坏向后兼容**:请求/响应结构不变,但**同一份 payload 可能从 200 变成 809124** —— 凡是座位数不在该大类档位内的提交(含手输、含车型切换后未更新座位数)都会被拒。809124 是 HTTP 200 下的业务失败。
- **前端是否必须同步上线**:**必须**。座位数必须改成档位下拉(否则运营按老习惯手输就会撞 809124);`seatOptionAdjusted` 必须展示(否则草稿改座位这件事不可见);组号输入框应改成只读展示。
- **存量数据影响**:不批量重算、不迁移。库里的历史非法档位只会在下一次保存/汇总时暴露:保存被 809124 拒(需管理员改成档位内值);预检与整团确认口**不**重判档位,不会把已确认流程卡死。
---
## 七、不影响范围(显式声明, 帮前端/QA 缩小排查面)
- **仅影响**:`PUT .../vehicle-requirement` 的座位档位校验(新增 809124);`GET .../vehicle-requirement/aggregate-draft` 的 `seats` 取值与新增 `seatOptionAdjusted`。
- **零影响**:
- 两个接口的请求/响应字段结构(除新增 `seatOptionAdjusted`)。
- 既有错误码 809116 / 809118 / 809117 / 809119 / 809120 / 809121 / 809102 等其余判据与报文。
- `GET .../vehicle-requirement`(读取回显)不触发校验,原样返回库里已落值。
- **整团确认预检(`GET .../requirement/confirm-check`)与 `confirm` 写口不重判座位档位**:它们读的是库里已存值,且确认链路在事务内,不在那里调字典 Feign。即「历史非法档位不会卡住确认」,只会在下次编辑保存时要求修正。
- `GET .../requirement/vehicle-households`、`GET .../requirement-summary` 未改动。
- 子订单级用车需求(582024 那条)未改动。
- fleet 服务与网关:零改动(`/admin/fleet/vehicle-types/list` 复用既有只读端点)。
- 数据库:无表变更、无 Flyway。
---
## 八、测试环境已验证
**部署读数**(测试服 `deploy-status.sh`,2026-09-24 12:07 读取):
```text
SERVICE BRANCH COMMIT BEHIND DEPLOYED_AT STATE
hl-order-service-v3 dev-v3 1638faf08 0/N 2026-09-24 12:06:26 ok
hl-gateway dev-v3 c238f38c3 182/Y 2026-09-21 14:44:55 ok
hl-fleet-service dev-v3 cc70c9ba3 6/N 2026-09-24 10:49:54 ok
```
被测服务 `BEHIND=0/N STATE=ok`;请求路径上的 `hl-gateway` 与断言依赖的 `hl-fleet-service` 同为 `ok`,两行的 BEHIND 右侧字母均为 `N`(落后提交未触及这两个服务本身)。
**网关真实请求与响应**(`https://api.test.1814.love`,`groupBatchId=2102692937584513025`,当前 DRAFT v3:BUS 16 座 ×1、SUV 5 座 ×1):
```text
① 原值提交 BUS 16 座 / SUV 5 座(均在档位内)
PUT /v3/admin/order/group-batch/2102692937584513025/vehicle-requirement
→ http 200, code=200, message=成功;回读 version 2→3,seats 仍为 16/5 ✓
② BUS 提交 13 座(bus 档位 12/15/16/19)
→ http 200, code=809124
message=第 BUS 组的座位数 13 不在车型 bus 的可选档位 [12, 15, 16, 19] 内,请从下拉项中选择 ✓
拒绝后回读:version 仍为 3、seats 仍为 16/5、remark 未变(整份零写入)✓
③ SUV 提交 6 座(suv 档位 5/7)
→ http 200, code=809124
message=第 SUV 组的座位数 6 不在车型 suv 的可选档位 [5, 7] 内,请从下拉项中选择 ✓
④ GET /v3/admin/order/group-batch/2102692937584513025/vehicle-requirement/aggregate-draft
→ code=200;draft.groups = BUS(bus, 16 座, 1 辆, 3 天) + SUV(suv, 5 座, 1 辆, 3 天)
seatOptionAdjusted = [](该团期子订单报的座位都合法,无兜底调整)
violations = [];droppedFleetItems 1 条、stale/padded 0 条 ✓
```
**定向测试逐类读数**(`mvn -o -pl hl-order-service-v3 -am test -Dtest='…' -DfailIfNoTests=false -Dhl.surefire.failIfNoTests=false`,14 个类合计 0 failures / 0 errors / 0 skipped,BUILD SUCCESS):
| 测试类 | Tests run |
|---|---|
| GroupVehicleRequirementSaveTest | 31 |
| GroupVehicleDraftAggregatorTest | 24 |
| GroupVehicleRequirementAggregateDraftTest | 12 |
| GroupVehicleRequirementConfirmCheckTest | 19 |
| GroupVehicleRequirementLockContractTest | 3 |
| GroupBatchRequirementServiceTest | 73 |
| GroupBatchRequirementServiceConfirmVehicleTest | 17 |
| GroupBatchRequirementServiceCheckVehicleTest | 17 |
| GroupBatchRequirementConfirmReflectionGuardTest | 4 |
| FleetVehicleTypeNameLoaderTest | 9 |
| TransactionalRemoteCallArchTest | 1 |
| ErrorCodeUniquenessGuardTest | 3 |
| RequirementGroupBatchErrorCodeRangeTest | 4 |
| ErrorCodeTemplateNumberFormattingTest | 1 |
---
## 十、相关文档
- 自动汇总草稿端点:#8220 → `changelogs-v2/2026-09/23_8220_团期正式行程用车需求自动汇总草稿-新增接口-管理后台.md`
- 车辆规格四字段(seats/count/specialTags/remark):#8152 → `changelogs-v2/2026-09/22_8152_团期用车需求补车辆规格与接送机汇总-修改接口-管理后台.md`
- 座位充足性校验扣司机座(809116):#8278 → `changelogs-v2/2026-09/23_8278_团级用车分组座位校验扣司机座-修改接口-管理后台.md`
- 座位数下拉先例(子订单级):#4856 / #4871 → `changelogs-v2/2026-07/50_4856_订单调整用车座位数按车型型号下拉-管理后台.md`
- 车型大类字典与 SUV 回显归一:#8202 / #8221 → `changelogs-v2/2026-09/`
---
## 关联 / 联系人
### 链接
- Issue: https://git.1814.love/wx/HL/issues/8311
- 后端 PR: 待建
### 联系人
- 后端: jw / wx