19 KiB
schema, ticket, title, consumer, author, change_type, backend_status, gateway_status, frontend_status, frontend_owner, frontend_ref, target_release, verified_at, status_note, updated_at, base
| schema | ticket | title | consumer | author | change_type | backend_status | gateway_status | frontend_status | frontend_owner | frontend_ref | target_release | verified_at | status_note | updated_at | base |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| hl-changelog/v2 | 7389 | 多夜配房释放改为逐夜回补(不再只回补首夜);resource 房务库存账本内部一致性告警从共用 310404 拆分为 310417-310420 语义化错误码 | admin | wx(GIT) | 修复 | deployed | not_required | not_required | mmg | not_required 2026-09-10 mmg: 修复全在后端内部(order-v3 多夜释放逐夜回补 + resource 账本告警码 310404 拆 310417-310420),无对外接口/字段/VO/路由变化;changelog 自答非必须同步上线,唯一触点=前端对 310404 硬编码分支。grep 实证前端零 310404/310417-310420 命中、零 /internal/resource 库存端点直连,触点不存在。错误码由拦截器透后端 message(无中央错误码字典),旧前端不受影响。 | 2026-09-10 | 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)
⚠️ 关键变化
- 多夜配房释放此前只对首夜调用 resource 释放接口,
stay_nights > 1时后 N-1 晚的库存被永久占住、无补偿入口(方向是少卖)。改后按stay_nights逐夜循环释放,某一夜失败不打断其余夜(best-effort),全部夜成功才把该行标DONE,否则留PENDING交DualDeductionReconcileJob兜底。 - resource 侧原本四处共用 310404("酒店ID/房型ID/日期不可为空")的内部一致性告警拆成四个语义码 310417-310420(详见"六.5、枚举")。这是前端/调用方唯一需要关心的契约变化:若有地方对 310404 做过硬编码分支处理(尤其是"同一 opId 二次请求内容不一致"这一幂等冲突场景),现在会收到 310417,不再是 310404。
- 本单无接口新增/修改/删除,无 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 兜底重放。
请求示例
不适用:内部私有方法调用,无 HTTP 请求体;每夜实际下发给 resource 的请求体见下方第 2 小节的请求示例。
响应示例
不适用:方法签名为 boolean,无 JSON 响应体。
空数据 / 降级响应
selectHoldingByAssignment 查无持有中记录时(如 CORE/CUSTOM 历史配房本就未扣库存),直接返回 true,视为释放成功,不发任何 resource 调用。
错误响应
不适用:本方法内部 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<HouseInventoryOpRespDTO>,字段 changed/remain/duplicated)未改;本单只影响错误态下 code/message 的取值范围。
请求示例
{
"date": "2026-05-02",
"qty": 2,
"opId": "release-88001-0-d2026-05-02"
}
(内部 Feign 端点请求体,字段本身未变,仅用于对应下方错误响应示例。)
响应示例
不适用:正常路径响应结构本单未改,不重复列出。
空数据 / 降级响应
不适用(本端点无"空数据"语义;账本命中后要么返回历史快照,要么按下方错误码之一拒绝)。
错误响应
{
"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 只摘录对调用方最直接可见的两条结论,不逐条复述实测数据:
三晚扣减后释放:
三晚 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
- 关联 PR: wx/HL#7423
关联 / 联系人
链接
联系人
- 后端负责人: @wx