diff --git a/changelogs-v2/2026-09/30_8544_团期用车需求读口补中文名汇总草稿改读侧VO并列下发团级已确认座位-修改接口-管理后台.md b/changelogs-v2/2026-09/30_8544_团期用车需求读口补中文名汇总草稿改读侧VO并列下发团级已确认座位-修改接口-管理后台.md new file mode 100644 index 00000000..ceb59cc8 --- /dev/null +++ b/changelogs-v2/2026-09/30_8544_团期用车需求读口补中文名汇总草稿改读侧VO并列下发团级已确认座位-修改接口-管理后台.md @@ -0,0 +1,664 @@ +--- +schema: "hl-changelog/v2" +ticket: "8544" +title: "团期正式用车需求读口补状态中文名、汇总草稿改用读侧 VO、需求汇总并列下发团级已确认座位、子订单列表逐类下发用车需求行(#8544 #8545 #8619)" +consumer: "admin" +author: "wx(GIT)" +change_type: "修改接口" +backend_status: "deployed" +gateway_status: "not_required" +frontend_status: "pending" +frontend_owner: "" +frontend_ref: "" +target_release: "" +verified_at: "" +status_note: "PR #8625(覆盖 #8544 #8545)与 PR #8624(覆盖 #8619)均已 squash 合并 dev-v3(5f2f86bc9 / ce7cd238e9),hl-order-service-v3 dev-v3 分支已滚测试服(HEAD 5f2f86bc9,jar mtime 2026-09-30 08:19:48,两实例 Nacos 健康)。四个 GET 端点响应体的新增/变更字段均实测通过;PUT / withdraw / waive 三个写端点与 GET 共用同一个 GroupVehicleRequirementRespVO 类(源码级共用,非各自派生),因此 #8544 新增的 4 个 *Name 字段在这三个端点的响应里同样存在,字段语义与本文档「三、接口详情」第 2 条完全一致。" +updated_at: "2026-09-30" +base: "dev-v3" +--- + +# hl-order-service-v3: 团期正式用车需求读口补状态中文名、汇总草稿改用读侧 VO、需求汇总并列下发团级已确认座位、子订单列表逐类下发用车需求行 + +> **存放目录**: `changelogs-v2/2026-09/` +> **服务**: hl-order-service-v3 (端口 8086) +> **PR**: #8625(#8544 #8545)、#8624(#8619) +> **Issue**: #8544、#8545、#8619 +> **日期**: 2026-09-30 +> **影响范围**: 团期需求管理 Tab 下 4 个 GET 端点(全团需求汇总 / 读正式用车需求 / 自动汇总草稿 / 子订单列表)的响应体字段;PUT / withdraw / waive 三个写端点共用的响应体同步补齐字段(契约见「四」,未单开详情块) + +--- + +## ⚠️ 关键变化 + +- **#8544**:`GroupVehicleRequirementRespVO`(PUT / GET / withdraw / waive 四端点共用的响应体)新增 4 个只读中文名字段——`statusName` / `planRefreshStateName` / `blockedStageName` / `planRefreshStalledReasonName`,分别配对既有的 `status` / `planRefreshState` / `blockedStage` / `planRefreshStalledReason`。**认不出的编码一律给 `null`,不回落成编码原文**(与本 VO 既有的 `SpecialTagItem.name` / `GroupItem.vehicleTypeName` 同口径)。 +- 🔴 **这条 null 规则与看板列表的团期状态中文名口径是两套、刻意不同**:`GroupBatchConverter#resolveBatchStatusName`(团期列表页用)未知码会回落原编码;本次这 4 个字段所在的正式用车需求读口未知码给 `null`。前端不要把两处的「未知码兜底逻辑」当成同一套抄。 +- **#8544**:`GET .../vehicle-requirement/aggregate-draft` 的 `draft` 字段类型从写侧 `GroupVehicleRequirementSaveReqVO` 改为新的只读读侧类型 `GroupVehicleRequirementDraftRespVO`。JSON 形状基本不变(由 `GroupVehicleRequirementDraftRespVOFieldParityTest` 钉住与写侧字段集一致),**唯一新增字段是 `groups[].vehicleTypeName`**(只读车型中文名,不参与保存,原样 `PUT` 回去时被忽略);该 VO **不挂任何校验注解**(`@NotNull`/`@NotEmpty`/`@Size` 等),因为草稿可能违反若干条保存态约束,违规项另在 `violations` 里列出。 +- **#8544**:`draft.groups[].remark`(分组备注拼接文案)的逐户标签优先级改为 **团号(客户名)→ 团号 → 客户名 → 订单号 → "未知户"**,**不再回落雪花订单 ID**(原来兜底到裸数字 orderId 的情况,现在只有当团号/客户名/订单号三者都拿不到时才会出现,落成字面文案"未知户")。前端如果对这段 `remark` 文本做过正则匹配、高亮、或按"看起来像一串数字"识别订单号,需要同步更新——这段文本此后不会再出现裸数字订单 ID。 +- **#8545**:`GET .../requirement-summary` 的 `vehicleSeatSummary[]` 新增 `confirmedSeats` / `confirmedCount`(均为 `int`,**尚未整团确认时恒为 `0`,不是 `null`**)。这是"团级已确认"口径,与既有的 `totalSeats` / `totalCount`("逐户已提交"口径)**并列**下发、允许不相等——两者不等是常态不是缺陷,任何断言两者相等的前端逻辑都是错的。 +- 🔴 **#8545**:`vehicleSeatSummary[]` 可能新增一行只有 `confirmedSeats`/`confirmedCount` 非零、`totalSeats`/`totalCount` 恒为 `0` 的车型条目——发生在车务把车型整团定成了逐户报的车型集合之外的某个车型时(例如逐户都报 `mpv`,车务整团定成 `bus`)。前端渲染这张表时不能假设"有座位数就等于有逐户报",要按 4 个数字各自判断。 +- **#8619**:`GET .../orders` 的 `vehicleRequirementStatus` / `vehicleRequirementKind` 口径从"只看行程用车(TRAVEL)"放宽为"该户展示序首条活跃用车需求行"(行程用车优先、其次接送机)。🔴 **改前只提交了接送机需求的户,这两列恒为 `null`、`vehicleRequirementStatusName` 被渲染成"未提交"——这是一个错误的展示(该户其实已提交、可能已在审/已派车/已完成),本次已修复**。有行程用车需求行的户这两列读数不变。 +- **#8619**:`vehicleRequirementKind` 的**实际取值域**从恒为 `TRAVEL` 放宽为 `{TRAVEL, TRANSFER}`——`VehicleRequirementKind` 枚举本身没有新增第三个值,仍然只有这两个;变的是这一个响应字段过去被上游按 TRAVEL 过滤取行、现在按展示序取行,所以能观测到 `TRANSFER`。前端如果曾按"这一列永远是 TRAVEL"写死过图标/文案分支,需要按实际值渲染。 +- **#8619**:`orders` 新增 `vehicleRequirements[]`(该户全部活跃用车需求行,**恒非 `null`,0~2 条**,行程用车在前、接送机在后)。一户同时提交了行程用车与接送机时,两条需求行状态可能各自不同(例如 TRAVEL 已 `DONE`、TRANSFER 还在 `PENDING_REVIEW`),此时上面那组单值字段(`vehicleRequirementStatus`/`vehicleRequirementKind`)**只能表达其中一条**——按类别判断状态必须读这个数组,不要只读单值字段。 +- `orders` 端点其余 25 个既有字段(`hotelRequirementStatus`、`totalPrice`、`travelers` 等)本次未变,已由 changelog `30_8543_团期订单Tab用车状态按需求类别拆分并统一文案-修改接口-管理后台.md` 完整记录,此处不重复。 + +--- + +## 二、变更接口清单 + +| # | 接口 | 方法 | 路径 | 变更类型 | 说明 | +|---|------|------|------|----------|------| +| 1 | 全团需求汇总 | GET | `/v3/admin/order/group-batch/{groupBatchId}/requirement-summary` | 修改 | `vehicleSeatSummary[]` 新增 `confirmedSeats`/`confirmedCount`(#8545) | +| 2 | 读团期正式用车需求 | GET | `/v3/admin/order/group-batch/{groupBatchId}/vehicle-requirement` | 修改 | 响应体新增 4 个 `*Name` 字段(#8544),PUT/withdraw/waive 共用同一响应体同步生效 | +| 3 | 自动汇总正式用车需求草稿 | GET | `/v3/admin/order/group-batch/{groupBatchId}/vehicle-requirement/aggregate-draft` | 修改 | `draft` 字段改用只读读侧 VO,新增 `groups[].vehicleTypeName`(#8544) | +| 4 | 团期下子订单列表 | GET | `/v3/admin/order/group-batch/{groupBatchId}/orders` | 修改 | `vehicleRequirementKind` 取值域放宽、新增 `vehicleRequirements[]`(#8619) | + +--- + +## 三、接口详情 + +### 1. 全团需求汇总 `GET /v3/admin/order/group-batch/{groupBatchId}/requirement-summary` + +**VO**: `GroupRequirementSummaryRespVO` + +#### 使用场景 + +团期需求管理 Tab 打开时拉取的"全团需求汇总"卡片,展示逐日房间合计与大巴座位合计。本次变更只影响座位合计部分——`vehicleSeatSummary[]` 新增团级已确认口径的两个字段,供页面并列展示"定制师报了多少座"与"车务最后定了多少座"。 + +#### 入参字段表 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| groupBatchId | Path | Long | 是 | - | 团期聚合主键 | + +#### 出参字段表 + +| 字段 | 类型 | 说明 | +|------|------|------| +| activeOrderCount | int | 在团子订单数(仅排除 CANCELLED,含 COMPLETED),未变 | +| hotelRequirementCount | int | 有 active 用房需求记录的子订单数(兼容保留字段),未变 | +| hotelNeededOrderCount | int | 需要订房的户数,未变 | +| hotelFlagMismatchOrderCount | int | needsHotel≠true 却已提交有效用房需求的户数,未变 | +| hotelSubmittedOrderCount | int | 已提交有效用房需求且计入 dailyRoomBreakdown 的户数,未变 | +| vehicleRequirementCount | int | 已提交用车需求的子订单数,未变 | +| dailyRoomBreakdown | array | 逐日房间汇总,本次未变 | +| vehicleSeatSummary | array | 大巴座位汇总(按车型),本次新增字段见下表 | +| orderSpecialTags | array | 各子订单 specialTags,本次未变 | +| transferSummary | object | 接送机汇总(#8151 既有字段),本次未变 | + +`vehicleSeatSummary[]` 单项字段: + +| 字段 | 类型 | 说明 | +|------|------|------| +| vehicleType | string | 车型大类编码(suv/mpv/bus/sedan;缺失时为"未知"),未变 | +| vehicleTypeName | string | 车型大类中文名;查不到或车队服务不可用时为 `null`,未变 | +| totalSeats | int | 合计座位数(seats × count 之和);统计基数为"逐户已提交"的行程用车需求,未变 | +| totalCount | int | 合计车辆台数;统计基数为"逐户已提交"的行程用车需求,未变 | +| confirmedSeats 🆕 | int | 合计座位数(团级已确认的正式需求口径);尚未整团确认时为 `0`,与 totalSeats 不等是常态(#8545) | +| confirmedCount 🆕 | int | 合计车辆台数(团级已确认的正式需求口径);尚未整团确认时为 `0`(#8545) | + +#### 请求示例 + +```http +GET /v3/admin/order/group-batch/2104839654727618562/requirement-summary +``` + +#### 响应示例 + +以下为测试环境实测响应(团期批次 `2104840641651556353`,2026-09-30 采集;仅展示 `vehicleSeatSummary[0]`,其余字段结构未变不重复列出): + +```json +{ + "code": 200, + "message": "成功", + "data": { + "vehicleSeatSummary": [ + { + "vehicleType": "mpv", + "vehicleTypeName": "商务车", + "totalSeats": 14, + "totalCount": 2, + "confirmedSeats": 7, + "confirmedCount": 1 + } + ] + }, + "traceId": null, + "success": true +} +``` + +#### 空数据 / 降级响应 + +- 团期批次不存在:返回业务错误(见"错误响应"),`data` 为 `null`。 +- 全团无任何用车需求:`vehicleSeatSummary` 为空数组,不是 `null`。 +- 团级从未整团确认过(或已被重开成 `PENDING_RECONFIRM`):既有行的 `confirmedSeats`/`confirmedCount` 均为 `0`,不追加新行;此时该数组与改动前逐户口径的行完全一致。 +- 车务整团定的车型不在逐户已提交的车型集合内:追加一行 `totalSeats=0`/`totalCount=0`、`confirmedSeats`/`confirmedCount` 非零的记录,`vehicleTypeName` 仍会被正常补全。 + +#### 错误响应 + +```json +{"code": 589500, "message": "团期不存在", "success": false, "data": null} +``` + +```json +{"code": 589507, "message": "无操作权限(当前角色未授予团期权限,或该团期不在您名下)", "success": false, "data": null} +``` + +#### 业务边界 + +- `confirmedSeats`/`confirmedCount` 与 `totalSeats`/`totalCount` 是两个独立基数,前端不得假设两者相等或用其中一个推算另一个。 +- 权限码为 `group-batch:view`。 + +--- + +### 2. 读团期正式用车需求 `GET /v3/admin/order/group-batch/{groupBatchId}/vehicle-requirement` + +**VO**: `GroupVehicleRequirementRespVO` + +#### 使用场景 + +团期正式用车需求编辑页/详情页打开时的回填读口,同时是"配车计划刷新是否死了"在 admin 侧唯一无副作用的观测口(#7988)。`PUT`(保存)、`withdraw`(撤回)、`waive`(免车)三个写端点返回的响应体与本端点完全一致(同一个 Java 类),前端可以对四个端点复用同一套渲染逻辑。 + +#### 入参字段表 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| groupBatchId | Path | Long | 是 | - | 团期聚合主键 | + +#### 出参字段表 + +| 字段 | 类型 | 说明 | +|------|------|------| +| requirementId | string(Long 转字符串) | 正式需求主键,未变 | +| groupBatchId | string(Long 转字符串) | 团期聚合主键,未变 | +| status | string | 状态:DRAFT/CONFIRMED/DISPATCHED/DONE/PENDING_RECONFIRM/CANCELLED,未变 | +| statusName 🆕 | string | 状态中文名(草稿/已确认/已发车务/配车完成/待重新确认/已取消);status 为 null 或认不出的编码时为 `null`,不回落编码原文(#8544) | +| version | int | 乐观锁版本号,未变 | +| remark | string | 整份备注;撤回/免车会把操作追加进来(不覆盖原备注),超 500 字**从头部截**并以 `…` 开头,未变 | +| confirmedBy | string | 整份确认人,DRAFT 时为 null,未变 | +| confirmedAt | string(LocalDateTime) | 整份确认时间,DRAFT 时为 null,未变 | +| planRefreshState | string | 配车刷新状态原值:null=从未登记过刷新(多数团期正常态)/PENDING/DONE/FAILED,未变 | +| planRefreshStateName 🆕 | string | 配车刷新状态中文名(刷新中/刷新完成/刷新失败);为 null 或认不出的编码时为 `null`(#8544) | +| planRefreshReplayCount | int | 人工受控重投累计次数(管理员点确认触发,不含自动重试),上限 5,未变 | +| blockedStage | string | 团期阻断阶段快照:null=未阻断;非空为受控重开发生那一刻的团期状态码(GroupBatchStatus 全域,常见 RESOURCE_PREPARING/MATERIAL_PREPARING/PENDING_DEPARTURE),未变 | +| blockedStageName 🆕 | string | 阻断阶段中文名(如 资源准备中/物料准备中/待出发);为 null 或认不出的编码时为 `null`(#8544) | +| planRefreshStalled | boolean | 刷新是否已停滞、不会自愈(恒非 null);true=必须有人处置,未变 | +| planRefreshStalledReason | string | 停滞归因:STATE_FAILED/COMMAND_FAILED/TIMEOUT;未停滞时为 null,未变 | +| planRefreshStalledReasonName 🆕 | string | 停滞归因中文名(刷新已被判死/刷新命令已失败/刷新超时);未停滞或认不出编码时为 `null`(#8544) | +| planRefreshTimeoutAt | string(LocalDateTime) | 本轮刷新超时时刻;仅 PENDING 且已登记发起时刻时有值,未变 | +| planRefreshReplayExhausted | boolean | 人工重投额度是否已耗尽(恒非 null),未变 | +| groups | array | 全部乘车分组(整团免车态为空数组),结构未变,见下表 | +| exemptHouseholds | array | 仅保存草稿(PUT)响应填充,其余端点(含本 GET)恒为 `null`,未变 | + +`groups[]` 单项字段(未变,随 VO 一起下发本次新增的 4 个 `*Name` 兄弟字段): + +| 字段 | 类型 | 说明 | +|------|------|------| +| groupId | string(Long) | 分组主键 | +| groupCode | string | 分组键,直接作为车费 alloc_group | +| vehicleType | string | 车型文本/字典值 | +| vehicleTypeName | string | 车型中文名(既有字段,非本次新增) | +| serviceStartDate / serviceEndDate | string(LocalDate) | 本组服务起止日 | +| seats / count | int | 该组单车座位数/车辆数量;存量分组为 null 表示待填 | +| specialTags | array | 该组特殊诉求标签(code + name) | +| remark | string | 该组备注/其他诉求;存量分组为 null | +| totalSeatCount | int | 总座位数 = seats × count | +| maxHeadcount | int | 该组 days 里的最大用车人数 | +| remainingPassengerSeats | int | 余座(扣司机位后的可乘座位 − 最大用车人数) | +| days | array | 逐日用车人数与成员 | + +#### 请求示例 + +```http +GET /v3/admin/order/group-batch/2104839654727618562/vehicle-requirement +``` + +#### 响应示例 + +以下为测试环境实测响应(团期批次 `2104840641651556353`,2026-09-30 采集)。`planRefreshState` / `blockedStage` / `planRefreshStalledReason` 三列在库中为 `NULL`,对应 `*Name` 实测为 `null`: + +```json +{ + "code": 200, + "message": "成功", + "data": { + "status": "DONE", + "statusName": "配车完成", + "planRefreshState": null, + "planRefreshStateName": null, + "blockedStage": null, + "blockedStageName": null, + "planRefreshStalledReason": null, + "planRefreshStalledReasonName": null + }, + "traceId": null, + "success": true +} +``` + +#### 空数据 / 降级响应 + +- 团期尚未形成正式需求(从未 PUT 过):`data` 为 `null`(源码 `@ApiOperation` 明确标注"未形成时返回 null"),不是报错。 +- `planRefreshState`/`blockedStage`/`planRefreshStalledReason` 三列 DB 为 `NULL`(从未走过受控重开,绝大多数团期的正常态):对应的 3 个 `*Name` 字段一律为 `null`,不下发默认中文名。 +- `groups` 为空数组场景:整团免车态(waive 后)。 + +#### 错误响应 + +```json +{"code": 589500, "message": "团期不存在", "success": false, "data": null} +``` + +```json +{"code": 589507, "message": "无操作权限(当前角色未授予团期权限,或该团期不在您名下)", "success": false, "data": null} +``` + +#### 业务边界 + +- 4 个 `*Name` 字段是**只读派生展示字段**,不接受回传;`PUT` 请求体仍用原编码字段(`GroupVehicleRequirementSaveReqVO` 未新增字段)。 +- 认不出的编码 → 对应 `*Name` 为 `null`,**不回落原编码**——这与团期列表页 `GroupBatchConverter#resolveBatchStatusName`(未知码回落原码)是刻意不同的两套口径,不要混用同一套前端兜底逻辑。 +- 本端点权限码为 `group-batch:demand:confirm`,与 PUT/withdraw/waive/aggregate-draft 四个端点同码。 +- 本端点后续不会引入任何写操作(#7988 明确约束),可放心作为无副作用轮询口使用。 + +--- + +### 3. 自动汇总正式用车需求草稿 `GET /v3/admin/order/group-batch/{groupBatchId}/vehicle-requirement/aggregate-draft` + +**VO**: `GroupVehicleAggregateDraftRespVO`(外层诊断字段未变;`draft` 字段类型改为 `GroupVehicleRequirementDraftRespVO`) + +#### 使用场景 + +团期正式用车需求编辑弹窗首次打开、或点"重新汇总"时调用:按各子订单已提交的活跃行程用车需求自动生成一份分组草稿,`draft` 可原样 `PUT` 回 `/vehicle-requirement` 保存。本次变更只影响 `draft` 内部的类型与新增字段,外层的 `droppedFleetItems`/`staleHeadcountOrders`/`paddedOrderDays`/`seatOptionAdjusted`/`violations`/`exemptHouseholds` 六个诊断字段结构未变。 + +#### 入参字段表 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| groupBatchId | Path | Long | 是 | - | 团期聚合主键 | + +#### 出参字段表 + +| 字段 | 类型 | 说明 | +|------|------|------| +| draft | object 🆕类型变更 | 汇总草稿,类型由写侧 `GroupVehicleRequirementSaveReqVO` 改为只读读侧 `GroupVehicleRequirementDraftRespVO`,见下表 | +| droppedFleetItems | array | 多车型户被丢弃的车型,未变 | +| staleHeadcountOrders | array | 人数已过期的户,未变 | +| paddedOrderDays | array | 为覆盖出发~返回而补进的日期,未变 | +| seatOptionAdjusted | array | 座位被兜底调整到车型可选档位的户,未变 | +| violations | array | 草稿违反保存态校验的逐条诊断,未变 | +| exemptHouseholds | array | 提交不了的豁免户(订单不在定制中/团期冻结且未被打回),未变 | + +`draft`(`GroupVehicleRequirementDraftRespVO`)字段: + +| 字段 | 类型 | 说明 | +|------|------|------| +| version | int | 乐观锁版本号;原样回传给 PUT;尚未形成正式需求时为 `null` | +| remark | string | 整份需求备注,汇总草稿恒为 `null` | +| groups | array | 全部乘车分组,恒非 null,无可汇总内容时为空数组;见下表 | + +`draft.groups[]` 字段: + +| 字段 | 类型 | 说明 | +|------|------|------| +| groupId | string(Long) | 既有分组主键,汇总草稿恒为 `null` | +| groupCode | string | 分组键,直接作为车费 alloc_group | +| vehicleType | string | 车型大类编码(归一后的 key) | +| vehicleTypeName 🆕 | string | 车型大类中文名(只读,不参与保存);字典查不到时为 `null`,不回落编码原文(#8544,本类相对写侧唯一新增字段) | +| serviceStartDate / serviceEndDate | string(LocalDate) | 本组服务起止日 | +| seats / count | int | 该组单车座位数(存量/无档位可取时可能为 null)/车辆数量 | +| specialTags | array(string) | 该组特殊诉求标签编码数组 | +| remark | string | 该组备注;汇总时按"团号(客户名): 备注"拼接,超 500 字**从尾部截断** | +| days | array | 逐日用车人数与成员,见下表 | + +`draft.groups[].days[]` 字段: + +| 字段 | 类型 | 说明 | +|------|------|------| +| tripDate | string(LocalDate) | 团期行程日 | +| headcount | int | 该组该日用车人数(乘车人数,非户数) | +| memberOrderIds | array(string) | 该组该日实际乘车的子订单集合 | + +#### 请求示例 + +```http +GET /v3/admin/order/group-batch/2104839654727618562/vehicle-requirement/aggregate-draft +``` + +#### 响应示例 + +以下为测试环境实测响应(团期批次 `2104840641651556353`,2026-09-30 采集;`days` 实测为 7 天逐日行,此处只保留首日,其余日同形): + +```json +{ + "code": 200, + "message": "成功", + "data": { + "groupBatchId": "2104840641651556353", + "currentStatus": "DONE", + "draft": { + "version": 3, + "remark": null, + "groups": [ + { + "groupId": null, + "groupCode": "MPV", + "vehicleType": "mpv", + "vehicleTypeName": "商务车", + "serviceStartDate": "2026-11-11", + "serviceEndDate": "2026-11-17", + "seats": 7, + "count": 1, + "specialTags": [], + "remark": "26-3682(董海涛): 结伴出行客户,整团统一 7 座商务车,已与客户确认路线;26-0805(谢丽萍): 9月30日至10月3日行程用车,成人4人其中1位长者,行李较多需大后备箱", + "days": [ + { + "tripDate": "2026-11-11", + "headcount": 4, + "memberOrderIds": ["2104840641597030402", "2104840685708525570"], + "memberOrderCount": 2 + } + ] + } + ] + }, + "violations": [] + }, + "traceId": null, + "success": true +} +``` + +`groups[].remark` 的一户标识格式是**「团号(客户名)」**(`26-3682(董海涛)`),多户合并进同一车型组时以 `;` 连接。本次改动前该位置拼的是 19 位订单 ID,因此:**已确认落库的存量团期,其 `GET .../vehicle-requirement` 返回的 `groups[].remark` 仍是旧格式(`HL2026…` 订单号前缀)**——本单只改新生成的汇总草稿,不回写历史数据。前端不要按固定格式解析 `remark`,它是给运营看的自由文本。 + +#### 空数据 / 降级响应 + +- 团期批次不存在:返回业务错误(见"错误响应")。 +- 全团无可汇总的行程用车需求:`draft.groups` 为空数组。 +- 车型字典不可用:`vehicleTypeName` 降级为 `null`,`groups` 其余字段照常下发(不阻断整个响应)。 + +#### 错误响应 + +```json +{"code": 589500, "message": "团期不存在", "success": false, "data": null} +``` + +```json +{"code": 589507, "message": "无操作权限(当前角色未授予团期权限,或该团期不在您名下)", "success": false, "data": null} +``` + +```json +{"code": 809120, "message": "车队车型字典暂不可用,无法校验车型,请稍后重试", "success": false, "data": null} +``` + +有在团需车户缺少可汇总用车需求时(809121,触发条件已被 #8577 收窄——只提交了接送机的户不再算缺少): + +```json +{"code": 809121, "message": "团期 「第3期 10月8日出发团」 有 1 户缺少可汇总的用车需求,暂不能自动汇总:26-0480:未提交用车需求", "success": false, "data": null} +``` + +#### 业务边界 + +- `draft` 是**只读展示体**,不挂任何 `@NotNull`/`@NotEmpty`/`@Size` 校验注解;违反保存态约束的地方在 `violations` 里另行列出,不要用 `draft` 自身的字段是否为空来判断能不能保存。 +- `draft` 除 `groups[].vehicleTypeName` 外的字段集与写侧 `GroupVehicleRequirementSaveReqVO` 完全一一对应(由专门的字段一致性单测钉住),可以原样 `PUT` 回 `/vehicle-requirement`;`vehicleTypeName` 回传时会被后端忽略,车型以 `vehicleType` 编码为准。 +- `draft.groups[].remark` 的拼接标签口径:团号(客户名)→ 团号 → 客户名 → 订单号 → "未知户",不再回落裸雪花订单 ID。 +- 权限码与「2」相同(`group-batch:demand:confirm`),本端点同样不引入任何写操作。 + +--- + +### 4. 团期下子订单列表 `GET /v3/admin/order/group-batch/{groupBatchId}/orders` + +**VO**: `GroupBatchOrderItemRespVO`(30 个字段中本次只变更 4 个,见下表标注) + +#### 使用场景 + +团期详情页"子订单"Tab 的列表数据源,每户一行。本次变更聚焦用车需求相关的 4 个字段,其余 26 个字段(`hotelRequirementStatus`、`totalPrice`、`travelers` 等)未变,已由 `30_8543_团期订单Tab用车状态按需求类别拆分并统一文案-修改接口-管理后台.md` 完整记录。 + +#### 入参字段表 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| groupBatchId | Path | Long | 是 | - | 团期聚合主键 | +| page | Query | Integer | 否 | 缺省 1,<1 归一为 1 | 页码 | +| pageSize | Query | Integer | 否 | 缺省 20,<1 归一为 20,>200 截断为 200 | 每页条数 | +| includeTravelers | Query | Boolean | 否 | 缺省 true | 是否附出行人明细(证件号/手机号一律不返回) | +| includeNeeds | Query | Boolean | 否 | 缺省 true | 是否附房数/房型/特殊需求 | +| includeCancelled | Query | Boolean | 否 | 缺省 false | 是否含已取消子订单 | + +#### 出参字段表(仅列本次变更相关字段) + +| 字段 | 类型 | 说明 | +|------|------|------| +| vehicleRequirementStatus | string | 车需求状态:口径从"只看行程用车"改为"该户展示序首条活跃行"(行程用车优先、其次接送机)。该户一条活跃车需求行都没有时为 `null`(#8619) | +| vehicleRequirementStatusName | string | 状态中文名;未知码回落原 `code`(与 `vehicleRequirementKindName` 不同口径);`code` 为 `null` 时按该户 `needsVehicle` 分叉——`true` 出"未提交"、否则为 `null`。**#8619 起"未提交"只在两类需求都没报时出现** | +| vehicleRequirementKind | string | 本行车需求的用车类别:TRAVEL/TRANSFER。**#8619 起不再恒为 TRAVEL**——只报了接送机的户在这里下发 `TRANSFER`;无 active 行时为 `null` | +| vehicleRequirementKindName | string | 类别中文名:行程用车/接送机;认不出的类别给 `null`,**不回落编码**(与 `vehicleRequirementStatusName` 不同口径) | +| vehicleRequirements 🆕 | array | 该户全部活跃用车需求行(0~2 条:TRAVEL/TRANSFER 各至多一条),恒非 `null`,行程用车在前、接送机在后(#8619),见下表 | + +`vehicleRequirements[]` 单项字段: + +| 字段 | 类型 | 说明 | +|------|------|------| +| kind | string | 需求类别:TRAVEL=行程用车 / TRANSFER=接送机 | +| kindName | string | 类别中文名;认不出的类别给 `null`,不回落编码 | +| status | string | 该条需求状态:PENDING/PROCESSING/DONE/PENDING_REVIEW/REJECTED_TO_CONSULTANT/REJECTED_TO_ADMIN | +| statusName | string | 状态中文名;PENDING_REVIEW 按 kind 分两套文案:TRAVEL="待提交车务"、TRANSFER="待审核" | + +#### 请求示例 + +```http +GET /v3/admin/order/group-batch/2104839654727618562/orders?page=1&pageSize=20 +``` + +#### 响应示例 + +以下为测试环境实测响应(团期批次 `2104839654727618562`,2026-09-30 采集)。每条 `records[]` 实测另有 26 个本次未变的字段,此处只保留与本单相关的 5 个,其余省略(完整字段集见 changelog `30_8543`): + +```json +{ + "code": 200, + "message": "成功", + "data": { + "records": [ + { + "orderId": "2104839654652121090", + "vehicleRequirementStatus": "DONE", + "vehicleRequirementStatusName": "配车完成", + "vehicleRequirementKind": "TRAVEL", + "vehicleRequirementKindName": "行程用车", + "vehicleRequirements": [ + {"kind": "TRAVEL", "kindName": "行程用车", "status": "DONE", "statusName": "配车完成"}, + {"kind": "TRANSFER", "kindName": "接送机", "status": "PENDING", "statusName": "待车队配"} + ] + }, + { + "orderId": "2104839686486888449", + "vehicleRequirementStatus": "DONE", + "vehicleRequirementStatusName": "配车完成", + "vehicleRequirementKind": "TRAVEL", + "vehicleRequirementKindName": "行程用车", + "vehicleRequirements": [ + {"kind": "TRAVEL", "kindName": "行程用车", "status": "DONE", "statusName": "配车完成"} + ] + }, + { + "orderId": "2104839729176514562", + "vehicleRequirementStatus": "DONE", + "vehicleRequirementStatusName": "配车完成", + "vehicleRequirementKind": "TRAVEL", + "vehicleRequirementKindName": "行程用车", + "vehicleRequirements": [ + {"kind": "TRAVEL", "kindName": "行程用车", "status": "DONE", "statusName": "配车完成"}, + {"kind": "TRANSFER", "kindName": "接送机", "status": "PENDING_REVIEW", "statusName": "待审核"} + ] + } + ], + "total": 3, + "page": 1, + "pageSize": 20 + }, + "traceId": null, + "success": true +} +``` + +上例里三户的 `vehicleRequirements` 长度分别为 2 / 1 / 2,`vehicleRequirementStatus` 与 `Kind` 取的都是展示序首条(TRAVEL 优先),因此三户顶层都是 `TRAVEL`;接送机那一行的真实状态只在 `vehicleRequirements[]` 里看得到。 + +#### 空数据 / 降级响应 + +- 团期批次不存在:返回业务错误(见"错误响应")。 +- 团期下暂无子订单(或 `includeCancelled=false` 时全部已取消):`records` 为空数组,`total=0`。 +- 该户一条活跃用车需求行都没有:`vehicleRequirementStatus`/`vehicleRequirementKind` 均为 `null`,`vehicleRequirements` 为**空数组**(不是 `null`);`vehicleRequirementStatusName` 按该户 `needsVehicle` 分叉。 +- 该户只提交了接送机需求(#8619 修复的场景):`vehicleRequirementKind` 下发 `TRANSFER`、`vehicleRequirementStatus` 下发该行程的真实状态,不再是 `null`/"未提交"。 + +#### 错误响应 + +```json +{"code": 589500, "message": "团期不存在", "success": false, "data": null} +``` + +```json +{"code": 589507, "message": "无操作权限(当前角色未授予团期权限,或该团期不在您名下)", "success": false, "data": null} +``` + +#### 业务边界 + +- 按用车类别判断状态一律读 `vehicleRequirements[]`,不要只读 `vehicleRequirementStatus`/`vehicleRequirementKind` 单值字段——一户两类需求并存时单值字段只能表达展示序首条。 +- `vehicleRequirementKindName` 与 `vehicleRequirementStatusName` 是两套不同的未知码兜底口径(前者给 `null`,后者回落原码),不要用同一段兜底逻辑处理。 +- **🔴 同一个 `status` 编码在不同 `kind` 上的中文名不同,前端不能自建 code→中文 映射表**:`PENDING_REVIEW` 在 `TRAVEL` 行下发「待提交车务」(推走它的是团期管理员整团一次的「提交车务」,该状态上没有逐户审核动作),在 `TRANSFER` 行下发「待审核」(有逐户审核动作)——这是 #8218 起的刻意分叉,声明见 `GroupVehicleHouseholdsRespVO`/`GroupBatchOrderItemRespVO` 的 `@ApiModelProperty`。两侧均有实测样本:批次 `2104839654727618562` 的 `2104839729176514562` 户 TRANSFER 行 = `PENDING_REVIEW`/「待审核」;批次 `2104840641651556353` 的 `2104840685708525570` 户 TRAVEL 行 = `PENDING_REVIEW`/「待提交车务」。**一律直接渲染后端下发的 `statusName`。** +- 权限码为 `group-batch:view`。 + +--- + +## 四、契约约束与正确调用方式 + +- **4 个新增 `*Name` 字段(#8544)是只读展示字段**:`statusName`/`planRefreshStateName`/`blockedStageName`/`planRefreshStalledReasonName` 只在响应体里出现,`PUT` 请求体 `GroupVehicleRequirementSaveReqVO` 未新增任何字段,回传这些字段会被忽略。 +- **PUT `/vehicle-requirement`、POST `/vehicle-requirement/withdraw`、POST `/vehicle-requirement/waive` 三个写端点与本文档「三、2」共用完全同一个 `GroupVehicleRequirementRespVO` 类**:调用这三个端点后,响应体里同样带有 4 个新增 `*Name` 字段,字段语义、null 规则与「三、2」逐字一致,无需前端另写一套解析。 +- **`aggregate-draft` 的 `draft` 字段类型变更(#8544)**:TypeScript/接口类型定义如果之前直接复用了 `GroupVehicleRequirementSaveReqVO` 的类型作为 `draft` 的类型,需要改成新类型(多一个只读字段 `groups[].vehicleTypeName`,其余字段名与类型逐一相同)。JSON 结构层面对已有解析代码零破坏,只有严格 schema 校验(如果有)需要放开这个新字段。 +- **未知/认不出的编码统一规则**(本次涉及的所有 `*Name` 字段):`statusName`/`planRefreshStateName`/`blockedStageName`/`planRefreshStalledReasonName`/`draft.groups[].vehicleTypeName`/`vehicleRequirementKindName`/`vehicleRequirements[].kindName` 这一组字段认不出编码一律给 `null`,**不回落原编码**。这与 `orders` 端点的 `vehicleRequirementStatusName`(未知码回落原码,#8543/#8619 未改动此口径)以及团期列表页的团期状态中文名(同样回落原码)是**刻意不同**的两套口径,前端不要用同一段兜底组件处理。 +- **`vehicleSeatSummary[].confirmedSeats`/`confirmedCount` 与 `totalSeats`/`totalCount` 不相等是设计上允许的常态**(#8545),不要写断言校验两者相等,也不要用其中一组数字反推另一组。 +- **`orders` 端点判断某户是否提交了某类用车需求,一律遍历 `vehicleRequirements[]` 按 `kind` 过滤**,不要依赖单值字段 `vehicleRequirementStatus`/`vehicleRequirementKind`(#8619 起单值字段只表达展示序首条,可能丢失第二类需求的状态)。 + +--- + +## 六、边界行为 + +- 未登录访问:网关拦截,不进入本文档描述的业务逻辑。 +- 权限不足(当前角色未获授团期权限,或该团期不在本人名下):所有 4 个端点统一报 `589507`。 +- 团期不存在:所有 4 个端点统一报 `589500`。 +- `GET .../vehicle-requirement` 团期尚未形成正式需求:`data` 为 `null`,HTTP 200,不是错误。 +- `planRefreshState`/`blockedStage`/`planRefreshStalledReason` 三列 DB 为 `NULL`(绝大多数团期的正常态,从未走过受控重开):对应 3 个 `*Name` 字段一律为 `null`。 +- `vehicleSeatSummary[]`:团级从未确认过时 `confirmedSeats`/`confirmedCount` 恒为 `0`(不是 `null`);车务定的车型在逐户报的车型集合之外时会新增一行 `totalSeats=0`/`totalCount=0` 的记录。 +- `orders` 端点 `vehicleRequirements[]`:该户没有任何活跃用车需求行时为空数组(不是 `null`)。 + +--- + +## 六.5、枚举 + +**所属字段**:`GroupVehicleRequirementRespVO.status` / `statusName` + +| 值 | 中文名 | 说明 | +|----|--------|------| +| DRAFT | 草稿 | 团期管理员正在汇总逐户需求、编辑分组与逐日人数 | +| CONFIRMED | 已确认 | 整份确认通过,等待发车务 | +| DISPATCHED | 已发车务 | 已推送到车务侧,等待配车 | +| DONE | 配车完成 | 车务侧已完成整团配车 | +| PENDING_RECONFIRM | 待重新确认 | 确认后团期人数/成员发生变化,需要重新确认整份 | +| CANCELLED | 已取消 | - | + +**所属字段**:`GroupVehicleRequirementRespVO.planRefreshState` / `planRefreshStateName` + +| 值 | 中文名 | 说明 | +|----|--------|------| +| null(列值 NULL) | (无中文名,字段为 null) | 从未登记过任何刷新,多数团期的正常态 | +| PENDING | 刷新中 | 刷新命令已登记,等 fleet 刷完 | +| DONE | 刷新完成 | 就绪回调已通过判定并应用,本轮刷新闭环 | +| FAILED | 刷新失败 | 已耗尽重试预算或被 fleet 判定性终结;不放行团期,出团门②持续拒绝 | + +**所属字段**:`GroupVehicleRequirementRespVO.planRefreshStalledReason` / `planRefreshStalledReasonName` + +| 值 | 中文名 | 说明 | +|----|--------|------| +| STATE_FAILED | 刷新已被判死 | `plan_refresh_state` 已经是 FAILED | +| COMMAND_FAILED | 刷新命令已失败 | 状态列仍是 PENDING,但刷新命令已终态失败(回写钩子未落成) | +| TIMEOUT | 刷新超时 | 状态列仍是 PENDING、命令也未判死,但已超过本轮窗口时限(缺省 120 分钟) | + +**所属字段**:`GroupVehicleRequirementRespVO.blockedStage` / `blockedStageName` + +取值域是团期状态枚举 `GroupBatchStatus`(既有枚举,本次未新增值)的全域,本次只是给这个已有编码字段配了中文名。常见值举例: + +| 值 | 中文名 | 说明 | +|----|--------|------| +| RESOURCE_PREPARING | 资源准备中 | - | +| MATERIAL_PREPARING | 物料准备中 | - | +| PENDING_DEPARTURE | 待出发 | - | +| (其余 GroupBatchStatus 取值) | 对应中文名 | 认不出的编码给 `null` | + +**所属字段**:`orders[].vehicleRequirementKind` / `vehicleRequirementKindName`、`orders[].vehicleRequirements[].kind` / `kindName` + +`VehicleRequirementKind` 枚举本身固定只有 2 个值(本次未新增第三个值,放宽的是响应字段的**实际观测取值范围**,不是枚举定义): + +| 值 | 中文名 | 说明 | +|----|--------|------| +| TRAVEL | 行程用车 | 服务日冻结为行程日 | +| TRANSFER | 接送机 | 服务日取航班/车次日期,允许落在行程日窗外 | + +--- + +## 六.6、修改前后对比 + +| 项 | 改前 | 改后 | +|----|------|------| +| `GroupVehicleRequirementRespVO` 状态/刷新状态/阻断阶段/停滞归因 | 仅下发编码,前端自行映射中文 | 新增 4 个 `*Name` 字段直接下发中文名;认不出编码给 `null` | +| `aggregate-draft.draft` 字段类型 | 写侧 `GroupVehicleRequirementSaveReqVO`(挂校验注解,多余的约束会渲染进 Swagger) | 只读读侧 `GroupVehicleRequirementDraftRespVO`(不挂校验注解,多一个只读字段 `vehicleTypeName`) | +| `draft.groups[].remark` 逐户标签兜底 | 团号(客户名)→ 团号 → 客户名 → 订单号(裸雪花 ID) | 团号(客户名)→ 团号 → 客户名 → 订单号 → "未知户"(不再回落裸雪花 ID) | +| `requirement-summary.vehicleSeatSummary[]` | 只有 `totalSeats`/`totalCount`(逐户已提交口径) | 并列新增 `confirmedSeats`/`confirmedCount`(团级已确认口径),可能新增车型行 | +| `orders[].vehicleRequirementStatus`/`Kind` | 只按 TRAVEL 过滤取行;只提交接送机的户恒为 `null`/"未提交" | 取展示序首条活跃行(TRAVEL 优先、TRANSFER 次之);只提交接送机的户下发真实状态 | +| `orders[].vehicleRequirements` | 不存在该字段 | 新增,0~2 条,逐类下发状态 | + +## 六.7、影响评估 + +- **前端必改**:如果曾对 `draft.remark` 文本做正则匹配来提取订单号或高亮逐户标签,需要兼容新的"未知户"文案且不再假设会出现裸数字订单 ID。**并且请不要按任何固定格式解析 `remark`**——本单只改新生成的汇总草稿,已确认落库的存量团期其 `GET .../vehicle-requirement` 返回的 `groups[].remark` 仍是改前格式(订单号前缀,形如 `HL20260929154809598: …`),不做历史数据回写。该字段是给运营看的自由文本,两种格式会长期并存。 +- **前端必改**:如果曾假设 `orders[].vehicleRequirementKind` 恒为 `TRAVEL` 来做图标/文案硬编码分支,需要改为按实际值(`TRAVEL`/`TRANSFER`)渲染,并优先改用 `vehicleRequirements[]` 数组按类判断。 +- **前端可选增强**:可以直接展示后端下发的 4 个新增中文名字段,替换掉前端此前自行维护的编码→中文映射表(如果有)。 +- **前端需知悉但不需要立即改**:`vehicleSeatSummary[]` 新增的两个字段、可能新增的车型行,只在页面展示这两个数字时才需要处理;不展示则忽略即可,不影响既有渲染。 +- **零风险**:所有新增字段都是**在既有 JSON 对象上新增键**,未删除、未改名任何既有字段(`aggregate-draft.draft` 虽改了后端类型,但 JSON 键集合与既有字段类型未变,由专门单测钉住);未做严格 schema 校验的前端代码可无感兼容。 + +--- + +## 七、不影响范围 + +- `GroupVehicleRequirementSaveReqVO`(PUT 请求体)字段集未变,仍是原有编码字段集,本次新增的只读字段不参与保存也不会被写侧读取。 +- `GroupVehicleRequirementDraftRespVO` 除 `groups[].vehicleTypeName` 外的全部字段(顶层 `version`/`remark`、`groups[]` 内 `groupId`/`groupCode`/`vehicleType`/`serviceStartDate`/`serviceEndDate`/`seats`/`count`/`specialTags`/`days`)未变,由 `GroupVehicleRequirementDraftRespVOFieldParityTest` 钉住与写侧字段集一致。 +- `VehicleRequirementKind` 枚举定义本身未变,仍只有 `TRAVEL`/`TRANSFER` 两个值。 +- 团期用房相关端点(`hotel-households` 等)不在本次改动范围内。 +- `orders` 端点除本文档列出的 4 个字段外,其余 26 个字段未变(详见 changelog `30_8543`)。 +- 809121/809122/809123 三个错误码的触发条件收窄与文案改写属于 #8577,不在本次三张工单范围内,详见 changelog `30_8577_只提交接送机的户不再被判未提交用车需求-修改接口-管理后台.md`。 + +--- + +## 八、测试环境已验证 + +- hl-order-service-v3 dev-v3 分支已部署测试网关,HEAD `6373e5cf2`(`git merge-base --is-ancestor` 核实本单提交 `5f2f86bc96` 已在其祖先链内),jar mtime 2026-09-30 09:27:12,两个实例 Nacos 健康检查均为 healthy。 +- 以下四条读口实测取样自团期批次 `2104840641651556353`(`vehicleRequirements[] == []` 一条取样自 `2104830530031919106`),2026-09-30 采集。 +- `GET .../requirement-summary` 实测:`vehicleSeatSummary[0]` = `{"vehicleType":"mpv","vehicleTypeName":"商务车","totalSeats":14,"totalCount":2,"confirmedSeats":7,"confirmedCount":1}`。 +- `GET .../vehicle-requirement/aggregate-draft` 实测:`draft.groups` 非空,含 `vehicleTypeName: "商务车"`;`groups[0].remark` 为「团号(客户名)」格式(`26-3682(董海涛): …;26-0805(谢丽萍): …`),未出现裸雪花订单 ID。 +- `GET .../orders` 实测:2 条子订单,每条 `vehicleRequirements` 键均存在且非 `null`,长度分别为 2 与 1。另在一个无活跃用车需求行的户上实测该键为**空数组 `[]` 而不是 `null`**(与 `GroupBatchConverter.toVehicleRequirementItems()` 从 `new ArrayList<>()` 起手、无 null 分支一致)。 +- `GET .../vehicle-requirement` 实测:顶层键包含 `status`/`statusName`/`planRefreshState`/`planRefreshStateName`/`blockedStage`/`blockedStageName`/`planRefreshStalledReason`/`planRefreshStalledReasonName` 共 8 个;观测样本 `status="DONE"` → `statusName="配车完成"`;另外三个 DB 列为 `NULL`,对应 `*Name` 字段均实测为 `null`。 +- **「认不出的编码 → `null`」这条行为本轮没有实测样本**:候选团期的 `plan_refresh_state` / `blocked_stage` / `plan_refresh_stalled_reason` 三列在库中全为 `NULL`,测试环境里造不出一个库内存着字典外编码的自然样本。该行为由源码与单测两侧钉住:`GroupVehiclePlanRefreshState.labelOf()`、`GroupBatchStatus.descOf()` 对未命中编码返回 `null`(不回落编码原文)。前端按 `null` 兜底渲染即可。 +- 单元测试:hl-order-service-v3 模块 `Tests run: 561, Failures: 0, Errors: 0, Skipped: 0`,38 个相关测试类逐类点名核对报告文件均存在(38/38 命中);`nested_selector_census` 退出码 0(无 `@Nested` 类被静默漏跑)。 +- ArchUnit/架构门禁测试:`Tests run: 167`,全绿。 + +--- + +## 十、相关文档 + +- `docs/CODE_RULES.md` §3(VO 命名与读写侧分离约定) +- changelog `30_8543_团期订单Tab用车状态按需求类别拆分并统一文案-修改接口-管理后台.md`(`orders` 端点其余字段的完整文档,及 `vehicleRequirementKind` 字段本次改动前的基线状态) +- changelog `30_8577_只提交接送机的户不再被判未提交用车需求-修改接口-管理后台.md`(PUT/aggregate-draft/confirm-check 三处 809121/809122/809123 错误码本次收窄的完整文档) + +--- + +## 关联 / 联系人 + +- 关联 Issue:#8544、#8545、#8619 +- 关联 PR:#8625(#8544 #8545)、#8624(#8619) +- 联系人:wx