|
@@ -41,6 +41,7 @@ import lombok.SneakyThrows;
|
|
|
import org.slf4j.Logger;
|
|
import org.slf4j.Logger;
|
|
|
import org.slf4j.LoggerFactory;
|
|
import org.slf4j.LoggerFactory;
|
|
|
import org.springframework.core.io.ClassPathResource;
|
|
import org.springframework.core.io.ClassPathResource;
|
|
|
|
|
+import org.springframework.dao.DuplicateKeyException;
|
|
|
import org.springframework.http.HttpStatus;
|
|
import org.springframework.http.HttpStatus;
|
|
|
import org.springframework.http.ResponseEntity;
|
|
import org.springframework.http.ResponseEntity;
|
|
|
import org.springframework.stereotype.Service;
|
|
import org.springframework.stereotype.Service;
|
|
@@ -388,7 +389,7 @@ public class WxPayServiceImpl implements WxPayService {
|
|
|
walletDetail.setTransactionTime(successTime);
|
|
walletDetail.setTransactionTime(successTime);
|
|
|
walletDetailService.updateById(walletDetail);
|
|
walletDetailService.updateById(walletDetail);
|
|
|
|
|
|
|
|
- // 支付记录
|
|
|
|
|
|
|
+ // 支付记录(幂等门闩:先落库 payLog,唯一索引 uk_out_trade_no 兜底并发重复回调)
|
|
|
var payLog = new PayLog();
|
|
var payLog = new PayLog();
|
|
|
payLog.setUserId(walletDetail.getUserId());
|
|
payLog.setUserId(walletDetail.getUserId());
|
|
|
payLog.setOpenid(transaction.getPayer().getOpenid());
|
|
payLog.setOpenid(transaction.getPayer().getOpenid());
|
|
@@ -405,7 +406,13 @@ public class WxPayServiceImpl implements WxPayService {
|
|
|
payLog.setCurrency(transaction.getAmount().getCurrency());
|
|
payLog.setCurrency(transaction.getAmount().getCurrency());
|
|
|
payLog.setPayerTotal(transaction.getAmount().getPayerTotal());
|
|
payLog.setPayerTotal(transaction.getAmount().getPayerTotal());
|
|
|
payLog.setPayerCurrency(transaction.getAmount().getPayerCurrency());
|
|
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 结算方案:充值资金记录分账流水,结算日统一处理,不再即时转入站点账户
|
|
// V2 结算方案:充值资金记录分账流水,结算日统一处理,不再即时转入站点账户
|
|
|
// 无归属站点的用户充值暂不生成分账记录,首次消费时追溯补建
|
|
// 无归属站点的用户充值暂不生成分账记录,首次消费时追溯补建
|
|
@@ -429,15 +436,14 @@ public class WxPayServiceImpl implements WxPayService {
|
|
|
LOGGER.error("微信支付通知处理异常,资金流水为空,回调信息:{}", transaction);
|
|
LOGGER.error("微信支付通知处理异常,资金流水为空,回调信息:{}", transaction);
|
|
|
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(Map.of("code", HttpStatus.INTERNAL_SERVER_ERROR, "message", "资金流水为空"));
|
|
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", "验签失败"));
|
|
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", "处理失败"));
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|