Bläddra i källkod

feat: 完善库存管理功能 — 识别时扣减 + 退款区分场景

核心变更:
1. 库存扣减时机从"支付时"改为"AI识别时"(商品物理离开柜子即扣减),
   支付回调作为兜底,幂等性保证不重复扣减
2. IN类型(异物放入)不扣库存,标记异常等人工审核
3. 退款增加 restoreStock 标志区分场景:识别多扣→恢复库存,
   质量问题→不恢复库存,默认恢复
4. deleteStockOnPaid 增加幂等性检查(基于 t_inventory_log)
5. restoreStockOnRefund 支持按 RefundItem 明细逐商品恢复(部分退款)
6. 支付分 4 个路径 + 微信支付回调 + 订单回调 + 直接完成订单,
   共 7 个支付路径全部接入库存扣减(兜底)
7. 移除 ProductService.updateProductStock 死代码
8. 新增 docs/库存管理模块.md 说明文档

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
skyline 1 vecka sedan
förälder
incheckning
bb6b852274
19 ändrade filer med 632 tillägg och 112 borttagningar
  1. 362 0
      docs/库存管理模块.md
  2. 5 0
      haha-admin/src/main/java/com/haha/admin/controller/OrderController.java
  3. 1 0
      haha-admin/src/main/java/com/haha/admin/controller/RefundApplicationController.java
  4. 17 0
      haha-admin/src/main/resources/db/migration/V6__add_refund_restore_stock.sql
  5. 2 0
      haha-common/src/main/java/com/haha/common/vo/RefundApplicationVO.java
  6. 3 0
      haha-entity/src/main/java/com/haha/entity/Refund.java
  7. 3 0
      haha-entity/src/main/java/com/haha/entity/dto/RefundDTO.java
  8. 9 0
      haha-service/src/main/java/com/haha/service/InventoryLogService.java
  9. 8 4
      haha-service/src/main/java/com/haha/service/OrderInventoryService.java
  10. 0 5
      haha-service/src/main/java/com/haha/service/ProductService.java
  11. 6 5
      haha-service/src/main/java/com/haha/service/RefundService.java
  12. 21 0
      haha-service/src/main/java/com/haha/service/impl/HahaCallbackServiceImpl.java
  13. 8 0
      haha-service/src/main/java/com/haha/service/impl/InventoryLogServiceImpl.java
  14. 84 87
      haha-service/src/main/java/com/haha/service/impl/OrderInventoryServiceImpl.java
  15. 10 1
      haha-service/src/main/java/com/haha/service/impl/OrderServiceImpl.java
  16. 0 6
      haha-service/src/main/java/com/haha/service/impl/ProductServiceImpl.java
  17. 43 2
      haha-service/src/main/java/com/haha/service/impl/RefundServiceImpl.java
  18. 19 2
      haha-service/src/main/java/com/haha/service/payment/impl/PaymentServiceImpl.java
  19. 31 0
      haha-service/src/main/java/com/haha/service/payment/payscore/impl/PayScoreServiceImpl.java

+ 362 - 0
docs/库存管理模块.md

