diff --git a/changelogs-v2/2026-09/23_8249_查看需求页未提交状态与户级用车预检-修改接口-管理后台.md b/changelogs-v2/2026-09/23_8249_查看需求页未提交状态与户级用车预检-修改接口-管理后台.md new file mode 100644 index 00000000..5691d9c5 --- /dev/null +++ b/changelogs-v2/2026-09/23_8249_查看需求页未提交状态与户级用车预检-修改接口-管理后台.md @@ -0,0 +1,644 @@ +--- +schema: "hl-changelog/v2" +ticket: "8249" +title: "团期「查看需求」:无需求行的户不再报 PENDING、户级未提交用车需求进预检清单、六条车务报文改写为可读名称" +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: "" +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