From 74dc7f0ede2fd98f7d27fdf70d4a627198143588 Mon Sep 17 00:00:00 2001 From: API Changelog Bot Date: Tue, 8 Sep 2026 22:30:06 +0800 Subject: [PATCH] =?UTF-8?q?docs(changelog):=20=E5=8F=91=E5=B8=83=20#7339?= =?UTF-8?q?=20=E5=BA=93=E5=AD=98=E5=B9=82=E7=AD=89=E5=A5=91=E7=BA=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...ˆ¿态库存幂等与补偿轮次-修改接口-管理后台.md | 321 ++++++++++++++++++ 1 file changed, 321 insertions(+) create mode 100644 changelogs-v2/2026-09/08_7339_房态库存幂等与补偿轮次-修改接口-管理后台.md diff --git a/changelogs-v2/2026-09/08_7339_房态库存幂等与补偿轮次-修改接口-管理后台.md b/changelogs-v2/2026-09/08_7339_房态库存幂等与补偿轮次-修改接口-管理后台.md new file mode 100644 index 00000000..3318e87f --- /dev/null +++ b/changelogs-v2/2026-09/08_7339_房态库存幂等与补偿轮次-修改接口-管理后台.md @@ -0,0 +1,321 @@ +--- +schema: "hl-changelog/v2" +ticket: "7339" +title: "房态库存操作改为请求体幂等契约并引入补偿轮次" +consumer: "internal" +author: "wx(GIT)" +change_type: "修改接口" +backend_status: "deployed" +gateway_status: "verified" +frontend_status: "not_required" +frontend_owner: "" +frontend_ref: "" +target_release: "" +verified_at: "" +status_note: "resource 与 order-v3 已在测试环境按 merge commit 439e53d1e 配对发布;内部 Feign、跨实例幂等、补偿轮次及 Job 已验证" +updated_at: "2026-09-08" +base: "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` + +#### 使用场景 + +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` + +| 字段 | 类型 | 说明 | +|------|------|------| +| changed | Integer | 首次操作实际改变数量 | +| remain | Integer | 首次操作后的可用库存 | +| duplicated | Boolean | 是否为已提交首次结果的重放 | + +#### 请求示例 + +```json +{ + "date": "2026-09-09", + "qty": 2, + "opId": "deduct-asgn-123-d1-rt456-h789-0" +} +``` + +#### 响应示例 + +```json +{ + "code": 200, + "message": "成功", + "data": { "changed": 2, "remain": 8, "duplicated": false }, + "success": true +} +``` + +#### 空数据 / 降级响应 + +`hotel.stock.enabled=false` 时不改变库存,但仍冻结该 `opId` 的首次成功快照;开关恢复后重放不会补执行。 + +```json +{ "code": 200, "data": { "changed": 0, "remain": 10, "duplicated": false }, "success": true } +``` + +#### 错误响应 + +```json +{ + "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` + +#### 使用场景 + +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` + +| 字段 | 类型 | 说明 | +|------|------|------| +| changed | Integer | 实际释放数量,等于 `min(qty, stockUsed)` | +| remain | Integer | 操作后的可用库存 | +| duplicated | Boolean | 是否为已提交首次结果的重放 | + +#### 请求示例 + +```json +{ + "date": "2026-09-09", + "qty": 999, + "opId": "release-2097325826075738113-1" +} +``` + +#### 响应示例 + +```json +{ + "code": 200, + "message": "成功", + "data": { "changed": 1, "remain": 10, "duplicated": false }, + "success": true +} +``` + +#### 空数据 / 降级响应 + +当前已用量为 0 时,接口成功返回 `changed=0`;不会把已用量更新为负数。 + +```json +{ "code": 200, "data": { "changed": 0, "remain": 10, "duplicated": false }, "success": true } +``` + +#### 错误响应 + +```json +{ + "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 订单、计价与支付流程。 + +--- + +## 八、测试环境已验证 + +```text +两服务同批部署 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](https://git.1814.love:8443/wx/HL/issues/7339) +- 关联 PR: [wx/HL#7356](https://git.1814.love:8443/wx/HL/pulls/7356) +- 部署回退: R0 `439e53d1e`;R-1 `4a39b7fbb`;R-2 `f36a5b4e` + +## 关联 / 联系人 + +### 链接 + +- **Issue**: [#7339](https://git.1814.love:8443/wx/HL/issues/7339) +- **PR**: [#7356](https://git.1814.love:8443/wx/HL/pulls/7356) +- **Merge commit**: [439e53d1e273d58e68988c5db39db3e21d4733f2](https://git.1814.love:8443/wx/HL/commit/439e53d1e273d58e68988c5db39db3e21d4733f2) + +### 联系人 + +- **后端负责人**: @wx