--- schema: "hl-changelog/v2" ticket: "8219" title: "团级正式用车需求:有户能交没交时保存被拒(809123),交不了的户列为豁免户" consumer: "admin" author: "jw(GIT)" change_type: "修改接口" backend_status: "deployed" gateway_status: "verified" frontend_status: "verified" frontend_owner: "mmg" frontend_ref: "9eb8268fd04f55821a3200b342c18e8cafcc7768" target_release: "v2.1" verified_at: "2026-09-24" status_note: "新增错误码 809123;三个接口新增豁免户清单字段;809122 / 809121 不再报豁免户 | 2026-09-24 mmg 交付:编辑弹窗诊断区五数组→六数组新增「以下户未参与排车(豁免)」块(订单号+reasonName),保存成功提示点名豁免户数;RequirementTab 预检区新增豁免户展示块(不进 ready 不置灰);三接口 JSDoc 补 809123/豁免字段口径;workaround 清理点 grep 零命中;Section 45 例+RequirementTab 33 例全绿" updated_at: "2026-09-24" base: "dev-v3" --- # order-v3: 团级正式用车需求——未提交户阻断保存、豁免户显式列出 > **存放目录**: `changelogs-v2/{YYYY-MM}/` > > **服务**: hl-order-service-v3 (端口 8007) > **PR**: #8317、#8319 > **Issue**: #8219 > **日期**: 2026-09-24 > **影响范围**: 管理后台「团期详情 → 用车 Tab」的正式用车需求编辑弹窗(保存、自动汇总)与整团确认预检横幅 --- ## ⚠️ 关键变化 1. **保存正式用车需求多一个拒绝码 809123**:团里有户「本可以提交行程用车需求却没提交」时,`PUT .../vehicle-requirement` 直接拒绝,报文逐户列出团号(无团号回落订单号)。改前保存完全不看户侧有没有提交,而 809109 又要求每个需车户逐日被分组覆盖,运营只能把没提交的户也编进分组、替它编车型人数才能存下去。 2. **三个接口新增豁免户清单**:`exemptHouseholds`(保存响应、自动汇总响应)/ `vehicleExemptHouseholds`(整团确认预检)。豁免户是「此刻在系统里提交不了行程用车需求」的户,它们**不阻断**保存 / 汇总 / 确认,也**不要求被分组覆盖**,但会逐户连同原因列出来,前端需要展示。 3. **809122(预检 / 整团确认)与 809121(自动汇总)不再报豁免户**:三处判定同源,豁免户只出现在豁免户清单里,不再被当成「未提交」。 4. **809109 只对已提交户判覆盖**:没有行程用车需求的户(无论未提交还是豁免)都不再被要求编进分组。 --- ## 一、背景 团期详情页用车 Tab 上,管理员保存团级正式行程用车需求时,系统不检查各户有没有提交行程用车需求;同时 809109 要求每个需车户的出发~返回日都被某个分组覆盖。结果是有户没提交时,管理员为了存下去只能把那户也编进分组、替它填车型人数,这份编出来的数据随后会下发给车务。 本次把「户有没有提交」收成一份判定,三类户分别处理: | 户的类别 | 判定 | 保存(PUT) | 整团确认预检 | 自动汇总 | |------|------|------|------|------| | 已提交 | 有 active 行程用车需求 | 照常参与 809109 覆盖 | 照常 | 照常进草稿 | | 未提交 | 订单定制中,且(团期未冻结 或 该户最新行程用车需求被打回) | **809123 拒绝** | 809122 | 809121 | | 豁免 | 订单不在定制中;或团期已过资源准备、该户未被打回 | 不阻断,列入 `exemptHouseholds` | 不报 809122,列入 `vehicleExemptHouseholds` | 不报 809121、不进草稿,列入 `exemptHouseholds` | --- ## 二、变更接口清单 | # | 接口 | 方法 | 路径 | 变更类型 | 说明 | |---|------|------|------|----------|------| | 1 | 保存团期正式用车需求 | PUT | `/v3/admin/order/group-batch/{groupBatchId}/vehicle-requirement` | 新增错误码 + 出参新增字段 | 新增 809123;响应新增 `exemptHouseholds` | | 2 | GB-ADM-012 整团确认需求缺失预检 | GET | `/v3/admin/order/group-batch/{groupBatchId}/requirement/confirm-check` | 出参新增字段 + 取值口径变更 | 新增 `vehicleExemptHouseholds`;809122 不再报豁免户 | | 3 | 自动汇总正式用车需求草稿 | GET | `/v3/admin/order/group-batch/{groupBatchId}/vehicle-requirement/aggregate-draft` | 出参新增字段 + 取值口径变更 | 新增 `exemptHouseholds`;809121 不再报豁免户、豁免户不进草稿 | --- ## 三、接口详情 ### 1. 保存团期正式用车需求 `PUT /v3/admin/order/group-batch/{groupBatchId}/vehicle-requirement` **VO**: `GroupVehicleRequirementSaveReqVO → GroupVehicleRequirementRespVO` #### 使用场景 用车 Tab「编辑正式用车需求」弹窗点保存时调用,全量替换整份团级正式用车需求(主表备注 + 全部乘车分组 + 全部逐日行)。本次新增:团里存在「本可以提交却没提交」行程用车需求的户时,保存被 809123 拒绝;提交不了的户(豁免户)不阻断保存,在响应 `exemptHouseholds` 里列出。鉴权走 `group-batch:demand:confirm`,仅「资源准备中」阶段可保存。 #### 入参 | 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | |------|------|------|------|------|------| | groupBatchId | Path | Long | ✅ | 雪花 ID,团期聚合主键 | 团期 ID | | version | Body | Integer | ❌ | 首次保存传 `null`;之后必须回传上次响应的 `version` | 乐观锁版本号 | | remark | Body | String | ❌ | — | 整份备注 | | groups | Body | Array | ✅ | 不能为 `null`;空数组合法(有需车户时会被 809103 拒) | 全部乘车分组 | | groups[].groupId | Body | Long | ❌ | 沿用已有分组时必须回传 | 分组主键 | | groups[].groupCode | Body | String | ✅ | 非空;同一份内不重复;已有分组不得改名 | 分组键 | | groups[].vehicleType | Body | String | ✅ | 非空;须在车队车型字典内 | 车型 | | groups[].serviceStartDate | Body | String | ✅ | `yyyy-MM-dd` | 本组服务开始日 | | groups[].serviceEndDate | Body | String | ✅ | `yyyy-MM-dd`,不早于开始日 | 本组服务结束日 | | groups[].seats | Body | Integer | ❌ | 与 `count` 同填同空 | 单车座位数 | | groups[].count | Body | Integer | ❌ | 与 `seats` 同填同空 | 车辆数 | | groups[].specialTags | Body | Array | ❌ | 字典 `vehicle_special_demand` 编码 | 特殊诉求标签 | | groups[].remark | Body | String | ❌ | — | 分组备注 | | groups[].days | Body | Array | ✅ | 非空;须正好铺满本组服务日范围 | 逐日用车明细 | | groups[].days[].tripDate | Body | String | ✅ | `yyyy-MM-dd` | 行程日 | | groups[].days[].headcount | Body | Integer | ✅ | 不小于当日成员户数 | 当日用车人数 | | groups[].days[].memberOrderIds | Body | Array | ✅ | 非空;须全属本团在团户 | 当日乘车子订单 ID | #### 出参 `Result` | 字段 | 类型 | 说明 | |------|------|------| | data.requirementId | String | 正式需求主键 | | data.groupBatchId | String | 团期聚合主键 | | data.status | String | 保存后恒为 `DRAFT` | | data.version | Integer | 新版本号,下次保存回传 | | data.remark | String | 整份备注 | | data.confirmedBy | String | DRAFT 时为 `null` | | data.confirmedAt | String | DRAFT 时为 `null` | | data.groups[] | Array | 保存后的全部乘车分组(字段与改前一致) | | data.exemptHouseholds[] | Array | **本次新增**:本次保存时被豁免的在团需车户,按 orderId 升序;无豁免户时为空数组 `[]` | | data.exemptHouseholds[].orderId | String | 子订单 ID | | data.exemptHouseholds[].orderNo | String | 子订单号 | | data.exemptHouseholds[].reason | String | 豁免原因码;本接口只会出现 `ORDER_NOT_CUSTOMIZING`,见六.5 | | data.exemptHouseholds[].reasonName | String | 豁免原因中文名(后端下发) | `GroupVehicleRequirementRespVO` 的其余字段(配车刷新观测块 `planRefreshState` 等)与改前一致。**`exemptHouseholds` 只在保存响应里填充**;同一个 VO 在读接口(`GET .../vehicle-requirement`)、撤回、免车、重开的响应里该字段为 `null`,表示「这个响应不回答豁免问题」。 #### 请求示例 TEST 实测往返(团期 `2103018513226752001`,三户:两户已提交、一户待支付未提交): ```json { "version": null, "remark": "#8219 AC-5 成功用例(ii)", "groups": [ { "groupCode": "BUS", "vehicleType": "bus", "serviceStartDate": "2026-12-11", "serviceEndDate": "2026-12-13", "days": [ { "tripDate": "2026-12-11", "headcount": 4, "memberOrderIds": ["2103018513088339969", "2103018515806265346"] }, { "tripDate": "2026-12-12", "headcount": 4, "memberOrderIds": ["2103018513088339969", "2103018515806265346"] }, { "tripDate": "2026-12-13", "headcount": 4, "memberOrderIds": ["2103018513088339969", "2103018515806265346"] } ] } ] } ``` #### 响应示例 同一次请求的真实响应(配车刷新观测块字段省略,与改前一致): ```json { "code": 200, "message": "成功", "data": { "requirementId": "2103018859139407874", "groupBatchId": "2103018513226752001", "status": "DRAFT", "version": 1, "remark": "#8219 AC-5 成功用例(ii)", "confirmedBy": null, "confirmedAt": null, "groups": [ { "groupId": "2103018859143602177", "groupCode": "BUS", "vehicleType": "bus", "vehicleTypeName": "大巴系列", "serviceStartDate": "2026-12-11", "serviceEndDate": "2026-12-13", "days": [ { "tripDate": "2026-12-11", "headcount": 4, "memberOrderIds": ["2103018513088339969", "2103018515806265346"], "memberOrderCount": 2 }, { "tripDate": "2026-12-12", "headcount": 4, "memberOrderIds": ["2103018513088339969", "2103018515806265346"], "memberOrderCount": 2 }, { "tripDate": "2026-12-13", "headcount": 4, "memberOrderIds": ["2103018513088339969", "2103018515806265346"], "memberOrderCount": 2 } ] } ], "exemptHouseholds": [ { "orderId": "2103018517358141442", "orderNo": "HL20260924150741326", "reason": "ORDER_NOT_CUSTOMIZING", "reasonName": "订单不在定制中" } ] }, "traceId": null, "success": true } ``` #### 空数据 / 降级响应 没有豁免户时 `exemptHouseholds` 是空数组,不是 `null`(TEST 实测团期 `2103018935689650178`,两户都已提交);其余结构同上: ```json { "code": 200, "message": "成功", "data": { "groupBatchId": "2103018935689650178", "status": "DRAFT", "version": 1, "exemptHouseholds": [] }, "traceId": null, "success": true } ``` #### 错误响应 团里有户本可以提交却没提交行程用车需求(本次新增,测试环境实测原文): ```json { "code": 809123, "message": "团期「第30期 T29 #7441 AC-29 batch6」有 2 户尚未提交行程用车需求,暂不能保存正式用车需求:26-0674、26-7797", "data": null, "traceId": null, "success": false } ``` 需车户存在但一个分组都没提交(改前已有): ```json { "code": 809103, "message": "本团存在需要用车的子订单,至少要提交一个乘车分组", "data": null, "traceId": null, "success": false } ``` | 错误码 | 触发条件 | |------|------| | 809123 | **新增**:存在未提交户;报文列出户数与逐户团号(无团号回落订单号),不含 orderId | | 809103 | 需车户非空但 `groups` 为空数组 | | 809101 / 809102 / 809104~809110 / 809115 | 与改前一致 | | 589501 | 团期阶段不是「资源准备中」 | #### 业务边界 - 判序:阶段守卫 → 免车态守卫(809115)→ 源状态守卫(809101)→ 乐观锁(809102)→ 分组改名守卫(809104)→ **未提交户阻断(809123)** → 六条逐日校验(809103~809110)。809123 排在逐日校验之前:有户没交时先让运营去催定制师,而不是先看到某户某天没被覆盖。 - 809123 与 809103 ~ 809110 一样是整笔零写入:拒绝时库里的正式需求版本不变。 - 本接口只在「资源准备中」可调,这个阶段定制师都能提交需求,所以本接口的豁免户只有「订单不在定制中」一种,`exemptHouseholds[].reason` 只会是 `ORDER_NOT_CUSTOMIZING`。 - 豁免户不要求被任何分组覆盖(809109 只对已提交户判),也可以被编进分组(不会报错)。 - 全团需车户都是豁免户时,`groups` 为空数组仍报 809103;分组的 `memberOrderIds` 又不能为空,这种团按现行口径要么把豁免户编进分组,要么走整团免车(`POST .../vehicle-requirement/waive`)。 - 809123 报文里只列团号(团号为空时列订单号),不带雪花 orderId,与 809121 / 809112 / 809114 同一口径;前端展示报文即可,不要从报文里解析订单号。 --- ### 2. GB-ADM-012 整团确认需求缺失预检 `GET /v3/admin/order/group-batch/{groupBatchId}/requirement/confirm-check` **VO**: `Path 参数 → GroupBatchRequirementCheckRespVO` #### 使用场景 管理后台点「整体确认需求」之前调用,把按钮置灰并摊开缺失清单。它与 `POST .../requirement/confirm` 共用同一套校验。本次新增 `vehicleExemptHouseholds`:列出提交不了行程用车需求、因而不报 809122 的户;809122 只报「本可以提交却没提交」的户。鉴权走 `group-batch:demand:confirm`。 #### 入参 | 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | |------|------|------|------|------|------| | groupBatchId | Path | Long | ✅ | 雪花 ID,团期聚合主键 | 团期 ID;无 Query、无 Body | #### 出参 `Result` | 字段 | 类型 | 说明 | |------|------|------| | data.groupBatchId | String | 团期聚合主键 | | data.batchStatus | String | 团期当前状态码 | | data.ready | Boolean | 可整体确认:`missing` 空且 `vehicleMissing` 空且阶段可确认。豁免户不影响 `ready` | | data.missing[] | Array | 住宿缺失清单(与改前一致) | | data.vehicleWaived | Boolean | 整团免车时为 `true` | | data.vehicleMissing[] | Array | 车侧缺失清单(结构与改前一致) | | data.vehicleMissing[].reason | String | `HOUSEHOLD_REQUIREMENT_NOT_SUBMITTED`(809122)**本次起只报未提交户,不再报豁免户** | | data.vehicleMissing[].orderId | String | 涉及的子订单 ID;团级条目为 `null` | | data.vehicleMissing[].orderNo | String | 子订单号;团级条目为 `null` | | data.vehicleMissing[].detail | String | 人话报文 | | data.vehicleExemptHouseholds[] | Array | **本次新增**:车侧豁免户,按 orderId 升序;`vehicleWaived=true` 或无豁免户时为空数组 `[]` | | data.vehicleExemptHouseholds[].orderId | String | 子订单 ID | | data.vehicleExemptHouseholds[].orderNo | String | 子订单号 | | data.vehicleExemptHouseholds[].reason | String | `ORDER_NOT_CUSTOMIZING` 或 `REQUIREMENT_FROZEN`,见六.5 | | data.vehicleExemptHouseholds[].reasonName | String | 豁免原因中文名 | | data.groupVehicleRequirementId | String | 活跃正式用车需求主键;无则 `null` | | data.groupVehicleRequirementStatus | String | 活跃正式用车需求状态;无则 `null` | | data.groupVehicleRequirementVersion | Integer | 活跃正式用车需求版本号;无则 `null` | 其余字段(`batchStatusName`、`checkedResourceTypes`、`transferSubmitEnabled`、`transferDeclaredWithoutRequirement` 等)与改前一致。 #### 请求示例 ```http GET /v3/admin/order/group-batch/2099927193172815873/requirement/confirm-check HTTP/1.1 Host: <网关地址> Authorization: Bearer ``` #### 响应示例 ```json { "code": 200, "message": "成功", "data": { "groupBatchId": "2099927193172815873", "batchStatus": "RESOURCE_PREPARING", "ready": false, "missing": [], "vehicleWaived": false, "vehicleMissing": [ { "reason": "HOUSEHOLD_REQUIREMENT_NOT_SUBMITTED", "groupCode": null, "tripDate": null, "orderId": "2099927193143455746", "orderNo": "HL20260916022352215", "detail": "该户尚未提交行程用车需求,请先让定制师提交后再整团提交车务" }, { "reason": "HOUSEHOLD_REQUIREMENT_NOT_SUBMITTED", "groupCode": null, "tripDate": null, "orderId": "2099927202878435329", "orderNo": "HL20260916022354533", "detail": "该户尚未提交行程用车需求,请先让定制师提交后再整团提交车务" } ], "vehicleExemptHouseholds": [ { "orderId": "2099927222981734402", "orderNo": "HL20260916022359336", "reason": "ORDER_NOT_CUSTOMIZING", "reasonName": "订单不在定制中" }, { "orderId": "2099927233643655170", "orderNo": "HL20260916022401707", "reason": "ORDER_NOT_CUSTOMIZING", "reasonName": "订单不在定制中" } ], "groupVehicleRequirementId": "2099927498304131073", "groupVehicleRequirementStatus": "DRAFT", "groupVehicleRequirementVersion": 4 }, "traceId": null, "success": true } ``` #### 空数据 / 降级响应 整团免车或没有豁免户时,`vehicleExemptHouseholds` 为空数组(不是 `null`): ```json { "code": 200, "message": "成功", "data": { "groupBatchId": "2099927193172815873", "batchStatus": "RESOURCE_PREPARING", "ready": true, "missing": [], "vehicleWaived": false, "vehicleMissing": [], "vehicleExemptHouseholds": [] }, "traceId": null, "success": true } ``` #### 错误响应 ```json { "code": 589500, "message": "团期不存在", "data": null, "traceId": null, "success": false } ``` 无需求确认权限: ```json { "code": 589507, "message": "无操作权限(当前角色未授予团期权限,或该团期不在您名下)", "data": null, "traceId": null, "success": false } ``` #### 业务边界 - 本接口只读,不写库,可安全重复调用。 - 豁免户**不进** `vehicleMissing`、**不影响** `ready`;它是提示信息,不是缺失。 - `vehicleExemptHouseholds` 的户集合是「在团需车户」(含已完成订单),`REQUIREMENT_FROZEN` 只在团期已过资源准备时出现(例如物料准备中)。 - 没有活跃正式用车需求(`vehicleMissing` 里有 `GROUP_REQUIREMENT_NOT_FOUND`)时,豁免户清单照常列出。 - 打回后未重提的户算未提交:冻结期内它仍可重提,所以照报 809122,不进豁免户清单。 - `POST .../requirement/confirm` 与本接口同一判定:存在 809122 条目时确认被拒、零写入;只有豁免户未提交时确认照常通过。 --- ### 3. 自动汇总正式用车需求草稿 `GET /v3/admin/order/group-batch/{groupBatchId}/vehicle-requirement/aggregate-draft` **VO**: `Path 参数 → GroupVehicleAggregateDraftRespVO` #### 使用场景 编辑弹窗里点「自动汇总」时调用,按各子订单的 active 行程用车需求汇总出一份可原样 PUT 的草稿,零写入。本次起豁免户不进汇总、不报 809121,改列在 `exemptHouseholds`;809121 只报「本可以提交却没提交」及车型 / 日期 / 人数缺失的户。鉴权与 `GET .../vehicle-requirement` 同码。 #### 入参 | 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | |------|------|------|------|------|------| | groupBatchId | Path | Long | ✅ | 雪花 ID,团期聚合主键 | 团期 ID;无 Query、无 Body | #### 出参 `Result` | 字段 | 类型 | 说明 | |------|------|------| | data.groupBatchId | String | 团期 ID | | data.currentStatus | String | 当前正式需求状态;未形成时为 `null` | | data.draft | Object | 汇总草稿,与保存请求体同形(`version` / `remark` / `groups`);豁免户不在任何分组里 | | data.droppedFleetItems[] | Array | 被丢弃的车型项(与改前一致) | | data.staleHeadcountOrders[] | Array | 实时人数与冻结人数不同的户(与改前一致) | | data.paddedOrderDays[] | Array | 补进分组的日期(与改前一致) | | data.violations[] | Array | 草稿预检违规(与保存同一份校验) | | data.exemptHouseholds[] | Array | **本次新增**:豁免户,按 orderId 升序;无则空数组 `[]` | | data.exemptHouseholds[].orderId | String | 子订单 ID | | data.exemptHouseholds[].orderNo | String | 子订单号 | | data.exemptHouseholds[].reason | String | `ORDER_NOT_CUSTOMIZING` 或 `REQUIREMENT_FROZEN`,见六.5 | | data.exemptHouseholds[].reasonName | String | 豁免原因中文名 | #### 请求示例 ```http GET /v3/admin/order/group-batch/2099751749718822913/vehicle-requirement/aggregate-draft HTTP/1.1 Host: <网关地址> Authorization: Bearer ``` #### 响应示例 测试环境实测(该团唯一的需车户订单待出发、没提交行程用车需求,被列为豁免户,因此 809121 不再触发;草稿零分组,保存同一份校验报 809103): ```json { "code": 200, "message": "成功", "data": { "groupBatchId": "2099751749718822913", "currentStatus": null, "draft": { "version": null, "remark": null, "groups": [] }, "droppedFleetItems": [], "staleHeadcountOrders": [], "paddedOrderDays": [], "violations": [ { "code": 809103, "reason": "NO_GROUP", "detail": "本团存在需要用车的子订单,至少要提交一个乘车分组", "groupCode": null, "tripDate": null, "orderId": null } ], "exemptHouseholds": [ { "orderId": "2099751749693657090", "orderNo": "HL20260915144643265", "reason": "ORDER_NOT_CUSTOMIZING", "reasonName": "订单不在定制中" } ] }, "traceId": null, "success": true } ``` #### 空数据 / 降级响应 全部需车户都已提交时 `exemptHouseholds` 为空数组;团里没有在团户时草稿 `groups` 为空数组: ```json { "code": 200, "message": "成功", "data": { "groupBatchId": "2099751749718822913", "currentStatus": null, "draft": { "version": null, "remark": null, "groups": [] }, "droppedFleetItems": [], "staleHeadcountOrders": [], "paddedOrderDays": [], "violations": [], "exemptHouseholds": [] }, "traceId": null, "success": true } ``` #### 错误响应 有户本可以提交却没提交(测试环境实测原文;同团另有 2 户已完成订单被豁免,不在清单里): ```json { "code": 809121, "message": "团期 「第30期 T29 #7441 AC-29 batch6」 有 2 户缺少可汇总的行程用车需求,暂不能自动汇总:26-0674:未提交行程用车需求、26-7797:未提交行程用车需求", "data": null, "traceId": null, "success": false } ``` 车型字典不可用: ```json { "code": 809120, "message": "车队车型字典暂不可用,无法校验车型,请稍后重试", "data": null, "traceId": null, "success": false } ``` #### 业务边界 - 本接口只读,不写库。 - 809121 的户数只计未提交户与车型 / 日期 / 人数缺失的户,不计豁免户;报文按团号列户。 - 豁免户不进草稿分组,草稿原样 PUT 时也不会因为豁免户没被覆盖而报 809109。 - 豁免户与 809123(保存)/ 809122(预检)是同一份判定:汇总放行的户,保存时也不会被判成未提交。 - `REQUIREMENT_FROZEN` 可能出现在本接口(团期已过资源准备时);同一份草稿在该阶段不能保存(保存只允许资源准备中)。 --- ## 四、契约约束与正确调用方式 > 本节只写**后端返回什么、前端据什么判定**。 ### 「保存正式用车需求」按钮的置灰条件 以预检接口为判据,满足任一条即置灰保存按钮并提示原因: | 条件 | 判定写法 | 提示 | |------|----------|------| | 有未提交户 | `confirm-check` 的 `vehicleMissing.some(i => i.reason === 'HOUSEHOLD_REQUIREMENT_NOT_SUBMITTED')` | 列出这些条目的 `orderNo`,提示先让定制师提交行程用车需求(保存会报 809123) | | 阶段不可保存 | `batchStatus !== 'RESOURCE_PREPARING'` | 仅资源准备中可编辑正式用车需求 | 豁免户**不是**置灰条件:`vehicleExemptHouseholds` 非空时按钮照常可点。 ### ✅ 正确 / ❌ 错误的判定写法 | 场景 | 判定写法 | |------|----------| | ✅ 判「这户需要催定制师」 | `vehicleMissing[].reason === 'HOUSEHOLD_REQUIREMENT_NOT_SUBMITTED'`,户取 `orderId` / `orderNo` | | ✅ 判「这户没交但不用催」 | 在 `vehicleExemptHouseholds[]` / `exemptHouseholds[]` 里;原因展示 `reasonName` | | ✅ 豁免户展示 | 在弹窗 / 预检横幅里单独列一块「以下户未参与排车」,逐户显示订单号 + `reasonName`;不要混进缺失清单 | | ❌ 用 `exemptHouseholds` 判「能不能保存」 | 豁免户不阻断保存,能不能保存看 809123 与 `vehicleMissing` | | ❌ 从 809123 / 809121 报文里抠订单号 | 报文只带团号,结构化户信息读 `vehicleMissing[].orderNo` / `exemptHouseholds[].orderNo` | | ❌ 假设豁免户清单在读接口 `GET .../vehicle-requirement` 里有值 | 该字段只在保存响应里填充,读接口为 `null` | --- ## 五、数据库行为 - 无 DDL、无 Flyway 脚本、无新表 / 新列 / 新状态值。 - `PUT .../vehicle-requirement`:809123 在任何写入之前抛出,整笔事务零写入;保存成功时的写库行为(失活旧版本 + 插新版本与分组 / 逐日行)与改前一致,豁免户清单不落库,是保存那一刻按户侧状态算出来的。 - `GET .../confirm-check`、`GET .../aggregate-draft`:只读,零写入。 - 判定读的是既有数据:订单状态(`order_main.order_status`)、团期状态(`order_group_batch.batch_status`)、行程用车需求行(`order_vehicle_requirement`,含历史版本判断是否被打回)。 --- ## 六、边界行为 - 未登录 → 401(网关拦截)。 - 团期不存在 → HTTP 200 + `code=589500`。 - 无权限 → HTTP 200 + `code=589507`。 - 豁免户清单在「无豁免户」时是空数组 `[]`;`GET .../vehicle-requirement` 等非保存响应里 `exemptHouseholds` 为 `null`。 - 整团免车(`vehicleWaived=true`)时车侧整体跳过,`vehicleExemptHouseholds` 为空数组。 - 全团需车户都是豁免户:保存空分组报 809103,分组成员又不能为空;按现行口径把豁免户编进分组或走整团免车。 - 809123 报文的户标识:团号 → 订单号 → 「某子订单」,不含雪花 ID。 --- ## 六.5、枚举 / 数据字典 ### exemptHouseholds[].reason / vehicleExemptHouseholds[].reason(豁免原因) **所属字段**: `GroupVehicleExemptHouseholdVO.reason` | **类型**: `String` | 值 | 中文(reasonName) | 说明 | 出现在哪些接口 | |----|------|------|------| | `ORDER_NOT_CUSTOMIZING` | 订单不在定制中 | 订单状态不是定制中(待支付 / 待出发 / 出行中 / 已完成),定制师提交行程用车需求会被 582017 拒 | 保存、预检、自动汇总 | | `REQUIREMENT_FROZEN` | 团期需求已冻结且该户未被打回 | 团期已过资源准备(物料准备中及之后),该户从没提交过或最新版本未被打回,定制师提交会被 589536 拒 | 仅预检、自动汇总;保存接口不会出现 | ### 新增错误码 | 错误码 | 模板 | 说明 | |------|------|------| | 809123 | `{0}有 {1} 户尚未提交行程用车需求,暂不能保存正式用车需求:{2}` | `{0}` 团期人话名(如 `团期「第30期 …」`,快照缺失为 `该团期`);`{1}` 户数;`{2}` 逐户团号,顿号分隔,无团号回落订单号 | ## 六.6、修改前后对比 ### 字段级对比 | 字段 | 改前 | 改后 | |------|------|------| | `GroupVehicleRequirementRespVO.exemptHouseholds` | 不存在 | 保存响应填充(数组),其它响应为 `null` | | `GroupBatchRequirementCheckRespVO.vehicleExemptHouseholds` | 不存在 | 恒为数组 | | `GroupVehicleAggregateDraftRespVO.exemptHouseholds` | 不存在 | 恒为数组 | | 错误码 809123 | 不存在 | 保存时有未提交户 | ### 行为级对比 | 行为 | 改前 | 改后 | |------|------|------| | 保存时有户能交没交 | 不检查;若该户没被分组覆盖报 809109,逼运营替它编成员 | 报 809123,点名这些户 | | 保存时有户交不了(如订单待支付) | 同上,必须编进分组 | 不阻断、不要求覆盖,列入 `exemptHouseholds` | | 809109 的检查对象 | 全部在团需车户 | 只检查已提交户 | | 预检 809122 | 凡没有 active 行程用车需求的放行户都报 | 只报未提交户;豁免户改列 `vehicleExemptHouseholds` | | 自动汇总 809121「未提交行程用车需求」 | 凡没有行程用车需求的需车户都报 | 只报未提交户;豁免户不进草稿、改列 `exemptHouseholds` | | 整团确认时只有豁免户没交 | 报 809122 拒绝 | 照常确认 | ## 六.7、影响评估 - **是否破坏向后兼容**: 否(字段层面)。三个接口只新增字段;错误码只新增 809123。行为上保存会多一种拒绝(809123),前端按通用错误处理展示 `message` 即可正确工作。 - **前端是否必须同步上线**: 否。不改前端时:809123 按通用错误弹窗展示;新增字段被忽略,豁免户不会显示在页面上(运营看不到哪些户没参与排车)。展示豁免户清单与按钮置灰需要前端改动。 - **前端 workaround 清理点**: 若页面有「把没提交的户也塞进分组才能保存」的操作引导,可以撤掉。 - **风险面**: 豁免户不参与排车,真要用车但订单尚在待支付的户需要运营从豁免户清单里看到并跟进。 ## 七、不影响范围 - **仅影响**: 上述三个管理后台接口,以及与预检同一判定的 `POST /v3/admin/order/group-batch/{groupBatchId}/requirement/confirm`(请求 / 响应结构不变,只是豁免户不再触发 809122)。 - **零影响**: - 小程序端(mp)全部接口。 - 子订单级行程用车需求提交 / 打回链路:582017、589536 的触发条件不变。 - `GET /v3/admin/order/group-batch/{groupBatchId}/vehicle-requirement` 读接口、撤回、免车、重开:结构不变(`exemptHouseholds` 为 `null`)。 - 接送机(TRANSFER)相关:只看行程用车(TRAVEL)。 - 数据库:无 DDL、无迁移。 --- ## 八、测试环境已验证 部署:`hl-order-service-v3` @ `dev-v3` `c0be69b02`,2026-09-24 11:04 两个实例(8086 / 8186)均 UP。经网关(自签 admin token)实测,全部为只读或被拒的写: ``` PUT /v3/admin/order/group-batch/2099927193172815873/vehicle-requirement(version=4,只放已提交户) → 200;code=809123「团期「第30期 T29 #7441 AC-29 batch6」有 2 户尚未提交行程用车需求, 暂不能保存正式用车需求:26-0674、26-7797」;两个实例返回一致 ✓ → 同团 2 户已完成订单未被点名 ✓;库里正式需求仍为 v4 DRAFT active(零写入)✓ → 不带 token → 401「缺少有效的 Authorization 头」✓ GET /v3/admin/order/group-batch/2099927193172815873/requirement/confirm-check → 200;vehicleMissing 的 809122 只有 26-0674 / 26-7797 两户 ✓ → vehicleExemptHouseholds = HL20260916022359336、HL20260916022401707(ORDER_NOT_CUSTOMIZING)✓ GET /v3/admin/order/group-batch/2099958693339566081/requirement/confirm-check(物料准备中) → 200;定制中且从没提交的 HL20260916042902319 不报 809122, 列入 vehicleExemptHouseholds(REQUIREMENT_FROZEN)✓ GET /v3/admin/order/group-batch/2099927193172815873/vehicle-requirement/aggregate-draft → 200;code=809121,只数到 2 户(26-0674、26-7797),豁免户不在清单 ✓ GET /v3/admin/order/group-batch/2099751749718822913/vehicle-requirement/aggregate-draft → 200;exemptHouseholds 列出 HL20260915144643265(ORDER_NOT_CUSTOMIZING),不报 809121 ✓ ``` 保存成功路径(2026-09-24 15:0x,自建团期,团期名前缀 `8219-AC5-jw-`,均为资源准备中): ``` 团期 2103018513226752001(A、B 定制中,C 待支付且无行程用车需求) PUT(分组只放 A) → 200;code=809123「…有 1 户尚未提交行程用车需求…:HL20260924150740940」,只点名 B、不含 C;零写入 ✓ B 提交行程用车需求后 PUT(分组放 A、B) → 200;code=200,DRAFT v1;exemptHouseholds 只有 C(HL20260924150741326,ORDER_NOT_CUSTOMIZING)✓ → GET 回读:分组成员只有 A、B,C 不在任何分组 ✓ 团期 2103018935689650178(两户都已提交) PUT(分组放两户) → 200;code=200,DRAFT v1;exemptHouseholds = [] ✓;回读两户都在分组成员里 ✓ ``` --- ## 十、相关文档 - 关联 Issue: [wx/HL#8219](https://git.1814.love/wx/HL/issues/8219) - 关联 PR: [wx/HL#8317](https://git.1814.love/wx/HL/pulls/8317)、[wx/HL#8319](https://git.1814.love/wx/HL/pulls/8319) - 809122 的来源:`changelogs-v2/2026-09/23_8249_查看需求页未提交状态与户级用车预检-修改接口-管理后台.md` - 自动汇总端点:`changelogs-v2/2026-09/23_8220_团期正式行程用车需求自动汇总草稿-新增接口-管理后台.md` ## 关联 / 联系人 ### 链接 - **Issue**: [#8219](https://git.1814.love/wx/HL/issues/8219) - **PR**: [#8317](https://git.1814.love/wx/HL/pulls/8317)、[#8319](https://git.1814.love/wx/HL/pulls/8319) - **Merge commit**: [c0be69b02](https://git.1814.love/wx/HL/commit/c0be69b02809baabb0a557f3d940d092e4344604) ### 联系人 - **后端负责人**: @jw