文件
hl-api-changelog/changelogs-v2/2026-09/20_8046_团期用房新增子订单订房记录接口-新增接口-管理后台.md
2026-09-21 09:49:45 +08:00

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 行这一条的出处

关联 / 联系人

链接

联系人

  • 后端负责人: @jw
  • 前端负责人: @mmg