文件
hl-api-changelog/changelogs-v2/2026-09/08_7339_房态库存幂等与补偿轮次-修改接口-管理后台.md
API Changelog Bot 74dc7f0ede
changelog-filename-gate / validate (push) Successful in 2s
docs(changelog): 发布 #7339 库存幂等契约
2026-09-08 22:30:49 +08:00

12 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 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 是否为已提交首次结果的重放

请求示例

{
  "date": "2026-09-09",
  "qty": 2,
  "opId": "deduct-asgn-123-d1-rt456-h789-0"
}

响应示例

{
  "code": 200,
  "message": "成功",
  "data": { "changed": 2, "remain": 8, "duplicated": false },
  "success": true
}

空数据 / 降级响应

hotel.stock.enabled=false 时不改变库存,但仍冻结该 opId 的首次成功快照;开关恢复后重放不会补执行。

{ "code": 200, "data": { "changed": 0, "remain": 10, "duplicated": false }, "success": true }

错误响应

{
  "code": 310403,
  "message": "房型库存不足或日历记录缺失",
  "success": false,
  "data": null
}

业务边界

  • 仅允许内部鉴权调用,不通过管理端网关暴露。
  • 首次账本写入、库存变化和结果快照属于同一事务。
  • 重复 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 是否为已提交首次结果的重放

请求示例

{
  "date": "2026-09-09",
  "qty": 999,
  "opId": "release-2097325826075738113-1"
}

响应示例

{
  "code": 200,
  "message": "成功",
  "data": { "changed": 1, "remain": 10, "duplicated": false },
  "success": true
}

空数据 / 降级响应

当前已用量为 0 时,接口成功返回 changed=0;不会把已用量更新为负数。

{ "code": 200, "data": { "changed": 0, "remain": 10, "duplicated": false }, "success": true }

错误响应

{
  "code": 310404,
  "message": "酒店ID/房型ID/日期不可为空",
  "success": false,
  "data": null
}

业务边界

  • 仅允许内部鉴权调用,不通过管理端网关暴露。
  • 释放先读取当前已用量,计算实际释放量,再用类型安全的期望值与下界 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 订单、计价与支付流程。

八、测试环境已验证

两服务同批部署 dev-v3@439e53d1e,每服务 2 个新实例,旧 PID 无重叠,四个健康探针均 HTTP 200 ✓
网关 GET /admin/hotel/items → HTTP 200 / code 200 ✓
网关 GET /v3/admin/house/staff → HTTP 200 / code 200 ✓
跨实例重复扣减返回首次快照,date/qty/opType 三类指纹冲突均拒绝 ✓
普通单夜轮次 opId:deduct-...-0/1 与 release-{logId}-0/1 精确命中 ✓
释放 qty=999、并发不同 release opId 后 stockUsed=0 且全库 stockUsed<0 为 0 ✓
库存不足 310403 在补库存后用旧 opId 重放仍拒绝 ✓
补偿 Job 跳过新鲜 PENDING;双实例并发触发后每轮 release ledger 恰好 1 条并收敛 DONE ✓
测试夹具、账本和订单日志全部清理;Job 1025/1026 已恢复 ACTIVE ✓

九、相关历史 PR

PR Issue 说明 是否仍有效
#7356 #7339 内部库存持久化幂等、释放 CAS 与补偿轮次 ✅ 最新

十、相关文档

  • 关联 Issue: wx/HL#7339
  • 关联 PR: wx/HL#7356
  • 部署回退: R0 439e53d1e;R-1 4a39b7fbb;R-2 f36a5b4e

关联 / 联系人

链接

联系人

  • 后端负责人: @wx