@@ -0,0 +1,362 @@
+# 库存管理模块说明
+
+> 本文档描述系统商品库存管理的完整架构,涵盖库存数据模型、服务层设计、库存变动流程、与订单/支付/退款模块的集成,以及幂等性保护机制。
+> 帮助开发人员理解库存数据的生命周期和关键设计决策。
+
+---
+
+## 一、模块概述
+
+### 1.1 库存管理范围
+
+本系统的库存管理**完全在本地进行**,haha 平台不提供库存管理 API。两者的分工:
+
+| 数据 | 管理方 | 说明 |
+|------|--------|------|
+| 商品信息(名称、条码、价格、图片) | haha 平台 → 本地同步 | `DataSyncService.syncProducts()` 定时拉取 |
+| 设备列表 | haha 平台 → 本地同步 | `DataSyncService.syncDevices()` |
+| 订单支付状态 | 本地 → haha 平台推送 | `notifyHahaOrderStatusChanged()` |
+| **库存数量** | **纯本地管理** | haha 不存库存,不提供库存接口 |
+
+### 1.2 库存维度
+
+库存按 **设备 + 商品** 维度管理,即同一商品在不同设备上有独立的库存记录(`t_device_inventory` 表)。
+
+### 1.3 为什么库存不放在 haha 平台?
+
+haha 平台的职责是 AI 识别(开关门事件 + 商品识别结果)和设备管理。其 SDK 提供的 `GoodsApi` 和 `OrderApi` 均无库存查询/更新端点,商品 API 仅管理产品目录和上架状态。
+
+---
+
+## 二、数据库设计
+
+### 2.1 核心表
+
+#### `t_device_inventory` — 设备库存表
+
+每台设备上每个商品一条记录,使用乐观锁保证并发安全。
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| `id` | BIGINT | 主键,雪花算法 |
+| `device_id` | VARCHAR(64) | 设备 SN |
+| `product_id` | BIGINT | 商品 ID(关联 `t_product`) |
+| `product_code` | VARCHAR(100) | haha 平台商品编码 |
+| `product_name` | VARCHAR(200) | 商品名称快照 |
+| `stock` | INT | **当前库存数量** |
+| `shelf_num` | INT | 货架层位置 |
+| `position` | VARCHAR(50) | 通道位置(left/right) |
+| `warning_threshold` | INT | 低库存预警阈值(默认 5) |
+| `last_restock_time` | DATETIME | 最近补货时间 |
+| `version` | INT | **乐观锁版本号**(MyBatis-Plus @Version) |
+| `create_time` | DATETIME | 创建时间 |
+| `update_time` | DATETIME | 更新时间 |
+
+唯一约束:`(device_id, product_id)`
+
+#### `t_inventory_log` — 库存变动日志表
+
+记录每一次库存变动的审计跟踪,用于溯源和统计。
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| `id` | BIGINT | 主键 |
+| `device_id` | VARCHAR(64) | 设备 ID |
+| `product_id` | BIGINT | 商品 ID |
+| `product_code` | VARCHAR(100) | 商品编码 |
+| `product_name` | VARCHAR(200) | 商品名称 |
+| `change_type` | INT | 变动类型(见下表) |
+| `change_quantity` | INT | 变动数量(正数增加,负数减少) |
+| `before_stock` | INT | 变动前库存 |
+| `after_stock` | INT | 变动后库存 |
+| `order_id` | VARCHAR(64) | 关联订单号(销售时) |
+| `activity_id` | VARCHAR(64) | 关联活动号 |
+| `operator_id` | BIGINT | 操作人 ID |
+| `operator_name` | VARCHAR(50) | 操作人名称 |
+| `remark` | VARCHAR(500) | 备注 |
+| `create_time` | DATETIME | 创建时间 |
+
+变动类型常量:
+
+| 常量 | 值 | 说明 |
+|------|---|------|
+| `TYPE_RESTOCK` | 1 | 上货增加 |
+| `TYPE_SALE` | 2 | 销售减少 |
+| `TYPE_ADJUST_ADD` | 3 | 调整增加 |
+| `TYPE_ADJUST_SUB` | 4 | 调整减少 |
+| `TYPE_INVENTORY` | 5 | 盘点调整 |
+
+#### `t_stock_record` — 上货记录表(补货任务)
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| `id` | BIGINT | 主键 |
+| `device_id` | VARCHAR(64) | 设备 ID |
+| `type` | INT | 类型:1=首次上货,2=补货 |
+| `status` | INT | 状态:1=进行中,2=已完成,3=已取消 |
+| `operator_id` | BIGINT | 操作人 ID |
+| `activity_id` | VARCHAR(64) | 活动号 |
+| `create_time` | DATETIME | 创建时间 |
+
+#### `t_stock_record_item` — 上货记录商品明细
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| `id` | BIGINT | 主键 |
+| `stock_record_id` | BIGINT | 上货记录 ID |
+| `product_id` | BIGINT | 商品 ID |
+| `product_code` | VARCHAR(100) | 商品编码 |
+| `product_name` | VARCHAR(200) | 商品名称 |
+| `quantity` | INT | 上货数量 |
+| `before_stock` | INT | 上货前库存 |
+| `after_stock` | INT | 上货后库存 |
+
+---
+
+## 三、服务层设计
+
+### 3.1 服务职责
+
+| 服务 | 职责 |
+|------|------|
+| `DeviceInventoryService` | 库存核心 CRUD:增、减、调整、查询、统计 |
+| `InventoryLogService` | 记录并查询库存变动日志,提供幂等性检查 |
+| `StockRecordService` | 管理上货任务(创建、完成、取消) |
+| `OrderInventoryService` | **桥梁服务**:连接订单/支付/退款与库存操作 |
+
+### 3.2 核心方法
+
+#### DeviceInventoryService
+
+```
+increaseStock()   → 补货入库,首次自动创建库存记录
+decreaseStock()   → 销售出库,库存不足抛 BusinessException
+adjustStock()     → 直接设置库存值,用于盘点/退款恢复
+getByDeviceAndProduct() → 查询单条库存记录
+getStatistics()   → 聚合统计(总记录数/总库存/低库存/零库存)
+```
+
+#### OrderInventoryService(桥梁服务)
+
+```
+decreaseStockOnPaid(Order)           → 扣减库存(幂等,识别时和支付时均可调用,先到达者生效)
+restoreStockOnRefund(Order, List)    → 退款成功后恢复库存(支持部分退款,受 restoreStock 标志控制)
+checkStockAvailable(deviceId, items) → 下单前校验库存是否充足
+```
+
+---
+
+## 四、库存变动流程
+
+### 4.1 补货入库流程
+
+```
+管理员/补货员 → POST /inventory/increase 或 POST /replenisher/stock/replenish
+    → DeviceInventoryService.increaseStock()
+        → 查询现有库存记录
+        → 不存在则新建,存在则累加
+        → InventoryLogService.logChange(TYPE_RESTOCK)
+```
+
+**特性**:首次为设备添加商品时自动创建库存记录;后续补货直接累加数量。
+
+### 4.2 销售出库流程(AI 识别后立即扣减)
+
+**核心原则**:用户关门那一刻,商品已物理离开柜子——库存应立即扣减,不等支付。支付是独立的财务环节,可能延迟甚至失败。
+
+**主流程**(识别时触发):
+
+```
+用户关门 → AI 识别完成 → handleOrcResult
+    → handleConsume (OUT 类型,正常取出)
+        → updateOrderFromRecognition(设置 items)
+        → decreaseStockOnPaid(order) ← 立即扣减
+            → 幂等性检查:查询 t_inventory_log 是否已有 TYPE_SALE 记录
+            → 解析订单 items JSON,遍历商品
+            → DeviceInventoryService.decreaseStock()
+                → 扣减并记录 InventoryLog(TYPE_SALE)
+    → IN 类型(异物放入)跳过,不扣库存
+```
+
+**兜底流程**(支付时触发,幂等性保证不会重复扣减):
+
+```
+用户支付成功
+    → 支付回调 / 支付分回调
+        → decreaseStockOnPaid(order) ← 若识别时已扣,幂等检查跳过
+```
+
+**触发点汇总**:
+
+| 阶段 | 触发路径 | 服务 | 方法 |
+|------|---------|------|------|
+| **识别时**(主) | AI 识别为 OUT 类型 | `HahaCallbackServiceImpl` | `handleConsume()` |
+| 支付时(兜底) | 微信支付 V3 回调 | `PaymentServiceImpl` | `handleCallback()` |
+| 支付时(兜底) | 支付分完结扣款成功 | `PayScoreServiceImpl` | `completeServiceOrder()` |
+| 支付时(兜底) | 支付分 USER_PAID 回调 | `PayScoreServiceImpl` | `handleCallback()` |
+| 支付时(兜底) | 支付分状态同步为 DONE | `PayScoreServiceImpl` | `syncPayScoreStatus()` |
+| 支付时(兜底) | 支付分同步支付信息 | `PayScoreServiceImpl` | `syncOrderPaid()` |
+| 支付时(兜底) | 订单回调中支付分扣费成功 | `HahaCallbackServiceImpl` | `processPayScorePayment()` |
+| 支付时(兜底) | 直接完成订单(无支付分) | `OrderServiceImpl` | `completeOrderWithPayScore()` |
+
+### 4.3 退款恢复库存流程(区分场景)
+
+**核心原则**:库存代表柜内物理商品数量。只有商品**没被拿走**时才恢复库存(如 AI 识别多扣);商品已被取走但退款(如质量问题)不应恢复。
+
+通过 `t_refund.restore_stock` 字段控制(默认 1=恢复):
+
+| 场景 | 举例 | `restoreStock` | 行为 |
+|------|------|----------------|------|
+| **识别多扣** | 拿了 1 瓶水,AI 识别成 2 瓶 | 1(默认) | 退钱 + **恢复库存** |
+| **订单错误** | 重复扣款、金额计算错误 | 1(默认) | 退钱 + **恢复库存**(用户实际没取走多余) |
+| **质量问题** | 拿了商品,发现有质量问题要退款 | 0 | 只退钱,**不恢复库存**(商品已取走) |
+| **用户不满** | 拿了商品,不满意要退款 | 0 | 只退钱,**不恢复库存**(商品已取走) |
+
+```
+管理员审核通过 / 直接退款成功
+    → RefundServiceImpl.approveRefund() 或 markRefunded()
+        → restoreStockForRefund(refund)
+            → 检查 refund.restoreStock:
+                → 0(不恢复)→ 跳过,仅记录日志
+                → 1 或 null(恢复)→ 继续
+            → 获取订单 + 退款商品明细(RefundItem 列表)
+            → OrderInventoryService.restoreStockOnRefund(order, refundItems)
+                → 逐商品 adjustStock(current + quantity)
+                → 记录 InventoryLog(TYPE_ADJUST_ADD)
+```
+
+**支持部分退款**:按 RefundItem 明细逐商品恢复,只恢复退款的那部分数量。
+
+---
+
+## 五、幂等性保护
+
+### 5.1 库存扣减幂等性
+
+`decreaseStockOnPaid()` 在识别时和支付时都会被调用,通过查询 `t_inventory_log` 表保证只扣一次:
+
+```java
+long deducted = inventoryLogService.countByOrderIdAndType(orderNo, TYPE_SALE);
+if (deducted > 0) {
+    log.info("订单库存已扣减,跳过重复扣减: orderNo={}", orderNo);
+    return;
+}
+```
+
+**保护场景**:
+- 识别时已扣减(主路径),支付回调到来时幂等跳过(兜底)
+- 微信支付回调可能因网络重试多次推送
+- 支付分回调与主动查询可能并发触发
+
+**注意**:扣减失败不阻断主流程(外部 try-catch 包裹),确保库存问题不影响用户支付体验。
+
+### 5.2 库存恢复幂等性
+
+退款恢复依赖 `restoreStock` 标志控制是否执行。如果执行恢复,按 `RefundItem` 精确的商品明细操作,每次调用只恢复本次退款的商品数量。
+
+**当前策略**:退款为管理员审核操作,重复调用概率极低。如需严格幂等,可在 `InventoryLog.remark` 中记录 `refundId` 实现精准去重。
+
+---
+
+## 六、API 端点
+
+### 6.1 管理后台(InventoryController)
+
+| 端点 | 方法 | 用途 | 权限 |
+|------|------|------|------|
+| `/inventory/device-stats` | GET | 按设备分组的库存统计 | `inventory:view` |
+| `/inventory/list` | GET | 库存列表(分页,可筛选) | `inventory:view` |
+| `/inventory/device/{deviceId}` | GET | 单台设备库存详情 | `inventory:view` |
+| `/inventory/low-stock` | GET | 低库存商品列表 | `inventory:view` |
+| `/inventory/statistics` | GET | 汇总统计 | `inventory:view` |
+| `/inventory/increase` | POST | 增加库存(上货) | `inventory:update` |
+| `/inventory/adjust` | POST | 调整库存 | `inventory:update` |
+| `/inventory/logs` | GET | 库存变动日志 | `inventory:view` |
+| `/inventory/logs/statistics` | GET | 库存变动统计 | `inventory:view` |
+| `/inventory/records` | GET | 上货记录列表 | `inventory:view` |
+| `/inventory/records/{id}` | GET | 上货记录详情 | `inventory:view` |
+| `/inventory/records` | POST | 创建上货记录(V1) | `inventory:update` |
+| `/inventory/records/v2` | POST | 创建上货记录(V2) | `inventory:update` |
+| `/inventory/records/{id}/complete` | PUT | 完成上货记录 | `inventory:update` |
+| `/inventory/records/{id}/cancel` | PUT | 取消上货记录 | `inventory:update` |
+| `/inventory/records/stocker-statistics` | GET | 补货员统计 | `inventory:view` |
+
+### 6.2 补货员端(ReplenisherOperationController)
+
+| 端点 | 方法 | 用途 |
+|------|------|------|
+| `/replenisher/my-info` | GET | 当前补货员信息 |
+| `/replenisher/device/list` | GET | 补货员管辖的设备列表(含库存概览) |
+| `/replenisher/device/inventory/{deviceId}` | GET | 单台设备库存详情 |
+| `/replenisher/stock/replenish` | POST | 执行补货操作 |
+
+---
+
+## 七、低库存预警
+
+系统支持低库存预警机制:
+
+- 每个库存记录的 `warning_threshold` 字段(默认值 5)
+- 当 `stock <= warning_threshold` 时,该商品出现在低库存列表中
+- 管理后台 `/inventory/low-stock` 和统计面板可查看低库存情况
+- 设备库存统计页按设备汇总:正常 / 低库存 / 缺货 三种状态
+
+---
+
+## 八、关键设计决策
+
+| 决策 | 原因 |
+|------|------|
+| **识别时立即扣减** | 关门时商品已物理离开柜子,库存应即时反映;不等支付(可能延迟/失败) |
+| 支付时兜底扣减 | 若识别时未扣(如异常跳过),支付回调仍会触发;幂等性保证不重复 |
+| 库存扣减不阻断主流程 | 库存问题不应影响用户支付/取货体验;异常仅记录日志 |
+| **退款恢复区分场景** | `restore_stock` 标志区分"识别多扣退库存"和"质量问题不退库存",避免恢复不该恢复的商品 |
+| 乐观锁而非悲观锁 | `t_device_inventory` 有 `@Version` 字段,MyBatis-Plus 自动处理并发冲突 |
+| 幂等性通过日志表实现 | 不在订单表增加 `stock_deducted` 字段,避免侵入;日志表本身就是审计需求 |
+| 退款恢复用 `adjustStock` | 相比 `increaseStock`,不需要补货所需的 shelf_num/position 等字段 |
+| 库存纯本地管理 | haha 平台不提供库存 API,且本地管理可灵活控制预警阈值、统计维度 |
+
+---
+
+## 九、相关文件索引
+
+### 实体
+- `haha-entity/.../entity/DeviceInventory.java`
+- `haha-entity/.../entity/InventoryLog.java`
+- `haha-entity/.../entity/StockRecord.java`
+- `haha-entity/.../entity/StockRecordItem.java`
+- `haha-entity/.../entity/Refund.java` — 含 `restoreStock` 标志
+- `haha-entity/.../entity/RefundItem.java`
+
+### 服务
+- `haha-service/.../service/DeviceInventoryService.java`
+- `haha-service/.../service/impl/DeviceInventoryServiceImpl.java`
+- `haha-service/.../service/InventoryLogService.java`
+- `haha-service/.../service/impl/InventoryLogServiceImpl.java`
+- `haha-service/.../service/OrderInventoryService.java`
+- `haha-service/.../service/impl/OrderInventoryServiceImpl.java`
+- `haha-service/.../service/StockRecordService.java`
+- `haha-service/.../service/impl/StockRecordServiceImpl.java`
+- `haha-service/.../service/RefundService.java`
+- `haha-service/.../service/impl/RefundServiceImpl.java`
+
+### 控制器
+- `haha-admin/.../controller/InventoryController.java`
+- `haha-admin/.../controller/ReplenisherOperationController.java`
+
+### SQL
+- `haha-admin/.../sql/inventory.sql`
+- `haha-admin/.../sql/inventory_test_data.sql`
+- `haha-admin/.../db/migration/V5__create_refund_tables.sql`
+- `haha-admin/.../db/migration/V6__add_refund_restore_stock.sql`
+
+### 集成点
+- `haha-service/.../service/impl/HahaCallbackServiceImpl.java` — **主路径**:AI 识别后立即扣减库存 + 支付分扣费兜底
+- `haha-service/.../service/payment/impl/PaymentServiceImpl.java` — 微信支付回调兜底扣减
+- `haha-service/.../service/payment/payscore/impl/PayScoreServiceImpl.java` — 支付分 4 个路径兜底扣减
+- `haha-service/.../service/impl/OrderServiceImpl.java` — 直接完成订单兜底扣减
+- `haha-service/.../service/impl/RefundServiceImpl.java` — 退款恢复库存(受 restoreStock 标志控制)
+
+### DTO / VO
+- `haha-entity/.../dto/RefundDTO.java` — 管理员退款请求,含 `restoreStock` 字段
+- `haha-common/.../vo/RefundApplicationVO.java` — 退款详情响应,含 `restoreStock` 字段

