比较提交

...
2 次代码提交
作者 SHA1 备注 提交日期
API Changelog Bot和Claude Fable 5.1 c5b40bb83e docs: 答复前端 #7324 后端需求(后端零改动)+ 补 #7322 我的团不过滤 batch_status
changelog-filename-gate / validate (push) Successful in 2s
前端据 H1 看板过滤 CANCELLED 的行为,推断「整团释放」按钮无可达路径。
该推断的关键环节不成立:我的团(GET /v3/admin/order/grab-pool/my-claims/
group-batches)刻意不按 batch_status 过滤,H2 详情也没有阶段闸门,
CANCELLED 团期经「我的团 → H2 详情」完全可达。

根因在我方:#7322 的 changelog 从未写过这条行为,前端看不到 javadoc。
本次一并补记,并在需求单上答复 status=answered-no-backend-change。

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-11 10:39:30 +08:00
API Changelog Bot和Claude Fable 5.1 65d786ea87 docs(changelog): #7325 团期房务按日订房确认 H7/H8/confirm-check(已部署实测 dfd5a8f6f)
3 个新端点、8 个新错误码、808616 删除、808602 日期上限改为 endDate-1。
已部署测试服并经网关实测,Flyway V20260910_302 success=1。

起草时「GET /confirm-check 任何后台角色都能调」写错,已更正:
读端点虽不标 @HouseWriteGuarded,但在编排层入口直接调
HouseReadGuard.assertHouseReadPermission(),非房务角色同样 808090。

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-11 10:37:49 +08:00
共修改 3 个文件,包含 737 行新增和 4 行删除
@@ -5,10 +5,13 @@ consumer: "backend"
requester: "mmg(前端)"
author: "mmg"
target_service: "hl-order-service-v3"
status: "proposed"
status: "answered-no-backend-change"
related_issue: "7324"
related_frontend_commit: "6710ad4a"
created_at: "2026-09-11"
answered_at: "2026-09-11"
answered_by: "Claude(后端管理者)"
backend_verdict: "可达路径已存在,后端零改动;我方 #7322 changelog 漏写「我的团」不过滤 batch_status,前端的判断在当时信息下是合理的"
---
# 需求:放开 H1 看板列表 `batchStatus` 对 `CANCELLED` 的过滤
@@ -75,3 +78,76 @@ created_at: "2026-09-11"
- 后端 PR: [#7465](https://git.1814.love:8443/wx/HL/pulls/7465)
- 前端交付 commit: `6710ad4a`(hl-admin `v2.1`)
- 相关源码: `HouseGroupBatchBoardManager.java`(H1 编排,`:60-64` 默认四态、`:224-236` batchStatus 过滤)、`HouseGroupBatchPlanGate.java`(阶段闸门,无需改)
---
## 🔧 后端答复(2026-09-11)
> **结论:本单不需要后端改动——可达路径已经存在。而你们不知道它存在,是因为我们的 #7322 changelog 漏写了一条关键行为。这个缺口在我方。**
### 一、结论先行
「整团释放按钮没有任何用户可达路径」这一判断**不成立**。已经存在的路径是:
```
GET /v3/admin/order/grab-pool/my-claims/group-batches ← 我的团(#7322 已交付且已验收)
↓ 该列表不过滤 batch_status,已流团(CANCELLED)团期就在里面
↓ 行上直接带 groupBatchId / batchStatus / batchStatusLabel
GET /v3/admin/house/group-batches/{groupBatchId} ← H2 详情(无阶段闸门)
↓ detail.batchStatus === 'CANCELLED' → canReleaseAll 成立
POST /v3/admin/house/group-batches/{groupBatchId}/room-plans/release-all
```
### 二、逐环节证据(五条,全部对源码复核过,不是从文档转述)
| # | 环节 | 证据 |
|---|---|---|
| 1 | **我的团不过滤 `batch_status`** | `HouseGroupGrabService.listMyGroups` javadoc 原话:「**不限制 batch_status**:已认领的团可能已流团 / 已结束,本单不自动释放指针,**房务仍要看得见并手动释放**」。代码 `q.setBatchStatus(req.getBatchStatus())` 原样下推,**不传就是不加这个条件**,没有 H1 那种「过滤空了回退默认四态」的逻辑 |
| 2 | **服务层也不注入状态** | `GroupBatchService.pageHouseClaimed` 直接把 query 下推 mapper,零注入 |
| 3 | **同一文件里的对照(最强证据)** | 紧挨着的 `GroupBatchService.pageHouseGrabPool` 却**强制**写死 `q.setBatchStatuses(REQUIREMENT_CONFIRMABLE_STATUSES)`。一个注入、一个不注入,**是刻意区分不是遗漏** |
| 4 | **我的团的行携带判定所需字段** | `HouseMyGroupItemRespVO extends HouseGroupGrabPoolItemRespVO`,父类有 `groupBatchId`、`batchStatus`、`batchStatusLabel` |
| 5 | **H2 详情没有阶段闸门** | `HouseGroupBatchBoardController.detail` 的门**只有归属**(未认领 808612 / 他人认领 808613),**不判 `batchStatus`**——CANCELLED 团只要是本人认领的就能打开 |
你对 release-all 端点本身的判断是对的:它只对 CANCELLED 放行、其它态 808618,端点自洽无 bug。
### 三、H1 过滤掉 CANCELLED 是刻意设计,且原注释就指明了入口在详情页
`HouseGroupBatchBoardManager.resolveBatchStatuses` 上方注释原话:
> 「为什么已流团的团不许出现在看板:本页是『要干活的团』,流团团的唯一动作是整团释放,**那个入口在详情页而不是列表页**;混在一起会让房务每天都在列表里看到一堆做不了任何事的团。」
注释写明了「详情页」,**但没写明从哪个列表进详情页**——这就是这次误判的接缝所在。
### 四、这次的根因在我方,不在你们
你们的推理链条本身是严密的:读 #7324 changelog → H1 明确写了「传 CANCELLED 被忽略」→ 详情从 H1 点开 → 因此 CANCELLED 详情不可达。**在你们掌握的信息里,这个结论是对的。**
问题出在 **#7322 的 changelog 从头到尾没有提过「我的团」不过滤 `batch_status`**(我逐行搜过那篇,零命中)。这条行为是后端刻意设计并写进 javadoc 的,却**没有进入交接件**——而 javadoc 你们看不到。
**所以这是我方的文档缺口,不是你们的疏忽。** #7322 的 changelog 我会补这一条。
### 五、前端需要做什么(如果需要)
把「我的团」列表行接到 H2 详情弹层——同一个弹层,入参就是行上已有的 `groupBatchId`。两端 API 都已交付并验收过(#7322 changelog `frontend_status: verified`),这是**接线**不是新功能。
如果「我的团」页当前打开的不是 H2 那个弹层,这一步是前端改动;**后端仍然零改动**。
补一条使用上的细节:我的团的 `batchStatus` 入参是**透传**的,所以
- 不传 → 全部已认领团(含 CANCELLED);
- 传 `CANCELLED` → **只看已流团的团**,做一个「待释放」筛选很自然。
这和 H1 的行为正好相反(H1 传 `CANCELLED` 会被丢弃并回退默认四态),**两个列表的 `batchStatus` 语义不同,别套用同一套前端逻辑**。
### 六、留给 wx 的产品判断(这一条不是 bug,是口径)
方案 A(H1 放开 CANCELLED)实现成本确实很低,你对「不破坏既有」的分析也站得住:写口闸门 `PLAN_WRITABLE_STATUSES` 不含 CANCELLED,放开读侧不影响任何写口判定——这点我复核过,属实。
**但我不建议改**,理由是它与「看板 = 要干活的团」这条既有定案直接冲突,而该定案是有成文理由的(见第三节注释)。已流团团期在看板里是纯噪声:房务对它只能做一个动作,而那个动作有专门的入口。
如果 wx 判断「房务更习惯在看板里找」,那我照做——**但那时它是一次产品口径调整,不是修 bug**,会另开工单并同步改注释里的定案说明。这个判断我交给 wx,不自行拍板。
---
**后端本单零改动。#7322 的 changelog 会补「我的团不过滤 batch_status」一条。**
@@ -10,10 +10,10 @@ gateway_status: "not_required"
frontend_status: "verified"
frontend_owner: "mmg"
frontend_ref: "745cd9b8"
target_release: ""
target_release: "hl-ui@745cd9b8"
verified_at: "2026-09-10"
status_note: "本篇覆盖 PR #7436(合并提交 ab4118a01)。2026-09-10 已部署测试服(HEAD 6d41d6148,已验 ab4118a01 为其祖先),Flyway V20260910_301 执行成功,order_group_batch 三列 house_claimer_id/house_claimer_name/house_claimed_at 已落库。32 条验收项中 28 条已取证,含用 5 个真实登录账号过网关实测的归属与接管链路。后台菜单(sys_menu)的两行抢单池入口已由 PR #7467(squash 7f791a250,hl-user-service 的 V20260910_001)补上。⚠️ 部署两条:① #7322 的部署清单原本不含 hl-user-service,本次起必须带上;② 菜单树按 roleId 缓在 Redis(cache:menu:tree:role:{roleId},TTL 3600s),Flyway 直连 DB 不触发失效,部署后需 DEL cache:menu:tree:role:* 否则最长要等一小时。前端已交付(commit 745cd9b8):新增团期抢单池页(抢单池·团期+我的团两 Tab,含超管团级接管)与普通抢单池独立页,orders 页摘除 pool scope,BatchHero 接负责房务三字段;checkpoint 全绿。"
updated_at: "2026-09-10"
updated_at: "2026-09-11"
base: "dev-v3"
---
@@ -392,7 +392,7 @@ Authorization: Bearer <持 SUPER_ADMIN 的管理端 token>
|---|---|---|---|---|---|
| scope | Query | String | 否 | `mine`(默认)/ `all` | 普通房务传 `all` → 808092 |
| keyword | Query | String | 否 | ≤32 字 | `batchNo`/`productName` 模糊 |
| batchStatus | Query | String | 否 | 团期九态任一,非法值 400 | — |
| batchStatus | Query | String | 否 | 团期九态任一,非法值 400 | **透传,不做任何过滤**——含 `CANCELLED`。不传 = 全部已认领团(含已流团)。⚠️ 与 H1 看板语义相反,详见下方补记 |
| needsReconfirm | Query | Boolean | 否 | `true`=只看 `requirement_confirmed=0` 的团 | 待管理员重新确认 |
| departDateFrom / departDateTo | Query | LocalDate | 否 | — | 出发日区间 |
| claimedAtFrom / claimedAtTo | Query | LocalDateTime | 否 | `yyyy-MM-dd'T'HH:mm:ss` | 认领时间区间 |
@@ -415,6 +415,30 @@ Authorization: Bearer <持 SUPER_ADMIN 的管理端 token>
(字段取自 `HouseMyGroupPageRespVO.java`、`HouseMyGroupItemRespVO.java`、`HouseMyGroupStatsVO.java`。)
#### 🔴 补记(2026-09-11):本列表**不按 `batch_status` 过滤**,已流团(CANCELLED)团期就在里面
这条行为原本就是后端的刻意设计,但**本篇首发时漏写了**,在此补上。
- `HouseGroupGrabService.listMyGroups` 的 javadoc 原话:「**不限制 batch_status**:已认领的团可能已流团 / 已结束,本单不自动释放指针,**房务仍要看得见并手动释放**」;
- 服务层 `GroupBatchService.pageHouseClaimed` 把查询条件**原样下推**,零注入;
对照:同一个类里的 `pageHouseGrabPool`(团期抢单池)却**强制**写死 `REQUIREMENT_CONFIRMABLE_STATUSES`。
一个注入、一个不注入,是刻意区分,不是遗漏。
**与 H1 房务团期看板(#7324)的语义正好相反,两个列表不能套用同一套前端逻辑:**
| | 本接口(我的团) | H1 看板 `GET /v3/admin/house/group-batches` |
|---|---|---|
| 不传 `batchStatus` | 全部已认领团,**含 CANCELLED** | 默认四态,**不含 CANCELLED** |
| 传 `batchStatus=CANCELLED` | **只看已流团的团**(可当「待释放」筛选用) | 被**静默丢弃**并回退默认四态 |
**这条为什么要紧**:「整团释放」写口 `POST .../room-plans/release-all` 仅对 `CANCELLED` 团期放行,
而 H2 详情 `GET /v3/admin/house/group-batches/{groupBatchId}` **没有阶段闸门**(只判归属:未认领 808612 / 他人认领 808613)。
所以拿一个 CANCELLED 团的 `groupBatchId` 去打 H2,是能打开的——**本接口就是拿到那个 id 的地方**。
前端 2026-09-11 曾据 H1 的过滤行为推断「整团释放按钮无任何可达路径」并提了后端需求
(`backend-requests/2026-09/11_7324_放开H1看板batchStatus过滤CANCELLED-整团释放可达.md`)。
那个推断在当时掌握的信息下是合理的——**缺的正是本篇漏写的这一条**,不是前端疏忽。
#### 请求示例
```json
@@ -0,0 +1,633 @@
---
schema: "hl-changelog/v2"
ticket: "7325"
title: "团期房务按日订房确认:3 个新端点(H7·H8·confirm-check);8 个新错误码;808616 删除;808602 日期上限改为 endDate-1"
consumer: "admin"
author: "wx(GIT)"
change_type: "新增接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "pending"
frontend_owner: "mmg"
frontend_ref: ""
target_release: ""
verified_at: "2026-09-11"
status_note: "2026-09-11 已部署测试服并经网关实测(squash dfd5a8f6f,PR #7497)。部署面只有 hl-order-service-v3 一个服务(两条独立路径核过:53 个改动文件全在该模块;对 hl-common / hl-gateway / 其它服务的命中数 = 0),未改 hl-common-*,无消费方需连带滚。Flyway V20260910_302 在测试库执行成功(flyway_schema_history 版本 20260910.302 success=1)。⚠️ 本篇起草时写错过一条并已更正:原写「GET /confirm-check 任何后台角色都能调」——不成立。读端点虽然不标 @HouseWriteGuarded、不走拦截器,但在编排层入口直接调 HouseReadGuard.assertHouseReadPermission()(GroupBatchRoomDayConfirmManager.java:489),非房务角色同样 808090。错因是只枚举了「注解+拦截器」一条机制就下了否定结论;2026-09-11 测试服实测(test_admin/CUSTOMIZER 打 GET 得 808090)与源码调用点双向坐实后已在「关键变化 1」更正。网关零改动:hl-gateway 自 2026-09-07 21:53 起未重启(jar mtime 与进程 lstart 两条路径核过),三个新端点均返回业务层响应而非路由未命中,证明既有 /v3/admin/** 通配已覆盖。⚠️ 尚未验证:H7/H8 真实确认业务逻辑未跑通——取证用的团期处于未认领态,POST 先撞 808612,需先造「整团抢单→提交需求→确认基线」数据链才能触达,本轮未做,如实标为未验证而不写「正常」。"
updated_at: "2026-09-11"
base: "dev-v3"
---
# 团期房务按日订房确认(H7·H8 + 只读预检)
> **服务**: `hl-order-service-v3`
> **PR**: #7497
> **Issue**: #7325
> **日期**: 2026-09-11
> **影响范围**: 管理后台房务团期页面新增「按日确认」与「整团确认」功能,及确认前的零副作用预检
---
## ⚠️ 关键变化
**#7325 是 #7324 房务团期看板(H1-H6)的后续交付,新增三个确认端点。三处易漏内容:**
### 1. 三个端点都有角色门,`GET /confirm-check` 也不例外
两条门是**两套不同机制**,别只看注解:
- 两个 POST:注解 `@HouseWriteGuarded` + 拦截器,在 `@Idempotent` 与 `@Lock4j` **之前**执行,非房务角色根本进不了幂等窗口;
- `GET /confirm-check`:**不走拦截器**,但在编排层入口直接调
`HouseReadGuard.assertHouseReadPermission()`(`GroupBatchRoomDayConfirmManager.java:489`)——**一样会拒**。
| 角色 | 两个 POST 写口 | `GET /confirm-check` |
|---|---|---|
| `ROOM_MANAGER`(房务管理员)| 放行 | 放行 |
| `SUPER_ADMIN`(超管) | 放行 | 放行 |
| `house_keeper_lead`(房务组长) | **808091**(只读监督,不可写) | **放行** |
| 其它后台角色(定制师 / 运营 / 客服 / 财务 / 素材管理员) | **808090** | **808090** |
🔴 **前端两个要点:**
1. **房务组长看得到预检、点不了确认。** 按「能看见就能点」渲染,组长点确认会拿到 808091;该文案可直接展示。
2. **非房务角色连预检都调不到**,返回的是 808090(业务码,HTTP 仍 200),**不是空数据**。别把 808090 渲染成「暂无差额」——那会让定制师以为团期没问题。
> ⚠️ **本条曾写错,2026-09-11 更正。** 起草时写的是「GET 任何后台角色都能调(不标 `@HouseWriteGuarded`)」。
> 错因是**只枚举了「注解 + 拦截器」一条机制**就下了否定结论,没查到读门是**编排层直接调用**的第二套机制。
> 测试服实测(`test_admin` / `CUSTOMIZER` 打 GET 得 808090)与源码调用点双向坐实后更正。
> 读写两套门分开的理由写在 `HouseReadGuard` 的 javadoc 里:组长写门必须拒(808091)、读门必须放行,
> 否则组长连看板都打不开,监督无从谈起。
### 2. 808616 已删除,前端可移除兜底分支
`GB_ROOM_PLAN_CONFIRMED_REVISION_UNAVAILABLE`(808616)是 #7324 为「已确认行改删连锁尚未接入」留的占位码。#7325 实现了连锁能力,该码零调用、按 CODE_RULES「0 调用即删」随实现消失。前端若有该码兜底分支可以移除——它不会再返回。
### 3. 808602 日期上限改为 `endDate - 1`(右端开区间)
入住日合法性校验:` stayDate ∈ [departDate, endDate)`。结束日是离店日、不产生间夜,所以 `stayDate == endDate` 判越界。前端日期选择器的可选上限应是 `endDate - 1`。
---
## 一、背景
`#7325` 交付两块能力补齐团期房务确认链路:
1. **预检(零写入)**:`GET /confirm-check` 逐日展示订房间数 vs 已确认需求的差额表、阻塞名单(越界户/无基线户)、整团能否确认的判定。
2. **按日确认**:`POST /days/{stayDate}/confirm` 单日确认订房,触发库存扣减、计划行翻已确认、分房重算、子订单完成标记。
3. **整团确认**:`POST /confirm` 先整团预检(零写入),通过后按日期升序逐日执行,支持幂等重跑(已处理日跳过)。
三个端点都标 `@HouseWriteGuarded` 进行角色门控(H7·H8,#7324 的 H4-H6 已标)。只读预检额外不标该注解,让组长也能看到差额表做监督。
新增错误码 8 个落在 `HouseGroupBatchErrorCode` 的 808604-808610 / 808630 / 808631 / 808643 九段(注:先占号后实现,有占位有真实)。同时删除 808616,更新既有 589560 的参数含义。
---
## 二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|---|---|---|---|---|
| 1 | 按日确认订房 | POST | `/v3/admin/house/group-batches/{groupBatchId}/room-plans/days/{stayDate}/confirm` | 新增接口 | 逐日执行,扣库存 + 翻态 + 重算分房 + 完成标记,返 3 个 ID 列表 + 警告 |
| 2 | 整团确认订房 | POST | `/v3/admin/house/group-batches/{groupBatchId}/room-plans/confirm` | 新增接口 | 先整团预检,再按日期升序逐日执行;支持幂等重跑与部分成功 |
| 3 | 订房确认预检 | GET | `/v3/admin/house/group-batches/{groupBatchId}/room-plans/confirm-check` | 新增接口 | 零副作用,返回逐日差额表、阻塞名单、整团能否确认;可选按单日过滤 |
---
## 三、接口详情
### 1. 按日确认订房 `POST /v3/admin/house/group-batches/{groupBatchId}/room-plans/days/{stayDate}/confirm`
**VO**: `无请求体 → GroupBatchRoomDayConfirmRespVO`
#### 使用场景
房务在日历上逐日打开某日的确认弹窗,点击「确认本日订房」后调用该端点。服务端原子性地扣库存、翻计划行状态、重算该日分房并检查子订单完成标记,返回处理细节(本次确认了哪些行、之前就确认的哪些行、真正扣了库存的哪些行)与告警。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---|---|---|
| `groupBatchId` | Path | Long | 是 | 雪花 ID | 团期主订单 ID |
| `stayDate` | Path | LocalDate(ISO) | 是 | yyyy-MM-dd 格式 | 入住日,须落在团期 `[departDate, endDate)` 内 |
#### 出参字段表 `Result<GroupBatchRoomDayConfirmRespVO>`
| 字段 | 类型 | 说明 |
|---|---|---|
| `groupBatchId` | String(雪花 ID,`ToStringSerializer`) | 团期主订单 ID |
| `stayDate` | String | 入住日(yyyy-MM-dd) |
| `confirmedPlanIds` | `List<Long>`(元素 `ToStringSerializer`) | 本次由待确认(PENDING)翻成已确认(CONFIRMED)的计划行 ID |
| `skippedPlanIds` | `List<Long>` | 本次之前已确认的计划行 ID(未重复扣库存,已确认状态无变化) |
| `deductedPlanIds` | `List<Long>` | 本次真实扣减了库存的计划行 ID(`deduct_inventory=true` 且非幂等短路) |
| `allocationCount` | Integer | 本日 diff-apply 之后的 active 分房行数 |
| `doneOrderIds` | `List<Long>` | 本次被置为已完成(DONE)的子订单 ID |
| `hotelReady` | Boolean | 本次结束后团期的配房完成标志(全日全户已分房 ∧ 无阻塞) |
| `warnings` | `List<GroupBatchRoomConfirmWarningVO>` | 告警清单(确认已成功,但有事实需关注) |
**GroupBatchRoomConfirmWarningVO** 结构:
| 字段 | 类型 | 说明 |
|---|---|---|
| `code` | String | 告警码,可能值:`BASELINE_INACTIVE`(该户待新版需求确认)、`DAY_OUT_OF_RANGE`(该户某晚越界)、`MANUAL_OVERFLOW`(人工分房超出边界)、`ALLOC_SHORTAGE`(该户该房型缺房)、`ALLOC_LEFTOVER`(计划行有房未分)、`ALLOC_SPLIT_HOTEL`(该户被分到多家酒店) |
| `orderId` | Long(可空,`ToStringSerializer`) | 相关子订单 ID,无具体户时为空 |
| `message` | String | 面向房务的说明文案 |
#### 请求示例
```http
POST /v3/admin/house/group-batches/20260610001/room-plans/days/2026-06-12/confirm
```
#### 响应示例
```json
{
"code": 200,
"success": true,
"data": {
"groupBatchId": "20260610001",
"stayDate": "2026-06-12",
"confirmedPlanIds": ["500001", "500002"],
"skippedPlanIds": [],
"deductedPlanIds": ["500001", "500002"],
"allocationCount": 3,
"doneOrderIds": ["900001"],
"hotelReady": false,
"warnings": [
{
"code": "BASELINE_INACTIVE",
"orderId": "900002",
"message": "订单900002有新版本需求待管理员确认,该户未置为已完成"
}
]
}
}
```
#### 空数据 / 降级响应
- 无空态:入参是团期 ID + 日期,必有对应团期则有响应。若该日零计划行或全已确认(幂等重跑),三个 ID 列表可能为空。
#### 错误响应
```json
{ "code": 808602, "message": "入住日 2026-06-15 不在团期出行区间内", "success": false }
{ "code": 808604, "message": "订房计划行已被确认或版本已变化,请刷新后重试", "success": false }
{ "code": 808605, "message": "酒店 示例酒店 房型 标间 库存不足(需 5 间),请调整订房计划或更换房型", "success": false }
{ "code": 808607, "message": "示例酒店 的 标间 订房 3 间 / 已确认需求 5 间(共 2 项不等,详见预检)", "success": false }
{ "code": 808609, "message": "入住日 2026-06-12 没有可确认的订房计划", "success": false }
{ "code": 808610, "message": "入住日 2026-06-12 订房 35 间,超过班期最大房间数 30", "success": false }
{ "code": 808630, "message": "团期确认采用预占失败(酒店 示例酒店 房型 标间),请稍后重试或等待对账任务收口", "success": false }
{ "code": 808643, "message": "该团有 2 户的分房已过时(首个订单 ORD202606120001),请先重算分房后再确认", "success": false }
{ "code": 808090, "message": "无权操作(仅房务角色可操作)", "success": false }
{ "code": 808091, "message": "无权操作(仅房务管理员或超管可操作)", "success": false }
```
#### 业务边界
- **幂等窗口**:按「团期 ID + 入住日」组合,3 秒租约。同日多次提交返回「处理中」。
- **已发生间夜冻结**:该日入住日早于操作当日且计划行状态已为 CONFIRMED 时,整个团期状态必须是 `TRAVELLING`/`TRIP_FINISHED`/`REVIEWING`/`CANCELLED` 之一,否则 808690。
- **部分成功回滚**:若某行扣库存失败(808605),本次已扣成功的行必须逐条归还,整事务回滚、零副作用。
---
### 2. 整团确认订房 `POST /v3/admin/house/group-batches/{groupBatchId}/room-plans/confirm`
**VO**: `无请求体 → GroupBatchRoomConfirmAllRespVO`
#### 使用场景
房务打开整团视图,点击「整团确认订房」,服务端先做整团预检(零写入),检查所有可执行日(非越界日)是否等量 + 不超班期 + 有基线;预检通过后按日期升序逐日执行,支持幂等重跑(已处理日自动跳过)。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---|---|---|
| `groupBatchId` | Path | Long | 是 | 雪花 ID | 团期主订单 ID |
#### 出参字段表 `Result<GroupBatchRoomConfirmAllRespVO>`
| 字段 | 类型 | 说明 |
|---|---|---|
| `groupBatchId` | String(雪花 ID,`ToStringSerializer`) | 团期主订单 ID |
| `confirmedDates` | `List<String>` | 本次新确认的入住日列表(yyyy-MM-dd);前 k−1 日成功后第 k 日失败时只返前 k−1 日,重跑会跳过 |
| `skippedDates` | `List<String>` | 本次之前已全部确认的入住日(幂等重跑时返回) |
| `emptyDemand` | Boolean | `true` 表示全团无需房户或每户每晚都自订,已直接置配房完成;`false` 表示有需房户需逐日确认 |
| `doneOrderIds` | `List<Long>`(元素 `ToStringSerializer`) | 本次累计被置为已完成的子订单 ID |
| `hotelReady` | Boolean | 本次结束后团期的配房完成标志 |
| `warnings` | `List<GroupBatchRoomConfirmWarningVO>` | 逐日告警并集 |
#### 请求示例
```http
POST /v3/admin/house/group-batches/20260610001/room-plans/confirm
```
#### 响应示例(正常确认)
```json
{
"code": 200,
"success": true,
"data": {
"groupBatchId": "20260610001",
"confirmedDates": ["2026-06-12", "2026-06-13"],
"skippedDates": [],
"emptyDemand": false,
"doneOrderIds": ["900001", "900002"],
"hotelReady": true,
"warnings": []
}
}
```
#### 响应示例(全团无需订房豁免出口)
```json
{
"code": 200,
"success": true,
"data": {
"groupBatchId": "20260610001",
"confirmedDates": [],
"skippedDates": [],
"emptyDemand": true,
"doneOrderIds": [],
"hotelReady": true,
"warnings": []
}
}
```
#### 空数据 / 降级响应
- 全团无需房或每户自订时直接返 `emptyDemand=true`,不再逐日处理。
- 第 k 日失败时已确认的日期在 `confirmedDates`,未处理的日期不返回(不是返回空,而是整体返回成功日期 + 错误响应)。
#### 错误响应
```json
{ "code": 808607, "message": "示例酒店 的 标间 订房 3 间 / 已确认需求 5 间(共 2 项不等,详见预检)", "success": false }
{ "code": 808610, "message": "入住日 2026-06-12 订房 35 间,超过班期最大房间数 30", "success": false }
{ "code": 808631, "message": "本团仍有 3 户住宿需求未填全或未落在团期区间内,不能按「无需订房」置配房完成", "success": false }
{ "code": 808090, "message": "无权操作(仅房务角色可操作)", "success": false }
{ "code": 808091, "message": "无权操作(仅房务管理员或超管可操作)", "success": false }
```
#### 业务边界
- **幂等窗口**:按「团期 ID + 'ALL'」组合,3 秒租约。
- **两步预检**:第一步全局检查(有基线、阶段允许);第二步逐日检查(每日等量 + 不超班期)。全通过才进执行。
- **豁免出口**:全团无需房或每户自订时直接置 `hotelReady=true`,跳过逐日等量校验与越界户一票否决。
- **部分成功**:若第 k 日失败,前 k−1 日已确认状态保留、不回滚,`confirmedDates` 只返前 k−1 日,重跑会自动跳过。
---
### 3. 订房确认预检 `GET /v3/admin/house/group-batches/{groupBatchId}/room-plans/confirm-check`
**VO**: `GroupBatchRoomConfirmCheckReqVO(GET 直接绑定) → GroupBatchRoomConfirmCheckRespVO`
#### 使用场景
在确认前查看「哪一天哪个房型差几间」的完整差额表,以及阻塞名单(越界户/无基线户)。支持按单日过滤,也支持不传日期查看有需求或有计划行的全部日。**零副作用,但同样有角色门**——非房务角色返 808090,房务组长放行(见「关键变化 1」)。
#### 入参字段表
`GroupBatchRoomConfirmCheckReqVO`(GET 参数直接绑定):
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---|---|---|
| `groupBatchId` | Path | Long | 是 | 雪花 ID | 团期主订单 ID |
| `stayDate` | Query | LocalDate(ISO) | 否 | yyyy-MM-dd;不传则返回「有需求或有计划行」的全部日 | 仅查看某一天的差额;确认弹窗按日打开时传,看板整团预检时不传 |
#### 出参字段表 `Result<GroupBatchRoomConfirmCheckRespVO>`
| 字段 | 类型 | 说明 |
|---|---|---|
| `groupBatchId` | String(雪花 ID,`ToStringSerializer`) | 团期主订单 ID |
| `batchStatus` | String | 团期主状态(如 `TRAVELLING`) |
| `stageAllowed` | Boolean | 团期阶段是否允许确认(`REVIEWING`/`RECRUITING`/`SETTLED`/`CANCELLED` 不允许) |
| `baselineExists` | Boolean | 是否存在任一户已确认的需求基线 |
| `hotelReady` | Boolean | 当前配房完成标志 |
| `maxRooms` | Integer | 班期最大房间数(`null` 或 `0` 表示不限) |
| `ready` | Boolean | 整团是否可确认(全部可执行日都等量 ∧ 阶段允许 ∧ 有基线);**注:`ready=true` 不等于配房完成标志能置上**(还需无阻塞) |
| `blockedByOutOfRange` | Boolean | 是否被越界户/无基线户阻塞(房务自己解决不了,需团期管理员改期/重新确认需求/置为不需配房) |
| `days` | `List<DayCheck>` | 逐日预检 |
| `noBaselineOrders` | `List<NoBaselineOrder>` | 需房但无已确认基线的户 |
| `outOfRangeOrders` | `List<OutOfRangeOrder>` | 越界或出发日缺失的户(阻塞名单,不是剔除名单) |
**DayCheck** 子结构(逐日明细):
| 字段 | 类型 | 说明 |
|---|---|---|
| `stayDate` | String(yyyy-MM-dd) | 入住日 |
| `planStatus` | String | 该日计划行状态:`NONE`(无计划)/ `PENDING`(全待确认)/ `PARTIAL`(混合)/ `CONFIRMED`(全已确认) |
| `plannedTotal` | Integer | 该日订房总间数 |
| `demandedTotal` | Integer | 该日已确认需求总间数 |
| `exceedsMaxRooms` | Boolean | 该日订房总数是否超过班期最大房间数 |
| `demand` | `Map<String, Integer>` | 该日各房型大类的需求间数(键=房型大类,值=间数);含越界户落在本格的需求 |
| `mismatch` | `List<CategoryMismatch>` | 逐房型大类的差额(仅列 `diff≠0` 的行) |
| `stale` | Boolean | 该日存在的分房记录基线与当前基线不一致(基线被重新确认过) |
| `dayReady` | Boolean | 该日是否就绪(等量 ∧ 不超班期);越界日恒为 `false` |
| `outOfBatchRange` | Boolean | 该日落在团期区间之外(越界户那一格,订不了也确认不了) |
**CategoryMismatch** 子结构(某日某房型的差额):
| 字段 | 类型 | 说明 |
|---|---|---|
| `roomCategory` | String | 房型大类(如 `STANDARD`) |
| `planned` | Integer | 订房间数 |
| `demanded` | Integer | 已确认需求间数 |
| `diff` | Integer | 差额 = 订房 − 需求;正数多订、负数少订 |
**NoBaselineOrder** 子结构(无基线户):
| 字段 | 类型 | 说明 |
|---|---|---|
| `orderId` | Long(`ToStringSerializer`) | 子订单 ID |
| `orderNo` | String | 订单号 |
| `reason` | String | 原因:`NOT_SUBMITTED`(未提过)/ `PENDING_REVIEW`(待确认)/ `REJECTED`(被打回) |
**OutOfRangeOrder** 子结构(越界或日期缺失户):
| 字段 | 类型 | 说明 |
|---|---|---|
| `orderId` | Long(`ToStringSerializer`) | 子订单 ID |
| `orderNo` | String | 订单号 |
| `departDate` | String(可空,yyyy-MM-dd) | 该户出发日(缺失为空) |
| `reason` | String | 原因:`OUT_OF_RANGE`(出发日外)/ `DATE_MISSING`(缺失日期) |
#### 请求示例
```http
GET /v3/admin/house/group-batches/20260610001/room-plans/confirm-check
GET /v3/admin/house/group-batches/20260610001/room-plans/confirm-check?stayDate=2026-06-12
```
#### 响应示例(有需求不等量)
```json
{
"code": 200,
"success": true,
"data": {
"groupBatchId": "20260610001",
"batchStatus": "TRAVELLING",
"stageAllowed": true,
"baselineExists": true,
"hotelReady": false,
"maxRooms": 30,
"ready": false,
"blockedByOutOfRange": false,
"days": [
{
"stayDate": "2026-06-12",
"planStatus": "PENDING",
"plannedTotal": 3,
"demandedTotal": 5,
"exceedsMaxRooms": false,
"demand": { "STANDARD": 5 },
"mismatch": [
{
"roomCategory": "STANDARD",
"planned": 3,
"demanded": 5,
"diff": -2
}
],
"stale": false,
"dayReady": false,
"outOfBatchRange": false
}
],
"noBaselineOrders": [],
"outOfRangeOrders": []
}
}
```
#### 响应示例(被越界户阻塞)
```json
{
"code": 200,
"success": true,
"data": {
"groupBatchId": "20260610001",
"batchStatus": "TRAVELLING",
"stageAllowed": true,
"baselineExists": true,
"hotelReady": false,
"maxRooms": null,
"ready": false,
"blockedByOutOfRange": true,
"days": [],
"noBaselineOrders": [],
"outOfRangeOrders": [
{
"orderId": "900003",
"orderNo": "ORD202606120003",
"departDate": "2026-06-08",
"reason": "OUT_OF_RANGE"
}
]
}
}
```
#### 空数据 / 降级响应
- `days[]` / `noBaselineOrders[]` / `outOfRangeOrders[]` 均可为空数组(团尚无需求提交或全部已就绪)。
- 单日查询(传了 `stayDate`)时,若查询日不在「有需求或有计划行」的日期范围内,`days` 返回空数组。
#### 错误响应
```json
{ "code": 589500, "message": "团期不存在", "success": false }
```
#### 业务边界
- **零副作用**:不更新任何状态,不消耗幂等令牌,即便并发调用也互不影响。
- **阶段允许**:`RECRUITING` / `REVIEWING` / `SETTLED` / `CANCELLED` 团期返 `stageAllowed=false`,但预检仍继续返回差额表(前端可用于诊断)。
- **与整团确认的异同**:
- 相同:都检查阶段、基线、等量、班期上限、阻塞情况。
- 不同:预检只读不扣库存,整团确认还会顺手补检「豁免出口」(全团无需房时直接置完成)。
---
## 四、契约约束与正确调用方式
### ✅ 正确 / ❌ 错误调用顺序
| 场景 | 调用顺序 |
|---|---|
| ✅ 逐日确认工作流 | 先调 `GET /confirm-check` 查看差额表 → 房务调整订房 → 再调 `POST /days/{stayDate}/confirm` 逐日确认 |
| ✅ 整团一键确认 | 先调 `GET /confirm-check` 检查整团是否 `ready=true` → 再调 `POST /confirm` 一并执行 |
| ✅ 幂等重跑 | 整团确认失败后(第 k 日失败),修复第 k 日问题 → 再调 `POST /confirm` 一次,前 k−1 日自动跳过 |
| ❌ 不看差额直接调用 POST | 房务无法预知会因 808607 / 808610 失败,应先看 confirm-check |
| ❌ 用旧的 `ready` 状态去执行 | 预检与执行之间可能有新的需求确认或订房改动,不能信时间轴之外的 `ready`;前端应在执行前再确认一遍 |
### 角色与权限拦截顺序
| 步骤 | 拦截点 | 返回码 |
|---|---|---|
| 1 | `@HouseWriteGuarded` 检查(两个 POST 才有) | 808090(非房务)/ 808091(组长只读) |
| 2 | `@Idempotent` 幂等窗(三个端点都有) | 「处理中」文案 |
| 3 | 业务逻辑 | 808602 / 808604 / 808605 / 等 |
非房务角色在第 1 步就被挡下、压根进不了幂等窗口。
---
## 五、数据库行为
| 操作 | `group_batch_room_plan` 表 | `group_batch_room_allocation` 表 | `group_batch` 表 |
|---|---|---|---|
| `POST /days/{stayDate}/confirm` 成功 | 匹配行 `plan_status` 改为 `CONFIRMED`;`version` 不变 | 逐日 diff-apply 生成新分房或调整存量分房(若自动分房上线) | 可能改 `hotel_ready=true`(全日全户已分房 ∧ 无阻塞) |
| `POST /confirm` 成功 | 全部匹配行 `plan_status` 改为 `CONFIRMED` | 逐日全量重算分房 | 最终置 `hotel_ready=true` 或保持 `false`(有阻塞) |
| `GET /confirm-check` 成功 | 无变化 | 无变化 | 无变化 |
| 库存扣减 | 在按日确认时按幂等键 `gbrp-{planId}-v{version}` 与 Feign 到 resource 扣库存(成功后落 `group_batch_room_plan_deduct_log`) | — | — |
---
## 六、边界行为
- **并发确认**:多个房务同时确认不同日期,由团期级 `@Lock4j` 序列化(30 秒租约)。
- **已发生间夜**:确认端点对已发生的间夜(`stayDate < today` ∧ `plan_status = CONFIRMED`)逐行返 808690,直接拒不执行。
- **阶段限制**:`RECRUITING` / `REVIEWING` / `SETTLED` / `CANCELLED` 团期调确认端点返 808600;预检仍然可调(只读)。
- **部分成功回滚**:某行失败时已扣的库存必须同事务归还(`afterCommit` 不执行),确保「计划行状态 vs 库存持有 log」对账平衡。
---
## 六.5、枚举 / 数据字典
### 计划行状态(GroupBatchRoomPlanDO.planStatus)
**所属字段**: `days[].planStatus` 及 `GroupBatchRoomPlanRespVO.planStatus` | **类型**: `String`
| 值 | 中文 | 说明 |
|----|------|------|
| `NONE` | 无 | 该日没有计划行 |
| `PENDING` | 待确认 | 该日全部计划行状态都是 `PENDING` |
| `PARTIAL` | 混合 | 该日既有 `PENDING` 也有 `CONFIRMED` 计划行 |
| `CONFIRMED` | 已确认 | 该日全部计划行状态都是 `CONFIRMED` |
### 告警码(GroupBatchRoomConfirmWarningVO.code)
**所属字段**: `GroupBatchRoomDayConfirmRespVO.warnings[].code` 及 `GroupBatchRoomConfirmAllRespVO.warnings[].code` | **类型**: `String`
| 值 | 中文 | 说明 |
|----|------|------|
| `BASELINE_INACTIVE` | 待新版需求确认 | 该户有新版本需求待管理员确认,本次未置为已完成 |
| `DAY_OUT_OF_RANGE` | 越界告警 | 该户某晚换算后落在团期区间外;需求已计入分母、该户进阻塞名单,整团配房完成置不上 |
| `MANUAL_OVERFLOW` | 人工分房超出 | 人工分房行超出计划行间数或该户需求,本次未自动调整 |
| `ALLOC_SHORTAGE` | 缺房告警 | 该户该房型还欠房;首次确认不会出现,它来自重确认 / 人工微调 / 改删连锁重算 |
| `ALLOC_LEFTOVER` | 多房告警 | 该计划行还有房没分出去(订了但没人住) |
| `ALLOC_SPLIT_HOTEL` | 跨酒店分房 | 该户同一晚被分到了不止一家酒店(算法精确匹配房型大类、不看酒店) |
### 无基线原因(GroupBatchRoomConfirmCheckRespVO.noBaselineOrders[].reason)
**所属字段**: `noBaselineOrders[].reason` | **类型**: `String`
| 值 | 中文 | 说明 |
|----|------|------|
| `NOT_SUBMITTED` | 未提交 | 该户从未提交过需求 |
| `PENDING_REVIEW` | 待确认 | 提过但最新版本待管理员确认 |
| `REJECTED` | 被打回 | 最新版本被打回,无有效基线 |
### 越界原因(GroupBatchRoomConfirmCheckRespVO.outOfRangeOrders[].reason)
**所属字段**: `outOfRangeOrders[].reason` | **类型**: `String`
| 值 | 中文 | 说明 |
|----|------|------|
| `OUT_OF_RANGE` | 出发日外 | 该户出发日落在团期 `[departDate, endDate)` 之外 |
| `DATE_MISSING` | 日期缺失 | 该户出发日为空,无法判定是否在团期内 |
---
## 七、不影响范围
- **仅影响**: 管理后台房务团期页面的确认弹窗与看板视图
- **零影响**:
- C 端行程相关接口
- 订单创建、详情查看
- 既有配房、退改、核单流程
- 其他后台模块
---
## 八、测试环境已验证
**部署**:2026-09-11 10:12:31 部署 `hl-order-service-v3` 到测试服(`dev-v3` @ `dfd5a8f6f`,双实例滚动)。
| 验的是什么 | 证据 |
|---|---|
| 进程 | `LISTEN *:8086`、`LISTEN *:8186` 两实例 |
| Nacos | 两实例 `"healthy":true,"enabled":true` |
| 跑的确是 `dfd5a8f6f` | **两条独立路径**:①服务器 `git rev-parse HEAD` = `dfd5a8f6ffb0e768548e787fe1b8cb276ccf86fc`;②jar mtime `10:12:31` 与 `.deployed` 登记一致,两实例 lstart 均晚于 jar mtime |
| Flyway | `flyway_schema_history` 版本 `20260910.302` `success=1`(10:12:38);`SHOW COLUMNS` 列序为 `order_id → hotel_id → room_type_id → room_count`,与 `AFTER` 定义相符 |
| 启动异常 | 两实例日志 `ERROR|Exception` 计数均为 **0** |
**网关实测**(走网关,非直连服务端口):
| 端点 | 角色 | 业务码 | 说明 |
|---|---|---|---|
| `GET /confirm-check` | `house_keeper_lead` | 200 | 正常返回预检体(`baselineExists:false` / `ready:false` / `noBaselineOrders[].reason=NOT_SUBMITTED`) |
| `GET /confirm-check` | `CUSTOMIZER` | **808090** | 读门生效(更正了本篇起草时的错误口径) |
| `GET /confirm-check` | `ROOM_MANAGER`(非该团认领人) | 808612 | 团未被认领 |
| `POST /days/{stayDate}/confirm` | `CUSTOMIZER` | **808090** | — |
| `POST /days/{stayDate}/confirm` | `house_keeper_lead` | **808091** | 只读监督角色 |
| `POST /confirm` | `CUSTOMIZER` | **808090** | — |
| `POST /confirm` | `house_keeper_lead` | **808091** | — |
三个端点全部返回**业务层响应**而非网关路由未命中,证明既有 `/v3/admin/**` 通配已覆盖新路径。
网关零改动另有独立证据:`hl-gateway` 自 2026-09-07 21:53 起未重启(jar mtime 与进程 lstart 两条路径核过),与本次改动无交集。
被拒的两次 POST **零副作用**已用 DB 复核:`group_batch_room_plan` 该团行数 = 0,`order_group_batch.update_time` 未变。
**单元测试**:合并前全量 `Tests run: 9925, Failures: 0, Errors: 0, Skipped: 7`,BUILD SUCCESS。
**Flyway 迁移验证**:新增 `GroupBatchRoomAllocationFlywayMySqlIT` 在真实 MySQL 8.0.33 容器上验过 `V20260910_302` 含存量行的 `ALTER`(先插一行存量再跑迁移,断言两列 NOT NULL 回填 0 且落在 `order_id` 之后,2 个用例绿)。
> 🔴 **尚未验证,如实标注**:**H7 / H8 的真实确认业务逻辑没有跑通。**
> 取证用的团期处于「未被房务认领」态,POST 先撞 808612 就返回了,触达不到确认逻辑本身。
> 要跑通需要先造「整团抢单 → 提交需求 → 管理员确认基线」这条数据链路,本轮没做——
> 所以本节证明的是**可达性 + 鉴权 + 迁移 + 启动**,**不是** H7/H8 的业务正确性。
> 后者目前只有单测覆盖。前端联调时若遇到与预期不符的确认行为,请直接反馈,不要假定后端已实测过。
**单元测试**: 合并前全量 `Tests run: 9925, Failures: 0, Errors: 0, Skipped: 7`,BUILD SUCCESS。
**Flyway 迁移验证**: 新增 `GroupBatchRoomAllocationFlywayMySqlIT` 在真实 MySQL 8.0.33 容器上验过 `V20260910_302` 含存量行的 `ALTER TABLE group_batch_room_allocation ADD COLUMN hotel_id / room_type_id`(2 个用例绿)。
---
## 九、相关历史 PR
| PR | Issue | 说明 | 是否仍有效 |
|----|-------|------|------------|
| #7465 | #7324 | 房务团期看板 H1-H6 + 整团释放(前一期确认前置工作) | ✅ 有效 |
| **本 PR #xxxx** | **#7325** | 按日确认 H7 + 整团确认 H8 + 只读预检 | ✅ 最新 |
---
## 十、相关文档
- **后续计划**: 分房与微调(#7326 H9-H11,定案中),按日确认分房重算全景(#7327 巡检)
- **错误码管理**: 见 `HouseGroupBatchErrorCode.java` 文档注释,段位 808600-808699,本单占 808604-808610 / 808630 / 808631 / 808643
---
## 关联 / 联系人
### 链接
- **Issue**: [#7325](https://git.1814.love:8443/wx/HL/issues/7325)
- **PR**: [#xxxx](https://git.1814.love:8443/wx/HL/pulls/xxxx)
- **Merge commit**: (合并后回填)
### 联系人
- **后端负责人**: @wx