Forráskód Böngészése

fix: 支付分扣费竞态条件修复 + items JSON解析兼容 + 漏单修复接口

三项修复:
1. payChannel 提前设置:createOrderWithPayScore 中先写 payChannel 再调支付分API,消除回调到达时 payChannel=null 的竞态窗口
2. processPayScorePayment 双保险:同时检查 payChannel 和 payScoreOrderId,payChannel 被覆盖时自动补全
3. parseItems JSON解析兼容:支持单对象和数组两种格式,解决 deserialize ArrayList 时遇到 JSON Object 的 MismatchedInputException
4. 新增 POST /order/repair-payscore 接口:修复已被跳过的未扣费支付分订单

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
skyline 6 napja
szülő
commit
41854ed1f5

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

@@ -249,4 +249,15 @@ public class OrderController {
         data.put("fixed", fixed);
         return Result.success("修复完成", data);
     }
+
+    /**
+     * 修复漏掉的支付分扣费:查询有支付分服务订单但未完成扣费的订单,重新发起扣费
+     */
+    @RequirePermission("order:update")
+    @PostMapping("/repair-payscore")
+    public Result<Map<String, Object>> repairPayScorePayments() {
+        com.haha.service.impl.OrderServiceImpl impl = (com.haha.service.impl.OrderServiceImpl) orderService;
+        Map<String, Object> data = impl.repairPayScorePayments();
+        return Result.success("支付分扣费修复完成", data);
+    }
 }

+ 10 - 2
haha-service/src/main/java/com/haha/service/impl/HahaCallbackServiceImpl.java

