58 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 | 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-v3PR: 分支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是团期口径(按团期出发日算);H11outOfRange[].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 必填) |
调用顺序
- 进页面 → H9(读)。
staleOrderCount > 0→ 先 H11(force=false)再做别的;直接 H10 会被 808643 拒。- 手工调整 → H10 → 用它返回的
days[]就地刷新,或重新拉 H9。 - 团期管理员侧看结果 → 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=[]、H12days=[]+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/**通配已覆盖,网关零改动成立。 - 单测:
GroupBatchRoomAllocationManagerTest46 / 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-新增接口-管理后台.mdchangelogs-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 管理后台)