Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

微信支付成功后,异步通知 NotificationParser.parse 解密数据异常 #112

Closed
pyrange opened this issue Jan 5, 2023 · 9 comments · Fixed by #113
Closed

微信支付成功后,异步通知 NotificationParser.parse 解密数据异常 #112

pyrange opened this issue Jan 5, 2023 · 9 comments · Fixed by #113

Comments

@pyrange
Copy link

pyrange commented Jan 5, 2023

错误描述

微信支付成功,异步通知解密数据异常

运行环境

java:JDK17
依赖微信sdk:wechatpay-java;0.2.4版本

示例代码

image


    // 使用自动更新平台证书的RSA配置
    // 一个商户号只能初始化一个配置,否则会因为重复的下载任务报错
    RSAAutoCertificateConfig config = new RSAAutoCertificateConfig.Builder()
            .merchantId(WeiXinPayConstant.MCH_ID)
            .privateKey(WeiXinPayConstant.MCH_PRIVATE_KEY)
            .merchantSerialNumber(WeiXinPayConstant.MCH_SERIAL_NO)
            .apiV3Key(WeiXinPayConstant.API_V3_KEY)
            .build();

异常信息:

com.wechat.pay.java.core.exception.ValidationException: Processing WechatPay notification,signature verification failed,signType[WECHATPAY2-SHA256-RSA2048]	serial[72A58B94F5350B13A124585D15BF88C1E55AB350]	message[1672906824
WYDi5b6dyyi36Cry6Vd95cYW5EOrwwNo
{"summary":"支付成功","event_type":"TRANSACTION.SUCCESS","create_time":"2023-01-05T16:16:20+08:00","resource":{"original_type":"transaction","algorithm":"AEAD_AES_256_GCM","ciphertext":"dFZoL6Qa18H3/R0fpBvviskQ1xVaXQDkRSJETChAtBcYfmNvBOqmipI8u1yTTl5ddgjarOdAuK5npSjMjfc1/OEPaNx91R2kG82e3NFaXP3csvtSLCNRN6h1MFBlMPqHVGk8SoOKqgGQAxAmzeLUzUlRI9gQm8GDRvJYzsIkTdWs//2yZZAdEOLXe29Uu+Zj41Te+/iHR0JkmHup9XXMSOZ+cjwmoTlycYd9b0u7j1O4f54fG78jsP76jt6YhniJPhi1nN9Mb2v4wYzH6LHA3wWN4oPLQmXMORL8F+gqm88yyrf79XlcZgovTXM1cUIklDb0pGyennQXSfrzz7Z8dxPmmiLo8zp8XHMUtsb81W8lV6oC4c/nykd7j6NPoI8F6VUYDptCH6aZu/nvsMyCRRvalSFHvLYwIIRtNc1/VB49KKF1H4EncY/lx3gOdI4FSrt8Q/wYt+QPqZ/1QEs0QlFYW98TlXPtCC9pgveR4gZRUHdrvYwxwJT2Gn5rQn18vhZDfxrFaBZLxLYruNyyGRXkBMPqsa9i0Exu6e/gcEcvylo9yQUoJ1GDliOIBJ8eF9rELEos","associated_data":"transaction","nonce":"Zs7DNh4Njdvf"},"resource_type":"encrypt-resource","id":"a179bbb2-257f-5c07-826a-287d39b995f6"}
]	sign[hpq6R1V4LO25TYN97VnhkPrRheXyn7WrtmYDLkeI4eF7O248YXYJC6TLCp/T57lNZBAyPtHz+4K0Rm7SqXAQzmGh/OvlAi3FTvXk/Wm8FOp59VqOerxw7HYD3UFItAS0rpozoIC5ha5b2FHqmGCp5l7OGIXLf9PnGW2s551im2BkBLb+1WW2f6udJywoGL/YGVZ4Jl7bwMFYf64GZAc0AIGQqRWBrL7zIiL8Nw4ajBZTiwDOlZiSJg9vzVaHJyj4nSKe8wPy5ZcPq2Hd4JZ0VUvakhiqyxjsz4KJE8nB8HQicMgBSZpuJPRPzxaWd/DB9x73XmCY2A26DRFfs0mZNA==]
	at com.wechat.pay.java.core.notification.NotificationParser.validateRequest(NotificationParser.java:93)
	at com.wechat.pay.java.core.notification.NotificationParser.parse(NotificationParser.java:49)
	at com.pyrange.app.main.web.service.payment.weixin.WeixinPaymentServiceImpl.paymentSuccessNotify(WeixinPaymentServiceImpl.java:367)
	at com.pyrange.app.main.web.controller.WeixinPaymentController.paymentSuccessNotify(WeixinPaymentController.java:37)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.base/java.lang.reflect.Method.invoke(Method.java:568)
	at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:343)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:196)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163)
	at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:752)
	at org.springframework.aop.framework.adapter.AfterReturningAdviceInterceptor.invoke(AfterReturningAdviceInterceptor.java:57)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:173)
	at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:752)
	at org.springframework.aop.framework.adapter.MethodBeforeAdviceInterceptor.invoke(MethodBeforeAdviceInterceptor.java:58)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:173)
	at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:752)
	at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:97)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)
	at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:752)
	at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:703)
	at com.pyrange.app.main.web.controller.WeixinPaymentController$$SpringCGLIB$$0.paymentSuccessNotify(<generated>)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.base/java.lang.reflect.Method.invoke(Method.java:568)
	at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:207)
	at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:152)
	at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:117)
	at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:884)
	at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:797)
	at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)
	at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1080)
	at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:973)
	at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1003)
	at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:906)
	at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:731)
	at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:880)
	at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:814)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:223)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:158)
	at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:185)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:158)
	at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:185)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:158)
	at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:185)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:158)
	at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:185)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:158)
	at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:197)
	at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:97)
	at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:542)
	at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:119)
	at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92)
	at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:78)
	at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:357)
	at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:400)
	at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65)
	at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:861)
	at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1739)
	at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52)
	at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1191)
	at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659)
	at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
	at java.base/java.lang.Thread.run(Thread.java:833)

