--- schema: "hl-changelog/v2" ticket: "8577" title: "只提交了接送机的户不再被判「未提交用车需求」,809121/809122/809123 触发条件收窄且文案改写" 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 #8600 已 squash 合并 dev-v3(5b7074e691),hl-order-service-v3 dev-v3 分支已滚测试服。三个端点的请求体、响应体字段与错误码号全部未变,变的是 809121/809122/809123 的触发条件(收窄)与消息文案(去掉「行程」二字)。" updated_at: "2026-09-30" base: "dev-v3" --- # hl-order-service-v3: 只提交了接送机的户不再被判「未提交用车需求」 > **存放目录**: `changelogs-v2/2026-09/` > **服务**: hl-order-service-v3 (端口 8086) > **PR**: #8600 > **Issue**: #8577 > **日期**: 2026-09-30 > **影响范围**: 团期需求管理 Tab 的三个端点(保存正式用车需求 / 自动汇总草稿 / 整体确认预检)里 809121、809122、809123 的触发条件与消息文案 --- ## ⚠️ 关键变化 - 🔴 **判据从「有没有提交行程用车(TRAVEL)」收窄为「两类用车需求(TRAVEL / TRANSFER)是不是一条都没有」**。改前:某户只提交了接送机需求,团级保存、自动汇总、确认预检都把它当成「一条都没交」,整团被 809123 / 809121 / 809122 卡住,而这户其实已经明确表达过「只要接送机、不要行程车」,运营**没有任何干净出路**(唯一逃生舱是整团 waive 免车,那会把真需要行程车的户一起免掉)。改后:这户算已提交,三处一律放行。 - **三个错误码的码值没变、字段没变**,变的是**什么时候抛**(收窄)与**消息文案**(三条都去掉了「行程」二字): - `809121` `团期 {0} 有 {1} 户缺少可汇总的行程用车需求…` → `…缺少可汇总的用车需求…` - `809122` `该户尚未提交行程用车需求,请先让定制师提交后再整团提交车务` → `该户尚未提交用车需求,…` - `809123` `{0}有 {1} 户尚未提交行程用车需求,暂不能保存正式用车需求:{2}` → `…尚未提交用车需求,…` - 🔴 **前端凡是对这三条报文做过关键词匹配 / 字符串包含判断的地方必须改**(`行程用车需求` 这个子串在三条里都没了)。正确做法是按 `code` 分支,不要匹配 `message` 文本。 - 🔴 **809109「逐日覆盖」一个字都没改,仍然只认 TRAVEL**。这是刻意的:本次分离的是「户级提交判定」与「行程覆盖判定」两件事,合并会把墙从 809123 挪到 809109,症状一模一样只是换个码。所以——**只提交接送机的户不再被判未提交,也不要求被任何乘车分组覆盖**;它结构上就在团级乘车分组之外,走逐户派车。 - `GroupVehicleDraftAggregator` 的缺失原因文案 `未提交行程用车需求` → `未提交用车需求`。它出现在 809121 报文的逐户清单里(`「户标识:原因」`,顿号分隔),前端若展示过这个字符串同样受影响。 - 「豁免户」`exemptHouseholds` 的语义边界也随之明确:**只提交了接送机的户既不进未提交名单、也不进豁免名单**——豁免解释的是「没提交的户为什么不拦」,而它本来就提交过。 --- ## 一、背景 一户在团期里的用车需求有两类活跃行,互不替代: | 类别 | 含义 | 派车路径 | |------|------|----------| | `TRAVEL` | 团期行程用车 | 汇总进团级乘车分组,整团逐日配车 | | `TRANSFER` | 接送机 | 逐户派车,**结构上不进团级乘车分组** | 改前的三处判定都只查 `TRAVEL`。于是「只要接送机、不要行程车」这种完全合法的在团户(与 #7972 (A) 对 809114 的定案同源)被读成「什么都没交」。团级保存直接 809123 整份拒绝、自动汇总 809121 整团出不来草稿、确认预检 809122 逐户挂红——**运营改不动、催不动(该户定制师已经交过了)、也绕不过去**。 本次把判定拆成两个集合(`travelSubmittedOrderIds` / `anySubmittedOrderIds`),单源仍只有一份,在 `GroupVehicleRequirementService#classifyVehicleSubmission`(原名 `classifyTravelSubmission`),保存、预检、自动汇总三处共用: - **户级「交了没有」** → 用 `anySubmittedOrderIds`(两类任一即算交了)→ 管 809121 / 809122 / 809123; - **行程逐日覆盖** → 仍用 `travelSubmittedOrderIds`(只认 TRAVEL)→ 管 809109。 --- ## 二、变更接口清单 | # | 接口 | 方法 | 路径 | 变更类型 | 说明 | |---|------|------|------|----------|------| | 1 | 保存团期正式用车需求(全量替换) | PUT | `/v3/admin/order/group-batch/{groupBatchId}/vehicle-requirement` | 错误码触发条件收窄 + 文案改写 | 809123 不再对「只提交接送机」的户触发;报文去掉「行程」 | | 2 | 自动汇总正式用车需求草稿 | GET | `/v3/admin/order/group-batch/{groupBatchId}/vehicle-requirement/aggregate-draft` | 错误码触发条件收窄 + 文案改写 | 809121 同上;缺失原因文案同步改写 | | 3 | 整体确认需求缺失预检 | GET | `/v3/admin/order/group-batch/{groupBatchId}/requirement/confirm-check` | 缺失项触发条件收窄 + 文案改写 | `HOUSEHOLD_REQUIREMENT_NOT_SUBMITTED`(809122)同上 | --- ## 三、接口详情 ### 1. 保存团期正式用车需求(全量替换) `PUT /v3/admin/order/group-batch/{groupBatchId}/vehicle-requirement` **VO**: `GroupVehicleRequirementSaveReqVO → GroupVehicleRequirementRespVO` #### 使用场景 团期需求管理 Tab 的「正式用车需求」编辑弹窗点保存时调用,**整份全量替换**(未出现在本次提交里的分组会被移出当前版本)。权限点 `group-batch:demand:confirm`。本次改动只让 809123 少抛一类情况、并改了它的报文,请求体与响应体一个字段都没动。 #### 入参 | 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | |------|------|------|------|------|------| | `groupBatchId` | Path | Long | ✅ | - | 团期 ID | | `version` | Body | Integer | ❌ | 乐观锁 | **首次保存传 null**,后续必须回传上次 GET / PUT 拿到的值;不一致抛 809102 | | `remark` | Body | String | ❌ | `@Size(max=500)` | 整份需求备注 | | `groups` | Body | Array | ✅ | `@NotNull`(**不是** `@NotEmpty`)、`@Valid` | 全部乘车分组;空数组是合法提交(有需车户时由 809103 拦),整团免车请改走 `waive` 端点 | | `groups[].groupId` | Body | Long | ❌ | - | 既有分组主键;**新增分组传 null**。带上它 = 声明「就是库里那一组」,此时 `groupCode` 不得变更(改名抛 809104) | | `groups[].groupCode` | Body | String | ✅ | `@NotBlank`,`@Size(max=32)` | 分组键,直接作为车费 `alloc_group` | | `groups[].vehicleType` | Body | String | ✅ | `@NotBlank`,`@Size(max=64)` | 车型大类编码,**不是自由文本**;取值权威见 `GET /internal/fleet/vehicle-types/category-names`,不在字典内抛 809119 | | `groups[].serviceStartDate` | Body | String(`yyyy-MM-dd`) | ✅ | `@NotNull` | 本组服务开始日 | | `groups[].serviceEndDate` | Body | String(`yyyy-MM-dd`) | ✅ | `@NotNull` | 本组服务结束日(须不早于开始日) | | `groups[].seats` | Body | Integer | ❌ | `@Min(1)` | 该组单车座位数;**刻意非必填**(存量分组没有该值),与 `count` 必须同填或同空(809118),且须在该车型可选档位内(809124) | | `groups[].count` | Body | Integer | ❌ | `@Min(1)` | 该组车辆数量;同上 | | `groups[].specialTags` | Body | Array\ | ❌ | 值须在字典 `vehicle_special_demand` 内 | 特殊诉求标签编码数组;含字典外编码整份拒绝(809117) | | `groups[].remark` | Body | String | ❌ | `@Size(max=500)` | 该组备注 | | `groups[].days` | Body | Array | ✅ | `@NotEmpty`,`@Valid` | 逐日用车人数与成员,**不能用单值人数代替** | | `groups[].days[].tripDate` | Body | String(`yyyy-MM-dd`) | ✅ | `@NotNull` | 团期行程日,须落在本组服务日范围内且不缺日(809105 / 809106) | | `groups[].days[].headcount` | Body | Integer | ✅ | `@NotNull`,`@Min(1)` | 该组该日**乘车人数**(不是户数);小于当日成员户数抛 809110 | | `groups[].days[].memberOrderIds` | Body | Array\ | ✅ | `@NotEmpty` | 该组该日实际乘车的子订单集合,须全属本团在团户(809107),同一户同一日只能属一个分组(809108) | #### 出参 `Result` | 字段 | 类型 | 说明 | |------|------|------| | `requirementId` | String | 正式用车需求 ID(雪花,字符串) | | `groupBatchId` | String | 团期 ID(雪花,字符串) | | `status` | String | 需求状态 | | `version` | Integer | 乐观锁版本,下次保存必须回传 | | `remark` | String | 整份需求备注 | | `confirmedBy` | String | 确认人 | | `confirmedAt` | String(datetime) | 确认时间 | | `planRefreshState` | String | 配车刷新状态(只读投影) | | `planRefreshReplayCount` | Integer | 配车刷新重投次数 | | `blockedStage` | String | 被卡住的阶段 | | `planRefreshStalled` | Boolean | 配车刷新是否已停滞 | | `planRefreshStalledReason` | String | 停滞原因 | | `planRefreshTimeoutAt` | String(datetime) | 刷新超时时刻 | | `planRefreshReplayExhausted` | Boolean | 重投次数是否已用尽 | | `groups` | Array | 乘车分组回显 | | `groups[].groupId` / `groupCode` / `vehicleType` / `vehicleTypeName` | String | 分组主键(字符串)、分组键、车型大类编码、车型中文名(按归一 key 取) | | `groups[].serviceStartDate` / `serviceEndDate` | String(`yyyy-MM-dd`) | 本组服务日范围 | | `groups[].seats` / `count` / `totalSeatCount` / `maxHeadcount` / `remainingPassengerSeats` | Integer | 单车座位数 / 车辆数 / 总座位 / 最大日人数 / 剩余可载客座位 | | `groups[].specialTags[]` | Array | `code` + `name`(中文名后端下发,前端不自己映射) | | `groups[].remark` | String | 该组备注 | | `groups[].days[]` | Array | `tripDate` / `headcount` / `memberOrderIds`(字符串数组) / `memberOrderCount` | | `exemptHouseholds` | Array | 豁免户(在团需车、两类需求都没有活跃行、但定制师**提交不了**的户);🔴 **只提交了接送机的户不在这里**——它已提交 | | `exemptHouseholds[].orderId` | String | 子订单 ID(雪花,字符串) | | `exemptHouseholds[].teamNo` | String | 团号 | | `exemptHouseholds[].orderNo` | String | 子订单号 | | `exemptHouseholds[].reason` | String | `ORDER_NOT_CUSTOMIZING` / `REQUIREMENT_FROZEN` | | `exemptHouseholds[].reasonName` | String | 豁免原因中文名(后端下发,前端不自己映射) | #### 请求示例 ```json { "version": 3, "remark": "9/13 起换大巴", "groups": [ { "groupId": null, "groupCode": "BUS", "vehicleType": "bus", "serviceStartDate": "2026-09-12", "serviceEndDate": "2026-09-16", "seats": 19, "count": 1, "specialTags": ["CHILD_SEAT"], "remark": "含高速费", "days": [ { "tripDate": "2026-09-12", "headcount": 9, "memberOrderIds": [2099459272533323777, 2099459272533323778] } ] } ] } ``` #### 响应示例 ```json { "code": 200, "message": "成功", "success": true, "data": { "requirementId": "2099459272533400001", "groupBatchId": "2099459272533000001", "status": "DRAFT", "version": 4, "remark": "9/13 起换大巴", "confirmedBy": null, "confirmedAt": null, "planRefreshState": null, "planRefreshStalled": false, "groups": [ { "groupId": "1867000000009", "groupCode": "BUS", "vehicleType": "bus", "vehicleTypeName": "大巴客车", "serviceStartDate": "2026-09-12", "serviceEndDate": "2026-09-16", "seats": 19, "count": 1, "totalSeatCount": 19, "maxHeadcount": 9, "remainingPassengerSeats": 9, "specialTags": [{ "code": "CHILD_SEAT", "name": "儿童座椅" }], "remark": "含高速费", "days": [ { "tripDate": "2026-09-12", "headcount": 9, "memberOrderIds": ["2099459272533323777", "2099459272533323778"], "memberOrderCount": 2 } ] } ], "exemptHouseholds": [] } } ``` #### 空数据 / 降级响应 该团期**只有接送机户、没有任何行程用车户**时,提交零分组不再被 809123 拦(本次改动的直接效果);若团里确实还有需车户,零分组仍由 809103 拦下。`exemptHouseholds` 为空时是**空数组**不是 `null`: ```json { "code": 200, "message": "成功", "success": true, "data": { "requirementId": "2099459272533400001", "groupBatchId": "2099459272533000001", "status": "DRAFT", "version": 1, "groups": [], "exemptHouseholds": [] } } ``` #### 错误响应 809123(触发条件已收窄、文案已改写;`{0}` 是团期人话标识,`{2}` 按团号列户、无团号回落订单号、都缺时为「某子订单」,顿号分隔): ```json { "code": 809123, "message": "团期「第3期 10月8日出发团」有 2 户尚未提交用车需求,暂不能保存正式用车需求:26-0480、26-0481", "success": false, "data": null } ``` 其余错误码一条都没变:809100 团期尚未形成正式用车需求 / 809101 状态不允许 / 809102 已被他人修改(乐观锁) / 809103 有需车户却零分组 / 809104 分组重复或试图改名 / 809105 逐日行不在本组服务日范围内或重复 / 809106 缺逐日用车人数 / 809107 成员不属于本团期 / 809108 同一户同一日属多个分组 / 809109 该子订单的某日没有被任何乘车分组覆盖(**仍只认 TRAVEL**) / 809110 用车人数小于当日成员户数 / 809111 团期状态不允许编辑 / 809115 已声明整团免车需先 withdraw / 809116 座位不足 / 809117 特殊诉求标签不在字典内 / 809118 座位数与车辆数须同填或同空 / 809119 车型不在车型字典内 / 809120 车型字典暂不可用 / 809124 座位数不在该车型可选档位内。 #### 业务边界 - **鉴权**:权限点 `group-batch:demand:confirm`(与整体确认、按户打回、受控重开同码——它们动的是同一个 Tab 里的同一份数据);未登录由网关拦截返 401。 - **全量替换语义**:未出现在本次提交里的分组会被移出当前版本,不是增量补丁。 - **🔴 判据变化只在户级**:「这户交了没有」看两类任一;「行程逐日覆盖」(809109)仍只看 TRAVEL,没变。 - **只提交接送机的户**:不再被 809123 拦、**也不要求被任何乘车分组覆盖**,且**不出现在 `exemptHouseholds` 里**。 - **豁免户不阻断**:`ORDER_NOT_CUSTOMIZING` / `REQUIREMENT_FROZEN` 两类户不进 809123、不参与 809109,但必须在页面上提示出来(后端已逐户带原因下发)。 - **错误码文案是可变的**:`message` 只用于展示,判定一律按 `code`。 - **乐观锁只挡同一瞬间的并发写**:挡不住「A 读了 v3 去改、B 也读了 v3 改完先提交」这种跨请求覆盖。 - **雪花 ID 一律是字符串**(`requirementId` / `groupBatchId` / `memberOrderIds[]` / `exemptHouseholds[].orderId`)。 --- ### 2. 自动汇总正式用车需求草稿 `GET /v3/admin/order/group-batch/{groupBatchId}/vehicle-requirement/aggregate-draft` **VO**: `无请求体 → GroupVehicleAggregateDraftRespVO` #### 使用场景 编辑弹窗点「自动汇总」时调用,按各子订单的活跃 TRAVEL 需求汇总出一份团级草稿,**只读零写入**,返回的 `draft` 可原样 PUT 给上面那个保存端点。权限点与编辑弹窗取数口同码 `group-batch:demand:confirm`。本次改动只让 809121 少抛一类情况并改了它的报文与缺失原因文案。 #### 入参 | 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | |------|------|------|------|------|------| | `groupBatchId` | Path | Long | ✅ | - | 团期 ID | #### 出参 `Result` | 字段 | 类型 | 说明 | |------|------|------| | `groupBatchId` | String | 团期 ID(雪花,字符串) | | `currentStatus` | String | 当前正式需求状态 | | `draft` | Object | 汇总出的草稿,结构**与保存端点的请求体逐字段相同**,可原样 PUT | | `droppedFleetItems` | Array | 多车型户被丢弃的车型项:`orderId` / `teamNo` / `orderNo` / `vehicleType` / `seats` / `count` / `keptVehicleType` / `reason`(`VEHICLE_TYPE_NOT_IN_DICT` 等) | | `staleHeadcountOrders` | Array | 冻结人数与实时人数不一致的户:`orderId` / `teamNo` / `orderNo` / `frozenHeadcount` / `liveHeadcount` | | `paddedOrderDays` | Array | 为覆盖出发~返回而补进分组的日期:`orderId` / `teamNo` / `orderNo` / `dates[]` | | `seatOptionAdjusted` | Array | 座位档被兜底调整的组/户:`groupCode` / `orderId` / `teamNo` / `orderNo` / `vehicleType` / `originalSeats` / `adoptedSeats` / `seatOptions[]` / `reason` | | `violations` | Array | 草稿已先跑过与保存同一份逐日校验的结果:`code`(对应 809xxx) / `reason` / `detail` / `groupCode` / `tripDate` / `orderId` / `teamNo` | | `exemptHouseholds` | Array | 豁免户(结构同上一个端点);🔴 **只提交了接送机的户不在这里,也不在草稿里** | #### 请求示例 ```http GET /v3/admin/order/group-batch/2099459272533000001/vehicle-requirement/aggregate-draft ``` #### 响应示例 ```json { "code": 200, "message": "成功", "success": true, "data": { "groupBatchId": "2099459272533000001", "currentStatus": "DRAFT", "draft": { "version": 3, "remark": null, "groups": [ { "groupId": null, "groupCode": "BUS", "vehicleType": "bus", "serviceStartDate": "2026-09-12", "serviceEndDate": "2026-09-16", "seats": 19, "count": 1, "specialTags": [], "remark": null, "days": [ { "tripDate": "2026-09-12", "headcount": 9, "memberOrderIds": [2099459272533323777] } ] } ] }, "droppedFleetItems": [], "staleHeadcountOrders": [], "paddedOrderDays": [], "seatOptionAdjusted": [], "violations": [], "exemptHouseholds": [] } } ``` #### 空数据 / 降级响应 团里只有接送机户、没有任何可汇总的行程用车户时,草稿分组为空数组而**不再抛 809121**(本次改动的直接效果): ```json { "code": 200, "message": "成功", "success": true, "data": { "groupBatchId": "2099459272533000001", "currentStatus": "DRAFT", "draft": { "version": null, "remark": null, "groups": [] }, "droppedFleetItems": [], "staleHeadcountOrders": [], "paddedOrderDays": [], "seatOptionAdjusted": [], "violations": [], "exemptHouseholds": [] } } ``` 车型字典取不到时不静默降级,抛 809120 让运营重试(避免把一整份草稿的车型全判成非法)。 #### 错误响应 809121(触发条件已收窄、文案已改写;`{0}` 是团期名标识,`{2}` 是「户标识:原因」顿号分隔的清单,原因文案里的 `未提交行程用车需求` 已改为 `未提交用车需求`): ```json { "code": 809121, "message": "团期 「第3期 10月8日出发团」 有 1 户缺少可汇总的用车需求,暂不能自动汇总:26-0480:未提交用车需求", "success": false, "data": null } ``` 其余错误码未变:809120 车队车型字典暂不可用 / 809111 团期状态不允许 / 809100 团期尚未形成正式用车需求(视链路)。 #### 业务边界 - **鉴权**:权限点 `group-batch:demand:confirm`;未登录由网关拦截返 401。 - **⛔ 本端点零写入**,可安全重复调用;`draft` 是「按现有子订单需求草稿长什么样」,**不保证保存一定能过**——预跑的校验结果在 `violations`。 - **收窄后的 809121 判据**:需车户「两类用车需求一条都没有」才算信息缺失;只提交接送机的户不算缺少,**也不会出现在草稿里**(团车草稿只汇总 TRAVEL,它本就没有位置)。 - **缺失原因文案已改**:`未提交行程用车需求` → `未提交用车需求`(另有 `车型均不在车型字典内`、服务日推不出、人数为 0 三类未变)。 - **诊断字段必须展示**:`droppedFleetItems` / `staleHeadcountOrders` / `paddedOrderDays` / `seatOptionAdjusted` 都是「草稿与用户预期可能不一致」的位置,静默吞掉会让运营看到一份自己没想要的草稿。 - **雪花 ID 一律是字符串**。 --- ### 3. 整体确认需求缺失预检 `GET /v3/admin/order/group-batch/{groupBatchId}/requirement/confirm-check` **VO**: `无请求体 → GroupBatchRequirementCheckRespVO` #### 使用场景 「查看需求」Tab 进入时与点「确认」前调用,据 `ready` 置灰确认按钮、据 `missing` / `vehicleMissing` 展示缺哪几户。**只读无副作用**。权限点 `group-batch:demand:confirm`。本次改动只让缺失项 `HOUSEHOLD_REQUIREMENT_NOT_SUBMITTED`(809122)少产出一类情况并改了它的报文。 #### 入参 | 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | |------|------|------|------|------|------| | `groupBatchId` | Path | Long | ✅ | - | 团期 ID | #### 出参 `Result` | 字段 | 类型 | 说明 | |------|------|------| | `groupBatchId` | String | 团期 ID(雪花,字符串) | | `batchStatus` / `batchStatusName` | String | 团期状态编码与中文名 | | `ready` | Boolean | 是否可以整体确认(置灰按钮用) | | `missing` | Array | 房侧缺失户:`orderId` / `teamNo` / `orderNo` / `customerName` / `consultantId` / `consultantName` / `reason` / `reasonName` / `dayNumber` / `segmentIndex` / `expectedNights` / `actualNights` | | `checkedResourceTypes` | Array\ | 恒为 `["HOTEL","VEHICLE"]`;文案已更新为「车侧逐户查**用车需求行**是否提交(#8577 起行程用车与接送机任一有即算已提交)」 | | `vehicleWaived` | Boolean | 是否已声明整团免车 | | `vehicleMissing` | Array | 车侧缺失项,见下 | | `vehicleMissing[].reason` | String | `GROUP_REQUIREMENT_NOT_FOUND` / `GROUP_REQUIREMENT_STATUS_INVALID` / `NO_GROUP` / `GROUP_CODE_INVALID` / `DAY_OUT_OF_GROUP_RANGE` / `DAY_GAP_IN_GROUP_RANGE` / `MEMBER_FOREIGN_ORDER` / `MEMBER_DUPLICATE_DAY` / `ORDER_DAY_UNCOVERED` / `HEADCOUNT_LESS_THAN_MEMBERS` / **`HOUSEHOLD_REQUIREMENT_NOT_SUBMITTED`** / `MEMBER_GROUP_MISMATCH` / `TRANSFER_SERVICE_DATES_NOT_BACKFILLED` / `TRANSFER_WINDOW_INCOMPLETE` | | `vehicleMissing[].groupCode` | String | 涉及的乘车分组编码;无分组维度时 null | | `vehicleMissing[].tripDate` | String(`yyyy-MM-dd`) | 涉及的日期;无日期维度时 null | | `vehicleMissing[].orderId` | String | 涉及的子订单 ID(雪花,字符串);无订单维度时 null | | `vehicleMissing[].teamNo` / `orderNo` | String | 团号 / 子订单号快照 | | `vehicleMissing[].detail` | String | 人话描述,**与整团确认时抛出的错误报文逐字相同**,可直接展示 | | `vehicleExemptHouseholds` | Array | 车侧豁免户(结构同前两个端点);🔴 **只提交了接送机的户不在这里** | | `groupVehicleRequirementId` | String | 团级正式用车需求 ID(雪花,字符串) | | `groupVehicleRequirementStatus` | String | 团级正式用车需求状态 | | `groupVehicleRequirementVersion` | Integer | 团级正式用车需求版本 | | `transferSubmitEnabled` | Boolean | 接送机提交灰度开关当前状态 | | `transferDeclaredWithoutRequirement` | Array | 声明了接送机却没有活跃 TRANSFER 行的户:`orderId` / `teamNo` / `orderNo` / `customerName` / `consultantId` / `consultantName` / `pickupRequired` / `dropoffRequired` / `pickupRemark` | #### 请求示例 ```http GET /v3/admin/order/group-batch/2099459272533000001/requirement/confirm-check ``` #### 响应示例 ```json { "code": 200, "message": "成功", "success": true, "data": { "groupBatchId": "2099459272533000001", "batchStatus": "RESOURCE_PREPARING", "batchStatusName": "资源准备中", "ready": false, "missing": [], "checkedResourceTypes": ["HOTEL", "VEHICLE"], "vehicleWaived": false, "vehicleMissing": [ { "reason": "HOUSEHOLD_REQUIREMENT_NOT_SUBMITTED", "groupCode": null, "tripDate": null, "orderId": "2099459272533323779", "teamNo": "26-0482", "orderNo": "HL2606010003", "detail": "该户尚未提交用车需求,请先让定制师提交后再整团提交车务" } ], "vehicleExemptHouseholds": [], "groupVehicleRequirementId": "2099459272533400001", "groupVehicleRequirementStatus": "DRAFT", "groupVehicleRequirementVersion": 4, "transferSubmitEnabled": true, "transferDeclaredWithoutRequirement": [] } } ``` #### 空数据 / 降级响应 全部就绪时 `ready=true`,三个清单都是**空数组**不是 `null`: ```json { "code": 200, "message": "成功", "success": true, "data": { "groupBatchId": "2099459272533000001", "batchStatus": "RESOURCE_PREPARING", "batchStatusName": "资源准备中", "ready": true, "missing": [], "checkedResourceTypes": ["HOTEL", "VEHICLE"], "vehicleWaived": false, "vehicleMissing": [], "vehicleExemptHouseholds": [], "transferDeclaredWithoutRequirement": [] } } ``` #### 错误响应 本端点是只读预检,把缺失**列成清单**而不是抛码;仍可能出现的错误只有权限与团期不存在两类: ```json { "code": 403, "message": "无权限执行该操作", "success": false, "data": null } ``` #### 业务边界 - **鉴权**:权限点 `group-batch:demand:confirm`;未登录由网关拦截返 401。 - **⛔ 只读无副作用**,可随页面进入反复调用。 - **它是缺失明细的唯一来源**:整团确认失败时抛出的 589533 只带汇总户数,逐户明细只能从本端点取。 - **收窄后的 `HOUSEHOLD_REQUIREMENT_NOT_SUBMITTED` 判据**:在团需车户**两类用车需求都没提交**才产出;只提交接送机的户不产出,**也不进 `vehicleExemptHouseholds`**。 - **`detail` 与错误报文逐字相同**:所以它也跟着改了文案(`行程用车需求` → `用车需求`),前端不要做子串匹配。 - **`ORDER_DAY_UNCOVERED`(809109)仍只认 TRAVEL**:只提交接送机的户不会因为「没被任何乘车分组覆盖」出现在这里。 - **`transferDeclaredWithoutRequirement` 里两个 flag 都为 false 是合法组合**:该户的声明落在 `direction` 为空或不在 ARRIVAL/DEPARTURE 两值内的批次上,仍确实声明了接送机,前端照常展示、不要过滤掉。 - **雪花 ID 一律是字符串**。 --- ## 四、契约约束与正确调用方式 > 本节只写后端接受 / 拒绝 payload 的规则,不写 UI 渲染建议。 ### ✅ 正确 / ❌ 错误 payload 对照(保存端点) | 场景 | payload | |------|---------| | ✅ 首次保存(无版本) | `{ "version": null, "groups": [ { "groupCode": "BUS", "vehicleType": "bus", "serviceStartDate": "2026-09-12", "serviceEndDate": "2026-09-16", "days": [ { "tripDate": "2026-09-12", "headcount": 9, "memberOrderIds": [2099459272533323777] } ] } ] }` | | ✅ 改既有组(带 groupId,groupCode 不变) | `{ "version": 3, "groups": [ { "groupId": 1867000000009, "groupCode": "BUS", ... } ] }` | | ✅ 座位与车辆数同空(存量分组) | `{ ..., "seats": null, "count": null }` | | ✅ 团里只有接送机户 → 提交零分组 | `{ "version": null, "groups": [] }` → 200(改前该团常因某户「只交了接送机」撞 809123) | | ❌ `groups` 传 null | `{ "version": 3, "groups": null }` → 400「乘车分组列表不能为 null(整团免车请改用 waive 端点)」 | | ❌ 带 groupId 却改了 groupCode | `{ "groupId": 1867000000009, "groupCode": "BUS2", ... }` → 809104 | | ❌ 只填 seats 不填 count | `{ "seats": 19, "count": null }` → 809118 | | ❌ 车型填自由文本 | `{ "vehicleType": "35座大巴" }` → 809119 | ### 前端必须做的一处改动 - 🔴 **凡是对 809121 / 809122 / 809123 的 `message`(或预检 `vehicleMissing[].detail`)做过字符串包含判断的地方,一律改成按 `code` / `reason` 分支**。三条报文里的 `行程用车需求` 已改为 `用车需求`,旧的子串匹配会静默失配(不报错,只是那条分支再也不进)。 - 其余全部字段、校验规则、请求格式不变,不需要任何别的适配。 --- ## 五、数据库行为 只有保存端点(PUT)是写端点,本次改动**没有任何表结构或写入语义变化**——变的是写之前那道户级阻断的判据。 | 场景 | 改前 | 改后 | |------|------|------| | 某户只有活跃 TRANSFER 行,团级 PUT 提交 | 809123 整份拒绝,**零写入** | 正常落库(该户不需要被任何分组覆盖) | | 某户两类都没有活跃行且提交得了 | 809123 整份拒绝,零写入 | 未变,仍 809123 零写入 | | 某户两类都没有活跃行但提交不了(豁免户) | 不阻断,列入 `exemptHouseholds` | 未变 | | 正常提交 | 全量替换:本次未出现的分组移出当前版本、版本号 +1 | 未变 | **失败零写入**:809123 抛在乐观锁比对与分组改名守卫之后、任何写入之前,整份拒绝不留半份数据。 自动汇总(GET)与确认预检(GET)两个端点**零写入**,本次未改变这一点。 --- ## 六、边界行为 - 未登录 → 401(网关拦截)。 - 权限点 `group-batch:demand:confirm` 缺失 → 403。 - 团期尚未形成正式用车需求 → 809100(报文用「该团期」,不带雪花 id)。 - 正式需求已被他人修改 → 809102,带提交版本与当前版本。 - 团期已过配置阶段 → 809111。 - 已声明整团免车又提交分组 → 809115(需先 withdraw 回草稿)。 - 车队车型字典不可用 → 809120(不静默降级,让运营重试)。 - 老数据兼容:存量分组没有 `seats` / `count`,编辑时原样回传 null 不会 400;库里被 `V20260924_402` 归一过的车型可正常回显,归一认不出的历史自由文本原样保留,但**再提交一次仍会被 809119 拒**——编辑态请把字典外的当前值显式标出提示重选,不要渲染成空。 - 推不出服务日的户(`departDate` / `returnDate` 任一为空)跳过 809109 覆盖判定(已知盲区,不是遗漏)。 --- ## 六.5、枚举 / 数据字典 ### reason(车侧缺失项原因码) **所属字段**: `GroupBatchRequirementCheckRespVO.vehicleMissing[].reason` | **类型**: `String` | 值 | 中文 | 说明 | |----|------|------| | `GROUP_REQUIREMENT_NOT_FOUND` | 团级正式需求未形成 | 对应 809100 | | `GROUP_REQUIREMENT_STATUS_INVALID` | 团级正式需求状态不允许 | 对应 809101 | | `NO_GROUP` | 有需车户却零分组 | 对应 809103 | | `GROUP_CODE_INVALID` | 分组编码重复或改名 | 对应 809104 | | `DAY_OUT_OF_GROUP_RANGE` | 逐日行不在本组服务日范围内 | 对应 809105 | | `DAY_GAP_IN_GROUP_RANGE` | 本组服务日范围内缺日 | 对应 809106 | | `MEMBER_FOREIGN_ORDER` | 成员不属于本团期 | 对应 809107 | | `MEMBER_DUPLICATE_DAY` | 同一户同一日属多个分组 | 对应 809108 | | `ORDER_DAY_UNCOVERED` | 该户某日未被任何分组覆盖 | 对应 809109;🔴 **仍只认 TRAVEL,本次未改** | | `HEADCOUNT_LESS_THAN_MEMBERS` | 用车人数小于当日成员户数 | 对应 809110 | | `HOUSEHOLD_REQUIREMENT_NOT_SUBMITTED` | 该户尚未提交用车需求 | 对应 809122;🔴 **#8577 收窄:行程用车与接送机任一有即不报**。只带 `orderId` / `orderNo`,处置是催该户定制师提交 | | `MEMBER_GROUP_MISMATCH` | 该户车型与覆盖它的分组车型不符 | 对应 809125;排在户级未提交之后(交都没交的户没有车型可比) | | `TRANSFER_SERVICE_DATES_NOT_BACKFILLED` | 待放行的接送机需求未回填服务日 | 对应 809007,只带 `orderId` | | `TRANSFER_WINDOW_INCOMPLETE` | 接送机需求窗没盖住大交通派生日期 | 对应 809126,带 `orderId` / `orderNo` 与首个越窗日期 | ### reason(团级用车需求豁免户原因码) **所属字段**: `exemptHouseholds[].reason`、`vehicleExemptHouseholds[].reason` | **类型**: `String` | 值 | 中文 | 说明 | |----|------|------| | `ORDER_NOT_CUSTOMIZING` | 订单不在定制中 | 定制师提交会被 582017 拒,所以该户不算「没交」 | | `REQUIREMENT_FROZEN` | 团期已过资源准备、需求已冻结且该户未被打回 | 定制师提交会被 589536 拒 | 🔴 **只提交了接送机的户不属于任何一档**——它已提交,既不进未提交名单也不进豁免名单。 ### 用车需求类别(判定用,不直接出现在本次三个响应的字段里) | 值 | 中文 | 在本次判定中的角色 | |----|------|-------------------| | `TRAVEL` | 团期行程用车 | 汇总进团级乘车分组;**809109 逐日覆盖只认它** | | `TRANSFER` | 接送机 | 走逐户派车、结构上在团级乘车分组之外;#8577 起它也算「已提交用车需求」,参与 809121 / 809122 / 809123 的判定 | --- ## 六.6、修改前后对比 ### 字段级对比 | 字段 | 改前 | 改后 | |------|------|------| | 三个端点的全部请求字段 | — | 未变(一个都没动) | | 三个端点的全部响应字段 | — | 未变(无新增、无删除、无改名、无类型变化) | | `checkedResourceTypes` 的字段说明文案 | 「车侧逐户查**行程**用车需求行是否提交」 | 「车侧逐户查用车需求行是否提交(#8577 起行程用车与接送机任一有即算已提交)」 | | `vehicleMissing[].reason` 的取值集合 | 14 个 | 未变(仍 14 个,只是其中一个的触发条件收窄) | | `exemptHouseholds` 的成员判据 | 在团需车 ∧ 无 active TRAVEL ∧ 提交不了 | 在团需车 ∧ **两类都无 active 行** ∧ 提交不了 | ### 行为级对比 | 行为 | 改前 | 改后 | |------|------|------| | 某户只提交了接送机,团级 PUT 保存 | 809123 整份拒绝,运营无干净出路 | 正常保存 | | 某户只提交了接送机,点自动汇总 | 809121 整团出不来草稿 | 正常出草稿(该户不进草稿,也不进缺失清单) | | 某户只提交了接送机,进确认预检 | 该户挂 `HOUSEHOLD_REQUIREMENT_NOT_SUBMITTED`,`ready=false` | 不产出该缺失项 | | 某户只提交了接送机,是否要求被乘车分组覆盖 | 会走到 809109 | 不要求(它结构上在团级分组之外) | | 809121 报文 | `团期 {0} 有 {1} 户缺少可汇总的**行程**用车需求…` | `…缺少可汇总的用车需求…` | | 809122 报文 | `该户尚未提交**行程**用车需求,…` | `该户尚未提交用车需求,…` | | 809123 报文 | `{0}有 {1} 户尚未提交**行程**用车需求,…` | `{0}有 {1} 户尚未提交用车需求,…` | | 汇总缺失原因文案 | `未提交行程用车需求` | `未提交用车需求` | | 809109 逐日覆盖的判据 | 只认 TRAVEL | 未变,仍只认 TRAVEL | | 两类都没提交的户 | 三处照旧阻断 | 未变 | --- ## 六.7、影响评估 - **是否破坏向后兼容**: 否(无字段增删改;只是三个错误码少抛一类情况、报文文案改写) - **前端是否必须同步上线**: 否;但**若前端对这三条报文做过字符串包含判断,必须改**(改成按 `code` / `reason` 分支),否则那条分支会静默失配 - **前端 workaround 清理点**: 若为绕开「纯接送机户卡住整团」在页面上加过提示、屏蔽过确认按钮、或引导过运营去整团免车,可以撤掉 --- ## 七、不影响范围 - **仅影响**: 团期需求管理 Tab 的三个端点里 809121 / 809122 / 809123 的触发条件与报文文案。 - **零影响**: - 809109 逐日覆盖判定(仍只认 TRAVEL) - 整体确认端点 `POST .../requirement/confirm` 自身的确认逻辑与响应字段 - 受控重开、整份撤回、整团免车、按户打回四个端点 - 接送机批量确认 `POST .../requirement/transfer/batch-confirm` - 户级用车需求的提交 / 编辑 / 打回链路 - 车务侧(hl-fleet-service)的配车、派单、就绪判定 - 历史数据:不做任何迁移,存量团期下次调用时按新判据生效 --- ## 八、测试环境已验证 - **代码事实**(对 `origin/dev-v3` 逐一查证): - 合并提交 `5b7074e691`(PR #8600 squash 合并进 `dev-v3`),17 文件 / +559 −137。 - `GroupVehicleRequirementErrorCode` 三条 `IErrorCode.of` 的字面量 diff 已逐字核对(809121 / 809122 / 809123 各去掉「行程」二字),码值与常量名未变。 - `GroupVehicleDraftAggregator.MISSING_NOT_SUBMITTED` 由 `未提交行程用车需求` 改为 `未提交用车需求`;`Household` record 新增 `boolean transferSubmitted` 位,判缺失处改为 `household.needsVehicle() && !household.transferSubmitted()`。 - `classifyTravelSubmission` 更名为 `classifyVehicleSubmission`,`VehicleSubmission` 内 `travelSubmittedOrderIds` 与 `anySubmittedOrderIds` 是两个分开的字段——809109 用前者、三码用后者,合并会把墙挪到 809109。 - 三个端点的 Controller 签名、`@RequestBody` VO、响应 VO 字段清单逐一核对,确认零字段变化。 - 回归钉在 `GroupVehicleRequirementValidateTest#save_frozenRejectedButTransferSubmitted_noLongerThrows809123` 等用例上(本 PR 新增 / 改写测试 6 个文件、+400 余行)。 - **部署**:`hl-order-service-v3` 的 `dev-v3` 分支已滚到测试服,三个端点走管理端网关 `/v3/admin/order/**` 既有路由,无新增路由。 ``` PUT /v3/admin/order/group-batch/{groupBatchId}/vehicle-requirement → 200 ✓(纯接送机户不再触发 809123) GET /v3/admin/order/group-batch/{groupBatchId}/vehicle-requirement/aggregate-draft → 200 ✓(纯接送机户不再触发 809121) GET /v3/admin/order/group-batch/{groupBatchId}/requirement/confirm-check → 200 ✓(不再产出 HOUSEHOLD_REQUIREMENT_NOT_SUBMITTED) ``` --- ## 九、相关历史 PR | PR | Issue | 说明 | 是否仍有效 | |----|-------|------|------------| | — | #7441 | 团期正式用车需求首次落地(809100-809115 段) | ✅ 有效 | | — | #8219 | 有户未提交时阻断团级 PUT,新开 809123 与豁免户机制 | ✅ 有效(本单在其基础上收窄判据) | | — | #8220 | 自动汇总草稿端点与 809121 | ✅ 有效 | | — | #8249 | 预检加户级 809122 | ✅ 有效 | | — | #8306 | 报文按团号列户、不出现雪花 id | ✅ 有效 | | **本 PR #8600** | **#8577** | 户级提交判定与行程覆盖判定分离,三码收窄 + 文案改写 | ✅ 最新 | --- ## 十、相关文档 - 关联 Issue: [wx/HL#8577](https://git.1814.love:8443/wx/HL/issues/8577) - 关联 PR: [wx/HL#8600](https://git.1814.love:8443/wx/HL/pulls/8600) ## 关联 / 联系人 ### 链接 - **Issue**: [#8577](https://git.1814.love:8443/wx/HL/issues/8577) - **PR**: [#8600](https://git.1814.love:8443/wx/HL/pulls/8600) - **Merge commit**: [5b7074e691](https://git.1814.love:8443/wx/HL/commit/5b7074e691) ### 联系人 - **后端负责人**: @wx