diff --git a/changelogs-v2/2026-09/29_frontend_团期详情查看需求页签重做为一键审核弹窗-前端优化-管理后台.md b/changelogs-v2/2026-09/29_frontend_团期详情查看需求页签重做为一键审核弹窗-前端优化-管理后台.md new file mode 100644 index 00000000..60356e21 --- /dev/null +++ b/changelogs-v2/2026-09/29_frontend_团期详情查看需求页签重做为一键审核弹窗-前端优化-管理后台.md @@ -0,0 +1,2070 @@ +--- +schema: "hl-changelog/v2" +ticket: "frontend" +title: "团期详情「查看需求」页签重做为一键审核弹窗:页签改只读总览加唯一主按钮,审核打回确认收进三步弹窗,全部调用既有接口,后端零改动" +consumer: "admin" +author: "wx(GIT)" +change_type: "前端优化" +backend_status: "not_required" +gateway_status: "not_required" +frontend_status: "pending" +frontend_owner: "" +frontend_ref: "" +target_release: "" +verified_at: "" +status_note: "后端零改动,17 个既有接口;三种提交模式的调用顺序、确认后接送机对账补放行、已确认态只可通过不可打回见正文四;4 项字段缺口本期不展示" +updated_at: "2026-09-29" +base: "dev-v3" +--- + +# 团期需求: 「查看需求」页签重做为一键审核弹窗 + +> **服务**: hl-order-service-v3(本条后端零改动,涉及接口全部为既有接口) +> **PR**: 无(纯前端改造) +> **Issue**: 无(ticket=frontend) +> **日期**: 2026-09-29 +> **影响范围**: 管理后台 → 订单管理 → 团期列表 → 出团详情 →「查看需求」页签(`src/views/order-v2/batch/detail/components/RequirementTab.vue` 及其子组件) +> **设计稿**: 「设计稿」https://claude.ai/artifact/R2b6RsiUvykdF9kBUdaQcC +> **源码基线**: 后端 `origin/dev-v3@7b702f5c`;前端 hl-ui `origin/v2.1@cdfaf793` + +--- + +## ⚠️ 关键变化(非必须,本版与上版行为不同 / 纠错 / 撤销时必写) + +- **页面结构变了**:原来是「汇总块 + 逐户块 + 正式用车需求块」平铺,顶部四个按钮;新设计是**只读总览**(状态条 + 三张汇总卡 + 逐晚用房 + 用车安排 + 逐户需求)加**唯一主按钮**「审核并确认需求」,点开是**三步审核弹窗**(①住房 ②接送机 ③行程用车)。 +- **取消的按钮**:「打回选中户」「新增正式行程用车需求」「更多」「整团确认需求」(`RequirementTab.vue:53 / :68 / :77 / :86`)。**取消的只是按钮,能力不取消**:整团免车(waive)进弹窗③「整团不用车」;撤回(withdraw)与受控重开(reopen)仍须保留入口,位置前端自定(见四)。 +- **四处以前容易想错的地方**(均已对源码核实): + 1. **确认后不能逐户打回**。需求已确认之后还能出现「待审核」行的,只有团期过了资源准备阶段后的接送机变更;这时团期阶段不是 `RESOURCE_PREPARING`,逐户打回报 **589501**。所以已确认态的「审核变更」**只有「通过」没有「打回」**。 + 2. **整团确认不一定放行全部接送机**。整团免车时一条车需求都不放行(含待审接送机);`needs_vehicle≠1` 的户报了接送机也不在放行集合里。确认成功后必须用响应里的 `transferDispatchedOrderIds` 对账,没放行的另调 `transfer/batch-confirm`。 + 3. **批量打回端点不覆盖接送机**。`POST …/requirement/reject` 的 `VEHICLE` 只打回行程用车,且整批只收一条原因;新设计每户各有原因、要分 kind 打回,**不用它**,改逐户打回。 + 4. **逐户打回(任何 kind)都会清团级确认标记**。所以同一次提交里,打回必须排在 `requirement/confirm` **之前**调;顺序反了,刚写上的确认标记会被打回清掉。 + +--- + +## 一、背景(选填) + +运营反馈原页签按钮多、确认前要在四个板块之间来回切,确认失败时不知道卡在哪一户。新设计把「看」和「审」分开:页签只读,所有写操作收进一个弹窗,底部按打回情况自动切换三种提交模式。 + +**本条与今天两条既有条目的关系**(两条都已由前端交付,与本条不冲突): + +| 既有条目 | 前端提交 | 与本条的关系 | +|------|------|------| +| `changelogs-v2/2026-09/29_frontend_团期详情用车汇总行程用车与接送机改逐条列出-前端优化-管理后台.md` | `a73ef932` | 它定的**数据口径**(按 `kind` 拆行程用车与接送机、`statusName` 原文直显、不按 `countedInSummary` 过滤)继续有效;它改的「用车汇总」块在本条被新设计 A 的「用车安排」吸收。**布局以本条为准,字段口径以原条为准。** | +| `changelogs-v2/2026-09/29_frontend_团期详情用车需求确认弹窗点确认不发请求-前端缺陷-管理后台.md` | `01acf773` | 它修的三处调用点(`VehicleHouseholdsSection.vue:334 / :387`、`RequirementTab.vue:588`)在新设计里随原组件一起删除;**新写的所有确认对话框同样必须用 `onPositiveClick`**,不要用 `onPositive`。 | + +**涉及文件(hl-ui `origin/v2.1@cdfaf793`,目录 `src/views/order-v2/batch/detail/components/`)**:本节与下文引用的前端行号、行数都取自这个快照,它早于上表两个前端提交 `a73ef932` / `01acf773`,按上表第二条,`:334 / :387 / :588` 三处在你们当前分支上应已改成 `onPositiveClick`,各文件行数也可能有出入;按函数名与按钮文案定位即可。 + +| 旧组件 | 行数 | 现调接口 | 去向 | +|------|------|------|------| +| `RequirementTab.vue` | 1055 | confirm-check / confirm / requirement-summary | **保留并重写**为新设计 A;`emit('contact', orderId)` 事件保留(`index.vue:163 → :705 onContactCustomizer` 打开会话抽屉) | +| `RoomSummarySection.vue` | 174 | (父级传入 summary) | 并入 A 的住房卡与「逐晚用房」表 | +| `VehicleSummarySection.vue` | 160 | (父级传入 summary) | 并入 A 的「用车安排」 | +| `RoomHouseholdsSection.vue` | 362 | hotel-households | 并入弹窗①,同时给 A 的逐户表供住房列数据 | +| `VehicleHouseholdsSection.vue` | 571 | vehicle-households / batch-confirm / 逐户 vehicle dispatch | 并入弹窗②③ 与已确认态「审核变更」 | +| `RequirementRejectModal.vue` | 213 | 批量 reject | **删除**(新设计不用批量打回) | +| `GroupVehicleRequirementSection.vue` | 800 | GET vehicle-requirement / reopen / waive / withdraw | 并入弹窗③;reopen / withdraw 入口位置前端自定 | +| `GroupVehicleRequirementEditModal.vue` | 1048 | aggregate-draft / PUT vehicle-requirement / 车型字典 | 并入弹窗③ | +| `HouseholdRequirementModal.vue` | 589 | room-plans | **保留**,由逐户表「详情」打开 | +| `__tests__/*.spec.js` | — | — | 随组件增删调整 | + +API 封装全部已存在,不需要新增:`src/api/orderV2GroupBatch.js`(`:115` 详情、`:279` 状态流水、`:629` 预检、`:656` 确认、`:716` 汇总、`:748` 住房逐户、`:790` 用车逐户、`:819` 接送机批量放行、`:865` 取正式需求、`:889` 重开、`:929` 保存正式需求、`:963` 自动汇总、`:979` 撤回、`:997` 免车);`src/api/orderV2.js`(`:1543` 住房逐户打回、`:1561` 用车逐户打回、`:1577` 住房逐户下发、`:1597` 用车逐户下发)。 + +--- + +## 二、变更接口清单 + +| # | 接口 | 方法 | 路径 | 变更类型 | 说明 | +|---|------|------|------|----------|------| +| 1 | 团期详情 | GET | `/v3/admin/order/group-batch/{groupBatchId}` | 调用方式变更(后端零改动) | 页签头部户数人数、阶段、确认标记 | +| 2 | 团期状态流水 | GET | `/v3/admin/order/group-batch/{groupBatchId}/status-logs` | 调用方式变更(后端零改动) | 已确认态「时间 由 某人 确认」 | +| 3 | 全团需求汇总 | GET | `/v3/admin/order/group-batch/{groupBatchId}/requirement-summary` | 调用方式变更(后端零改动) | 三张汇总卡、逐晚用房、接送机汇总 | +| 4 | 住房逐户记录 | GET | `/v3/admin/order/group-batch/{groupBatchId}/requirement/hotel-households` | 调用方式变更(后端零改动) | 逐户表住房列、弹窗① | +| 5 | 用车逐户记录 | GET | `/v3/admin/order/group-batch/{groupBatchId}/requirement/vehicle-households` | 调用方式变更(后端零改动) | 逐户表车侧两列、弹窗②③、审核变更 | +| 6 | 整团确认预检 | GET | `/v3/admin/order/group-batch/{groupBatchId}/requirement/confirm-check` | 调用方式变更(后端零改动) | 状态条预检、弹窗底部缺失提示、大交通声明提示 | +| 7 | 取正式用车需求 | GET | `/v3/admin/order/group-batch/{groupBatchId}/vehicle-requirement` | 调用方式变更(后端零改动) | 行程用车卡、用车安排矩阵、弹窗③ | +| 8 | 正式用车需求自动汇总 | GET | `/v3/admin/order/group-batch/{groupBatchId}/vehicle-requirement/aggregate-draft` | 调用方式变更(后端零改动) | 弹窗③「按户需求重新汇总」 | +| 9 | 保存正式用车需求 | PUT | `/v3/admin/order/group-batch/{groupBatchId}/vehicle-requirement` | 调用方式变更(后端零改动) | 弹窗③「仅保存草稿」及确认前落草稿 | +| 10 | 整团免车 | POST | `/v3/admin/order/group-batch/{groupBatchId}/vehicle-requirement/waive` | 调用方式变更(后端零改动) | 弹窗③「整团不用车」 | +| 11 | 撤回正式用车需求 | POST | `/v3/admin/order/group-batch/{groupBatchId}/vehicle-requirement/withdraw` | 调用方式变更(后端零改动) | 已免车或已确认后改分组的前置动作 | +| 12 | 整团确认需求 | POST | `/v3/admin/order/group-batch/{groupBatchId}/requirement/confirm` | 调用方式变更(后端零改动) | 模式一、模式二的核心提交 | +| 13 | 住房逐户打回 | POST | `/v3/admin/order/{id}/hotel-requirement/reject` | 调用方式变更(后端零改动) | 弹窗①打回 | +| 14 | 用车逐户打回 | POST | `/v3/admin/order/{id}/vehicle-requirement/reject` | 调用方式变更(后端零改动) | 弹窗②打回(kind=TRANSFER)、弹窗③打回(kind=TRAVEL) | +| 15 | 住房逐户下发 | POST | `/v3/admin/order/{id}/hotel-requirement/dispatch` | 调用方式变更(后端零改动) | 已确认态「审核变更」住房通过 | +| 16 | 用车逐户下发 | POST | `/v3/admin/order/{id}/vehicle-requirement/dispatch` | 调用方式变更(后端零改动) | 已确认态「审核变更」车侧通过 | +| 17 | 接送机批量放行 | POST | `/v3/admin/order/group-batch/{groupBatchId}/requirement/transfer/batch-confirm` | 调用方式变更(后端零改动) | 确认后补放行接送机、已确认态接送机批量通过 | + +--- + +## 三、接口详情 + +### 1. 团期详情 `GET /v3/admin/order/group-batch/{groupBatchId}` + +**VO**: `(无请求体) → GroupBatchDetailRespVO` + +#### 使用场景 + +页签头部的户数、人数、阶段,以及「待确认态 / 已确认态」的判定。出团详情页 `index.vue` 已经在拉这个接口(封装 `getGroupBatchDetail`,`orderV2GroupBatch.js:115`),由父组件把结果传进页签即可,不必重复请求。弹窗提交成功后要重拉一次,因为 `requirementConfirmed` 会变。 + +#### 入参 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| groupBatchId | Path | Long | ✅ | 团期 ID | 路径参数 | + +#### 出参 `Result` + +| 字段 | 类型 | 说明 | +|------|------|------| +| groupBatchId | Long(JSON 字符串) | 团期 ID(`GroupBatchDetailRespVO.java:34`) | +| batchNo | String | 班期编号(`:48`) | +| batchStatus | String | 团期阶段码,枚举见六.5(`:58`) | +| batchStatusName | String | 阶段中文名(`:61`) | +| enrolledPeople | Integer | 活跃子订单人数合计,页头「N 人」与用车矩阵「在团人数」用它(`:130`) | +| subOrderCount | Integer | 活跃子订单户数,页头「N 户」用它(`:152`) | +| vehicleReady | Boolean | 配车完成或整团免车成立(`:164`)。**不是**「已下发车务」,别用它渲染已确认态的「用车已下发车务」 | +| requirementConfirmed | Boolean | 需求整体确认标志(`:176`)。false=待确认态,true=已确认态 | +| departDate | LocalDate | 出发日(`:248`) | +| endDate | LocalDate | 结束日(`:251`) | + +本页只用上表字段,其余字段不在本条范围。 + +#### 请求示例 + +```http +GET /v3/admin/order/group-batch/2100856430494973953 +``` + +#### 响应示例 + +按 VO 结构组装,取值为示意,不是测试服读数(只列本页用到的字段): + +```json +{ + "code": 200, + "message": "成功", + "data": { + "groupBatchId": "2100856430494973953", + "batchNo": "T26-8867", + "batchStatus": "RESOURCE_PREPARING", + "batchStatusName": "资源准备中", + "enrolledPeople": 7, + "subOrderCount": 2, + "vehicleReady": false, + "requirementConfirmed": false, + "departDate": "2026-10-08", + "endDate": "2026-10-10" + }, + "traceId": null, + "success": true +} +``` + +#### 空数据 / 降级响应 + +无空数据形态:团期不存在直接报 589500(`requireById`)。`enrolledPeople / subOrderCount` 在没有活跃子订单时为 0。 + +#### 错误响应 + +```json +{ + "code": 589507, + "message": "无操作权限(当前角色未授予团期权限,或该团期不在您名下)", + "data": null, + "traceId": null, + "success": false +} +``` + +#### 业务边界 + +- 判权 `group-batch:view`(`GroupBatchQueryController.java:113`),另有数据级归属校验:定制师角色只能看自己名下的团期,否则 589507。 +- 团期不存在报 589500「团期不存在」。 +- `requirementConfirmed` 会被后端自动清回 false:资源准备阶段有户改住房或用车需求、有户转团时都会清(`GroupBatchService.java:2629` `reopenRequirementConfirmation`)。页面不要缓存这个值做判断,每次进入页签与每次弹窗提交后都以最新详情为准。 + +--- + +### 2. 团期状态流水 `GET /v3/admin/order/group-batch/{groupBatchId}/status-logs` + +**VO**: `(无请求体) → List` + +#### 使用场景 + +已确认态状态条的「{时间} 由 {人} 确认」。取法:列表按时间**升序**、不分页,取 `eventType = BATCH_REQUIREMENT_CONFIRM` 的**最后一条**,用它的 `changedAt` 与 `operatorName`。封装 `getGroupBatchStatusLogs`(`orderV2GroupBatch.js:279`)。 + +#### 入参 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| groupBatchId | Path | Long | ✅ | 团期 ID | 路径参数 | + +#### 出参 `Result>` + +| 字段 | 类型 | 说明 | +|------|------|------| +| logId | Long(JSON 字符串) | 流水 ID | +| groupBatchId | Long(JSON 字符串) | 团期 ID | +| changeType | String | 变更大类 | +| eventType | String | 事件类型,本页只认三个值,见六.5 | +| eventTypeName | String | 事件中文名 | +| fromStatus / fromStatusName | String | 变更前团期阶段(数据类事件可能为空) | +| toStatus / toStatusName | String | 变更后团期阶段 | +| content | String | 事件描述 | +| reason | String | 原因(打回类事件为打回原因) | +| operatorType | String | 操作人类型 | +| operatorId | Long | 操作人 ID | +| operatorName | String | 操作人姓名,状态条「由 某人 确认」用它 | +| extra | Object | 扩展信息;逐户打回写入的 extra 只含本户 orderId | +| changedAt | LocalDateTime | 事件时间,状态条时间用它 | + +#### 请求示例 + +```http +GET /v3/admin/order/group-batch/2100856430494973953/status-logs +``` + +#### 响应示例 + +按 VO 结构组装,取值为示意,不是测试服读数: + +```json +{ + "code": 200, + "message": "成功", + "data": [ + { + "logId": "2105000000000000001", + "groupBatchId": "2100856430494973953", + "changeType": "DATA", + "eventType": "BATCH_REQUIREMENT_CONFIRM", + "eventTypeName": "需求整体确认", + "fromStatus": null, + "fromStatusName": null, + "toStatus": null, + "toStatusName": null, + "content": "需求整体确认", + "reason": null, + "operatorType": "ADMIN", + "operatorId": "2021059720172838914", + "operatorName": "王骁", + "extra": null, + "changedAt": "2026-09-29 16:20:00" + } + ], + "traceId": null, + "success": true +} +``` + +#### 空数据 / 降级响应 + +```json +{ "code": 200, "message": "成功", "data": [], "traceId": null, "success": true } +``` + +团期不存在或没有任何流水时返回空数组。已确认但列表里找不到 `BATCH_REQUIREMENT_CONFIRM` 时,状态条**不展示**「时间 由 某人 确认」这一行,其余照常。 + +#### 错误响应 + +```json +{ + "code": 589507, + "message": "无操作权限(当前角色未授予团期权限,或该团期不在您名下)", + "data": null, + "traceId": null, + "success": false +} +``` + +#### 业务边界 + +- 判权 `group-batch:view`,在 Service 入口校验(`GroupBatchStatusLogService.java:203`)。 +- 时间线写入失败只记日志、不回滚业务,所以「已确认但没有确认流水」是可能出现的形态,前端必须容忍(见上节降级)。 +- 取「最后一条」而不是「第一条」:确认 → 被打回或自动清标记 → 再确认,会留下多条 `BATCH_REQUIREMENT_CONFIRM`。 +- 本条对「确认人取自状态流水」这一推导未在测试服实测。 + +--- + +### 3. 全团需求汇总 `GET /v3/admin/order/group-batch/{groupBatchId}/requirement-summary` + +**VO**: `(无请求体) → GroupRequirementSummaryRespVO` + +#### 使用场景 + +新设计 A 的住房卡、接送机卡、逐晚用房表、特殊需求户数。进入页签调用一次,弹窗提交成功后重拉。封装 `getGroupRequirementSummary`(`orderV2GroupBatch.js:716`)。 + +#### 入参 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| groupBatchId | Path | Long | ✅ | 团期 ID | 路径参数 | + +#### 出参 `Result` + +| 字段 | 类型 | 说明 | +|------|------|------| +| activeOrderCount | Integer | 在团子订单数 | +| hotelRequirementCount | Integer | 住房需求条数 | +| hotelNeededOrderCount | Integer | 需要住房的户数,住房卡「N/N 户已提交」的分母 | +| hotelFlagMismatchOrderCount | Integer | 住房标记与需求不一致的户数 | +| hotelSubmittedOrderCount | Integer | 已提交住房需求的户数,「N/N 户已提交」的分子 | +| vehicleRequirementCount | Integer | 用车需求条数 | +| dailyRoomBreakdown[] | Array | 逐晚用房,**只计已提交且未被打回的户,不含客户自订的晚;全团都自订的晚没有条目** | +| dailyRoomBreakdown[].dayNumber | Integer | 第几晚 | +| dailyRoomBreakdown[].stayDate | LocalDate | 入住日 | +| dailyRoomBreakdown[].rooms[] | Array | 该晚按房型大类合计:`roomCategory` / `roomCategoryName` / `totalRoomCount` | +| dailyRoomBreakdown[].hotels[] | Array | 该晚按酒店拆:`hotelId` / `hotelName` / `totalRoomCount` / `rooms[]`(同上三字段) | +| vehicleSeatSummary[] | Array | 行程用车按车型合计(**只算 TRAVEL**):`vehicleType` / `vehicleTypeName` / `totalSeats` / `totalCount` | +| orderSpecialTags[] | Array | 各户特殊需求:`orderId` / `teamNo` / `requirementType`(HOTEL / VEHICLE)/ `specialTags`(String 数组) | +| transferSummary | Object | 接送机汇总,**恒不为 null** | +| transferSummary.householdCount | Integer | 报了接送机的户数 | +| transferSummary.pickupHouseholdCount | Integer | 要接机的户数 | +| transferSummary.dropoffHouseholdCount | Integer | 要送机的户数 | +| transferSummary.headcount | Integer | 接送机总人数 | +| transferSummary.vehicleSeatSummary[] | Array | 接送机按车型:`vehicleType` / `vehicleTypeName` / `seats` / `count` | +| transferSummary.serviceDates[] | LocalDate[] | 接送机涉及日期的并集(**不带方向**) | + +#### 请求示例 + +```http +GET /v3/admin/order/group-batch/2100856430494973953/requirement-summary +``` + +#### 响应示例 + +测试服实测原文(2026-09-29 16:38,经网关,格式化排版,内容未改): + +```json +{ + "code": 200, + "message": "成功", + "data": { + "activeOrderCount": 2, + "hotelRequirementCount": 2, + "hotelNeededOrderCount": 2, + "hotelFlagMismatchOrderCount": 0, + "hotelSubmittedOrderCount": 2, + "vehicleRequirementCount": 2, + "dailyRoomBreakdown": [ + { + "dayNumber": 1, + "stayDate": "2026-10-08", + "rooms": [ + { "roomCategory": "DELUXE", "roomCategoryName": "豪华房", "totalRoomCount": 1 }, + { "roomCategory": "KING", "roomCategoryName": "豪华大床", "totalRoomCount": 1 }, + { "roomCategory": "STANDARD", "roomCategoryName": "标间", "totalRoomCount": 1 } + ], + "hotels": [ + { + "hotelId": "2023714929877450753", + "hotelName": "呼伦贝尔香格里拉大酒店", + "totalRoomCount": 3, + "rooms": [ + { "roomCategory": "DELUXE", "roomCategoryName": "豪华房", "totalRoomCount": 1 }, + { "roomCategory": "KING", "roomCategoryName": "豪华大床", "totalRoomCount": 1 }, + { "roomCategory": "STANDARD", "roomCategoryName": "标间", "totalRoomCount": 1 } + ] + } + ] + }, + { + "dayNumber": 2, + "stayDate": "2026-10-09", + "rooms": [ + { "roomCategory": "STANDARD", "roomCategoryName": "标间", "totalRoomCount": 3 } + ], + "hotels": [ + { + "hotelId": "3001000000000000002", + "hotelName": "海拉尔海棠酒店", + "totalRoomCount": 3, + "rooms": [ + { "roomCategory": "STANDARD", "roomCategoryName": "标间", "totalRoomCount": 3 } + ] + } + ] + } + ], + "vehicleSeatSummary": [ + { "vehicleType": "bus", "vehicleTypeName": "大巴系列", "totalSeats": 38, "totalCount": 2 } + ], + "orderSpecialTags": [ + { "orderId": "2100856430239121409", "teamNo": "26-7060", "requirementType": "HOTEL", "specialTags": ["禁烟"] }, + { "orderId": "2102309919002943489", "teamNo": "26-2355", "requirementType": "HOTEL", "specialTags": ["含早餐", "景观房", "高楼层"] }, + { "orderId": "2102309919002943489", "teamNo": "26-2355", "requirementType": "VEHICLE", "specialTags": ["儿童安全座椅"] } + ], + "transferSummary": { + "householdCount": 2, + "pickupHouseholdCount": 2, + "dropoffHouseholdCount": 2, + "headcount": 7, + "vehicleSeatSummary": [ + { "vehicleType": "suv", "vehicleTypeName": "SUV系列", "seats": 5, "count": 1 }, + { "vehicleType": "mpv", "vehicleTypeName": "商务车", "seats": 7, "count": 1 } + ], + "serviceDates": ["2026-10-08", "2026-10-10", "2026-10-11"] + } + }, + "traceId": null, + "success": true +} +``` + +按这份实测数据,住房卡应显示:峰值 3 间/晚(两晚各 3 间取最大)、间夜 6、酒店 2 家;接送机卡应显示:2 户 · 7 人,接机 2 户、送机 2 户,SUV系列 5座 ×1 · 商务车 7座 ×1。 + +#### 空数据 / 降级响应 + +没有任何户提交时:`dailyRoomBreakdown`、`vehicleSeatSummary`、`orderSpecialTags` 为空数组;`transferSummary` 仍是对象,计数为 0、数组为空。 + +```json +{ + "code": 200, + "message": "成功", + "data": { + "activeOrderCount": 2, + "hotelRequirementCount": 0, + "hotelNeededOrderCount": 2, + "hotelFlagMismatchOrderCount": 0, + "hotelSubmittedOrderCount": 0, + "vehicleRequirementCount": 0, + "dailyRoomBreakdown": [], + "vehicleSeatSummary": [], + "orderSpecialTags": [], + "transferSummary": { "householdCount": 0, "pickupHouseholdCount": 0, "dropoffHouseholdCount": 0, "headcount": 0, "vehicleSeatSummary": [], "serviceDates": [] } + }, + "traceId": null, + "success": true +} +``` + +(按 VO 结构组装,取值为示意。) + +#### 错误响应 + +```json +{ + "code": 589500, + "message": "团期不存在", + "data": null, + "traceId": null, + "success": false +} +``` + +#### 业务边界 + +- 判权 `group-batch:view`(`GroupBatchRequirementController.java:91`);无权 589507。团期不存在 589500。 +- 汇总口径是「在团」子订单(只排除已取消),被打回的户不计入逐晚用房。 +- 逐晚用房**没有**全团自订晚的条目:晚数不能用 `dailyRoomBreakdown.length` 算,要用 `endDate − departDate`(见四「字段来源」)。 +- `vehicleSeatSummary` 只含行程用车;接送机车型看 `transferSummary.vehicleSeatSummary`,两者不要相加。 + +--- + +### 4. 住房逐户记录 `GET /v3/admin/order/group-batch/{groupBatchId}/requirement/hotel-households` + +**VO**: `(无请求体) → GroupHotelHouseholdsRespVO` + +#### 使用场景 + +逐户表的住房列、弹窗①的逐户审核列表、逐晚用房表的「城市」和「客户自订」备注。封装 `getGroupHotelHouseholds`(`orderV2GroupBatch.js:748`)。 + +#### 入参 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| groupBatchId | Path | Long | ✅ | 团期 ID | 路径参数 | + +#### 出参 `Result` + +| 字段 | 类型 | 说明 | +|------|------|------| +| groupBatchId | Long(JSON 字符串) | 团期 ID | +| departDate / endDate | LocalDate | 出发日 / 结束日 | +| householdCount | Integer | 需要住房的户数(与汇总 `hotelNeededOrderCount` 同口径) | +| countedHouseholdCount | Integer | 计入汇总的户数 | +| households[] | Array | 逐户,按 `orderNo` 升序 | +| households[].orderId | Long(JSON 字符串) | 子订单 ID,逐户打回 / 下发的路径参数 `{id}` 用它 | +| households[].orderNo | String | 订单号 | +| households[].teamNo | String | 团号,可能为 null | +| households[].customerName | String | 客户名 | +| households[].participantCount | Integer | 该户人数 | +| households[].consultantId / consultantName | Long / String | 定制师 | +| households[].status | String | 住房需求状态,见六.5;**未提交为 null**;打回为 `REJECTED_*` | +| households[].statusName | String | 状态中文名,直接显示 | +| households[].countedInSummary | Boolean | 是否计入汇总;打回态为 false | +| households[].remark | String | 备注 | +| households[].specialTags | String[] | 特殊需求 | +| households[].returnRemark / returnedAt | String / LocalDateTime | 最近一次打回原因与时间 | +| households[].days[] | Array | 逐晚:`dayNumber` / `stayDate` / `customerSelfBooked` / `hotels[]` | +| households[].days[].hotels[] | Array | `hotelId` / `hotelName` / `city` / `district` / `totalRoomCount` / `rooms[]` | +| households[].days[].hotels[].rooms[] | Array | `roomTypeId` / `roomTypeName` / `roomCategory` / `roomCategoryName` / `roomCount` | + +#### 请求示例 + +```http +GET /v3/admin/order/group-batch/2101506167098511362/requirement/hotel-households +``` + +#### 响应示例 + +按 VO 结构组装,ID 取 VO 自带示例值,其余取值为示意,不是测试服读数: + +```json +{ + "code": 200, + "message": "成功", + "data": { + "groupBatchId": "2101506167098511362", + "departDate": "2026-10-08", + "endDate": "2026-10-10", + "householdCount": 1, + "countedHouseholdCount": 1, + "households": [ + { + "orderId": "2101506167043985410", + "orderNo": "HL20260920101530112", + "teamNo": "26-0480", + "customerName": "张丽", + "participantCount": 3, + "consultantId": "2021059720172838914", + "consultantName": "王骁", + "status": "PENDING_REVIEW", + "statusName": "待审核", + "countedInSummary": true, + "remark": "老人同行,尽量安排低楼层", + "specialTags": ["禁烟"], + "returnRemark": null, + "returnedAt": null, + "days": [ + { + "dayNumber": 1, + "stayDate": "2026-10-08", + "customerSelfBooked": false, + "hotels": [ + { + "hotelId": "2023714929877450753", + "hotelName": "呼伦贝尔香格里拉大酒店", + "city": "呼伦贝尔", + "district": "海拉尔区", + "totalRoomCount": 2, + "rooms": [ + { "roomTypeId": "2023714929877450801", "roomTypeName": "高级双床房", "roomCategory": "STANDARD", "roomCategoryName": "标间", "roomCount": 2 } + ] + } + ] + } + ] + } + ] + }, + "traceId": null, + "success": true +} +``` + +#### 空数据 / 降级响应 + +```json +{ + "code": 200, + "message": "成功", + "data": { "groupBatchId": "2101506167098511362", "departDate": "2026-10-08", "endDate": "2026-10-10", "householdCount": 0, "countedHouseholdCount": 0, "households": [] }, + "traceId": null, + "success": true +} +``` + +(按 VO 结构组装。)需要住房但一次都没提交的户**也会列出**,`status` 为 null、`days` 为空。 + +#### 错误响应 + +```json +{ + "code": 589507, + "message": "无操作权限(当前角色未授予团期权限,或该团期不在您名下)", + "data": null, + "traceId": null, + "success": false +} +``` + +#### 业务边界 + +- 判权 `group-batch:view`;团期不存在 589500。 +- 被打回的户照样列出(`countedInSummary=false`、`status` 为 `REJECTED_TO_CONSULTANT`),用于「已打回」筛选;它不计入逐晚用房。 +- 本列表的「计入汇总的户」逐晚加总 = 汇总接口的 `dailyRoomBreakdown`,两处数字应一致。 +- `city` 只在这里有,汇总接口的 `hotels[]` 没有城市;逐晚用房表的城市按 `hotelId` 关联本接口取。 + +--- + +### 5. 用车逐户记录 `GET /v3/admin/order/group-batch/{groupBatchId}/requirement/vehicle-households` + +**VO**: `(无请求体) → GroupVehicleHouseholdsRespVO` + +#### 使用场景 + +逐户表的「接送机」「行程用车」两列,弹窗②(接送机)与弹窗③下半「各户行程用车需求」,已确认态「审核变更」的车侧待审行。封装 `getGroupVehicleHouseholds(groupBatchId, kind)`(`orderV2GroupBatch.js:790`)。**不传 `kind` 一次返回两类**,前端按 `requirements[].kind` 拆;沿用既有条目按 kind 分两次取也可以。 + +#### 入参 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| groupBatchId | Path | Long | ✅ | 团期 ID | 路径参数 | +| kind | Query | String | ❌ | `TRAVEL` / `TRANSFER`,其他值报 809000 | 不传返回两类 | + +#### 出参 `Result` + +| 字段 | 类型 | 说明 | +|------|------|------| +| groupBatchId | Long(JSON 字符串) | 团期 ID | +| departDate / endDate | LocalDate | 出发日 / 结束日 | +| householdCount | Integer | 应报车的户数(含一条都没提交的户),行程用车卡「N/N 户已报」的分母 | +| vehicleRowCount | Integer | 返回的需求行条数 | +| countedHouseholdCount | Integer | 有活跃行程用车行的户数,「N/N 户已报」的分子 | +| households[].orderId | Long(JSON 字符串) | 子订单 ID | +| households[].orderNo / teamNo / customerName | String | 订单号 / 团号 / 客户名 | +| households[].participantCount | Integer | 该户人数 | +| households[].consultantId / consultantName | Long / String | 定制师 | +| households[].countedInSummary | Boolean | 是否计入汇总 | +| households[].status / statusName | String | 户级状态:**null=该户一条都没提交**;非 null 时取展示顺序首条(TRAVEL 优先)的状态 | +| households[].requirements[] | Array | 0~2 条(每个 kind 至多一条);**被打回的行已失活,不在这里** | +| requirements[].requirementId | Long(JSON 字符串) | 需求行 ID | +| requirements[].kind / kindName | String | `TRAVEL` 行程用车 / `TRANSFER` 接送机 | +| requirements[].status / statusName | String | `PENDING_REVIEW` / `PENDING` / `PROCESSING` / `DONE`;`statusName` 原文直显(TRAVEL 待审显示「待提交车务」) | +| requirements[].fleet[] | Array | `vehicleType` / `vehicleTypeName` / `seats` / `count` | +| requirements[].specialTags[] | Array | `code` / `name` | +| requirements[].remark | String | 备注 | +| requirements[].serviceDates | LocalDate[] | 服务日期;接送机的日期**不带方向** | +| requirements[].headcount | Integer | 人数 | +| requirements[].totalSeatCount | Integer | 总座位数 | +| requirements[].remainingPassengerSeats | Integer | 扣司机座与人数后的剩余座位 | +| requirements[].pickupRequired / dropoffRequired | Boolean | 是否接机 / 送机,**仅 TRANSFER 有值**,TRAVEL 为 null | +| requirements[].returnRemark / returnedAt | String / LocalDateTime | 最近一次打回原因与时间 | + +#### 请求示例 + +```http +GET /v3/admin/order/group-batch/2100856430494973953/requirement/vehicle-households +``` + +#### 响应示例 + +测试服实测原文(2026-09-29 16:38,经网关,未传 kind;备注字段为测试数据原样保留): + +```json +{"code":200,"message":"成功","data":{"groupBatchId":"2100856430494973953","departDate":"2026-10-08","endDate":"2026-10-10","householdCount":2,"vehicleRowCount":4,"countedHouseholdCount":2,"households":[{"orderId":"2100856430239121409","orderNo":"HL20260918155619496","teamNo":"26-7060","customerName":"王有亿","participantCount":3,"consultantId":"2021059720172838914","consultantName":"王骁","countedInSummary":true,"status":"PENDING_REVIEW","statusName":"待提交车务","requirements":[{"requirementId":"2100883541330944002","kind":"TRAVEL","kindName":"行程用车","status":"PENDING_REVIEW","statusName":"待提交车务","fleet":[{"vehicleType":"bus","vehicleTypeName":"大巴系列","seats":19,"count":1}],"specialTags":[],"remark":"#7949 AC-2 定制师提交","serviceDates":["2026-10-08","2026-10-09","2026-10-10"],"headcount":3,"totalSeatCount":19,"remainingPassengerSeats":15,"pickupRequired":null,"dropoffRequired":null,"returnRemark":null,"returnedAt":null},{"requirementId":"2102202475291549698","kind":"TRANSFER","kindName":"接送机","status":"PENDING_REVIEW","statusName":"待审核","fleet":[{"vehicleType":"suv","vehicleTypeName":"SUV系列","seats":5,"count":1}],"specialTags":[{"code":"儿童安全座椅","name":"儿童安全座椅"}],"remark":"444444","serviceDates":["2026-10-08","2026-10-11"],"headcount":3,"totalSeatCount":5,"remainingPassengerSeats":1,"pickupRequired":true,"dropoffRequired":true,"returnRemark":null,"returnedAt":null}]},{"orderId":"2102309919002943489","orderNo":"HL20260922161158291","teamNo":"26-2355","customerName":"王二麻子","participantCount":4,"consultantId":"2083111674486693889","consultantName":"刘畅","countedInSummary":true,"status":"PENDING_REVIEW","statusName":"待提交车务","requirements":[{"requirementId":"2104838140441280514","kind":"TRAVEL","kindName":"行程用车","status":"PENDING_REVIEW","statusName":"待提交车务","fleet":[{"vehicleType":"bus","vehicleTypeName":"大巴系列","seats":19,"count":1}],"specialTags":[{"code":"儿童安全座椅","name":"儿童安全座椅"}],"remark":"12121221","serviceDates":["2026-10-08","2026-10-09","2026-10-10"],"headcount":4,"totalSeatCount":19,"remainingPassengerSeats":14,"pickupRequired":null,"dropoffRequired":null,"returnRemark":null,"returnedAt":null},{"requirementId":"2104838140579692545","kind":"TRANSFER","kindName":"接送机","status":"PENDING_REVIEW","statusName":"待审核","fleet":[{"vehicleType":"mpv","vehicleTypeName":"商务车","seats":7,"count":1}],"specialTags":[{"code":"儿童安全座椅","name":"儿童安全座椅"}],"remark":"32","serviceDates":["2026-10-08","2026-10-10"],"headcount":4,"totalSeatCount":7,"remainingPassengerSeats":2,"pickupRequired":true,"dropoffRequired":true,"returnRemark":null,"returnedAt":null}]}]},"traceId":null,"success":true} +``` + +注意同一户 TRAVEL 待审的 `statusName` 是「待提交车务」、TRANSFER 待审是「待审核」,两者 `status` 都是 `PENDING_REVIEW`。判断用 `status`,显示用 `statusName`。 + +#### 空数据 / 降级响应 + +```json +{ + "code": 200, + "message": "成功", + "data": { "groupBatchId": "2100856430494973953", "departDate": "2026-10-08", "endDate": "2026-10-10", "householdCount": 0, "vehicleRowCount": 0, "countedHouseholdCount": 0, "households": [] }, + "traceId": null, + "success": true +} +``` + +(按 VO 结构组装。)应报车但一条都没提交的户会列出,`status` 为 null、`requirements` 为空数组。 + +#### 错误响应 + +```json +{ + "code": 809000, + "message": "用车需求类别非法:BUS", + "data": null, + "traceId": null, + "success": false +} +``` + +(按错误码模板 `用车需求类别非法:{0}` 组装。) + +#### 业务边界 + +- 判权 `group-batch:view`;团期不存在 589500;`kind` 非法 809000。 +- **车侧没有「已打回」可读**:打回后该行失活、从 `requirements` 里消失,读数与「未提交」相同。逐户表「已打回」筛选只能覆盖住房侧(见四「字段缺口」)。 +- 列表范围 = 在团户里 `needs_vehicle=1` 的户,并上有活跃用车需求行的户。下单时 `needs_vehicle` 恒写 true,所以实际上就是全部在团户;没报任何用车的户也在列表里,`status` 为 null、`requirements` 为 `[]`。 +- 最多 500 户(超出截断),按 `orderNo` 升序(空值排最后),同值再按 `orderId`。 +- 一户最多一条 TRAVEL、一条 TRANSFER。 + +--- + +### 6. 整团确认预检 `GET /v3/admin/order/group-batch/{groupBatchId}/requirement/confirm-check` + +**VO**: `(无请求体) → GroupBatchRequirementCheckRespVO` + +#### 使用场景 + +状态条「预检通过,可以确认 / 还有 N 项未就绪」、弹窗底部 `missingText`、大交通声明提示条。进入页签调用一次;弹窗打开时、弹窗③保存草稿或免车后各重拉一次。封装 `confirmGroupBatchRequirementCheck`(`orderV2GroupBatch.js:629`)。 + +#### 入参 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| groupBatchId | Path | Long | ✅ | 团期 ID | 路径参数 | + +#### 出参 `Result` + +| 字段 | 类型 | 说明 | +|------|------|------| +| groupBatchId | Long(JSON 字符串) | 团期 ID | +| batchStatus / batchStatusName | String | 团期阶段 | +| ready | Boolean | `missing` 为空 且 `vehicleMissing` 为空 且 阶段为 `RESOURCE_PREPARING` 时为 true | +| missing[] | Array | 住房侧缺失:`orderId` / `teamNo` / `orderNo` / `customerName` / `consultantId` / `consultantName` / `reason` / `reasonName` / `dayNumber` / `segmentIndex` / `expectedNights` / `actualNights`;`reason` 见六.5 | +| checkedResourceTypes | String[] | 本次检查了哪些资源类型 | +| vehicleWaived | Boolean | 是否已声明整团免车 | +| vehicleMissing[] | Array | 车侧违规:`reason` / `groupCode` / `tripDate` / `orderId` / `teamNo` / `orderNo` / `detail`;`reason` 见六.5。免车时恒为空 | +| vehicleExemptHouseholds | Array | 豁免用车的户 | +| groupVehicleRequirementId / Status / Version | Long / String / Integer | 当前活跃正式用车需求;没有时为 null | +| transferSubmitEnabled | Boolean | 接送机提交开关状态 | +| transferDeclaredWithoutRequirement[] | Array | 大交通里声明了接送、但没报接送机需求的户:`orderId` / `teamNo` / `orderNo` / `customerName` / `consultantId` / `consultantName` / `pickupRequired` / `dropoffRequired` / `pickupRemark`。**只是提示,不进 ready、不阻断确认** | + +#### 请求示例 + +```http +GET /v3/admin/order/group-batch/2104327837991518210/requirement/confirm-check +``` + +#### 响应示例 + +测试服实测原文(2026-09-27,整团免车的团期): + +```json +{"code":200,"message":"成功","data":{"groupBatchId":"2104327837991518210","batchStatus":"RESOURCE_PREPARING","batchStatusName":"资源准备中","ready":true,"missing":[],"checkedResourceTypes":["HOTEL","VEHICLE"],"vehicleWaived":true,"vehicleMissing":[],"vehicleExemptHouseholds":[],"groupVehicleRequirementId":"2104331168285671426","groupVehicleRequirementStatus":"CONFIRMED","groupVehicleRequirementVersion":1,"transferSubmitEnabled":true,"transferDeclaredWithoutRequirement":[]},"traceId":null,"success":true} +``` + +#### 空数据 / 降级响应 + +没有降级形态。团期阶段不对时照常返回 200,`ready=false`,`missing` 与 `vehicleMissing` 可能都为空——此时 `ready=false` 的原因就是阶段,按 `batchStatusName` 提示。 + +#### 错误响应 + +```json +{ + "code": 589507, + "message": "无操作权限(当前角色未授予团期权限,或该团期不在您名下)", + "data": null, + "traceId": null, + "success": false +} +``` + +#### 业务边界 + +- 判权 `group-batch:demand:confirm`(`GroupBatchRequirementController.java:160`)。只有 view 权限的角色调它会 589507,页签要能在拿不到预检时照常渲染只读部分,并隐藏主按钮。 +- 车侧检查顺序:团级六条(809100 / 809101 / 809103~809110 对应的原因)→ 户级未提交行程用车(`HOUSEHOLD_REQUIREMENT_NOT_SUBMITTED`)→ 待放行接送机未回填服务日(`TRANSFER_SERVICE_DATES_NOT_BACKFILLED`)→ 接送机窗不全(`TRANSFER_WINDOW_INCOMPLETE`)→ 户车型与分组车型不符(`MEMBER_GROUP_MISMATCH`)。 +- 整团免车时车侧检查整段跳过(`vehicleMissing` 恒空,809007 也不查),`transferDeclaredWithoutRequirement` 不受免车影响照常给。 +- 确认时后端按同一套规则再算一遍,预检通过不等于确认一定成功(中间可能有户改了需求),确认失败按错误码处理。 + +--- + +### 7. 取正式用车需求 `GET /v3/admin/order/group-batch/{groupBatchId}/vehicle-requirement` + +**VO**: `(无请求体) → GroupVehicleRequirementRespVO` + +#### 使用场景 + +行程用车卡(状态、组数、逐日覆盖、车型)、「用车安排」的分组×日期矩阵、弹窗③打开时的初始草稿。封装 `getGroupVehicleRequirement`(`orderV2GroupBatch.js:865`)。 + +#### 入参 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| groupBatchId | Path | Long | ✅ | 团期 ID | 路径参数 | + +#### 出参 `Result` + +| 字段 | 类型 | 说明 | +|------|------|------| +| requirementId | Long(JSON 字符串) | 正式需求 ID | +| groupBatchId | Long(JSON 字符串) | 团期 ID | +| status | String | 见六.5 `GroupVehicleRequirementStatus` | +| version | Integer | 版本号,PUT 时原样带回 | +| remark | String | 整份备注 | +| confirmedBy / confirmedAt | String / LocalDateTime | 确认人 / 确认时间 | +| planRefreshState / planRefreshReplayCount / blockedStage / planRefreshStalled / planRefreshStalledReason / planRefreshTimeoutAt / planRefreshReplayExhausted | — | 受控重开链路专用,普通流程为空,本页不展示 | +| groups[] | Array | 乘车分组;**`status=CONFIRMED` 且 `groups` 为空 = 整团免车** | +| groups[].groupId / groupCode | Long / String | 分组 ID / 分组编码(如 BUS、BUS2) | +| groups[].vehicleType / vehicleTypeName | String | 车型 | +| groups[].seats / count | Integer | 单车座位 / 车辆数 | +| groups[].serviceStartDate / serviceEndDate | LocalDate | 本组服务起止 | +| groups[].specialTags[] | Array | `code` / `name` | +| groups[].remark | String | 组备注 | +| groups[].totalSeatCount / maxHeadcount / remainingPassengerSeats | Integer | 总座位 / 最大单日人数 / 剩余座位 | +| groups[].days[] | Array | 逐日:`tripDate` / `headcount` / `memberOrderIds` / `memberOrderCount` | +| exemptHouseholds | Array | 豁免用车的户 | + +#### 请求示例 + +```http +GET /v3/admin/order/group-batch/2100856430494973953/vehicle-requirement +``` + +#### 响应示例 + +按 VO 结构组装,取值为示意,不是测试服读数: + +```json +{ + "code": 200, + "message": "成功", + "data": { + "requirementId": "2104900000000000001", + "groupBatchId": "2100856430494973953", + "status": "DRAFT", + "version": 1, + "remark": null, + "confirmedBy": null, + "confirmedAt": null, + "planRefreshState": null, + "planRefreshReplayCount": null, + "blockedStage": null, + "planRefreshStalled": null, + "planRefreshStalledReason": null, + "planRefreshTimeoutAt": null, + "planRefreshReplayExhausted": null, + "groups": [ + { + "groupId": "2104900000000000011", + "groupCode": "BUS", + "vehicleType": "bus", + "serviceStartDate": "2026-10-08", + "serviceEndDate": "2026-10-10", + "vehicleTypeName": "大巴系列", + "seats": 19, + "count": 1, + "specialTags": [{ "code": "儿童安全座椅", "name": "儿童安全座椅" }], + "remark": null, + "totalSeatCount": 19, + "maxHeadcount": 7, + "remainingPassengerSeats": 11, + "days": [ + { "tripDate": "2026-10-08", "headcount": 7, "memberOrderIds": ["2100856430239121409", "2102309919002943489"], "memberOrderCount": 2 }, + { "tripDate": "2026-10-09", "headcount": 7, "memberOrderIds": ["2100856430239121409", "2102309919002943489"], "memberOrderCount": 2 }, + { "tripDate": "2026-10-10", "headcount": 7, "memberOrderIds": ["2100856430239121409", "2102309919002943489"], "memberOrderCount": 2 } + ] + } + ], + "exemptHouseholds": [] + }, + "traceId": null, + "success": true +} +``` + +#### 空数据 / 降级响应 + +```json +{ "code": 200, "message": "成功", "data": null, "traceId": null, "success": true } +``` + +还没形成正式需求时 `data=null`:行程用车卡显示「未形成正式需求」,矩阵不画。 + +#### 错误响应 + +```json +{ + "code": 589507, + "message": "无操作权限(当前角色未授予团期权限,或该团期不在您名下)", + "data": null, + "traceId": null, + "success": false +} +``` + +#### 业务边界 + +- 判权 `group-batch:demand:confirm`(**不是 view**)。只有 view 权限的角色看页签时这里会 589507,行程用车卡与用车矩阵要降级为「无权查看」而不是整页报错。 +- 整团免车的判定只能用「`status=CONFIRMED` 且 `groups` 为空」,没有单独的布尔字段;预检接口的 `vehicleWaived` 可以作为交叉核对。 +- `version` 是乐观锁,编辑前取、保存时带回,页面停留期间被别人改过会 809102。 + +--- + +### 8. 正式用车需求自动汇总 `GET /v3/admin/order/group-batch/{groupBatchId}/vehicle-requirement/aggregate-draft` + +**VO**: `(无请求体) → GroupVehicleAggregateDraftRespVO` + +#### 使用场景 + +弹窗③「按户需求重新汇总」,以及还没有正式需求时打开弹窗③的首次自动汇总。只计算、不落库;返回的 `draft` 可以原样作为 PUT 的请求体。封装 `getGroupVehicleAggregateDraft`(`orderV2GroupBatch.js:963`)。 + +#### 入参 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| groupBatchId | Path | Long | ✅ | 团期 ID | 路径参数 | + +#### 出参 `Result` + +| 字段 | 类型 | 说明 | +|------|------|------| +| groupBatchId | Long(JSON 字符串) | 团期 ID | +| currentStatus | String | 当前活跃正式需求状态,没有时为 null | +| draft | GroupVehicleRequirementSaveReqVO | 汇总出的草稿,结构同第 9 节请求体,可原样 PUT | +| droppedFleetItems[] | Array | 汇总时被舍弃的车型:`orderId` / `teamNo` / `orderNo` / `vehicleType` / `seats` / `count` / `keptVehicleType` / `reason` | +| staleHeadcountOrders[] | Array | 人数已变的户:`orderId` / `teamNo` / `orderNo` / `frozenHeadcount` / `liveHeadcount` | +| paddedOrderDays[] | Array | 被补齐日期的户:`orderId` / `teamNo` / `orderNo` / `dates` | +| seatOptionAdjusted[] | Array | 座位档位被调整的组:`groupCode` / `orderId` / `teamNo` / `orderNo` / `vehicleType` / `originalSeats` / `adoptedSeats` / `seatOptions` / `reason` | +| violations[] | Array | 草稿仍违反的规则:`code`(Integer,809 段码)/ `reason` / `detail` / `groupCode` / `tripDate` / `orderId` / `teamNo` | +| exemptHouseholds | Array | 豁免用车的户 | + +#### 请求示例 + +```http +GET /v3/admin/order/group-batch/2100856430494973953/vehicle-requirement/aggregate-draft +``` + +#### 响应示例 + +按 VO 结构组装,取值为示意,不是测试服读数: + +```json +{ + "code": 200, + "message": "成功", + "data": { + "groupBatchId": "2100856430494973953", + "currentStatus": null, + "draft": { + "version": null, + "remark": null, + "groups": [ + { + "groupId": null, + "groupCode": "BUS", + "vehicleType": "bus", + "serviceStartDate": "2026-10-08", + "serviceEndDate": "2026-10-10", + "seats": 19, + "count": 1, + "specialTags": ["儿童安全座椅"], + "remark": null, + "days": [ + { "tripDate": "2026-10-08", "headcount": 7, "memberOrderIds": ["2100856430239121409", "2102309919002943489"] }, + { "tripDate": "2026-10-09", "headcount": 7, "memberOrderIds": ["2100856430239121409", "2102309919002943489"] }, + { "tripDate": "2026-10-10", "headcount": 7, "memberOrderIds": ["2100856430239121409", "2102309919002943489"] } + ] + } + ] + }, + "droppedFleetItems": [], + "staleHeadcountOrders": [], + "paddedOrderDays": [], + "seatOptionAdjusted": [], + "violations": [], + "exemptHouseholds": [] + }, + "traceId": null, + "success": true +} +``` + +#### 空数据 / 降级响应 + +没有降级形态:车型字典读取失败时直接报 809120(见下),有户还没提交行程用车时报 809121,都不会返回半份草稿。 + +#### 错误响应 + +```json +{ + "code": 809121, + "message": "团期 「第3期 10月8日出发团」 有 1 户缺少可汇总的行程用车需求,暂不能自动汇总:26-2355:未提交行程用车需求", + "data": null, + "traceId": null, + "success": false +} +``` + +(按错误码模板 `团期 {0} 有 {1} 户缺少可汇总的行程用车需求,暂不能自动汇总:{2}` 组装。`{0}` 是「第N期 班期名」外加一对直角引号,两者都缺时为「(未命名)」;`{2}` 是逐户「团号(缺则订单号):原因」,多户用顿号分隔。) + +#### 业务边界 + +- 判权 `group-batch:demand:confirm`。 +- 809120:车型字典读取失败,提示用户重试即可。 +- 809121:有户的行程用车汇总不出来,不能汇总。原因有四种:未提交行程用车需求、车型均不在车型字典内、推不出用车日期、订单人数为 0。弹窗③下半的逐户列表会显示是哪几户,message 里也逐户点名。 +- `violations` 非空时照样可以 PUT 成草稿,但确认会被同样的规则挡住;弹窗③要把 `violations` 逐条显示在对应分组 / 日期旁。 +- 只读、无副作用,可以反复调用。 + +--- + +### 9. 保存正式用车需求 `PUT /v3/admin/order/group-batch/{groupBatchId}/vehicle-requirement` + +**VO**: `GroupVehicleRequirementSaveReqVO → GroupVehicleRequirementRespVO` + +#### 使用场景 + +弹窗③「仅保存草稿」;模式一 / 模式二确认前,如果弹窗③里改过分组或还没有正式需求,先 PUT 落成草稿再调 confirm。封装 `saveGroupVehicleRequirement`(`orderV2GroupBatch.js:929`)。 + +#### 入参 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| groupBatchId | Path | Long | ✅ | 团期 ID | 路径参数 | +| version | Body | Integer | ❌ | 首次保存传 null;之后必须等于当前版本 | 乐观锁 | +| remark | Body | String | ❌ | ≤500 | 整份备注 | +| groups | Body | Array | ✅ | `@NotNull`,空数组合法 | 乘车分组 | +| groups[].groupId | Body | Long | ❌ | 新组为 null | 已有组带回原 ID | +| groups[].groupCode | Body | String | ✅ | ≤32,不可改名(改名 = 删旧组加新组) | 分组编码 | +| groups[].vehicleType | Body | String | ✅ | ≤64,须在车型字典内 | 车型 | +| groups[].serviceStartDate / serviceEndDate | Body | LocalDate | ✅ | `@NotNull` | 本组服务起止 | +| groups[].seats | Body | Integer | ❌ | `@Min(1)`,须在该车型可选档位内;与 count 同填或同空 | 单车座位 | +| groups[].count | Body | Integer | ❌ | `@Min(1)`;与 seats 同填或同空 | 车辆数 | +| groups[].specialTags | Body | String[] | ❌ | 须在字典内 | 特殊诉求 | +| groups[].remark | Body | String | ❌ | ≤500 | 组备注 | +| groups[].days | Body | Array | ✅ | `@NotEmpty` | 逐日 | +| groups[].days[].tripDate | Body | LocalDate | ✅ | 在本组服务日范围内、不重复 | 日期 | +| groups[].days[].headcount | Body | Integer | ✅ | `@Min(1)`,不小于当日成员户数 | 当日人数 | +| groups[].days[].memberOrderIds | Body | Long[] | ✅ | `@NotEmpty`;须属本团;同户同日只能在一个组 | 当日成员子订单 | + +#### 出参 `Result` + +| 字段 | 类型 | 说明 | +|------|------|------| +| status | String | 保存后恒为 `DRAFT` | +| version | Integer | 已 +1,下次保存用它 | +| groups / 其余字段 | — | 同第 7 节 | + +#### 请求示例 + +```json +{ + "version": 1, + "remark": null, + "groups": [ + { + "groupId": "2104900000000000011", + "groupCode": "BUS", + "vehicleType": "bus", + "serviceStartDate": "2026-10-08", + "serviceEndDate": "2026-10-10", + "seats": 19, + "count": 1, + "specialTags": ["儿童安全座椅"], + "remark": null, + "days": [ + { "tripDate": "2026-10-08", "headcount": 7, "memberOrderIds": ["2100856430239121409", "2102309919002943489"] }, + { "tripDate": "2026-10-09", "headcount": 7, "memberOrderIds": ["2100856430239121409", "2102309919002943489"] }, + { "tripDate": "2026-10-10", "headcount": 7, "memberOrderIds": ["2100856430239121409", "2102309919002943489"] } + ] + } + ] +} +``` + +#### 响应示例 + +按 VO 结构组装,取值为示意:结构同第 7 节响应示例,`status` 为 `DRAFT`、`version` 为 2。 + +```json +{ + "code": 200, + "message": "成功", + "data": { + "requirementId": "2104900000000000001", + "groupBatchId": "2100856430494973953", + "status": "DRAFT", + "version": 2, + "remark": null, + "confirmedBy": null, + "confirmedAt": null, + "groups": [ + { + "groupId": "2104900000000000011", + "groupCode": "BUS", + "vehicleType": "bus", + "vehicleTypeName": "大巴系列", + "serviceStartDate": "2026-10-08", + "serviceEndDate": "2026-10-10", + "seats": 19, + "count": 1, + "totalSeatCount": 19, + "maxHeadcount": 7, + "remainingPassengerSeats": 11, + "days": [ + { "tripDate": "2026-10-08", "headcount": 7, "memberOrderIds": ["2100856430239121409", "2102309919002943489"], "memberOrderCount": 2 } + ] + } + ], + "exemptHouseholds": [] + }, + "traceId": null, + "success": true +} +``` + +#### 空数据 / 降级响应 + +`groups: []` 是合法请求,但本团有需要用车的户时报 809103;整团不用车应走第 10 节免车,不要 PUT 空分组。 + +#### 错误响应 + +```json +{ + "code": 809102, + "message": "正式用车需求已被他人修改(提交版本 1,当前版本 2),请刷新后重试", + "data": null, + "traceId": null, + "success": false +} +``` + +(按错误码模板组装。) + +#### 业务边界 + +- 判权 `group-batch:demand:confirm`;阶段须为 `RESOURCE_PREPARING`,否则 589501。 +- 错误按后端校验顺序:先在事务外做字典校验 809117(特殊标签)/ 809120(车型字典读不到)/ 809119(车型不在字典内),这三个排在阶段守卫之前 → 团期不存在 589500 → 阶段 589501 → 809115(已免车,须先撤回)→ 809101(当前不是 DRAFT,比如已 CONFIRMED,须先撤回)→ 809102(版本不对)→ 809104(分组重复或改名)→ 809123(有户还没提交行程用车)→ 分组 / 座位 / 档位类只抛第一条(809103、809105~809110、809116、809118、809124)→ 写入时版本被并发推进,仍报 809102。 +- 入参校验失败返回 `code=400`,`message` 为注解原文(如「提交备注不能超过 500 字」这类),HTTP 仍是 200。 +- 幂等 5 秒,同团重复提交返回 `code=100502`「正式用车需求保存中,请勿重复提交」。 +- 保存只落草稿,不下发车务;下发由第 12 节确认完成。 + +--- + +### 10. 整团免车 `POST /v3/admin/order/group-batch/{groupBatchId}/vehicle-requirement/waive` + +**VO**: `GroupVehicleRequirementWaiveReqVO → GroupVehicleRequirementRespVO` + +#### 使用场景 + +弹窗③「整团不用车」。整团不需要平台用车的团期,**不免车就确认不了**(没有正式需求时 confirm 报 809100)。封装 `waiveGroupVehicleRequirement`(`orderV2GroupBatch.js:997`)。 + +#### 入参 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| groupBatchId | Path | Long | ✅ | 团期 ID | 路径参数 | +| reason | Body | String | ✅ | `@NotBlank`「免车原因不能为空」;≤200「免车原因不能超过 200 字」 | 免车原因 | + +#### 出参 `Result` + +| 字段 | 类型 | 说明 | +|------|------|------| +| status | String | 恒为 `CONFIRMED` | +| groups | Array | 恒为空数组 | +| 其余字段 | — | 同第 7 节 | + +#### 请求示例 + +```json +{ "reason": "客户自行包车,全程不需要平台用车" } +``` + +#### 响应示例 + +按 VO 结构组装,取值为示意: + +```json +{ + "code": 200, + "message": "成功", + "data": { + "requirementId": "2104331168285671426", + "groupBatchId": "2104327837991518210", + "status": "CONFIRMED", + "version": 1, + "remark": "客户自行包车,全程不需要平台用车", + "confirmedBy": "2021059720172838914", + "confirmedAt": "2026-09-27 10:12:00", + "groups": [], + "exemptHouseholds": [] + }, + "traceId": null, + "success": true +} +``` + +#### 空数据 / 降级响应 + +已经免车的团期再调一次按幂等成功处理,返回同样的免车态。 + +#### 错误响应 + +```json +{ + "code": 809114, + "message": "车务已开工(2026-10-08:BUS),不能再声明整团免车", + "data": null, + "traceId": null, + "success": false +} +``` + +(按错误码模板 `车务已开工({0}:{1}),不能再声明整团免车` 组装。) + +#### 业务边界 + +- 判权 `group-batch:demand:confirm`;阶段须为 `RESOURCE_PREPARING / TRIP_FINISHED / REVIEWING` 之一(`GroupBatchService.java:2468`),否则 589501。 +- 带分组的活跃正式需求须为 `DRAFT` 或 `CONFIRMED`,否则 809101(零写入);带分组时失活旧版本、换一份零分组的免车版本;零分组且已是确认态时按幂等成功返回;车务已开工 809114(在写入之前)。 +- 免车只是把正式需求置成「已确认且无分组」,**不会**整团确认需求;弹窗仍要走第 12 节 confirm 才算确认。 +- 免车后 confirm 不放行任何车需求(含待审接送机),见四「对账补放行」。 +- 免车后要改回有车:先第 11 节撤回,再 PUT 分组,否则 PUT 报 809115。 +- 幂等 5 秒,重复提交返回 `code=100502`「整团免车声明处理中,请勿重复提交」。 + +--- + +### 11. 撤回正式用车需求 `POST /v3/admin/order/group-batch/{groupBatchId}/vehicle-requirement/withdraw` + +**VO**: `GroupVehicleRequirementWithdrawReqVO → GroupVehicleRequirementRespVO` + +#### 使用场景 + +两种情况必须先撤回:①已免车,想改成有车;②正式需求已确认(`CONFIRMED` / `DISPATCHED`),想改分组。原来挂在「更多」菜单里,按钮取消后入口位置前端自定(建议放弹窗③分组区右上角,仅在 `status ∈ {CONFIRMED, DISPATCHED}` 时出现)。封装 `withdrawGroupVehicleRequirement`(`orderV2GroupBatch.js:979`)。 + +#### 入参 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| groupBatchId | Path | Long | ✅ | 团期 ID | 路径参数 | +| reason | Body | String | ✅ | `@NotBlank`「撤回原因不能为空」;≤200「撤回原因不能超过 200 字」 | 撤回原因 | + +#### 出参 `Result` + +| 字段 | 类型 | 说明 | +|------|------|------| +| status | String | 恒为 `DRAFT` | +| version | Integer | 已 +1 | +| confirmedBy / confirmedAt | String / LocalDateTime | 已清空 | +| 其余字段 | — | 同第 7 节 | + +#### 请求示例 + +```json +{ "reason": "客户临时增加一天包车,需要重新分组" } +``` + +#### 响应示例 + +按 VO 结构组装,取值为示意: + +```json +{ + "code": 200, + "message": "成功", + "data": { + "requirementId": "2104331168285671426", + "groupBatchId": "2104327837991518210", + "status": "DRAFT", + "version": 2, + "remark": "客户临时增加一天包车,需要重新分组", + "confirmedBy": null, + "confirmedAt": null, + "groups": [], + "exemptHouseholds": [] + }, + "traceId": null, + "success": true +} +``` + +#### 空数据 / 降级响应 + +没有正式需求时不是空数据,而是报 809100。 + +#### 错误响应 + +```json +{ + "code": 809101, + "message": "正式用车需求当前状态 DRAFT 不允许本次操作,期望 [CONFIRMED, DISPATCHED]", + "data": null, + "traceId": null, + "success": false +} +``` + +(按错误码模板 `正式用车需求当前状态 {0} 不允许本次操作,期望 {1}` 组装。`{1}` 是后端集合的 `toString()`,方括号里两个值的先后顺序不固定,前端原样显示 message,不要解析它。) + +#### 业务边界 + +- 判权 `group-batch:demand:confirm`;**无团期阶段守卫**。 +- 撤回本身不受团期阶段限制,但撤回之后的第 9 节 PUT 与第 12 节 confirm 只在 `RESOURCE_PREPARING` 可用,其他阶段报 589501。团期已过 `RESOURCE_PREPARING` 时,撤回入口要提示这一点。 +- 状态须为 `CONFIRMED` 或 `DISPATCHED`,否则 809101;没有正式需求 809100。 +- 撤回后:回到 `DRAFT`、`version+1`、原因追加进 `remark`、清确认人与确认时间、复位团期 `vehicleReady`。 +- 撤回不清团级 `requirementConfirmed`;改完分组后要再走一次第 12 节 confirm,正式需求才会回到 `CONFIRMED`。 +- 幂等 5 秒,重复提交返回 `code=100502`「正式用车需求撤回中,请勿重复提交」。 + +--- + +### 12. 整团确认需求 `POST /v3/admin/order/group-batch/{groupBatchId}/requirement/confirm` + +**VO**: `(无请求体) → GroupBatchRequirementConfirmRespVO` + +#### 使用场景 + +弹窗底部模式一「确认需求并下发」、模式二「打回 N 户并确认其余」的核心一步。一次调用把住房、行程用车、接送机的待审行一起放行,并把正式用车需求推到已确认。封装 `confirmGroupBatchRequirement`(`orderV2GroupBatch.js:656`)。 + +#### 入参 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| groupBatchId | Path | Long | ✅ | 团期 ID | 路径参数;**无请求体** | + +#### 出参 `Result` + +| 字段 | 类型 | 说明 | +|------|------|------| +| groupBatchId | Long(JSON 字符串) | 团期 ID | +| requirementConfirmed | Boolean | 恒为 true | +| dispatchedOrderIds | Long[] | 住房侧本次由待审放行的户 | +| skippedOrderIds | Long[] | 住房侧本次未动的户(已是 PENDING / PROCESSING / DONE) | +| dispatchedCount | Integer | = `dispatchedOrderIds` 长度 | +| vehicleDispatchedOrderIds | Long[] | 行程用车(TRAVEL)本次放行的户 | +| transferDispatchedOrderIds | Long[] | 接送机(TRANSFER)本次放行的户,**对账补放行就用它** | +| vehicleSkippedOrderIds | Long[] | 车侧本次未动的户(两类合并去重);免车时为空数组 | +| vehicleDispatchedCount | Integer | 车侧放行的**条数**(不是户数)= 上面两个列表长度之和 | +| groupVehicleRequirementId | Long | 正式需求 ID;整团免车且无正式需求时为 null | +| groupVehicleRequirementStatus | String | 确认后正式需求的**实际状态**:`DRAFT / PENDING_RECONFIRM → CONFIRMED`;`DISPATCHED / DONE` 原样返回 | +| groupVehicleRequirementVersion | Integer | 确认后版本号 | +| planRefreshState / planRefreshOutboxId / planRefreshReplayKind / planRefreshReplayCount / blockedStage | — | 受控重开链路专用,普通确认为 null | +| advanceUnblocked | Boolean | 恒为 false | + +#### 请求示例 + +```http +POST /v3/admin/order/group-batch/2100856430494973953/requirement/confirm +``` + +#### 响应示例 + +按 VO 结构组装,取值为示意,不是测试服读数: + +```json +{ + "code": 200, + "message": "成功", + "data": { + "groupBatchId": "2100856430494973953", + "requirementConfirmed": true, + "dispatchedOrderIds": ["2100856430239121409", "2102309919002943489"], + "skippedOrderIds": [], + "dispatchedCount": 2, + "vehicleDispatchedOrderIds": ["2100856430239121409", "2102309919002943489"], + "transferDispatchedOrderIds": ["2100856430239121409", "2102309919002943489"], + "vehicleSkippedOrderIds": [], + "vehicleDispatchedCount": 4, + "groupVehicleRequirementId": "2104900000000000001", + "groupVehicleRequirementStatus": "CONFIRMED", + "groupVehicleRequirementVersion": 3, + "planRefreshState": null, + "planRefreshOutboxId": null, + "planRefreshReplayKind": null, + "planRefreshReplayCount": null, + "blockedStage": null, + "advanceUnblocked": false + }, + "traceId": null, + "success": true +} +``` + +#### 空数据 / 降级响应 + +没有降级形态:要么整体成功,要么报错且**零写入**。 + +#### 错误响应 + +```json +{ + "code": 589533, + "message": "仍有 1 户未提交需求或需求不完整,无法整体确认,请先查看缺失清单", + "data": null, + "traceId": null, + "success": false +} +``` + +(按错误码模板 `仍有 {0} 户未提交需求或需求不完整,无法整体确认,请先查看缺失清单` 组装。) + +#### 业务边界 + +- 判权 `group-batch:demand:confirm`。 +- 后端执行顺序(`GroupBatchRequirementService.java:505-584`,同一事务,任一步失败整团零写入):①阶段守卫 589501 → ②住房缺失 589533 → ③车侧违规,抛第一条对应的 809 段码(809100 / 809101 / 809103~809110 / 809122 / 809007 / 809126 / 809125 等)→ ④写确认标记与时间线 → ⑤住房待审户放行 → ⑥正式用车需求推到已确认 → ⑦行程用车与接送机待审行放行 → 提交后通知房务。 +- **整团免车时 ⑥⑦ 都不做**:待审的行程用车与接送机一条都不放行(`:518-520`、`:564`)。 +- **放行集合只含在团、`needs_vehicle=1`、未退团的户**(`:1206-1211`):`needs_vehicle≠1` 的户报了接送机,这里也不放行。 +- 上两条的后果:确认成功后前端必须用 `transferDispatchedOrderIds` 对账,见四「对账补放行」。 +- 同一团期的确认与逐户打回共用一把团级锁。 +- 幂等 5 秒,重复提交返回 `code=100502`「需求确认处理中,请勿重复提交」。 +- 受控重开链路(正式需求处于 `PENDING_RECONFIRM`)下还可能报 809204(车务配车尚未覆盖完整)、809210(刷新重试次数已达上限);普通确认不会遇到。 + +--- + +### 13. 住房逐户打回 `POST /v3/admin/order/{id}/hotel-requirement/reject` + +**VO**: `RejectReqVO → Void` + +#### 使用场景 + +弹窗①某户选「打回」并填了原因,提交时逐户调用。封装 `rejectHotelRequirement(orderId, payload)`(`orderV2.js:1543`)。 + +#### 入参 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| id | Path | Long | ✅ | 子订单 ID(取 hotel-households 的 `orderId`) | 路径参数 | +| returnRemark | Body | String | ✅ | `@NotBlank`「打回/驳回备注不能为空」;≤500「打回/驳回备注不能超过 500 字」 | 打回原因,定制师可见 | + +#### 出参 `Result` + +| 字段 | 类型 | 说明 | +|------|------|------| +| data | null | 成功无返回体 | + +#### 请求示例 + +```json +{ "returnRemark": "第 2 晚房型与人数对不上,3 人只订了 1 间大床,请核对后重新提交" } +``` + +#### 响应示例 + +```json +{ "code": 200, "message": "成功", "data": null, "traceId": null, "success": true } +``` + +#### 空数据 / 降级响应 + +无返回体,成功即 `data=null`。 + +#### 错误响应 + +```json +{ + "code": 589535, + "message": "子订单 26-2355 已分房,请先由房务调整配房后再打回", + "data": null, + "traceId": null, + "success": false +} +``` + +(按错误码模板 `子订单 {0} 已分房,请先由房务调整配房后再打回` 组装。) + +#### 业务边界 + +- 判权 `group-batch:demand:confirm`;团期阶段须为 `RESOURCE_PREPARING`,否则 589501(零写入)。 +- 错误按后端校验顺序:订单不存在 581007 → 非团单 582083 → 阶段 589501 → 该户已有配房 589535(要房务先调整配房)→ 订单无有效需求行 582031 → 源状态不是 `PENDING_REVIEW` / `PENDING` 582083 → 并发改动 582083。 +- 成功后:该户住房需求退回定制师(`REJECTED_TO_CONSULTANT`),**清团级确认标记**,写时间线 `BATCH_REQUIREMENT_REJECT`,给定制师生成返工待办并发站内信。 +- 与整团确认共用团级锁;每户一次调用,各自独立提交,已成功的打回不会因为后面某户失败而回滚。 + +--- + +### 14. 用车逐户打回 `POST /v3/admin/order/{id}/vehicle-requirement/reject` + +**VO**: `RejectReqVO → Void` + +#### 使用场景 + +弹窗②接送机打回(`kind=TRANSFER`)、弹窗③下半各户行程用车打回(`kind=TRAVEL`)。封装 `rejectVehicleRequirement(orderId, payload, kind)`(`orderV2.js:1561`)。 + +#### 入参 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| id | Path | Long | ✅ | 子订单 ID | 路径参数 | +| kind | Query | String | ❌ | `TRAVEL` / `TRANSFER`,**不传按 TRAVEL**;其他值 809000 | 打回哪一类;接送机必须显式传 `TRANSFER` | +| returnRemark | Body | String | ✅ | `@NotBlank`「打回/驳回备注不能为空」;≤500「打回/驳回备注不能超过 500 字」 | 打回原因 | + +#### 出参 `Result` + +| 字段 | 类型 | 说明 | +|------|------|------| +| data | null | 成功无返回体 | + +#### 请求示例 + +```http +POST /v3/admin/order/2102309919002943489/vehicle-requirement/reject?kind=TRANSFER +Content-Type: application/json + +{ "returnRemark": "送机日期与大交通返程不一致,请按返程航班重新提交" } +``` + +#### 响应示例 + +```json +{ "code": 200, "message": "成功", "data": null, "traceId": null, "success": true } +``` + +#### 空数据 / 降级响应 + +无返回体,成功即 `data=null`。 + +#### 错误响应 + +```json +{ + "code": 589501, + "message": "团期状态不允许当前操作", + "data": null, + "traceId": null, + "success": false +} +``` + +#### 业务边界 + +- 判权 `group-batch:demand:confirm`;团期阶段须为 `RESOURCE_PREPARING`,否则 589501(零写入)。**已确认态「审核变更」出现时团期已过这个阶段,所以那里不能打回。** +- 错误按后端校验顺序:订单不存在 581007 → 非团单 582083 → 阶段 589501 → 该户用车需求正在最终确认 582092 → 订单无有效需求行 582031 → 源状态不是 `PENDING_REVIEW` / `PENDING` 582083 → 并发改动 582083。车侧没有 589535。 +- `kind` 只打回指定的那一类:打回接送机不影响该户行程用车,反之亦然。 +- 成功后:该行退回定制师并失活(从 vehicle-households 里消失),**不论 kind 都会清团级确认标记**,写时间线 `BATCH_REQUIREMENT_REJECT`。 +- 打回 TRAVEL 后该户缺行程用车,整团确认会被 809122 挡住;打回 TRANSFER 不挡确认(推理成立,未在测试服实测),该户可能出现在预检的 `transferDeclaredWithoutRequirement` 提示里。 + +--- + +### 15. 住房逐户下发 `POST /v3/admin/order/{id}/hotel-requirement/dispatch` + +**VO**: `DispatchReqVO → Void` + +#### 使用场景 + +已确认态「审核变更」里住房行点「通过」。首次确认不用它(confirm 已一并放行住房)。封装 `dispatchHotelRequirement(orderId, payload)`(`orderV2.js:1577`)。 + +#### 入参 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| id | Path | Long | ✅ | 子订单 ID | 路径参数 | +| dispatchRemark | Body | String | ❌ | ≤500「提交备注不能超过 500 字」 | 下发备注;**请求体必须有**,可传 `{}` | + +#### 出参 `Result` + +| 字段 | 类型 | 说明 | +|------|------|------| +| data | null | 成功无返回体 | + +#### 请求示例 + +```json +{ "dispatchRemark": "" } +``` + +#### 响应示例 + +```json +{ "code": 200, "message": "成功", "data": null, "traceId": null, "success": true } +``` + +#### 空数据 / 降级响应 + +无返回体,成功即 `data=null`。 + +#### 错误响应 + +```json +{ + "code": 582083, + "message": "需求状态不允许此操作,请检查当前状态", + "data": null, + "traceId": null, + "success": false +} +``` + +#### 业务边界 + +- 判权 `group-batch:demand:confirm`;**无团期阶段守卫**。 +- 守卫顺序:订单不存在 581007 → 非团单 582083 → 团期未成团 589552 / 未建团 589553 → 无有效需求行 582031 → 当前不是 `PENDING_REVIEW` 582083 → 并发更新失败 582083。 +- 成功后:`PENDING_REVIEW → PENDING`,写下发备注,房控状态置待处理,写时间线,该团配房完成标记失效。 +- 按源码,资源准备阶段有户改住房会把整团打回「需求待确认」,所以已确认态下住房待审行正常不会出现;页面按数据渲染即可,出现就按本接口处理。 + +--- + +### 16. 用车逐户下发 `POST /v3/admin/order/{id}/vehicle-requirement/dispatch` + +**VO**: `DispatchReqVO → Void` + +#### 使用场景 + +已确认态「审核变更」里车侧行点「通过」:接送机传 `kind=TRANSFER`,行程用车传 `kind=TRAVEL`。多户接送机一起通过时可以改用第 17 节批量放行。封装 `dispatchVehicleRequirement(orderId, payload, kind)`(`orderV2.js:1597`)。 + +#### 入参 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| id | Path | Long | ✅ | 子订单 ID | 路径参数 | +| kind | Query | String | ❌ | `TRAVEL` / `TRANSFER`,**不传按 TRAVEL**;其他值 809000 | 下发哪一类;接送机必须显式传 `TRANSFER` | +| dispatchRemark | Body | String | ❌ | ≤500「提交备注不能超过 500 字」 | 下发备注;请求体必须有,可传 `{}` | + +#### 出参 `Result` + +| 字段 | 类型 | 说明 | +|------|------|------| +| data | null | 成功无返回体 | + +#### 请求示例 + +```http +POST /v3/admin/order/2102309919002943489/vehicle-requirement/dispatch?kind=TRANSFER +Content-Type: application/json + +{ "dispatchRemark": "" } +``` + +#### 响应示例 + +```json +{ "code": 200, "message": "成功", "data": null, "traceId": null, "success": true } +``` + +#### 空数据 / 降级响应 + +无返回体,成功即 `data=null`。 + +#### 错误响应 + +测试服实测原文(2026-09-29 16:38,经网关,用不存在的订单 ID 探错误信封): + +```json +{"code":581007,"message":"订单不存在","data":null,"traceId":null,"success":false} +``` + +#### 业务边界 + +- 判权 `group-batch:demand:confirm`;**无团期阶段守卫**,已确认态也能调。 +- 守卫顺序:该户用车需求正在最终确认 582092「用车需求正在最终确认,请稍后重试」(最先)→ 订单不存在 581007 → 非团单 582083 → 团期未成团 589552 / 未建团 589553 → 无有效需求行 582031 → 当前不是 `PENDING_REVIEW` 582083 → 接送机服务日期未回填 809007(在状态更新之前)→ 并发更新失败 582083。 +- 成功后:`PENDING_REVIEW → PENDING`,写下发备注,车控状态置待处理,软删该行旧的实配记录,写时间线。 +- `kind` 传错类别会动错行:接送机不传 `kind` 会按 TRAVEL 去找行程用车行。 + +--- + +### 17. 接送机批量放行 `POST /v3/admin/order/group-batch/{groupBatchId}/requirement/transfer/batch-confirm` + +**VO**: `TransferBatchConfirmReqVO → TransferBatchConfirmRespVO` + +#### 使用场景 + +两处:①模式一 / 模式二 confirm 成功后,把弹窗②判为通过、但不在 `transferDispatchedOrderIds` 里的户补放行;②已确认态「审核变更」里接送机多户一起通过。封装 `batchConfirmTransferRequirements`(`orderV2GroupBatch.js:819`)。 + +#### 入参 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| groupBatchId | Path | Long | ✅ | 团期 ID | 路径参数 | +| orderIds | Body | Long[] | ✅ | `@NotEmpty`「请至少选择一个要确认的子订单」;≤200「单次确认不超过 200 户」 | 要放行接送机的子订单 | +| dispatchRemark | Body | String | ❌ | ≤500「确认备注不能超过 500 字」 | 放行备注 | + +#### 出参 `Result` + +| 字段 | 类型 | 说明 | +|------|------|------| +| groupBatchId | Long(JSON 字符串) | 团期 ID | +| requestedCount | int | 请求户数 | +| successCount | int | 成功户数 | +| failedCount | int | 失败户数 | +| succeededOrderIds | Long[] | 成功的户 | +| failed[] | Array | 失败明细:`orderId` / `teamNo` / `errorCode`(Integer)/ `reason` | + +#### 请求示例 + +```json +{ "orderIds": ["2100856430239121409", "2102309919002943489"], "dispatchRemark": "" } +``` + +#### 响应示例 + +按 VO 结构组装,取值为示意: + +```json +{ + "code": 200, + "message": "成功", + "data": { + "groupBatchId": "2100856430494973953", + "requestedCount": 2, + "successCount": 1, + "failedCount": 1, + "succeededOrderIds": ["2100856430239121409"], + "failed": [ + { "orderId": "2102309919002943489", "teamNo": "26-2355", "errorCode": 582083, "reason": "需求状态不允许此操作,请检查当前状态" } + ] + }, + "traceId": null, + "success": true +} +``` + +#### 空数据 / 降级响应 + +**允许部分成功**:`code=200` 不代表全部成功,必须看 `failedCount`,大于 0 时把 `failed[]` 逐户列给用户。 + +#### 错误响应 + +```json +{ + "code": 589597, + "message": "子订单 26-2355 不属于本团期", + "data": null, + "traceId": null, + "success": false +} +``` + +(按错误码模板 `子订单 {0} 不属于本团期` 组装。) + +#### 业务边界 + +- 判权 `group-batch:demand:confirm`;团期不存在 589500;团期已取消 589501;其他阶段都允许。 +- 有任一户不属于本团期,**整批拒绝**报 589597,一户都不放行。 +- 逐户失败(状态不对、服务日期未回填等)进 `failed[]`,不影响其他户。 +- 幂等 120 秒,按「团期 + 这批 orderIds」判重,重复提交返回 `code=100502`「接送机需求确认处理中,请勿重复提交」。 + +--- + +## 四、契约约束与正确调用方式(接口类必写) + +> 本节只写后端接受 / 拒绝的规则与前端必须遵守的调用顺序。 + +### ✅ 正确 / ❌ 错误 payload 对照 + +| 场景 | payload | +|------|---------| +| ✅ 接送机逐户打回 | `POST /v3/admin/order/{id}/vehicle-requirement/reject?kind=TRANSFER` + `{ "returnRemark": "原因" }` | +| ❌ 接送机打回不带 kind | `POST /v3/admin/order/{id}/vehicle-requirement/reject` → 按 TRAVEL 打回了该户的**行程用车** | +| ❌ 打回不带原因 | `{ "returnRemark": "" }` → `code=400`「打回/驳回备注不能为空」 | +| ✅ 逐户下发无备注 | `{}` 或 `{ "dispatchRemark": "" }` | +| ❌ 逐户下发不带请求体 | 无 body → 请求体缺失,接口不受理 | +| ✅ 首次保存正式需求 | `{ "version": null, "groups": [ … ] }` | +| ❌ 改完不带版本 | 已有版本时传 `version: null` 或旧版本 → 809102 | +| ✅ 整团不用车 | `POST …/vehicle-requirement/waive` + `{ "reason": "原因" }` | +| ❌ 整团不用车走空分组 | `PUT …/vehicle-requirement` + `{ "groups": [] }`(本团有需要用车的户)→ 809103 | +| ✅ 整团确认 | `POST …/requirement/confirm`,无请求体 | +| ❌ 用批量打回处理接送机 | `POST …/requirement/reject` 的 `VEHICLE` 只覆盖 TRAVEL,只有接送机的户报 589534 | + +### 切换状态时的必要动作 + +**弹窗底部三种提交模式与调用顺序**(按顺序串行调用,**任一步失败就停下、提示错误、重拉页签数据**;已成功的逐户打回不回滚): + +| 模式 | 触发条件 | 按钮文案 | 调用顺序 | +|------|------|------|------| +| 一 | 三步里没有任何「打回」 | 确认需求并下发 | ① 弹窗③改过分组或还没有正式需求 → `PUT vehicle-requirement`;选了整团不用车 → `waive`(二选一,都不需要就跳过)→ ② `POST requirement/confirm` → ③ 对账补放行(见下) | +| 二 | 只有弹窗②(接送机)有打回 | 打回 N 户并确认其余 | ① 逐户 `vehicle-requirement/reject?kind=TRANSFER` → ② 同模式一的 PUT / waive → ③ `POST requirement/confirm` → ④ 对账补放行 | +| 三 | 弹窗①(住房)或弹窗③(行程用车)有任一打回 | 打回 N 项 | 逐户 `hotel-requirement/reject`、`vehicle-requirement/reject?kind=TRAVEL`,弹窗②的接送机打回(`kind=TRANSFER`)一并提交;**不调 confirm** | + +- **打回一律排在 confirm 之前**:逐户打回会清团级确认标记,排在后面会把刚写上的确认清掉。 +- **模式三不调 confirm 的原因**:住房打回后该户算缺失(589533),行程用车打回后该户缺行程用车(809122),确认必失败。 +- **模式三不提交弹窗③的分组编辑**:打回行程用车后 PUT 会被 809123 挡住。要保留分组编辑,让用户先点「仅保存草稿」(PUT)再提交打回;弹窗里要提示这一点。 +- **有打回项没填原因时禁用提交按钮**(`returnRemark` 必填、≤500)。免车原因同理(必填、≤200)。 +- N 的口径:模式二的 N = 接送机打回户数;模式三的 N = 三步打回行数合计(同一户打了住房和行程用车算 2 项)。 + +**对账补放行(模式一、模式二 confirm 成功后必做)**: + +1. 取弹窗②里判为「通过」、且该行 `status = PENDING_REVIEW` 的户,记为 A。 +2. 从 A 里去掉 `confirm` 响应 `transferDispatchedOrderIds` 里已有的户,剩下的记为 B。 +3. B 非空 → `POST requirement/transfer/batch-confirm`,`orderIds = B`;看 `failedCount`,失败户逐条提示。 +4. 会出现 B 非空的两种情况:整团免车(confirm 一条车需求都不放行);该户 `needs_vehicle≠1`(不在放行集合)。 + +**已确认态(`requirementConfirmed=true`)的「审核变更(N)」**: + +- N = 住房与车侧所有 `status = PENDING_REVIEW` 的行数。 +- 只提供「通过」:住房 → 第 15 节逐户下发;接送机 → 第 16 节 `kind=TRANSFER` 或第 17 节批量;行程用车 → 第 16 节 `kind=TRAVEL`。 +- 「打回」只在 `batchStatus = RESOURCE_PREPARING` 时显示;按源码,这个阶段有户改需求会自动清确认标记、页签回到待确认态,所以已确认态下「打回」实际不会出现。 + +**主按钮可用性**: + +- `batchStatus ≠ RESOURCE_PREPARING` 且未确认:确认与打回都会 589501,主按钮置灰并显示 `batchStatusName`。 +- 预检 589507(无 demand:confirm 权限):隐藏主按钮、只读展示。 +- 预检 `ready=false` 但阶段正确:主按钮**仍可点**(用户要进弹窗去打回),弹窗里模式一 / 模式二的提交按钮按 `missing + vehicleMissing` 条数给出 `missingText` 并禁用。 + +**原「更多」菜单里三项能力的去处**: + +- 整团免车(waive)→ 弹窗③「整团不用车」。 +- 撤回(withdraw)→ 前端自定入口,仅 `status ∈ {CONFIRMED, DISPATCHED}` 时显示;已免车改有车、已确认改分组都必须先撤回。 +- 受控重开(`POST …/vehicle-requirement/reopen`,封装 `reopenGroupVehicleRequirement`,`orderV2GroupBatch.js:889`)→ 前端自定入口,契约不变。 + +**字段来源与前端算法(设计 A 页签主体)**: + +| 展示元素 | 来源 / 算法 | +|------|------| +| 页头 N 户 N 人 | 详情 `subOrderCount` / `enrolledPeople` | +| 状态条「需求待确认 / 需求已确认」 | 详情 `requirementConfirmed` | +| 状态条「N 户已全部提交」 | 汇总 `hotelSubmittedOrderCount == hotelNeededOrderCount` 且 用车逐户 `countedHouseholdCount == householdCount` | +| 状态条「预检通过,可以确认」 | 预检 `ready`;为 false 时显示 `missing.length + vehicleMissing.length` 条未就绪 | +| 已确认态「时间 由 某人 确认」 | 状态流水最后一条 `BATCH_REQUIREMENT_CONFIRM` 的 `changedAt` / `operatorName`(未实测) | +| 已确认态「审核变更(N)」列表 | 状态条下方逐行列出 `status = PENDING_REVIEW` 的行:`customerName` + 行的 `kindName`(住房行写「住房」)+ 该行内容;每行一个「通过」,右上「全部通过(N)」。接口不区分新报与修改,界面不写「新报 / 改了」 | +| 已确认态「N 户已下发房务」 | 住房逐户 `status ∈ {PENDING, PROCESSING, DONE}` 的户数 | +| 已确认态「N 户已下发车务」(接送机) | 用车逐户 TRANSFER 行 `status ∈ {PENDING, PROCESSING, DONE}` 的户数 | +| 已确认态「正式需求已确认 N 组」 | 正式需求 `status` + `groups.length` | +| 住房卡「待审核 N」 | 住房逐户 `status = PENDING_REVIEW` 的户数 | +| 住房卡「间/晚(峰值)」 | 每晚 `dailyRoomBreakdown[].rooms[].totalRoomCount` 求和,取各晚最大值 | +| 住房卡「N 晚」 | `endDate − departDate`(天数差),**不用** `dailyRoomBreakdown.length` | +| 住房卡「间夜」 | 全部晚的 `totalRoomCount` 总和 | +| 住房卡「N 家酒店」 | `dailyRoomBreakdown[].hotels[].hotelId` 去重计数 | +| 住房卡「N 晚有 N 户客户自订」 | 住房逐户 `days[].customerSelfBooked=true`:按 `stayDate` 去重得晚数,按 `orderId` 去重得户数 | +| 住房卡「N/N 户已提交」 | 汇总 `hotelSubmittedOrderCount / hotelNeededOrderCount` | +| 住房卡「N 户有特殊需求」 | 汇总 `orderSpecialTags` 中 `requirementType=HOTEL` 的 `orderId` 去重计数 | +| 接送机卡「待审核 N」 | 用车逐户 TRANSFER 行 `status = PENDING_REVIEW` 的户数 | +| 接送机卡「N 户 · N 人」 | `transferSummary.householdCount / headcount` | +| 接送机卡「接机 N 户 N 人 · 送机 N 户 N 人」 | 户数用 `pickupHouseholdCount / dropoffHouseholdCount`;人数 = 用车逐户 TRANSFER 行按 `pickupRequired=true` / `dropoffRequired=true` 分别对 `headcount` 求和;**日期不配方向**(见缺口) | +| 接送机卡车型 | `transferSummary.vehicleSeatSummary[]` → `{vehicleTypeName} {seats}座 ×{count}` | +| 行程用车卡「正式需求 · 草稿」 | 正式需求 `status` 映射中文(六.5);`data=null` 显示「未形成正式需求」;`CONFIRMED` 且 `groups` 为空显示「整团不用车」 | +| 行程用车卡「N 组 · 逐日覆盖 x/y 天」 | N = `groups.length`;x = `groups[].days[].tripDate` 去重后落在 `departDate..endDate` 内的天数;y = `endDate − departDate + 1` | +| 行程用车卡车型(日期) | `groups[]` → `{vehicleTypeName} {seats}座 ×{count}({serviceStartDate}–{serviceEndDate})` | +| 行程用车卡「N/N 户已报」 | 用车逐户 `countedHouseholdCount / householdCount` | +| 大交通声明提示 | 预检 `transferDeclaredWithoutRequirement[]`:`customerName` + 声明了接机(`pickupRequired`)/ 送机(`dropoffRequired`)+ 定制师 `consultantName`;「联系定制师」→ `emit('contact', orderId)` | +| 逐晚用房表 晚次 / 日期 | 以 `departDate..endDate−1` 逐晚生成行,按 `stayDate` 关联 `dailyRoomBreakdown`;没有条目的晚在备注列标「全团客户自订」或「无已提交需求」(用住房逐户 `customerSelfBooked` 区分) | +| 逐晚用房表 城市 · 酒店 | `hotels[].hotelName`;城市按 `hotelId` 关联住房逐户 `days[].hotels[].city` | +| 逐晚用房表 房型列 | **按数据里出现的 `roomCategory` 动态生成列**,列头用 `roomCategoryName`;设计稿里的「标间 / 大床 / 亲子 / 家庭」四列只是示意,实测数据里出现的是 DELUXE / KING / STANDARD | +| 逐晚用房表 合计 / 间夜合计 | 行合计 = 该晚 `rooms[].totalRoomCount` 之和;末行按列求和 | +| 用车安排 行程用车矩阵 | 行 = `groups[]`(`groupCode` + 车型);列 = `departDate..endDate`;格 = 该组该日 `days[].headcount`,无则空 | +| 用车安排「在团人数」 | 详情 `enrolledPeople` | +| 用车安排 接送机 | 按方向分两行:接机行取用车逐户 TRANSFER 行里 `pickupRequired=true` 的户,送机行取 `dropoffRequired=true` 的户;每行列客户名、人数(`headcount` 求和)、车型(按 `fleet` 计数)。标题旁单列服务日期 `transferSummary.serviceDates`,**不与方向配对** | +| 逐户表 数据集 | 住房逐户 ∪ 用车逐户,按 `orderId` 合并 | +| 逐户表 筛选「全部」 | 合并后户数 | +| 逐户表 筛选「待审核」 | 住房 `status` 或任一车侧行 `status` 为 `PENDING_REVIEW` 的户 | +| 逐户表 筛选「已打回」 | 住房 `status` 为 `REJECTED_TO_CONSULTANT` / `REJECTED_TO_ADMIN` 的户(车侧见缺口) | +| 逐户表 筛选「未提交」 | 住房 `status=null` 或 用车逐户户级 `status=null` 的户 | +| 逐户表 筛选「有特殊需求」 | 住房 `specialTags` 非空 或 任一车侧行 `specialTags` 非空 | +| 逐户表 搜索 | 前端本地过滤 `customerName` / `teamNo` / `orderNo` / `consultantName` | +| 逐户表 户 / 人数 / 定制师 | `customerName`(副行 `teamNo`,null 显示 —)/ `participantCount` / `consultantName` | +| 逐户表 住房 | `days[].hotels[].hotelName` 去重拼接;副行 晚数与房数合计 | +| 逐户表 接送机 | TRANSFER 行:接 / 送标记 + `serviceDates` + `fleet`;无 TRANSFER 行且在 `transferDeclaredWithoutRequirement` 里 →「未报 · 大交通声明了接机 / 送机」;不在用车逐户列表里 →「不需要」;其余 → — | +| 逐户表 行程用车 | TRAVEL 行 `fleet` + `serviceDates`;不在用车逐户列表里 →「不需要」 | +| 逐户表 特殊需求 | 住房 `specialTags` ∪ 车侧 `specialTags[].name` | +| 逐户表 备注 | 住房 `remark` 与车侧 `remark` 分行显示 | +| 逐户表 状态 | 住房与车侧各显示 `statusName` 原文;户级徽标按「已打回 > 待审核 > 未提交 > 已下发」取最高 | +| 逐户表 详情 | 打开保留的 `HouseholdRequirementModal` | +| 逐户表 联系定制师 | `emit('contact', orderId)` → `index.vue:705` | + +**字段来源与前端算法(设计 B 审核弹窗)**: + +| 弹窗元素 | 来源 / 算法 | +|------|------| +| 步骤条 ①住房 ②接送机 ③行程用车 | 各步待审行数与已打回数由前端统计 | +| ① 逐户列表 | 住房逐户 `households[]`;`status ∈ {PENDING_REVIEW, PENDING}` 的户可选「通过 / 打回」,其余只读 | +| ①「全部重置为通过」 | 前端清空本步所有打回标记与原因 | +| ①「本次下发」 | 本步判为通过且 `status=PENDING_REVIEW` 的户,按 `days[].hotels[].rooms[].roomCount` 求和得间夜 | +| ② 逐户列表 | 用车逐户 TRANSFER 行:接机 `pickupRequired` / 送机 `dropoffRequired` / 人数 `headcount` / 车型 `fleet` / `specialTags` / `remark` | +| ② 大交通声明提示 | 预检 `transferDeclaredWithoutRequirement[]` | +| ③ 正式需求编辑区 | 有正式需求用第 7 节 `groups`;没有或点「按户需求重新汇总」用第 8 节 `draft`;`violations` 逐条标在对应组 / 日期旁 | +| ③「添加分组」 | 前端在 `groups` 里新增一组(`groupId=null`,`groupCode` 不可与已有组重复) | +| ③ 逐日覆盖 `coverText` | 同设计 A「逐日覆盖 x/y 天」 | +| ③「整团不用车」 | 第 10 节 waive(需填原因) | +| ③「仅保存草稿」 | 第 9 节 PUT,成功后用返回的 `version` 覆盖本地 | +| ③ 各户行程用车需求 | 用车逐户 TRAVEL 行;打回走第 14 节 `kind=TRAVEL` | +| 底部 `missingText` | 预检 `missing[]` 与 `vehicleMissing[]` 条数;`reasonName` / `detail` 逐条可展开 | +| 底部 `submitLabel` | 按上面三种模式切换 | + +**字段缺口(本期不展示)**: + +| 设计稿元素 | 为什么没有 | 本期处理 | +|------|------|------| +| 户级变更前后差异(「第 4 晚加 1 间大床」)与「新报 / 修改」的区分 | 所有接口只返回当前版本,不返回上一版或差异 | 本期不展示,只列「某户 住房 / 接送机 / 行程用车 待审核」 | +| 接送机按日期拆方向(「10/03 接机 5 户 16 人 · 10/08 送机 5 户 16 人」) | `serviceDates` 不带方向,接口没有「哪一天是接机」 | 本期不展示日期与方向的配对;按方向显示户数与人数,日期单列 | +| 大交通声明的接送日期 | `transferDeclaredWithoutRequirement` 只有方向标记与 `pickupRemark` | 本期不展示日期 | +| 车侧「已打回」 | 车侧打回后该行失活,不再返回,与「未提交」读数相同 | 本期「已打回」筛选只覆盖住房侧 | + +--- + +## 五、数据库行为(涉及写操作时必写) + +本条后端零改动,下表只列前端各提交动作在后端产生的既有写入,供前端判断「失败时有没有部分写入」。 + +| 前端动作 | 后端写入 | 失败时 | +|----------|---------------------|---------------------| +| `PUT vehicle-requirement` | 正式用车需求新版本落为 `DRAFT`,`version+1` | 零写入 | +| `waive` | 正式用车需求置为 `CONFIRMED` 且无分组,记免车原因 | 零写入 | +| `withdraw` | 正式用车需求回 `DRAFT`、`version+1`、原因追加进备注、清确认人与时间、复位团期 `vehicleReady` | 零写入 | +| `requirement/confirm` | 团期 `requirementConfirmed=true` + 时间线 `BATCH_REQUIREMENT_CONFIRM`;住房待审行 → `PENDING`;正式需求 → `CONFIRMED`;车侧待审行 → `PENDING`(免车时后两项不做) | 同一事务,整团零写入 | +| 逐户 `reject`(住房 / 用车) | 该户该类需求行 → `REJECTED_TO_CONSULTANT` 并写打回原因;清团级 `requirementConfirmed`;时间线 `BATCH_REQUIREMENT_REJECT` | 该户零写入;**此前已成功的其他户不回滚** | +| 逐户 `dispatch`(住房 / 用车) | 该户该类需求行 `PENDING_REVIEW → PENDING`,写下发备注,房控 / 车控置待处理 | 该户零写入 | +| `transfer/batch-confirm` | 成功户的接送机行 → `PENDING` | 逐户独立,部分成功 | + +--- + +## 六、边界行为 + +- 未登录 → 网关拦截,返回信封 `code=401`。 +- 无团期权限 → 589507;只有 `group-batch:view` 的角色能看汇总、逐户、详情、流水,看不了预检与正式用车需求(这两个要 `group-batch:demand:confirm`),页签要能部分降级。 +- 团期不存在 → 589500(状态流水除外,它返回空数组)。 +- 团期阶段不是 `RESOURCE_PREPARING` → 确认、逐户打回、保存正式需求都是 589501;逐户下发、接送机批量放行、撤回不受阶段限制。 +- 同一团期两个人同时操作:确认与逐户打回共用团级锁;正式需求靠 `version` 乐观锁(809102);各写接口有 5 秒(接送机批量 120 秒)幂等窗口,重复点击返回 `code=100502`,前端提交期间锁按钮即可。 +- 入参校验失败 → HTTP 200、`code=400`,`message` 为注解原文,多条用「; 」连接。 +- 所有 ID(`groupBatchId`、`orderId`、`requirementId` 等)在 JSON 里是字符串,前端不要转 Number。 + +--- + +## 六.5、枚举 / 数据字典(接口出现枚举时必写) + +### batchStatus(com.hulalv.order.groupbatch.enums.GroupBatchStatus) + +**所属字段**: `GroupBatchDetailRespVO.batchStatus`、`GroupBatchRequirementCheckRespVO.batchStatus` | **类型**: `String` + +| 值 | 中文 | 说明 | +|----|------|------| +| `RECRUITING` | 招募中 | 确认 / 打回 589501 | +| `RESOURCE_PREPARING` | 资源准备中 | **唯一**可整团确认、可打回、可保存正式用车需求的阶段 | +| `MATERIAL_PREPARING` | 物料准备中 | 确认 / 打回 589501 | +| `PENDING_DEPARTURE` | 待出发 | 同上 | +| `TRAVELLING` | 出行中 | 同上 | +| `TRIP_FINISHED` | 出行完毕 | 同上;可免车 | +| `REVIEWING` | 核单中 | 同上;可免车 | +| `SETTLED` | 已结算 | 同上 | +| `CANCELLED` | 已取消 | 同上;接送机批量放行也拒 | + +### status(com.hulalv.order.requirement.enums.RequirementStatus) + +**所属字段**: 住房逐户 `households[].status`、用车逐户 `households[].status` 与 `requirements[].status` | **类型**: `String` + +| 值 | 中文 | 说明 | +|----|------|------| +| `PENDING_REVIEW` | 待审核 | 定制师已提交、团期管理员未放行;车侧 TRAVEL 的 `statusName` 显示「待提交车务」 | +| `PENDING` | 待房务配 | 已放行;车侧 `statusName` 以接口返回为准 | +| `PROCESSING` | 配房中 | 房务 / 车务处理中 | +| `DONE` | 配房完成 | 处理完成 | +| `REJECTED_TO_CONSULTANT` | 驳回 | 已打回定制师;只在住房逐户出现,车侧打回行不返回 | +| `REJECTED_TO_ADMIN` | 驳回 | 被下游驳回到团期管理员 | +| `null` | — | 该户一条都没提交 | + +显示一律用接口返回的 `statusName`,不要用本表中文自行翻译(车侧与住房侧同一状态的中文不同)。 + +### kind(com.hulalv.order.requirement.enums.VehicleRequirementKind) + +**所属字段**: 用车逐户 `requirements[].kind`;逐户打回 / 下发的 Query `kind` | **类型**: `String` + +| 值 | 中文 | 说明 | +|----|------|------| +| `TRAVEL` | 行程用车 | Query 不传时的默认值 | +| `TRANSFER` | 接送机 | 必须显式传 | + +### status(com.hulalv.order.groupbatch.enums.GroupVehicleRequirementStatus) + +**所属字段**: `GroupVehicleRequirementRespVO.status`、预检 `groupVehicleRequirementStatus`、确认响应 `groupVehicleRequirementStatus` | **类型**: `String` + +| 值 | 中文 | 说明 | +|----|------|------| +| `DRAFT` | 草稿 | 可 PUT;确认时推到 `CONFIRMED` | +| `CONFIRMED` | 已确认 | `groups` 为空即整团免车;改分组须先撤回 | +| `DISPATCHED` | 已发车务 | 改分组须先撤回 | +| `DONE` | 配车完成 | — | +| `PENDING_RECONFIRM` | 待重新确认 | 受控重开链路;确认时推到 `CONFIRMED` | +| `CANCELLED` | 已取消 | 流团时关闭并失活,之后取正式用车需求读不到(`data` 为 null) | + +### missing[].reason(GroupBatchRequirementCheckRespVO.MissingItem) + +**所属字段**: 预检 `missing[].reason`(中文见同行 `reasonName`) | **类型**: `String` + +| 值 | 中文 | 说明 | +|----|------|------| +| `NOT_SUBMITTED` | 未提报 | 该户需要住房但没有有效住房需求(含被打回后未重提) | +| `ROOM_CATEGORY_MISSING` | 缺房型或房数 | 某晚某段缺房型大类或房数小于 1,附 `dayNumber` / `segmentIndex` | +| `INVALID_REQUIREMENT` | 需求结构异常 | 逐晚数据为空或结构不完整 | +| `NIGHTS_MISMATCH` | 住宿晚数对不上 | 附 `expectedNights` / `actualNights` | +| `DAY_NUMBER_INVALID` | 晚序号异常 | 晚序号跳号、重复或越界,附 `expectedNights` / `actualNights` | + +### vehicleMissing[].reason(GroupBatchRequirementCheckRespVO) + +**所属字段**: 预检 `vehicleMissing[].reason`(无中文名字段,展示用 `detail`) | **类型**: `String` + +| 值 | 中文 | 说明 | +|----|------|------| +| `GROUP_REQUIREMENT_NOT_FOUND` | 未形成正式需求 | 对应 809100;处置:弹窗③汇总后保存,或整团不用车 | +| `GROUP_REQUIREMENT_STATUS_INVALID` | 正式需求状态不对 | 对应 809101 | +| `NO_GROUP` | 没有分组 | 对应 809103 | +| `GROUP_CODE_INVALID` | 分组编码重复或改名 | 对应 809104 | +| `DAY_OUT_OF_GROUP_RANGE` | 逐日行超出本组服务日 | 对应 809105 | +| `DAY_GAP_IN_GROUP_RANGE` | 本组服务日有缺日 | 对应 809106 | +| `MEMBER_FOREIGN_ORDER` | 成员不属本团 | 对应 809107 | +| `MEMBER_DUPLICATE_DAY` | 同户同日在两个组 | 对应 809108 | +| `ORDER_DAY_UNCOVERED` | 某户某日没被分组覆盖 | 对应 809109 | +| `HEADCOUNT_LESS_THAN_MEMBERS` | 当日人数小于成员户数 | 对应 809110 | +| `HOUSEHOLD_REQUIREMENT_NOT_SUBMITTED` | 该户没交行程用车 | 对应 809122,只带 `orderId` / `orderNo`,处置是催定制师提交 | +| `MEMBER_GROUP_MISMATCH` | 户车型与分组车型不符 | 对应 809125,处置是重新汇总并保存 | +| `TRANSFER_SERVICE_DATES_NOT_BACKFILLED` | 接送机服务日期未回填 | 对应 809007,只带 `orderId` | +| `TRANSFER_WINDOW_INCOMPLETE` | 接送机窗没盖住大交通日期 | 对应 809126,带 `tripDate`,处置是让定制师重新提交接送机需求 | +| `GROUP_SPEC_INCOMPLETE` | 座位数与车辆数没同填 | 对应 809118(源码常量 `GroupVehicleRequirementService.java:229`,VO 注释未列) | +| `GROUP_SEATS_INSUFFICIENT` | 座位不够 | 对应 809116(同上 `:232`) | +| `GROUP_SEATS_NOT_IN_OPTIONS` | 座位不在可选档位 | 对应 809124(同上 `:235`) | +| `GROUP_SEAT_OPTIONS_UNAVAILABLE` | 档位读取失败 | 复用 809120 语义(同上 `:238`) | + +上表「中文」列是本条给的说明性译名,接口不返回;遇到表外的值按 `detail` 原文展示。 + +### eventType(com.hulalv.order.groupbatch.enums.GroupBatchLogEventType,本页只用三个) + +**所属字段**: `GroupBatchStatusLogItemVO.eventType` | **类型**: `String` + +| 值 | 中文 | 说明 | +|----|------|------| +| `BATCH_REQUIREMENT_CONFIRM` | 需求整体确认 | 已确认态「时间 由 某人 确认」取最后一条 | +| `BATCH_REQUIREMENT_REJECT` | 需求打回 | 批量或逐户打回;逐户时 `extra` 只含本户 orderId | +| `BATCH_REQUIREMENT_REOPENED` | 需求重开待确认 | 有户改需求或转团,后端自动清确认标记 | + +## 六.6、修改前后对比(修改/删除类接口必写,新增跳过) + +### 字段级对比 + +| 字段 | 改前 | 改后 | +|------|------|------| +| 所有接口的请求与响应字段 | 不变 | 不变(后端零改动) | +| vehicle-households 的 `kind` 调法 | 父组件按 `TRAVEL` / `TRANSFER` 分两次取 | 可不传 `kind` 一次取两类,按 `requirements[].kind` 拆;分两次取仍然有效 | + +### 行为级对比 + +| 行为 | 改前 | 改后 | +|------|------|------| +| 打回 | 顶部「打回选中户」→ 批量打回端点,整批一条原因,接送机打不了 | 弹窗内逐户逐类打回,每户各自原因,接送机 `kind=TRANSFER` | +| 整团确认 | 顶部「整团确认需求」直接 confirm | 弹窗底部按三种模式串行调用,确认后对账补放行接送机 | +| 正式行程用车需求 | 「新增正式行程用车需求」按钮 + 独立编辑弹窗 | 收进弹窗③,确认前按需 PUT | +| 免车 / 撤回 / 重开 | 「更多」菜单 | 免车进弹窗③;撤回与重开入口前端自定 | +| 接送机放行 | 用车逐户块勾选 + 批量放行按钮 | 首次确认随 confirm 放行并对账补放行;已确认态走「审核变更」 | +| 已确认后的待审行 | 可在逐户块逐条下发 | 「审核变更(N)」只提供通过 | + +## 六.7、影响评估(修改/删除类必写) + +- **是否破坏向后兼容**: 否(后端零改动,旧页签照常可用) +- **前端是否必须同步发版**: 否 +- **前端 workaround 清理点**: `RequirementRejectModal.vue` 及批量打回封装 `rejectGroupBatchRequirement` 的调用点(`orderV2GroupBatch.js:679`,封装本身可留);`RequirementTab.vue:53 / :68 / :77 / :86` 四个按钮与 `:588` 整团确认对话框;`VehicleHouseholdsSection.vue` 的勾选与批量放行按钮(并入弹窗②与审核变更) + +## 七、不影响范围(显式声明, 帮前端/QA 缩小排查面) + +- **仅影响**: 出团详情「查看需求」页签及其子组件 +- **零影响**: + - 后端全部接口与数据(零改动) + - 出团详情其他页签(整团总览、子订单、配房明细、行程、用餐、操作记录等) + - 定制师侧提交住房 / 用车 / 接送机需求的页面 + - 房务、车务侧的工作台 + - `HouseholdRequirementModal`(保留原样,只是改由逐户表「详情」打开) + +--- + +## 八、测试环境已验证 + +测试服网关实测,带 ✓ 的为本条示例所用原文: + +``` +GET /v3/admin/order/group-batch/2100856430494973953/requirement-summary → 200 + 两晚逐晚用房 + 接送机汇总 ✓(2026-09-29 16:38) +GET /v3/admin/order/group-batch/2100856430494973953/requirement/vehicle-households → 200 + 2 户 4 行(不传 kind 两类同返)✓(2026-09-29 16:38) +POST /v3/admin/order/1/vehicle-requirement/dispatch?kind=TRANSFER → 581007 订单不存在(错误信封形态)✓(2026-09-29 16:38) +GET /v3/admin/order/group-batch/2104327837991518210/requirement/confirm-check → 200 + vehicleWaived=true + ready=true ✓(2026-09-27) +``` + +验证团期: `groupBatchId=2100856430494973953`(2 户,出发 2026-10-08,资源准备中);免车团期 `groupBatchId=2104327837991518210`。 + +其余接口的示例按 VO 结构组装、取值为示意,已在各节示例上方逐一标注;「确认人取自状态流水」「打回接送机不挡确认」两条是按源码推理,未在测试服实测,同样已在对应小节标注。 + +--- + +## 十、相关文档 + +- 设计稿: 「设计稿」https://claude.ai/artifact/R2b6RsiUvykdF9kBUdaQcC +- 同日既有条目: `changelogs-v2/2026-09/29_frontend_团期详情用车汇总行程用车与接送机改逐条列出-前端优化-管理后台.md` +- 同日既有条目: `changelogs-v2/2026-09/29_frontend_团期详情用车需求确认弹窗点确认不发请求-前端缺陷-管理后台.md` +- 后端源码(`origin/dev-v3@7b702f5c`,`hl-order-service-v3`): `GroupBatchRequirementController.java`、`GroupBatchQueryController.java`、`HotelRequirementAdminController.java`、`VehicleRequirementAdminController.java`、`GroupBatchRequirementService.java`、`GroupVehicleRequirementService.java`、`GroupBatchService.java` +- 前端源码(hl-ui `origin/v2.1@cdfaf793`): `src/views/order-v2/batch/detail/components/`、`src/api/orderV2GroupBatch.js`、`src/api/orderV2.js` + +## 关联 / 联系人 + +### 链接 + +- **Issue**: 无(ticket=frontend) +- **PR**: 无(后端零改动) +- **前端提交**: 前端完成后填入 frontmatter `frontend_ref` + +### 联系人 + +- **后端负责人**: @wx +- **前端负责人**: @mmg