diff --git a/changelogs-v2/2026-09/10_7389_多夜配房释放改逐夜回补resource库存账本告警码拆分-修复-管理后台.md b/changelogs-v2/2026-09/10_7389_多夜配房释放改逐夜回补resource库存账本告警码拆分-修复-管理后台.md new file mode 100644 index 00000000..31f9ad0c --- /dev/null +++ b/changelogs-v2/2026-09/10_7389_多夜配房释放改逐夜回补resource库存账本告警码拆分-修复-管理后台.md @@ -0,0 +1,296 @@ +--- +schema: "hl-changelog/v2" +ticket: "7389" +title: "多夜配房释放改为逐夜回补(不再只回补首夜);resource 房务库存账本内部一致性告警从共用 310404 拆分为 310417-310420 语义化错误码" +consumer: "admin" +author: "wx(GIT)" +change_type: "修复" +backend_status: "deployed" +gateway_status: "not_required" +frontend_status: "not_required" +frontend_owner: "" +frontend_ref: "" +target_release: "" +verified_at: "" +status_note: "PR #7423(合并提交 f5545b148)已合并 dev-v3。已部署测试服: hl-order-service-v3 与 hl-resource-service 两个模块均已滚动, 2026-09-10 11:08/11:10 同为 HEAD 7e7e04fca(f5545b148 是其祖先, 已用 git merge-base --is-ancestor 核实包含)。网关零改动(无新端点、无路由变更), gateway_status 按此填 not_required。backend_status=deployed 为真实态。" +updated_at: "2026-09-10" +base: "dev-v3" +--- + +# 房务多夜配房释放改逐夜回补;resource 库存账本告警码拆分(修复) + +> **服务**: hl-order-service-v3, hl-resource-service +> **PR**: #7423 +> **Issue**: #7389 +> **日期**: 2026-09-10 +> **影响范围**: 多晚配房释放/回滚链路(order-v3 `HouseDualDeductionService`);resource 房务库存账本内部一致性告警码(`RoomInventoryOpLogService`,4 个码从共用 310404 拆分为 310417-310420) + +--- + +## ⚠️ 关键变化 + +1. **多夜配房释放此前只对首夜调用 resource 释放接口**,`stay_nights > 1` 时后 N-1 晚的库存被永久占住、无补偿入口(方向是少卖)。**改后按 `stay_nights` 逐夜循环释放**,某一夜失败不打断其余夜(best-effort),全部夜成功才把该行标 `DONE`,否则留 `PENDING` 交 `DualDeductionReconcileJob` 兜底。 +2. **resource 侧原本四处共用 310404**("酒店ID/房型ID/日期不可为空")**的内部一致性告警拆成四个语义码 310417-310420**(详见"六.5、枚举")。**这是前端/调用方唯一需要关心的契约变化**:若有地方对 310404 做过硬编码分支处理(尤其是"同一 opId 二次请求内容不一致"这一幂等冲突场景),现在会收到 310417,不再是 310404。 +3. 本单**无接口新增/修改/删除**,无 Flyway、无表结构变更、无 VO 字段增删,网关零改动。以下按模板编号组织,但"三、接口详情"等节按"无对外接口契约变化、只有内部行为/错误码变化"的实际情况填写,不强行套用逐接口 VO 表格。 + +--- + +## 一、背景 + +`house_dual_deduction_log` 一行记的是"一次扣减占了 `stay_nights` 晚"的库存持有关系。改前的正常释放路径(`HouseDualDeductionService.restoreLogRecord`)只对 `log.getStayDate()` 这一晚调用 resource `releaseRoomInventory`;当 `stay_nights > 1`(跨夜配房)时,第 2..N 晚扣掉的库存永久没有对应的释放调用,且没有任何补偿入口能把这部分库存要回来。 + +同一 PR 顺带修了 resource 侧的一处诊断缺陷:`RoomInventoryOpLogService` 里四条内部一致性判断分支(幂等键内容冲突、拒绝结果落库失败、结果快照落库失败、结果快照缺失)此前全部抛同一个 `310404`「酒店ID/房型ID/日期不可为空」,排障时看到的文案和真实原因(例如"同一 opId 被另一组请求内容占用")完全对不上号,遂拆成四个语义码。 + +--- + +## 二、变更接口清单 + +本单不改变任何对外 HTTP 接口的方法 / 路径 / 请求体 / 响应体结构,故没有严格意义上的"变更接口"。受影响的是以下既有内部链路在特定边界场景下的行为与错误码: + +| # | 接口/链路 | 方法 | 路径 | 变更类型 | 说明 | +|---|------|------|------|----------|------| +| 1 | 配房库存释放(订单取消/换酒店/删配房时内部触发) | POST | `/internal/resource/hotels/{hotelId}/room-types/{roomTypeId}/release` | 调用方(order-v3)行为变更 | 多夜持有的库存由"只发首夜一次调用"改为"逐夜循环调用";该内部 Feign 端点自身请求/响应体结构不变 | +| 2 | 库存操作账本一致性校验(deduct/release 共用的内部一致性分支) | POST | `/internal/resource/hotels/{hotelId}/room-types/{roomTypeId}/deduct`、`/internal/resource/hotels/{hotelId}/room-types/{roomTypeId}/release` | 错误码细分 | 4 种内部一致性异常场景的返回码由共用 `310404` 拆分为 `310417`/`310418`/`310419`/`310420`;请求体/响应体结构不变 | + +--- + +## 三、接口详情 + +本单不新增/修改/删除任何对外 VO 契约,不适用模板逐接口 VO + 入参/出参字段表结构。按"行为变更点"分两小节说明。 + +### 1. 多夜配房释放逐夜回补 + +**涉及方法**:`HouseDualDeductionService.restoreLogRecord`(私有方法,被 `restore(assignmentId, reason)` / `restoreByLogId(logId, reason)` / `restoreByLogIdAndRound(...)` 共用),无独立对外 HTTP 签名。 + +#### 使用场景 + +订单取消、换酒店、删配房等触发释放已扣减的房型库存时调用。此前 `stay_nights > 1`(跨夜配房,一次扣减覆盖多晚)时只有首夜被释放。 + +#### 入参字段表 + +不适用(内部私有方法,参数来自 `HouseDualDeductionLogDO` 实体字段 `hotelId`/`roomTypeId`/`stayDate`/`stayNights`/`roomCount`,本单未新增/删除任何列或字段)。 + +#### 出参字段表 + +不适用。`restore` / `restoreByLogId` 对外返回 `boolean`:`true` = 全部夜释放成功且已标 `DONE`;`false` = 已标 `PENDING`,待 `DualDeductionReconcileJob` 兜底重放。 + +#### 请求示例 + +```json +不适用:内部私有方法调用,无 HTTP 请求体;每夜实际下发给 resource 的请求体见下方第 2 小节的请求示例。 +``` + +#### 响应示例 + +```json +不适用:方法签名为 boolean,无 JSON 响应体。 +``` + +#### 空数据 / 降级响应 + +`selectHoldingByAssignment` 查无持有中记录时(如 CORE/CUSTOM 历史配房本就未扣库存),直接返回 `true`,视为释放成功,不发任何 resource 调用。 + +#### 错误响应 + +```json +不适用:本方法内部 try/catch 吞掉 resource 调用异常(转为 markReleasePending 落库标 PENDING),不对外抛出异常,无对外错误响应体。 +``` + +#### 业务边界 + +- 释放循环次数 = `normalizedNights(logRecord.getStayNights())`:`stayDate` 起逐日 `plusDays(i)`,与扣减侧 `callResourceDeduct` 的夜序严格镜像。 +- opId 由 `buildReleaseOpId(logId, round, stayDate, date)` 构造:首夜(`date == stayDate`)返回 `release-{logId}-{round}`(与改动前逐字节一致,供跨版本在途 `PENDING` 行重放命中同一幂等键、不重复回补);第 2..N 夜返回 `release-{logId}-{round}-d{date}`。全类唯一一份实现,正常释放与跨夜扣减补偿两条路径共用,有反射测试守着不许再写第二份编法。 +- best-effort:某一夜释放失败不中断循环,继续释放剩余夜;只要有一夜失败,整行走 `markReleasePending`(留 `PENDING`),不会推进为 `DONE`;全部夜成功才调用 `markRolledBackCas` 标 `DONE`。 +- `normalizedNights` 归一化规则由"`null → 1`"扩为"`null` 或 `< 1` → 1",避免历史脏数据 `stay_nights=0`(或负数)导致循环 0 次却把行标 `DONE`(库存被永久判死)。 +- `stay_nights` 为 `null` 的存量行按 1 夜处理,行为与本单改动前逐字一致(不迁移不回填)。 + +--- + +### 2. resource 房务库存账本内部一致性告警码拆分 + +**涉及类**:`HotelErrorCode`(`hl-resource-service`);触发点 `RoomInventoryOpLogService.execute` / `RoomInventoryOpLogService.replay`。 + +#### 使用场景 + +order-v3 通过 `ResourceFeignClient` 调用 resource 的库存扣减/释放内部端点时,resource 侧用请求携带的 `opId` 做幂等账本(`room_inventory_op_log`,唯一键 `uk_op_id`)。以下四种内部一致性场景改前全部返回 `310404`,改后各自独立返回。 + +#### 入参字段表 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| date | Body | LocalDate | 是 | NotNull | 入住日期;本单未改 | +| qty | Body | Integer | 是 | NotNull, Min 1 | 请求变更房数;本单未改 | +| opId | Body | String | 是 | NotBlank, Size max 256 | 调用方业务键 + 持有轮次 + 必要日期后缀;本单未改字段本身,但 order-v3 侧的构造规则见第 1 小节 | + +(`RoomInventoryOpReqDTO`,order-v3 与 resource 两端各有一份同结构 DTO,字段名与约束逐一核对一致,本单均未改动。) + +#### 出参字段表 + +不适用——正常路径响应体结构(`Result`,字段 `changed`/`remain`/`duplicated`)未改;本单只影响错误态下 `code`/`message` 的取值范围。 + +#### 请求示例 + +```json +{ + "date": "2026-05-02", + "qty": 2, + "opId": "release-88001-0-d2026-05-02" +} +``` + +(内部 Feign 端点请求体,字段本身未变,仅用于对应下方错误响应示例。) + +#### 响应示例 + +```json +不适用:正常路径响应结构本单未改,不重复列出。 +``` + +#### 空数据 / 降级响应 + +不适用(本端点无"空数据"语义;账本命中后要么返回历史快照,要么按下方错误码之一拒绝)。 + +#### 错误响应 + +```json +{ + "code": 310417, + "message": "操作幂等键 release-88001-0-d2026-05-02 已存在且请求内容不一致", + "success": false, + "data": null +} +``` + +(`HotelErrorCode.HOUSE_INVENTORY_OP_ID_CONFLICT`,message 模板 `操作幂等键 {0} 已存在且请求内容不一致` 中的 `{0}` 由 `MessageFormat` 替换为实际 opId;`hl-resource-service/src/main/java/com/hulalv/resource/errorcode/HotelErrorCode.java:216-217`。触发:同一 `opId` 第二次到达时,账本命中行的 `hotelId`/`roomTypeId`/`priceDate`/`quantity`/`opType` 与本次请求任一不一致。) + +#### 业务边界 + +- `310417`(`HOUSE_INVENTORY_OP_ID_CONFLICT`,`HotelErrorCode.java:216-217`):同一 opId 对应的 hotelId/roomTypeId/date/qty/opType 与首次请求不一致,属**确定性冲突**——order-v3 侧 `toResourceBusinessException` 对该码做了特判:不当库存不足处理(不标 `DONE` 放行重扣),也不静默吞掉,而是原样上抛并记 error 日志,让失败分支走 `markResourceFailed`(留 `NONE` 阻断同轮重放),**不再对该码做空转重试**。 +- `310418`(`HOUSE_INVENTORY_LEDGER_REJECT_PERSIST_FAILED`,`HotelErrorCode.java:220-221`):库存不足的拒绝结果回写账本影响 0 行(同一事务内账本行被并发改写),属内部一致性异常。 +- `310419`(`HOUSE_INVENTORY_LEDGER_SNAPSHOT_PERSIST_FAILED`,`HotelErrorCode.java:224-225`):成功结果快照回写账本影响 0 行,同上。 +- `310420`(`HOUSE_INVENTORY_LEDGER_SNAPSHOT_MISSING`,`HotelErrorCode.java:228-229`):账本按 opId 命中该行(指纹逐字段一致)但首次结果快照缺失(`changed`/`remain` 任一为 null 且非库存不足结果),无法安全重放(既不能当成功也不能当拒绝)。 +- 指纹逐字段一致的正常重放(同 opId、同 hotelId/roomTypeId/date/qty/opType)仍返回首次快照并置 `duplicated=true`,不受本次改动影响。 + +--- + +## 四、契约约束与正确调用方式 + +> 本节只写后端接受/拒绝 payload 的规则,不写 UI 渲染建议。 + +本单未改变任何入参校验规则或状态切换动作,无新增正确/错误 payload 对照需要记录。唯一需要调用方(order-v3 及未来其他消费 resource 内部库存端点的服务)确认的规则是:**同一 opId 二次调用时,请求内容必须与首次逐字段一致**(hotelId/roomTypeId/date/qty/opType),否则拒绝并返回 310417——这条判定逻辑改前也存在,只是改前返回码是 310404,本单只是把返回码改准确,未改变判定逻辑本身。 + +### 切换状态时的必要动作 + +无。本单不涉及任何需要调用方额外显式置空/切换的字段。 + +--- + +## 五、数据库行为 + +本单无 Flyway、无表结构变更、无字段增删。`house_dual_deduction_log`、`room_inventory_op_log` 两张表的列定义均未动,仅改变了写入这两张表的调用节奏(多夜释放从 1 次调用变成 N 次调用)与错误码分支的选择,不改变任何列的取值集合。 + +--- + +## 六、边界行为 + +- 多夜释放某一夜网络/业务异常:不中断其余夜,整行留 `PENDING` 交 `DualDeductionReconcileJob` 兜底重放;不抛异常给上层调用方。 +- `stay_nights` 为 `null`(历史存量行):按 1 夜处理,与本单改动前逐字节一致。 +- `stay_nights` 为脏数据 `0` 或负数:按 1 夜兜底,避免循环 0 次却把行标 `DONE` 导致库存永久判死。 +- 首夜与非首夜的 opId 编法不同(见第一小节),跨版本在途 `PENDING` 行重放时首夜仍命中改动前的旧键,不会二次真实释放。 +- deduct 路径命中 310417(幂等键冲突):order-v3 不再重试,直接标记该轮扣减为失败(`resourceFailed`)并原样上抛,不做静默降级。 + +--- + +## 六.5、枚举 / 数据字典 + +### resource 房务库存账本一致性错误码(`HotelErrorCode`,段位 310417-310420,工单 #7389 新增) + +**所属字段**:内部 Feign 错误态响应 `Result.code` | **类型**:`Integer` + +| 值 | 常量名 | message 模板 | 触发条件 | +|----|------|------|------| +| `310417` | `HOUSE_INVENTORY_OP_ID_CONFLICT` | `操作幂等键 {0} 已存在且请求内容不一致` | 同一 opId 第二次到达,账本命中行的 hotelId/roomTypeId/date/qty/opType 与本次请求任一不一致 | +| `310418` | `HOUSE_INVENTORY_LEDGER_REJECT_PERSIST_FAILED` | `库存操作拒绝结果落库失败(操作幂等键 {0})` | 库存不足的拒绝结果回写账本影响 0 行 | +| `310419` | `HOUSE_INVENTORY_LEDGER_SNAPSHOT_PERSIST_FAILED` | `库存操作结果快照落库失败(操作幂等键 {0})` | 成功结果快照回写账本影响 0 行 | +| `310420` | `HOUSE_INVENTORY_LEDGER_SNAPSHOT_MISSING` | `库存操作首次结果快照缺失,无法重放(操作幂等键 {0})` | 账本命中但首次结果快照(changed/remain)缺失,且非库存不足结果 | + +(`hl-resource-service/src/main/java/com/hulalv/resource/errorcode/HotelErrorCode.java:214-229`;`{0}` 为 `MessageFormat` 占位符,替换为实际 opId。改前这四个场景全部复用 `310404 HOUSE_INVENTORY_PARAM_BLANK`「酒店ID/房型ID/日期不可为空」,见同文件第 150-152 行。) + +**order-v3 侧的特判**:仅 `310417` 被 `HouseDualDeductionService.toResourceBusinessException` 特判为不可重试(原样上抛 + error 日志,阻断同轮重放);`310418`/`310419`/`310420` 未做特判,走原有"其他 resource 业务异常 → 原样上抛"分支。 + +--- + +## 六.6、修改前后对比 + +### 字段级对比 + +| 字段/返回值 | 改前 | 改后 | +|------|------|------| +| resource 内部账本一致性告警码 | 均为 `310404`(4 种场景共用) | 拆分为 `310417`/`310418`/`310419`/`310420`,各自独立 | +| 端点请求/响应字段结构 | 无变化 | 无变化 | + +### 行为级对比 + +| 行为 | 改前 | 改后 | +|------|------|------| +| 多夜配房释放 | 只对首夜调 `releaseRoomInventory`,`stay_nights > 1` 时后 N-1 晚永久少释放 | 按 `stay_nights` 逐夜循环释放,夜序与扣减侧镜像 | +| 释放 opId 编法 | 首夜固定 `release-{logId}-{round}` | 首夜不变,仍是 `release-{logId}-{round}`;第 2..N 夜新增 `release-{logId}-{round}-d{date}` | +| 单夜释放失败处理 | 唯一一次调用失败即整行留 `PENDING` | best-effort:某夜失败不中断其余夜释放,仍是"有失败即留 PENDING、全成功才 DONE" | +| `normalizedNights` 归一化 | `null → 1` | `null` 或 `< 1` → 1 | +| resource 幂等键冲突时的重试策略(order-v3 侧) | 无特判,与其他 resource 业务异常同等对待 | `310417` 特判为确定性失败,标记 `resourceFailed`,不再空转重试 | + +--- + +## 六.7、影响评估 + +- **是否破坏向后兼容**:否。对外 HTTP 接口方法/路径/请求体/响应体结构均未变化;单夜配房(`stay_nights <= 1`,绝大多数场景)的释放行为与 opId 与改动前逐字节一致。 +- **前端是否必须同步上线**:否。本单不涉及任何前端可见的接口/字段变化;仅当调用方对 `310404` 做过针对"内部一致性异常"这一特定子场景的硬编码处理时,才需要知道现在会收到 `310417`-`310420`。 +- **前端 workaround 清理点**:无强制清理项。如有代码对 `310404` 做了范围过宽的兜底判断(把"参数为空"和"幂等键冲突"混为一谈),建议改为按 `310417` 单独识别幂等冲突场景。 + +--- + +## 七、不影响范围 + +- **仅影响**:order-v3 多夜配房释放的内部循环行为(`HouseDualDeductionService.restoreLogRecord`);resource 房务库存账本四种内部一致性异常的错误码取值(`RoomInventoryOpLogService`)。 +- **零影响**: + - 无新增/修改/删除任何对外 HTTP 接口;网关路由零改动。 + - 单夜配房(`stay_nights <= 1`)的释放 opId 与改动前逐字节一致(`release-{logId}-{round}`,无 `-d` 后缀)。 + - `house_dual_deduction_log`、`room_inventory_op_log` 表结构未变,无 Flyway、无数据迁移。 + - 库存不足码 `310403`、参数为空码 `310404` 本身未删除,`310404` 仍会在其原有的真正"参数为空"场景下返回,只是不再被这四个内部一致性分支借用。 + - 正常重放(指纹一致的重复请求)路径未变,仍返回首次快照并置 `duplicated=true`。 + +--- + +## 八、测试环境已验证 + +以下两条已在测试服网关实测通过,其余 AC-1/2/3/6/13 已实测通过但本 changelog 只摘录对调用方最直接可见的两条结论,不逐条复述实测数据: + +```text +三晚扣减后释放: + 三晚 stock_used 全部回到 0 [验证通过] + room_inventory_op_log 生成三条记录, opId 为 + release-{logId}-0(首夜)、release-{logId}-0-d{date}(第2晚)、release-{logId}-0-d{date}(第3晚) [验证通过] + +同一 opId 二次请求内容不一致(改 qty / 改 date)各自单独发起: + 两种场景均返回 310417 [验证通过] + 账本对应 opId 各仅 1 行(未产生第二行), 价格日历库存零变动 [验证通过] +``` + +--- + +## 十、相关文档 + +- 关联 Issue: [wx/HL#7389](https://git.1814.love:8443/wx/HL/issues/7389) +- 关联 PR: [wx/HL#7423](https://git.1814.love:8443/wx/HL/pulls/7423) + +## 关联 / 联系人 + +### 链接 + +- **Issue**: [#7389](https://git.1814.love:8443/wx/HL/issues/7389) +- **PR**: [#7423](https://git.1814.love:8443/wx/HL/pulls/7423) +- **Merge commit**: [f5545b148](https://git.1814.love:8443/wx/HL/commit/f5545b148) + +### 联系人 + +- **后端负责人**: @wx