@@ -1448,13 +1448,21 @@ public class HahaCallbackServiceImpl implements HahaCallbackService {
      * @param orderMoney 订单金额
      */
     private void processPayScorePayment(Order order, Object orderMoney) {
-        // 检查是否为支付分订单
-        if (order.getPayChannel() == null || !PaymentChannel.WECHAT_PAYSCORE.getCode().equals(order.getPayChannel())) {
+        // 检查是否为支付分订单(payChannel 可能因并发回调被覆盖,同时检查 payScoreOrderId)
+        boolean isPayScore = PaymentChannel.WECHAT_PAYSCORE.getCode().equals(order.getPayChannel())
+                || order.getPayScoreOrderId() != null;
+        if (!isPayScore) {
             log.debug("订单未使用支付分,跳过扣费 - orderId: {}, payChannel: {}",
                     order.getId(), order.getPayChannel());
             return;
         }
 
+        // 补全可能丢失的 payChannel
+        if (order.getPayChannel() == null) {
+            order.setPayChannel(PaymentChannel.WECHAT_PAYSCORE.getCode());
+            order.setPayType("微信支付分");
+        }
+
         // 检查支付分服务是否可用
         if (payScoreService == null) {
             log.warn("支付分服务未初始化,无法处理扣费 - orderId: {}", order.getId());

+ 21 - 8
haha-service/src/main/java/com/haha/service/impl/OrderInventoryServiceImpl.java

@@ -56,10 +56,7 @@ public class OrderInventoryServiceImpl implements OrderInventoryService {
         }
 
         try {
-            List<Map<String, Object>> items = objectMapper.readValue(
-                    order.getItems(),
-                    new TypeReference<List<Map<String, Object>>>() {}
-            );
+            List<Map<String, Object>> items = parseItems(order.getItems());
 
             String deviceId = order.getDeviceId();
 
@@ -152,10 +149,7 @@ public class OrderInventoryServiceImpl implements OrderInventoryService {
         }
 
         try {
-            List<Map<String, Object>> itemsList = objectMapper.readValue(
-                    items,
-                    new TypeReference<List<Map<String, Object>>>() {}
-            );
+            List<Map<String, Object>> itemsList = parseItems(items);
 
             for (Map<String, Object> item : itemsList) {
                 Long productId = extractProductId(item);
@@ -184,6 +178,25 @@ public class OrderInventoryServiceImpl implements OrderInventoryService {
         }
     }
 
+    /**
+     * 解析items字段,兼容JSON数组和单个JSON对象两种格式
+     */
+    private List<Map<String, Object>> parseItems(String items) {
+        try {
+            return objectMapper.readValue(items,
+                    new TypeReference<List<Map<String, Object>>>() {});
+        } catch (Exception e) {
+            try {
+                Map<String, Object> single = objectMapper.readValue(items,
+                        new TypeReference<Map<String, Object>>() {});
+                return java.util.Collections.singletonList(single);
+            } catch (Exception ex) {
+                log.error("解析订单items失败: items={}", items, ex);
+                return java.util.Collections.emptyList();
+            }
+        }
+    }
+
     private Long extractProductId(Map<String, Object> item) {
         Object productIdObj = item.get("productId");
         if (productIdObj != null) {

+ 55 - 2
haha-service/src/main/java/com/haha/service/impl/OrderServiceImpl.java

@@ -9,6 +9,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import com.haha.common.constant.OrderConstants;
 import com.haha.common.enums.OrderStatus;
+import com.haha.common.enums.PayScoreState;
 import com.haha.common.enums.PayStatus;
 import com.haha.common.enums.PaymentChannel;
 import com.haha.common.config.CommonConfig;
@@ -655,6 +656,11 @@ public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements
             return null;
         }
 
+        // 立即设置支付渠道,防止后续回调到达时 payChannel 为 null 而跳过扣费
+        order.setPayChannel(PaymentChannel.WECHAT_PAYSCORE.getCode());
+        order.setPayType("微信支付分");
+        this.updateById(order);
+
         if (openId == null || openId.isEmpty()) {
             log.warn("[支付分集成] openId为空,跳过支付分服务订单创建 - orderId: {}", order.getId());
             return order;
@@ -668,8 +674,6 @@ public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements
                         order.getId(), result.getOutOrderNo());
                 order.setPayScoreOrderId(result.getOutOrderNo());
                 order.setPayScoreState(result.getState());
-                order.setPayChannel(PaymentChannel.WECHAT_PAYSCORE.getCode());
-                order.setPayType("微信支付分");
                 this.updateById(order);
             } else {
                 log.warn("[支付分集成] 支付分服务订单创建失败 - orderId: {}, errorCode: {}, errorMsg: {}",
@@ -1276,6 +1280,55 @@ public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements
         return result;
     }
 
+    /**
+     * 修复漏掉的支付分扣费:查询有支付分服务订单但未完成扣费的订单,重新发起扣费
+     *
+     * 适用场景:订单创建后因并发回调导致 payChannel 为 null,processPayScorePayment 跳过扣费
+     * 筛选条件:payScoreOrderId 已设置 + payScoreState 为 CREATED/DOING + payStatus 为 UNPAID + totalAmount > 0
+     *
+     * @return { total, success, failed }
+     */
+    @Transactional(rollbackFor = Exception.class)
+    public Map<String, Object> repairPayScorePayments() {
+        List<Order> orders = lambdaQuery()
+                .isNotNull(Order::getPayScoreOrderId)
+                .ne(Order::getPayScoreOrderId, "")
+                .in(Order::getPayScoreState, PayScoreState.CREATED.getCode(), PayScoreState.DOING.getCode())
+                .eq(Order::getPayStatus, OrderConstants.PAY_STATUS_UNPAID)
+                .gt(Order::getTotalAmount, BigDecimal.ZERO)
+                .list();
+
+        int success = 0, failed = 0;
+        for (Order order : orders) {
+            try {
+                BigDecimal amount = order.getPaidAmount() != null
+                        && order.getPaidAmount().compareTo(BigDecimal.ZERO) > 0
+                        ? order.getPaidAmount() : order.getTotalAmount();
+
+                boolean result = completeOrderWithPayScore(order.getId(), amount);
+                if (result) {
+                    success++;
+                    log.info("[支付分修复] 扣费成功 - orderId: {}, orderNo: {}, amount: {}",
+                            order.getId(), order.getOrderNo(), amount);
+                } else {
+                    failed++;
+                    log.warn("[支付分修复] 扣费失败 - orderId: {}, orderNo: {}",
+                            order.getId(), order.getOrderNo());
+                }
+            } catch (Exception e) {
+                failed++;
+                log.error("[支付分修复] 扣费异常 - orderId: {}, error: {}", order.getId(), e.getMessage());
+            }
+        }
+
+        Map<String, Object> result = new HashMap<>();
+        result.put("total", orders.size());
+        result.put("success", success);
+        result.put("failed", failed);
+        log.info("[支付分修复] 完成 - total: {}, success: {}, failed: {}", orders.size(), success, failed);
+        return result;
+    }
+
     /**
      * 标准化图片URL
      */