24 KiB
schema, ticket, title, consumer, author, change_type, backend_status, gateway_status, frontend_status, frontend_owner, frontend_ref, target_release, verified_at, status_note, updated_at, base
| schema | ticket | title | consumer | author | change_type | backend_status | gateway_status | frontend_status | frontend_owner | frontend_ref | target_release | verified_at | status_note | updated_at | base |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| hl-changelog/v2 | 8046 | 团期用房新增子订单订房记录接口 | admin | jw(GIT) | 新增接口 | deployed | not_required | verified | mmg | 3362a94e2425456276c4906e6ee991716146cdf8 | 2026-09-21 | 团期「查看需求 · 用房」板块是上下两块,此前只有上块(汇总)有接口。本次新增下块: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 例全过。 | 2026-09-20 | dev-v3 |
order-v3: 团期用房新增子订单订房记录接口
服务: hl-order-service-v3 (端口 8086) PR: #8055(主体)、#8057(补 district)、#8059(打回户改看最新版本行)、#8060(AC-11 固化) Issue: #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<GroupHotelHouseholdsRespVO>
使用场景
团期详情「查看需求」页「用房」板块的下半部分:逐户展示定制师填报了什么,团期管理员据此审核或打回。与上半部分的汇总表并列展示,两者各调各的接口。
入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---|---|---|
| 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<String> | 特殊需求标签(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 | 间数 |
请求示例
GET /v3/admin/order/group-batch/2101506167098511362/requirement/hotel-households
Authorization: Bearer <token>
响应示例
{
"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 晚。
空数据 / 降级响应
团期下没有需订房子订单时:
{
"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 为 [],其它户不受影响。
错误响应
{
"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
- #8030 用房汇总(本接口的姊妹接口,同一板块的上半部分)
- #8023 需订房户判据(本接口沿用同一套
countsTowardHotel口径) - #7925 汇总计入状态集合(本次收口为
RequirementStatus.isSummaryCounted单源) - #7316
requirement-summary判权接group-batch:view(本接口沿用) - #7324 「团期看板要看全部版本而不是 active 行」——打回后无 active 行这一条的出处