--- 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<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` | 字段 | 类型 | 说明 | |------|------|------| | 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<SpecialTagItem> | **本批新增** 特殊诉求标签(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<String> | 成员子订单集合(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` | 字段 | 类型 | 说明 | |------|------|------| | data | Object | 结构与第 1 节「出参」逐字段相同;该团尚未形成正式用车需求时为 `null` | | data.groups[].seats / count | Integer | 本批新增;存量分组为 null | | data.groups[].specialTags | Array<SpecialTagItem> | 本批新增;存量分组为空数组 | | 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 ``` 无请求体。 #### 响应示例 ```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` | 字段 | 类型 | 说明 | |------|------|------| | 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<FleetItem> | 车队明细,定制师所报原貌,不做合并 | | requirements[].specialTags | Array<SpecialTagItem> | 特殊诉求标签;未填为空列表 | | requirements[].remark | String | 备注 / 其他诉求,≤500;未填为 null | | requirements[].serviceDates | Array<LocalDate> | 本需求冻结的服务日期;未冻结时为空列表 | | 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 ``` 无请求体。不传 `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` | 字段 | 类型 | 说明 | |------|------|------| | 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<TransferSeatItem> | 按车型聚合的接送机座位/台数 | | transferSummary.serviceDates | Array<LocalDate> | 接送机涉及的服务日期去重集合(升序);未冻结服务日的需求不贡献日期 | | 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 ``` 无请求体。 #### 响应示例 ```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` | 字段 | 类型 | 说明 | |------|------|------| | 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 ``` 无请求体。 #### 响应示例 ```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` 编码)/ `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