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 |
7339 |
房态库存操作改为请求体幂等契约并引入补偿轮次 |
internal |
wx(GIT) |
修改接口 |
deployed |
verified |
not_required |
|
|
|
|
resource 与 order-v3 已在测试环境按 merge commit 439e53d1e 配对发布;内部 Feign、跨实例幂等、补偿轮次及 Job 已验证 |
2026-09-08 |
dev-v3 |
房务库存:房态库存幂等与补偿轮次
服务: hl-resource-service、hl-order-service-v3
PR: #7356
Issue: #7339
日期: 2026-09-08
影响范围: order-v3 到 resource 的内部房态扣减/释放 Feign 契约与补偿生命周期
⚠️ 关键变化
库存扣减和释放的 date、qty、opId 从 Query 参数改为 JSON 请求体,属于不兼容的内部 Feign 变更;hl-resource-service 与 hl-order-service-v3 必须同批发布,禁止新旧版本混跑。补偿操作 ID 必须带轮次;普通单夜释放严格为 release-{logId}-{rollbackRound}。
一、背景
原接口缺少持久化幂等账本,同一业务动作在超时重试、跨实例重放或补偿重试时可能重复改变库存。现在服务端冻结首次请求指纹和首次结果,订单侧用 rollbackRound 区分同一持有关系的重新扣减轮次,并保留既有跨夜循环语义。
二、变更接口清单
| # |
接口 |
方法 |
路径 |
变更类型 |
说明 |
| 1 |
扣减房型库存 |
POST |
/internal/resource/hotels/{hotelId}/room-types/{roomTypeId}/deduct |
Query 改 JSON Body |
增加持久化幂等、请求指纹与首次结果重放 |
| 2 |
释放房型库存 |
POST |
/internal/resource/hotels/{hotelId}/room-types/{roomTypeId}/release |
Query 改 JSON Body |
按实际已用量下界 CAS 释放,避免负数 |
三、接口详情
1. 扣减房型库存 POST /internal/resource/hotels/{hotelId}/room-types/{roomTypeId}/deduct
VO: RoomInventoryOpReqDTO -> Result<HouseInventoryOpRespDTO>
使用场景
order-v3 在配房库存持有时通过内部 Feign 调用。调用方必须为每个生命周期轮次生成稳定且唯一的 opId;同一 opId 的重试必须保持路径、日期、数量和操作类型完全一致。
入参
| 字段 |
位置 |
类型 |
必填 |
约束 |
说明 |
| hotelId |
Path |
Long |
✅ |
非空 |
酒店 ID |
| roomTypeId |
Path |
Long |
✅ |
非空 |
房型 ID |
| date |
Body |
LocalDate |
✅ |
YYYY-MM-DD |
入住日期 |
| qty |
Body |
Integer |
✅ |
>= 1 |
扣减数量 |
| opId |
Body |
String |
✅ |
非空,最长 256 |
单夜为 deduct-{idempotencyKey}-{rollbackRound};仅多夜追加 -d{nightIndex} |
出参 Result<HouseInventoryOpRespDTO>
| 字段 |
类型 |
说明 |
| changed |
Integer |
首次操作实际改变数量 |
| remain |
Integer |
首次操作后的可用库存 |
| duplicated |
Boolean |
是否为已提交首次结果的重放 |
请求示例
响应示例
空数据 / 降级响应
hotel.stock.enabled=false 时不改变库存,但仍冻结该 opId 的首次成功快照;开关恢复后重放不会补执行。
错误响应
业务边界
- 仅允许内部鉴权调用,不通过管理端网关暴露。
- 首次账本写入、库存变化和结果快照属于同一事务。
- 重复
opId 返回首次 changed/remain 且 duplicated=true,不会读取当前库存伪造新快照。
- 同一
opId 改变 hotelId、roomTypeId、date、qty 或操作类型会拒绝。
- 库存不足的
310403 结果持久化;补充库存后重放旧 opId 仍拒绝。
2. 释放房型库存 POST /internal/resource/hotels/{hotelId}/room-types/{roomTypeId}/release
VO: RoomInventoryOpReqDTO -> Result<HouseInventoryOpRespDTO>
使用场景
order-v3 删除配房、取消订单、换酒店回滚或补偿 Job 释放房态。普通单夜必须使用 release-{logId}-{rollbackRound};日期后缀只用于保留的多夜循环。
入参
| 字段 |
位置 |
类型 |
必填 |
约束 |
说明 |
| hotelId |
Path |
Long |
✅ |
非空 |
酒店 ID |
| roomTypeId |
Path |
Long |
✅ |
非空 |
房型 ID |
| date |
Body |
LocalDate |
✅ |
YYYY-MM-DD |
释放日期 |
| qty |
Body |
Integer |
✅ |
>= 1 |
请求释放上限 |
| opId |
Body |
String |
✅ |
非空,最长 256 |
单夜严格为 release-{logId}-{rollbackRound};仅多夜追加 -d{stayDate} |
出参 Result<HouseInventoryOpRespDTO>
| 字段 |
类型 |
说明 |
| changed |
Integer |
实际释放数量,等于 min(qty, stockUsed) |
| remain |
Integer |
操作后的可用库存 |
| duplicated |
Boolean |
是否为已提交首次结果的重放 |
请求示例
响应示例
空数据 / 降级响应
当前已用量为 0 时,接口成功返回 changed=0;不会把已用量更新为负数。
错误响应
业务边界
- 仅允许内部鉴权调用,不通过管理端网关暴露。
- 释放先读取当前已用量,计算实际释放量,再用类型安全的期望值与下界 CAS 更新;0 行时重读并有界重试。
- 同一释放
opId 重放不重复回补;不同释放 opId 并发也保证 stockUsed >= 0。
rollbackRound 初始为 0;仅当同一扣减日志从 DONE 重新进入持有状态时原子自增一次。
nights > 1 继续按既有多夜循环处理,不拆分原扣减日志,也不拒绝多夜请求。
四、契约约束与正确调用方式
✅ 正确 / ❌ 错误 payload 对照
| 场景 |
payload |
| ✅ 首次扣减 |
{ "date": "2026-09-09", "qty": 2, "opId": "deduct-asgn-123-d1-rt456-h789-0" } |
| ✅ 同操作重试 |
请求体与路径全部不变 |
| ✅ 单夜释放 |
{ "date": "2026-09-09", "qty": 2, "opId": "release-456-0" } |
| ❌ 沿用旧 Query 参数 |
?date=2026-09-09&qty=2&opId=...,新版本不再接受 |
| ❌ 重用 opId 改数量 |
同 opId 将 qty 从 2 改为 3,会拒绝 |
切换状态时的必要动作
hl-resource-service 与 hl-order-service-v3 必须同批停旧、同 commit 启新;禁止任一服务先恢复流量形成 Query/Body 混版。回退必须两服务一起从 R0 回到 R-1,必要时再一起回 R-2;新增迁移保持前向兼容,不回滚 DDL。
五、数据库行为
| 调用行为 |
外部可观察结果 |
| 首次成功扣减/释放 |
操作身份、完整请求指纹和首次结果原子提交 |
| 相同 opId 完整重放 |
不再改变库存,返回首次快照并标记 duplicated |
| 相同 opId 不同指纹 |
拒绝,不覆盖首次身份或结果 |
| 库存不足扣减 |
返回并持久化 310403,后续重放仍拒绝 |
| DONE 日志重新扣减 |
rollbackRound 原子 +1,新轮次使用新的扣减/释放 opId |
六、边界行为
- 未携带有效内部令牌 → 403。
- 扣减库存不足或日历缺失 → 310403,且旧
opId 永久关闭。
- 日期、数量、路径或操作类型与原
opId 指纹不一致 → 310404 拒绝。
- 释放数量大于当前已用量 → 只释放实际已用量。
- 并发重复和并发不同释放键 → 最终库存一致,
stockUsed 不小于 0。
- 补偿 Job 只认
updateTime 已超过宽限期的精确 PENDING 快照;新鲜更新不会被旧扫描结果误处理。
六.6、修改前后对比
字段级对比
| 字段 |
改前 |
改后 |
| date |
Query @RequestParam |
JSON Body RoomInventoryOpReqDTO.date |
| qty |
Query @RequestParam |
JSON Body RoomInventoryOpReqDTO.qty,>= 1 |
| opId |
Query @RequestParam |
JSON Body RoomInventoryOpReqDTO.opId,最长 256,完整指纹冻结 |
| duplicated |
非稳定重放语义 |
明确表示命中首次已提交快照 |
| rollbackRound |
无 |
从 0 开始,DONE 复活时自增一次 |
行为级对比
| 行为 |
改前 |
改后 |
| 重复请求 |
可能重复改变库存 |
唯一账本重放首次结果 |
| 库存不足后旧请求重试 |
补库存后可能延迟执行 |
310403 持久拒绝,旧 opId 不再执行 |
| 超量释放 |
缺少轮次隔离和下界保护 |
按实际已用量释放,类型安全 CAS 有界重试 |
| 同一日志重新持有 |
操作身份可能与旧释放碰撞 |
rollbackRound 区分各生命周期 |
六.7、影响评估
- 是否破坏向后兼容: 是,内部 Feign 从 Query 改为 JSON Body。
- 前端是否必须同步上线: 否;无 frontend API 变化。
- 前端 workaround 清理点: 无。
- 服务发布约束: resource 与 order-v3 必须同批发布/同批回退,不能混版。
七、不影响范围
- 仅影响: order-v3 调 resource 的内部房态扣减、释放及补偿生命周期。
- 零影响:
- 管理后台和小程序 HTTP 入参/出参。
- 既有酒店、房型和价格日历读取接口。
- 多夜配房循环;不拆日志、不拒绝
nights > 1。
- 其他 order-v3 订单、计价与支付流程。
八、测试环境已验证
九、相关历史 PR
| PR |
Issue |
说明 |
是否仍有效 |
| #7356 |
#7339 |
内部库存持久化幂等、释放 CAS 与补偿轮次 |
✅ 最新 |
十、相关文档
关联 / 联系人
链接
联系人