--- schema: "hl-changelog/v2" ticket: "8249" title: "团期「查看需求」:无需求行的户不再报 PENDING、户级未提交用车需求进预检清单、六条车务报文改写为可读名称" consumer: "admin" author: "wx(GIT)" change_type: "修改接口" backend_status: "deployed" gateway_status: "verified" frontend_status: "not_required" frontend_owner: "" frontend_ref: "" target_release: "" verified_at: "" status_note: "前端判 not_required:状态列 statusName||status||— 三级兜底 null 安全;PENDING 判等 12 处全无关;vehicleMissing 按 orderNo 结构化分流即契约推荐判据;809 报文零文本匹配分支(拦截器透 toast/detail 直显)" updated_at: "2026-09-23" base: "dev-v3" --- # order-v3: 团期「查看需求」页三处契约变更(含报文变更) > **存放目录**: `changelogs-v2/{YYYY-MM}/` > > **服务**: hl-order-service-v3 (端口 8007) > **PR**: #8286 > **Issue**: #8249 > **日期**: 2026-09-23 > **影响范围**: 管理后台「团期详情 → 查看需求」tab 的子订单列表状态列、整团确认预检横幅、车务相关弹窗文案 --- ## ⚠️ 关键变化 1. **状态字段会返 `null` 了**:`hotelRequirementStatus` / `vehicleRequirementStatus` 在「该户一条 active 需求行都没有」时返回 `null`。改前恒回落字符串 `"PENDING"`,页面把一个根本没提交的户显示成「待房务配 / 待车队配」,与同屏预检横幅里的 `NOT_SUBMITTED` 自相矛盾。配对的 `*StatusName` 此时按该户是否需要该资源分叉:需要 → `"未提交"`,不需要 → `null`。 2. **预检清单多一类条目**:`confirm-check` 的 `vehicleMissing[]` 新增 `reason = "HOUSEHOLD_REQUIREMENT_NOT_SUBMITTED"`(错误码 809122),表示「该户在团需车、却一条行程用车需求都没提交」。它与团级的 `GROUP_REQUIREMENT_NOT_FOUND` **并列出现**,不互斥、不被团级缺席短路——改前车侧只查团级正式需求,「某户一行都没提交」在预检里没有任何位置能表达。 3. 🔴 **报文变更**:六个 809 段错误码的**渲染文本**被改写(雪花 ID → 可读名称 / 直接删掉标识符),见「六.6、修改前后对比」。因为 `GroupVehicleViolation.detail()` 就是 `toException().getMessage()`,**预检横幅上的 `detail` 与确认/保存/免车弹窗里的 `message` 是同一份字符串**,两处同时改变。凡是对 `vehicleMissing[].detail` 或错误 `message` 做**文本匹配 / 正则解析 / 截取订单号**的前端逻辑,这次会失效;纯展示(原样渲染这段人话)不需要改。订单号已经由 `vehicleMissing[].orderNo` 结构化下发,不要再从 `detail` 里抠。 --- ## 一、背景 「查看需求」页上同一个团期的同一户,三处读数互相矛盾:子订单列表说「待房务配」(=已提交、等资源侧接),预检横幅说「未提报」,而整团确认点下去报的是团级的「尚未形成正式用车需求」——运营据此去催车务,实际该催的是定制师。 | 维度 | 改前 | 改后 | |------|------|------| | 无 active 需求行时 `*Status` | `"PENDING"`(借用了真实状态之一) | `null` | | 无 active 需求行时 `*StatusName` | 「待房务配」/「待车队配」 | 「未提交」(该户需要该资源时)/ `null` | | 车侧预检覆盖粒度 | 只有团级正式需求的六条校验 | 团级六条 + **逐户**查行程用车需求是否提交 + 待放行接送机需求服务日回填 | | 车务报文里的标识符 | 雪花 ID(`团期 2100856430494973953`、`子订单 2102309919002943489`) | 可读名称(`团期「第3期 10月8日出发团」`、`子订单 HL20260922161158291`)或直接省略 | --- ## 二、变更接口清单 | # | 接口 | 方法 | 路径 | 变更类型 | 说明 | |---|------|------|------|----------|------| | 1 | A3 团期下子订单列表 | GET | `/v3/admin/order/group-batch/{groupBatchId}/orders` | 出参取值变更 | 四个需求状态字段在无 active 行时改为 `null` + 「未提交」 | | 2 | GB-ADM-012 整团确认需求缺失预检 | GET | `/v3/admin/order/group-batch/{groupBatchId}/requirement/confirm-check` | 出参枚举扩充 + 报文变更 | `vehicleMissing[].reason` 新增 `HOUSEHOLD_REQUIREMENT_NOT_SUBMITTED`;`detail` 文案改写 | --- ## 三、接口详情 ### 1. A3 团期下子订单列表 `GET /v3/admin/order/group-batch/{groupBatchId}/orders` **VO**: `Query 参数 → PageResult` #### 使用场景 管理后台「团期详情 → 查看需求 / 子订单」tab 打开时调用,渲染该团期下每一户的一行摘要(团号、客户、金额、房需求状态、车需求状态、定制师、出行人)。本次改动只影响其中四个需求状态字段的取值,分页与其余字段的契约不变。鉴权走团期权限码 `group-batch:view`,未登录由网关拦截。 #### 入参 | 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | |------|------|------|------|------|------| | groupBatchId | Path | Long | ✅ | 雪花 ID,团期聚合主键 | 团期 ID | | page | Query | Integer | ❌ | 缺省 1;小于 1 归一为 1 | 页码,从 1 起 | | pageSize | Query | Integer | ❌ | 缺省 20;小于 1 归一为 20;大于 200 截断为 200 | 每页条数 | | includeTravelers | Query | Boolean | ❌ | 缺省 true | 是否附 `travelers[]`;证件号/手机号一律不返回 | | includeNeeds | Query | Boolean | ❌ | 缺省 true | 是否附 `roomCount` / `roomType` / `specialNeeds` | | includeCancelled | Query | Boolean | ❌ | 缺省 false | 是否含已取消子订单;缺省只返活跃集 | #### 出参 `Result>` | 字段 | 类型 | 说明 | |------|------|------| | data.total | Long | 总条数 | | data.page | Integer | 当前页码 | | data.pageSize | Integer | 每页条数 | | data.records[].orderId | String | 子订单 ID(雪花,JSON 里是字符串) | | data.records[].orderNo | String | 订单编号 | | data.records[].teamNo | String | 团号;订金支付成功后生成,未付订金为 `null` | | data.records[].customerName | String | 客户姓名 | | data.records[].participantCount | Integer | 出行人总数 | | data.records[].orderStatus | String | 订单状态码 | | data.records[].orderStatusName | String | 订单状态中文名 | | data.records[].payStatus | String | 支付状态(UNPAID / DEPOSIT_PAID / FULLY_PAID) | | data.records[].payStatusName | String | 支付状态中文名 | | data.records[].contractStatus | String | 合同状态;无合同时 `null` | | data.records[].contractStatusName | String | 合同状态中文名;无合同时 `null` | | data.records[].insuranceStatus | String | 保险状态;无保险时 `null` | | data.records[].insuranceStatusName | String | 保险状态中文名;无保险时 `null` | | data.records[].paidAmount | String | 已支付金额(订金 + 尾款),两位小数字符串 | | data.records[].balanceAmount | String | 待支付尾款金额,两位小数字符串 | | data.records[].hotelRequirementStatus | String | **本次变更**:房需求状态码;无 active 房需求行时为 `null`(改前回落 `"PENDING"`) | | data.records[].hotelRequirementStatusName | String | **本次变更**:房需求状态中文名;`hotelRequirementStatus` 为 `null` 时该户需房则为 `"未提交"`、不需房则为 `null` | | data.records[].vehicleRequirementStatus | String | **本次变更**:车需求状态码;无 active 行时为 `null`(改前回落 `"PENDING"`) | | data.records[].vehicleRequirementStatusName | String | **本次变更**:车需求状态中文名;`vehicleRequirementStatus` 为 `null` 时该户需车则为 `"未提交"`、不需车则为 `null` | | data.records[].consultantName | String | 定制师姓名(创单时固化) | | data.records[].totalPrice | String | 本户应收,两位小数字符串;取消单为 `"0.00"` | | data.records[].tierCode | String | 档位码(形如 `2A1C`) | | data.records[].tierName | String | 档位名(形如 `2成人1儿童`) | | data.records[].travelerInfoComplete | Boolean | 出行人资料是否齐全 | | data.records[].roomCount | Integer | 房数;`includeNeeds=true` 时返回 | | data.records[].roomType | String | 房型文本;`includeNeeds=true` 时返回,无需求行时 `null` | | data.records[].roomTypeName | String | 房型中文名;字典不可达时回落原值 | | data.records[].specialNeeds | String | 特殊需求;缺需求行时回落订单备注 | | data.records[].contactPhone | String | 联系人手机号(脱敏,前 3 后 4) | | data.records[].groupChatUnreadCount | Integer | 「联系定制师」按钮未读角标 | | data.records[].travelers[].name | String | 出行人姓名 | | data.records[].travelers[].type | String | 出行人类型(ADULT / CHILD / YOUNG_CHILD / BABY) | | data.records[].travelers[].age | Integer | 出团日时的周岁;缺出生日期时 `null` | | data.records[].travelers[].birthdayInTrip | Boolean | 是否行程期间过生日 | #### 请求示例 ```http GET /v3/admin/order/group-batch/2100856430494973953/orders HTTP/1.1 Host: <网关地址> Authorization: Bearer ``` #### 响应示例 ```json { "code": 200, "message": "成功", "data": { "records": [ { "orderId": "2100856430239121409", "orderNo": "HL20260918155619496", "teamNo": "26-7060", "customerName": "王有亿", "participantCount": 3, "orderStatus": "CUSTOMIZING", "orderStatusName": "定制中", "payStatus": "DEPOSIT_PAID", "payStatusName": "已付定金", "contractStatus": null, "contractStatusName": null, "insuranceStatus": null, "insuranceStatusName": null, "paidAmount": "1500.00", "balanceAmount": "9440.00", "hotelRequirementStatus": "PENDING_REVIEW", "hotelRequirementStatusName": "待审核", "vehicleRequirementStatus": "PENDING_REVIEW", "vehicleRequirementStatusName": "待提交车务", "consultantName": "王骁", "totalPrice": "10940.00", "tierCode": "2A1C", "tierName": "2成人1儿童", "travelerInfoComplete": true, "roomCount": 1, "roomType": "DELUXE", "roomTypeName": "豪华房", "specialNeeds": "禁烟", "contactPhone": "137****1407", "groupChatUnreadCount": 0, "travelers": [ { "name": "王有亿", "type": "ADULT", "age": 41, "birthdayInTrip": false }, { "name": "李美丽", "type": "ADULT", "age": 36, "birthdayInTrip": false }, { "name": "王小明", "type": "CHILD", "age": 8, "birthdayInTrip": false } ] }, { "orderId": "2102309919002943489", "orderNo": "HL20260922161158291", "teamNo": "26-2355", "customerName": "王二麻子", "participantCount": 4, "orderStatus": "CUSTOMIZING", "orderStatusName": "定制中", "payStatus": "DEPOSIT_PAID", "payStatusName": "已付定金", "contractStatus": null, "contractStatusName": null, "insuranceStatus": null, "insuranceStatusName": null, "paidAmount": "2000.00", "balanceAmount": "13920.00", "hotelRequirementStatus": null, "hotelRequirementStatusName": "未提交", "vehicleRequirementStatus": null, "vehicleRequirementStatusName": "未提交", "consultantName": "刘畅", "totalPrice": "15920.00", "tierCode": "4A", "tierName": "4成人", "travelerInfoComplete": false, "roomCount": 2, "roomType": null, "roomTypeName": null, "specialNeeds": "干", "contactPhone": "185****0000", "groupChatUnreadCount": 0, "travelers": [] } ], "total": 2, "page": 1, "pageSize": 20 }, "traceId": null, "success": true } ``` #### 空数据 / 降级响应 团期下没有活跃子订单(或 `page` 超出范围)时返回空页,信封与分页字段结构同上,`records` 为空数组,不返 404、不 500: ```json { "code": 200, "message": "成功", "data": { "records": [], "total": 0, "page": 1, "pageSize": 20 }, "traceId": null, "success": true } ``` #### 错误响应 ```json { "code": 589500, "message": "团期不存在", "data": null, "traceId": null, "success": false } ``` 无团期权限或该团期不在本人名下: ```json { "code": 589507, "message": "无操作权限(当前角色未授予团期权限,或该团期不在您名下)", "data": null, "traceId": null, "success": false } ``` #### 业务边界 - 业务失败走 HTTP 200 + `success:false` 的信封;未登录由网关返 401,不在本接口的码集里。 - `hotelRequirementStatus` / `vehicleRequirementStatus` 与各自的 `*StatusName` **必须成对读**:`Status` 为 `null` 且 `StatusName` 为 `"未提交"`,表示该户需要这项资源但一条需求行都没提交;两者**都**为 `null` 表示该户本来就不需要这项资源,页面渲染「—」。 - 不要再用 `status === 'PENDING'` 判断「没提交」——`PENDING` 现在唯一含义是「已提交、等资源侧接单」。判「没提交」的唯一判据是 `status == null`。 - `*StatusName` 在遇到枚举外的未知码时回落原 code,前端渲染保持 `statusName || status || '—'` 的三级兜底即可,不会拿到空串。 - 本接口只读,不写库;重复调用无副作用。 - `includeCancelled=false`(缺省)时已取消子订单不出现在结果里,`total` 同口径。 --- ### 2. GB-ADM-012 整团确认需求缺失预检 `GET /v3/admin/order/group-batch/{groupBatchId}/requirement/confirm-check` **VO**: `Path 参数 → GroupBatchRequirementCheckRespVO` #### 使用场景 管理后台在点「整体确认需求」**之前**调用,用于把按钮置灰并把缺失清单摊开给运营看。它与 `POST .../requirement/confirm` 共用同一套校验:预检里出现的每一条,都会在确认时以对应错误码抛出且**零写入**。鉴权走 `group-batch:demand:confirm`。本次改动新增了车侧的户级校验,并改写了车侧 `detail` 的文案。 #### 入参 | 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | |------|------|------|------|------|------| | groupBatchId | Path | Long | ✅ | 雪花 ID,团期聚合主键 | 团期 ID;无 Query、无 Body | #### 出参 `Result` | 字段 | 类型 | 说明 | |------|------|------| | data.groupBatchId | String | 团期聚合主键 | | data.batchStatus | String | 团期当前状态码 | | data.batchStatusName | String | 团期状态中文名;`batchStatus` 为 `null` 时为 `null` | | data.ready | Boolean | 是否可整体确认:`missing` 空 **且** `vehicleMissing` 空 **且**阶段可确认。`missing` 空而 `ready=false` 时原因在 `vehicleMissing` | | data.missing[] | Array | 住宿缺失清单,按 orderId 升序,每户至多一条 | | data.missing[].orderId | String | 子订单 ID | | data.missing[].orderNo | String | 子订单号 | | data.missing[].customerName | String | 客户姓名 | | data.missing[].consultantId | String | 定制师 adminId(字符串,便于拼跳转) | | data.missing[].consultantName | String | 定制师姓名快照 | | data.missing[].reason | String | 住宿缺失原因码,见六.5 | | data.missing[].reasonName | String | 住宿缺失原因中文名 | | data.missing[].dayNumber | Integer | 第几天;无该维度时 `null` | | data.missing[].segmentIndex | Integer | 段序;无该维度时 `null` | | data.missing[].expectedNights | Integer | 期望晚数;无该维度时 `null` | | data.missing[].actualNights | Integer | 实际晚数;无该维度时 `null` | | data.checkedResourceTypes | Array | 本次预检覆盖的资源类型,恒为 `["HOTEL","VEHICLE"]`;两类均已覆盖到逐户粒度 | | data.vehicleWaived | Boolean | 整团免车时为 `true`,此时车侧校验整体跳过、`vehicleMissing` 恒为空数组 | | data.vehicleMissing[] | Array | 车侧缺失清单,按校验顺序全部列出、不按户合并;`vehicleWaived=true` 时为空数组 | | data.vehicleMissing[].reason | String | **本次扩充**:车侧缺失原因码,新增 `HOUSEHOLD_REQUIREMENT_NOT_SUBMITTED`,见六.5 | | data.vehicleMissing[].groupCode | String | 涉及的乘车分组编码;无分组维度时 `null` | | data.vehicleMissing[].tripDate | String | 涉及日期(`yyyy-MM-dd`);无日期维度时 `null` | | data.vehicleMissing[].orderId | String | 涉及的子订单 ID;无订单维度时 `null` | | data.vehicleMissing[].orderNo | String | 子订单号快照;订单不属于本团期时 `null` | | data.vehicleMissing[].detail | String | **本次报文变更**:人话描述,与整团确认抛出的错误报文逐字相同,可直接展示;户级条目不再带雪花 orderId | | data.groupVehicleRequirementId | String | 当前活跃正式用车需求主键;无则 `null` | | data.groupVehicleRequirementStatus | String | 活跃正式用车需求状态;无则 `null`。`DISPATCHED` / `DONE` 同属预检可通过的正常状态 | | data.groupVehicleRequirementVersion | Integer | 活跃正式用车需求版本号;无则 `null` | | data.transferSubmitEnabled | Boolean | 当前环境是否开放接送机需求提交;`false` 时提交 `kind=TRANSFER` 会被 809004 拒 | | data.transferDeclaredWithoutRequirement[] | Array | 声明了接送机却没报 TRANSFER 需求的户(提示性,不进 `ready`、不阻断确认);无此类户时为空数组、不是 `null` | #### 请求示例 ```http GET /v3/admin/order/group-batch/2100856430494973953/requirement/confirm-check HTTP/1.1 Host: <网关地址> Authorization: Bearer ``` #### 响应示例 ```json { "code": 200, "message": "成功", "data": { "groupBatchId": "2100856430494973953", "batchStatus": "RESOURCE_PREPARING", "batchStatusName": "资源准备中", "ready": false, "missing": [ { "orderId": "2102309919002943489", "orderNo": "HL20260922161158291", "customerName": "王二麻子", "consultantId": "2083111674486693889", "consultantName": "刘畅", "reason": "NOT_SUBMITTED", "reasonName": "未提报", "dayNumber": null, "segmentIndex": null, "expectedNights": null, "actualNights": null } ], "checkedResourceTypes": ["HOTEL", "VEHICLE"], "vehicleWaived": false, "vehicleMissing": [ { "reason": "GROUP_REQUIREMENT_NOT_FOUND", "groupCode": null, "tripDate": null, "orderId": null, "orderNo": null, "detail": "团期「第3期 10月8日出发团」尚未形成正式用车需求" }, { "reason": "HOUSEHOLD_REQUIREMENT_NOT_SUBMITTED", "groupCode": null, "tripDate": null, "orderId": "2102309919002943489", "orderNo": "HL20260922161158291", "detail": "该户尚未提交行程用车需求,请先让定制师提交后再整团提交车务" } ], "groupVehicleRequirementId": null, "groupVehicleRequirementStatus": null, "groupVehicleRequirementVersion": null, "transferSubmitEnabled": true, "transferDeclaredWithoutRequirement": [] }, "traceId": null, "success": true } ``` #### 空数据 / 降级响应 需求齐备、可以整体确认时,两份清单都是空数组(不是 `null`),`ready` 为 `true`;字段集与上例完全相同: ```json { "code": 200, "message": "成功", "data": { "groupBatchId": "2100856430494973953", "batchStatus": "RESOURCE_PREPARING", "batchStatusName": "资源准备中", "ready": true, "missing": [], "checkedResourceTypes": ["HOTEL", "VEHICLE"], "vehicleWaived": false, "vehicleMissing": [], "transferDeclaredWithoutRequirement": [] }, "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` **不按户合并、不短路**:团级的 `GROUP_REQUIREMENT_NOT_FOUND` 与户级的 `HOUSEHOLD_REQUIREMENT_NOT_SUBMITTED` 会同时出现(实测响应即如此)。前者说「团级需求没形成」,后者说「是谁挡着它形成」,两条的处置对象不同,都要渲染出来。 - 区分条目是团级还是户级的唯一结构化判据是 `orderId` / `orderNo` 是否为 `null`,**不要靠解析 `detail` 文本**。户级条目恒带 `orderId` 与 `orderNo`。 - `missing` 为空但 `ready=false` 时,原因一定在 `vehicleMissing`;`vehicleWaived=true` 时车侧整体跳过,`vehicleMissing` 恒为空数组,这是合法逃生口不是异常。 - `transferDeclaredWithoutRequirement` 是提示性名单,不进 `ready`、不阻断确认,也不并进 `vehicleMissing`;`transferSubmitEnabled=false` 时这份名单**照报**,此时该提示「当前环境未开放接送机需求提交」而不是隐藏名单。 - 预检通过不等于确认一定成功:确认时的 CAS 冲突(809101 / 809112)只在写入阶段才可能发生,整团回滚、零写入。 --- ## 四、契约约束与正确调用方式 > 本节只写**后端返回什么、前端据什么判定**,不写 UI 渲染建议。 ### ✅ 正确 / ❌ 错误的判定写法 | 场景 | 判定写法 | |------|----------| | ✅ 判「该户没提交房需求」 | `hotelRequirementStatus === null && hotelRequirementStatusName === '未提交'`,或直接 `hotelRequirementStatus == null` | | ✅ 判「该户不需要房」 | `hotelRequirementStatus == null && hotelRequirementStatusName == null` | | ✅ 渲染状态列 | `statusName ?? status ?? '—'`(三级兜底,`null` 安全) | | ❌ 判「没提交」用 `status === 'PENDING'` | `PENDING` 现在唯一含义是「已提交、等资源侧接单」,这样写会把真正在排队的户也算成未提交 | | ❌ 从 `vehicleMissing[].detail` 里正则抠订单号 | 报文已改写,户级条目的 `detail` 里不再有 ID;订单号读 `vehicleMissing[].orderNo` | | ✅ 判车侧缺失条目是团级还是户级 | `item.orderId == null` → 团级(跳去团级用车需求编辑);否则户级(跳到该子订单) | | ❌ 假设 `vehicleMissing` 至多一条 | 团级与户级条目会并列出现,数组长度可以大于 1 | ### 报文变更涉及的调用口 下列写口在失败时抛出的 `message` 与预检 `detail` 同源,本次一起改变;它们的**请求契约、字段名、错误码码值都没有变**,变的只是给人看的那段文本: | 方法 | 路径 | 涉及错误码 | 弹窗场景 | |------|------|-----------|----------| | POST | `/v3/admin/order/group-batch/{groupBatchId}/requirement/confirm` | 809100 / 809103-809110 / 809122 / 809007 | 点「整体确认需求」,车侧不过时弹第一条违规的报文(零写入) | | PUT | `/v3/admin/order/group-batch/{groupBatchId}/vehicle-requirement` | 809107 / 809108 / 809109 / 809115 | 保存/提交乘车分组被校验拒绝 | | POST | `/v3/admin/order/group-batch/{groupBatchId}/vehicle-requirement/waive` | 809114 | 声明整团免车时车务已开工被拒 | | GET | `/v3/admin/order/group-batch/{groupBatchId}/requirement/confirm-check` | 同上全部 | 预检横幅里的 `vehicleMissing[].detail` | --- ## 五、数据库行为 本次两个变更接口都是只读端点,**零写入**。 | 前端动作 | 写库行为 | |----------|----------| | 调 `GET .../orders` | 无 | | 调 `GET .../requirement/confirm-check` | 无 | | 调 `POST .../requirement/confirm` 且车侧校验不过(含新增的 809122) | 无:阶段守卫 → 住宿缺失(589533)→ 车侧校验的顺序全部排在任何写入之前,抛出即整笔事务零写入 | 新增的 809122 不引入任何新表、新列、新状态值;它是对既有「该户有没有 active 行程用车需求行」的一次查询结果的表达。 --- ## 六、边界行为 - 未登录 → 401(网关拦截,不进业务码集)。 - 团期不存在 → HTTP 200 + `code=589500`。 - 无权限 / 团期不在本人名下 → HTTP 200 + `code=589507`。 - 老数据兼容:历史上没有 active 需求行的户,改前读出 `"PENDING"`、改后读出 `null`,**存量数据不迁移**,同一条记录在两个版本上读数不同,差异来自读取时的回落逻辑而非库里的值。 - 枚举外未知状态码:`*StatusName` 回落原 code,不抛异常、不返空串。 - 该户不需要房 / 不需要车时,对应的 `*Status` 与 `*StatusName` **都**是 `null`,前端渲染「—」。 - `vehicleMissing` 与 `transferDeclaredWithoutRequirement` 在「一条都没有」时是空数组,不是 `null`。 --- ## 六.5、枚举 / 数据字典 ### hotelRequirementStatus / vehicleRequirementStatus(RequirementStatus) **所属字段**: `GroupBatchOrderItemRespVO.hotelRequirementStatus` / `.vehicleRequirementStatus` | **类型**: `String` | 值 | 中文(房 / 车) | 说明 | |----|------|------| | `null` | 未提交 / `null` | **本次新增取值**:该户没有 active 需求行。该户需要这项资源 → `*StatusName` 为「未提交」;不需要 → `*StatusName` 也是 `null` | | `PENDING` | 待房务配 / 待车队配 | 已提交并放行,等资源侧接单 | | `PROCESSING` | 配房中 / 配车中 | 资源侧处理中 | | `DONE` | 配房完成 / 配车完成 | 资源侧完成 | | `PENDING_REVIEW` | 待审核 / 待提交车务 | 定制师已报、等管理员动作;车侧本列恒为行程用车(TRAVEL),故用「待提交车务」(#8218) | | `REJECTED_TO_CONSULTANT` | 已驳回定制师 | 打回定制师重提 | | `REJECTED_TO_ADMIN` | 已驳回管理员 | 打回管理员 | ### vehicleMissing[].reason(车侧缺失原因) **所属字段**: `GroupBatchRequirementCheckRespVO.VehicleMissingItem.reason` | **类型**: `String` | 值 | 中文 | 说明 | |----|------|------| | `GROUP_REQUIREMENT_NOT_FOUND` | 团级正式用车需求未形成 | 809100;团级维度,`orderId` / `orderNo` 为 `null` | | `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 | | `HEADCOUNT_LESS_THAN_MEMBERS` | 逐日用车人数小于当日成员户数 | 809110 | | `HOUSEHOLD_REQUIREMENT_NOT_SUBMITTED` | 该户尚未提交行程用车需求 | **本次新增**,809122;户级维度,恒带 `orderId` / `orderNo`;处置是催该户定制师提交 | | `TRANSFER_SERVICE_DATES_NOT_BACKFILLED` | 待放行接送机需求未回填服务日 | 809007;只带 `orderId` | ### missing[].reason(住宿缺失原因) **所属字段**: `GroupBatchRequirementCheckRespVO.MissingItem.reason` | **类型**: `String` | 值 | 中文 | 说明 | |----|------|------| | `NOT_SUBMITTED` | 未提报 | 该户需房但没有 active 房需求行(含被打回后未重提) | | `ROOM_CATEGORY_MISSING` | 缺房型 | 需求行存在但房型行缺失 | | `INVALID_REQUIREMENT` | 需求结构不合法 | days 结构不可用 | | `NIGHTS_MISMATCH` | 晚数不符 | 实际晚数与期望晚数不一致 | | `DAY_NUMBER_INVALID` | 天序不合法 | dayNumber 越界或重复 | ## 六.6、修改前后对比 ### 字段级对比 | 字段 | 改前 | 改后 | |------|------|------| | `GroupBatchOrderItemRespVO.hotelRequirementStatus` | 无 active 房需求行时回落 `"PENDING"` | 无 active 行时为 `null` | | `GroupBatchOrderItemRespVO.hotelRequirementStatusName` | 相应显示「待房务配」 | code 为 `null` 时:该户需房 → `"未提交"`;不需房 → `null` | | `GroupBatchOrderItemRespVO.vehicleRequirementStatus` | 无 active 车需求行时回落 `"PENDING"` | 无 active 行时为 `null` | | `GroupBatchOrderItemRespVO.vehicleRequirementStatusName` | 相应显示「待车队配」 | code 为 `null` 时:该户需车 → `"未提交"`;不需车 → `null` | | `GroupBatchRequirementCheckRespVO.vehicleMissing[].reason` | 11 个取值 | 追加 `HOUSEHOLD_REQUIREMENT_NOT_SUBMITTED`,共 12 个 | | `GroupBatchRequirementCheckRespVO.checkedResourceTypes` | 值仍是 `["HOTEL","VEHICLE"]`,但车侧只覆盖团级 | 值不变;车侧实际覆盖扩到逐户(这是把字段宣称的覆盖补齐,不是取值变化) | ### 报文变更(六个错误码的渲染文本) | 错误码 | 改前渲染 | 改后渲染 | |--------|----------|----------| | 809100 | `团期 2100856430494973953 尚未形成正式用车需求` | `团期「第3期 10月8日出发团」尚未形成正式用车需求`;团期名与期号快照都缺时为 `该团期尚未形成正式用车需求` | | 809107 | `子订单 2102309919002943489 不属于本团期,不能作为乘车成员` | `该子订单不属于本团期,不能作为乘车成员` | | 809108 | `子订单 2102309919002943489 在 2026-10-09 同时属于分组 BUS、SUV,同一户同一日只能属于一个分组` | `该子订单在 2026-10-09 同时属于分组 BUS、SUV,同一户同一日只能属于一个分组` | | 809109 | `子订单 2102309919002943489 的 2026-09-13 没有被任何乘车分组覆盖` | `该子订单的 2026-09-13 没有被任何乘车分组覆盖` | | 809114 | `车务已开工(团期 2100856430494973953:团级配车已就绪)` / `车务已开工(子订单 2102309919002943489:用车需求已到 DISPATCHED)` | `车务已开工(团期「第3期 10月8日出发团」:团级配车已就绪)` / `车务已开工(子订单 HL20260922161158291:用车需求已到 DISPATCHED)`;取不到订单号时为 `某子订单` | | 809115 | `团期 2100856430494973953 已声明整团免车,提交乘车分组前请先整份撤回(withdraw)回草稿` | `团期「第3期 10月8日出发团」已声明整团免车,提交乘车分组前请先整份撤回(withdraw)回草稿` | **码值一个都没变**,变的只是 `message` / `detail` 的文本。809107 / 809108 / 809109 是直接删掉标识符:这三条只出现在预检清单上,而清单行已经由 `orderNo` 字段结构化带了订单号,文本里再带一个雪花 ID 就是同一行上的第二个标识符。809114 保留标识符只把 ID 换成订单号:它只落在免车声明弹窗上,弹窗外没有第二处说明是哪一户。 ### 行为级对比 | 行为 | 改前 | 改后 | |------|------|------| | 团里有户一条需求都没提交时的列表显示 | 「待房务配 / 待车队配」(与同屏预检横幅的「未提报」矛盾) | 「未提交」 | | 车侧预检对「某户一行行程用车需求都没提交」 | 无任何表达位置,整团确认只报团级的 809100 | `vehicleMissing` 里多一条 809122,带 `orderId` / `orderNo` | | 团级缺席时是否还检查户级 | 团级缺席即返回,不再往下查 | 两层并列输出,团级缺席不短路户级 | | 整团确认在户级未提交时的结果 | 报 809100(运营据此去催车务,方向错了) | 报 809122(催该户定制师提交),零写入 | | 车务报文里的标识符 | 雪花 ID | 团期可读名称 / 订单号 / 直接省略 | ## 六.7、影响评估 - **是否破坏向后兼容**: 是(部分)。`*Status` 新增 `null` 取值;对 `detail` / `message` 做文本匹配或抠 ID 的逻辑会失效。字段名、字段数量、错误码码值、请求契约均未变。 - **前端是否必须同步上线**: 否。已核对 `hl-ui origin/v2.1` 的 `src/views/order-v2/batch/detail/components/RequirementTab.vue`:状态列渲染为 `r.hotelRequirementStatusName || r.hotelRequirementStatus || '—'`(`:812-814`),对 `null` 安全;车侧缺失清单按 `item.orderNo` 是否存在分流(`:654` `vehicleReqMissing`、`:708-714` `onVehicleMissingClick`),809122 条目带 `orderNo`,会被正确归为户级并跳到该子订单。当前前端不改也能正确工作。 - **前端 workaround 清理点**: 若页面上有「把 `PENDING` 当作未提交」的本地兜底判断,可以撤掉——后端现在用 `null` 表达「未提交」,语义是唯一的。 - **风险面**: 唯一需要人工确认的是「是否存在对 `vehicleMissing[].detail` 做字符串解析的代码」。上述两个消费点都只做原样展示,未发现解析逻辑。 ## 七、不影响范围 - **仅影响**: 管理后台「团期详情 → 查看需求」tab 的子订单状态列、整团确认预检横幅、车务相关弹窗文案。 - **零影响**: - 小程序端(mp)全部接口:本次改动只落在 `/v3/admin/` 前缀下。 - `GET /v3/admin/order/group-batch/{groupBatchId}/requirement-summary`(用房·汇总 / 用车·汇总):字段与取值一个没动。该页此前的「汇总加载失败」已定位为前端同 tick 重复请求被去重拦截器 abort 所致,mmg 已在 `hl-ui 4b33ccbd9` 修复并已在测试环境前端 dist 里(`2026-09-23 18:06` 构建),与本次后端改动无关。 - 房需求的提交 / 打回 / 放行链路:`missing[]` 的口径、589533 的触发条件与报文一律不变。 - 接送机(TRANSFER)相关:`transferSubmitEnabled`、`transferDeclaredWithoutRequirement`、809007 的语义与取值不变。 - 订单列表 / 订单详情 / 金额相关接口:未触及。 - 历史数据:存量不迁移;差异只来自读取时的回落逻辑。 - 数据库:无 DDL、无 Flyway 脚本、无新列。 --- ## 八、测试环境已验证 部署:`hl-order-service-v3` @ `dev-v3` `fc81fff1f`,jar 构建时间 `2026-09-23 18:36:51`,两个实例(8086 / 8186)均 UP。以下读数经网关实测: ``` GET /v3/admin/order/group-batch/2100856430494973953/orders → 200;26-2355(HL20260922161158291)hotelRequirementStatus=null / StatusName=未提交, vehicleRequirementStatus=null / StatusName=未提交 ✓ → 200;26-7060(HL20260918155619496)hotelRequirementStatus=PENDING_REVIEW / 待审核, vehicleRequirementStatus=PENDING_REVIEW / 待提交车务(无回归)✓ GET /v3/admin/order/group-batch/2100856430494973953/requirement/confirm-check → 200;vehicleMissing 里 GROUP_REQUIREMENT_NOT_FOUND 与 HOUSEHOLD_REQUIREMENT_NOT_SUBMITTED 两条同时出现(未被团级缺席短路)✓ → 809100 的 detail 已是「团期「第3期 10月8日出发团」尚未形成正式用车需求」(无雪花 ID)✓ → 809122 的条目带 orderId=2102309919002943489 / orderNo=HL20260922161158291 ✓ → checkedResourceTypes=["HOTEL","VEHICLE"]、vehicleWaived=false、ready=false ✓ GET /v3/admin/order/group-batch/2100856430494973953/requirement-summary → 200 × 3 次(3.56s / 2.11s / 2.55s),服务端日志零 ERROR / Exception ✓ ``` 验证团期: `groupBatchId=2100856430494973953`(第3期 10月8日出发团,2 户活跃子订单) --- ## 十、相关文档 - 关联 Issue: [wx/HL#8249](https://git.1814.love:8443/wx/HL/issues/8249) - 关联 PR: [wx/HL#8286](https://git.1814.love:8443/wx/HL/pulls/8286) - 同页状态文案的上一次分叉(`PENDING_REVIEW` 车侧按 kind 分叉): `changelogs-v2/2026-09/23_8218_行程用车状态文案按kind分叉-修改接口-管理后台.md` ## 关联 / 联系人 ### 链接 - **Issue**: [#8249](https://git.1814.love:8443/wx/HL/issues/8249) - **PR**: [#8286](https://git.1814.love:8443/wx/HL/pulls/8286) - **Merge commit**: [fc81fff1f](https://git.1814.love:8443/wx/HL/commit/fc81fff1fd21e4a002c417041ed3fbb5bc2ce96a) ### 联系人 - **后端负责人**: @wx