文件
hl-api-changelog/changelogs-v2/2026-09/13_7326_团期房务分房H9-H11与团期配房明细H12-新增接口-管理后台.md
T
2026-09-14 11:11:19 +08:00

58 KiB
原始文件 Blame 文件历史

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 7326 团期房务「分房」页三端点(H9 总览 / H10 人工微调 / H11 重算)+ 团期详情「配房明细」只读区块(H12);days[].balanced 统一为纯算术口径 admin wx(GIT) 新增接口 deployed verified verified mmg ad34c8ab 2026-09-14 部署面只有 hl-order-service-v3 一个服务(本分支改动文件全在该模块 + hl-common 零命中),未改 hl-common-*,无消费方需连带滚;无 Flyway、无表变更。网关零改动:/v3/admin/** 已由 #3264 通配,四个端点均返回业务层响应而非路由未命中。⚠️ 覆盖范围写在脸上:测试服网关实测的是 hl-order-service-v3 @ e9855bf21(2026-09-13 15:58 部署),其后的 4df361bf6(days[].balanced 两端公式统一为纯算术)只有单测覆盖(GroupBatchRoomAllocationManagerTest 46/0/0/0 + 三个 ArchTest 合计 58/0/0/0),未再经网关实测——本篇「关键变化 2」描述的就是这一次改动,前端按新口径接即可,但它在测试服上的实证要等下一次部署。 另有 5fd39de3b(三处契约描述与实现对不上的订正:H9 日级 balanced 的 @ApiModelProperty 漏改、H11 根级 balanced 写「无人工冲突」而实际是 noStale&&noBlocked、808131 文案与 BedType label 四个词错三个),**这三处改的正是前端在 Swagger 上读到的定义**,同样只有单测覆盖(63/0/0/0)。⚠️ 网关实测那一轮的两处前置是 SQL 直更达成的(把团期推到 SETTLED 取 808600、直接插 order_hotel_requirement v2 造过时基线),所以「团期推进到 SETTLED」与「定制师改需求→管理员确认→房务收到新基线」这两条业务链路本轮没验过,只验了闸门读到该状态会拒。⚠️ 未实测(盲区):808612 未认领团、808091 房务组长写口、589507 的 Feign 降级支、planStatus 的 PENDING/NONE 两档、manualConflicts[]/outOfRange[]/skippedDays[] 三类不平项(全程恒空)、travelerRoomGroups[](order_traveler.room_group_no 全 NULL)、hotel_ready 由 false 置 true 的方向、并发与锁。以上各项的契约按源码写,不按实测写。【前端 hl-admin】【前端 hl-admin】全量闭环(ref ad34c8ab,2026-09-14):批1 H12 配房明细只读块(RoomPlansTab+getGroupBatchRoomPlans)+批2 分房页 H9/H10/H11(group-batch-room-plan.js 三端点+AllocationSection/AdjustModal/RebuildModal+BoardDetailModal 分房 Tab);根级 balanced 与日级 days[].balanced 分开渲染,六类不平项一条不吞,808643 引导重算,床型/roomGroupNo/身份键守红线;两批各 checkpoint 13 项全绿。 2026-09-13 dev-v3

团期房务「分房」页(H9·H10·H11)与团期详情「配房明细」只读区块(H12)

服务: hl-order-service-v3 PR: 分支 feature/7326-room-allocation(合并后回填 PR 号) Issue: #7326 日期: 2026-09-13 影响范围: 管理后台房务团期看板新增「分房」页(H9 读 + H10 人工微调 + H11 重算),团期详情新增「配房明细」只读区块(H12)


⚠️ 关键变化

四个端点全是新增,无存量调用方。但有五处「按直觉写会写错」的地方,逐条列在下面。

1. 🔴 H9 的 stayDate 格式错走的是全局绑定兜底,返回体 code=400,不是 100001

按 100001 写的分支永远命不中。实际观察到的是:

{
  "code": 400,
  "message": "参数【stayDate】格式不正确(日期请用 yyyy-MM-dd,日期时间请用 yyyy-MM-dd'T'HH:mm:ss)",
  "data": null,
  "success": false
}

两点必须说清:

  • HTTP 状态码仍是 200。OrderGlobalExceptionHandler#handleBind 标了 @ResponseStatus(HttpStatus.OK),400 是响应体 code 字段的值,不是 HTTP status。axios 那种「非 2xx 才进 catch」的拦截器不会被触发,必须按 code 判。
  • 括号里的格式提示不是恒有的。只有当被拒的值里含形似日期的数字串(匹配 \d{4}-\d{1,2}-\d{1,2})时才拼上;传 ?stayDate=abc 或 ?stayDate=2026/10/01 拿到的是没有括号的 参数【stayDate】格式不正确。别对 message 做全等匹配或前缀截断,直接整串展示。

参数位置本身是对的:stayDate 是 Query 参数(GET 用 VO 绑定,@Valid 且无 @RequestBody),?stayDate=2026-10-01 生效且只返回该日。不是那类「文档写 Body 实际 Query、传了像没传」的静默丢弃。

2. 🔴 days[].balanced 现在只答「够不够」一个问题——别再当「这天完全 OK」用

2026-09-13 把 H9 与 H11 两端的日级公式统一成纯算术:

端点 旧公式(本次改掉) 新公式(两端逐字相同)
H9 days[].balanced leftover 全 0 && 逐户平 && 无过时户 leftover 全 0 && 逐户 shortage/surplus 全 0
H10 / H11 days[].balanced 逐户平 && leftover 空 && 无越界户 leftover 全 0 && 逐户 shortage/surplus 全 0

改的原因:两端各多了一项对方没有的非算术量,在真实数据上会对同一天给出相反结论(越界户那一支 H9=true / H11=false;过时户那一支 H9=false / H11=true)。日级字段吃全团判定还会让「6-13 的事实」污染「6-12 的结论」——房务被告知 6-12 分得不对,而问题在 6-13。

🔴 根级 balanced 与 hotelReady 一个字没动:它们仍然一票否决过时户与阻塞户。所以**「这一天算平了」和「这个团好了」现在是两个独立的量**,days[].balanced 全 true + 根级 balanced=false 是正常且常见的一组值,不是数据错乱。

「我想知道 X,该看哪个字段」对照表

我想知道 看哪个字段 出现在
这一天房够不够(订房没剩、每户每房型都配齐) days[].balanced H9 / H10 / H11
这一天订房订实了没有(计划行确认到哪一步) days[].planStatus:NONE / PENDING / CONFIRMED / MIXED H9 / H10 / H11 / H12
这一户的分房还是不是按它当前已确认的需求分的 households[].stale(户级)、plans[].allocations[].stale(行级,取值同该户) H9;H12 是 allocations[].stale
全团有几户过时 staleOrderCount(H9 根级)、staleOrderIdsBefore[](只有 H11 会填,是本次重算开始前的名单;H10 恒为空数组) H9 / H11
有没有房务自己解不了的阻塞户、该找谁 blockedHouseholds[](根级,含 reason / owner / message) H9
这一天有哪些户改期改出了团期区间 days[].outOfRange[](含 reason / dayNumber) H10 / H11
哪条计划行还有房没分出去 days[].plans[].leftoverRooms(H9,可以为负数 = 超分)、days[].leftover[](H10 / H11,只在 > 0 时出现) H9 / H10 / H11
哪一户还欠房 households[].shortage[](H9)、days[].shortage[](H10 / H11) H9 / H10 / H11
哪一户被多分了 households[].surplus[](H9) H9
人工分房行跟新需求打架了 days[].manualConflicts[](含 roomCategory) H10 / H11
哪几天这次没被重算 skippedDays[](reason=PLAN_NOT_CONFIRMED) H11
整团配房算不算完成 hotelReady(根级) H9 / H10 / H11 / H12
这个团 / 这一次操作是不是全好了(含过时与阻塞) balanced(根级) H9 / H10 / H11

⚠️ balanced=true + planStatus=MIXED + hotelReady=false 是一组相容的值:房够了、但库存还没扣完(有计划行仍是 PENDING),整团完成标志按「逐日全覆盖 + 全 CONFIRMED」判,所以仍为 false。别把这组值渲染成矛盾态。

3. 四个端点三套门,GET 也拦

角色 H9 GET /allocations H10 / H11 两个写口 H12 GET /room-plans
ROOM_MANAGER(房务管理员) 放行 放行 看是否持 group-batch:view
SUPER_ADMIN(超管) 放行 放行 放行
house_keeper_lead(房务组长) 放行(只读监督角色) 808091 看是否持 group-batch:view
其它后台角色(定制师 / 运营 / 客服 / 财务…) 808090 808090 看是否持 group-batch:view,无则 589507

再叠一层团期归属门(H9 / H10 / H11 都有,H12 没有):

  • 组长与超管可读任意团;普通房务只能操作本人认领的团——团未被认领 808612、被别人认领 808613。
  • 超管写口免归属校验(人离职 / 团转手的唯一处置口)。

🔴 前端两个要点:组长看得到「分房」页、点不了保存与重算(点了拿 808091,文案可直接展示);非房务角色连 H9 都调不到,拿到的是 808090 业务码(HTTP 仍 200),不是空数据——别渲染成「暂无分房」。

4. 🔴 不平项一条都不许吞:拿到 200 不等于「都安排好了」

H10 / H11 的响应里有六个列表会带出「操作成功、但有事实要知道」:days[].leftover[]、days[].shortage[]、days[].manualConflicts[]、days[].outOfRange[]、skippedDays[]、根级 warnings[]。

后端的不变量是「缺口一律显式返回、不静默兜底」。只弹一个「保存成功」而不展开这六个列表,等于把不变量作废——房务会以为都安排好了,实际有户没房住。

5. 错误码 message 里的雪花 ID 不再带千分位

本单修掉了「MessageFormat 把 Long 渲染成 70,001」的问题(涉及 808606 / 808641 / 808642 / 808640 / 808643 等带 ID 占位符的码)。前端如果做过「把 message 里的逗号去掉」的兼容,现在可以不做了;但别反过来依赖「一定没有逗号」——这些 message 一律整串展示,不要解析。


一、背景

#7324 落了房务团期看板与整团按日订房计划 CRUD(H1–H6),#7325 落了按日 / 整团确认与自动分房(H7 / H8 / 预检)。到这一步,系统能算出「哪一天订几间、谁该住几间」,但房务看不到分房结果、改不了分房、需求变了之后没法重算。

本单补齐这三件事(H9 / H10 / H11),并给团期管理员一个不含金额的只读明细(H12)——团期详情原型里的「逐日住宿表」此前读的是前端写死数据、按「一户一间」渲染,与真实分房结果不符,本次起以 H12 为准重做。


二、变更接口清单

# 接口 方法 路径 变更类型 说明
1 团期分房总览(只读) GET /v3/admin/house/group-batches/{groupBatchId}/allocations 新增 逐日 × 计划行 × 各户,含对平差额、来源与过时标记
2 人工微调分房 POST /v3/admin/house/group-batches/{groupBatchId}/allocations 新增 按计划行全量覆盖其下人工行,随后重跑该日自动分房
3 重算分房 POST /v3/admin/house/group-batches/{groupBatchId}/allocations/rebuild 新增 按新基线重跑自动分房,人工行默认保留
4 团期配房明细(只读) GET /v3/admin/order/group-batch/{groupBatchId}/room-plans 新增 团期管理员侧订房层 + 分房层,不含金额

三、接口详情

1. 团期分房总览(只读) GET /v3/admin/house/group-batches/{groupBatchId}/allocations

VO: GroupBatchRoomAllocationOverviewReqVO → Result<GroupBatchRoomAllocationOverviewRespVO>

使用场景

房务团期看板 →「分房」页首屏与每次操作后的刷新。零副作用,可随意轮询。页面上的三块内容全部来自本接口:逐日的计划行 × 分房行表、逐户对平表(欠分 / 多分)、以及顶部的阻塞户提示条。

日期筛选器切换某一天时带 stayDate 再请求一次即可,不必前端切片——带了就只返回该日。

入参

字段 位置 类型 必填 约束 说明
groupBatchId Path Long(JSON 里是字符串数字) ✅ - 团期主订单 ID
stayDate Query String ❌ yyyy-MM-dd 只看某一入住日;不传 = 全部「有计划行或有需求」的日

出参 Result<GroupBatchRoomAllocationOverviewRespVO>

字段 类型 说明
groupBatchId String 团期主订单 ID
batchNo String 团期号
requirementConfirmed Boolean 团期需求是否已整体确认
hotelReady Boolean 团期配房完成标志
balanced Boolean 根级:返回范围内所有日都对平、且无过时户、无阻塞户
staleOrderCount Integer 过时户数(去重,全团口径,不受 stayDate 筛选影响)
days[] 数组 逐日明细,按入住日升序
days[].stayDate String 入住日 yyyy-MM-dd
days[].planStatus String 该日计划行状态汇总:NONE / PENDING / CONFIRMED / MIXED
days[].balanced Boolean 日级纯算术:该日全部计划行 leftoverRooms=0 且全部户 shortage/surplus 均空
days[].plannedRooms / allocatedRooms / demandRooms Integer 该日订房总间数 / 已分总间数 / 已确认需求总间数
days[].plans[] 数组 该日计划行,按 planId 升序
days[].plans[].planId String 计划行 ID
days[].plans[].hotelId / hotelName String / String 酒店 ID 与名称快照
days[].plans[].roomTypeId / roomTypeName / roomCategory String / String / String 房型 ID / 名称快照 / 房型大类
days[].plans[].planStatus String 行状态:PENDING / CONFIRMED
days[].plans[].plannedRooms / allocatedRooms Integer 订房间数 / 已分间数
days[].plans[].leftoverRooms Integer 剩余 = 订房 − 已分,负数表示超分
days[].plans[].allocations[] 数组 该计划行下的分房行,按 (orderId, roomGroupNo) 升序
days[].plans[].allocations[].allocId String 分房行 ID
days[].plans[].allocations[].orderId / orderNo String / String 分给哪一户
days[].plans[].allocations[].roomCount Integer 占几间
days[].plans[].allocations[].allocSource String 来源:AUTO / MANUAL
days[].plans[].allocations[].roomGroupNo String 家庭分组号 F{N},可空
days[].plans[].allocations[].travelerCount Integer 该房入住人数,可空
days[].plans[].allocations[].bedType / bedTypeLabel String / String 床型 code / 中文名,可空
days[].plans[].allocations[].remark String 备注,可空
days[].plans[].allocations[].confirmedRequirementId String 本行依据的已确认需求版本 ID,可空
days[].plans[].allocations[].stale Boolean 所属户的分房是否过时(户级判定,同户各行同值)
days[].households[] 数组 该日各户对平,按 orderId 升序(含一条分房行都没有的户)
days[].households[].orderId / orderNo String / String 子订单
days[].households[].demandState String CONFIRMED / PENDING_REVIEW / NONE / OUT_OF_RANGE
days[].households[].stale Boolean 该户分房是否过时
days[].households[].currentConfirmedRequirementId String 该户当前已确认需求版本 ID,可空
days[].households[].demand[] 数组 {roomCategory, rooms} 已确认需求(按房型大类)
days[].households[].allocated[] 数组 {roomCategory, rooms} 已分(按所属计划行的房型大类汇总)
days[].households[].shortage[] 数组 {roomCategory, rooms} 欠分(需求 − 已分,逐房型;只列缺口)
days[].households[].surplus[] 数组 {roomCategory, rooms} 多分(已分 − 需求,逐房型;只列溢出)
days[].households[].travelerRoomGroups[] 数组 {roomGroupNo, travelerCount} 出行人侧家庭分组,供 H10 的 roomGroupNo 下拉取值
blockedHouseholds[] 数组 根级:阻塞整团配房完成、且房务无法自行解除的户
blockedHouseholds[].orderId / orderNo / customerName String / String / String 户
blockedHouseholds[].reason String NO_BASELINE / REJECTED_NOT_RESUBMITTED / STAY_DATE_OUT_OF_RANGE
blockedHouseholds[].owner String 责任人角色:CUSTOMIZER / GROUP_BATCH_ADMIN
blockedHouseholds[].message String 面向房务的说明:卡在哪、该找谁(可直接展示)

请求示例

GET /v3/admin/house/group-batches/1936482073991827457/allocations?stayDate=2026-06-12 HTTP/1.1
Authorization: Bearer {token}

响应示例

{
  "code": 200,
  "message": "成功",
  "success": true,
  "data": {
    "groupBatchId": "1936482073991827457",
    "batchNo": "GB20260612001",
    "requirementConfirmed": true,
    "hotelReady": false,
    "balanced": false,
    "staleOrderCount": 1,
    "days": [
      {
        "stayDate": "2026-06-12",
        "planStatus": "CONFIRMED",
        "balanced": true,
        "plannedRooms": 3,
        "allocatedRooms": 3,
        "demandRooms": 3,
        "plans": [
          {
            "planId": "1936482074001827460",
            "hotelId": "1901234567890123456",
            "hotelName": "香格里拉大酒店",
            "roomTypeId": "1901234567890123999",
            "roomTypeName": "高级双床房",
            "roomCategory": "STANDARD",
            "planStatus": "CONFIRMED",
            "plannedRooms": 3,
            "allocatedRooms": 3,
            "leftoverRooms": 0,
            "allocations": [
              {
                "allocId": "1936482074011827470",
                "orderId": "1936482070001827001",
                "orderNo": "HL2026061200001",
                "roomCount": 2,
                "allocSource": "MANUAL",
                "roomGroupNo": "F1",
                "travelerCount": 3,
                "bedType": "twin",
                "bedTypeLabel": "双床",
                "remark": "老人住低楼层",
                "confirmedRequirementId": "2099031373212775178",
                "stale": true
              },
              {
                "allocId": "1936482074011827471",
                "orderId": "1936482070001827002",
                "orderNo": "HL2026061200002",
                "roomCount": 1,
                "allocSource": "AUTO",
                "roomGroupNo": "F1",
                "travelerCount": 2,
                "bedType": null,
                "bedTypeLabel": null,
                "remark": null,
                "confirmedRequirementId": "2099031373212775180",
                "stale": false
              }
            ]
          }
        ],
        "households": [
          {
            "orderId": "1936482070001827001",
            "orderNo": "HL2026061200001",
            "demandState": "CONFIRMED",
            "stale": true,
            "currentConfirmedRequirementId": "2099031373212775179",
            "demand": [{ "roomCategory": "STANDARD", "rooms": 2 }],
            "allocated": [{ "roomCategory": "STANDARD", "rooms": 2 }],
            "shortage": [],
            "surplus": [],
            "travelerRoomGroups": [{ "roomGroupNo": "F1", "travelerCount": 3 }]
          },
          {
            "orderId": "1936482070001827002",
            "orderNo": "HL2026061200002",
            "demandState": "CONFIRMED",
            "stale": false,
            "currentConfirmedRequirementId": "2099031373212775180",
            "demand": [{ "roomCategory": "STANDARD", "rooms": 1 }],
            "allocated": [{ "roomCategory": "STANDARD", "rooms": 1 }],
            "shortage": [],
            "surplus": [],
            "travelerRoomGroups": []
          }
        ]
      }
    ],
    "blockedHouseholds": [
      {
        "orderId": "1936482070001827003",
        "orderNo": "HL2026061200003",
        "customerName": "张三",
        "reason": "NO_BASELINE",
        "owner": "CUSTOMIZER",
        "message": "该户尚未提交住宿需求,请联系定制师提交"
      }
    ]
  }
}

上例正是「days[0].balanced=true 而根级 balanced=false」的典型形态:这一天算术上分平了,但全团有 1 户过时、1 户无基线。

空数据 / 降级响应

团期存在但一条计划行、一条需求都没有时,days 为空数组,balanced 按「无阻塞无过时」取 true,不 404 也不 500:

{
  "code": 200,
  "message": "成功",
  "success": true,
  "data": {
    "groupBatchId": "1936482073991827457",
    "batchNo": "GB20260612001",
    "requirementConfirmed": false,
    "hotelReady": false,
    "balanced": true,
    "staleOrderCount": 0,
    "days": [],
    "blockedHouseholds": []
  }
}

错误响应

{
  "code": 808090,
  "message": "未登录或非房务角色,无权操作",
  "data": null,
  "success": false
}

stayDate 格式错的形态见「关键变化 1」(code=400,不是 100001)。

业务边界

  • 零副作用:不写任何表,可随意刷新 / 轮询。
  • 角色门:ROOM_MANAGER / SUPER_ADMIN / house_keeper_lead 放行,其余 808090。组长能读、不能写。
  • 归属门:组长与超管可读任意团;普通房务只能读本人认领的团(未认领 808612 / 他人认领 808613)。
  • stayDate 只影响 days[]:根级 staleOrderCount、blockedHouseholds[]、hotelReady 一律是全团口径,不随筛选变。
  • stale 是户级判定:同一户在同一天的每一条分房行 stale 取值相同;行上的 confirmedRequirementId 照常返回,需要逐行标红自己比。
  • 越界户的需求计入对平分母(不是剔除):它在区间内的那些晚照常参与 demand / shortage 计算,同时进 blockedHouseholds[] 一票否决 hotelReady。

2. 人工微调分房 POST /v3/admin/house/group-batches/{groupBatchId}/allocations

VO: GroupBatchRoomAllocationSaveReqVO → Result<GroupBatchRoomAllocationRebuildRespVO>

使用场景

「分房」页上房务手工指定「谁住哪条计划行、占几间、算哪个家庭分组、什么床型」。保存按钮提交本接口。

覆盖粒度是「本次提交里出现过的 planId」,不是整团:items 里没出现的计划行一行都不动。所以前端可以按计划行(或按天)分批保存,不必每次回传全团。

「恢复自动分房」按钮走 clearPlanIds,不是提交一个空 items——覆盖语义下空 items 无法区分「这次没有人工行要提交」和「把这条计划行的人工行清空」。

roomGroupNo 的下拉取值用 H9 同日该户的 households[].travelerRoomGroups[]。

入参

字段 位置 类型 必填 约束 说明
groupBatchId Path Long ✅ - 团期主订单 ID
items Body 数组 ❌ @Size(max=500);与 clearPlanIds 至少一个非空 覆盖后的人工分房行集合
items[].planId Body Long ✅ 须属本团、未软删、已确认 订房计划行 ID
items[].orderId Body Long ✅ 须为本团在团子订单 分给哪一户
items[].roomCount Body Integer ✅ @Min(1) @Max(99) 该家庭分组占几间
items[].roomGroupNo Body String ❌ ^F[1-9][0-9]{0,2}$;不填按 F1 家庭分组号;(planId, orderId, roomGroupNo) 是身份键
items[].travelerCount Body Integer ❌ @Min(1) @Max(9) 该房入住人数
items[].bedType Body String ❌ single / double / twin / family 床型 code,非法落 808131
items[].remark Body String ❌ @Size(max=256) 备注
clearPlanIds Body 数组(Long) ❌ @Size(max=200);与 items 至少一个非空 这些计划行下的人工分房全部软删,回到纯自动分房

⚠️ clearPlanIds[] 实现是 List<Long>(早期文档写成 String 数组)。实测传 JSON 数字可用,Jackson 对数字字符串也能收进 Long ⇒ 两种写法都不会被静默丢弃,属口径不准不是缺陷。建议与其它雪花 ID 一样统一传字符串,避免 JS 大整数精度问题。

⚠️ 同一个 planId 不得同时出现在 items 与 clearPlanIds 里(两条指令相反),违反落 100001,message 点名是哪条计划行。

出参 Result<GroupBatchRoomAllocationRebuildRespVO>

与 H11 共用同一个返回结构(字段表见下一节)。H10 的五点固定差别:

字段 类型 说明
force Boolean H10 恒 false
days[] 数组 只含受影响日(由本次提交的 planId 反查出的入住日),不是整团
days[].manualReset Integer H10 恒 0(H10 不重置人工行,只覆盖)
staleOrderIdsBefore[] 数组 H10 恒为空数组(只有 H11 会填)——受影响户一旦过时,H10 在写入前就已经按 808643 整单拒了
skippedDays[] 数组 H10 恒为空数组:计划行未确认在 H10 是 808640 直接拒,不是「跳过」

请求示例

{
  "items": [
    {
      "planId": "1936482074001827460",
      "orderId": "1936482070001827001",
      "roomCount": 2,
      "roomGroupNo": "F1",
      "travelerCount": 3,
      "bedType": "twin",
      "remark": "老人住低楼层"
    }
  ],
  "clearPlanIds": ["1936482074001827461"]
}

响应示例

{
  "code": 200,
  "message": "成功",
  "success": true,
  "data": {
    "groupBatchId": "1936482073991827457",
    "force": false,
    "balanced": true,
    "hotelReady": true,
    "days": [
      {
        "stayDate": "2026-06-12",
        "planStatus": "CONFIRMED",
        "balanced": true,
        "autoInserted": 1,
        "autoUpdated": 0,
        "autoDeleted": 1,
        "manualKept": 1,
        "manualReset": 0,
        "leftover": [],
        "shortage": [],
        "manualConflicts": [],
        "outOfRange": []
      }
    ],
    "skippedDays": [],
    "staleOrderIdsBefore": [],
    "replacedAllocIds": ["1936482074011827472"],
    "warnings": []
  }
}

空数据 / 降级响应

本接口不存在「空数据」形态:items 与 clearPlanIds 同时为空直接落 100001(不会返回一个空结果让人以为保存成功)。只清空人工行(只传 clearPlanIds)时,days[] 仍会带出被影响日重算后的完整结果。

{
  "code": 100001,
  "message": "items 与 clearPlanIds 不能同时为空",
  "data": null,
  "success": false
}

错误响应

{
  "code": 808643,
  "message": "该团有 2 户的分房已过时(首个订单 1936482070001827001),请先重算分房后再确认",
  "data": null,
  "success": false
}

🔴 808643 的处置是引导用户先点「重算」(H11,force=false),不是让他重试保存——重试多少次都还是 808643。

业务边界

  • 写门 + 归属门:@HouseWriteGuarded 在 @Idempotent / @Lock4j 之前执行(组长 808091 / 其它角色 808090),随后阶段闸门 808600、归属门 808612 / 808613。
  • 校验顺序固定,任一失败整单拒绝、零写入:参数 100001 → 团期存在 589500 → 阶段 808600 → 归属 808612/808613 → 计划行存在且属本团 808601 → 已确认 808640 → 户在团 808642 → 过时闸 808643 → 数量守卫 808606 / 808641。
  • 幂等窗口 3 秒,键只取 groupBatchId:同一个团 3 秒内的**第二次提交(哪怕 body 不同)**会拿到 100502。整单覆盖本就是整团一把,前端连点保存要做防抖。
  • 团期级分布式锁 house:gb:room:{groupBatchId},租约 60 秒;与按日确认(H7)互斥。
  • 写入行 allocSource=MANUAL;覆盖后对受影响日重跑自动分房,自动行按差异 insert / update / 软删。
  • 后置联动:分平的户置「配房完成」、不平的户回退;整团按「逐日全覆盖 + 全 CONFIRMED + 无阻塞」判 hotelReady;团级时间线记一条 BATCH_ROOM_ALLOC_MANUAL。

3. 重算分房 POST /v3/admin/house/group-batches/{groupBatchId}/allocations/rebuild

VO: GroupBatchRoomAllocationRebuildReqVO → Result<GroupBatchRoomAllocationRebuildRespVO>

使用场景

团期管理员重新确认了住宿需求之后,房务在「分房」页点「重算」,按新基线重跑自动分房。H9 的 staleOrderCount > 0 或 H10 撞到 808643 时,都要引导用户来点这个按钮。

两档差别很大,前端要分开做:

  • 默认档 force=false:人工分房行业务列零改动、零删除(只刷新它记录的需求版本号),与新需求冲突的人工行保留并进 manualConflicts[]。
  • force=true:把范围内人工行整片软删重来,不可逆,因此 reason 必填,且会写进团级时间线。前端必须做二次确认弹窗 + 原因输入框。

入参

字段 位置 类型 必填 约束 说明
groupBatchId Path Long ✅ - 团期主订单 ID
force Body Boolean ❌ 默认 false true = 先软删范围内全部人工分房行
stayDate Body String ❌ yyyy-MM-dd 只重算某一入住日;不传 = 整团全部已确认日
reason Body String force=true 时必填 @Size(max=256) 重置原因,写进团级时间线

⚠️ reason 的条件必填不在 Bean Validation 上,由编排层判并抛 100001(message:force=true 时 reason 必填)。

出参 Result<GroupBatchRoomAllocationRebuildRespVO>

字段 类型 说明
groupBatchId String 团期主订单 ID
force Boolean 回显本次是否重置了人工分房
balanced Boolean 根级:本次范围内所有日都对平、且无过时户、无阻塞户
hotelReady Boolean 后置判定之后的团期配房完成标志
days[] 数组 本次处理过的入住日,按日期升序
days[].stayDate String 入住日
days[].planStatus String 该日计划行状态汇总:NONE / PENDING / CONFIRMED / MIXED(与 H9 同取值域、同算法)
days[].balanced Boolean 日级纯算术,与 H9 同公式;与 planStatus 正交
days[].autoInserted / autoUpdated / autoDeleted Integer 自动分房行本次新插 / 原地改写 / 软删的行数
days[].manualKept / manualReset Integer 保留的人工行数 / 被 force 软删的人工行数
days[].leftover[] 数组 {planId, hotelName, roomTypeName, roomCategory, rooms} 计划行还有几间没分出去(只在 > 0 时出现)
days[].shortage[] 数组 {orderId, orderNo, roomCategory, rooms} 某户某房型还欠几间
days[].manualConflicts[] 数组 {allocId, orderId, orderNo, roomCategory, manualRooms, demandRooms} 人工行占额超过该户当前基线需求(force=false 时行未动)
days[].outOfRange[] 数组 {orderId, orderNo, departDate, dayNumber, reason} 该户改期后这一天落在团期区间外;reason = OUT_OF_RANGE / DATE_MISSING;dayNumber 按该户自己的出发日算(出发日 = 第 1 天),出发日缺失时为 null
skippedDays[] 数组 {stayDate, reason} 未处理的日,reason=PLAN_NOT_CONFIRMED
staleOrderIdsBefore[] String 数组 本次开始前判定为分房过时的户(结束后已全部回填为当前基线)
replacedAllocIds[] String 数组 本次被软删的分房行 ID(自动 + 人工),供核单排查
warnings[] 数组 {code, orderId, message} 操作已成功,但有事实需要知晓;取值见「六.5、枚举 / 数据字典」

⚠️ warnings[]、days[].planStatus、manualConflicts[].roomCategory、outOfRange[].reason 四处是早期契约表里没有的,以本篇为准。

请求示例

{
  "force": true,
  "stayDate": "2026-06-12",
  "reason": "客户临时并房,原人工安排整体作废,按新需求重来"
}

响应示例

{
  "code": 200,
  "message": "成功",
  "success": true,
  "data": {
    "groupBatchId": "1936482073991827457",
    "force": true,
    "balanced": false,
    "hotelReady": false,
    "days": [
      {
        "stayDate": "2026-06-12",
        "planStatus": "MIXED",
        "balanced": false,
        "autoInserted": 2,
        "autoUpdated": 1,
        "autoDeleted": 0,
        "manualKept": 0,
        "manualReset": 2,
        "leftover": [
          {
            "planId": "1936482074001827460",
            "hotelName": "香格里拉大酒店",
            "roomTypeName": "高级双床房",
            "roomCategory": "STANDARD",
            "rooms": 1
          }
        ],
        "shortage": [
          {
            "orderId": "1936482070001827004",
            "orderNo": "HL2026061200004",
            "roomCategory": "FAMILY",
            "rooms": 1
          }
        ],
        "manualConflicts": [],
        "outOfRange": [
          {
            "orderId": "1936482070001827003",
            "orderNo": "HL2026061200003",
            "departDate": "2026-06-08",
            "dayNumber": 5,
            "reason": "OUT_OF_RANGE"
          }
        ]
      }
    ],
    "skippedDays": [
      { "stayDate": "2026-06-13", "reason": "PLAN_NOT_CONFIRMED" }
    ],
    "staleOrderIdsBefore": ["1936482070001827001"],
    "replacedAllocIds": ["1936482074011827470", "1936482074011827473"],
    "warnings": [
      {
        "code": "DAY_OUT_OF_RANGE",
        "orderId": "1936482070001827003",
        "message": "该户有住宿晚落在团期区间之外,需求已计入对平分母"
      }
    ]
  }
}

空数据 / 降级响应

范围内一条已确认计划行都没有时不返回空结果,直接落 808644(见下)——返回空的话房务会以为「重算完了、没事」。只有 PENDING 计划行的日进 skippedDays[],days[] 相应为空数组:

{
  "code": 200,
  "message": "成功",
  "success": true,
  "data": {
    "groupBatchId": "1936482073991827457",
    "force": false,
    "balanced": true,
    "hotelReady": false,
    "days": [],
    "skippedDays": [{ "stayDate": "2026-06-13", "reason": "PLAN_NOT_CONFIRMED" }],
    "staleOrderIdsBefore": [],
    "replacedAllocIds": [],
    "warnings": []
  }
}

错误响应

{
  "code": 808644,
  "message": "该范围内没有已确认的订房计划,请先按日确认订房再重算分房",
  "data": null,
  "success": false
}

业务边界

  • 权限、阶段闸、归属门与 H10 完全一致(808090 / 808091 / 808600 / 808612 / 808613)。
  • 幂等键带上了 stayDate({groupBatchId}:{stayDate},3 秒):所以「连着重算两天」不会撞 100502,而「3 秒内重算同一天两次」会。不传 stayDate 的整团重算,键退化成同一个值。
  • 不扣库存、不还库存:重算只动分房行,订房与库存归 H4–H8。
  • force=false 时人工行业务列零改动(orderId / stayDate / planId / roomCount / roomGroupNo / bedType 全不变、零删除),但会刷新它记录的需求版本号——这是「重算完就不再过时」的实现方式。
  • 不平项显式返回:leftover / shortage / manualConflicts / outOfRange / skippedDays 逐项列出,前端不得只弹「成功」。
  • force=true 的 reason 会进团级时间线(事件 BATCH_ROOM_ALLOC_REBUILD),是事后唯一能回答「谁凭什么清了我的人工安排」的记录。

4. 团期配房明细(只读) GET /v3/admin/order/group-batch/{groupBatchId}/room-plans

VO: GroupBatchRoomPlanDetailRespVO

使用场景

团期详情页「配房明细」只读区块:团期管理员看整团订了哪些房、分给了谁。纯读、不含任何金额。

与房务侧的 /v3/admin/house/group-batches/{id}/room-plans 不是同一个东西:那边是写口(录订房、改删、按日确认)走房务角色门;这边是只读、走 group-batch:view 平台权限码,且结构上就没有金额字段。

原型里的「逐日住宿表」按「一户一间」渲染的是前端写死数据,与真实分房结果不符,请按本接口字段表重做。

入参

字段 位置 类型 必填 约束 说明
groupBatchId Path Long ✅ - 团期主订单 ID

无查询参数、无请求体。

出参 Result<GroupBatchRoomPlanDetailRespVO>

字段 类型 说明
groupBatchId / batchNo String / String 团期
departDate / endDate String / String 团期出发日 / 结束日 yyyy-MM-dd,可空
hotelReady Boolean 团期配房完成标志
progress String 订房进度:NOT_STARTED / PARTIAL / COMPLETE
allocationState String 分房状态:NONE / PARTIAL / BALANCED / UNBALANCED
days[] 数组 逐日明细,按入住日升序
days[].stayDate String 入住日
days[].dayNumber Integer 团期口径第 N 天 = 入住日 − 团期出发日 + 1;团期出发日为空时为 null
days[].planStatus String 该日计划行状态汇总:PENDING / CONFIRMED / MIXED
days[].plannedRooms / allocatedRooms Integer 该日订房总间数 / 已分总间数
days[].plans[] 数组 订房层
days[].plans[].planId String 计划行 ID
days[].plans[].hotelName / roomTypeName / roomCategory String 酒店名 / 房型名快照、房型大类
days[].plans[].plannedRooms Integer 订房间数
days[].plans[].planStatus String 行状态:PENDING / CONFIRMED
days[].plans[].replaceReason String 同日替换原因,可空
days[].plans[].allocations[] 数组 分房层,按 (orderId, roomGroupNo) 升序
days[].plans[].allocations[].orderId / orderNo String / String 分给哪一户
days[].plans[].allocations[].roomCount Integer 占几间
days[].plans[].allocations[].allocSource String AUTO / MANUAL
days[].plans[].allocations[].roomGroupNo / travelerCount String / Integer 家庭分组号 / 入住人数,可空
days[].plans[].allocations[].bedType / bedTypeLabel String / String 床型 code / 中文名,可空
days[].plans[].allocations[].stale Boolean 该户分房依据的需求版本是否已过时

🔒 结构上不存在的字段(不是「不填」,是没有):protoPrice、settlementPrice、settleType、allocId、confirmedRequirementId,以及出行人姓名与联系方式。户名请按 orderId 关联既有的配房逐户明细取。

请求示例

GET /v3/admin/order/group-batch/1936482073991827457/room-plans HTTP/1.1
Authorization: Bearer {token}

响应示例

{
  "code": 200,
  "message": "成功",
  "success": true,
  "data": {
    "groupBatchId": "1936482073991827457",
    "batchNo": "GB20260612001",
    "departDate": "2026-06-11",
    "endDate": "2026-06-15",
    "hotelReady": false,
    "progress": "PARTIAL",
    "allocationState": "UNBALANCED",
    "days": [
      {
        "stayDate": "2026-06-12",
        "dayNumber": 2,
        "planStatus": "CONFIRMED",
        "plannedRooms": 3,
        "allocatedRooms": 3,
        "plans": [
          {
            "planId": "1936482074001827460",
            "hotelName": "香格里拉大酒店",
            "roomTypeName": "高级双床房",
            "roomCategory": "STANDARD",
            "plannedRooms": 3,
            "planStatus": "CONFIRMED",
            "replaceReason": null,
            "allocations": [
              {
                "orderId": "1936482070001827001",
                "orderNo": "HL2026061200001",
                "roomCount": 2,
                "allocSource": "MANUAL",
                "roomGroupNo": "F1",
                "travelerCount": 3,
                "bedType": "twin",
                "bedTypeLabel": "双床",
                "stale": true
              }
            ]
          }
        ]
      }
    ]
  }
}

空数据 / 降级响应

该团一条计划行都没有时,days 为空数组、progress=NOT_STARTED、allocationState=NONE,不 404:

{
  "code": 200,
  "message": "成功",
  "success": true,
  "data": {
    "groupBatchId": "1936482073991827457",
    "batchNo": "GB20260612001",
    "departDate": "2026-06-11",
    "endDate": "2026-06-15",
    "hotelReady": false,
    "progress": "NOT_STARTED",
    "allocationState": "NONE",
    "days": []
  }
}

错误响应

{
  "code": 589507,
  "message": "无操作权限(非团期管理员 / 非本定制师名下)",
  "data": null,
  "success": false
}

业务边界

  • 权限判定排在团期存在性校验之前:无权限的人拿到的一律是 589507,探不出某个团期存不存在(反过来会成为越权探测口)。
  • 权限码是 group-batch:view(复用,未新增权限码);判权走的是网关透传的当前角色,不是 adminId。
  • 权限查询失败按「关闭」处理:底层 Feign 异常降级为「无权限」,返回 589507 而不是放行。
  • 零写入,@Transactional(readOnly = true)。
  • 无归属门:持 group-batch:view 即可看任意团期的配房明细(与房务侧的「只能看自己认领的团」不同)。
  • days[].dayNumber 是团期口径(按团期出发日算);H11 outOfRange[].dayNumber 是该户自己的行程口径(按该户出发日算)。两者同名不同基准,不要混用。

四、契约约束与正确调用方式

本节只写后端接受 / 拒绝 payload 的规则,不写 UI 渲染建议。

✅ 正确 / ❌ 错误 payload 对照(H10)

场景 payload
✅ 只提交人工行 { "items": [{ "planId": "701", "orderId": "801", "roomCount": 1 }], "clearPlanIds": [] }
✅ 只恢复自动分房 { "items": [], "clearPlanIds": ["702"] }
✅ 一次提交里两件事都做(不同的 planId) { "items": [{ "planId": "701", ... }], "clearPlanIds": ["702"] }
✅ 同户两个家庭分组各占一间 items: [{planId:"701", orderId:"801", roomGroupNo:"F1", roomCount:1}, {planId:"701", orderId:"801", roomGroupNo:"F2", roomCount:1}]
❌ 两个集合都空 { "items": [], "clearPlanIds": [] } → 100001
❌ 同一个 planId 同时出现在两边 { "items": [{ "planId": "701", ... }], "clearPlanIds": ["701"] } → 100001
❌ 同户同计划行提交两条都不填分组 不填按 F1 归一 ⇒ 身份键撞车 → 100001
❌ 用空 items 表达「清空这条计划行」 计划行不在 items 里就一行都不动,不是清空

✅ 正确 / ❌ 错误 payload 对照(H11)

场景 payload
✅ 默认重算整团 {} 或 { "force": false }
✅ 只重算一天 { "stayDate": "2026-06-12" }
✅ 强制重置并说明原因 { "force": true, "reason": "客户临时并房,原安排作废" }
❌ 强制重置不给原因 { "force": true } → 100001(force=true 时 reason 必填)

调用顺序

  1. 进页面 → H9(读)。
  2. staleOrderCount > 0 → 先 H11(force=false)再做别的;直接 H10 会被 808643 拒。
  3. 手工调整 → H10 → 用它返回的 days[] 就地刷新,或重新拉 H9。
  4. 团期管理员侧看结果 → H12。

错误码总表

码 触发 出现在 前端处置
400(响应体 code) stayDate 格式非法(全局绑定兜底) H9 整串展示 message;HTTP 仍 200
100001 参数非法:两集合同时为空 / 同一 planId 两边都有 / 身份三元组重复 / force=true 缺 reason / @Valid 失败 H10 / H11 整串展示 message(点名到具体计划行)
100502 3 秒幂等窗内重复提交 H10 / H11 提示「处理中,请勿重复提交」,按钮防抖
589500 团期不存在 四个端点 返回列表页
589507 无 group-batch:view 权限(含降级) H12 隐藏「配房明细」区块
808090 未登录或非房务角色 H9 / H10 / H11 别渲染成空数据
808091 房务组长为只读监督角色 H10 / H11 组长侧直接隐藏写按钮
808131 床型非法(single/double/twin/family 之外) H10 表单侧用固定下拉,不让用户自由输入
808600 团期当前阶段不允许(message 含当前阶段) H10 / H11 整串展示;写按钮按阶段禁用
808601 计划行不存在 / 不属本团 / 已软删 H10 刷新 H9 重来
808606 某计划行人工分房合计超出订房间数(message 含计划行 ID 与两个间数) H10 整串展示
808612 该团期尚未被房务认领 H9 / H10 / H11 引导去团期抢单池认领
808613 该团期由其他房务认领 H9 / H10 / H11 只读展示或引导联系认领人
808640 计划行尚未确认 H10 引导先做按日确认订房
808641 超该户该日该房型已确认需求(message 含 roomCategory 与两个间数) H10 整串展示
808642 订单不是该团期的在团子订单 H10 刷新 H9 重取户列表
808643 该团有 N 户分房已过时 H10 🔴 引导先点「重算」(H11),重试保存无效
808644 范围内没有已确认的订房计划 H11 引导先做按日确认订房

⚠️ 实测覆盖范围:上表 808090 / 808600 / 808601 / 808606 / 808640 / 808641 / 808642 / 808643 / 808644 / 100001 / 100502 / 589500 / 589507 / 400 已在测试服经网关实测;808612(没造未认领团)、808091(库里没有房务组长可登录账号)、589507 的降级支(只验了「角色确实没这个权限码」这一支)按源码写、未实测。


五、数据库行为

只写外部可观察的行为,不涉及表结构细节。本单无表变更、无 Flyway。

前端动作 可观察结果
H9 任意调用 零写入
H12 任意调用 零写入
H10 提交 items 出现在 items 里的计划行,其下人工分房行被整体替换(未出现的计划行一行不动);随后该日自动分房行按差异新插 / 原地改写 / 软删
H10 提交 clearPlanIds 这些计划行下的人工分房行全部软删,该日回到纯自动分房
H11 force=false 自动分房行按新基线差异重算;人工行业务内容不变、仅刷新它记录的需求版本号
H11 force=true 范围内人工分房行全部软删后重来,被删的 ID 出现在 replacedAllocIds[],reason 进团级时间线
H10 / H11 结束时 分平的户被置「配房完成」,不平的户从「已完成」退回;团期 hotelReady 按「逐日全覆盖 + 全 CONFIRMED + 无阻塞户」置位或清零
H10 / H11 结束时 团级时间线各记一条:BATCH_ROOM_ALLOC_MANUAL / BATCH_ROOM_ALLOC_REBUILD

失败零写入:H10 / H11 的全部校验在同一个事务内,任一条不过整单回滚,不存在「改了一半」。

不动库存:分房层的任何操作都不扣、不还酒店库存——库存只在按日确认(H7 / H8)与计划行删除(H6)时变动。


六、边界行为

  • 未登录 → 401(网关拦截)。
  • 团期不存在 → 589500(业务码,HTTP 200),不是 404。
  • 角色 / 权限不足 → 808090 / 808091 / 589507(业务码,HTTP 200),不是空数据。
  • stayDate 格式错 → 响应体 code=400,HTTP 200。
  • 团期一条计划行都没有 → H9 days=[]、H12 days=[] + progress=NOT_STARTED + allocationState=NONE,不异常。
  • 团期 departDate 为空 → H12 的 days[].dayNumber 为 null(算不出第几天),其余字段照常返回。
  • 户没有出行人分组数据 → H9 travelerRoomGroups[] 为空数组,前端的 roomGroupNo 下拉退化为手填 F1。
  • 并发:同一团期的 H7 / H10 / H11 走同一把团期锁,后到的请求等待而不是报错;超出锁租约才失败。

六.5、枚举 / 数据字典

每个枚举单独一节,值以后端常量为准。

计划行状态 planStatus(行级)

所属字段: H9 days[].plans[].planStatus、H12 days[].plans[].planStatus | 类型: String

值 中文 说明
PENDING 未确认 已录入但还没按日确认,库存未扣
CONFIRMED 已确认 已按日确认,库存已扣

日聚合状态 planStatus(日级)

所属字段: H9 / H11 / H12 的 days[].planStatus | 类型: String

值 中文 说明
NONE 无计划行 该日一条 active 计划行都没有
PENDING 全未确认 该日计划行全是 PENDING
CONFIRMED 全已确认 该日计划行全是 CONFIRMED
MIXED 混合 两种都有(例如已确认的日又新补了一行)

分房来源 allocSource

所属字段: H9 / H12 的 allocations[].allocSource | 类型: String

值 中文 说明
AUTO 自动分房 算法生成,会被重算改写 / 软删
MANUAL 人工分房 H10 写入,重算默认保留(force=true 才重置)

户需求状态 demandState

所属字段: H9 days[].households[].demandState | 类型: String

四态互斥且有优先级:越界 > 有基线(再分「另有新版待审」与「就是当前版」)> 无基线。

值 中文 说明
OUT_OF_RANGE 改期越界 该户有住宿晚落在团期区间外;需求仍计入对平分母,同时阻塞 hotelReady
PENDING_REVIEW 有新版待确认 该户当前 active 需求还没被管理员确认,对平仍按上一个已确认版本
CONFIRMED 正常 按当前已确认版本对平
NONE 无需求 该户该日没有已确认需求

阻塞原因 blockedHouseholds[].reason 与责任人 owner

所属字段: H9 根级 blockedHouseholds[] | 类型: String

reason owner 含义 / 解锁出口
NO_BASELINE CUSTOMIZER 该户尚未提交住宿需求 → 找定制师提交
NO_BASELINE GROUP_BATCH_ADMIN 该户需求有新版待团期管理员确认 → 找管理员确认
REJECTED_NOT_RESUBMITTED CUSTOMIZER 需求被打回后没重新提交 → 找定制师重提
STAY_DATE_OUT_OF_RANGE GROUP_BATCH_ADMIN 该户改期改出团期区间 → 找管理员改期回区间或置为不需配房

⚠️ 三个解锁出口全不在房务手上,所以这三个端点都不提供「强制完成」入口。message 字段已写好面向房务的完整说明,直接展示即可。

越界原因 outOfRange[].reason

所属字段: H10 / H11 days[].outOfRange[] | 类型: String

值 中文 说明
OUT_OF_RANGE 落在区间外 该户有出发日,但这一晚算出来不在团期区间内
DATE_MISSING 出发日缺失 该户 departDate 为空,算不出第几天,dayNumber 为 null

跳过原因 skippedDays[].reason

所属字段: H11 skippedDays[] | 类型: String

值 中文 说明
PLAN_NOT_CONFIRMED 计划行未确认 该日还有未确认的订房计划,本次不重算

告警码 warnings[].code

所属字段: H10 / H11 根级 warnings[] | 类型: String

这两个端点只会产出下面两个码(同一个告警结构在按日确认 H7 上还有另外四个码,那是 #7325 的场景,本单两个端点不产出):

值 中文 说明
BASELINE_INACTIVE 基线未生效 该户有新版需求待管理员确认,本次未把它置为配房完成
DAY_OUT_OF_RANGE 有晚越界 该户有住宿晚落在团期区间外,需求已计入分母,整团完成标志置不上

床型 bedType / bedTypeLabel

所属字段: H10 入参 items[].bedType;H9 / H12 出参 allocations[].bedType + bedTypeLabel | 类型: String

bedType bedTypeLabel
single 单人床
double 大床
twin 双床
family 家庭房

⚠️ 传这四个之外的值落 808131。

🔴 2026-09-13 本篇发布前订正:本段初稿写「808131 的 message 用的是另一套旧词(应为单床/双床/双床房/家庭房)、别拿它做选项」——那个错误文案已在 5fd39de3b 修掉,现在 message 是「床型取值非法(应为单人床/大床/双床/家庭房)」,与上表 bedTypeLabel 一致。

但下拉仍然请按 bedTypeLabel 渲染,不要解析错误文案——理由变了:不是因为它现在错,而是错误 message 本来就不是契约,它随时可能再被改写,而 bedTypeLabel 是接口出参、改它要走 changelog。

订房进度 progress 与分房状态 allocationState(H12)

所属字段: H12 根级 | 类型: String

progress 含义
NOT_STARTED 一条计划行都没有
PARTIAL 有计划行,但还没覆盖全部服务日、或还有行没确认
COMPLETE 每个服务日都有计划行且全部已确认
allocationState 含义
NONE 一条分房行都没有
PARTIAL 有分房行,但订房尚未全部确认(「平不平」还没有定论)
BALANCED 订房全确认、每条计划行都分完、且无过时户
UNBALANCED 订房全确认,但有计划行没分完 / 超分,或存在过时户

房型大类 roomCategory

所属字段: 四个端点的 roomCategory(计划行快照与逐房型对平项) | 类型: String

取值随订房计划行携带(由 #7324 落,单源在资源侧房型字典,例如 STANDARD),本单不新增取值、不做翻译——直接展示后端返回的字符串。


七、不影响范围

  • 仅影响:管理后台房务团期看板的「分房」页(H9 / H10 / H11)与团期详情的「配房明细」只读区块(H12)。
  • 零影响:#7324 的团期看板与订房计划 CRUD(H1–H6)、#7325 的按日 / 整团确认与预检(H7 / H8 / H8c)、逐户(非团单)房务配房、团期需求提交与审核、团期抢单池、核单与结算——本单只新增端点,未改任何既有端点的入参、出参或错误码。
  • 网关零改动:/v3/admin/** 已通配,四个端点无需新增路由。
  • 无表变更、无 Flyway、无权限码新增(H12 复用 group-batch:view)。
  • 小程序端零影响。
  • 一处内部行为变化,前端看不见:团期基线变更事件不再触发旧的「差量重配」链路(它会绕过本单的分房口径),改由房务显式点 H11 重算。

八、测试环境已验证

  • 部署面:只有 hl-order-service-v3 一个服务。本分支改动文件全部落在该模块,对 hl-common / hl-gateway / 其它服务命中数 = 0,无消费方需连带滚。
  • 网关实测:2026-09-13 15:58 部署 hl-order-service-v3 @ e9855bf21 后,经网关实测四个端点各一次(H9 / H12 的 GET、H10 / H11 的 POST),另实测 H10 的 808643 与 H11 的 force=true。四个端点均返回业务层响应而非路由未命中,证明既有 /v3/admin/** 通配已覆盖,网关零改动成立。
  • 单测:GroupBatchRoomAllocationManagerTest 46 / 0 / 0 / 0;连同 HouseModuleBoundaryArchTest(5)、HouseRoomAllocationStaleSingleSourceArchTest(3)、GroupBatchRoomLifecycleDesignTest(4) 合计 58 / 0 / 0 / 0 全绿。
  • 🔴 本篇「关键变化 2」那次改动(days[].balanced 两端公式统一)在上述部署之后才提交,只有单测覆盖,未再经网关实测。契约按源码写,测试服上的实证等下一次部署。
  • 🔴 网关实测那一轮的两处前置是 SQL 直更达成的,不是走业务链路:① 把团期改成「已结算」取 808600(取完已复原,并配了「复原后同一请求 200」的阳性对照);② 直接插一版新的住宿需求造「分房过时」。⇒ 「团期推进到已结算」与「定制师改需求 → 管理员确认 → 房务收到新基线」这两条业务链路本轮没验过,只验了闸门读到该状态会拒。
  • 🔴 明确未覆盖(盲区):808612(没造未认领团)、808091(库里没有房务组长可登录账号)、589507 的降级支、planStatus 的 PENDING / NONE 两档(只见到 CONFIRMED / MIXED)、manualConflicts[] / outOfRange[] / skippedDays[] 三类不平项(全程恒空,装配逻辑一次都没被执行到)、travelerRoomGroups[](测试数据里出行人分组全为空)、hotelReady 由 false 置 true 的方向、并发与锁。这些字段的契约按源码写,联调时请按本篇字段表准备,不要因为「实测样例里没见过」就当它不会出现。

九、相关历史 PR

  • #7324 房务团期看板与整团按日订房计划 CRUD(H1–H6)——本单的计划行、房型大类、归属门 / 阶段闸来源。
  • #7325 团期房务按日订房确认(H7 / H8 + 只读预检)——本单的自动分房算法、对平判据、告警结构来源。

十、相关文档

  • changelogs-v2/2026-09/10_7324_房务团期看板与整团按日订房计划CRUD-新增接口-管理后台.md
  • changelogs-v2/2026-09/11_7325_团期房务按日订房确认H7-H8-预检-新增接口-管理后台.md
  • 仓内设计稿:docs/group/团期房务实现方案-v1.0.html、docs/group/实施单/05-团期房务.html(H9–H12 行已随本单标注「已实现」)

关联 / 联系人

链接

  • Issue: #7326
  • 分支: feature/7326-room-allocation

联系人

  • 后端负责人: @wx
  • 前端: mmg(hl-ui 管理后台)