重现bug的步骤

  1. 微信小程序支付
  2. 收到微信通知

预期行为

正常解析密文

导致错误的代码片段

Transaction parse = parser.parse(requestParam, Transaction.class);

操作系统

win10

Java 版本

java 17

wechatpay-java 版本

0.2.4

其他信息

@pyrange pyrange added the bug Something isn't working label Jan 5, 2023
@xy-peng
Copy link
Contributor

xy-peng commented Jan 5, 2023

body要使用原始的报文,而不是 toJSONString() 的字符串。经过序列化和反序列化之后,跟原文不一致。

@xy-peng xy-peng removed the bug Something isn't working label Jan 5, 2023
@xy-peng xy-peng mentioned this issue Jan 5, 2023
@402626224
Copy link

public ResponseEntity<WxResultInfoVo> wxNotify(
        @RequestHeader("wechatpay-serial") String serialNumber,
        @RequestHeader("wechatpay-nonce") String nonce,
        @RequestHeader("wechatpay-signature") String signature,
        @RequestHeader("wechatpay-timestamp") String timestamp,
        @RequestHeader("wechatpay-signature-type") String signType,
        HttpServletRequest request
) {
        String body = IOUtils.toString(request.getInputStream());
        RequestParam requestParam = new RequestParam.Builder()
                .serialNumber(serialNumber.trim())
                .nonce(nonce.trim())
                .signature(signature.trim())
                .timestamp(timestamp.trim())
                // 若未设置signType,默认值为 WECHATPAY2-SHA256-RSA2048
                .signType(signType.trim())
                .body(body)
                .build();
            NotificationParser parser = new NotificationParser(config);
            Map map = parser.parse(requestParam, Map.class);

这样子也是验签不通过

@Roc1991x
Copy link

Roc1991x commented Nov 5, 2023

能给一个获取原始body的例子么?

@liguangling-lgl
Copy link

private String getRequestBody(HttpServletRequest request) {
    ByteArrayOutputStream body = new ByteArrayOutputStream();
    try {
        ServletInputStream inputStream = request.getInputStream();
        byte[] buffer = new byte[1024];
        for (int length; (length = inputStream.read(buffer)) != -1; ) {
            body.write(buffer, 0, length);
        }
    } catch (IOException ex) {
        log.error("支付回调,读取数据流异常", ex);
    }
    log.info("支付回调,通知消息体:{}", body);
    return body.toString();
}

有的极少订单回调会报这个解签错误,大部分订单可以成功,这个是body读取的代码示例,不清楚为什么会这样,微信sdk版本是0.2.7

@AndromedaX7
Copy link

感谢大佬们先踩坑了,让咱吃了现成的了

@helloWorld233333333
Copy link

private String getRequestBody(HttpServletRequest request) {
    ByteArrayOutputStream body = new ByteArrayOutputStream();
    try {
        ServletInputStream inputStream = request.getInputStream();
        byte[] buffer = new byte[1024];
        for (int length; (length = inputStream.read(buffer)) != -1; ) {
            body.write(buffer, 0, length);
        }
    } catch (IOException ex) {
        log.error("支付回调,读取数据流异常", ex);
    }
    log.info("支付回调,通知消息体:{}", body);
    return body.toString();
}

有的极少订单回调会报这个解签错误,大部分订单可以成功,这个是body读取的代码示例,不清楚为什么会这样,微信sdk版本是0.2.7

我猜你要找的是‘签名探测流量’,https://pay.weixin.qq.com/docs/merchant/development/interface-rules/signature-verification.html

@songfayuan
Copy link

`package com.github.modules.miniapp.service.impl;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.github.common.utils.HttpServletUtils;
import com.github.common.utils.Response;
import com.github.config.wx.WxPayConfig;
import com.github.modules.miniapp.entity.CreateOrderReq;
import com.github.modules.miniapp.entity.PayOrderInfo;
import com.github.modules.miniapp.entity.QueryOrderReq;
import com.github.modules.miniapp.entity.RefundOrderReq;
import com.github.modules.miniapp.service.WxMiniappPayService;
import com.wechat.pay.java.core.Config;
import com.wechat.pay.java.core.exception.HttpException;
import com.wechat.pay.java.core.exception.MalformedMessageException;
import com.wechat.pay.java.core.exception.ServiceException;
import com.wechat.pay.java.core.exception.ValidationException;
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.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 lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;

import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.math.BigDecimal;
import java.util.HashMap;
import java.util.Map;

/**

  • 微信小程序支付服务实现

  • @author songfayuan

  • @Date 2024/9/14 14:23
    */
    @slf4j
    @service
    public class WxMiniappPayServiceImpl implements WxMiniappPayService {

    @Autowired
    private WxPayConfig wxPayConfig;
    @Autowired
    @qualifier("rsaAutoCertificateConfig")
    private Config config;
    @Autowired
    @qualifier("rsaAutoCertificateConfig")
    private Config notificationConfig;

    /**

    • 预支付订单/统一下单

    • @param req

    • @return
      */
      @OverRide
      public Response createOrder(CreateOrderReq req) {
      // 创建本地订单
      // 这里做本地业务相关的处理,包括生成一个订单号传递给微信,等于通过这个值来形成两边的数据对应。后续微信那边会返回他们的订单编号,也建议存在自己这边的数据库里。
      PayOrderInfo order = new PayOrderInfo();
      order.setOutTradeNo("test_out_trade_no_" + System.currentTimeMillis());
      order.setDescription("测试订单");
      order.setAmount(new BigDecimal("0.01"));

      // 请求微信支付相关配置
      JsapiServiceExtension service = new JsapiServiceExtension.Builder()
      .config(config)
      .signType("RSA")
      .build();
      PrepayWithRequestPaymentResponse response = new PrepayWithRequestPaymentResponse();
      try {
      PrepayRequest request = new PrepayRequest();
      request.setAppid(wxPayConfig.getAppid());
      request.setMchid(wxPayConfig.getMerchantId());
      request.setDescription(order.getDescription());
      request.setOutTradeNo(order.getOutTradeNo());
      request.setNotifyUrl(wxPayConfig.getPayNotifyUrl());
      Amount amount = new Amount();
      // 微信支付的单位是分,这里都需要乘以100
      amount.setTotal(order.getAmount().multiply(new BigDecimal("100")).intValue());
      request.setAmount(amount);
      Payer payer = new Payer();
      payer.setOpenid(req.getWxOpenId());
      request.setPayer(payer);
      log.info("请求预支付下单,请求参数:{}", JSONObject.toJSONString(request));
      // 调用预下单接口
      response = service.prepayWithRequestPayment(request);
      log.info("订单【{}】发起预支付成功,返回信息:{}", order.getOutTradeNo(), JSONObject.toJSONString(response));
      } catch (HttpException e) {
      // 发送HTTP请求失败
      log.error("微信下单发送HTTP请求失败,错误信息:", e);
      return Response.error("下单失败");
      } catch (ServiceException e) {
      // 服务返回状态小于200或大于等于300,例如500
      log.error("微信下单服务状态错误,错误信息:", e);
      return Response.error("下单失败");
      } catch (MalformedMessageException e) {
      // 服务返回成功,返回体类型不合法,或者解析返回体失败
      log.error("服务返回成功,返回体类型不合法,或者解析返回体失败,错误信息:", e);
      return Response.error("下单失败");
      } catch (ValidationException e) {
      // 验证签名失败
      log.error("微信下单验证签名失败,错误信息:", e);
      return Response.error("下单失败");
      } catch (Exception e) {
      log.error("微信下单失败,错误信息:", e);
      return Response.error("下单失败");
      }

      // TODO 更新订单状态
      // 这里就可以更新订单状态为待支付之类的
      return Response.success(response);
      }

    /**

    • 支付回调

    • 注意:

    • 对后台通知交互时,如果微信收到应答不是成功或超时,微信认为通知失败,微信会通过一定的策略定期重新发起通知,尽可能提高通知的成功率,但微信不保证通知最终能成功

    • 同样的通知可能会多次发送给商户系统。商户系统必须能够正确处理重复的通知。 推荐的做法是,当商户系统收到通知进行处理时,先检查对应业务数据的状态,并判断该通知是否已经处理。如果未处理,则再进行处理;如果已处理,则直接返回结果成功。在对业务数据进行状态检查和处理之前,要采用数据锁进行并发控制,以避免函数重入造成的数据混乱。

    • 如果在所有通知频率后没有收到微信侧回调。商户应调用查询订单接口确认订单状态。

    • @param request

    • @return

    • @throws IOException
      */
      @OverRide
      public String payNotify(HttpServletRequest request) throws IOException {
      // 请求头Wechatpay-Signature
      String signature = request.getHeader("Wechatpay-Signature");
      // 请求头Wechatpay-nonce
      String nonce = request.getHeader("Wechatpay-Nonce");
      // 请求头Wechatpay-Timestamp
      String timestamp = request.getHeader("Wechatpay-Timestamp");
      // 微信支付证书序列号
      String serial = request.getHeader("Wechatpay-Serial");
      // 签名方式
      String signType = request.getHeader("Wechatpay-Signature-Type");
      // 构造 RequestParam
      RequestParam requestParam = new RequestParam.Builder()
      .serialNumber(serial)
      .nonce(nonce)
      .signature(signature)
      .timestamp(timestamp)
      .signType(signType)
      .body(HttpServletUtils.getRequestBody(request))
      .build();
      log.info("微信支付回调body信息:{}", requestParam.getBody());
      // 初始化 NotificationParser
      NotificationParser parser = new NotificationParser((NotificationConfig) notificationConfig);
      // 以支付通知回调为例,验签、解密并转换成 Transaction
      log.info("验签参数:{}", JSONObject.toJSONString(requestParam));
      Map<String, String> returnMap = new HashMap<>(2);
      returnMap.put("code", "FAIL");
      returnMap.put("message", "失败");
      Transaction transaction = null;
      try {
      transaction = parser.parse(requestParam, Transaction.class);
      } catch (MalformedMessageException e) {
      log.error("验签失败,解析微信支付应答或回调报文异常,返回信息:", e);
      return JSONObject.toJSONString(returnMap);
      } catch (ValidationException e) {
      log.error("验签失败,验证签名失败,返回信息:", e);
      return JSONObject.toJSONString(returnMap);
      } catch (Exception e) {
      log.error("验签失败,返回信息:", e);
      return JSONObject.toJSONString(returnMap);
      }
      log.info("验签成功!-支付回调结果:{}", transaction.toString());
      // 判断订单状态
      if (Transaction.TradeStateEnum.SUCCESS != transaction.getTradeState()) {
      log.info("内部订单号【{}】,微信支付订单号【{}】支付未成功", transaction.getOutTradeNo(), transaction.getTransactionId());
      return JSONObject.toJSONString(returnMap);
      }

      // TODO 修改订单信息

      returnMap.put("code", "SUCCESS");
      returnMap.put("message", "成功");
      return JSONObject.toJSONString(returnMap);
      }

    /**

    • 根据支付订单号查询订单

    • 需要调用查询接口的情况:

    • 当商户后台、网络、服务器等出现异常,商户系统最终未接收到支付通知。

    • 调用支付接口后,返回系统错误或未知交易状态情况。

    • 调用付款码支付API,返回USERPAYING的状态。

    • 调用关单或撤销接口API之前,需确认支付状态。

    • @param req

    • @return
      */
      @OverRide
      public Response queryOrder(QueryOrderReq req) {
      QueryOrderByIdRequest queryRequest = new QueryOrderByIdRequest();
      queryRequest.setMchid(wxPayConfig.getMerchantId());
      queryRequest.setTransactionId(req.getTransactionId());
      try {
      JsapiServiceExtension service =
      new JsapiServiceExtension.Builder()
      .config(config)
      .signType("RSA")
      .build();
      Transaction result = service.queryOrderById(queryRequest);
      if (Transaction.TradeStateEnum.SUCCESS != result.getTradeState()) {
      log.info("内部订单号【{}】,微信支付订单号【{}】支付未成功", result.getOutTradeNo(), result.getTransactionId());
      return Response.error(result.getTradeStateDesc());
      }
      log.info("根据支付订单号查询订单:内部订单号【{}】,微信支付订单号【{}】支付成功", result.getOutTradeNo(), result.getTransactionId());
      log.info("根据支付订单号查询订单:订单数据data = {}", JSONObject.toJSONString(result));

       // TODO 修改订单信息
       return Response.success(result.getTradeStateDesc());
      

      } catch (ServiceException e) {
      log.error("根据支付订单号查询订单:订单查询失败,发送HTTP请求成功,返回异常,返回码:{},返回信息:", e.getErrorCode(), e);
      return Response.error("订单查询失败");
      } catch (MalformedMessageException e) {
      log.error("根据支付订单号查询订单:订单查询失败,解析微信支付应答或回调报文异常,返回信息:", e);
      return Response.error("订单查询失败");
      } catch (ValidationException e) {
      log.error("根据支付订单号查询订单:订单查询失败,验证签名失败,返回信息:", e);
      return Response.error("验证签名失败");
      } catch (HttpException e) {
      log.error("根据支付订单号查询订单:订单查询失败,发送HTTP请求失败:", e);
      return Response.error("订单查询失败");
      } catch (Exception e) {
      log.error("根据支付订单号查询订单:订单查询失败,异常:", e);
      return Response.error("订单查询失败");
      }
      }

    /**

    • 根据商户订单号查询订单

    • 需要调用查询接口的情况:

    • 当商户后台、网络、服务器等出现异常,商户系统最终未接收到支付通知。

    • 调用支付接口后,返回系统错误或未知交易状态情况。

    • 调用付款码支付API,返回USERPAYING的状态。

    • 调用关单或撤销接口API之前,需确认支付状态。

    • @param req

    • @return
      /
      @OverRide
      public Response queryOrderByOutTradeNo(QueryOrderReq req) {
      QueryOrderByOutTradeNoRequest queryRequest = new QueryOrderByOutTradeNoRequest();
      queryRequest.setMchid(wxPayConfig.getMerchantId());
      queryRequest.setOutTradeNo(req.getOutTradeNo());
      try {
      JsapiServiceExtension service =
      new JsapiServiceExtension.Builder()
      .config(config)
      .signType("RSA")
      .build();
      Transaction result = service.queryOrderByOutTradeNo(queryRequest);
      // trade_state【交易状态】交易状态,枚举值:SUCCESS:支付成功REFUND:转入退款
      NOTPAY:未支付CLOSED:已关闭REVOKED:已撤销(仅付款码支付会返回)*USERPAYING:用户支付中(仅付款码支付会返回)*PAYERROR:支付失败(仅付款码支付会返回)
      if (Transaction.TradeStateEnum.SUCCESS != result.getTradeState()) {
      log.info("内部订单号【{}】,微信支付订单号【{}】支付未成功", result.getOutTradeNo(), result.getTransactionId());
      return Response.error(result.getTradeStateDesc());
      }
      log.info("根据商户订单号查询订单:内部订单号【{}】,微信支付订单号【{}】支付成功", result.getOutTradeNo(), result.getTransactionId());
      log.info("根据商户订单号查询订单:订单数据data = {}", JSONObject.toJSONString(result));

       // 支付订单号
       String transactionId = result.getTransactionId();
      
       // TODO 修改订单信息
       return Response.success(result.getTradeStateDesc());
      

      } catch (ServiceException e) {
      log.error("根据商户订单号查询订单:订单查询失败,发送HTTP请求成功,返回异常,返回码:{},返回信息:", e.getErrorCode(), e);
      return Response.error("订单查询失败");
      } catch (MalformedMessageException e) {
      log.error("根据商户订单号查询订单:订单查询失败,解析微信支付应答或回调报文异常,返回信息:", e);
      return Response.error("订单查询失败");
      } catch (ValidationException e) {
      log.error("根据商户订单号查询订单:订单查询失败,验证签名失败,返回信息:", e);
      return Response.error("验证签名失败");
      } catch (HttpException e) {
      log.error("根据商户订单号查询订单:订单查询失败,发送HTTP请求失败:", e);
      return Response.error("订单查询失败");
      } catch (Exception e) {
      log.error("根据商户订单号查询订单:订单查询失败,异常:", e);
      return Response.error("订单查询失败");
      }
      }

    /**

    • 关闭订单
    • 以下情况需要调用关单接口:
    • 商户订单支付失败需要生成新单号重新发起支付,要对原订单号调用关单,避免重复支付;
    • 系统下单后,用户支付超时,系统退出不再受理,避免用户继续,请调用关单接口。
    • @param req
    • @return
      */
      @OverRide
      public Response closeOrder(QueryOrderReq req) {
      // 初始化服务
      JsapiServiceExtension service =
      new JsapiServiceExtension.Builder()
      .config(config)
      .signType("RSA")
      .build();
      CloseOrderRequest request = new CloseOrderRequest();
      // 调用request.setXxx(val)设置所需参数,具体参数可见Request定义
      request.setMchid(wxPayConfig.getMerchantId());
      request.setOutTradeNo(req.getOutTradeNo());
      // 调用接口
      try {
      service.closeOrder(request);
      } catch (HttpException e) {
      log.error("关闭订单申请失败,发送HTTP请求失败:", e);
      return Response.error("关闭订单申请失败");
      } catch (MalformedMessageException e) {
      log.error("关闭订单申请失败,解析微信支付应答或回调报文异常,返回信息:", e);
      return Response.error("关闭订单申请失败");
      } catch (ValidationException e) {
      log.error("关闭订单申请失败,验证签名失败,返回信息:", e);
      return Response.error("验证签名失败");
      } catch (ServiceException e) {
      log.error("关闭订单申请失败,发送HTTP请求成功,返回异常,返回码:{},返回信息:", e.getErrorCode(), e);
      return Response.error("关闭订单失败:" + e.getErrorMessage());
      } catch (Exception e) {
      log.error("关闭订单申请失败,异常:", e);
      return Response.error("关闭订单申请失败");
      }
      return Response.success("关闭订单申请成功");
      }

    /**

    • 退款

    • 交易时间超过一年的订单无法提交退款(按支付成功时间+365天计算)

    • 微信支付退款支持单笔交易分多次退款,多次退款需要提交原支付订单的商户订单号和设置不同的退款单号。申请退款总金额不能超过订单金额。 一笔退款失败后重新提交,请不要更换退款单号,请使用原商户退款单号

    • 请求频率限制:150qps,即每秒钟正常的申请退款请求次数不超过150次

    • 每个支付订单的部分退款次数不能超过50次

    • 如果同一个用户有多笔退款,建议分不同批次进行退款,避免并发退款导致退款失败

    • 申请退款接口的返回仅代表业务的受理情况,具体退款是否成功,需要通过退款查询接口获取结果

    • 错误或无效请求频率限制:6qps,即每秒钟异常或错误的退款申请请求不超过6次

    • 一个月之前的订单申请退款频率限制为:5000/min

    • 同一笔订单多次退款的请求需相隔1分钟

    • @param req

    • @return
      */
      @OverRide
      public Response refund(RefundOrderReq req) {
      // 初始化服务
      RefundService service = new RefundService.Builder().config(config).build();
      CreateRequest request = new CreateRequest();
      // 调用request.setXxx(val)设置所需参数,具体参数可见Request定义
      request.setOutTradeNo(req.getOutTradeNo());
      request.setOutRefundNo("REFUND_" + req.getOutTradeNo());
      AmountReq amount = new AmountReq();
      // 订单总金额,单位为分,只能为整数,详见支付金额
      amount.setTotal(decimalToLong(req.getTotalAmount()));
      // 退款金额,单位为分,只能为整数,不能超过支付总额
      amount.setRefund(decimalToLong(req.getRefundAmount()));
      amount.setCurrency("CNY");

      request.setAmount(amount);
      request.setNotifyUrl(wxPayConfig.getRefundNotifyUrl());
      // 调用接口
      Refund refund = null;
      try {
      refund = service.create(request);
      } catch (HttpException e) {
      log.error("退款申请失败,发送HTTP请求失败:", e);
      return Response.error("退款失败");
      } catch (MalformedMessageException e) {
      log.error("退款申请失败,解析微信支付应答或回调报文异常,返回信息:", e);
      return Response.error("退款失败");
      } catch (ValidationException e) {
      log.error("退款申请失败,验证签名失败,返回信息:", e);
      return Response.error("验证签名失败");
      } catch (ServiceException e) {
      log.error("退款申请失败,发送HTTP请求成功,返回异常,返回码:{},返回信息:", e.getErrorCode(), e);
      return Response.error("退款失败:" + e.getErrorMessage());
      } catch (Exception e) {
      log.error("退款申请失败,异常:", e);
      return Response.error("退款失败");
      }
      if (Status.SUCCESS.equals(refund.getStatus())) {
      log.info("退款成功!-订单号:{}", req.getOutTradeNo());
      return Response.success("退款成功");
      } else if (Status.CLOSED.equals(refund.getStatus())) {
      log.info("退款关闭!-订单号:{}", req.getOutTradeNo());
      return Response.error("退款关闭");
      } else if (Status.PROCESSING.equals(refund.getStatus())) {
      log.info("退款处理中!-订单号:{}", req.getOutTradeNo());
      return Response.error("退款处理中");
      } else if (Status.ABNORMAL.equals(refund.getStatus())) {
      log.info("退款异常!-订单号:{}", req.getOutTradeNo());
      return Response.error("退款异常");
      }
      return Response.error("退款失败");
      }

    /**

    • 查询单笔退款(通过商户退款单号)

    • 提交退款申请后,通过调用该接口查询退款状态。退款有一定延时,建议查询退款状态在提交退款申请后1分钟发起,一般来说零钱支付的退款5分钟内到账,银行卡支付的退款1-3个工作日到账。

    • @param outRefundNo 商户退款单号

    • @return
      */
      @OverRide
      public Response queryByOutRefundNo(String outRefundNo) {
      // 初始化服务
      RefundService service = new RefundService.Builder().config(config).build();
      QueryByOutRefundNoRequest request = new QueryByOutRefundNoRequest();
      // 调用request.setXxx(val)设置所需参数,具体参数可见Request定义
      request.setOutRefundNo(outRefundNo);
      // 调用接口
      Refund refund = null;
      try {
      refund = service.queryByOutRefundNo(request);
      log.info("退款查询结果:{}", JSONObject.toJSONString(refund));
      //【退款状态】退款到银行发现用户的卡作废或者冻结了,导致原路退款银行卡失败,可前往商户平台(pay.weixin.qq.com)-交易中心,手动处理此笔退款。可选取值:SUCCESS:退款成功CLOSED:退款关闭PROCESSING:退款处理中ABNORMAL:退款异常【退款状态】退款到银行发现用户的卡作废或者冻结了,导致原路退款银行卡失败,可前往商户平台(pay.weixin.qq.com)-交易中心,手动处理此笔退款。可选取值:SUCCESS:退款成功CLOSED:退款关闭PROCESSING:退款处理中ABNORMAL:退款异常
      if (Status.SUCCESS.equals(refund.getStatus())) {
      log.info("退款成功!-订单号:{}", outRefundNo);
      return Response.success("退款成功");
      } else if (Status.CLOSED.equals(refund.getStatus())) {
      log.info("退款关闭!-订单号:{}", outRefundNo);
      return Response.error("退款关闭");
      } else if (Status.PROCESSING.equals(refund.getStatus())) {
      log.info("退款处理中!-订单号:{}", outRefundNo);
      return Response.success("退款处理中");
      } else if (Status.ABNORMAL.equals(refund.getStatus())) {
      log.info("退款异常!-订单号:{}", outRefundNo);
      return Response.error("退款异常");
      }
      } catch (HttpException e) {
      log.error("退款查询失败,发送HTTP请求失败:", e);
      return Response.error("退款查询失败");
      } catch (MalformedMessageException e) {
      log.error("退款查询失败,解析微信支付应答或回调报文异常,返回信息:", e);
      return Response.error("退款查询失败");
      } catch (ValidationException e) {
      log.error("退款查询失败,验证签名失败,返回信息:", e);
      return Response.error("退款查询失败");
      } catch (ServiceException e) {
      log.error("退款查询失败,发送HTTP请求成功,返回异常,返回码:{},返回信息:", e.getErrorCode(), e);
      return Response.error("退款查询失败");
      } catch (Exception e) {
      log.error("退款查询失败,异常:", e);
      return Response.error("退款查询失败");
      }

      return Response.success(refund);
      }

    /**

    • 微信小程序退款回调

    • 注意:

    • 对后台通知交互时,如果微信收到应答不是成功或超时,微信认为通知失败,微信会通过一定的策略定期重新发起通知,尽可能提高通知的成功率,但微信不保证通知最终能成功

    • 同样的通知可能会多次发送给商户系统。商户系统必须能够正确处理重复的通知。 推荐的做法是,当商户系统收到通知进行处理时,先检查对应业务数据的状态,并判断该通知是否已经处理。如果未处理,则再进行处理;如果已处理,则直接返回结果成功。在对业务数据进行状态检查和处理之前,要采用数据锁进行并发控制,以避免函数重入造成的数据混乱。

    • 如果在所有通知频率后没有收到微信侧回调。商户应调用查询订单接口确认订单状态。

    • @param request

    • @return
      */
      @OverRide
      public String refundNotify(HttpServletRequest request) {
      Map<String, String> returnMap = new HashMap<>(2);
      returnMap.put("code", "FAIL");
      returnMap.put("message", "失败");
      try {
      // 请求头Wechatpay-Signature
      String signature = request.getHeader("Wechatpay-Signature");
      // 请求头Wechatpay-nonce
      String nonce = request.getHeader("Wechatpay-Nonce");
      // 请求头Wechatpay-Timestamp
      String timestamp = request.getHeader("Wechatpay-Timestamp");
      // 微信支付证书序列号
      String serial = request.getHeader("Wechatpay-Serial");
      // 签名方式
      String signType = request.getHeader("Wechatpay-Signature-Type");
      // 构造解析器和请求参数
      NotificationParser parser = new NotificationParser((NotificationConfig) notificationConfig);
      RequestParam requestParam = new RequestParam.Builder()
      .serialNumber(serial)
      .nonce(nonce)
      .signature(signature)
      .timestamp(timestamp)
      .signType(signType)
      .body(HttpServletUtils.getRequestBody(request))
      .build();
      log.info("微信小程序支付退款回调验签参数: {}", JSON.toJSONString(requestParam));
      // 解析通知
      RefundNotification notification = null;
      try {
      notification = parser.parse(requestParam, RefundNotification.class);
      } catch (MalformedMessageException e) {
      log.error("微信小程序支付退款回调:回调通知参数不正确、解析通知数据失败:", e);
      returnMap.put("message", "回调通知参数不正确");
      return JSONObject.toJSONString(returnMap);
      } catch (ValidationException e) {
      log.error("微信小程序支付退款回调:签名验证失败 ", e);
      returnMap.put("message", "签名验证失败");
      return JSONObject.toJSONString(returnMap);
      } catch (Exception e) {
      log.error("微信小程序支付退款回调:未知异常 ", e);
      returnMap.put("message", "未知异常");
      return JSONObject.toJSONString(returnMap);
      }
      log.info("微信小程序支付退款回调解析成功: {}", JSON.toJSONString(notification));
      // 根据退款状态处理
      Status refundStatus = notification.getRefundStatus();
      switch (refundStatus) {
      case SUCCESS:
      // TODO 退款成功逻辑

               returnMap.put("code", "SUCCESS");
               returnMap.put("message", "退款成功");
               return JSONObject.toJSONString(returnMap);
           case PROCESSING:
               log.warn("退款处理中: {}", notification);
               returnMap.put("message", "退款处理中,请稍后查询");
               return JSONObject.toJSONString(returnMap);
           case ABNORMAL:
               log.error("退款异常: {}", notification);
               returnMap.put("message", "退款异常,请联系客服");
               return JSONObject.toJSONString(returnMap);
           case CLOSED:
               log.warn("退款已关闭: {}", notification);
               returnMap.put("message", "退款已关闭,操作失败");
               return JSONObject.toJSONString(returnMap);
           default:
               log.error("未知退款状态: {}", refundStatus);
               returnMap.put("message", "未知退款状态");
               return JSONObject.toJSONString(returnMap);
       }
      

      } catch (Exception e) {
      log.error("退款回调处理异常", e);
      returnMap.put("message", "退款处理异常");
      return JSONObject.toJSONString(returnMap);
      }
      }

    /**

    • 金额转换
    • @param money
    • @return
      */
      private static long decimalToLong(BigDecimal money) {
      return money.multiply(BigDecimal.valueOf(100)).longValue();
      }

}
`

`package com.github.config.wx;

import com.wechat.pay.java.core.Config;
import com.wechat.pay.java.core.RSAAutoCertificateConfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.annotation.Resource;

/**

  • 微信支付证书自动更新配置

  • 一个商户号只能初始化一个配置,否则会因为重复的下载任务报错

  • @author songfayuan

  • @Date 2024/9/30 15:57
    */
    @slf4j
    @configuration
    public class WxPayAutoCertificateConfig {
    @resource
    private WxPayConfig wxPayConfig;

    /**

    • 初始化商户配置
    • @return
      */
      @bean
      public Config rsaAutoCertificateConfig() {
      // 这里把Config作为配置Bean是为了避免多次创建资源,一般项目运行的时候这些东西都确定了
      // 具体的参数改为申请的数据,可以通过读配置文件的形式获取
      Config config = new RSAAutoCertificateConfig.Builder()
      .merchantId(wxPayConfig.getMerchantId())
      .privateKeyFromPath(wxPayConfig.getPrivateKeyPath())
      .merchantSerialNumber(wxPayConfig.getMerchantSerialNumber())
      .apiV3Key(wxPayConfig.getApiV3Key())
      .build();

// Config config = new RSAAutoCertificateConfig.Builder()
// .merchantId(wxPayConfig.getMerchantId())
// .privateKeyFromPath(wxPayConfig.getPrivateKeyPath())
// .merchantSerialNumber(wxPayConfig.getMerchantSerialNumber())
// .apiV3Key(wxPayConfig.getApiV3Key())
// .build();
log.info("初始化微信支付商户配置完成...");
return config;
}
}`

`package com.github.common.utils;

import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;

/**

  • HttpServletRequest 获取请求体

  • @author songfayuan

  • @Date 2024/9/30 19:11
    */
    public class HttpServletUtils {

    /**

    • 获取请求体
    • @param request
    • @return
    • @throws IOException
      */
      public static String getRequestBody(HttpServletRequest request) throws IOException {
      ServletInputStream stream = null;
      BufferedReader reader = null;
      StringBuffer sb = new StringBuffer();
      try {
      stream = request.getInputStream();
      // 获取响应
      reader = new BufferedReader(new InputStreamReader(stream, StandardCharsets.UTF_8));
      String line;
      while ((line = reader.readLine()) != null) {
      sb.append(line);
      }
      } catch (IOException e) {
      throw new IOException("读取返回支付接口数据流出现异常!");
      } finally {
      reader.close();
      }
      return sb.toString();
      }

}
`

已解决

@songfayuan
Copy link

亲测完整教程:微信支付|SpringBoot集成微信小程序创建订单&支付&退款(apiV3+SDK保姆级教程)
https://blog.csdn.net/u011019141/article/details/144447844

@snowwalk
Copy link

我甚至先怀疑了一下jdk自带的base64有问题,都没怀疑微信回调的有问题。建议把签名探测 放在回调文档那一页,并突出展示。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

9 participants