From d807fad55febf64b28e18755d5b2f2a690dbf861 Mon Sep 17 00:00:00 2001 From: API Changelog Bot Date: Thu, 10 Sep 2026 15:43:35 +0800 Subject: [PATCH] =?UTF-8?q?docs(changelog):=20#7322=20=E5=9B=A2=E6=9C=9F?= =?UTF-8?q?=E6=95=B4=E5=9B=A2=E6=8A=A2=E5=8D=95=E4=B8=8E=E6=8A=A2=E5=8D=95?= =?UTF-8?q?=E6=B1=A0=E5=88=86=E6=B5=81=EF=BC=88=E5=89=8D=E7=AB=AF=E5=A5=91?= =?UTF-8?q?=E7=BA=A6=E4=BA=A4=E6=8E=A5=E4=BB=B6=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 五个新端点(抢单池列表/认领/释放/接管/我的团)+ 五个既有端点改造。 团单户不再进逐户抢单池,改以整团为单位认领;order_group_batch 新增 house_claimer_id / house_claimer_name / house_claimed_at 三列(Flyway V20260910_301 已执行)。 已部署测试服 HEAD 6d41d6148,32 条验收项中 28 条已取证, 含用 5 个真实登录账号过网关实测的归属与接管链路。 ⚠️ 正文已注明一项尚缺的交付:sys_menu 的两行抢单池菜单入口尚未随迁移落库, 在补上之前新端点只能直接调用、后台菜单里点不到。 Co-Authored-By: Claude Fable 5.1 --- ...期整团抢单与抢单池分流-新增接口-管理后台.md | 959 ++++++++++++++++++ 1 file changed, 959 insertions(+) create mode 100644 changelogs-v2/2026-09/10_7322_团期整团抢单与抢单池分流-新增接口-管理后台.md diff --git a/changelogs-v2/2026-09/10_7322_团期整团抢单与抢单池分流-新增接口-管理后台.md b/changelogs-v2/2026-09/10_7322_团期整团抢单与抢单池分流-新增接口-管理后台.md new file mode 100644 index 00000000..8c609f2f --- /dev/null +++ b/changelogs-v2/2026-09/10_7322_团期整团抢单与抢单池分流-新增接口-管理后台.md @@ -0,0 +1,959 @@ +--- +schema: "hl-changelog/v2" +ticket: "7322" +title: "团期整团抢单与抢单池分流:新增团期抢单池/我的团/接管四写口,普通抢单池排除团单逐户行" +consumer: "admin" +author: "wx(GIT)" +change_type: "新增接口" +backend_status: "deployed" +gateway_status: "not_required" +frontend_status: "pending" +frontend_owner: "mmg" +frontend_ref: "" +target_release: "" +verified_at: "" +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)的两行抢单池入口尚未随 迁移落库,已另行补 Flyway;在那之前新端点只能直接调用、后台菜单里点不到。" +updated_at: "2026-09-10" +base: "dev-v3" +--- + +# 团期模块:整团抢单与抢单池分流(新增团期抢单池/我的团/团级接管,普通池排除团单逐户行) + +> **服务**: `hl-order-service-v3` +> **PR**: #7436 +> **Issue**: #7322 +> **日期**: 2026-09-10 +> **影响范围**: 房务管家「抢单池」体系——新增「抢单池·团期」与「我的团」两组共 5 个端点;改造既有「抢单池·普通」列表与逐户 claim/transfer 两个端点;团期详情追加 3 个只读字段 + +--- + +## ⚠️ 关键变化 + +给房务加「整团认领」:团期管理员整体确认需求后,该团以**整团一条**进入独立的「团期抢单池」,由一个房务整团认领;此前团单会把每一户拆成一行落进**普通**抢单池,被抢 N 次、落到 N 个房务手上——本次同步把团单逐户行从普通池移出。 + +1. **新增 5 个端点**(详见「三、接口详情」):团期抢单池列表、整团认领、整团释放、**团级接管(超管专属,处理历史脏数据死结)**、我的团(含组长/超管监督视图)。 +2. **`GET /v3/admin/order/grab-pool/hotel-requirements`(普通抢单池)行为变化**:追加 `order_main.product_batch_id IS NULL` 过滤,团期子订单的逐户需求行**不再出现**在普通池,`total` 同口径减少;`productType=GROUP` 筛选仍接受但结果恒为空。 +3. **`POST /v3/admin/order/hotel-requirements/{requirementId}/claim`(逐户抢单)行为变化**:对团期子订单新增守卫,**无条件**拒绝,新增错误码 **808650**,零写入。 +4. **`POST /v3/admin/order/hotel-requirements/{requirementId}/transfer`(逐户转单)行为变化**:对团期子订单新增守卫——普通房务恒拒 808650;`SUPER_ADMIN` 仅在该团**尚未被整团认领**(`house_claimer_id IS NULL`)时放行,用于把历史脏数据集中到一个在职房务名下,团已被整团认领后超管也拒。 +5. **`POST /v3/admin/order/hotel-requirements/{requirementId}/release`(逐户释放)行为变化**:⚠️ 与工单正文「口径与定案 #6」早期结论(release 不改)不同——2026-09-08 第六轮复审后追加了一道更严守卫:**团期子订单**若仍存在任意 active 逐户配房行(不限 `confirm_status`,含询房中 `INQUIRING` 候选)→ 新增错误码 **808662**,零写入;非团单行为完全不变(仍是既有的「仅 `CONFIRMED` 阻塞释放」口径 808021)。 +6. **`GET /v3/admin/order/group-batch/{groupBatchId}`(团期详情)追加 3 个只读字段**:`houseClaimerId` / `houseClaimerName` / `houseClaimedAt`(均可空,未认领为 `null`),供团期管理员侧展示「负责房务」。 +7. **团级指针是团单房务归属的唯一真源,认领/接管不回写任何户级字段**(不动 `claimer_id`/`status`/`house_status`/`room_control_status`,不 fire 户级状态机,不绑会话,不广播 SSE)——认领后户级 `room_control_status` 仍会显示「待配房」直到后续订房工单(`#7325`)把它置 `DONE`,这是有意为之,不是缺陷。 +8. **无 SSE 广播**:团期池/我的团页面需前端手动或定时刷新,不会像普通抢单池那样实时推送变更。 +9. **不新增权限码**,5 个新端点走既有的「房务角色门」(`HouseWriteGuard`,写口)与新增的读口角色门(`ROOM_MANAGER`/`house_keeper_lead`/`SUPER_ADMIN`,否则 808090),hl-user-service **零 Flyway**(菜单种子是否需要待确认,见「口径与定案 #11」,如需要会是独立的 user-service Flyway,不影响本篇端点契约)。 +10. **数据库变更**:`order_group_batch` 新增 3 列 + 1 索引(`V20260910_301__order_group_batch_add_house_claimer.sql`),无新表。 + +--- + +## 一、背景 + +团期管理员点「整体确认」后,`RequirementService.dispatchGroupHotelRequirements` 把每一户的 active 房需求 CAS 成 `status=PENDING`、`claimer_id=NULL`,而普通抢单池此前无任何团单排除条件——一个 30 户的团在普通池里是 30 行,会被抢 30 次、落到 N 个房务手上。「某个房务认领了整个团」这个概念此前在代码里不存在:`GroupBatchDO.batchManagerId` 语义是「团期管理员」不是房务,且全仓零写入。 + +本单给 `order_group_batch` 加三列认领指针,新建团期专属的抢单池/我的团/接管三个写口 + 两个读口,并把团单逐户行从普通池移出、给逐户 claim/transfer 加团单守卫,杜绝「一个团被多个房务分持」。历史脏数据(团单户已被逐户抢走)由「团级接管」端点(超管专属)兜底处理,测试库不存在生产存量(`origin/main` 上团期相关三个核心类全部零命中,二期功能尚未上线现网)。 + +--- + +## 二、变更接口清单 + +| # | 接口 | 方法 | 路径 | 变更类型 | 说明 | +|---|---|---|---|---|---| +| 1 | 团期抢单池列表 | GET | `/v3/admin/order/grab-pool/group-batches` | 新增 | 整团一条,分页 + 多维筛选 | +| 2 | 整团认领 | POST | `/v3/admin/order/grab-pool/group-batches/{groupBatchId}/claim` | 新增 | CAS 先抢先得,户级零副作用 | +| 3 | 整团释放回池 | POST | `/v3/admin/order/grab-pool/group-batches/{groupBatchId}/release` | 新增 | 认领人本人或超管 | +| 4 | 团级接管 | POST | `/v3/admin/order/grab-pool/group-batches/{groupBatchId}/takeover` | 新增 | 超管专属,处理历史脏数据死结 | +| 5 | 我的团 | GET | `/v3/admin/order/grab-pool/my-claims/group-batches` | 新增 | 含 `scope=all` 组长/超管监督视图 | +| 6 | 普通抢单池列表 | GET | `/v3/admin/order/grab-pool/hotel-requirements` | 行为修改 | 追加排除团单逐户行 | +| 7 | 逐户抢单 | POST | `/v3/admin/order/hotel-requirements/{requirementId}/claim` | 行为修改 | 新增 808650 团单守卫 | +| 8 | 逐户转单 | POST | `/v3/admin/order/hotel-requirements/{requirementId}/transfer` | 行为修改 | 新增 808650 团单守卫(超管有条件例外) | +| 9 | 逐户释放 | POST | `/v3/admin/order/hotel-requirements/{requirementId}/release` | 行为修改 | 团单户新增 808662 更严守卫(active 配房未清空不可释放),非团单不变 | +| 10 | 团期详情 | GET | `/v3/admin/order/group-batch/{groupBatchId}` | 行为修改 | 响应追加 3 个只读字段 | + +--- + +## 三、接口详情 + +### 1. 团期抢单池列表 `GET /v3/admin/order/grab-pool/group-batches` + +**VO**: `HouseGroupGrabPoolPageReqVO → PageResult` + +#### 使用场景 + +房务管家「抢单池·团期」页,展示未被认领、需求已整体确认、团期阶段在四态内的团(整团一条)。角色 `ROOM_MANAGER` / `house_keeper_lead` / `SUPER_ADMIN`,其它角色 808090。 + +#### 入参字段表 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|---|---|---|---|---|---| +| keyword | Query | String | 否 | ≤32 字 | `batchNo`/`productName` 模糊二选一 OR | +| productId | Query | Long | 否 | — | 产品精确过滤 | +| batchStatus | Query | String | 否 | 只接受 `RESOURCE_PREPARING`/`MATERIAL_PREPARING`/`PENDING_DEPARTURE`/`TRAVELLING`,非法值 400 | 团期阶段精确过滤 | +| departDateFrom / departDateTo | Query | LocalDate | 否 | `yyyy-MM-dd` | 出发日区间 | +| page | Query | Integer | 否 | ≥1,默认 1 | 页码 | +| pageSize | Query | Integer | 否 | 1-100,默认 20 | 每页条数 | +| sortBy | Query | String | 否 | `departDate,asc`(默认)/ `createTime,desc` | 排序 | + +#### 出参字段表 `Result>` + +| 字段 | 类型 | 说明 | +|---|---|---| +| `data.list[].groupBatchId` | String(Long ToString) | 团期主订单 ID | +| `data.list[].batchNo` | String | 运营团期号 | +| `data.list[].productId` | String(Long ToString) | 产品 ID | +| `data.list[].productName` | String | 产品名快照 | +| `data.list[].batchName` | String | 班期名快照 | +| `data.list[].batchLabel` | String | 第 N 期快照 | +| `data.list[].batchStatus` | String | 团期状态 code | +| `data.list[].batchStatusLabel` | String | 团期状态中文 | +| `data.list[].departDate` / `endDate` / `enrollDeadline` | LocalDate | 可空 | +| `data.list[].enrolledRooms` / `enrolledPeople` | Integer | 已报名房数 / 人数(持久计数器直取) | +| `data.list[].activeOrderCount` | Integer | 活跃子订单数(排除 `CANCELLED`) | +| `data.list[].hotelOrderCount` | Integer | 已放行到房务的需房户数(active 房需求 `status ∈ {PENDING,PROCESSING,DONE}` 的户数) | +| `data.list[].hotelReady` | Boolean | 团期酒店资源是否已就绪 | +| `data.list[].daysToDepart` | Integer | 今天到出发日天数,无出发日 `null` | +| `data.list[].urgencyLevel` / `urgencyLabel` | String | 紧急度 code/中文(`NORMAL`/`URGENT`/`CRITICAL`) | +| `data.list[].createTime` | LocalDateTime | 团期创建时间 | + +(字段取自 `HouseGroupGrabPoolItemRespVO.java:17-86`。) + +#### 请求示例 + +```json +GET /v3/admin/order/grab-pool/group-batches?page=1&pageSize=20&sortBy=departDate,asc +Authorization: Bearer <持房务三角色之一的管理端 token> +``` + +#### 响应示例 + +```json +{ + "code": 200, "message": "成功", "success": true, + "data": { + "total": 1, "pages": 1, "current": 1, "size": 20, + "list": [ + { "groupBatchId": "1930000000000000001", "batchNo": "GB26060101", "productId": "1001", + "productName": "额吉的故乡", "batchName": "6月首发团", "batchLabel": "第3期", + "batchStatus": "RESOURCE_PREPARING", "batchStatusLabel": "资源准备中", + "departDate": "2026-06-15", "endDate": "2026-06-20", "enrollDeadline": "2026-06-10", + "enrolledRooms": 12, "enrolledPeople": 26, "activeOrderCount": 12, "hotelOrderCount": 10, + "hotelReady": false, "daysToDepart": 12, "urgencyLevel": "NORMAL", "urgencyLabel": "正常", + "createTime": "2026-05-01 10:00:00" } + ] + } +} +``` + +(示例字段取自 VO 定义与 `HouseGroupGrabService.toPoolItem` 装配逻辑,非真实网关调用。) + +#### 空数据 / 降级响应 + +无符合条件的团时 `list` 为空数组、`total=0`,返回 200。0 需房户的团(全团客户自订酒店)同样进池,`hotelOrderCount=0` 明示,判定条件不含需房户数。 + +#### 错误响应 + +```json +{ "code": 808090, "message": "未登录或非房务角色,无权操作", "success": false, "data": null } +``` + +#### 业务边界 + +- 入池判定 = `house_claimer_id IS NULL AND requirement_confirmed=1 AND batch_status IN (RESOURCE_PREPARING, MATERIAL_PREPARING, PENDING_DEPARTURE, TRAVELLING)`(+ 未软删)。 +- 读口角色门与写口不同:`house_keeper_lead`(房务组长)在这里**可以正常查看**(三角色平权只读),只有写操作(认领/释放/接管)才会把组长挡在 808091。 +- 无角色(系统态/内部调用/单测)放行,与 `HouseWriteGuard` 同口径。 + +--- + +### 2. 整团认领 `POST /v3/admin/order/grab-pool/group-batches/{groupBatchId}/claim` + +**VO**: `无请求体 → Result` + +#### 使用场景 + +房务在团期抢单池卡片点击「认领」,CAS 先抢先得。成功后只写团级三列 + 团期时间线 + 通知事件,**户级行零变化**。 + +#### 入参字段表 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|---|---|---|---|---|---| +| `groupBatchId` | Path | Long | 是 | 团期主订单 ID(雪花) | 前端应按 String 传递避免精度丢失 | + +无请求体。 + +#### 出参字段表 `Result` + +| 字段 | 类型 | 说明 | +|---|---|---| +| `data` | null | 成功即 200,与逐户 claim 一致 | + +#### 请求示例 + +```json +POST /v3/admin/order/grab-pool/group-batches/1930000000000000001/claim +Authorization: Bearer <持 ROOM_MANAGER 或 SUPER_ADMIN 的管理端 token> +``` + +#### 响应示例 + +```json +{ "code": 200, "message": "成功", "success": true, "data": null } +``` + +#### 空数据 / 降级响应 + +无(写操作)。 + +#### 错误响应 + +```json +{ "code": 808654, "message": "该团有 2 户已被房务 王五 逐户抢单,请先释放后再整团认领", "success": false, "data": null } +``` + +| 码 | 符号 | 触发 | +|---|---|---| +| 808090 | `NOT_LOGIN_OR_NOT_HOUSE_ROLE` | 未登录 / 非房务角色 | +| 808091 | `READ_ONLY_LEAD_FORBIDDEN` | 房务组长(只读监督,不可写) | +| 589500 | `GROUP_BATCH_NOT_FOUND` | 团期不存在 | +| 808654 | `GB_GRAB_HOUSEHOLD_CLAIMED_BY_OTHERS` | 「该团有 {0} 户已被房务 {1} 逐户抢单,请先释放后再整团认领」——有户被**别人**逐户抢走 | +| 808659 | `GB_GRAB_TAKEOVER_HAS_HOUSE_ASSIGNMENT` | 「该团有 {0} 户存在逐户配房(首个订单 {1}),请先删除配房后再操作」——分流后剩余户存在 active 逐户配房行 | +| 808651 | `GB_GRAB_BATCH_NOT_CLAIMABLE` | 「该团期当前不可认领(需求未整体确认或团期阶段不允许)」——CAS 落空且未被他人认领 | +| 808652 | `GB_GRAB_BATCH_ALREADY_CLAIMED` | 「该团期已被其他房务认领」 | +| 808653 | `GB_GRAB_BATCH_CLAIMED_BY_SELF` | 「该团期已由您认领,请勿重复认领」 | + +(错误码字面量与消息文案取自 `HouseGroupBatchErrorCode.java:72-121`。) + +#### 业务边界 + +- 顺序不可颠倒:脏数据预检(808654)→ 冻结名单分流(历史已 finalize 户不受影响)→ 无损前置(808659)→ 团级 CAS → 时间线 → afterCommit 通知(`HouseGroupGrabService.claim`,`HouseGroupGrabService.java:245-284`)。 +- 全部持有人都是本人时 808654 不触发(本人把「逐户抢的团」升级成整团认领)。 +- 认领**不**回写户级 `claimer_id`/`status`/`house_status`/`room_control_status`,不 fire 户级状态机,不绑定会话,不广播 SSE——团级指针是唯一真源。 +- 三个写口(claim/release/takeover)共用同一把团级 `@Lock4j`(`name=HouseGroupBatchLockConstants.GROUP_BATCH_HOUSE_LOCK_NAME`),配合 CAS 保证并发安全,与幂等注解(`@Idempotent`,5 秒防重复提交)叠加。 + +--- + +### 3. 整团释放回团期池 `POST /v3/admin/order/grab-pool/group-batches/{groupBatchId}/release` + +**VO**: `HouseGroupReleaseReqVO → Result` + +#### 使用场景 + +认领人本人释放自己认领的团,或 `SUPER_ADMIN` 强制释放任意团(需附理由)。释放后该团若仍满足进池条件即重新出现在团期池。 + +#### 入参字段表 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|---|---|---|---|---|---| +| `groupBatchId` | Path | Long | 是 | 团期主订单 ID | — | +| `reason` | Body | String | 否(超管必填≥10字) | ≤200 字 | 超管释放必填理由(trim 后 <10 字 → 808657);普通房务可空,空白兜底文案「释放回池」 | + +请求体可为 `null`(`@RequestBody(required=false)`)。 + +#### 出参字段表 `Result` + +| 字段 | 类型 | 说明 | +|---|---|---| +| `data` | null | 成功 200 | + +#### 请求示例 + +```json +POST /v3/admin/order/grab-pool/group-batches/1930000000000000001/release +Authorization: Bearer <认领人本人 token> + +{ "reason": "临时调休,交回团期池" } +``` + +#### 响应示例 + +```json +{ "code": 200, "message": "成功", "success": true, "data": null } +``` + +#### 空数据 / 降级响应 + +无。 + +#### 错误响应 + +```json +{ "code": 808655, "message": "该团期不属于当前房务,无法释放", "success": false, "data": null } +``` + +| 码 | 符号 | 触发 | +|---|---|---| +| 808090 / 808091 | 角色门 | 同上 | +| 589500 | `GROUP_BATCH_NOT_FOUND` | 团期不存在 | +| 808656 | `GB_GRAB_BATCH_NOT_CLAIMED` | 「该团期尚未被认领,无需释放」 | +| 808655 | `GB_GRAB_RELEASE_NOT_OWNER` | 「该团期不属于当前房务,无法释放」——非认领人且非超管;或并发落空(CAS 时已被他人释放/接管) | +| 808657 | `GB_GRAB_ADMIN_RELEASE_REASON_TOO_SHORT` | 「超管操作原因长度不足 10 字」 | +| **808660** | `GB_HOUSE_RELEASE_HAS_ROOM_PLAN` | 「该团仍有 {0} 条未取消的订房计划,无法释放(请先处理订房计划或走接管)」——该团存在未取消的订房计划行(`GroupBatchRoomPlanService.countActivePlans`) | + +#### 业务边界 + +- 判定顺序:登录/角色门 → 团期存在 → 归属判定(本人/超管)→ reason 长度(超管)→ **已订房守卫 808660** → CAS 释放 → 时间线 → 通知(`HouseGroupGrabService.release`,`HouseGroupGrabService.java:296-341`)。 +- 808660 存在的理由:若无此守卫,把团释放成无主但计划行与已扣库存都还在,之后没有任何角色能继续处理;超管要强行换人须走「团级接管」而非「释放」。 +- 释放后原认领人的历史轨迹只记团期时间线(`BATCH_HOUSE_RELEASE`),不落回任何户级表。 + +--- + +### 4. 团级接管 `POST /v3/admin/order/grab-pool/group-batches/{groupBatchId}/takeover` + +**VO**: `HouseGroupTakeoverReqVO → Result` + +#### 使用场景 + +**超管专属**。用于处理历史脏数据死结——当同一历史团的户被两个不同房务分别持有、且至少一户已有确认配房时,整团 `claim`(撞 808654)、持有人逐户 `release`(撞既有的 808021 已确认配房守卫)、逐户 `transfer`(撞本单新增的 808650)三条路同时封死,接管是唯一的无损出口。接管 = 覆盖式指定团期房务归属 + 清理该团历史遗留的户级抢单归属。 + +#### 入参字段表 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|---|---|---|---|---|---| +| `groupBatchId` | Path | Long | 是 | 团期主订单 ID | — | +| `toUserId` | Body | Long | 是 | 须在 user-service 在职房务列表内,否则 808011 | 接管人 userId | +| `reason` | Body | String | 是 | trim 后 10-200 字 | 接管原因,写入团级时间线 | + +#### 出参字段表 `Result` + +| 字段 | 类型 | 说明 | +|---|---|---| +| `data.groupBatchId` | String(Long ToString) | 团期主订单 ID | +| `data.fromClaimerId` | String(Long ToString,可空) | 接管前的团级认领人(未认领时 `null`) | +| `data.toClaimerId` | String(Long ToString) | 接管后的团级认领人 | +| `data.toClaimerName` | String | 接管人真实姓名(user-service 解析失败时兜底 `user-{id}`) | +| `data.clearedOrderIds` | Array\ | 本次被清掉户级 `claimer_id` 的子订单 ID 列表 | +| `data.legacyFinalizedOrderIds` | Array\ | 冻结名单里的历史户(保留逐户配房,不迁移,见「业务边界」) | +| `data.skippedOrderIds` | Array\ | 户级 CAS 并发落空、未清掉的子订单,需重跑或人工处理 | + +(字段取自 `HouseGroupTakeoverRespVO.java:16-46`。) + +#### 请求示例 + +```json +POST /v3/admin/order/grab-pool/group-batches/1930000000000000001/takeover +Authorization: Bearer <持 SUPER_ADMIN 的管理端 token> + +{ "toUserId": 30002, "reason": "原认领房务离职,指派新房务接管该团" } +``` + +#### 响应示例 + +```json +{ + "code": 200, "message": "成功", "success": true, + "data": { + "groupBatchId": "1930000000000000001", "fromClaimerId": "30001", "toClaimerId": "30002", + "toClaimerName": "李四", "clearedOrderIds": ["100", "101"], + "legacyFinalizedOrderIds": ["102"], "skippedOrderIds": [] + } +} +``` + +(示例结构取自 `HouseGroupTakeoverRespVO` 字段定义与 `HouseGroupGrabService.takeoverTx` 装配逻辑,非真实网关调用。) + +#### 空数据 / 降级响应 + +无被清户/冻结户/落空户时对应数组为空列表 `[]`,不是 `null`。 + +#### 错误响应 + +```json +{ "code": 808658, "message": "团期接管仅超级管理员可操作", "success": false, "data": null } +``` + +| 码 | 符号 | 触发 | +|---|---|---| +| 808090 / 808091 | 角色门 | 未登录/非房务角色/组长只读 | +| **808658** | `GB_GRAB_TAKEOVER_NOT_SUPER_ADMIN` | 「团期接管仅超级管理员可操作」——非超管调用 | +| 808657 | `GB_GRAB_ADMIN_RELEASE_REASON_TOO_SHORT` | `reason` trim 后 <10 字 | +| 808011 | `RECEIVER_NOT_FOUND` | 「接收人不存在或已离职」——`toUserId` 不在在职房务列表 | +| 589500 | `GROUP_BATCH_NOT_FOUND` | 团期不存在 | +| 808659 | `GB_GRAB_TAKEOVER_HAS_HOUSE_ASSIGNMENT` | 分流后剩余户存在 active 逐户配房行,须先删配房 | +| 808651 | `GB_GRAB_BATCH_NOT_CLAIMABLE` | 团级覆盖 CAS 落空(团期已被软删等极端并发场景) | + +#### 业务边界 + +- **判定顺序**(不可颠倒):超管判定 → `reason` 长度 → 接管人在职校验(Feign,**事务外**执行)→ 团期存在 → 冻结名单分流 → 无损前置(808659)→ 清剩余户级归属(CAS 落空计入 `skippedOrderIds`,不抛错)→ 团级覆盖 CAS → 时间线(复用 `BATCH_HOUSE_CLAIM` 事件类型,靠 `extra.takeover=true` 区分)→ afterCommit 通知(`HouseGroupGrabService.takeover`/`takeoverTx`,`HouseGroupGrabService.java:368-465`)。 +- **为什么接口是非事务外壳 + 事务内层两段**:接管人姓名解析要调 `houseStaffFeign.listHouseStaff`(Feign),与写操作放在同一事务违反「事务内禁同步 Feign」(CODE_RULES §9);外壳(`takeover`)纯读完成超管判定/reason 校验/姓名解析后,经自注入代理调用真正的事务方法(`takeoverTx`)。**对外契约(路径/入参/响应/错误码/执行顺序)不受这个内部拆分影响**,前端无需关心。 +- **冻结名单(`legacyFinalizedOrderIds`)语义**:接管时若某户在**首次**启用团期归属那一刻 `house_status=CONFIRMED`(即已有实际住宿),该户会被冻结、保留其既有的逐户 `house_hotel_assignment` 配房,**不迁移、不清 `claimer_id`、不计入团期需求分母、不计入团期分房**;此后该户即使重跑接管/认领也保持冻结(只读表,不重算)。 +- **无损前置(808659)只对分流后的剩余户求值**:真正冻结的历史户不受该守卫影响;判定用「是否存在 active 逐户配房行」(不限 `confirm_status`,含询房中候选),因为候选房同样占用已扣库存。 +- `skippedOrderIds` 非空时是并发导致的部分成功(户级 CAS 落空但团级已覆盖成功),需要人工核对或重试清理这些户的残留 `claimer_id`。 +- Feign 调用仍在 Controller 的团级 `@Lock4j` 范围**之内**(有意如此,非漏网):把它挪到锁外只能挪进 Controller 做业务编排,违反分层铁律;持分布式锁期间卡住只让同团后续写入排队(锁有 30 秒过期兜底),比持 DB 事务卡住吊死连接池代价小得多。 + +--- + +### 5. 我的团 `GET /v3/admin/order/grab-pool/my-claims/group-batches` + +**VO**: `HouseMyGroupPageReqVO → HouseMyGroupPageRespVO` + +#### 使用场景 + +当前房务查看自己已整团认领的团(`scope=mine`,默认);房务组长/超管可传 `scope=all` 查看全部已认领团(监督视图,行内带认领人)。不限团期阶段——已认领的团可能已流团/已结束,本单不自动释放指针,房务仍要看得见并手动释放。 + +#### 入参字段表 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|---|---|---|---|---|---| +| scope | Query | String | 否 | `mine`(默认)/ `all` | 普通房务传 `all` → 808092 | +| keyword | Query | String | 否 | ≤32 字 | `batchNo`/`productName` 模糊 | +| batchStatus | Query | String | 否 | 团期九态任一,非法值 400 | — | +| needsReconfirm | Query | Boolean | 否 | `true`=只看 `requirement_confirmed=0` 的团 | 待管理员重新确认 | +| departDateFrom / departDateTo | Query | LocalDate | 否 | — | 出发日区间 | +| claimedAtFrom / claimedAtTo | Query | LocalDateTime | 否 | `yyyy-MM-dd'T'HH:mm:ss` | 认领时间区间 | +| page / pageSize | Query | Integer | 否 | 默认 1/20,pageSize≤100 | — | +| sortBy | Query | String | 否 | `claimedAt,desc`(默认)/ `departDate,asc` | — | + +#### 出参字段表 `Result` + +| 字段 | 类型 | 说明 | +|---|---|---| +| `data.total` | Long | 当前筛选条件下的总数(DB count,任何情况下都准) | +| `data.stats.total` | Long | 该 scope 下已认领团数(不叠加列表筛选,只按 scope/认领人过滤) | +| `data.stats.needsReconfirm` | Long | 其中待管理员重新确认的团数;**`stats.total` 超过扫描上限 500 时为 `null`**,前端显示「—」 | +| `data.stats.cancelled` | Long | 其中已流团的团数;同口径超上限为 `null` | +| `data.list[]` | `HouseMyGroupItemRespVO` | 字段 = 团期池列表项全部字段(见「## 1.」出参)+ 下列 4 个 | +| `data.list[].houseClaimerId` | String(Long ToString) | 整团认领房务 adminId | +| `data.list[].houseClaimerName` | String | 整团认领房务姓名 | +| `data.list[].houseClaimedAt` | LocalDateTime | 整团认领时间 | +| `data.list[].requirementConfirmed` | Boolean | `false` 时前端应标「待管理员重新确认」 | + +(字段取自 `HouseMyGroupPageRespVO.java`、`HouseMyGroupItemRespVO.java`、`HouseMyGroupStatsVO.java`。) + +#### 请求示例 + +```json +GET /v3/admin/order/grab-pool/my-claims/group-batches?scope=mine&page=1&pageSize=20 +Authorization: Bearer <房务 token> +``` + +#### 响应示例 + +```json +{ + "code": 200, "message": "成功", "success": true, + "data": { + "total": 1, + "stats": { "total": 1, "needsReconfirm": 0, "cancelled": 0 }, + "list": [ + { "groupBatchId": "1930000000000000001", "batchNo": "GB26060101", "productId": "1001", + "productName": "额吉的故乡", "batchStatus": "RESOURCE_PREPARING", "batchStatusLabel": "资源准备中", + "activeOrderCount": 12, "hotelOrderCount": 10, "hotelReady": false, + "houseClaimerId": "30001", "houseClaimerName": "张三", + "houseClaimedAt": "2026-06-01 10:00:00", "requirementConfirmed": true } + ] + } +} +``` + +(部分字段省略以节省篇幅,完整字段见出参字段表;示例非真实网关调用。) + +#### 空数据 / 降级响应 + +未认领任何团时 `list=[]`、`total=0`、`stats.total=0`。`stats.total` 超过 500(扫描上限)时 `stats.needsReconfirm`/`stats.cancelled` 为 `null`(不是少算的数字),前端应显示「—」而不是 0——这是刻意设计,避免与 `total` 自相矛盾。 + +#### 错误响应 + +```json +{ "code": 808090, "message": "未登录或非房务角色,无权操作", "success": false, "data": null } +``` + +```json +{ "code": 808092, "message": "无权查看全部房务订单(仅房务组长或超管可查看)", "success": false, "data": null } +``` + +#### 业务边界 + +- `scope=all` 仅 `house_keeper_lead`/`SUPER_ADMIN` 可用,普通房务传 `all` → 808092(复用既有码,不是 808091「组长只读」,文案更贴切)。 +- `stats` 统计口径**只按 `scope`/认领人过滤,不叠加 `keyword`/日期等列表筛选**——头部数字是「我一共有多少团」,不随筛选跳动。 + +--- + +### 6. 普通抢单池列表(改造) `GET /v3/admin/order/grab-pool/hotel-requirements` + +**VO**: `HouseGrabPageReqVO → PageResult`(本单**不改**任何入参/出参字段,只改查询条件) + +#### 使用场景 + +房务管家「抢单池·普通」页,逐户一条。本次改造后团期子订单的逐户需求行不再出现,只保留非团单的逐户需求。 + +#### 入参字段表 + +无字段变化,沿用既有 `HouseGrabPageReqVO`: + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|---|---|---|---|---|---| +| keyword | Query | String | 否 | — | 订单号/团号/客人姓名/产品名 OR 模糊 | +| productType | Query | String | 否 | `CORE`/`ROUTE`/`CUSTOM`/`GROUP` | 传 `GROUP` 改后恒返空页(见业务边界) | +| productName | Query | String | 否 | — | 产品名模糊 | +| consultantId | Query | Long | 否 | — | 定制师精确 | +| guestName | Query | String | 否 | — | 客人姓名模糊 | +| departDateFrom / departDateTo | Query | LocalDate | 否 | — | 出发日区间 | +| page / pageSize | Query | Integer | 否 | 默认 1/20,pageSize≤100 | — | +| sortBy | Query | String | 否 | `createTime,desc`(默认)/ `departDate,asc` | — | + +#### 出参字段表 + +无字段变化,沿用既有 `HouseGrabPageItemRespVO`: + +| 字段 | 类型 | 说明 | +|---|---|---| +| `data.list[].id` / `orderId` / `orderNo` / `teamNo` / `guestName` / `personsDesc` | 各自既有类型 | 需求/订单基本信息,本次不改 | +| `data.list[].productType` / `productName` / `productNo` / `route` / `departDate` / `nights` / `cities` / `totalAmount` | 各自既有类型 | 产品/行程信息,本次不改 | +| `data.list[].consultantName` / `consultantId` / `consultantRemark` | 各自既有类型 | 定制师信息,本次不改 | +| `data.list[].requirementNote` / `dispatchRemark` / `special` / `requirementVersion` | 各自既有类型 | 需求备注/版本,本次不改 | +| `data.list[].urgencyLevel` / `urgencyLabel` / `daysToDepart` / `manualUrgent` / `createTime` | 各自既有类型 | 紧急度/时间信息,本次不改 | +| `data.list[].isRework` / `reworkPrevClaimerName` | 各自既有类型 | 返工标记,本次不改 | + +(完整 29 个字段定义见 `HouseGrabPageItemRespVO.java:28-119`,本单零改动,此处不逐一展开类型。) + +#### 请求示例 + +```json +GET /v3/admin/order/grab-pool/hotel-requirements?page=1&pageSize=20&sortBy=createTime,desc +Authorization: Bearer <房务 token> +``` + +#### 响应示例 + +```json +{ + "code": 200, "message": "成功", "success": true, + "data": { + "total": 1, "pages": 1, "current": 1, "size": 20, + "list": [ + { "id": "7001", "orderId": "200", "orderNo": "HL2606000200", "teamNo": null, + "guestName": "王五", "productType": "CORE", "productName": "川西深度", + "departDate": "2026-06-10", "createTime": "2026-06-01 09:00:00" } + ] + } +} +``` + +(部分字段省略以节省篇幅;完整结构见出参字段表,示例非真实网关调用。) + +#### 空数据 / 降级响应 + +无符合条件行时 `list=[]`、`total=0`,返回 200;本次改造后团期子订单逐户行不再计入 `total`,非团单场景的空数据行为与改前完全一致。 + +#### 错误响应 + +```json +{ "code": 400, "message": "参数校验失败", "success": false, "data": null } +``` + +无新增业务错误码,仅由全局参数校验处理器处理非法入参(如 `pageSize > 100`)。 + +#### 业务边界 + +- **改前**:返回全部 `status=PENDING AND is_active=1 AND claimer_id IS NULL` 的行,含团期子订单逐户行。 +- **改后**:追加 `order_main.product_batch_id IS NULL`,团期子订单逐户行不再出现,`total` 同口径减少。 +- `productType=GROUP` 的筛选仍会被接受,但结果恒为空(团单只在团期池出现)——**前端建议隐藏「产品类型」下拉里的 `GROUP` 选项**,避免用户选中后困惑于「为什么筛出来是空的」。 +- 团单谓词用 `product_batch_id IS NULL` 而不是 `product_type <> 'GROUP'`:团期产品但尚未归到任何团期(`product_batch_id` 为空)的订单没有团期管理员确认链,若按 `product_type` 排除会让这类单在两个池里都不出现。 + +--- + +### 7. 逐户抢单(改造) `POST /v3/admin/order/hotel-requirements/{requirementId}/claim` + +**VO**: `无请求体 → Result`(本单不改请求/响应结构,只在业务逻辑最前面加一道团单守卫) + +#### 使用场景 + +房务对非团期子订单逐户抢单,行为不变;对**团期子订单**新增拦截,引导房务改用团期抢单池整团认领。 + +#### 入参字段表 + +无变化: + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|---|---|---|---|---|---| +| `requirementId` | Path | Long | 是 | 用房需求 ID | 与改前完全一致 | + +#### 出参字段表 + +无变化: + +| 字段 | 类型 | 说明 | +|---|---|---| +| `data` | null | 成功 200,与改前完全一致 | + +#### 请求示例 + +```json +POST /v3/admin/order/hotel-requirements/7002/claim +Authorization: Bearer <房务 token> +``` + +#### 响应示例 + +```json +{ "code": 200, "message": "成功", "success": true, "data": null } +``` + +#### 空数据 / 降级响应 + +无(写操作),与改前一致。 + +#### 错误响应 + +```json +{ "code": 808650, "message": "团期订单不支持逐户抢单/转单,请到团期抢单池整团认领", "success": false, "data": null } +``` + +| 码 | 符号 | 触发 | +|---|---|---| +| 808090 / 808091 | 角色门 | 现状不变 | +| 808002 | `REQUIREMENT_NOT_FOUND` | 现状不变 | +| 808004 | `ORDER_CANCELLED` | 现状不变 | +| 808003 | `REQUIREMENT_CLAIMED_BY_SELF` | 现状不变 | +| 808001 | `REQUIREMENT_ALREADY_CLAIMED` | 现状不变 | +| **808650** | `GB_GRAB_ORDER_IS_GROUP` | **新增**:需求所属订单 `product_batch_id != null`,无条件拒,零写入 | + +#### 业务边界 + +- **改前**:对团单户 CAS 成功即逐户抢走。 +- **改后**:CAS **之前**先判团单,团单户零写入直接抛 808650;非团单行为完全不变。 +- 守卫放在 CAS 之前而非之后:即使抛错回滚,CAS 之后再判也已经真写过一次并多一次锁竞争。 + +--- + +### 8. 逐户转单(改造) `POST /v3/admin/order/hotel-requirements/{requirementId}/transfer` + +**VO**: `HouseTransferReqVO → Result`(本单不改请求/响应结构,只在既有流程中插入一道团单守卫) + +#### 使用场景 + +超管指派或房务转单,非团单行为不变;对**团期子订单**新增有条件拦截(三分支,见业务边界)。 + +#### 入参字段表 + +无变化: + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|---|---|---|---|---|---| +| `requirementId` | Path | Long | 是 | 用房需求 ID | 与改前完全一致 | +| `toUserId` | Body | Long(JSON String) | 是 | 接收人房务 ID | 与改前完全一致 | +| `reason` | Body | String | 否(超管≥10字) | ≤200 字 | 与改前完全一致 | +| `skipUpperLimit` | Body | Boolean | 否 | 历史字段,仅审计留痕 | 与改前完全一致 | + +#### 出参字段表 + +无变化: + +| 字段 | 类型 | 说明 | +|---|---|---| +| `data` | null | 成功 200,与改前完全一致 | + +#### 请求示例 + +```json +POST /v3/admin/order/hotel-requirements/7002/transfer +Authorization: Bearer + +{ "toUserId": "30002", "reason": "原房务请假,临时指派" } +``` + +#### 响应示例 + +```json +{ "code": 200, "message": "成功", "success": true, "data": null } +``` + +#### 空数据 / 降级响应 + +无(写操作),与改前一致。 + +#### 错误响应 + +```json +{ "code": 808650, "message": "团期订单不支持逐户抢单/转单,请到团期抢单池整团认领", "success": false, "data": null } +``` + +现状 808010/808011/808013/808014/808016/808001/808002/808090/808091/808930(状态机)均不变;**新增 808650**(`GB_GRAB_ORDER_IS_GROUP`)。 + +#### 业务边界 + +- **改前**:团单户可被逐户转单。 +- **改后(三分支)**: + 1. 普通房务对团单户调用 → 恒 **808650**; + 2. `SUPER_ADMIN` 对团单户调用,且该团 `order_group_batch.house_claimer_id IS NULL`(尚未被整团认领)→ **放行**(用于把存量脏数据集中到一个在职房务名下); + 3. `SUPER_ADMIN` 对团单户调用,但该团**已被整团认领** → 同样 **808650**。 +- 团期行查不到(`order_group_batch` 尚未 lazy 建行)时按「未被整团认领」处理放行——拒绝会把历史脏数据唯一的无损出口封死。 +- 判定顺序:查到 active 需求后立即判团单归属,早于持有人校验(第 3 步)与转单次数上限判定。 + +--- + +### 9. 逐户释放(改造) `POST /v3/admin/order/hotel-requirements/{requirementId}/release` + +**VO**: `HouseReleaseReqVO → Result`(本单不改请求/响应结构,只在既有守卫链后追加一道团单专属守卫) + +#### 使用场景 + +房务释放自己抢到的需求回普通抢单池;对**团期子订单**追加更严格的前置检查,防止释放后候选房与已扣库存无人能继续处理。 + +⚠️ **与工单正文「口径与定案 #6」的早期结论不同**:正文最初写「release、close:不改」,但 2026-09-08 第六轮复审后给团单户追加了本节的 808662 守卫;`close` 端点确认未改动。 + +#### 入参字段表 + +无变化: + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|---|---|---|---|---|---| +| `requirementId` | Path | Long | 是 | 用房需求 ID | 与改前完全一致 | +| `reason` | Body | String | 否 | ≤200 字 | 与改前完全一致,`@RequestBody(required=false)` | + +#### 出参字段表 + +无变化: + +| 字段 | 类型 | 说明 | +|---|---|---| +| `data` | null | 成功 200,与改前完全一致 | + +#### 请求示例 + +```json +POST /v3/admin/order/hotel-requirements/7002/release +Authorization: Bearer <认领人本人 token> + +{ "reason": "客户改期,暂时放弃" } +``` + +#### 响应示例 + +```json +{ "code": 200, "message": "成功", "success": true, "data": null } +``` + +#### 空数据 / 降级响应 + +无(写操作),与改前一致。 + +#### 错误响应 + +```json +{ "code": 808662, "message": "该团单户仍有 2 条配房(含询房中候选),请先删除配房后再释放", "success": false, "data": null } +``` + +| 码 | 符号 | 触发 | +|---|---|---| +| 808090 / 808091 | 角色门 | 现状不变 | +| 808002 | `REQUIREMENT_NOT_FOUND` | 现状不变 | +| 808020 | `RELEASE_NOT_OWNER` | 现状不变 | +| 808021 | `RELEASE_HAS_CONFIRMED_ASSIGNMENT` | 现状不变(仅数 `CONFIRMED`,全部订单通用) | +| **808662** | `GB_HOUSE_RELEASE_HAS_ACTIVE_ASSIGNMENT` | **新增,仅团期子订单**:存在 active 配房行(含 `INQUIRING` 候选,不限 `confirm_status`),零写入 | + +#### 业务边界 + +- **改前**:团单户释放判据与普通单完全一致,只看 `RELEASE_HAS_CONFIRMED_ASSIGNMENT`(808021,仅数 `CONFIRMED` 状态的配房)。 +- **改后**:普通单判据不变;**团期子订单**在既有 808021 判定之后,额外追加「active 配房行(不限 `confirm_status`,含询房中候选)不为 0 → 808662」的更严守卫。 +- **为什么团单不能沿用「仅 `CONFIRMED` 阻塞」这条既有规则**:换酒店场景会 reopen 需求、软删旧确认行、插入 `INQUIRING` 候选且库存已真实扣减,此时 `CONFIRMED` 计数为 0、808021 不拦;若放行释放,`claimer_id` 被清空后,候选房与已占库存将无法被任何角色继续处理——delete/clear/finalize 会被 `HouseClaimGuard` 以 808116 拒绝(该守卫无超管免检分支),逐户 claim 被 808650 拒绝,逐户 transfer 的 CAS 条件(`claimer` 非空且 `status='PROCESSING'`)在释放后两者都不满足,团级 `takeover` 不处理这类冻结旧户,团级 `release` 又会被同团新户的订房计划以 808660 拒绝——形成无人能处理的死结,故必须在释放前一步就拦住。 +- `close`(标记异常完成)端点本次**未改动**,仅 `release` 追加了本条守卫。 + +--- + +### 10. 团期详情(改造:响应追加三字段) `GET /v3/admin/order/group-batch/{groupBatchId}` + +**VO**: `无请求体 → GroupBatchDetailRespVO`(现有字段全部不变,仅追加 3 个只读字段) + +#### 使用场景 + +团期管理员侧团期详情页,展示「负责房务」;也是 `#7328`(房务↔团期管理员会话)取会话对端的依据。 + +#### 入参字段表 + +无变化: + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|---|---|---|---|---|---| +| `groupBatchId` | Path | Long | 是 | 团期主订单 ID | 与改前完全一致 | + +#### 出参字段表(仅列追加部分,其余既有字段本次不改动) + +| 字段 | 类型 | 说明 | +|---|---|---| +| `data.houseClaimerId` | String(Long ToString,可空) | 整团认领房务 adminId(未认领 `null`) | +| `data.houseClaimerName` | String(可空) | 整团认领房务姓名(未认领 `null`) | +| `data.houseClaimedAt` | LocalDateTime(可空) | 整团认领时间(未认领 `null`) | + +(取自 `GroupBatchDetailRespVO.java:190-204`;与 `batchManagerId`「团期管理员」是两个不同的人,全站契约写死不启用 `batchManagerId` 表示房务,不可混用。) + +#### 请求示例 + +```json +GET /v3/admin/order/group-batch/1930000000000000001 +Authorization: Bearer <团期管理员 token> +``` + +#### 响应示例 + +```json +{ + "code": 200, "message": "成功", "success": true, + "data": { + "groupBatchId": "1930000000000000001", + "houseClaimerId": "30001", "houseClaimerName": "张三", "houseClaimedAt": "2026-06-01 10:00:00" + } +} +``` + +(仅展示本次新增的三字段,既有字段(数十个)省略以节省篇幅,完整既有结构不受本次改动影响。) + +#### 空数据 / 降级响应 + +该团未被整团认领时,追加的三字段均为 `null`,其余既有字段的空数据行为不受影响。 + +#### 错误响应 + +```json +{ "code": 589500, "message": "团期不存在", "success": false, "data": null } +``` + +无变化,沿用既有 589500。 + +#### 业务边界 + +- 三字段权限现状不变(沿用团期详情既有权限),本次仅多返回三个字段,不改变任何鉴权逻辑。 + +--- + +## 四、契约约束与正确调用方式 + +### 团期抢单池写口(claim / release / takeover) + +| 场景 | 结果 | +|---|---| +| ✅ `ROOM_MANAGER`/`SUPER_ADMIN` 认领未被占用且已整体确认的团 | 200 | +| ❌ `house_keeper_lead` 调 claim/release/takeover | 808091(只读监督) | +| ❌ 非房务角色调 claim/release/takeover | 808090 | +| ❌ 普通房务调 takeover | 808658(仅超管) | +| ❌ 该团有户被别人逐户抢走时整团 claim | 808654 | +| ❌ 该团有户存在 active 逐户配房行时 claim/takeover | 808659 | +| ❌ 释放时该团仍有未取消订房计划 | 808660 | + +### 普通池与逐户 claim/transfer + +| 场景 | 结果 | +|---|---| +| 团期子订单的逐户需求行 | 不再出现在普通抢单池列表 | +| 对团期子订单逐户 claim | 808650,零写入 | +| 普通房务对团期子订单逐户 transfer | 808650 | +| 超管对**未被整团认领**的团期子订单逐户 transfer | 放行(存量脏数据集中用) | +| 超管对**已被整团认领**的团期子订单逐户 transfer | 808650 | +| 逐户 close 对团单户 | 不变 | +| 逐户 release 对团单户,且无 active 配房行 | 放行(保留作为历史脏数据清理出口) | +| 逐户 release 对团单户,但仍有 active 配房行(含 `INQUIRING` 候选) | **808662**(新增,非团单不受影响,仍是既有 808021 口径) | + +- 「我的接单」列表中历史上被逐户抢走的团单户仍会出现(脏数据可见性即清理入口,`GET /v3/admin/order/grab-pool/my-claims/hotel` 本单不改)。 +- 转单候选「在跟单数」统计(`countInProgressHotelClaimsByClaimer`)**不计**团级认领,这是已知口径差,前端展示候选人工作量时应注意。 + +### 切换状态时的必要动作 + +- 无状态机字段切换;团级三列(`house_claimer_id`/`house_claimer_name`/`house_claimed_at`)是简单 CAS 覆盖,不经状态机。 + +--- + +## 五、数据库行为 + +- `order_group_batch` 新增 3 列 + 1 索引(`V20260910_301__order_group_batch_add_house_claimer.sql`): + - `house_claimer_id BIGINT NULL`(整团认领房务 adminId,`NULL`=未认领) + - `house_claimer_name VARCHAR(64) NULL`(认领房务姓名快照) + - `house_claimed_at DATETIME NULL`(认领时间) + - `KEY idx_house_claimer_id (house_claimer_id)` + - 版本号说明:正文原规划 `V20260908_301`,因 `#7323` 的 `V20260908_311` 已先合入并在测试服执行过,Flyway 按乱序拒绝更低版本号,故实际落地为 `V20260910_301`。 +- 无新表;`house_operation_log`、`order_hotel_requirement` 本次均不写入改动。 +- 认领 / 释放 / 接管均写团期时间线(`group_batch_status_log`,事件类型 `BATCH_HOUSE_CLAIM`/`BATCH_HOUSE_RELEASE`,DATA 类),时间线写入失败仅记 WARN 日志、不回滚主事务。 +- 认领 / 释放 / 接管**不写入**任何户级表(`order_hotel_requirement`、`house_hotel_assignment` 等)——团级接管例外:会 CAS 清理**非冻结名单**户的 `order_hotel_requirement.claimer_id`(详见「三、4」业务边界)。 +- 团级写口 afterCommit 发通知中心事件(`GROUP_BATCH_HOUSE_CLAIMED`/`GROUP_BATCH_HOUSE_RELEASED`),`notification_event_config` 未配置时 dispatcher 只记日志,不影响主链路,不阻塞接口响应。 + +--- + +## 六、边界行为 + +- 0 需房户的团(全团客户自订酒店)仍会进入团期抢单池,`hotelOrderCount=0` 明示,房务据此自行跳过。 +- 团期无出发日(`depart_date` 可空)时,按出发日排序的分页里 `NULL` 行的页内位置由 Service 内存处理挪到本页末尾,不做跨页重排。 +- 认领后管理员对该团任一户打回需求,会清掉团级 `requirement_confirmed` 标记(但**不清认领指针**):「我的团」里该团 `requirementConfirmed=false`,前端应标「待管理员重新确认」;团级计划行/分房行校验本单不涉及,留给后续工单补充。 +- 「我的团」头部统计 `needsReconfirm`/`cancelled` 在 `stats.total > 500` 时返回 `null`(不是少算的数字),前端应显示「—」。 +- `house_operation_log` 与逐户「我的接单」列表不受团级认领影响,历史被逐户抢走的团单户仍在原列表可见,作为清理入口。 +- 权限服务或房务员工列表 Feign(团级接管的接管人姓名解析)异常时按兜底名 `user-{id}` 处理并跳过在职校验;接收人列表非空但查无该人时才会明确拒 808011。 +- **极窄边界场景(不属于本篇 10 个端点,供排查参考)**:冻结名单内的历史户(首次启用团期归属时已有确认配房)被 `release` 清空房务归属后,若定制师对该户提交新版本用房需求(既有的 `upsertHotelRequirement` 端点,走「已抢/已配房后的需求调整」分支),后端会 fail-closed 拒绝并返 **808661**(`GB_HOUSE_LEGACY_RESUBMIT_NO_OWNER`),提示需先清空候选配房再重提,避免出现「旧房仍占库存却无人能处理」。该端点请求/响应契约本身未变,仅新增这一条极窄场景下的业务错误。 + +--- + +## 六.5、枚举 / 数据字典 + +### 错误码新增清单(`HouseGroupBatchErrorCode.java`,段位 808600-808699 内本单占用 808650-808662) + +| 码 | 符号 | 消息 | +|---|---|---| +| 808650 | `GB_GRAB_ORDER_IS_GROUP` | 团期订单不支持逐户抢单/转单,请到团期抢单池整团认领 | +| 808651 | `GB_GRAB_BATCH_NOT_CLAIMABLE` | 该团期当前不可认领(需求未整体确认或团期阶段不允许) | +| 808652 | `GB_GRAB_BATCH_ALREADY_CLAIMED` | 该团期已被其他房务认领 | +| 808653 | `GB_GRAB_BATCH_CLAIMED_BY_SELF` | 该团期已由您认领,请勿重复认领 | +| 808654 | `GB_GRAB_HOUSEHOLD_CLAIMED_BY_OTHERS` | 该团有 {0} 户已被房务 {1} 逐户抢单,请先释放后再整团认领 | +| 808655 | `GB_GRAB_RELEASE_NOT_OWNER` | 该团期不属于当前房务,无法释放 | +| 808656 | `GB_GRAB_BATCH_NOT_CLAIMED` | 该团期尚未被认领,无需释放 | +| 808657 | `GB_GRAB_ADMIN_RELEASE_REASON_TOO_SHORT` | 超管操作原因长度不足 10 字(释放/接管共用) | +| 808658 | `GB_GRAB_TAKEOVER_NOT_SUPER_ADMIN` | 团期接管仅超级管理员可操作 | +| 808659 | `GB_GRAB_TAKEOVER_HAS_HOUSE_ASSIGNMENT` | 该团有 {0} 户存在逐户配房(首个订单 {1}),请先删除配房后再操作(claim 与 takeover 共用) | +| 808660 | `GB_HOUSE_RELEASE_HAS_ROOM_PLAN` | 该团仍有 {0} 条未取消的订房计划,无法释放(请先处理订房计划或走接管) | +| 808661 | `GB_HOUSE_LEGACY_RESUBMIT_NO_OWNER` | 该户历史配房仍在但已无房务归属,请先由房务或超管清空候选配房后再重提需求——触发于**既有**用房需求提交端点(`RequirementService.upsertHotelRequirement`,非本篇新增/改造的 10 个端点之一),场景是冻结名单内的历史户被 release 后房务归属清空、定制师又对其重提新版本需求,`RequirementService.java:895-904` | +| 808662 | `GB_HOUSE_RELEASE_HAS_ACTIVE_ASSIGNMENT` | 该团单户仍有 {0} 条配房(含询房中候选),请先删除配房后再释放——触发于本篇「### 9. 逐户释放(改造)」 | + +(取自 `HouseGroupBatchErrorCode.java:72-150`。⚠️ 与工单正文「口径与定案 #6」早期表述不同——808661/808662 均已在**本次合并的 PR #7436** 内实际接线并可触发,不是预留给后续工单的空常量:808662 挂在本篇改造的逐户释放端点上;808661 挂在既有的用房需求提交端点(未改请求/响应契约,只是新增了一条极窄场景下的业务错误,未纳入本篇「三、接口详情」的 10 个端点,因为该端点本身不属于房务抢单池体系、且触发条件极窄——仅冻结名单内旧户被 release 后又被重提新版本需求时命中)。) + +### `batchStatus`(团期抢单池筛选,`HouseGroupGrabPoolPageReqVO.batchStatus`) + +| 值 | 含义 | +|---|---| +| `RESOURCE_PREPARING` | 资源准备中 | +| `MATERIAL_PREPARING` | 材料准备中 | +| `PENDING_DEPARTURE` | 待出发 | +| `TRAVELLING` | 行程中 | + +(仅这四态可入池;「我的团」的 `batchStatus` 筛选接受团期完整九态,因为已认领的团可能已流团/已结束。) + +--- + +## 七、不影响范围 + +- 「我的接单」(逐户)、待办、日历、详情、组长监督视图等既有房务工作台功能**不变**——团级认领不回写户级,这些视图读的都是户级数据。 +- 团期需求确认/打回(`group-batch:demand:confirm`)不受影响,仍是团进池的唯一触发条件之一。 +- 逐户 `close` 端点**不改**。逐户 `release` 端点对**非团单**行为完全不改;对团单户继续可用(作为历史脏数据清理出口),但新增了 808662 更严守卫,见「三、9.」——这一点与工单正文早期「release 不改」的表述不同,以本篇为准。 +- 「我的接单」`GET /v3/admin/order/grab-pool/my-claims/hotel` 不改。 +- 房务日历不展示抢单池订单,本单排除条件不影响日历。 +- 无 SSE 广播改动:团期池变更不推送实时事件,前端仍需沿用既有的普通池 SSE(不受影响)或手动/定时刷新团期池。 +- CODE_RULES 不改;网关路由零改动(`/v3/admin/order/**` 已通配)。 + +--- + +## 八、测试环境已验证 + +**当前状态:代码已合入 `dev-v3`(HEAD `353f4b2d6`),测试服部署与网关实测待管理者安排,部署后回写本节与 frontmatter。** + +已完成的验证(单测/编译层面,不等价于网关实测): + +- 单测覆盖(`HouseGroupGrabServiceTest`,节选,完整清单见工单 #7322「测试要求」):认领成功写时间线、CAS 落空按持有人分类返 808652/808653/808651、脏数据户返 808654 且零写入、未登录返 808090、组长返 808091、释放非本人/未认领/超管理由过短分别返 808655/808656/808657、`scope=all` 非组长返 808092。 +- `HouseGrabServiceImplTest` 追加:团单户逐户 claim 返 808650 且零 CAS、团单户逐户 transfer 返 808650、团单户逐户 release 在无 active 配房行时仍放行、有 active 配房行(含 `INQUIRING` 候选)时返 808662。 +- IT(Testcontainers/H2):`selectGrabPoolPageWithJoin` 排除团单户后 `total` 与真实剩余行数一致;`GroupBatchMapper` 新增 default 方法(`selectHouseGrabPoolPage`/`selectHouseClaimedPage`/`casHouseClaim`/`casHouseRelease`/`casHouseTakeover`)覆盖四态过滤、并发 CAS 落空场景。 +- ArchTest:`HouseModuleBoundaryArchTest`、`MapperBoundaryArchTest`、`RedLineArchTest`、`HouseErrorCodeRangeTest`(新增段位 808650-808662 全部登记)均绿。 + +**待补(部署后由管理者执行并回填)**:网关实测 5 个新端点 + 4 个改造端点的全部错误码路径、Flyway 执行结果(`SHOW COLUMNS`/`SHOW INDEX` 核对)、角色矩阵、菜单种子口径确认结果(见「十、相关文档」)。 + +--- + +## 十、相关文档 + +- 团期看板权限地基 #6902、团期需求确认/打回 #7210:本单角色门不复用这两处的平台权限码体系,走既有的 `HouseWriteGuard` 房务角色门。 +- 团期房务批次后续工单:`#7323`(订房地基/扣减来源分支)、`#7324`(房务团期看板/订房计划 CRUD,读本单 `GroupBatchDetailRespVO` 新三字段、提供 `countActivePlans` 供本单 808660 使用)、`#7325`/`#7326`(按日确认与自动分房、分房微调)、`#7327`(双源巡检契约)、`#7328`(房务↔团期管理员会话,参与人取本单 `house_claimer_id`)。 +- 旧拟议契约 `docs/group/团期模块接口文档-v2.0.html` GB-ADM-016(`POST /v3/admin/order/group-batch/{groupBatchId}/hotel-requirements/claim`,逐户批量薄编排)**已被本单替代**,不采用该旧契约。 +- **待确认(口径与定案 #11)**:房务管家菜单是否需要 `sys_menu` 种子行(hl-user-service Flyway),需连测试库核实后由管理者补充,不影响本篇端点契约本身。 + +--- + +## 关联 / 联系人 + +### 链接 + +- **Issue**: [#7322](https://git.1814.love:8443/wx/HL/issues/7322) +- **PR**: [#7436](https://git.1814.love:8443/wx/HL/pulls/7436)(合并提交 [ab4118a01](https://git.1814.love:8443/wx/HL/commit/ab4118a0171927f4409e8de23925b1c5443e21ae)) + +### 联系人 + +- **后端负责人**: wx +- **待确认对象(前端 hl-ui)**: mmg——房务管家需新增「抢单池·团期」「我的团」两个页面(含团级接管的超管专属入口);普通抢单池「产品类型」筛选下拉建议隐藏 `GROUP` 选项;团期详情页展示「负责房务」三字段;无 SSE,页面需手动或定时刷新;`sys_menu` 菜单种子的 `path`/`component` 命名请回复后由管理者补 Flyway