Parcourir la source

fix: 微信支付回调幂等改造,唯一索引兜底并发重复投递

- PayLog 先落库作为幂等门闩,捕获 DuplicateKeyException 视为已处理返回200
- catch 拆分:验签失败返回401,业务处理失败返回500(微信重试时不会重复入账)
- 依赖 v20 脚本新增的 uk_out_trade_no 唯一索引

Co-Authored-By: Claude <noreply@anthropic.com>
skyline il y a 3 semaines
Parent
commit
936fc87f86

+ 16 - 10
car-wash-service/src/main/java/com/kym/service/wechat/impl/WxPayServiceImpl.java

@@ -41,6 +41,7 @@ import lombok.SneakyThrows;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.core.io.ClassPathResource;
+import org.springframework.dao.DuplicateKeyException;
 import org.springframework.http.HttpStatus;
 import org.springframework.http.ResponseEntity;
 import org.springframework.stereotype.Service;
@@ -388,7 +389,7 @@ public class WxPayServiceImpl implements WxPayService {
                 walletDetail.setTransactionTime(successTime);
                 walletDetailService.updateById(walletDetail);
 
-                // 支付记录
+                // 支付记录(幂等门闩:先落库 payLog,唯一索引 uk_out_trade_no 兜底并发重复回调)
                 var payLog = new PayLog();
                 payLog.setUserId(walletDetail.getUserId());
                 payLog.setOpenid(transaction.getPayer().getOpenid());
@@ -405,7 +406,13 @@ public class WxPayServiceImpl implements WxPayService {
                 payLog.setCurrency(transaction.getAmount().getCurrency());
                 payLog.setPayerTotal(transaction.getAmount().getPayerTotal());
                 payLog.setPayerCurrency(transaction.getAmount().getPayerCurrency());
-                payLogService.save(payLog);
+                try {
+                    payLogService.save(payLog);
+                } catch (DuplicateKeyException e) {
+                    // 并发重复回调:唯一索引拦截,视为已处理
+                    LOGGER.warn("微信支付回调重复投递已被唯一索引拦截,outTradeNo={}", transaction.getOutTradeNo());
+                    return ResponseEntity.status(HttpStatus.OK).build();
+                }
 
                 // V2 结算方案:充值资金记录分账流水,结算日统一处理,不再即时转入站点账户
                 // 无归属站点的用户充值暂不生成分账记录,首次消费时追溯补建
@@ -429,15 +436,14 @@ public class WxPayServiceImpl implements WxPayService {
                 LOGGER.error("微信支付通知处理异常,资金流水为空,回调信息:{}", transaction);
                 return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(Map.of("code", HttpStatus.INTERNAL_SERVER_ERROR, "message", "资金流水为空"));
             }
-        } catch (Exception e) {
-            if (e instanceof ValidationException) {
-                // 签名验证失败,返回 401 UNAUTHORIZED 状态码
-                LOGGER.error("微信支付通知验签失败", e);
-            }
-            if (e instanceof BusinessException) {
-                LOGGER.error("业务异常", e);
-            }
+        } catch (ValidationException e) {
+            // 签名验证失败,返回 401 UNAUTHORIZED 状态码
+            LOGGER.error("微信支付通知验签失败", e);
             return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(Map.of("code", HttpStatus.UNAUTHORIZED, "message", "验签失败"));
+        } catch (Exception e) {
+            // 业务处理失败返回 500,微信会重试投递;重试时由唯一索引/幂等快路径保证不会重复入账
+            LOGGER.error("微信支付通知处理异常", e);
+            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(Map.of("code", HttpStatus.INTERNAL_SERVER_ERROR, "message", "处理失败"));
         }
     }