+ 5 - 0
haha-admin/src/main/java/com/haha/admin/controller/OrderController.java

@@ -135,6 +135,11 @@ public class OrderController {
             params.put("refundAmount", dto.getRefundAmount());
         }
 
+        // 退款后是否恢复库存(前端传入)
+        if (dto.getRestoreStock() != null) {
+            params.put("restoreStock", dto.getRestoreStock());
+        }
+
         // 调用支付服务执行退款
         try {
             RefundResult result = paymentService.refund(params);

+ 1 - 0
haha-admin/src/main/java/com/haha/admin/controller/RefundApplicationController.java

@@ -126,6 +126,7 @@ public class RefundApplicationController {
         vo.setReviewRemark(refund.getRemark());
         vo.setCreateTime(refund.getCreateTime());
         vo.setReviewTime(refund.getRefundTime());
+        vo.setRestoreStock(refund.getRestoreStock());
 
         // 状态映射:REFUNDED->1, REJECTED->2, 其他->0
         if (RefundStatus.REFUNDED.getCode().equals(refund.getStatus())) {

+ 17 - 0
haha-admin/src/main/resources/db/migration/V6__add_refund_restore_stock.sql

@@ -0,0 +1,17 @@
+-- ============================================
+-- t_refund 新增字段:是否恢复库存
+-- 1=恢复(默认),0=不恢复
+-- 用于区分"识别多扣退库存"和"质量问题不退库存"场景
+-- ============================================
+SET @col_exists = (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
+                   WHERE TABLE_SCHEMA = DATABASE()
+                   AND TABLE_NAME = 't_refund'
+                   AND COLUMN_NAME = 'restore_stock');
+
+SET @sql = IF(@col_exists = 0,
+    'ALTER TABLE `t_refund` ADD COLUMN `restore_stock` TINYINT NOT NULL DEFAULT 1 COMMENT ''退款后是否恢复库存:1=恢复,0=不恢复'' AFTER `status`',
+    'SELECT ''Column restore_stock already exists, skipping'' AS message');
+
+PREPARE stmt FROM @sql;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;

+ 2 - 0
haha-common/src/main/java/com/haha/common/vo/RefundApplicationVO.java

@@ -20,6 +20,8 @@ public class RefundApplicationVO {
     private String reason;
     private List<RefundProductVO> refundProducts;
     private Integer status;
+    /** 退款后是否恢复库存:1=恢复,0=不恢复 */
+    private Integer restoreStock;
     @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
     private LocalDateTime createTime;
     @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")

+ 3 - 0
haha-entity/src/main/java/com/haha/entity/Refund.java

@@ -53,6 +53,9 @@ public class Refund implements Serializable {
     @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
     private LocalDateTime updateTime;
 
+    /** 退款后是否恢复库存:1=恢复,0=不恢复(默认1)。用于区分"识别多扣退库存"和"质量问题不退库存"场景 */
+    private Integer restoreStock;
+
     public boolean isPending() {
         return "PENDING".equals(status);
     }

+ 3 - 0
haha-entity/src/main/java/com/haha/entity/dto/RefundDTO.java

@@ -12,6 +12,9 @@ public class RefundDTO {
     /** 自定义退款金额(当没有选择商品时使用) */
     private BigDecimal refundAmount;
 
+    /** 退款后是否恢复库存:true=恢复(默认),false=不恢复。用于区分"识别多扣退库存"和"质量问题不退库存" */
+    private Boolean restoreStock;
+
     /** 退款商品列表(选择商品退款时使用,优先级高于 refundAmount) */
     private List<RefundProductDTO> products;
 

+ 9 - 0
haha-service/src/main/java/com/haha/service/InventoryLogService.java

@@ -65,4 +65,13 @@ public interface InventoryLogService extends IService<InventoryLog> {
      * @return 变动记录列表
      */
     List<Map<String, Object>> getByProductCode(String productCode, int limit);
+
+    /**
+     * 统计指定订单号和变动类型的日志数量(用于幂等性检查)
+     *
+     * @param orderId    订单号
+     * @param changeType 变动类型
+     * @return 日志数量
+     */
+    long countByOrderIdAndType(String orderId, Integer changeType);
 }

+ 8 - 4
haha-service/src/main/java/com/haha/service/OrderInventoryService.java

@@ -1,6 +1,9 @@
 package com.haha.service;
 
 import com.haha.entity.Order;
+import com.haha.entity.RefundItem;
+
+import java.util.List;
 
 /**
  * 订单库存服务接口
@@ -9,18 +12,19 @@ import com.haha.entity.Order;
 public interface OrderInventoryService {
 
     /**
-     * 订单支付成功,扣减库存
+     * 订单支付成功,扣减库存(幂等:重复调用自动跳过)
      *
      * @param order 订单
      */
     void decreaseStockOnPaid(Order order);
 
     /**
-     * 订单退款,恢复库存
+     * 订单退款,恢复库存(按退款商品明细恢复,支持部分退款)
      *
-     * @param order 订单
+     * @param order       订单
+     * @param refundItems 退款商品明细
      */
-    void restoreStockOnRefund(Order order);
+    void restoreStockOnRefund(Order order, List<RefundItem> refundItems);
 
     /**
      * 检查订单商品库存是否充足

+ 0 - 5
haha-service/src/main/java/com/haha/service/ProductService.java

@@ -76,11 +76,6 @@ public interface ProductService extends IService<Product> {
      */
     List<Product> getProductListBydeviceId(String deviceId);
     
-    /**
-     * 更新商品库存
-     */
-    boolean updateProductStock(String deviceId, Long productId, Integer stock);
-    
     /**
      * 根据分类ID获取商品列表
      */

+ 6 - 5
haha-service/src/main/java/com/haha/service/RefundService.java

@@ -14,13 +14,14 @@ public interface RefundService extends IService<Refund> {
     /**
      * 创建退款记录(含商品明细)
      *
-     * @param orderId 订单ID
-     * @param reason 退款原因
-     * @param products 退款商品列表(可选)
-     * @param refundAmount 自定义退款金额(可选)
+     * @param orderId       订单ID
+     * @param reason        退款原因
+     * @param products      退款商品列表(可选)
+     * @param refundAmount  自定义退款金额(可选)
+     * @param restoreStock  退款后是否恢复库存:true=恢复(默认),false=不恢复
      * @return 退款记录
      */
-    Refund createRefundRecord(Long orderId, String reason, List<Map<String, Object>> products, BigDecimal refundAmount);
+    Refund createRefundRecord(Long orderId, String reason, List<Map<String, Object>> products, BigDecimal refundAmount, Boolean restoreStock);
 
     /**
      * 从Map参数创建退款记录

+ 21 - 0
haha-service/src/main/java/com/haha/service/impl/HahaCallbackServiceImpl.java

@@ -103,6 +103,9 @@ public class HahaCallbackServiceImpl implements HahaCallbackService {
     @Autowired
     private com.haha.service.ProductService productService;
 
+    @Autowired
+    private com.haha.service.OrderInventoryService orderInventoryService;
+
     @Override
     public void handleMessage(Map<String, Object> params) {
         // 签名验证:确保回调来自哈哈平台
@@ -438,6 +441,17 @@ public class HahaCallbackServiceImpl implements HahaCallbackService {
         updateOrderFromRecognition(order, skuListStr, resourceInfoStr, confidence, activityId);
         logRecognizeResultDetails(resultStr, skuListStr, resourceInfoStr);
 
+        // OUT类型(正常取出):识别后立即扣减库存,因为商品已物理离开柜子
+        // IN类型(异物放入):不扣库存,标记异常等人工审核
+        if (!RecognizeActionType.IN.getCode().equals(order.getOrderType())) {
+            try {
+                orderInventoryService.decreaseStockOnPaid(order);
+                log.info("识别后扣减库存: orderNo={}, orderType={}", order.getOrderNo(), order.getOrderType());
+            } catch (Exception e) {
+                log.error("识别后扣减库存异常: orderNo={}", order.getOrderNo(), e);
+            }
+        }
+
         // 低置信度订单拉取开门前后图片供人工审核
         if (confidence != null && confidence.compareTo(PaymentConstants.CONFIDENCE_THRESHOLD) < 0) {
             log.info("识别置信度较低({}),拉取订单图像供人工审核 - activityId: {}", confidence, activityId);
@@ -1477,6 +1491,13 @@ public class HahaCallbackServiceImpl implements HahaCallbackService {
                             order.getId(), state, result.getOutOrderNo());
                     // 通知哈哈平台支付已完成
                     syncPayStatusToHaha(order);
+
+                    // 支付成功后扣减库存
+                    try {
+                        orderInventoryService.decreaseStockOnPaid(order);
+                    } catch (Exception e) {
+                        log.error("支付分扣费后扣减库存异常: orderNo={}", order.getOrderNo(), e);
+                    }
                 } else if (PayScoreState.isPaying(state)) {
                     log.warn("支付分扣款已发起但用户待支付 - orderId: {}, state: {}, outOrderNo: {}",
                             order.getId(), state, result.getOutOrderNo());

+ 8 - 0
haha-service/src/main/java/com/haha/service/impl/InventoryLogServiceImpl.java

@@ -105,4 +105,12 @@ public class InventoryLogServiceImpl extends ServiceImpl<InventoryLogMapper, Inv
     public List<Map<String, Object>> getByProductCode(String productCode, int limit) {
         return inventoryLogMapper.selectByProductCode(productCode, limit);
     }
+
+    @Override
+    public long countByOrderIdAndType(String orderId, Integer changeType) {
+        return lambdaQuery()
+                .eq(InventoryLog::getOrderId, orderId)
+                .eq(InventoryLog::getChangeType, changeType)
+                .count();
+    }
 }

+ 84 - 87
haha-service/src/main/java/com/haha/service/impl/OrderInventoryServiceImpl.java

@@ -5,7 +5,9 @@ import com.fasterxml.jackson.databind.ObjectMapper;
 import com.haha.entity.DeviceInventory;
 import com.haha.entity.InventoryLog;
 import com.haha.entity.Order;
+import com.haha.entity.RefundItem;
 import com.haha.service.DeviceInventoryService;
+import com.haha.service.InventoryLogService;
 import com.haha.service.OrderInventoryService;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -25,128 +27,108 @@ public class OrderInventoryServiceImpl implements OrderInventoryService {
     @Autowired
     private DeviceInventoryService deviceInventoryService;
 
+    @Autowired
+    private InventoryLogService inventoryLogService;
+
     @Autowired
     private ObjectMapper objectMapper;
 
     @Override
     @Transactional(rollbackFor = Exception.class)
     public void decreaseStockOnPaid(Order order) {
+        String orderNo = order.getOrderNo();
+
+        // 幂等性检查:已扣减过则跳过
+        long deductedCount = inventoryLogService.countByOrderIdAndType(orderNo, InventoryLog.TYPE_SALE);
+        if (deductedCount > 0) {
+            log.info("订单库存已扣减,跳过重复扣减: orderNo={}", orderNo);
+            return;
+        }
+
         if (order.getItems() == null || order.getItems().isEmpty()) {
-            log.warn("订单商品明细为空,跳过库存扣减: orderNo={}", order.getOrderNo());
+            log.warn("订单商品明细为空,跳过库存扣减: orderNo={}", orderNo);
             return;
         }
 
         try {
-            // 解析商品明细
             List<Map<String, Object>> items = objectMapper.readValue(
                     order.getItems(),
                     new TypeReference<List<Map<String, Object>>>() {}
             );
 
             String deviceId = order.getDeviceId();
-            String orderNo = order.getOrderNo();
 
             for (Map<String, Object> item : items) {
-                String productCode = (String) item.get("productCode");
-                if (productCode == null) {
-                    productCode = (String) item.get("code");
-                }
-                String productName = (String) item.get("productName");
-                if (productName == null) {
-                    productName = (String) item.get("name");
-                }
+                Long productId = extractProductId(item);
+                Integer quantity = extractQuantity(item);
+                String productCode = extractProductCode(item);
 
-                Integer quantity = 1;
-                Object qtyObj = item.get("quantity");
-                if (qtyObj != null) {
-                    quantity = Integer.parseInt(qtyObj.toString());
+                if (productId == null) {
+                    log.warn("商品ID为空,跳过扣减: orderNo={}, item={}", orderNo, item);
+                    continue;
                 }
 
-                Long productId = null;
-                Object productIdObj = item.get("productId");
-                if (productIdObj != null) {
-                    productId = Long.parseLong(productIdObj.toString());
-                }
-
-                // 扣减库存
-                if (productId != null) {
+                try {
                     deviceInventoryService.decreaseStock(
                             deviceId,
                             productId,
                             quantity,
                             orderNo,
-                            null, // 系统自动操作
+                            null,
                             "系统"
                     );
                     log.info("订单扣减库存: orderNo={}, deviceId={}, productCode={}, quantity={}",
                             orderNo, deviceId, productCode, quantity);
+                } catch (Exception e) {
+                    log.error("单件商品库存扣减失败: orderNo={}, productId={}, quantity={}, error={}",
+                            orderNo, productId, quantity, e.getMessage());
                 }
             }
         } catch (Exception e) {
-            log.error("订单库存扣减失败: orderNo={}", order.getOrderNo(), e);
-            // 不抛出异常,避免影响订单流程
+            log.error("订单库存扣减失败: orderNo={}", orderNo, e);
         }
     }
 
     @Override
     @Transactional(rollbackFor = Exception.class)
-    public void restoreStockOnRefund(Order order) {
-        if (order.getItems() == null || order.getItems().isEmpty()) {
-            log.warn("订单商品明细为空,跳过库存恢复: orderNo={}", order.getOrderNo());
+    public void restoreStockOnRefund(Order order, List<RefundItem> refundItems) {
+        if (refundItems == null || refundItems.isEmpty()) {
+            log.warn("退款商品明细为空,跳过库存恢复: orderNo={}", order.getOrderNo());
             return;
         }
 
-        try {
-            // 解析商品明细
-            List<Map<String, Object>> items = objectMapper.readValue(
-                    order.getItems(),
-                    new TypeReference<List<Map<String, Object>>>() {}
-            );
-
-            String deviceId = order.getDeviceId();
-            String orderNo = order.getOrderNo();
-
-            for (Map<String, Object> item : items) {
-                Long productId = null;
-                Object productIdObj = item.get("productId");
-                if (productIdObj != null) {
-                    productId = Long.parseLong(productIdObj.toString());
-                }
+        String deviceId = order.getDeviceId();
+        String orderNo = order.getOrderNo();
 
-                String productCode = (String) item.get("productCode");
-                if (productCode == null) {
-                    productCode = (String) item.get("code");
-                }
-                String productName = (String) item.get("productName");
-                if (productName == null) {
-                    productName = (String) item.get("name");
-                }
+        for (RefundItem item : refundItems) {
+            if (item.getProductId() == null) {
+                log.warn("退款商品ID为空,跳过库存恢复: orderNo={}", orderNo);
+                continue;
+            }
 
-                Integer quantity = 1;
-                Object qtyObj = item.get("quantity");
-                if (qtyObj != null) {
-                    quantity = Integer.parseInt(qtyObj.toString());
+            try {
+                DeviceInventory inventory = deviceInventoryService.getByDeviceAndProduct(deviceId, item.getProductId());
+                if (inventory == null) {
+                    log.warn("设备库存记录不存在,跳过恢复: deviceId={}, productId={}, orderNo={}",
+                            deviceId, item.getProductId(), orderNo);
+                    continue;
                 }
 
-                // 恢复库存(使用调整方式)
-                if (productId != null) {
-                    DeviceInventory inventory = deviceInventoryService.getByDeviceAndProduct(deviceId, productId);
-                    if (inventory != null) {
-                        deviceInventoryService.adjustStock(
-                                deviceId,
-                                productId,
-                                inventory.getStock() + quantity,
-                                "订单退款恢复库存: " + orderNo,
-                                null,
-                                "系统"
-                        );
-                        log.info("退款恢复库存: orderNo={}, deviceId={}, productCode={}, quantity={}",
-                                orderNo, deviceId, productCode, quantity);
-                    }
-                }
+                int newStock = inventory.getStock() + item.getQuantity();
+                deviceInventoryService.adjustStock(
+                        deviceId,
+                        item.getProductId(),
+                        newStock,
+                        "退款恢复库存: orderNo=" + orderNo + ", refundId=" + item.getRefundId(),
+                        null,
+                        "系统"
+                );
+                log.info("退款恢复库存: orderNo={}, deviceId={}, productId={}, quantity={}, stockBefore={}, stockAfter={}",
+                        orderNo, deviceId, item.getProductId(), item.getQuantity(), inventory.getStock(), newStock);
+            } catch (Exception e) {
+                log.error("退款恢复库存失败: orderNo={}, productId={}, error={}",
+                        orderNo, item.getProductId(), e.getMessage());
             }
-        } catch (Exception e) {
-            log.error("订单库存恢复失败: orderNo={}", order.getOrderNo(), e);
         }
     }
 
@@ -163,17 +145,8 @@ public class OrderInventoryServiceImpl implements OrderInventoryService {
             );
 
             for (Map<String, Object> item : itemsList) {
-                Long productId = null;
-                Object productIdObj = item.get("productId");
-                if (productIdObj != null) {
-                    productId = Long.parseLong(productIdObj.toString());
-                }
-
-                Integer quantity = 1;
-                Object qtyObj = item.get("quantity");
-                if (qtyObj != null) {
-                    quantity = Integer.parseInt(qtyObj.toString());
-                }
+                Long productId = extractProductId(item);
+                Integer quantity = extractQuantity(item);
 
                 if (productId != null) {
                     DeviceInventory inventory = deviceInventoryService.getByDeviceAndProduct(deviceId, productId);
@@ -185,7 +158,31 @@ public class OrderInventoryServiceImpl implements OrderInventoryService {
             return true;
         } catch (Exception e) {
             log.error("检查库存可用性失败", e);
-            return true; // 解析失败默认可用,避免影响正常流程
+            return true;
+        }
+    }
+
+    private Long extractProductId(Map<String, Object> item) {
+        Object productIdObj = item.get("productId");
+        if (productIdObj != null) {
+            return Long.parseLong(productIdObj.toString());
+        }
+        return null;
+    }
+
+    private Integer extractQuantity(Map<String, Object> item) {
+        Object qtyObj = item.get("quantity");
+        if (qtyObj != null) {
+            return Integer.parseInt(qtyObj.toString());
+        }
+        return 1;
+    }
+
+    private String extractProductCode(Map<String, Object> item) {
+        String code = (String) item.get("productCode");
+        if (code == null) {
+            code = (String) item.get("code");
         }
+        return code;
     }
 }

+ 10 - 1
haha-service/src/main/java/com/haha/service/impl/OrderServiceImpl.java

@@ -100,6 +100,10 @@ public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements
     @Lazy
     private com.haha.service.ProductService productService;
 
+    @Autowired
+    @Lazy
+    private com.haha.service.OrderInventoryService orderInventoryService;
+
     @Override
     public IPage<Order> getPage(int page, int pageSize, String orderNo, String deviceId,
                                  String payStatus, Integer status, String startDate, String endDate,
@@ -729,8 +733,13 @@ public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements
             order.setPayTime(LocalDateTime.now());
             boolean updateResult = this.updateById(order);
 
-            // 订单完成后异步触发邀请激活逻辑
+            // 支付成功后扣减库存
             if (updateResult) {
+                try {
+                    orderInventoryService.decreaseStockOnPaid(order);
+                } catch (Exception e) {
+                    log.error("[支付分集成] 扣减库存异常 - orderNo={}", order.getOrderNo(), e);
+                }
                 triggerInviteActivationAsync(order.getUserId(), orderId);
             }
 

+ 0 - 6
haha-service/src/main/java/com/haha/service/impl/ProductServiceImpl.java

@@ -157,12 +157,6 @@ public class ProductServiceImpl extends ServiceImpl<ProductMapper, Product> impl
         return list();
     }
 
-    @Override
-    public boolean updateProductStock(String deviceId, Long productId, Integer stock) {
-        // 这里需要实现更新商品库存的逻辑
-        return true;
-    }
-
     @Override
     public List<Product> getProductListByCategory(Integer categoryId) {
         // 这里需要实现根据分类ID获取商品列表的逻辑,暂时返回所有商品

+ 43 - 2
haha-service/src/main/java/com/haha/service/impl/RefundServiceImpl.java

@@ -41,6 +41,9 @@ public class RefundServiceImpl extends ServiceImpl<RefundMapper, Refund> impleme
     @Lazy
     private PaymentService paymentService;
 
+    @Autowired
+    private com.haha.service.OrderInventoryService orderInventoryService;
+
     public RefundServiceImpl(RefundItemMapper refundItemMapper, OrderService orderService,
                              OrderGoodsService orderGoodsService) {
         this.refundItemMapper = refundItemMapper;
@@ -50,7 +53,7 @@ public class RefundServiceImpl extends ServiceImpl<RefundMapper, Refund> impleme
 
     @Override
     @Transactional(rollbackFor = Exception.class)
-    public Refund createRefundRecord(Long orderId, String reason, List<Map<String, Object>> products, BigDecimal refundAmount) {
+    public Refund createRefundRecord(Long orderId, String reason, List<Map<String, Object>> products, BigDecimal refundAmount, Boolean restoreStock) {
         Order order = orderService.getById(orderId);
         if (order == null) {
             throw new RuntimeException("订单不存在");
@@ -121,6 +124,8 @@ public class RefundServiceImpl extends ServiceImpl<RefundMapper, Refund> impleme
         refund.setRefundAmount(finalRefundAmount);
         refund.setTotalAmount(order.getTotalAmount());
         refund.setStatus(RefundStatus.PENDING.getCode());
+        // 退款后是否恢复库存:默认为恢复(1),除非前端明确传 false
+        refund.setRestoreStock(restoreStock == null || restoreStock ? 1 : 0);
         refund.setReason(reason);
         refund.setCreateTime(LocalDateTime.now());
         refund.setUpdateTime(LocalDateTime.now());
@@ -174,7 +179,13 @@ public class RefundServiceImpl extends ServiceImpl<RefundMapper, Refund> impleme
             refundAmount = new BigDecimal(params.get("refundAmount").toString());
         }
 
-        return createRefundRecord(orderId, reason, products, refundAmount);
+        Boolean restoreStock = null;
+        if (params.containsKey("restoreStock") && params.get("restoreStock") != null) {
+            Object val = params.get("restoreStock");
+            restoreStock = val instanceof Boolean ? (Boolean) val : Boolean.parseBoolean(val.toString());
+        }
+
+        return createRefundRecord(orderId, reason, products, refundAmount, restoreStock);
     }
 
     @Override
@@ -285,6 +296,10 @@ public class RefundServiceImpl extends ServiceImpl<RefundMapper, Refund> impleme
             refund.setReviewTime(LocalDateTime.now());
             refund.setUpdateTime(LocalDateTime.now());
             this.updateById(refund);
+
+            // 退款成功后恢复库存
+            restoreStockForRefund(refund);
+
             log.info("退款审核通过并执行成功 - refundId: {}, transactionId: {}", refundId, result.getRefundId());
             return true;
         } else {
@@ -330,6 +345,32 @@ public class RefundServiceImpl extends ServiceImpl<RefundMapper, Refund> impleme
             refund.setRefundTime(LocalDateTime.now());
             refund.setUpdateTime(LocalDateTime.now());
             this.updateById(refund);
+
+            // 退款成功后恢复库存
+            restoreStockForRefund(refund);
+        }
+    }
+
+    /**
+     * 退款成功后恢复对应商品的库存
+     * 仅当 restoreStock=1 时执行(质量问题等不需要退库存的场景跳过)
+     */
+    private void restoreStockForRefund(Refund refund) {
+        // restoreStock 默认为 1(恢复),仅当明确为 0 时跳过
+        if (refund.getRestoreStock() != null && refund.getRestoreStock() == 0) {
+            log.info("退款标记为不恢复库存,跳过: refundId={}, orderId={}", refund.getId(), refund.getOrderId());
+            return;
+        }
+        try {
+            Order order = orderService.getById(refund.getOrderId());
+            if (order == null) {
+                log.warn("订单不存在,跳过库存恢复: orderId={}", refund.getOrderId());
+                return;
+            }
+            List<RefundItem> refundItems = getRefundItems(refund.getId());
+            orderInventoryService.restoreStockOnRefund(order, refundItems);
+        } catch (Exception e) {
+            log.error("退款恢复库存异常: refundId={}, orderId={}", refund.getId(), refund.getOrderId(), e);
         }
     }
 

+ 19 - 2
haha-service/src/main/java/com/haha/service/payment/impl/PaymentServiceImpl.java

@@ -45,6 +45,9 @@ public class PaymentServiceImpl implements PaymentService {
 
     @Autowired
     private RefundService refundService;
+
+    @Autowired
+    private com.haha.service.OrderInventoryService orderInventoryService;
     
     @Override
     public PaymentResult createPayment(Long orderId, PaymentChannel channel, Map<String, String> extra) {
@@ -374,8 +377,15 @@ public class PaymentServiceImpl implements PaymentService {
                 order.setTransactionId(result.getTransactionId());
                 order.setPayTime(result.getPayTime() != null ? result.getPayTime() : LocalDateTime.now());
                 orderService.updateById(order);
-                log.info("订单更新成功: orderId={}, transactionId={}, payTime={}", 
+                log.info("订单更新成功: orderId={}, transactionId={}, payTime={}",
                         order.getId(), order.getTransactionId(), order.getPayTime());
+
+                // 支付成功后扣减库存
+                try {
+                    orderInventoryService.decreaseStockOnPaid(order);
+                } catch (Exception e) {
+                    log.error("支付回调扣减库存异常: orderNo={}", orderNo, e);
+                }
             } else {
                 log.error("订单不存在,无法更新transactionId: orderNo={}", orderNo);
             }
@@ -544,9 +554,16 @@ public class PaymentServiceImpl implements PaymentService {
         // 是否立即执行退款(管理员直接退款 vs 用户提交申请)
         boolean immediate = params.containsKey("immediate") && Boolean.TRUE.equals(params.get("immediate"));
 
+        // 退款后是否恢复库存(默认恢复)
+        Boolean restoreStock = null;
+        if (params.containsKey("restoreStock") && params.get("restoreStock") != null) {
+            Object val = params.get("restoreStock");
+            restoreStock = val instanceof Boolean ? (Boolean) val : Boolean.parseBoolean(val.toString());
+        }
+
         try {
             // 创建退款记录(PENDING)
-            com.haha.entity.Refund refundRecord = refundService.createRefundRecord(orderId, reason, products, customRefundAmount);
+            com.haha.entity.Refund refundRecord = refundService.createRefundRecord(orderId, reason, products, customRefundAmount, restoreStock);
             BigDecimal refundAmount = refundRecord.getRefundAmount();
 
             if (immediate) {

+ 31 - 0
haha-service/src/main/java/com/haha/service/payment/payscore/impl/PayScoreServiceImpl.java

@@ -55,6 +55,9 @@ public class PayScoreServiceImpl implements PayScoreService {
     @Autowired(required = false)
     private StringRedisTemplate stringRedisTemplate;
 
+    @Autowired
+    private com.haha.service.OrderInventoryService orderInventoryService;
+
     private static final String REDIS_KEY_PREFIX = "payscore:preauth:";
     private static final long REDIS_TTL_MINUTES = 10;
     private static final String REDIS_TRACKING_KEY = "payscore:preauth:tracking:";
@@ -186,6 +189,13 @@ public class PayScoreServiceImpl implements PayScoreService {
                 // 同步支付状态到哈哈平台
                 orderService.notifyHahaOrderStatusChanged(order.getOrderNo());
                 log.info("[支付分服务] 完结并扣款成功 - orderId: {}, state: {}", orderId, state);
+
+                // 支付成功后扣减库存
+                try {
+                    orderInventoryService.decreaseStockOnPaid(order);
+                } catch (Exception e) {
+                    log.error("[支付分服务] 扣减库存异常 - orderNo={}", order.getOrderNo(), e);
+                }
             } else if (PayScoreState.isPaying(state)) {
                 log.warn("[支付分服务] 完结成功但用户待支付 - orderId: {}, state: {}", orderId, state);
             } else {
@@ -317,6 +327,13 @@ public class PayScoreServiceImpl implements PayScoreService {
             }
             log.info("[支付分服务] 用户完成支付 - orderId: {}, amount: {}元, transactionId: {}",
                     order.getId(), result.getTotalAmount(), result.getTransactionId());
+
+            // 支付成功后扣减库存
+            try {
+                orderInventoryService.decreaseStockOnPaid(order);
+            } catch (Exception e) {
+                log.error("[支付分服务] 扣减库存异常 - orderNo={}", order.getOrderNo(), e);
+            }
         } else if ("PAYSCORE.REFUND_PAID".equals(eventType)) {
             order.setRefundStatus(RefundStatus.REFUNDED.getCode());
             order.setPayStatus(OrderConstants.PAY_STATUS_REFUND);
@@ -394,6 +411,13 @@ public class PayScoreServiceImpl implements PayScoreService {
                 }
                 // 同步支付状态到哈哈平台
                 orderService.notifyHahaOrderStatusChanged(order.getOrderNo());
+
+                // 支付成功后扣减库存
+                try {
+                    orderInventoryService.decreaseStockOnPaid(order);
+                } catch (Exception e) {
+                    log.error("[支付分服务] 扣减库存异常 - orderNo={}", order.getOrderNo(), e);
+                }
             } else if (PayScoreState.isRevoked(newState)) {
                 order.setStatus(OrderStatus.CANCELLED.getCode());
             } else if (PayScoreState.isExpired(newState)) {
@@ -518,6 +542,13 @@ public class PayScoreServiceImpl implements PayScoreService {
             order.setPayScoreFailReason(null);
             orderService.updateById(order);
             log.info("[支付分服务] 同步支付信息成功 - orderId: {}", orderId);
+
+            // 支付成功后扣减库存
+            try {
+                orderInventoryService.decreaseStockOnPaid(order);
+            } catch (Exception e) {
+                log.error("[支付分服务] 扣减库存异常 - orderNo={}", order.getOrderNo(), e);
+            }
         }
 
         return result;