--- schema: "hl-changelog/v2" ticket: "8046" title: "团期用房新增子订单订房记录接口" consumer: "admin" author: "jw(GIT)" change_type: "新增接口" backend_status: "deployed" gateway_status: "not_required" frontend_status: "verified" frontend_owner: "mmg" frontend_ref: "3362a94e2425456276c4906e6ee991716146cdf8" target_release: "" verified_at: "2026-09-21" status_note: "团期「查看需求 · 用房」板块是上下两块,此前只有上块(汇总)有接口。本次新增下块:GET /v3/admin/order/group-batch/{groupBatchId}/requirement/hotel-households,一次返回该团期下所有需订房子订单的逐晚填报明细——每户一张卡(团号 / 联系人 / 人数 / 定制师 / 需求状态 / 配房需求 / 特殊需求标签 / 打回原因)+ 逐晚(住宿日期 / 酒店 / 城市 + 区县 / 房型 / 间数;页面地点那列请用 district 而非 city)。此前这块数据只能逐户调 GET /v3/admin/order/{orderId}/itinerary,N 户 N 次。两块的数字关系是硬约束:汇总就是把子订单记录汇总出来的,requirement-summary 的逐日间数 == 本接口里 countedInSummary=true 那些户的逐日加总,一间不差;判据与逐晚间数都与汇总走同一份单源(RequirementStatus.isSummaryCounted 与 HotelRequirementDaysNormalizer),由单测守护。但本列表比汇总宽:被打回正在改的户也会列出来并标 countedInSummary=false(否则管理员一打回,那户就从页面消失,没法跟进谁还没改回来),前端应灰显并标注「打回中 · 未计入汇总」。客户自订晚同理——汇总整晚跳过,本接口仍列出该晚并置 customerSelfBooked=true、hotels 为空数组,避免卡片上「第 N 晚凭空消失」。requirement-summary 一字未改。 前端实证维持 pending(mmg 2026-09-20):真实前端特性(「查看需求·用房」下半块,与已交付的上半块汇总 RoomSummarySection 同页并列),changelog 自标前端可延后接入;接入要点已确认:地点列用 district、打回户灰显「打回中·未计入汇总」、自订晚列出 hotels=[]、未提交户 status=null days=[] 照常列出;本期不派发,记入前端待审清单待排期。前端 2026-09-21 已交付:新增 RoomHouseholdsSection 与 RoomSummarySection 同页并列(RequirementTab),每户一卡(团号/联系人/人数/定制师未指派兜底/statusName 或「未提交」+特殊标签+配房需求)+逐晚扁平表;打回户按 status 原码 REJECTED_* 前缀判定灰显+「打回中 · 未计入汇总」标注+打回原因 alert;未提交户(status=null、days=[])照常列出催办;客户自订晚单行「客户自订」;地点列用 district 不用 city;roomTypeName null 依次回落 roomCategoryName/roomCategory;stats 条显「共 N 户 · 计入汇总 M 户 · K 户未计入(打回或未提交)」;整团确认/按户打回后与汇总同步 reload。定向 spec 8 例+RequirementTab 10 例全过。" updated_at: "2026-09-20" base: "dev-v3" --- # order-v3: 团期用房新增子订单订房记录接口 > **服务**: hl-order-service-v3 (端口 8086) > **PR**: [#8055](https://git.1814.love:8443/wx/HL/pulls/8055)(主体)、[#8057](https://git.1814.love:8443/wx/HL/pulls/8057)(补 district)、[#8059](https://git.1814.love:8443/wx/HL/pulls/8059)(打回户改看最新版本行)、[#8060](https://git.1814.love:8443/wx/HL/pulls/8060)(AC-11 固化) > **Issue**: [#8046](https://git.1814.love:8443/wx/HL/issues/8046) > **日期**: 2026-09-20 > **影响范围**: 管理后台「团期订单 → 查看需求」页的「用房」板块下半部分(子订单订房记录) --- ## ⚠️ 关键变化 - **纯新增一个只读端点**,既有接口一个没动(`requirement-summary` 逐字未改)。 - **汇总 == Σ子订单**:`requirement-summary` 的逐日间数严格等于本接口里 `countedInSummary=true` 那些户的逐日加总。两处走同一份状态判据与同一个 days 归一器,不是各算各的。 - **本列表比汇总宽**:被打回正在改的户也列出来,标 `countedInSummary=false`。前端要灰显并标注「打回中 · 未计入汇总」,否则用户会觉得两块数字对不上。 - **客户自订晚保留**:汇总整晚跳过,本接口列出该晚并标记,`hotels` 为空数组。 - **地域是反查来的**:days JSON 里不存 city/district,按 hotelId 批量查资源库得到;取不到为 `null`,不影响间数。**页面地点那一列用 `district` 而不是 `city`**(见出参表说明)。 --- ## 一、背景 原型「用房」板块是上下两块: | 块 | 内容 | 改前 | |---|---|---| | 上:汇总 | Day N \| 酒店 \| 房型 \| 所需间数 | ✅ `requirement-summary`(#8030 已交付) | | 下:子订单订房记录 | 每户一张卡 + 逐晚(第N晚 / 日期 / 城市·酒店 / 间数 / 房型)+ 配房需求 + 打回 | ❌ **无接口** | 下块此前只能逐户调 `GET /v3/admin/order/{orderId}/itinerary`,N 户就是 N 次请求。 最接近的现有端点 `GET /v3/admin/house/group-batches/{groupBatchId}/room-requirements` 三处对不上:结构是 day-major(第N晚 → 各户)而原型要 order-major(每户 → 逐晚);每户每晚只有 `roomCategory` + `roomCount`,**酒店在上游归一的「段视图」那步就被丢掉了**;另缺定制师姓名、真实房型名、需求级配房备注。 --- ## 二、变更接口清单 | # | 接口 | 方法 | 路径 | 变更类型 | 说明 | |---|------|------|------|----------|------| | 1 | 团期子订单订房记录 | GET | `/v3/admin/order/group-batch/{groupBatchId}/requirement/hotel-households` | 新增 | 一次返回该团期下所有需订房子订单的逐晚填报明细 | --- ## 三、接口详情 ### 1. 团期子订单订房记录 `GET /v3/admin/order/group-batch/{groupBatchId}/requirement/hotel-households` **VO**: `Result` #### 使用场景 团期详情「查看需求」页「用房」板块的下半部分:逐户展示定制师填报了什么,团期管理员据此审核或打回。与上半部分的汇总表并列展示,两者各调各的接口。 #### 入参 | 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | |------|------|------|------|------|------| | groupBatchId | Path | Long | ✅ | 团期 ID | 团期不存在返回 589500 | #### 出参 | 字段 | 类型 | 说明 | |------|------|------| | groupBatchId | String | 团期 ID(字符串序列化防 JS 精度丢失) | | departDate | String(yyyy-MM-dd) | 团期出发日;未定时为 `null`,此时各晚 `stayDate` 也全为 `null` | | householdCount | Integer | 本列表户数(= 需订房户数,与汇总的 `hotelNeededOrderCount` 同口径) | | countedHouseholdCount | Integer | 其中计入汇总间数的户数;小于 `householdCount` 时,差值就是正被打回的户 | | households[] | List | 子订单订房记录,按 `orderNo` 升序 | | households[].orderId | String | 子订单 ID | | households[].orderNo | String | 子订单团号 | | households[].customerName | String | 主联系人姓名 | | households[].participantCount | Integer | 出行人数(成人+儿童+小童+婴儿,走订单域单源公式) | | households[].consultantId | String | 定制师 ID;未指派为 `null` | | households[].consultantName | String | 定制师姓名;未指派为 `null` | | households[].status | String | 需求状态码(`PENDING_REVIEW` / `PENDING` / `PROCESSING` / `DONE` / `REJECTED_TO_CONSULTANT` / `REJECTED_TO_ADMIN`);该户尚未提交需求时为 `null` | | households[].statusName | String | 状态中文名;状态为空或未知值时为 `null` | | households[].countedInSummary | Boolean | **该户是否计入了汇总间数**。四个计入态为 `true`,两个打回态为 `false`,未提交需求为 `false` | | households[].remark | String | 配房需求自由文本(如「希望安排有窗房间」);未填为 `null` | | households[].specialTags | List\ | 特殊需求标签(`house_special_demand` 字典,如 连通房 / 同层相邻);未填为 `[]` | | households[].returnRemark | String | 打回原因;未被打回过为 `null` | | households[].returnedAt | String(yyyy-MM-dd HH:mm:ss) | 打回时间;未被打回过为 `null` | | households[].days[] | List | 逐晚明细,按 `dayNumber` 升序;未提交需求时为 `[]` | | households[].days[].dayNumber | Integer | 第几晚(从 1 起) | | households[].days[].stayDate | String(yyyy-MM-dd) | 住宿日期 = 出发日 + dayNumber − 1;团期无出发日为 `null` | | households[].days[].customerSelfBooked | Boolean | 该晚是否客户自订。为 `true` 时 `hotels` 为空数组且不计入汇总间数,**但该晚仍会列出** | | households[].days[].hotels[] | List | 该晚的住宿(一般 1 家;「分住」时多家;自订晚为 `[]`) | | households[].days[].hotels[].hotelId | String | 酒店 ID;该段没有候选或未填酒店时为 `null`(间数照常不丢) | | households[].days[].hotels[].hotelName | String | 酒店名:优先取需求里落库的快照,快照缺的按 ID 批量补;资源服务不可用时为 `null` | | households[].days[].hotels[].city | String | 酒店所在**地级市**中文名(如「呼伦贝尔市」),按 hotelId 反查资源库;未维护或资源服务不可用时为 `null` | | households[].days[].hotels[].district | String | 酒店所在**区县**中文名(如「海拉尔区」「额尔古纳市」)。**页面上「海拉尔 · 海堂酒店」这一列请用本字段**,不要用 `city`——TEST 实测全库酒店的 `city` 几乎清一色「呼伦贝尔市」(额尔古纳 14 家、海拉尔 7 家、满洲里 6 家全在同一个 city 下),拿它渲染同一团期每晚都显示同一个地级市,区分不出地点 | | households[].days[].hotels[].totalRoomCount | Integer | 该晚该酒店合计间数 | | households[].days[].hotels[].rooms[] | List | 该晚该酒店的房型行(定制师填的原貌,不做合并) | | households[].days[].hotels[].rooms[].roomTypeId | String | 房型 ID;未填为 `null` | | households[].days[].hotels[].rooms[].roomTypeName | String | 房型名称,定制师填的真实房型;未填为 `null` | | households[].days[].hotels[].rooms[].roomCategory | String | 房型大类编码(`room_category` 字典);缺失时为「未知」 | | households[].days[].hotels[].rooms[].roomCategoryName | String | 房型大类中文名;字典无此编码或不可用时为 `null` | | households[].days[].hotels[].rooms[].roomCount | Integer | 间数 | #### 请求示例 ```http GET /v3/admin/order/group-batch/2101506167098511362/requirement/hotel-households Authorization: Bearer ``` #### 响应示例 ```json { "code": 200, "message": "成功", "data": { "groupBatchId": "2101506167098511362", "departDate": "2026-09-27", "householdCount": 2, "countedHouseholdCount": 2, "households": [ { "orderId": "2101506167043985410", "orderNo": "HL20260920105808925", "customerName": "张三", "participantCount": 6, "consultantId": "2083457702519873537", "consultantName": "金卫", "status": "PENDING_REVIEW", "statusName": "待审核", "countedInSummary": true, "remark": "", "specialTags": [], "returnRemark": null, "returnedAt": null, "days": [ { "dayNumber": 1, "stayDate": "2026-09-27", "customerSelfBooked": false, "hotels": [ { "hotelId": "2023714929877450753", "hotelName": "呼伦贝尔香格里拉大酒店", "city": "呼伦贝尔市", "district": "满洲里市", "totalRoomCount": 2, "rooms": [ { "roomTypeId": "3002000000000000013", "roomTypeName": "大床房", "roomCategory": "KING", "roomCategoryName": "豪华大床", "roomCount": 1 }, { "roomTypeId": "3002000000000000013", "roomTypeName": "大床房", "roomCategory": "KING", "roomCategoryName": "豪华大床", "roomCount": 1 } ] } ] }, { "dayNumber": 2, "stayDate": "2026-09-28", "customerSelfBooked": false, "hotels": [ { "hotelId": "3001000000000000005", "hotelName": "额尔古纳白桦大酒店", "city": "呼伦贝尔市", "district": "额尔古纳市", "totalRoomCount": 2, "rooms": [ { "roomTypeId": "3002000000000000005", "roomTypeName": "主题标间", "roomCategory": "STANDARD", "roomCategoryName": "标间", "roomCount": 1 }, { "roomTypeId": "3002000000000000005", "roomTypeName": "主题标间", "roomCategory": "KING", "roomCategoryName": "豪华大床", "roomCount": 1 } ] } ] } ] }, { "orderId": "2101507276118626306", "orderNo": "HL20260920110233355", "customerName": "王五", "participantCount": 2, "consultantId": "2083457702519873537", "consultantName": "金卫", "status": "PENDING_REVIEW", "statusName": "待审核", "countedInSummary": true, "remark": null, "specialTags": [], "returnRemark": null, "returnedAt": null, "days": [ { "dayNumber": 1, "stayDate": "2026-09-27", "customerSelfBooked": false, "hotels": [ { "hotelId": "3001000000000000005", "hotelName": "额尔古纳白桦大酒店", "city": "呼伦贝尔市", "district": "额尔古纳市", "totalRoomCount": 1, "rooms": [ { "roomTypeId": "3002000000000000005", "roomTypeName": "主题标间", "roomCategory": "STANDARD", "roomCategoryName": "标间", "roomCount": 1 } ] } ] } ] } ] } } ``` > 上面是 TEST 环境 2026-09-20 的真实读数(团期 `jw测试1期`),为控制篇幅只保留了每户前 1–2 晚。 #### 空数据 / 降级响应 团期下没有需订房子订单时: ```json { "code": 200, "message": "成功", "data": { "groupBatchId": "2101506167098511362", "departDate": "2026-09-27", "householdCount": 0, "countedHouseholdCount": 0, "households": [] } } ``` 资源服务不可用时**不报错**:`hotelName` 回落到需求里的快照名(快照也没有才为 `null`)、`city` 与 `district` 为 `null`,间数与酒店 ID 照常返回。字典不可用时 `roomCategoryName` 为 `null`,`roomCategory` 编码与间数照常。某户 days JSON 损坏时该户 `days` 为 `[]`,**其它户不受影响**。 #### 错误响应 ```json { "code": 589507, "message": "无操作权限(当前角色未授予团期权限,或该团期不在您名下)", "data": null } ``` | code | 触发条件 | |---|---| | 589507 | 当前角色没有 `group-batch:view` | | 589500 | 团期不存在 | | 401 | 未登录(网关拦截) | #### 业务边界 - **汇总 == Σ子订单(硬约束)**:`requirement-summary` 的 `dailyRoomBreakdown[].rooms[].totalRoomCount` 逐天等于本接口 `countedInSummary=true` 各户该晚各酒店 `rooms[].roomCount` 之和。两处用同一个状态判据(`RequirementStatus.isSummaryCounted`)和同一个 days 归一器,单测有「明细聚合回去必等于汇总视角」的不变量守护。 - **列表比汇总宽**:被打回(`REJECTED_TO_CONSULTANT` / `REJECTED_TO_ADMIN`)的户仍出现在 `households[]`,`countedInSummary=false`。这是刻意的——汇总排除打回态是因为作废草稿的间数会让「全团要几间」随一份正在改的需求浮动(#7925);而明细若也排除,管理员一打回那户就从页面消失,没法跟进。 - **打回户的 `status` / `returnRemark` / `days` 照常返回**:打回是*原地把需求行改成 `REJECTED_*` 并失活*,该户此时**没有任何 active 行**。本端点对这种户回退到最新一版历史记录取展示信息,所以前端能拿到「被谁打回、什么原因、原先填的是哪几晚」。而 `countedInSummary` 仍只认 active 行,与汇总严格同源。 - **打回户与从未提交户可区分**:前者 `status=REJECTED_*` 且 `days` 非空,后者 `status=null` 且 `days` 为 `[]`。 - **哪些户进列表**:`needsHotel=true` 的户 ∪ 已提交有效需求的户(#8023 同口径)。标记为需订房但一份需求都没提交的户**也会列出**,`status` 为 `null`、`days` 为 `[]`——这正是管理员要催的人。 - **只取首方案**:一段有多个候选酒店时只取 `candidates[0]`,与汇总同口径(候选是房控择一,累加全部会翻倍)。 - **分住**:同一晚拆多段住多家时 `hotels[]` 出多条;多段指向同一家酒店会合并成一条。 - **排序**:`households[]` 按 `orderNo` 升序(`orderNo` 为空的排最后);`days[]` 按 `dayNumber` 升序。 - **上限**:单次最多返回 500 户,超出截断并打 warn。团期正常几十户,超过说明数据异常。 - **性能**:房型中文名与酒店名/城市都对整份结果**一次性批量**补齐,不在逐户逐晚的循环里打字典或 Feign。 --- ## 四、契约约束与正确调用方式 - 本接口与 `requirement-summary` **并列调用**,不要用其中一个推算另一个:汇总不含逐户信息,本接口不含车辆/特殊需求全景。 - 前端渲染两块时,若发现数字对不上,先看 `householdCount` 与 `countedHouseholdCount` 的差值——差值就是被打回的户数,属预期行为而非 bug。 - `orderId` / `consultantId` / `groupBatchId` / `hotelId` 均为字符串序列化,**不要**当数字解析。 - 打回动作仍走既有端点(`POST /v3/admin/order/{id}/hotel-requirement/reject` 单户、`POST .../group-batch/{id}/requirement/reject` 批量),本次未改。 --- ## 六、边界行为 | 场景 | 行为 | |---|---| | 户被打回 | 仍在列表里,`countedInSummary=false`,带出 `status=REJECTED_*` / `returnRemark` / `returnedAt`,以及原先填的逐晚明细 | | 户被打回后重新提交 | 回到 `countedInSummary=true`,打回痕迹清空(TEST 实测) | | 户标记需订房但未提交需求 | 仍在列表里,`status` 为 null、`days` 为 `[]`、`countedInSummary=false` | | 户 needsHotel=false 但已提交有效需求 | 在列表里(#8023 口径),`countedInSummary=true` | | 户既没标记也没有效需求 | 不进列表 | | 客户自订晚 | 该晚仍列出,`customerSelfBooked=true`、`hotels` 为 `[]` | | 同一晚分住多家 | `hotels[]` 出多条,间数不丢 | | 同一晚多段同一家 | 合并成一条 | | 段未填酒店 | `hotelId` 为 `null` 的一条,间数不丢 | | 团期无出发日 | 所有 `stayDate` 为 `null`,不推算 | | 资源服务不可用 | `city` / `district` 为 `null`,`hotelName` 回落快照名 | | 字典不可用 | `roomCategoryName` 为 `null`,编码与间数照常 | | 某户 days JSON 损坏 | 该户 `days` 为 `[]`,其它户不受影响 | | 团期下无在团子订单 | `households` 为 `[]`,不发起需求与资源服务调用 | --- ## 六.7、影响评估 - **是否破坏向后兼容**: 纯新增端点,无任何既有接口改动。 - **前端是否必须同步上线**: 否。不接入则「用房」板块保持现状(只有汇总表)。 - **回滚**: 回滚本 PR 即可,只读接口、无数据变更、无表结构变更。 --- ## 七、不影响范围 - `GET .../requirement-summary`(汇总)**一字未改**,既有字段与数值逐字不变。 - `room-plans` / `allocations`(房务订房计划与分房结果)未动。 - 住宿需求提交契约未动(**没有**往 days JSON 加 city 字段,城市一律由 hotelId 反查)。 - 未改 hl-resource-service。 - 打回 / 审核 / 确认相关端点未动。 - 无数据库表结构变更,无 Flyway 迁移。 --- ## 八、测试环境已验证 TEST 环境(`hl-order-service-v3`,dev-v3,2026-09-20),团期 `jw测试1期`(`2101506167098511362`,出发 2026-09-27,6 晚,3 个子订单 / 2 户需订房)。 ``` AC-1 单户多晚多酒店:张三 6 晚跨 3 家酒店,逐晚 hotels[] 与 rooms[] 正确 ✓ AC-2 多户 + orderNo 升序 + 联系人/人数/定制师(金卫) 与订单表一致 ✓ AC-3 交叉对账(程序化,非目测): 逐日总数 Day1-6 汇总 3/3/3/3/3/3 == Σ子订单 3/3/3/3/3/3 ✓ 逐日×酒店×大类 三级全等 ✓ AC-4 打回王五后四件事同时成立: ① 仍在列表里;② status=REJECTED_TO_CONSULTANT / statusName=驳回; ③ returnRemark 与 returnedAt 带出,原填 6 晚明细仍可见; ④ 汇总 Day1-6 由 3 降为 2,且降后 AC-3 等式仍全等 ✓ 打回后重新提交 → countedInSummary 回到 true,打回痕迹清空 ✓ AC-5 客户自订晚:第5晚仍列出、customerSelfBooked=true、hotels=[], 汇总该日由 3 降为 2 ✓ AC-6 未填酒店的段:hotelId=null 一条,间数不丢,不变量仍成立 ✓ AC-7 酒店名:快照优先;快照缺名的按 ID 批量补出正确名称 ✓ AC-8 stayDate = 出发日 + dayNumber − 1,跨月 09-30 → 10-01 正确 ✓ AC-9 房型:roomTypeName(大床房/主题标间)与 roomCategoryName(豪华大床/ 标间)均正确译出,单测显式打桩避免空洞断言 ✓ AC-10 判权:低权限角色 1002/user → 589507 被拦;超管 200 ✓ AC-11 批量加载:整份结果字典与资源服务各只调一次,去重后仅传 3 个 hotelId ✓ ``` 地域字段实测:`city` 全部是「呼伦贝尔市」,`district` 分别是「满洲里市 / 额尔古纳市 / 海拉尔区」——**页面地点那一列必须用 `district`**。 单测:`GroupBatchHotelHouseholdServiceTest` 19/19、`HotelRequirementDaysNormalizerTest` 76/76(含「明细聚合回去必等于汇总视角」不变量)、相关回归 194/194。order-v3 全量 A 组 8334 用例 / B 组 3995 用例,3 个失败均非本次引入并逐个做过基线对照(`FinanceFeignContextMockInventoryTest` 基线同样红、`GroupBatchAuditVersionCasMySqlIT` 并发 flaky 单跑 10/10 绿、`MapperBoundaryArchTest` 已知基线红 #7989)。 --- ## 十、相关文档 - 工单 [#8046](https://git.1814.love:8443/wx/HL/issues/8046) - [#8030](https://git.1814.love:8443/wx/HL/issues/8030) 用房汇总(本接口的姊妹接口,同一板块的上半部分) - [#8023](https://git.1814.love:8443/wx/HL/issues/8023) 需订房户判据(本接口沿用同一套 `countsTowardHotel` 口径) - [#7925](https://git.1814.love:8443/wx/HL/issues/7925) 汇总计入状态集合(本次收口为 `RequirementStatus.isSummaryCounted` 单源) - [#7316](https://git.1814.love:8443/wx/HL/issues/7316) `requirement-summary` 判权接 `group-batch:view`(本接口沿用) - [#7324](https://git.1814.love:8443/wx/HL/issues/7324) 「团期看板要看全部版本而不是 active 行」——打回后无 active 行这一条的出处 --- ## 关联 / 联系人 ### 链接 - **Issue**: [#8046](https://git.1814.love:8443/wx/HL/issues/8046) - **PR**: [#8055](https://git.1814.love:8443/wx/HL/pulls/8055)、[#8057](https://git.1814.love:8443/wx/HL/pulls/8057)、[#8059](https://git.1814.love:8443/wx/HL/pulls/8059)、[#8060](https://git.1814.love:8443/wx/HL/pulls/8060) ### 联系人 - **后端负责人**: @jw - **前端负责人**: @mmg