Kaynağa Gözat

fix: 微信退款回调幂等与兜底

- refundLog 为空时不再 NPE,返回500让微信重试
- 成功处理路径增加条件更新门闩(status<>退款成功),并发重复回调不再重复扣减冻结金额
- walletDetail 检查移到门闩之前,避免状态已置成功但流水未落导致不一致
- frozen_amount 扣减改为参数化 setSql,去掉字符串拼接

Co-Authored-By: Claude <noreply@anthropic.com>
skyline 3 hafta önce
ebeveyn
işleme
dac5c3ef92

+ 34 - 18
car-wash-service/src/main/java/com/kym/service/wechat/impl/WxPayServiceImpl.java

@@ -705,9 +705,13 @@ public class WxPayServiceImpl implements WxPayService {
             RefundNotification refundNotification = ((NotificationParser) notifyRes[1]).parse((RequestParam) notifyRes[0], RefundNotification.class);
             LOGGER.info("微信退款回调{}:验签解密完毕,数据:\n{}", notifyRes[2], refundNotification);
 
-            //退款日志在申请时插入,接收通知时更新
+            // 退款日志在申请时插入,接收通知时更新
             var refundLog = refundLogService.lambdaQuery().eq(RefundLog::getOutRefundNo, refundNotification.getOutRefundNo()).one();
-            // 防止重复处理消息
+            if (refundLog == null) {
+                LOGGER.error("微信退款回调:未找到退款记录,outRefundNo={}", refundNotification.getOutRefundNo());
+                return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(Map.of("code", HttpStatus.INTERNAL_SERVER_ERROR, "message", "退款记录不存在"));
+            }
+            // 幂等快路径:已处理成功直接返回
             if (RefundLog.STATUS_退款成功.equals(refundLog.getStatus())) {
                 return ResponseEntity.status(HttpStatus.OK).build();
             }
@@ -715,28 +719,41 @@ public class WxPayServiceImpl implements WxPayService {
             DateTime dt = DateUtil.parse(refundNotification.getSuccessTime());
             LocalDateTime successTime = LocalDateTimeUtil.of(dt);
 
-            refundLogService.lambdaUpdate()
-                    .set(RefundLog::getRefundId, refundNotification.getRefundId())
-                    .set(RefundLog::getTransactionId, refundNotification.getTransactionId())
-                    .set(RefundLog::getUserReceivedAccount, refundNotification.getUserReceivedAccount())
-                    .set(RefundLog::getSuccessTime, successTime)
-                    .set(RefundLog::getStatus, refundNotification.getRefundStatus().name())
-                    .set(RefundLog::getTotal, refundNotification.getAmount().getTotal().intValue())
-                    .set(RefundLog::getRefund, refundNotification.getAmount().getRefund().intValue())
-                    .eq(RefundLog::getId, refundLog.getId()).update();
-
             if (RefundLog.STATUS_退款成功.equals(refundNotification.getRefundStatus().name())) {
+                // 资金流水必须在申请退款时已存在;检查放在门闩之前,避免门闩已置成功但后续失败造成不一致
+                var walletDetail = walletDetailService.getWalletDetailByOrderNo(refundNotification.getOutRefundNo(), WalletDetail.TYPE_退款);
+                if (walletDetail == null) {
+                    LOGGER.error("微信退款回调:未找到退款资金流水,outRefundNo={}", refundNotification.getOutRefundNo());
+                    return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(Map.of("code", HttpStatus.INTERNAL_SERVER_ERROR, "message", "退款流水不存在"));
+                }
+
+                // 幂等门闩:条件更新(status <> 已成功),并发重复回调第二个事务更新 0 行直接跳过
+                boolean claimed = refundLogService.lambdaUpdate()
+                        .set(RefundLog::getRefundId, refundNotification.getRefundId())
+                        .set(RefundLog::getTransactionId, refundNotification.getTransactionId())
+                        .set(RefundLog::getUserReceivedAccount, refundNotification.getUserReceivedAccount())
+                        .set(RefundLog::getSuccessTime, successTime)
+                        .set(RefundLog::getStatus, refundNotification.getRefundStatus().name())
+                        .set(RefundLog::getTotal, refundNotification.getAmount().getTotal().intValue())
+                        .set(RefundLog::getRefund, refundNotification.getAmount().getRefund().intValue())
+                        .eq(RefundLog::getId, refundLog.getId())
+                        .ne(RefundLog::getStatus, RefundLog.STATUS_退款成功)
+                        .update();
+                if (!claimed) {
+                    return ResponseEntity.status(HttpStatus.OK).build();
+                }
+
                 // 冻结金额扣减此次(退款金额+优惠金额),优惠金额字段减去申请退款时的优惠金额
                 var account = accountService.getAccountByUserId(refundLog.getUserId());
-                accountService.lambdaUpdate().setSql("frozen_amount = (frozen_amount - %d) , discount_amount = (discount_amount - %d)"
-                                .formatted(refundNotification.getAmount().getRefund().intValue(), refundLog.getDiscountAmount()))
+                var refundAmount = refundNotification.getAmount().getRefund().intValue();
+                accountService.lambdaUpdate()
+                        .setSql("frozen_amount = (frozen_amount - {0}), discount_amount = (discount_amount - {1})",
+                                refundAmount, refundLog.getDiscountAmount())
                         .eq(Account::getUserId, refundLog.getUserId()).update();
 
                 // 更新资金流水
                 // 注意:此时 balance 已在 applyWxRefund 阶段清零,
                 // beforeBalance 应反映退款前余额(即退款金额),afterBalance 为 0
-                var walletDetail = walletDetailService.getWalletDetailByOrderNo(refundNotification.getOutRefundNo(), WalletDetail.TYPE_退款);
-                var refundAmount = refundNotification.getAmount().getRefund().intValue();
                 walletDetailService.lambdaUpdate()
                         .set(WalletDetail::getStatus, WalletDetail.STATUS_已确认)
                         .set(WalletDetail::getTransactionId, refundNotification.getTransactionId())
@@ -744,7 +761,6 @@ public class WxPayServiceImpl implements WxPayService {
                         .set(WalletDetail::getAmount, refundAmount)
                         .set(WalletDetail::getBeforeBalance, account.getBalance() + refundAmount)
                         .set(WalletDetail::getAfterBalance, account.getBalance())
-                        .set(WalletDetail::getTransactionTime, successTime)
                         .eq(WalletDetail::getId, walletDetail.getId()).update();
                 LOGGER.info("微信退款回调{}:业务处理结束", notifyRes[2]);
 
@@ -754,7 +770,7 @@ public class WxPayServiceImpl implements WxPayService {
                         .setFromStationId(stationId)
                         .setToStationId(stationId)
                         .setTradeNo(refundNotification.getTransactionId())
-                        .setAmount(refundNotification.getAmount().getRefund().intValue())
+                        .setAmount(refundAmount)
                         .setType(SplitRecord.TYPE_REFUND);
                 splitRecordService.save(refundSplit);