--- schema: "hl-changelog/v2" ticket: "7326" title: "团期分房重算/人工微调:重判后不平的已完成户退回处理中,时间线新增 reopenedOrderIds" consumer: "admin" author: "wx(GIT)" change_type: "修改接口" backend_status: "deployed" gateway_status: "not_required" frontend_status: "verified" frontend_owner: "mmg" frontend_ref: "917b1848abed2657ac4aea5a3f88c87b6b36e4d3" target_release: "" verified_at: "2026-09-15" status_note: "PR #7689(Issue #7326 AC-8)已 squash 合并 dev-v3(合并提交 b57413ab8)。2026-09-15 代码已确认部署在测试服(deploy-status.sh 实测),并对 GET status-logs 做了真实网关取证,证实 extra.reopenedOrderIds 字段真实存在(恒为数组)。backend_status 记 deployed:代码已部署且字段契约已实测。⚠️ 如实标注未覆盖范围:取证样本恰好都是空数组,未采到非空(真的退回)场景——该场景目前只有单测覆盖,非测试服端到端验证,详见正文「八」节。gateway_status=not_required:本次涉及的三个端点均为已有路由(/v3/admin/house/group-batches/**、/v3/admin/order/group-batch/**),未新增/修改任何路径或方法。frontend_status=pending:待前端确认「人工微调/重算分房提交后,之前已标记完成的户可能被静默退回处理中」这件事是否需要在结果弹窗提示,或在待配房列表/订单详情页对这类回退做特殊展示,故不定为 not_required。 前端已闭环(917b1848):BoardDetailModal onAllocSaved 在 hotelReady=false 时补查 status-logs 取最新 H10/H11 记录 extra.reopenedOrderIds,非空则结果弹窗点名退回户数并升 warning,补查失败不阻塞;StatusLogsPanel extraText 补「退回处理中 N 户」展示(空数组/历史缺键兜住);spec 共 +4 例。" updated_at: "2026-09-15" base: "dev-v3" --- # 团期房务: 分房重算/人工微调把重判不平的已完成户退回处理中 > **存放目录**: > - 一期(v2,无 `order-v3` 标签的工单)→ `changelogs/{YYYY-MM}/` > - 二期(v3,`order-v3` 标签的工单)→ `changelogs-v2/{YYYY-MM}/` > > **服务**: hl-order-service-v3 (端口 8086) > **PR**: [#7689](https://git.1814.love:8443/wx/HL/pulls/7689)(Issue #7326 AC-8) > **Issue**: #7326 > **日期**: 2026-09-15 > **影响范围**: 管理后台团期「人工微调分房」「重算分房」两个写口新增的副作用(已完成户可能被退回处理中);团期状态流水(时间线)接口的 `extra` 新增 `reopenedOrderIds` 字段 --- ## ⚠️ 关键变化(非必须,本版与上版行为不同 / 纠错 / 撤销时必写) - **本次变了什么**:H10(人工微调分房,`POST /v3/admin/house/group-batches/{groupBatchId}/allocations`)与 H11(重算分房,`POST .../allocations/rebuild`)在判定「团级配房完成标志判不出就绪、需要置回 false」这一分支时(`resetWhenNotReady=true`),新增了一个户级动作:把「此前已经是住宿已完成(`house_status=CONFIRMED` 且需求 `status=DONE`),但按最新分房重判后又不平」的户,CAS 退回处理中——需求 `status` 从 `DONE` 回退到 `PROCESSING`,`house_status` 改写为 `PENDING_CLAIM`,订单 `flow_status` 尝试从 `PENDING_CONFIRM` CAS 到 `RESOURCE_PREPARING`,并重新同步一次该订单的房务待办(`GroupBatchRoomDayConfirmManager.java:707-744`、`:833-843`;`GroupBatchRoomLifecycleManager.java:547-559`)。 - **前端/调用方以前以为的是什么**:H10/H11 只会影响「团级」的 `hotelReady` 之类聚合标志;某个户一旦被标记为「住宿已完成」,除非房务在这户自己的详情页手动操作,否则它的状态不会被别的团级操作(人工微调另一天、或重算另一晚)连带改动。 - **实际现在是什么**:只要本次 H10/H11 调用判不出团级就绪(`resetWhenNotReady=true` 分支被触发),凡是重判后不平的已完成户都会被一并静默退回处理中;H10/H11 自身的直接响应体(`GroupBatchRoomAllocationRebuildRespVO`)**不携带**这批被退回的户名单——调用方必须另外读团期状态流水接口(`GET /v3/admin/order/group-batch/{groupBatchId}/status-logs`),在 `eventType=BATCH_ROOM_ALLOC_MANUAL`/`BATCH_ROOM_ALLOC_REBUILD` 的记录里读 `extra.reopenedOrderIds` 才能知道具体是哪些子订单(`GroupBatchRoomAllocationManager.java:250-258`、`:329-338`)。不读时间线,前端只能看到该户重新出现在待配房列表/订单详情的房务进度条回退到「待配房」,却不知道原因。 --- ## 一、背景(选填) Issue #7326 AC-8:H10/H11 共用的 `GroupBatchRoomDayConfirmManager#settleHouseholdsAndHotelReady` 此前只做团级 `resetHotelReady`(把团级 `hotelReady` 置回 false),户级的 `DONE` + `house_status=CONFIRMED` 原样留着不动(`GroupBatchRoomDayConfirmManager.java:728`,2026-09-15 对照 origin/dev-v3 核实更新,2026-09-14 起草时的 708-712 因中间插入 #7700/#7459 等提交已漂移约 +20 行;javadoc 原话:「同一个假信号的户级版本」)。这会导致:核单侧依赖 `house_status=CONFIRMED`/需求 `status=DONE` 的户级闸门继续放行,待办也已经从房务列表里消失,但这户实际上已经因为重算/人工微调而少了房,两边状态不一致。 本次改动让 `resetWhenNotReady=true` 时,团级 `resetHotelReady` 与户级 `reopenUnbalancedHouseholds` 两个反向动作成对执行(同一个开关,不新增第二个开关;理由见 `GroupBatchRoomDayConfirmManager.java:713-716` javadoc:拆成两个参数就是同一条判据两份实现,容易两条链路各改各的、对「这个团安排好了没有」给出矛盾答案)。 H7/H8(另外两个会触发 `settleHouseholdsAndHotelReady` 的入口,见 `GroupBatchRoomDayConfirmManager.java:233`、`:346`)传的固定是 `resetWhenNotReady=false`——它们只会让事实变好(确认计划行、扣库存),不会触发户级回退,本次改动**不影响** H7/H8 这两个入口的行为。 --- ## 二、变更接口清单 | # | 接口 | 方法 | 路径 | 变更类型 | 说明 | |---|------|------|------|----------|------| | 1 | 人工微调分房 | POST | `/v3/admin/house/group-batches/{groupBatchId}/allocations` | 副作用扩大 | 判不出团级就绪时,重判后不平的已完成户被 CAS 退回处理中;时间线 `extra` 新增 `reopenedOrderIds` | | 2 | 重算分房 | POST | `/v3/admin/house/group-batches/{groupBatchId}/allocations/rebuild` | 副作用扩大 | 同上 | | 3 | 团期状态流水(时间线) | GET | `/v3/admin/order/group-batch/{groupBatchId}/status-logs` | 响应内容新增字段 | `eventType=BATCH_ROOM_ALLOC_MANUAL`/`BATCH_ROOM_ALLOC_REBUILD` 两类记录的 `extra` 新增 `reopenedOrderIds`(数组,恒不为 null,可为空数组) | --- ## 三、接口详情 三个端点的请求参数、错误码均未变化;H10/H11 的直接响应体字段也未变化,只是多了一个不反映在响应体里的副作用;端点 3 的响应体新增一个 `extra` 子字段。以下逐接口自包含描述。 ### 1. 人工微调分房 `POST /v3/admin/house/group-batches/{groupBatchId}/allocations` **VO**: `GroupBatchRoomAllocationSaveReqVO → Result` #### 使用场景 房务在「团期分房」页面对某几条已确认计划行提交人工分房覆盖时调用(H10)。本次改动后,若提交这次微调导致判不出团级就绪,且团内存在此前已标记「住宿已完成」但按新分房结果不平的户,这些户会被自动退回处理中——这是本接口触发的副作用,不在直接响应体里体现,前端需要另外查询时间线(见本文件端点 3)或该户的订单详情才能感知。 #### 入参 | 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | |------|------|------|------|------|------| | items | Body | Array\ | ❌ | 与 clearPlanIds 至少一个非空;最多 500 行 | 覆盖后的人工分房行集合(本次未改) | | items[].planId | Body | String(雪花 ID) | ✅ | 须属本团、未软删且已确认 | 订房计划行 ID | | items[].orderId | Body | String(雪花 ID) | ✅ | 须为本团在团子订单 | 分给哪一户 | | items[].roomCount | Body | Integer | ✅ | 1-99 | 占用间数 | | items[].roomGroupNo | Body | String | ❌ | 形如 F1~F999,不填按 F1 | 家庭分组号 | | items[].travelerCount | Body | Integer | ❌ | 1-9 | 该房入住人数 | | items[].bedType | Body | String | ❌ | single/double/twin/family,非法 808131 | 床型 code | | items[].remark | Body | String | ❌ | ≤256 字 | 备注 | | clearPlanIds | Body | Array\(雪花 ID) | ❌ | 与 items 至少一个非空;最多 200 条 | 这些计划行下的人工分房全部软删(回到纯自动分房) | (以上入参字段结构本次未改,见 `GroupBatchRoomAllocationSaveReqVO.java`、`GroupBatchRoomAllocationItemReqVO.java`) #### 出参 `Result` | 字段 | 类型 | 说明 | |------|------|------| | data.groupBatchId | String(雪花 ID) | 团期主订单 ID | | data.force | Boolean | 本次是否重置了人工分房(H10 恒 false) | | data.balanced | Boolean | 本次范围内所有日都对平、且无过时户、无阻塞户 | | data.hotelReady | Boolean | 后置判定之后团期的配房完成标志;**本次起:若有已完成户被退回处理中,通常伴随此值为 false** | | data.days[] | Array\ | 本次处理过的入住日结果(`leftover`/`shortage`/`manualConflicts`/`outOfRange` 等,字段结构本次未改) | | data.skippedDays[] | Array\ | 未处理的入住日(计划行未确认) | | data.staleOrderIdsBefore[] | Array\ | 本次开始前判定为分房过时的户 | | data.replacedAllocIds[] | Array\ | 本次被软删的分房行 ID | | data.warnings[] | Array\ | 户完成联动与团级判定过程中产生的告警 | **⚠️ 本次改动不在这里新增字段**:被退回处理中的户 ID 列表**不在** `GroupBatchRoomAllocationRebuildRespVO` 里,只能从本文件端点 3(时间线)的 `extra.reopenedOrderIds` 读到。 #### 请求示例 ```json { "items": [ { "planId": "880101", "orderId": "70001", "roomCount": 1, "roomGroupNo": "F1" } ], "clearPlanIds": [] } ``` #### 响应示例 以下取自 `GroupBatchRoomAllocationManagerTest#saveManual_householdsReopened_writesThemIntoTimeline` 测试夹具(团期 `groupBatchId=9001`,提交人工分房时触发户 `orderId=70002` 被判不平并退回),不是测试服抓包报文;响应体本身的字段结构与本次改动前逐字节相同: ```json { "code": 200, "message": "成功", "data": { "groupBatchId": "9001", "force": false, "balanced": false, "hotelReady": false, "days": [], "skippedDays": [], "staleOrderIdsBefore": [], "replacedAllocIds": [], "warnings": [] }, "success": true } ``` #### 空数据 / 降级响应 `items`/`clearPlanIds` 均传空数组时按 100001 拒绝(既有校验,本次未改,见「四、契约约束」);本接口不存在「有效提交但返回空数据」的形态——只要通过校验,`days` 至少包含受影响的入住日。 #### 错误响应 计划行未确认或不属本团(既有错误码,本次未改): ```json { "code": 808601, "message": "计划行不存在或未确认", "data": null, "success": false } ``` 受影响户存在过时分房时整单拒绝(既有错误码,本次未改): ```json { "code": 808643, "message": "存在分房过时的户,请先重算", "data": null, "success": false } ``` #### 业务边界 - **鉴权**:`@HouseWriteGuarded`,非房务角色在幂等/锁窗口之前即被拦截;团期须处于可配房阶段,否则 `assertConfirmableStage` 拒绝(既有逻辑,本次未改)。 - **团期级锁**:`@Lock4j`(`house-groupbatch-lock`),同团期并发提交串行化,本次未改。 - **新增副作用只在 `resetWhenNotReady=true` 分支触发**:本端点内部固定传 `true`(`GroupBatchRoomAllocationManager.java:392` 起的 `settleAndFinish` 私有方法),即 H10 每次调用都参与户级回退判定,不是可选行为,前端不能假设「只微调了没关系的一天就不会影响别的已完成户」。 - **只退真的从 DONE 被 CAS 掉的户**:若某户虽然「不平」但此前并不是完成态(`status≠DONE`),CAS 直接返回 0 行,`reopenedOrderIds` 不计入它,也不会有任何写操作发生在它身上(`GroupBatchRoomLifecycleManager.java:601-613` `reopenHousehold` 方法体,2026-09-15 核实更新,2026-09-14 起草时的 551-553 已漂移约 +50 行)。 - **全自订户不受影响**:全自订户走的是 `completeSelfBookedHouseholds` 独立判定路径,不进入 `evaluateHouseholds` 的返回集合,因而结构性地碰不到本次新增的退回逻辑(`GroupBatchRoomDayConfirmManager.java:826-829`)。 --- ### 2. 重算分房 `POST /v3/admin/house/group-batches/{groupBatchId}/allocations/rebuild` **VO**: `GroupBatchRoomAllocationRebuildReqVO → Result` #### 使用场景 团期管理员重新确认需求后,房务在「团期分房」页面点击「重算分房」调用(H11)。可指定 `stayDate` 只重算某一天,或不传重算整团全部已确认日;`force=true` 会先把范围内的人工分房整片软删。本次改动后,重算若导致某个已完成户不再平,同样会被自动退回处理中。 #### 入参 | 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | |------|------|------|------|------|------| | force | Body | Boolean | ❌ | 默认 false | 是否先重置全部人工分房 | | stayDate | Body | String(日期,`yyyy-MM-dd`) | ❌ | 不传 = 整团全部已确认日 | 只重算某一入住日 | | reason | Body | String | `force=true` 时 ✅ | ≤256 字 | 重置原因,写进团级时间线 | (以上入参字段结构本次未改,见 `GroupBatchRoomAllocationRebuildReqVO.java`) #### 出参 `Result` 字段结构与端点 1 完全相同(两端点共用同一个 `GroupBatchRoomAllocationRebuildRespVO`),差异只在于 `force`/`days` 的取值来源不同;`reopenedOrderIds` 同样不出现在这里。 | 字段 | 类型 | 说明 | |------|------|------| | data.groupBatchId | String(雪花 ID) | 团期主订单 ID | | data.force | Boolean | 本次是否重置了人工分房(随请求 `force` 回显) | | data.balanced | Boolean | 本次范围内所有日都对平、且无过时户、无阻塞户 | | data.hotelReady | Boolean | 后置判定之后团期的配房完成标志;**本次起:若有已完成户被退回处理中,通常伴随此值为 false** | | data.days[] | Array\ | 本次处理过的入住日结果(字段结构本次未改) | | data.skippedDays[] | Array\ | 未处理的入住日(计划行未确认) | | data.staleOrderIdsBefore[] | Array\ | 本次开始前判定为分房过时的户 | | data.replacedAllocIds[] | Array\ | 本次被软删的分房行 ID | | data.warnings[] | Array\ | 户完成联动与团级判定过程中产生的告警 | #### 请求示例 ```json { "force": false, "stayDate": "2026-06-12" } ``` #### 响应示例 以下取自 `GroupBatchRoomAllocationManagerTest#rebuild_householdsReopened_writesThemIntoTimeline` 测试夹具(团期 `groupBatchId=9001`,重算导致户 `orderId=70002` 判不平并退回),不是测试服抓包报文: ```json { "code": 200, "message": "成功", "data": { "groupBatchId": "9001", "force": false, "balanced": false, "hotelReady": false, "days": [], "skippedDays": [], "staleOrderIdsBefore": [], "replacedAllocIds": [], "warnings": [] }, "success": true } ``` #### 空数据 / 降级响应 范围内无已确认计划行时拒绝(既有错误码 808644,本次未改);未确认的日进 `skippedDays`,不算错误。 #### 错误响应 范围内无已确认计划行(既有错误码,本次未改): ```json { "code": 808644, "message": "范围内无已确认计划行,无法重算", "data": null, "success": false } ``` `force=true` 但未填 `reason`(既有校验,本次未改): ```json { "code": 100001, "message": "reason 不能为空", "data": null, "success": false } ``` #### 业务边界 - **鉴权/锁**:同端点 1。 - **新增副作用同样固定触发**:本端点内部同样固定传 `resetWhenNotReady=true`(`GroupBatchRoomAllocationManager.java:329` 起),`force=false`(默认档,只重跑自动分房、保留人工分房)与 `force=true`(先软删人工分房再重来)两档都会触发户级回退判定,不因 `force` 取值而豁免。 - **`stayDate` 只影响本次重算的范围,不影响户级回退的判定范围**:`reopenUnbalancedHouseholds` 对全团重新判定平衡(`evaluateHouseholds` 判的是每户全部基线晚,不按 `stayDate` 收窄),因此即使只重算了某一天,如果这户在**另一天**早就不平,本次也会一并把它退回处理中(`GroupBatchRoomDayConfirmManager.java:821-823` javadoc)。 - **只退真的从 DONE 被 CAS 掉的户**:同端点 1。 --- ### 3. 团期状态流水(时间线) `GET /v3/admin/order/group-batch/{groupBatchId}/status-logs` **VO**: `Result>`(无请求体,仅路径参数) #### 使用场景 管理后台团期详情页查看操作历史/时间线时调用,按 `changedAt` 升序返回该团期全部流水,不分页。本次起,H10/H11 触发的 `BATCH_ROOM_ALLOC_MANUAL`/`BATCH_ROOM_ALLOC_REBUILD` 两类记录的 `extra` 多一个 `reopenedOrderIds` 字段,前端可用它在时间线条目上提示「本次操作把 N 户退回了处理中」。 #### 入参 | 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | |------|------|------|------|------|------| | groupBatchId | Path | Long | ✅ | - | 团期主订单 ID | #### 出参 `Result>` | 字段 | 类型 | 说明 | |------|------|------| | data[].logId | String(雪花 ID) | 流水 ID | | data[].groupBatchId | String(雪花 ID) | 团期 ID | | data[].changeType | String | `STATUS`/`DATA`;本次涉及的两类事件恒为 `DATA` | | data[].eventType | String | 事件类型值,本次涉及 `BATCH_ROOM_ALLOC_MANUAL`/`BATCH_ROOM_ALLOC_REBUILD` | | data[].eventTypeName | String | 事件类型中文标签,如「房务人工微调分房」「房务重算分房」 | | data[].content | String | 展示文本,如「房务人工微调分房(1 行,涉及 1 天)」 | | data[].extra | Object/null | 附加快照(已从 JSON 字符串解析为对象);**本次起,`BATCH_ROOM_ALLOC_MANUAL`/`BATCH_ROOM_ALLOC_REBUILD` 两类记录新增 `extra.reopenedOrderIds` 字段** | | data[].extra.reopenedOrderIds | Array\ | **新增字段**。本次操作真被 CAS 退回处理中的子订单 ID 数组;恒为数组(不会是 null),CAS 未命中任何户时为 `[]` | | data[].changedAt | String(时间) | 变更时间 | (`operatorType`/`operatorId`/`operatorName`/`reason` 等既有字段本次未改,未逐一列出) #### 请求示例 ```http GET /v3/admin/order/group-batch/9001/status-logs Authorization: Bearer ``` #### 响应示例 以下取自 `GroupBatchRoomAllocationManagerTest#rebuild_householdsReopened_writesThemIntoTimeline` 测试夹具中对 `extra` 的断言(该用例只断言 `extra` map 的内容,未经过完整的 `GroupBatchStatusLogItemVO` 序列化链路;`logId`/`changedAt` 等字段为示意值,不是取自该用例): ```json { "code": 200, "message": "成功", "data": [ { "logId": "90501", "groupBatchId": "9001", "changeType": "DATA", "eventType": "BATCH_ROOM_ALLOC_REBUILD", "eventTypeName": "房务重算分房", "fromStatus": "RESOURCE_PREPARING", "fromStatusName": "资源准备中", "toStatus": "RESOURCE_PREPARING", "toStatusName": "资源准备中", "content": "房务重算分房(1 天,force=false)", "reason": null, "operatorType": "ADMIN", "operatorId": "1001", "operatorName": "小呼", "extra": { "force": false, "reason": null, "stayDate": "2026-06-12", "unbalancedDays": ["2026-06-12"], "manualReset": 0, "reopenedOrderIds": [70002] }, "changedAt": "2026-09-14 10:21:33" } ], "success": true } ``` 未触发任何户级回退时,同一 `extra` 里 `reopenedOrderIds` 为空数组(取自 `rebuild_manualExceedsNewDemand_keepsManualAndReportsConflict` 等既有用例默认桩 `SettleResult(List.of(), List.of(), false)`): ```json { "extra": { "reopenedOrderIds": [] } } ``` #### 空数据 / 降级响应 该团期从未发生过任何生命周期事件(理论场景,团期一旦创建即有 `BATCH_GROUP` 建团记录)时返回空数组: ```json { "code": 200, "message": "成功", "data": [], "success": true } ``` `extra` 解析失败或该条记录本身无附加数据时,`extra` 为 `null`,不是空对象(既有降级规则,本次未改,见 `GroupBatchStatusLogItemVO.java:17-19` javadoc)。 #### 错误响应 `groupBatchId` 指向不存在的团期(既有错误码,本次未改): ```json { "code": 808001, "message": "团期不存在", "data": null, "success": false } ``` #### 业务边界 - **鉴权**:走网关统一鉴权,未登录 401;本端点不带额外角色限制(与改动前一致)。 - **`reopenedOrderIds` 只对 `BATCH_ROOM_ALLOC_MANUAL`/`BATCH_ROOM_ALLOC_REBUILD` 两类事件出现**:其余事件类型(如 `BATCH_GROUP`、`BATCH_HOUSE_CLAIM`)的 `extra` 结构不受本次改动影响。 - **该字段只反映「CAS 真命中」的户**:调用方不应据此反推「团内还有多少户不平」——不平但本来就不是完成态的户不会出现在这里(不属于「退回」,因为它们从未处于「已完成」态)。 - **写入是尽力而为、不阻断主链路**:时间线写入走 `recordHouseTimelineQuietly` 降级包装,DB 类异常之外的失败只记日志不抛出;但注意它与主事务共用一个事务边界,DB 类异常仍会导致整个 H10/H11 调用连带回滚(`GroupBatchService.java:346-360`(origin 行号)javadoc 明确写了这条边界)。 - **只读**:本端点不提供任何写口。 --- ## 四、契约约束与正确调用方式(接口类必写) > 本节只写**后端接受/拒绝 payload 的规则与调用后必须做的动作**,不写 UI 渲染建议。 ### ✅ 正确 / ❌ 错误 调用方式对照 | 场景 | 说明 | |------|------| | ✅ 提交 H10/H11 后,若 `hotelReady=false`,前端另外查一次时间线确认是否有户被退回 | `reopenedOrderIds` 只在时间线里,响应体的 `hotelReady=false` 只说明团级判不出就绪,不说明是不是有已完成户被动了状态 | | ✅ 展示某户的房务进度条前,以 `GET /admin/house/orders/{orderId}` 返回的 `progress.houseStatus` 为准 | 该户是否被退回处理中,最终权威落在这个字段上,不要用前端本地缓存的「已完成」状态继续渲染 | | ❌ 认为「H10/H11 返回 200 且响应体没有报错字段」就等于「除了本次显式提交的行,其他户状态不受影响」 | 已完成户被退回是**静默副作用**,不在响应体里报告,200 不代表没有连带影响 | | ❌ 把 `extra.reopenedOrderIds` 缺失当成「没有户被退回」 | 该字段本次才新增;改动前产生的历史时间线记录(`changedAt` 早于本次上线)不会有这个字段,前端按字段缺失兜底为「未知」而不是「一定为空」 | ### 切换状态时的必要动作 被退回处理中的户,其需求会重新出现在房务「待配房」相关列表/抢单池、待办也会重新生成(`orderTodoSyncContract.syncForOrder(orderId, SYNC_REASON_REOPEN)`,实现在 `GroupBatchRoomLifecycleManager.java:611`,2026-09-15 核实更新;⚠️ 2026-09-14 起草时把这个内部原因常量的字面值错写成了 `"REOPEN"`,源码实际值是 `"group room plan revised"`——该值只用于待办同步的内部 reason 参数,不出现在任何对外响应字段里,故本节此前的错误不影响契约本身,只更正引用准确性)。前端若维护了「已完成」本地状态缓存或已勾选/已处理的 UI 状态,收到时间线里的 `reopenedOrderIds` 非空后应主动刷新这些户的展示,不要依赖用户手动刷新页面。 --- ## 五、数据库行为(涉及写操作时必写) | 前置条件 | `order_hotel_requirement.status` 列 | `order_hotel_requirement.house_status` 列 | 子订单 `order_main.flow_status` 列 | |----------|--------------------------------------|---------------------------------------------|--------------------------------------| | 该户当前 `status=DONE`,重判后不平,CAS 命中 | `DONE` → `PROCESSING`(CAS,仅当前恰为 `DONE` 时生效) | → `PENDING_CLAIM` | 仅当前恰为 `PENDING_CONFIRM` 时 CAS → `RESOURCE_PREPARING`,否则原样不动(CAS 结果不影响 `reopenedOrderIds` 是否计入该户) | | 该户当前 `status≠DONE`(已经不是完成态) | 不变(CAS 0 行) | 不变 | 不变 | | 该户重判后仍平(`balanced=true`) | 不变 | 不变 | 不变 | **幂等/并发**:`reopenHousehold` 的第一条语句就是 `requirement.status` 的 DONE-only CAS,非完成态或已被并发操作改动的户会在这一步返回 0 行,后续 `house_status`/`flow_status`/待办同步三个写操作都不会执行(`RequirementService.java:3560-3566`(origin 行号,未在本轮复核);`GroupBatchRoomLifecycleManager.java:601-613`,2026-09-15 核实更新)。 --- ## 六、边界行为 - 未登录 → 401(网关拦截) - 团期不存在 → 808001(端点 3)/ 已有错误码(端点 1/2 建团期不存在时的行为本次未改) - 计划行未确认/不属本团 → 808601(既有,未改) - 存在过时分房 → 808643(既有,未改) - 范围内无已确认计划行 → 808644(既有,未改) - 户重判后不平但本来就不是完成态 → 不产生任何写操作,不出现在 `reopenedOrderIds` 里,不是错误 - 全自订户 → 结构性不参与本次新增的判定,不受影响 - 时间线写入失败(非 DB 类异常)→ 降级为只记日志,H10/H11 主链路不受影响;DB 类异常仍会导致整次调用回滚 --- ## 六.5、枚举 / 数据字典(接口出现枚举时必写) 以下枚举不是本文件端点 1/2/3 直接返回的字段,但是理解「已完成户被退回处理中」这一行为对页面展示影响的必要背景——它们出现在**另一个未被本次改动的既有端点** `GET /admin/house/orders/{orderId}`(房务侧订单详情聚合,§2.0)的 `data.itinerary.progress.houseStatus` 字段上;本次改动只是让部分订单的这个字段值出现回退,字段本身与端点契约均未变。 ### houseStatus(`com.hulalv.house.statemachine.HouseStateEnum`) **所属字段**: `HouseOrderDetailRespVO.Itinerary.Progress.houseStatus`(`GET /admin/house/orders/{orderId}`,非本次变更接口) | **类型**: `String` | 值 | 中文 | 说明 | |----|------|------| | `PENDING_CLAIM` | 待配房 | **本次改动后,被退回处理中的户会落到这个值**(`HouseStateEnum.java:34`) | | `CLAIMING` | 抢单中 | 本次未涉及 | | `PENDING_FINALIZE` | 待核实 | 本次未涉及 | | `CONFIRMED` | 已完成 | **被退回前的原值**(`HouseStateEnum.java:48`) | | `EXCEPTION` | 异常 | 本次未涉及 | ### flow_status(`com.hulalv.order.core.enums.OrderFlowStatus`,节选) **所属字段**: 子订单 `order_main.flow_status`(内部字段,无对外只读端点直接暴露该原始枚举值;本次列出仅为解释 CAS 的判定条件) | **类型**: `String` | 值 | 中文 | 说明 | |----|------|------| | `PENDING_CONFIRM` | 待确认 | 被退回时 CAS 的**判定条件**:仅当子订单当前恰为此值,回退才会把它 CAS 成 `RESOURCE_PREPARING`(`OrderFlowStatus.java:28`) | | `RESOURCE_PREPARING` | 资源准备 | CAS 命中后的**目标值**(`OrderFlowStatus.java:27`) | --- ## 六.6、修改前后对比(修改/删除类接口必写,新增跳过) ### 字段级对比 | 字段 | 改前 | 改后 | |------|------|------| | H10/H11 直接响应体(`GroupBatchRoomAllocationRebuildRespVO`) | 字段结构不变 | **字段结构完全不变**——本次不改响应体任何字段 | | `status-logs` 响应 `extra`(`BATCH_ROOM_ALLOC_MANUAL`/`BATCH_ROOM_ALLOC_REBUILD`) | 无 `reopenedOrderIds` 字段 | 新增 `reopenedOrderIds: Array`,恒为数组(不为 null) | | 已完成户的 `house_status`(经 `GET /admin/house/orders/{orderId}`) | 一旦 `CONFIRMED`,不会因为**别的户**的分房调整而改变 | 团级 H10/H11 判不出团级就绪时,重判不平的已完成户可能被静默改回 `PENDING_CLAIM` | ### 行为级对比 | 行为 | 改前 | 改后 | |------|------|------| | H10/H11 判不出团级就绪时(`resetWhenNotReady=true` 分支) | 只置回团级 `hotelReady`;户级 `DONE`/`CONFIRMED` 原样保留 | 团级置回 + 重判不平的已完成户 CAS 退回处理中(需求 `status`/`house_status`/子订单 `flow_status`/待办同步四件事) | | 房务待配房列表/抢单池 | 已完成户不会出现 | 被退回处理中的户会重新出现(`orderTodoSyncContract.syncForOrder` 触发待办重同步) | | 时间线可追溯性 | H10/H11 的时间线记录只报告受影响日/受影响计划行,不报告户级状态回退 | 新增 `extra.reopenedOrderIds`,可精确定位是哪些子订单被退回 | | H7/H8(`resetWhenNotReady=false` 的两个入口) | 不触发户级回退 | **仍不触发**,本次改动对它们零影响 | --- ## 六.7、影响评估(修改/删除类必写) - **是否破坏向后兼容**:否。三个端点的路径、方法、请求参数、错误码均未变化;H10/H11 响应体字段结构未变;端点 3 只新增一个此前不存在的可选字段(`extra.reopenedOrderIds`),旧前端忽略未知字段不受影响。 - **前端是否必须同步上线**:不同步上线不会导致接口调用失败,但存在一个真实的展示风险——已完成户可能被静默退回,若前端在人工微调/重算分房的结果弹窗、以及待配房相关列表页对此毫无提示,运营/房务可能不知道「为什么这户又出现在待办里了」。建议前端读取端点 3 的 `extra.reopenedOrderIds` 并在结果里提示。 - **前端 workaround 清理点**:无(此前没有相关字段可供 workaround)。 --- ## 七、不影响范围(显式声明, 帮前端/QA 缩小排查面) - **仅影响**:H10(人工微调)、H11(重算分房)判不出团级就绪分支下的已完成户状态;团期状态流水接口 `BATCH_ROOM_ALLOC_MANUAL`/`BATCH_ROOM_ALLOC_REBUILD` 两类记录的 `extra`。 - **零影响**: - H9 分房总览(只读,`GET /v3/admin/house/group-batches/{groupBatchId}/allocations`)——零副作用,未调用 `settleHouseholdsAndHotelReady` - H7/H8(`resetWhenNotReady=false` 的两个既有入口) - 全自订户的完成态判定 - 团级 `hotelReady`/`resetHotelReady` 本身的既有语义与既有判定逻辑 - 月度对账、核单结算等其他房务只读端点(另见 `changelogs-v2/2026-09/14_7327_月度对账并入团期计划与分房行-修改接口-管理后台.md`) - 团期车务(fleet)域 --- ## 八、测试环境已验证 **部署与实测状态**:修复本身(PR #7689,合并提交 `b57413ab8`)已合入 dev-v3 并是测试服当前部署基线(`hl-order-service-v3` 2026-09-15 13:12 部署于 `6a7b43a17`,经 `deploy-status.sh` 实测确认)的祖先,代码已在测试服跑着。2026-09-15 对 `GET /v3/admin/order/group-batch/{groupBatchId}/status-logs` 做了真实网关取证(账号 1001,只读 GET),**证实了 `extra.reopenedOrderIds` 字段真实存在于线上响应**,但取证用的团期(`groupBatchId=2099692512066105346`,另一会话当天为工单 #7327 AC-24/25 造的真实夹具)当时只有单户、且该户处于单户团自动回填场景,本轮 7 条 `BATCH_ROOM_ALLOC_MANUAL` 记录的 `extra.reopenedOrderIds` 均为空数组——**证实了字段契约(恒为数组、不为 null),但没有证实非空场景**(即真的有已完成户被退回处理中的那条主线)。这不是负面证据,单测已覆盖非空场景,backend_status 仍记 `deployed`(代码已部署、字段契约已实测),但「真的有户被退回处理中」这一核心场景请前端联调时按「单测覆盖、未端到端验证」对待,待专项造数(多户团、其中一户先置 DONE 再改分房让它不平)后补测。以下先列本机自动化单测断言,**不是测试服抓包**: ``` GroupBatchRoomDayConfirmManagerTest(origin/dev-v3,新增 4 例): settleHouseholdsAndHotelReady_unbalancedDoneHousehold_reopenedWithItsOwnBasis → 重判不平的已完成户被退回,reopenedOrderIds=[ORDER_ID],doneOrderIds 不含该户 ✓ settleHouseholdsAndHotelReady_unbalancedNotDoneHousehold_notListedAsReopened → 不平但原非完成态的户,CAS 落空,不进回退名单(verify(reopenHousehold) 正向对照) ✓ settleHouseholdsAndHotelReady_mixedBalance_reopensOnlyTheUnbalancedHousehold → 同一次调用,已平户走完成、不平户走回退,两条路互斥 ✓ settleHouseholdsAndHotelReady_unbalancedDoneHouseholdButResetNotRequested_neverReopens → resetWhenNotReady=false(H7/H8 语义)时,同样的不平事实不触发任何回退 ✓ GroupBatchRoomAllocationManagerTest(origin/dev-v3,新增 2 例): rebuild_householdsReopened_writesThemIntoTimeline → H11 时间线 extra.reopenedOrderIds=[OTHER_ORDER_ID=70002] ✓ saveManual_householdsReopened_writesThemIntoTimeline → H10 时间线同样落 extra.reopenedOrderIds=[OTHER_ORDER_ID=70002] ✓ 全量(PR 描述自报):Tests run: 81, Failures: 0, Errors: 0(GroupBatchRoomAllocationManagerTest=49 + GroupBatchRoomDayConfirmManagerTest=32) ``` **2026-09-15 测试服真实取证(账号 1001,只读 GET,证实字段契约,未证实非空场景)**: ``` GET /v3/admin/order/group-batch/2099692512066105346/status-logs → 200 → 共 16 条流水,其中 7 条 eventType=BATCH_ROOM_ALLOC_MANUAL → 逐条 extra 均含 reopenedOrderIds 字段,7 条全部为空数组 [] → 例:{"stayDates": ["2027-01-25"], "planIds": ["2099692682136743937"], "itemCount": 2, "clearedPlanIds": [], "reopenedOrderIds": []} → 该团期是当天另一会话为工单 #7327 AC-24/25 造的真实夹具(单户团),字段存在但本次未观察到 非空样本;非空场景(真的有已完成户被退回)目前仍只有单测覆盖 ``` 网关:本次无新增/修改路径,三个端点沿用既有路由。测试服部署已确认(见上),`extra.reopenedOrderIds` 字段的存在性已实测,但「非空、真的退回了户」这一核心场景仍待专项造数验证,建议按此专项补测。 --- ## 十、相关文档 - 关联 Issue: [wx/HL#7326](https://git.1814.love:8443/wx/HL/issues/7326) - 关联 PR: [wx/HL#7689](https://git.1814.love:8443/wx/HL/pulls/7689) - 同一 Issue 下与本次相关的既有背景:H10/H11 的完整契约、错误码见工单 #7326 正文 - 团期状态流水端点的通用契约背景: `changelogs-v2/2026-09/14_7327_月度对账并入团期计划与分房行-修改接口-管理后台.md`(引用了同一个 `GroupBatchStatusLogItemVO`) - 同一 Issue 下 #7325 的另一改动(confirm-check 预检行粒度变化 + 旧 finalize 拒绝越界户,PR #7700,已合并): `changelogs-v2/2026-09/15_7325_越界户预检列出末晚与旧finalize拒绝越界户-修改接口-管理后台.md` ## 关联 / 联系人 ### 链接 - **Issue**: [#7326](https://git.1814.love:8443/wx/HL/issues/7326) - **PR**: [#7689](https://git.1814.love:8443/wx/HL/pulls/7689) - **Merge commit**: [b57413ab8](https://git.1814.love:8443/wx/HL/commit/b57413ab8eb201d41110e2330909de51a6524b13) ### 联系人 - **后端负责人**: @wx