package com.kym.service.wechat.impl; import cn.dev33.satoken.stp.StpUtil; import cn.hutool.core.date.DateTime; import cn.hutool.core.date.DateUtil; import cn.hutool.core.date.LocalDateTimeUtil; import cn.hutool.core.io.IoUtil; import cn.hutool.core.util.RandomUtil; import com.alibaba.fastjson2.JSONObject; import com.kym.common.config.WxPayConfig; import com.kym.common.constant.ResponseEnum; import com.kym.common.exception.BusinessException; import com.kym.common.utils.CommUtil; import com.kym.common.utils.LambadaTools; import com.kym.common.utils.OrderUtils; import com.kym.entity.Account; import com.kym.entity.*; import com.kym.service.*; import com.kym.service.wechat.WxPayService; import com.wechat.pay.java.core.Config; import com.wechat.pay.java.core.RSAAutoCertificateConfig; import com.wechat.pay.java.core.exception.ValidationException; import com.wechat.pay.java.core.http.DefaultHttpClientBuilder; import com.wechat.pay.java.core.http.okhttp.OkHttpClientAdapter; import com.wechat.pay.java.core.notification.NotificationConfig; import com.wechat.pay.java.core.notification.NotificationParser; import com.wechat.pay.java.core.notification.RequestParam; import com.wechat.pay.java.service.payments.jsapi.JsapiService; import com.wechat.pay.java.service.payments.jsapi.JsapiServiceExtension; import com.wechat.pay.java.service.payments.jsapi.model.Amount; import com.wechat.pay.java.service.payments.jsapi.model.*; import com.wechat.pay.java.service.payments.model.Transaction; import com.wechat.pay.java.service.refund.RefundService; import com.wechat.pay.java.service.refund.model.*; import jakarta.annotation.PostConstruct; import jakarta.servlet.ServletInputStream; import jakarta.servlet.http.HttpServletRequest; import lombok.SneakyThrows; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.core.io.ClassPathResource; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.math.BigDecimal; import java.math.RoundingMode; import java.nio.charset.StandardCharsets; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; /** * @author skyline * @description 微信支付 * @date 2023-08-10 14:03 */ @Service public class WxPayServiceImpl implements WxPayService { private static final Logger LOGGER = LoggerFactory.getLogger(WxPayServiceImpl.class); public static JsapiService jsapiService; public static RefundService refundService; public static Config config; private final WxPayConfig conf; private final WalletDetailService walletDetailService; private final PayLogService payLogService; private final AccountService accountService; private final RefundLogService refundLogService; private final ActivityService activityService; private final UserRechargeRightsService userRechargeRightsService; private final RechargeConfigService rechargeConfigService; private final StationAccountService stationAccountService; private final SplitRecordService splitRecordService; /** * 微信支付专用,支持自动签名验签解密等 */ private OkHttpClientAdapter wxHttpClient; public WxPayServiceImpl(WxPayConfig conf, WalletDetailService walletDetailService, PayLogService payLogService, AccountService accountService, RefundLogService refundLogService, ActivityService activityService, UserRechargeRightsService userRechargeRightsService, RechargeConfigService rechargeConfigService, StationAccountService stationAccountService, SplitRecordService splitRecordService) { this.conf = conf; this.walletDetailService = walletDetailService; this.payLogService = payLogService; this.accountService = accountService; this.refundLogService = refundLogService; this.activityService = activityService; this.userRechargeRightsService = userRechargeRightsService; this.rechargeConfigService = rechargeConfigService; this.stationAccountService = stationAccountService; this.splitRecordService = splitRecordService; } /** * 商户订单号查询订单 */ public static Transaction queryOrderByOutTradeNo() { QueryOrderByOutTradeNoRequest request = new QueryOrderByOutTradeNoRequest(); // 调用request.setXxx(val)设置所需参数,具体参数可见Request定义 // 调用接口 return jsapiService.queryOrderByOutTradeNo(request); } /** * 初始化 * * @throws IOException */ @PostConstruct void init() throws IOException { String privateKey; File file = new File(conf.getKeyPath()); if (file.exists()) { privateKey = IoUtil.read(new FileInputStream(file), StandardCharsets.UTF_8); } else { ClassPathResource resource = new ClassPathResource(conf.getKeyPath()); privateKey = IoUtil.read(resource.getInputStream(), StandardCharsets.UTF_8); } config = new RSAAutoCertificateConfig.Builder() .merchantId(conf.getMchid()) // 商户号 .privateKey(privateKey)//商户API私钥路径 .merchantSerialNumber(conf.getMchsn()) // 商户证书序列号 .apiV3Key(conf.getV3key()) // 商户APIV3密钥 .build(); jsapiService = new JsapiService.Builder().config(config).build(); refundService = new RefundService.Builder().config(config).build(); wxHttpClient = (OkHttpClientAdapter) new DefaultHttpClientBuilder().newInstance().config(config).build(); // // 初始化微信电子发票开发配置,主要是回调地址 todo 区块链电子发票开通后放开 // devConfig(); } /** * 回调签名验证失败排查指引 (body中字段顺序变化造成验签失败是个大坑) * https://developers.weixin.qq.com/community/pay/article/doc/0004a879f60928d89340b8b9f64c13 * https://github.com/wechatpay-apiv3/wechatpay-apache-httpclient/issues/152 * * @param request * @return */ @SneakyThrows Object[] handleWxNotify(HttpServletRequest request) { var no = RandomUtil.randomInt(1000, 9999); var signature = request.getHeader("Wechatpay-Signature"); var serial = request.getHeader("Wechatpay-Serial"); var nonce = request.getHeader("Wechatpay-Nonce"); var timestamp = request.getHeader("Wechatpay-Timestamp"); var signatureType = request.getHeader("Wechatpay-Signature-Type"); // 应对探测流量 if (signature.contains("SIGNTEST")) { throw new BusinessException("接收到签名探测流量"); } LOGGER.info("微信支付回调{}: Request参数: signature:{},serial:{},nonce:{},timestamp:{},signatureType:{}", no, signature, serial, nonce, timestamp, signatureType); ServletInputStream inputStream = request.getInputStream(); ByteArrayOutputStream result = new ByteArrayOutputStream(); byte[] buffer = new byte[1024]; for (int lenght; (lenght = inputStream.read(buffer)) != -1; ) { result.write(buffer, 0, lenght); } var body = result.toString(); LOGGER.info("微信支付回调{}:\nBody数据:\n{}", no, result); // 构造 RequestParam RequestParam requestParam = new RequestParam.Builder() .serialNumber(serial) .nonce(nonce) // 随机数 .signature(signature) .timestamp(timestamp) .body(body) .build(); LOGGER.info("微信支付回调{}:构造 RequestParam完毕", no); // 如果已经初始化了 RSAAutoCertificateConfig,可直接使用 // 初始化 NotificationParser NotificationParser parser = new NotificationParser((NotificationConfig) config); return new Object[]{requestParam, parser, no}; } /** * JSAPI支付下单 */ @Override @Transactional public PrepayWithRequestPaymentResponse wxPay(Long rechargeConfigId, String stationId) { // 充值配置 var rechargeConfig = rechargeConfigService.getById(rechargeConfigId); if ((rechargeConfig == null) || rechargeConfig.getRechargeAmount() <= 0) { throw new BusinessException(ResponseEnum.WX_PAY_AMOUNT_ERROR); } var rechargeAmount = rechargeConfig.getRechargeAmount(); var openid = StpUtil.getSession().getString("openid"); var userId = StpUtil.getLoginIdAsLong(); // 生成订单号 String outTradeNo = OrderUtils.getOrderNo(); // 创建钱包流水 var walletDetail = new WalletDetail() .setType(WalletDetail.TYPE_充值) .setStatus(WalletDetail.STATUS_待确认) .setUserId(userId) .setAmount(rechargeAmount) .setOrderNo(outTradeNo); walletDetailService.save(walletDetail); // request.setXxx(val)设置所需参数,具体参数可见Request定义 PrepayRequest request = new PrepayRequest(); Amount amount = new Amount(); // 传入金额单位为分 amount.setTotal(rechargeAmount); request.setAmount(amount); request.setAppid(conf.getAppid()); request.setMchid(conf.getMchid()); request.setDescription("超级进化车生活充值"); request.setNotifyUrl(conf.getNotifyUrl()); request.setOutTradeNo(outTradeNo); // 把stationId传给微信,微信回调时携带再取出,记录是充值到哪个站点 request.setAttach(stationId); Payer payer = new Payer(); payer.setOpenid(openid); request.setPayer(payer); JsapiServiceExtension service = new JsapiServiceExtension.Builder().config(config).build(); // response包含了调起支付所需的所有参数,可直接用于前端调起支付 return service.prepayWithRequestPayment(request); } /** * 微信支付订单号查询订单 */ public Transaction queryOrderById(String transactionId) { QueryOrderByIdRequest request = new QueryOrderByIdRequest(); // 调用request.setXxx(val)设置所需参数,具体参数可见Request定义 request.setMchid(conf.getMchid()); request.setTransactionId(transactionId); // 调用接口 return jsapiService.queryOrderById(request); } /** * 关闭订单 * * @param outTradeNo */ public void closeOrder(String outTradeNo) { CloseOrderRequest request = new CloseOrderRequest(); // 调用request.setXxx(val)设置所需参数,具体参数可见Request定义 request.setMchid(conf.getMchid()); request.setOutTradeNo(outTradeNo); // 调用接口 jsapiService.closeOrder(request); } /** * 微信支付结果通知(充值完成) * * @param request * @return */ @SneakyThrows @Override @Transactional(rollbackFor = Exception.class) public ResponseEntity wxNotify(HttpServletRequest request) { try { var notifyRes = handleWxNotify(request); // 以支付通知回调为例,验签、解密并转换成 Transaction Transaction transaction = ((NotificationParser) notifyRes[1]).parse((RequestParam) notifyRes[0], Transaction.class); LOGGER.info("微信支付回调{}:验签解密完毕,数据:{}", notifyRes[2], transaction); // 判断是否已经接收处理过通知 if (payLogService.lambdaQuery().eq(PayLog::getOutTradeNo, transaction.getOutTradeNo()).one() != null) { return ResponseEntity.status(HttpStatus.OK).build(); } DateTime dt = DateUtil.parse(transaction.getSuccessTime()); LocalDateTime successTime = LocalDateTimeUtil.of(dt); // 钱包流水 var walletDetail = walletDetailService.getWalletDetailByOrderNo(transaction.getOutTradeNo(), WalletDetail.TYPE_充值); if (walletDetail != null) { // 更新余额 var account = accountService.getAccountByUserId(walletDetail.getUserId()); accountService.lambdaUpdate().setSql("balance = (balance + %d)".formatted(transaction.getAmount().getTotal())) .eq(Account::getUserId, walletDetail.getUserId()).update(); walletDetail.setStatus(WalletDetail.STATUS_已确认); //已确认 walletDetail.setCurrency(transaction.getAmount().getCurrency()); walletDetail.setAmount(transaction.getAmount().getTotal()); walletDetail.setBeforeBalance(account.getBalance()); walletDetail.setAfterBalance(account.getBalance() + walletDetail.getAmount()); walletDetail.setTransactionTime(successTime); walletDetailService.updateById(walletDetail); // 异步处理充值服务费打折权益活动相关逻辑 activityService.handleRechargeActivity(walletDetail.getUserId(), transaction.getAmount().getTotal()); // 支付记录 var payLog = new PayLog(); payLog.setUserId(walletDetail.getUserId()); payLog.setOpenid(transaction.getPayer().getOpenid()); payLog.setBankType(transaction.getBankType()); payLog.setMchId(transaction.getMchid()); payLog.setOutTradeNo(transaction.getOutTradeNo()); payLog.setTransactionId(transaction.getTransactionId()); payLog.setSuccessTime(successTime); payLog.setTradeType(transaction.getTradeType().name()); payLog.setTradeState(transaction.getTradeState().name()); payLog.setAttach(transaction.getAttach()); var totalAmount = transaction.getAmount().getTotal(); payLog.setTotal(totalAmount); payLog.setCurrency(transaction.getAmount().getCurrency()); payLog.setPayerTotal(transaction.getAmount().getPayerTotal()); payLog.setPayerCurrency(transaction.getAmount().getPayerCurrency()); payLogService.save(payLog); // 用户(此时要知道用户归属的站点)充值的资金先进到洗车站商户账户的基本户和冻结户,然后在消费时再将冻结户金额进行分润 var stationId = transaction.getAttach(); // 70%进入站点商户基本户,30%进入站点商户冻结户 todo 后面将比例进行配置化(需要考虑历史数据处理) var stationBasicAmount = (int) (totalAmount * 0.7); var stationFreezeAmount = totalAmount - stationBasicAmount; stationAccountService.lambdaUpdate() .setSql("balance = (balance + %d), frozen_amount = (frozen_amount + %d)".formatted(stationBasicAmount, stationFreezeAmount)) .eq(StationAccount::getStationId, stationId) .update(); // 分账记录 var splitRecord = new SplitRecord() .setFromStationId(stationId) .setToStationId(stationId) .setTradeNo(transaction.getTransactionId()) .setAmount(totalAmount) .setType(SplitRecord.TYPE_RECHARGE); splitRecordService.save(splitRecord); LOGGER.info("微信支付回调{}:业务处理结束", notifyRes[2]); return ResponseEntity.status(HttpStatus.OK).build(); } else { 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); } return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(Map.of("code", HttpStatus.UNAUTHORIZED, "message", "验签失败")); } } /** * 申请退款 */ @Override @Transactional(rollbackFor = Exception.class) public void applyWxRefund(String reason) { var userId = StpUtil.getLoginIdAsLong(); // var chargeOrder = chargeOrderService.getChargingOrderByUserId(userId); // if (chargeOrder != null) { // throw new BusinessException("存在未完结的订单,请等待所有订单完结之后重试"); // } // todo 洗车未完结订单校验 var account = accountService.getAccountByUserId(userId); if (account.getBalance() <= 0) { throw new BusinessException("账户余额不足,无需退款"); } // 校验余额大于优惠金额 if (account.getBalance() <= account.getDiscountAmount()) { throw new BusinessException("不可退金额高于钱包余额,不支持退款"); } // 将余额转移至冻结余额 accountService.lambdaUpdate().setSql(" frozen_amount = (frozen_amount + balance) ,balance = 0").eq(Account::getUserId, userId).update(); // 退款时,充值权益失效(权益余额转入冻结余额,权益状态设置为失效) userRechargeRightsService.lambdaUpdate() .setSql("frozen_balance = (rights_balance + frozen_balance) , rights_balance = 0") .set(UserRechargeRights::getStatus, UserRechargeRights.STATUS_无效) .eq(UserRechargeRights::getUserId, userId) .eq(UserRechargeRights::getStatus, UserRechargeRights.STATUS_有效) .update(); // 余减去优惠金额作为退款金额 AtomicInteger refundAmount = new AtomicInteger(account.getBalance() - account.getDiscountAmount()); var originalRefundAmount = BigDecimal.valueOf(refundAmount.get()); // 充值记录 var payLogs = payLogService.lambdaQuery().eq(PayLog::getUserId, userId).eq(PayLog::getTradeState, PayLog.STATUS_充值成功).orderByDesc(PayLog::getSuccessTime).list(); // 历史退款金额 var refundsAmount = refundLogService.lambdaQuery().eq(RefundLog::getUserId, userId).list().stream().mapToInt(RefundLog::getRefund).sum(); // 充值金额-历史退款金额不足以支付余额退款 if (!CommUtil.isEmptyOrNull(payLogs)) { var total = payLogs.stream().mapToInt(PayLog::getTotal).sum(); if ((total - refundsAmount) < account.getBalance()) { LOGGER.error("用户:{},历史充值金额:{},不足以支付余额退款金额:{}", userId, total - refundsAmount, account.getBalance()); throw new BusinessException("充值金额不足以支付余额退款"); } } // 最后一次的充值金额可以覆盖退款金额 if (!CommUtil.isEmptyOrNull(payLogs) && payLogs.get(0).getTotal() >= refundAmount.get()) { // 退款日志 var refundLog = new RefundLog().setUserId(payLogs.get(0).getUserId()).setOutTradeNo(payLogs.get(0).getOutTradeNo()) .setTotal(payLogs.get(0).getTotal()) .setRefund(refundAmount.get()) .setDiscountAmount(account.getDiscountAmount()) .setOutRefundNo(OrderUtils.getOrderNo()); refundLogService.save(refundLog); } else if (!CommUtil.isEmptyOrNull(payLogs) && payLogs.get(0).getTotal() < refundAmount.get()) { // 最后一次的充值金额不能覆盖退款金额,拆分成多笔退款 int amount = 0; // 用来退款的充值记录 var refundPayLogs = new ArrayList(); for (PayLog payLog : payLogs) { amount += payLog.getTotal(); refundPayLogs.add(payLog); if (amount >= refundAmount.get()) { break; } } // 需要退款的记录数量 var size = refundPayLogs.size(); var refundLogList = new ArrayList(size); var newRefundLogList = new ArrayList(size); refundPayLogs.forEach(LambadaTools.forEachWithIndex((item, index) -> { // 如果不是最后一笔,金额全退,最后一笔退剩余的金额 if (index < size - 1) { refundLogList.add(new RefundLog().setUserId(item.getUserId()).setOutTradeNo(item.getOutTradeNo()) .setTotal(item.getTotal()).setRefund(item.getTotal()).setOutRefundNo(OrderUtils.getOrderNo())); refundAmount.addAndGet(-item.getTotal()); } else { refundLogList.add(new RefundLog().setUserId(item.getUserId()).setOutTradeNo(item.getOutTradeNo()) .setTotal(item.getTotal()).setRefund(refundAmount.get()).setOutRefundNo(OrderUtils.getOrderNo())); } })); // 不可退金额按退款金额比例放入每笔退款中 refundLogList.forEach(LambadaTools.forEachWithIndex((refundLog, index) -> { int discountAmount; if (index < size - 1) { discountAmount = BigDecimal.valueOf(account.getDiscountAmount()).multiply((BigDecimal.valueOf(refundLog.getRefund()).divide(originalRefundAmount, 2, RoundingMode.HALF_UP))).intValue(); } else { // 前面存在精度误差,最后一个元素用总数相减最精确 discountAmount = account.getDiscountAmount() - newRefundLogList.stream().mapToInt(RefundLog::getDiscountAmount).sum(); } refundLog.setDiscountAmount(discountAmount); refundLog.setReason(CommUtil.isEmptyOrNull(reason) ? reason : JSONObject.parseObject(reason).getString("reason")); newRefundLogList.add(index, refundLog); })); refundLogService.saveBatch(newRefundLogList); } else { // 退款异常 LOGGER.error("退款异常:userId:{},退款金额:{},payLogs:{}", userId, originalRefundAmount, payLogs); throw new BusinessException("退款异常"); } } /** * 处理退款 * * @return */ @Override public void wxRefund(long refundLogId) { // 通过退款申请id获取退款申请记录 var refundLog = refundLogService.getById(refundLogId); // 钱包流水 var walletDetail = new WalletDetail() .setType(WalletDetail.TYPE_提现) .setStatus(WalletDetail.STATUS_待确认) .setUserId(refundLog.getUserId()) .setAmount(refundLog.getRefund()) .setOrderNo(refundLog.getOutRefundNo()); walletDetailService.save(walletDetail); CreateRequest request = new CreateRequest(); // 调用request.setXxx(val)设置所需参数,具体参数可见Request定义 // 原始第三方订单号 request.setOutTradeNo(refundLog.getOutTradeNo()); // 退款订单号 request.setOutRefundNo(refundLog.getOutRefundNo()); // 退款原因 request.setReason("用户申请退款"); // 回调通知地址 request.setNotifyUrl(conf.getRefundNotifyUrl()); var amount = new AmountReq(); // 退款金额 amount.setRefund((long) refundLog.getRefund()); amount.setTotal((long) refundLog.getTotal()); amount.setCurrency(refundLog.getCurrency()); request.setAmount(amount); var refund = refundService.create(request); refundLog.setChannel(refund.getChannel().name()); refundLog.setFundsAccount(refund.getFundsAccount().name()); refundLog.setCurrency(refund.getAmount().getCurrency()); refundLog.setAdminUserId(StpUtil.getLoginIdAsLong()); refundLog.setAdminUsername(StpUtil.getSession().getString("username")); refundLog.setStatus(RefundLog.STATUS_退款处理中); refundLogService.updateById(refundLog); } /** * 查询单笔退款(通过商户退款单号) * * @return */ @Override public Refund queryByOutRefundNo(String outRefundNo) { QueryByOutRefundNoRequest request = new QueryByOutRefundNoRequest(); // 调用request.setXxx(val)设置所需参数,具体参数可见Request定义 request.setOutRefundNo(outRefundNo); // 调用接口 return refundService.queryByOutRefundNo(request); } /** * 微信退款结果通知 * * @param request * @return */ @Override @Transactional(rollbackFor = Exception.class) public ResponseEntity wxRefundNotify(HttpServletRequest request) { var notifyRes = handleWxNotify(request); try { // 以支付通知回调为例,验签、解密并转换成 RefundNotification 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.STATUS_退款成功.equals(refundLog.getStatus())) { return ResponseEntity.status(HttpStatus.OK).build(); } 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 account = accountService.getAccountByUserId(refundLog.getUserId()); accountService.lambdaUpdate().setSql("frozen_amount = (frozen_amount - (%d + %d)) , discount_amount = (discount_amount - %d)" .formatted(refundNotification.getAmount().getRefund().intValue(), refundLog.getDiscountAmount(), refundLog.getDiscountAmount())) .eq(Account::getUserId, refundLog.getUserId()).update(); // 更新资金流水 var walletDetail = walletDetailService.getWalletDetailByOrderNo(refundNotification.getOutRefundNo(), WalletDetail.TYPE_提现); walletDetailService.lambdaUpdate() .set(WalletDetail::getStatus, WalletDetail.STATUS_已确认) .set(WalletDetail::getTransactionId, refundNotification.getTransactionId()) .set(WalletDetail::getTransactionTime, successTime) .set(WalletDetail::getAmount, refundNotification.getAmount().getRefund().intValue()) .set(WalletDetail::getBeforeBalance, account.getBalance()) .set(WalletDetail::getAfterBalance, account.getBalance() - refundNotification.getAmount().getRefund().intValue()) .set(WalletDetail::getTransactionTime, successTime) .eq(WalletDetail::getId, walletDetail.getId()).update(); LOGGER.info("微信退款回调{}:业务处理结束", notifyRes[2]); return ResponseEntity.status(HttpStatus.OK).build(); } else { // 退款失败 LOGGER.error("微信退款失败,用户id:{},退款状态:{} \n 退款结果通知详情:{}", refundLog.getUserId(), refundNotification.getRefundStatus().name(), refundNotification); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(Map.of("code", HttpStatus.INTERNAL_SERVER_ERROR, "message", "退款处理异常")); } } catch (ValidationException e) { // 签名验证失败,返回 401 UNAUTHORIZED 状态码 LOGGER.error("微信退款通知验签失败", e); return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(Map.of("code", HttpStatus.UNAUTHORIZED, "message", "验签失败")); } } //================================================================发票===================================================================== }