文件
hl-api-changelog/changelogs-v2/2026-09/22_8152_团期用车需求补车辆规格与接送机汇总-修改接口-管理后台.md
T

1027 行
54 KiB
Markdown
原始文件 Blame 文件历史

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
---
schema: "hl-changelog/v2"
ticket: "8152"
title: "团期用车需求补车辆规格(座位/车辆数/特殊诉求/备注),新增子订单用车需求记录端点,汇总与预检补接送机口径"
consumer: "admin"
author: "wx(GIT)"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "not_required"
frontend_status: "verified"
frontend_owner: "mmg"
frontend_ref: "14cb7110ab6072af1dc3ee767951771be9972436"
target_release: "v2.1"
verified_at: "2026-09-22"
status_note: "一次提交覆盖三张工单:#8152 团级乘车分组补车辆规格四字段 + 三个新错误码(809116/809117/809118);#8151 新增 GET .../requirement/vehicle-households 并给 requirement-summary 补 transferSummary;#8153 给 requirement/confirm-check 补 transferSubmitEnabled 与 transferDeclaredWithoutRequirement。gateway_status: not_required —— 本批零网关改动,/v3/admin/** 通配已配在 hl-gateway application.yml 的 order-service-v3 路由上,无新增 /admin/ 前缀。frontend_status: pending —— 新增端点与新增字段均需前端渲染,后端不代填。 前端已交付(hl-admin v2.1 @ 14cb7110):用车板块全链路——编辑弹窗规格四字段+paired rule 前置 809118、余座负值直显不 clamp 不拦提交;新建 VehicleSummarySection(两套字段名分开映射)与 VehicleHouseholdsSection(户数≠行数);confirm-check 接送机缺口提示不随免车/未开放短路。 订正残留已清(0b65814a06):VehicleHouseholdsSection 注释与 spec 夹具换车务口径真值,渲染面本就纯透传无行为变更。"
updated_at: "2026-09-22"
base: "dev-v3"
---
# order-v3 团期需求: 团级用车需求补车辆规格,新增子订单用车需求记录端点,汇总/预检补接送机口径
> **存放目录**: 二期(v3) → `changelogs-v2/2026-09/`
>
> **服务**: hl-order-service-v3(团期需求域)、hl-fleet-service(配车覆盖度基线,无对外接口变化)
> **PR**: 见文末「关联 / 联系人」
> **Issue**: #8152 / #8151 / #8153
> **日期**: 2026-09-22
> **影响范围**: 管理后台「团期详情 → 查看需求」Tab 的用车板块(团级正式用车需求编辑弹窗、子订单用车需求记录、全团需求汇总、整体确认预检)
---
## ⚠️ 关键变化(非必须,本版与上版行为不同 / 纠错 / 撤销时必写)
**一条口径不一致必须在页面上原样透出,不要自行抹平:**
- **拦截口径不扣司机位**:`PUT .../vehicle-requirement` 的座位充足性校验判的是 `seats × count < 该组最大单日乘车人数`,**总座位里含驾驶位**。
- **回显口径扣司机位**:响应里的 `remainingPassengerSeats` = `max(0, seats × count − count)` − 该组最大单日乘车人数,**每辆车扣 1 个司机位**。
- 于是 **20 座 × 1 辆 / 20 人能保存成功,而返回的 `remainingPassengerSeats` 是 `-1`**。
**展示侧**:`remainingPassengerSeats` **必须允许负数**,不能 clamp 到 0。负值就是缺口,clamp 之后「刚好坐不下 1 人」与「刚好坐满」在页面上完全同形,运营拿不到补车信号。
🔴 **提交侧(更要紧的一条)**:保存成功的分组,其 `remainingPassengerSeats` **可以为负数**。这是「余座缺口」的展示值,**不是校验失败信号,前端不得据此拦截提交**。是否允许保存由后端 809116 判定(口径:`seats × count` 与该组最大乘车人数比较,不扣司机位);前端只需把负数如实显示为缺口,用于提示运营补车。
响应体里**没有任何字段**表示「这一单已经过了后端闸门」——`remainingPassengerSeats` 是唯一的座位信号。若前端按「负数 = 不合法」做提交拦截,`20 座 × 1 辆 / 20 人`这类组合在 UI 上就**永远提交不了**,而后端 API 是放行的:严口径事实上变成了闸门,且后端全绿、日志无异常、无人会发现。
---
## 一、背景(选填)
团级正式用车需求原先只填车型文本,车务拿到的是「35座大巴」这种字符串,排车时「几座 × 几辆」只能靠人问;同时团期管理员在「查看需求」Tab 里看不到定制师逐户报了什么(用房侧早有「汇总 + 子订单订房记录」上下两块,用车只有团级那一块),等于对着一个汇总数字签字。本批补齐三件事:团级分组的车辆规格、子订单用车需求记录端点、接送机(TRANSFER)在汇总与预检里的口径。
| 维度 | 本批之前 | 本批之后 |
|------|----------|----------|
| 团级乘车分组可填规格 | 仅车型文本 | 车型 + 座位数 + 车辆数 + 特殊诉求标签 + 备注 |
| 团期「查看需求」用车板块 | 只有团级正式需求一块 | 团级正式需求 + 子订单用车需求记录(新端点) |
| `requirement-summary` 车侧口径 | 只统计行程用车(TRAVEL) | 车侧字段口径**不变**,接送机另挂 `transferSummary` |
| 整体确认预检的接送机信息 | 无 | `transferSubmitEnabled` + `transferDeclaredWithoutRequirement` 缺口名单 |
---
## 二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 保存团期正式用车需求 | PUT | `/v3/admin/order/group-batch/{groupBatchId}/vehicle-requirement` | 请求体新增 4 字段 + 响应新增 8 字段 + 3 个新错误码 | 分组元素补 seats/count/specialTags/remark |
| 2 | 读团期正式用车需求 | GET | `/v3/admin/order/group-batch/{groupBatchId}/vehicle-requirement` | 响应新增 8 字段 | 与 1 共用同一响应体,原样回显 |
| 3 | 团期子订单用车需求记录 | GET | `/v3/admin/order/group-batch/{groupBatchId}/requirement/vehicle-households` | 新增接口 | 形态对齐既有 hotel-households |
| 4 | 全团需求汇总 | GET | `/v3/admin/order/group-batch/{groupBatchId}/requirement-summary` | 响应新增 transferSummary | 既有车侧字段零改动 |
| 5 | 整体确认需求缺失预检 | GET | `/v3/admin/order/group-batch/{groupBatchId}/requirement/confirm-check` | 响应新增 2 字段 | 接送机开关 + 缺口名单 |
---
## 三、接口详情
### 1. 保存团期正式用车需求 `PUT /v3/admin/order/group-batch/{groupBatchId}/vehicle-requirement`
**VO**: `GroupVehicleRequirementSaveReqVO → GroupVehicleRequirementRespVO`
#### 使用场景
团期详情「查看需求 → 团级正式用车需求」编辑弹窗点保存。语义是**整份全量替换**:未出现在本次提交里的分组会被移出当前版本。本批在分组元素里新增了 4 个可填字段,其余字段与提交语义均不变。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| 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&lt;String&gt; | ❌ | 取值须在字典 `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&lt;Long&gt; | ✅ | 非空,须全属本团在团户 | 该组该日实际乘车的子订单集合 |
#### 出参 `Result<GroupVehicleRequirementRespVO>`
| 字段 | 类型 | 说明 |
|------|------|------|
| requirementId | String | 正式需求主键(Long 序列化为字符串) |
| groupBatchId | String | 团期聚合主键(Long 序列化为字符串) |
| status | String | DRAFT / CONFIRMED / DISPATCHED / DONE / PENDING_RECONFIRM / CANCELLED;PUT 后为 DRAFT |
| version | Integer | 版本号,下次提交须回传 |
| remark | String | 整份备注(撤回/免车会追加,不覆盖) |
| confirmedBy | String | 整份确认人;DRAFT 时为 null |
| confirmedAt | LocalDateTime | 整份确认时间;DRAFT 时为 null |
| planRefreshState | String | 配车刷新状态原值;null = 从未登记过刷新 |
| planRefreshReplayCount | Integer | 人工受控重投累计次数,上限 5 |
| blockedStage | String | 团期被阻断阶段快照;null = 未阻断 |
| planRefreshStalled | Boolean | 刷新是否已停滞、不会自愈(恒非 null),判「要不要现在找人」看它 |
| planRefreshStalledReason | String | STATE_FAILED / COMMAND_FAILED / TIMEOUT;未停滞为 null |
| planRefreshTimeoutAt | LocalDateTime | 本轮刷新超时时刻;非 PENDING 为 null |
| planRefreshReplayExhausted | Boolean | 人工重投额度是否耗尽(恒非 null) |
| groups | Array | 全部乘车分组;整团免车态为空数组 |
| groups[].groupId | String | 分组主键(Long 序列化为字符串),下次提交同一组必须回传 |
| groups[].groupCode | String | 分组键 = 车费 alloc_group |
| groups[].vehicleType | String | 车型文本/字典值 |
| **groups[].vehicleTypeName** | String | **本批新增** 车型中文名,后端下发,前端不自己映射编码 |
| groups[].serviceStartDate / serviceEndDate | LocalDate | 本组服务日范围 |
| **groups[].seats** | Integer | **本批新增** 单车座位数;**存量分组为 null(表示待填),不是 0** |
| **groups[].count** | Integer | **本批新增** 车辆数量;存量分组为 null |
| **groups[].specialTags** | Array&lt;SpecialTagItem&gt; | **本批新增** 特殊诉求标签(code + name);存量分组为空数组 |
| **groups[].remark** | String | **本批新增** 该组备注;存量分组为 null |
| **groups[].totalSeatCount** | Integer | **本批新增** 总座位数 = seats × count(含驾驶位);座位或数量缺一即为 null |
| **groups[].maxHeadcount** | Integer | **本批新增** 该组 days 里的最大用车人数 |
| **groups[].remainingPassengerSeats** | Integer | **本批新增** 余座 = 扣司机位后的可乘座位 − maxHeadcount;**可为负**;座位或数量缺一即为 null |
| groups[].specialTags[].code / name | String | 字典编码 / 中文名;字典查不到该编码时 name 为 null(不回落成编码) |
| groups[].days[].tripDate | LocalDate | 团期行程日 |
| groups[].days[].headcount | Integer | 该组该日用车人数 |
| groups[].days[].memberOrderIds | Array&lt;String&gt; | 成员子订单集合(Long 序列化为字符串) |
| groups[].days[].memberOrderCount | Integer | 当日成员户数 = memberOrderIds.size(),供填人数时对照 |
#### 请求示例
```json
{
"version": 3,
"remark": "9/13 起换大巴",
"groups": [
{
"groupId": "1867000000009",
"groupCode": "BUS",
"vehicleType": "35座大巴",
"serviceStartDate": "2026-09-12",
"serviceEndDate": "2026-09-13",
"seats": 19,
"count": 2,
"specialTags": ["child_seat"],
"remark": "含高速费",
"days": [
{ "tripDate": "2026-09-12", "headcount": 9, "memberOrderIds": [2101506167043985410] },
{ "tripDate": "2026-09-13", "headcount": 9, "memberOrderIds": [2101506167043985410] }
]
}
]
}
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"data": {
"requirementId": "1867000000101",
"groupBatchId": "2101506167098511362",
"status": "DRAFT",
"version": 4,
"remark": "9/13 起换大巴",
"confirmedBy": null,
"confirmedAt": null,
"planRefreshState": null,
"planRefreshReplayCount": null,
"blockedStage": null,
"planRefreshStalled": false,
"planRefreshStalledReason": null,
"planRefreshTimeoutAt": null,
"planRefreshReplayExhausted": false,
"groups": [
{
"groupId": "1867000000009",
"groupCode": "BUS",
"vehicleType": "35座大巴",
"vehicleTypeName": "大巴",
"serviceStartDate": "2026-09-12",
"serviceEndDate": "2026-09-13",
"seats": 19,
"count": 2,
"specialTags": [{ "code": "child_seat", "name": "儿童安全座椅" }],
"remark": "含高速费",
"totalSeatCount": 38,
"maxHeadcount": 9,
"remainingPassengerSeats": 27,
"days": [
{
"tripDate": "2026-09-12",
"headcount": 9,
"memberOrderIds": ["2101506167043985410"],
"memberOrderCount": 1
},
{
"tripDate": "2026-09-13",
"headcount": 9,
"memberOrderIds": ["2101506167043985410"],
"memberOrderCount": 1
}
]
}
]
},
"success": true
}
```
#### 空数据 / 降级响应
- 整团没有在团需车户时,`groups: []` 是合法提交,响应 `groups` 为空数组。
- 存量分组(本批之前建的)回显时 `seats`/`count`/`remark` 为 `null`、`specialTags` 为 `[]`、`totalSeatCount`/`remainingPassengerSeats` 为 `null`。**`null` 表示「这一组还没填过车辆规格」,与填了 0 是两件事(0 通不过 `@Min(1)`)**,前端不要把 null 兜成 0。
- 车型中文名依赖车队侧车型库:查不到编码或车队不可用时 `vehicleTypeName` 为 `null`,接口仍 200,不阻断页面。
```json
{ "code": 200, "message": "成功", "data": { "groups": [] }, "success": true }
```
#### 错误响应
```json
{
"code": 809116,
"message": "第 BUS 组座位数不足:19 座 × 1 辆 = 19 座,少于该组最大乘车人数 20 人",
"success": false,
"data": null
}
```
```json
{
"code": 809118,
"message": "第 BUS 组的座位数与车辆数必须同时填写,或同时留空",
"success": false,
"data": null
}
```
```json
{
"code": 809117,
"message": "第 BUS 组特殊诉求标签 wheelchair_lift 不在字典内,请从下拉项中选择",
"success": false,
"data": null
}
```
#### 业务边界
- **报文里「第 {0} 组」的 {0} 填的是 `groupCode`,不是序号**。809116/809117/809118 三条均如此,报文渲染出来形如「第 BUS 组…」,前端可直接展示、也可据此定位是哪一组填错。
- **一次只抛一条**:整份提交的多条违规按校验遍历顺序收集,只抛第一条。前端改完一处再保存可能撞上下一条。
- **座位充足性判据 = `seats × count < 该组最大单日乘车人数`,不扣司机位**(拦截口径),与响应里扣司机位的 `remainingPassengerSeats`(回显口径)刻意差一个司机位。这样「刚好坐满、司机另开一辆」的既有排法不会被一刀拦死。
- **seats 与 count 同生同死**:只填一个抛 809118;两个都不填 = 存量形态,座位校验整体跳过。
- **标签校验在事务外先做**:`specialTags` 要调字典服务,校验不过时零写入。字典按 ACTIVE 项放行,运营新增字典项后立即可用(后端未硬编码枚举)。
- 标签编码会被 trim、丢空白、按首次出现去重保序后落库,前端重复提交同一编码不会报错,但回显只有一条。
- 整份审核粒度是「整份」,不支持增量补丁;整团不需要用车请走 `POST .../vehicle-requirement/waive`,不要提交空 groups 代替(有需车户时会撞 809103)。
- 乐观锁:`version` 不一致抛 809102,其报文第二个占位符**不保证是数字**(CAS 落空分支传的是说明文本),前端不要按数字解析。
---
### 2. 读团期正式用车需求 `GET /v3/admin/order/group-batch/{groupBatchId}/vehicle-requirement`
**VO**: `GroupVehicleRequirementSaveReqVO → GroupVehicleRequirementRespVO`
#### 使用场景
打开团期「查看需求 → 团级正式用车需求」时读取回显,与保存端点共用同一个响应体(`PUT` / `GET` / `withdraw` / `waive` 四端点同体)。本批新增的 8 个字段在这里原样可读。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| groupBatchId | Path | Long | ✅ | - | 团期聚合主键 |
无请求体。
#### 出参 `Result<GroupVehicleRequirementRespVO>`
| 字段 | 类型 | 说明 |
|------|------|------|
| data | Object | 结构与第 1 节「出参」逐字段相同;该团尚未形成正式用车需求时为 `null` |
| data.groups[].seats / count | Integer | 本批新增;存量分组为 null |
| data.groups[].specialTags | Array&lt;SpecialTagItem&gt; | 本批新增;存量分组为空数组 |
| data.groups[].vehicleTypeName | String | 本批新增;车队不可用或无此编码时为 null |
| data.groups[].totalSeatCount / maxHeadcount / remainingPassengerSeats | Integer | 本批新增;座位或数量缺一时前者与后者为 null |
#### 请求示例
```http
GET /v3/admin/order/group-batch/2101506167098511362/vehicle-requirement HTTP/1.1
Host: <网关域名>
Authorization: Bearer <token>
```
无请求体。
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"data": {
"requirementId": "1867000000101",
"groupBatchId": "2101506167098511362",
"status": "CONFIRMED",
"version": 4,
"groups": [
{
"groupId": "1867000000010",
"groupCode": "SUV",
"vehicleType": "suv",
"vehicleTypeName": "SUV系列",
"serviceStartDate": "2026-09-12",
"serviceEndDate": "2026-09-12",
"seats": 20,
"count": 1,
"specialTags": [],
"remark": null,
"totalSeatCount": 20,
"maxHeadcount": 20,
"remainingPassengerSeats": -1,
"days": [
{
"tripDate": "2026-09-12",
"headcount": 20,
"memberOrderIds": ["2101506167043985410"],
"memberOrderCount": 1
}
]
}
]
},
"success": true
}
```
上面就是「关键变化」那条口径不一致的真实形态:20 座 × 1 辆载 20 人保存成功,余座 `-1`。
#### 空数据 / 降级响应
该团尚未形成正式用车需求时 `data` 为 `null`(不是空对象),前端按「尚未创建」渲染:
```json
{ "code": 200, "message": "成功", "data": null, "success": true }
```
#### 错误响应
```json
{
"code": 403,
"message": "无权限访问",
"success": false,
"data": null
}
```
#### 业务边界
- 判权码 `group-batch:view`,与 `requirement-summary` / `hotel-households` 同码。
- `status` 不是恒定值,不要按「读到即 CONFIRMED」渲染。
- 配车刷新观测块七个字段是只读投影,不接受回传、不参与任何提交;判「要不要现在找人」看 `planRefreshStalled`,别看 `planRefreshState`。
- 存量团期读出来的分组 `seats`/`count` 为 null 是正常形态(绝大多数历史团期都是),不要当异常画红。
---
### 3. 团期子订单用车需求记录 `GET /v3/admin/order/group-batch/{groupBatchId}/requirement/vehicle-households`
**VO**: `GroupVehicleRequirementSaveReqVO → GroupVehicleHouseholdsRespVO`
#### 使用场景
**本批新增端点。** 团期详情「查看需求」Tab 的用车板块下半块:上面是团级正式需求,下面就是本端点——定制师逐户报了什么。形态刻意对齐既有 `.../requirement/hotel-households`,前端可复用用房需求那套「每户一张卡」的展示组件。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| groupBatchId | Path | Long | ✅ | - | 团期聚合主键 |
| kind | Query | String | ❌ | `TRAVEL` / `TRANSFER` | 需求类别过滤;**不传则两类都返**(前端默认不传) |
无请求体。
#### 出参 `Result<GroupVehicleHouseholdsRespVO>`
| 字段 | 类型 | 说明 |
|------|------|------|
| groupBatchId | String | 团期 ID(Long 序列化为字符串) |
| departDate | LocalDate | 团期出发日期;未定出发日时为 null |
| householdCount | int | 本列表户数,**按 orderId 去重**(一户同时报了两类只算 1 户) |
| vehicleRowCount | int | 需求行数(一户可能 TRAVEL + TRANSFER 两行),与 householdCount 刻意不等价 |
| countedHouseholdCount | int | 计入 `requirement-summary` 车侧汇总的户数(= 有活跃 TRAVEL 行的户数) |
| households | Array | 逐户明细,按 orderNo 升序 |
| households[].orderId | String | 子订单 ID(Long 序列化为字符串) |
| households[].orderNo | String | 子订单团号 |
| households[].customerName | String | 主联系人姓名 |
| households[].participantCount | int | 出行人数(成人 + 儿童 + 小童 + 婴儿) |
| households[].consultantId | String | 定制师 ID(Long 序列化为字符串);未指派为 null |
| households[].consultantName | String | 定制师姓名;未指派为 null |
| households[].countedInSummary | boolean | 该户是否计入车侧汇总(= 有活跃 TRAVEL 行);只报接送机的户为 false |
| households[].requirements | Array | 该户的用车需求行,0~2 条(TRAVEL / TRANSFER 各至多一条活跃行) |
| requirements[].requirementId | String | 需求行 ID(Long 序列化为字符串) |
| requirements[].kind | String | `TRAVEL` = 行程用车 / `TRANSFER` = 接送机 |
| requirements[].kindName | String | 类别中文名,后端下发,前端不自己映射 |
| requirements[].status | String | PENDING_REVIEW / PENDING / PROCESSING / DONE |
| requirements[].statusName | String | 状态中文名(车务口径);`status` 为空时为 `null`,遇未知状态码时原样回落该 code 字符串 |
| requirements[].fleet | Array&lt;FleetItem&gt; | 车队明细,定制师所报原貌,不做合并 |
| requirements[].specialTags | Array&lt;SpecialTagItem&gt; | 特殊诉求标签;未填为空列表 |
| requirements[].remark | String | 备注 / 其他诉求,≤500;未填为 null |
| requirements[].serviceDates | Array&lt;LocalDate&gt; | 本需求冻结的服务日期;未冻结时为空列表 |
| requirements[].headcount | Integer | 乘车人数(不含司机) |
| requirements[].totalSeatCount | Integer | 总座位数 = Σ(seats × count);fleet 为空时为 0 |
| requirements[].remainingPassengerSeats | Integer | 余座 = 可载客座位(已按每车扣 1 个司机位)− 乘车人数 |
| requirements[].pickupRequired | Boolean | 是否需要平台接机/接站;**仅 TRANSFER 有意义** |
| requirements[].dropoffRequired | Boolean | 是否需要平台送机/送站;**仅 TRANSFER 有意义** |
| requirements[].returnRemark | String | 打回原因;未被打回过为 null |
| requirements[].returnedAt | LocalDateTime | 打回时间;未被打回过为 null |
| fleet[].vehicleType | String | 车型编码(suv / mpv / bus / sedan);缺失为 null |
| fleet[].vehicleTypeName | String | 车型中文名;车队不可用或无此编码时为 null |
| fleet[].seats / count | Integer | 单车座位数 / 车辆数 |
| specialTags[].code / name | String | 标签编码(字典 `vehicle_special_demand` 的 value)/ 中文名;字典不可用或无此编码时 name 为 null |
#### 请求示例
```http
GET /v3/admin/order/group-batch/2101506167098511362/requirement/vehicle-households?kind=TRANSFER HTTP/1.1
Host: <网关域名>
Authorization: Bearer <token>
```
无请求体。不传 `kind` 时 URL 为 `.../requirement/vehicle-households`,两类都返。
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"data": {
"groupBatchId": "2101506167098511362",
"departDate": "2026-09-12",
"householdCount": 1,
"vehicleRowCount": 2,
"countedHouseholdCount": 1,
"households": [
{
"orderId": "2101506167043985410",
"orderNo": "GT-26-0081",
"customerName": "陈昊",
"participantCount": 4,
"consultantId": "10001",
"consultantName": "李四",
"countedInSummary": true,
"requirements": [
{
"requirementId": "2101506167043985411",
"kind": "TRAVEL",
"kindName": "行程用车",
"status": "DONE",
"statusName": "配车完成",
"fleet": [
{ "vehicleType": "suv", "vehicleTypeName": "SUV系列", "seats": 7, "count": 1 }
],
"specialTags": [{ "code": "child_seat", "name": "儿童安全座椅" }],
"remark": "含高速费",
"serviceDates": ["2026-09-12", "2026-09-13"],
"headcount": 4,
"totalSeatCount": 7,
"remainingPassengerSeats": 2,
"pickupRequired": null,
"dropoffRequired": null,
"returnRemark": null,
"returnedAt": null
},
{
"requirementId": "2101506167043985412",
"kind": "TRANSFER",
"kindName": "接送机",
"status": "PENDING",
"statusName": "待车队配",
"fleet": [
{ "vehicleType": "mpv", "vehicleTypeName": "商务车", "seats": 7, "count": 1 }
],
"specialTags": [],
"remark": null,
"serviceDates": ["2026-09-12"],
"headcount": 4,
"totalSeatCount": 7,
"remainingPassengerSeats": 2,
"pickupRequired": true,
"dropoffRequired": false,
"returnRemark": null,
"returnedAt": null
}
]
}
]
},
"success": true
}
```
注意这一例里 `householdCount=1` 而 `vehicleRowCount=2`——**表头「共 N 户」必须取 `householdCount`,不能取 `households.length` 之外的行数**。
#### 空数据 / 降级响应
团里没有任何活跃用车需求(或按 `kind` 过滤后为空)时,三个计数为 0、`households` 为空数组,接口仍 200:
```json
{
"code": 200,
"message": "成功",
"data": {
"groupBatchId": "2101506167098511362",
"departDate": "2026-09-12",
"householdCount": 0,
"vehicleRowCount": 0,
"countedHouseholdCount": 0,
"households": []
},
"success": true
}
```
车型中文名与标签中文名依赖车队侧与字典:不可用时对应 `vehicleTypeName` / `name` 为 `null`,其余字段照常返回,不 500、不阻断页面。
#### 错误响应
```json
{
"code": 403,
"message": "无权限访问",
"success": false,
"data": null
}
```
#### 业务边界
- 判权码与既有 `hotel-households` / `requirement-summary` 一致(`group-batch:view`)。
- **户数与行数是两个数**:`householdCount` 按 orderId 去重,`vehicleRowCount` 数行。照抄用房侧的写法在「一户两类」时会把表头写错。
- **被打回的需求行不在本列表内**(打回 = 原地置 REJECTED_* 并失活)。这与用房侧「列出打回户并灰显」**刻意不同**,前端不要按用房那套去找 `countedInSummary=false` 的打回户。
- `countedInSummary=false` 在本端点的含义是「该户没有活跃 TRAVEL 行」——典型就是只报了接送机的户。车侧汇总只统计行程用车。
- `pickupRequired` / `dropoffRequired` 仅 TRANSFER 行有意义,TRAVEL 行上为 null。
- 不传 `kind` = 两类都返,这与提交侧「不传按 TRAVEL」的缺省相反:只读筛选若沿用提交侧缺省,接送机会整类从页面消失且无提示。
- 🔴 **`statusName` 口径订正说明(曾误用房务枚举 label)**:本端点的 `statusName` 原实现直接取 `RequirementStatus` 枚举自带的 `label`——该枚举被房、车两域共用,`label` 是**房务侧视角**文案(`PENDING` 的 label 字面量就是「待房务配」),枚举类自己的 javadoc 已写明「车需求场景下的展示文案由前端另行映射」。已由 `45adfd7ed`(PR #8173,`fix(order-v3): 用车逐户需求读口 statusName 改用车务文案,不再吐枚举的房务 label (#8151)`)修正为改调 `GroupBatchConverter.resolveRequirementStatusName(code, true)`,与团期其它用车读口(团级正式需求的 `vehicleRequirementStatusName` 等)同一口径。完整取值:
| `status` | `statusName`(车务口径) |
|------|------|
| `PENDING` | 待车队配 |
| `PROCESSING` | 配车中 |
| `DONE` | 配车完成 |
| `PENDING_REVIEW` | 待审核 |
| `REJECTED_TO_CONSULTANT` | 已驳回定制师 |
| `REJECTED_TO_ADMIN` | 已驳回管理员 |
| 其它未知码 | 原样回落该 code 字符串 |
| 空 / `null` | `null` |
后两个驳回态在映射范围内,但被打回的需求行已失活、不出现在本端点的列表里(见前一条),故本列表实际只会出现前 4 行。
🔎 **同源残留(写给后续做 grep 的人,两处均不影响运行)**:上述被订正的四个错误取值(待处理 / 处理中 / 已完成 / 待审核)在本文件之外还有两个落点,hl-admin `14cb7110` 实读结果:
- `VehicleHouseholdsSection.vue:191` 的注释写着「四枚举:待审核/待处理/处理中/已完成」。**不影响渲染**:同文件 `:78` 是 `req.statusName` 为空则回落 `req.status` 再回落破折号的纯透传表达式,无任何字面分支;`:65` 注释已明写「kindName/statusName 后端下发,禁自建编码→中文映射」。后端文案变了,页面显示跟着变。
- `__tests__/VehicleHouseholdsSection.spec.js:39,57,111,113` 的夹具是 `statusName: '已完成'` / `'待处理'`。**不会变红**:用例断言的是透传行为(输入什么就渲染什么),夹具换成任何字符串都绿。
两处都只是文本,改与不改都不影响功能。列在这里是因为下一个人 grep「待处理」时会同时撞上它们,得能分清哪些是错的、哪些是它们本该的样子。
▶ 责任归属写在明处:这四个取值是**后端先写错在本交接件里**、前端照着抄的。契约文档错与实现错同等有害,已由 `e011960` 订正。
---
### 4. 全团需求汇总 `GET /v3/admin/order/group-batch/{groupBatchId}/requirement-summary`
**VO**: `GroupVehicleRequirementSaveReqVO → GroupRequirementSummaryRespVO`
#### 使用场景
团期「查看需求」Tab 顶部汇总块。本批只做一件事:新增 `transferSummary`(接送机汇总)。**既有车侧字段(`vehicleRequirementCount`、`vehicleSeatSummary`)口径零改动,仍只统计行程用车。**
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| groupBatchId | Path | Long | ✅ | - | 团期聚合主键 |
无请求体。
#### 出参 `Result<GroupRequirementSummaryRespVO>`
| 字段 | 类型 | 说明 |
|------|------|------|
| activeOrderCount 等既有字段 | int / Array | 本批不改,口径不变 |
| vehicleRequirementCount | int | 已提交用车需求的子订单数(既有,仍是行程用车口径) |
| vehicleSeatSummary | Array | 大巴座位汇总(既有,仍是行程用车口径) |
| **transferSummary** | Object | **本批新增** 接送机汇总;恒非 null(空团也给空壳) |
| transferSummary.householdCount | int | 有活跃接送机需求的户数(按 orderId 去重) |
| transferSummary.pickupHouseholdCount | int | 其中需要平台接机/接站的户数(pickupRequired=true) |
| transferSummary.dropoffHouseholdCount | int | 其中需要平台送机/送站的户数(dropoffRequired=true) |
| transferSummary.headcount | int | 接送机总人数(各户 headcount 之和;未填人数的户按 0 计) |
| transferSummary.vehicleSeatSummary | Array&lt;TransferSeatItem&gt; | 按车型聚合的接送机座位/台数 |
| transferSummary.serviceDates | Array&lt;LocalDate&gt; | 接送机涉及的服务日期去重集合(升序);未冻结服务日的需求不贡献日期 |
| TransferSeatItem.vehicleType | String | 车型大类编码(suv / mpv / bus / sedan;需求里缺失时为「未知」) |
| TransferSeatItem.vehicleTypeName | String | 车型大类中文名;查不到或车队不可用时为 null |
| TransferSeatItem.seats | int | 合计座位数 = Σ(单车座位 × 台数) |
| TransferSeatItem.count | int | 合计车辆台数 |
#### 请求示例
```http
GET /v3/admin/order/group-batch/2101506167098511362/requirement-summary HTTP/1.1
Host: <网关域名>
Authorization: Bearer <token>
```
无请求体。
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"data": {
"activeOrderCount": 6,
"vehicleRequirementCount": 5,
"vehicleSeatSummary": [
{ "vehicleType": "bus", "vehicleTypeName": "大巴", "totalSeats": 38, "totalCount": 2 }
],
"transferSummary": {
"householdCount": 2,
"pickupHouseholdCount": 2,
"dropoffHouseholdCount": 1,
"headcount": 7,
"vehicleSeatSummary": [
{ "vehicleType": "mpv", "vehicleTypeName": "商务车", "seats": 14, "count": 2 }
],
"serviceDates": ["2026-09-12", "2026-09-16"]
}
},
"success": true
}
```
#### 空数据 / 降级响应
团里没有接送机需求(含空团)时 `transferSummary` **仍是对象、不是 null**,计数为 0、数组为空——前端可以无条件 `v-for`,不必做 null 判断:
```json
{
"code": 200,
"data": {
"transferSummary": {
"householdCount": 0,
"pickupHouseholdCount": 0,
"dropoffHouseholdCount": 0,
"headcount": 0,
"vehicleSeatSummary": [],
"serviceDates": []
}
},
"success": true
}
```
#### 错误响应
```json
{
"code": 403,
"message": "无权限访问",
"success": false,
"data": null
}
```
#### 业务边界
- 判权码 `group-batch:view`(本端点暴露客户特殊需求)。
- **`transferSummary` 与既有车侧字段是两套数**:既有 `vehicleSeatSummary` 只含行程用车,接送机的座位/台数只在 `transferSummary.vehicleSeatSummary` 里。两者不要相加当「全团用车」展示,除非页面明确要合计。
- 🔴 **两组同域字段名不一致,必须写两套映射**:接送机侧与行程车侧都是「按车型聚合的座位/台数」,字段含义完全相同,但**名字不同**(按工单接口表刻意为之,本批不改)。照既有字段名去取新对象会拿到 `undefined`:
| 含义 | 行程车侧 `vehicleSeatSummary[]`(既有 `VehicleSeatItem`) | 接送机侧 `transferSummary.vehicleSeatSummary[]`(新增 `TransferSeatItem`) |
|------|------|------|
| 车型编码 | `vehicleType` | `vehicleType`(同名) |
| 车型中文名 | `vehicleTypeName` | `vehicleTypeName`(同名) |
| 合计座位数 | `totalSeats` | **`seats`** |
| 合计车辆台数 | `totalCount` | **`count`** |
前端不要复用同一个渲染/取数函数,需要两套映射。
- 车型编码在需求里缺失时归到 `"未知"` 一条,台数不丢。
- `headcount` 把未填人数的户按 0 计,所以它可能小于 `householdCount` × 实际人数,只作汇总参考。
---
### 5. 整体确认需求缺失预检 `GET /v3/admin/order/group-batch/{groupBatchId}/requirement/confirm-check`
**VO**: `GroupVehicleRequirementSaveReqVO → GroupBatchRequirementCheckRespVO`
#### 使用场景
「查看需求」Tab 进入时、以及点「整体确认」前调用,据 `ready` 置灰按钮、据 `missing` / `vehicleMissing` 展示缺哪几户。本批新增两个接送机相关字段。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| groupBatchId | Path | Long | ✅ | - | 团期聚合主键 |
无请求体。
#### 出参 `Result<GroupBatchRequirementCheckRespVO>`
| 字段 | 类型 | 说明 |
|------|------|------|
| ready / missing / vehicleMissing / vehicleWaived 等既有字段 | - | 本批不改 |
| **transferSubmitEnabled** | Boolean | **本批新增** 当前环境是否开放接送机需求提交(配置 `hl.order.requirement.transfer-kind-submit-enabled`) |
| **transferDeclaredWithoutRequirement** | Array | **本批新增** 在大交通声明了接送机、却没有活跃 TRANSFER 用车需求行的户;**提示性,不进 ready、不阻断确认** |
| 该数组项 .orderId | String | 子订单 ID(Long 序列化为字符串) |
| 该数组项 .orderNo | String | 子订单号 |
| 该数组项 .customerName | String | 客户姓名 |
| 该数组项 .consultantId | String | 定制师 adminId(Long 序列化为字符串);未指派为 null |
| 该数组项 .consultantName | String | 定制师姓名快照;未指派为 null |
| 该数组项 .pickupRequired | Boolean | 大交通里声明了接机/接站 |
| 该数组项 .dropoffRequired | Boolean | 大交通里声明了送机/送站 |
| 该数组项 .pickupRemark | String | 已声明批次上的文字备注(多条去重后以「;」连接);无备注为 null |
#### 请求示例
```http
GET /v3/admin/order/group-batch/2101506167098511362/requirement/confirm-check HTTP/1.1
Host: <网关域名>
Authorization: Bearer <token>
```
无请求体。
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"data": {
"groupBatchId": "2101506167098511362",
"batchStatus": "RESOURCE_PREPARING",
"batchStatusName": "资源准备中",
"ready": true,
"missing": [],
"checkedResourceTypes": ["HOTEL", "VEHICLE"],
"vehicleWaived": true,
"vehicleMissing": [],
"groupVehicleRequirementId": "1867000000101",
"groupVehicleRequirementStatus": "CONFIRMED",
"groupVehicleRequirementVersion": 4,
"transferSubmitEnabled": false,
"transferDeclaredWithoutRequirement": [
{
"orderId": "60123456789001",
"orderNo": "HL2606010001",
"customerName": "陈昊",
"consultantId": "10001",
"consultantName": "李四",
"pickupRequired": true,
"dropoffRequired": false,
"pickupRemark": "航班 CA1234,落地 14:20"
}
]
},
"success": true
}
```
上面这一例同时演示了两条边界:`vehicleWaived=true`(整团免车)**不**清空缺口名单;`transferSubmitEnabled=false` 时名单也照报。
#### 空数据 / 降级响应
无此类户时 `transferDeclaredWithoutRequirement` 是**空数组、不是 null**(null 与「一条都没有」在前端是两种渲染):
```json
{
"code": 200,
"data": { "transferSubmitEnabled": true, "transferDeclaredWithoutRequirement": [] },
"success": true
}
```
#### 错误响应
```json
{
"code": 403,
"message": "无权限访问",
"success": false,
"data": null
}
```
#### 业务边界
- 🔴 **`transferDeclaredWithoutRequirement` 不跟随 `vehicleWaived` 短路**:整团免车豁免的是**团级行程用车**,与「某户自己声明了要接送机」无关。所以团期标了免车,这个名单照样可能非空——**前端不要据 `vehicleWaived` 隐藏它**。
- 🔴 **`transferSubmitEnabled=false` 时名单同样照报**:缺口是事实,与能不能立刻补是两件事。此时前端应提示「当前环境未开放接送机需求提交」(定制师提交 `kind=TRANSFER` 会被 809004 拒),但**不要因此隐藏名单**——否则管理员会照着名单去催定制师,定制师撞一堵不会变的墙。
- 该名单是**提示**不是缺失:不进 `ready`、不阻断整团确认、也不并进 `vehicleMissing`(后者语义严格是「TRANSFER 行已存在但服务日未补录」)。混在一起的代价是一个误填会把整团确认卡死,而运营没有手段标记「确认过了、不用管」。
- 本字段只在 `confirm-check` 读口上有值;整团确认写口共用同一 VO,那条路径上它恒为**空数组**。
- 车侧新增的两个预检原因码会出现在 `vehicleMissing[].reason` 里:`GROUP_SPEC_INCOMPLETE`(对应 809118)与 `GROUP_SEATS_INSUFFICIENT`(对应 809116),`detail` 字段与整团确认时抛出的报文逐字相同,可直接展示。
---
## 四、契约约束与正确调用方式(接口类必写)
> 本节只写**后端接受/拒绝 payload 的规则**,不写 UI 渲染建议。
### ✅ 正确 / ❌ 错误 payload 对照(`PUT .../vehicle-requirement` 的分组元素)
| 场景 | payload 片段 |
|------|---------|
| ✅ 存量分组不填规格(两个都不传) | `{ "groupCode": "BUS", "seats": null, "count": null }` → 座位校验整体跳过 |
| ✅ 规格填全且坐得下 | `{ "groupCode": "BUS", "seats": 19, "count": 2 }`,该组最大单日人数 36 → 38 ≥ 36 通过 |
| ✅ 刚好坐满(含驾驶位) | `{ "groupCode": "SUV", "seats": 20, "count": 1 }`,最大单日人数 20 → 20 < 20 不成立,**通过**,回显 `remainingPassengerSeats: -1` |
| ❌ 只填座位数 | `{ "groupCode": "BUS", "seats": 19, "count": null }` → 809118 |
| ❌ 只填车辆数 | `{ "groupCode": "BUS", "seats": null, "count": 2 }` → 809118 |
| ❌ 座位不足 | `{ "groupCode": "BUS", "seats": 19, "count": 1 }`,最大单日人数 20 → 809116 |
| ❌ 座位数为 0 | `{ "groupCode": "BUS", "seats": 0, "count": 1 }` → 400(`@Min(1)`),不是 809116 |
| ❌ 字典外标签 | `{ "groupCode": "BUS", "specialTags": ["wheelchair_lift"] }`(字典无此项)→ 809117,整份零写入 |
### 切换状态时的必要动作
- 想把某组从「已填规格」改回「未填」,必须把 `seats` 与 `count` **同时显式传 null**,只清一个会撞 809118。
- 想清空某组特殊诉求,传 `"specialTags": []` 或 `null` 均可(后端归一处理);不要传 `[""]`(空白项会被丢弃,但不要依赖这个)。
- 编辑存量团期时,把 `GET` 拿到的 `seats` / `count`(可能是 null)**原样回传**即可,后端不会因为它们是 null 而拒绝。
---
## 五、数据库行为
只写外部可观察行为:
| 前端提交(分组元素) | 再次 `GET` 读到 |
|----------|------------------|
| `seats=19, count=2` | `seats: 19, count: 2, totalSeatCount: 38, remainingPassengerSeats: 38 − 2 − maxHeadcount` |
| `seats=null, count=null` | `seats: null, count: null, totalSeatCount: null, remainingPassengerSeats: null` |
| `specialTags=["child_seat","child_seat"," "]` | `specialTags: [{ "code": "child_seat", "name": "儿童安全座椅" }]`(trim + 丢空白 + 去重保序) |
| `remark="含高速费"` | `remark: "含高速费"` |
**存量分组的四个新字段没有默认值**:本批之前建的分组行读出来是 `seats: null` / `count: null` / `specialTags: []` / `remark: null`,**不是 0、不是空串**。前端渲染必须区分「未填」与「填了值」。
**整份替换语义不变**:保存成功后未出现在本次提交里的分组被移出当前版本,其规格字段随之不可见。
---
## 六、边界行为
- 未登录 → 401(网关拦截)。
- 无 `group-batch:view` 权限 → 403。
- 团期不存在 → 对应团期错误码,不 500。
- 车队服务 / 字典服务降级 → `vehicleTypeName`、`specialTags[].name` 为 `null`,其余字段照常返回,不 500 不阻断页面(**只读端点降级不回落成编码,避免前端把编码当中文展示**)。
- 老数据兼容 → 存量分组无规格字段 → `seats` / `count` / `remark` 为 null、`specialTags` 为 `[]`,不异常;存量团期打开编辑弹窗后原样保存不会被新校验拒绝。
- 生产环境:二期(order-v3 / fleet)尚未上线生产,本批路径在生产环境为 404。这是既有事实,不是本批引入的。
---
## 六.5、枚举 / 数据字典
### kind(用车需求类别)
**所属字段**: `GroupVehicleHouseholdsRespVO.households[].requirements[].kind`、以及 `vehicle-households` 的 query 参数 `kind` | **类型**: `String`
| 值 | 中文 | 说明 |
|----|------|------|
| `TRAVEL` | 行程用车 | 团期行程期间的用车需求;计入 `requirement-summary` 既有车侧字段 |
| `TRANSFER` | 接送机 | 接送机/接送站需求;计入 `transferSummary`,不进既有车侧字段 |
query 参数不传 = 两类都返。
### status(子订单用车需求行状态)
**所属字段**: `GroupVehicleHouseholdsRespVO.households[].requirements[].status` | **类型**: `String`
| 值 | 中文 | 说明 |
|----|------|------|
| `PENDING_REVIEW` | 待审核 | 定制师已提交,等团期侧核对 |
| `PENDING` | 待车队配 | 已进入处理队列 |
| `PROCESSING` | 配车中 | 车务作业中 |
| `DONE` | 配车完成 | 本需求行已闭环 |
被打回的需求行已失活,不出现在本端点的列表里;`statusName` 是车务口径中文名(见上表),`status` 为空时为 `null`,遇未知状态码时原样回落该 code 字符串。
### specialTags(字典 `vehicle_special_demand`)
**所属字段**: `GroupVehicleRequirementSaveReqVO.groups[].specialTags`(提交,`List<String>` 编码)/ `GroupVehicleRequirementRespVO.groups[].specialTags`(回显,`{code,name}`) | **类型**: `String` 编码
取值**由运营在字典里维护、后端按字典实际内容校验,代码里不硬编码枚举**——前端必须从字典下拉取值,不要自己写死一份列表。只放行字典里 ACTIVE 的项,含字典外编码整份拒绝(809117)。回显时 `name` 为 null 表示字典里查不到该编码(不回落成编码)。
### vehicleType(车型大类编码)
**所属字段**: `transferSummary.vehicleSeatSummary[].vehicleType`、`vehicle-households` 的 `fleet[].vehicleType` | **类型**: `String`
| 值 | 中文 | 说明 |
|----|------|------|
| `suv` | SUV系列 | 中文名取自车型管理库,后端下发 |
| `mpv` | 商务车 | 同上 |
| `bus` | 大巴 | 同上 |
| `sedan` | 轿车 | 同上 |
| `未知` | 未知 | 需求里车型编码缺失时的归并项(仅汇总类字段出现,台数不丢) |
注意团级 `GroupVehicleRequirementSaveReqVO.groups[].vehicleType` 是**自由文本/字典值、不设档位枚举**(例:`35座大巴`),与上表的大类编码不是同一个取值空间。
---
## 六.6、修改前后对比
### 字段级对比
| 字段 | 改前 | 改后 |
|------|------|------|
| `PUT .../vehicle-requirement` 请求 `groups[]` | groupId / groupCode / vehicleType / serviceStartDate / serviceEndDate / days | 额外接受 `seats`、`count`、`specialTags`、`remark`(均非必填) |
| 响应 `groups[]` | groupId / groupCode / vehicleType / 服务日 / days | 额外返回 `vehicleTypeName`、`seats`、`count`、`specialTags`、`remark`、`totalSeatCount`、`maxHeadcount`、`remainingPassengerSeats` |
| `requirement-summary` | 无接送机信息 | 新增 `transferSummary`(恒非 null),既有车侧字段口径不变 |
| `requirement/confirm-check` | 无接送机信息 | 新增 `transferSubmitEnabled`、`transferDeclaredWithoutRequirement`(空时为 `[]`) |
| `vehicleMissing[].reason` 取值 | 不含规格相关原因 | 新增 `GROUP_SPEC_INCOMPLETE`、`GROUP_SEATS_INSUFFICIENT` |
| 新端点 | 无 | `GET .../requirement/vehicle-households` |
### 行为级对比
| 行为 | 改前 | 改后 |
|------|------|------|
| 提交只填座位数或只填车辆数 | 接受(无此字段) | 809118 拒绝,整份零写入 |
| 提交坐不下的规格 | 接受(无此字段) | 809116 拒绝(判据不扣司机位) |
| 提交字典外特殊诉求标签 | 接受(无此字段) | 809117 拒绝,事务外先校验,零写入 |
| 存量团期原样保存 | 通过 | **仍通过**(四个新字段非必填,null 时校验整体跳过) |
| 整团免车(`vehicleWaived=true`)时的接送机缺口 | 无此名单 | 名单**照报**,不随免车短路 |
---
## 六.7、影响评估
- **是否破坏向后兼容**: 否。四个新请求字段全部非必填,不传即维持原行为;新增响应字段是增量。
- **前端是否必须同步上线**: 是——新增端点与新增字段需要前端渲染;不改前端时页面仍可用,但团级分组无法填写车辆规格、看不到子订单用车记录与接送机汇总。
- **前端 workaround 清理点**:
- 若此前在前端自行维护过一份 `vehicle_special_demand` 标签中文映射表,可撤掉,改用后端下发的 `specialTags[].name`。
- 若此前在前端自行维护过车型编码→中文的映射,可撤掉,改用 `vehicleTypeName`。
- 若此前把 `remainingPassengerSeats` 之类余座数做过 `Math.max(0, x)` 处理,**必须撤掉**——负值是有效信号。
- 同理,**不要新增「余座为负则禁止保存」的前端校验**:那会把后端放行的组合在 UI 上锁死(见「关键变化」提交侧那条)。
---
## 七、不影响范围(显式声明, 帮前端/QA 缩小排查面)
- **仅影响**: 管理后台「团期详情 → 查看需求」Tab 的用车板块(团级正式用车需求、子订单用车需求记录、全团汇总、整体确认预检)。
- **零影响**:
- `requirement-summary` 的用房侧全部字段(`dailyRoomBreakdown`、`hotelNeededOrderCount` 等)与 `hotel-households` 端点。
- `requirement-summary` 既有车侧字段 `vehicleRequirementCount` / `vehicleSeatSummary` 的口径(仍只统计行程用车)。
- 子订单侧用车需求提交端点的字段与校验(本批不改定制师提交侧)。
- `POST .../requirement/confirm`(整团确认写口)的判决逻辑:新增的接送机缺口名单不进 `ready`、不阻断确认。
- `POST .../vehicle-requirement/withdraw` / `waive` / `reopen` 三个写端点的语义(它们与 PUT/GET 共用响应体,因此同样多出 8 个字段,但行为不变)。
- 历史数据:存量分组不迁移,四个新字段保持 null,下次编辑保存时才可能写入。
---
## 八、测试环境已验证
**网关路由**:本批零网关改动。`/v3/admin/**` 通配已配在 `hl-gateway` 的 `order-service-v3` 路由上,新增的 `.../requirement/vehicle-households` 落在既有通配内,无新增 `/admin/` 前缀需要配路由(故 `gateway_status: not_required`)。
**自动化回归**(分支 `feat/8152-group-vehicle-structured`,本批新增/改写的用例类):
```
GroupVehicleRequirementSaveTest 保存链路(含四个新字段落库与回显) ✓
GroupVehicleRequirementValidateTest 809116 / 809117 / 809118 三条校验 ✓
VehicleSeatCalculatorTest 余座口径(扣司机位、允许为负) ✓
GroupBatchVehicleHouseholdServiceTest vehicle-households 户数/行数/kind 过滤 ✓
GroupBatchRequirementSummaryTest transferSummary 聚合与空团空壳 ✓
GroupVehicleRequirementConfirmCheckTest transferSubmitEnabled / 缺口名单不短路 ✓
TransferDeclarationSupportTest 大交通声明 → 缺口名单推导 ✓
RequirementServiceVehicleKindsQueryTest TRAVEL / TRANSFER 分类取数 ✓
GroupDispatchCoverageCalculatorTest(fleet) 团级规格进配车覆盖度基线 ✓
RedLineArchTest 架构红线门禁 ✓
```
**数据库变更**:`V20260922_401__order_group_vehicle_group_add_fleet_fields.sql`,四列全部可空无默认值(存量行保持 null,见「五、数据库行为」)。
**活体实测**(测试服网关 `https://api.test.1814.love:9443`,2026-09-22 13:0x–13:1x;`hl-order-service-v3` @ `f1986f996`,`hl-fleet-service` @ `c4321f961`)。取一个带真实接送机需求的团期(3 户,其中 pickup 2 户、dropoff 1 户,合计 6 人,车型汇总 suv / 15 座 / 3 辆)逐个请求:
| 端点 | HTTP | 业务 code | 实测要点 |
|---|---|---|---|
| `GET .../vehicle-requirement` | 200 | 200 | 分组元素 8 个新字段全部出现:`vehicleTypeName` `seats` `count` `specialTags` `remark` `totalSeatCount` `maxHeadcount` `remainingPassengerSeats`。该团期分组属存量数据,除 `maxHeadcount` = 6 外新字段为 null / 空数组——**存量行的 GET 侧兼容已实证** |
| `GET .../requirement/vehicle-households` | 200 | 200 | `householdCount` = 3、`vehicleRowCount` = 3、`countedHouseholdCount` = 0、`households[]` 返回 3 条真实行(含 `kind` / `status` / `fleet` / `serviceDates`) |
| 同端点 `?kind=TRANSFER` | 200 | 200 | 过滤参数被接受,返回同 3 条(均为 TRANSFER),无报错 |
| `GET .../requirement-summary` | 200 | 200 | `transferSummary` 节点出现,6 个子字段全部有值:`householdCount` = 3 / `pickupHouseholdCount` = 2 / `dropoffHouseholdCount` = 1 / `headcount` = 6 / `vehicleSeatSummary[]` = 1 条 / `serviceDates` = `["2026-10-30"]` |
| `GET .../requirement/confirm-check` | 200 | 200 | `transferSubmitEnabled` = true(Boolean)、`transferDeclaredWithoutRequirement` = `[]`(数组) |
⚠️ **上表最后一行的空数组是阴性对照,不是「该字段恒为空」**:该团期的 3 户**全都有** TRANSFER 需求行,本就不该进缺口名单。字段的类型与位置已固定,按契约对接即可;名单非空时的元素结构见本篇「三、接口详情」第 5 节的字段表。
---
## 十、相关文档
- 关联 Issue: [wx/HL#8152](https://git.1814.love:8443/wx/HL/issues/8152)、[wx/HL#8151](https://git.1814.love:8443/wx/HL/issues/8151)、[wx/HL#8153](https://git.1814.love:8443/wx/HL/issues/8153)
- 同批交接件: `changelogs-v2/2026-09/22_8154_团期管理员只读查看子订单与金额面端点收回-修复-管理后台.md`(团期管理员权限行为变更)
- 既有对位端点(形态参考): `changelogs-v2/2026-09/20_8046_团期用房新增子订单订房记录接口-新增接口-管理后台.md`
## 关联 / 联系人
### 链接
- **Issue**: [#8152](https://git.1814.love:8443/wx/HL/issues/8152) / [#8151](https://git.1814.love:8443/wx/HL/issues/8151) / [#8153](https://git.1814.love:8443/wx/HL/issues/8153)
- **分支**: `feat/8152-group-vehicle-structured`
### 联系人
- **后端负责人**: @wx