changelog-filename-gate / validate (push) Failing after 2s
面向 mmg:房务「分房」页三端点(H9 总览 / H10 人工微调 / H11 重算) 与团期详情「配房明细」只读区块(H12),四个端点全部新增。 覆盖范围写在 frontmatter 的 status_note 里,不藏在正文: - 网关实测的是 e9855bf21;其后的 4df361bf6(days[].balanced 两端统一为纯算术) 与 5fd39de3b(三处契约描述订正)只有单测覆盖; - 网关实测那轮有两处前置是 SQL 直更达成的,对应的两条业务链路本轮没验过; - 八项明确未覆盖的盲区逐条列出,这些项的契约按源码写、不按实测写。 几条前端最容易踩的,正文里逐条点名: 1. stayDate 格式错是 HTTP 200 + 响应体 code=400,不是 HTTP 400 —— axios 那种「非 2xx 才进 catch」的拦截器不会被触发,必须在 then 里判 body.code; 且格式提示括号不是恒有的(被拒值不像日期时只给不带括号的短句),别做 message 全等匹配。 2. days[].balanced 是纯算术口径(该日 leftover 全 0 且逐户 shortage/surplus 全 0), 不含过时户与越界户——那两项只进根级 balanced。配了「我想知道 X 该看哪个字段」对照表。 3. H10 的 items 与 clearPlanIds 不得指向同一 planId(100001);三条上限 500/200/99。 4. 床型下拉按 bedTypeLabel 渲染,不要解析错误 message —— 理由不是它现在错(808131 文案已在 5fd39de3b 修对),而是错误 message 本来就不是契约。 PR 号待合并后回填(分支 feature/7326-room-allocation)。 两道校验器均通过: - validate-changelog-frontmatter.mjs --verbose:4 个端点逐条回显、endpoint-count 4,与正文标题数对上; - validate-changelog-filenames.mjs 的 validateChangelogPath:TARGET errors=[], 阳性对照(日前缀改 12_)正确报 E_DAY,证明该门禁有分辨力。 Refs #7326 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1097 行
57 KiB
Markdown
1097 行
57 KiB
Markdown
---
|
||
schema: "hl-changelog/v2"
|
||
ticket: "7326"
|
||
title: "团期房务「分房」页三端点(H9 总览 / H10 人工微调 / H11 重算)+ 团期详情「配房明细」只读区块(H12);days[].balanced 统一为纯算术口径"
|
||
consumer: "admin"
|
||
author: "wx(GIT)"
|
||
change_type: "新增接口"
|
||
backend_status: "deployed"
|
||
gateway_status: "verified"
|
||
frontend_status: "pending"
|
||
frontend_owner: ""
|
||
frontend_ref: ""
|
||
target_release: ""
|
||
verified_at: ""
|
||
status_note: "部署面只有 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 的方向、并发与锁。以上各项的契约按源码写,不按实测写。"
|
||
updated_at: "2026-09-13"
|
||
base: "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` 写的分支**永远命不中**。实际观察到的是:
|
||
|
||
```json
|
||
{
|
||
"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 | 面向房务的说明:卡在哪、该找谁(可直接展示) |
|
||
|
||
#### 请求示例
|
||
|
||
```http
|
||
GET /v3/admin/house/group-batches/1936482073991827457/allocations?stayDate=2026-06-12 HTTP/1.1
|
||
Authorization: Bearer {token}
|
||
```
|
||
|
||
#### 响应示例
|
||
|
||
```json
|
||
{
|
||
"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**:
|
||
|
||
```json
|
||
{
|
||
"code": 200,
|
||
"message": "成功",
|
||
"success": true,
|
||
"data": {
|
||
"groupBatchId": "1936482073991827457",
|
||
"batchNo": "GB20260612001",
|
||
"requirementConfirmed": false,
|
||
"hotelReady": false,
|
||
"balanced": true,
|
||
"staleOrderCount": 0,
|
||
"days": [],
|
||
"blockedHouseholds": []
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 错误响应
|
||
|
||
```json
|
||
{
|
||
"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 直接拒,不是「跳过」 |
|
||
|
||
#### 请求示例
|
||
|
||
```json
|
||
{
|
||
"items": [
|
||
{
|
||
"planId": "1936482074001827460",
|
||
"orderId": "1936482070001827001",
|
||
"roomCount": 2,
|
||
"roomGroupNo": "F1",
|
||
"travelerCount": 3,
|
||
"bedType": "twin",
|
||
"remark": "老人住低楼层"
|
||
}
|
||
],
|
||
"clearPlanIds": ["1936482074001827461"]
|
||
}
|
||
```
|
||
|
||
#### 响应示例
|
||
|
||
```json
|
||
{
|
||
"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[]` 仍会带出被影响日重算后的完整结果。
|
||
|
||
```json
|
||
{
|
||
"code": 100001,
|
||
"message": "items 与 clearPlanIds 不能同时为空",
|
||
"data": null,
|
||
"success": false
|
||
}
|
||
```
|
||
|
||
#### 错误响应
|
||
|
||
```json
|
||
{
|
||
"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` 四处是**早期契约表里没有的**,以本篇为准。
|
||
|
||
#### 请求示例
|
||
|
||
```json
|
||
{
|
||
"force": true,
|
||
"stayDate": "2026-06-12",
|
||
"reason": "客户临时并房,原人工安排整体作废,按新需求重来"
|
||
}
|
||
```
|
||
|
||
#### 响应示例
|
||
|
||
```json
|
||
{
|
||
"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[]` 相应为空数组:
|
||
|
||
```json
|
||
{
|
||
"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": []
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 错误响应
|
||
|
||
```json
|
||
{
|
||
"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` 关联既有的配房逐户明细取**。
|
||
|
||
#### 请求示例
|
||
|
||
```http
|
||
GET /v3/admin/order/group-batch/1936482073991827457/room-plans HTTP/1.1
|
||
Authorization: Bearer {token}
|
||
```
|
||
|
||
#### 响应示例
|
||
|
||
```json
|
||
{
|
||
"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**:
|
||
|
||
```json
|
||
{
|
||
"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": []
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 错误响应
|
||
|
||
```json
|
||
{
|
||
"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](https://git.1814.love:8443/wx/HL/issues/7326)
|
||
- **分支**: `feature/7326-room-allocation`
|
||
|
||
### 联系人
|
||
|
||
- **后端负责人**: @wx
|
||
- **前端**: mmg(hl-ui 管理后台)
|