04、SpringBoot + 微信支付 --- 支付通知-->接收支付通知和返回应答

目录

Native 下单

2、支付通知--接收支付通知和返回应答

完整需求介绍:

就是当用户扫描二维码并支付之后,微信支付那边就会返回一个【支付通知】,来告诉商户他的支付结果。商户收到这个支付通知后,会进行一些签名的验签和订单的处理操作之类的,然后再响应回给微信系统,向微信支付系统应答我们对这个支付通知的处理情况。

(应答包括:接收支付通知成功(响应码:200或204)和接收失败(响应码:4xx或5xx))

正常就是告诉微信它发来的支付通知,我们这边已经收到了,并且通过支付通知的数据,处理完自己的核心业务了,并给微信支付平台一个成功的应答。

现在就是做图片中的这步:

微信平台异步通知商户支付结果,商户端告知支付通知的接收情况。

2-1、需求1:应答测试

这里是接收微信支付系统发来的商户支付后的支付通知,这边先不做验签和订单处理,先简单的写几个应答情况给微信支付平台看应答效果

应答情况分为:

接收支付通知成功则返回响应码200或204

接受支付通知失败则返回响应码5xx或4xx

支付通知:

2-2、应答的代码:

这里是接收微信支付系统发来的商户支付后的支付通知,这边先不做验签和订单处理,先简单的测试应答情况给微信支付平台

WxPayController 的这个 nativeNotify() 方法是让微信支付平台来调用的,不是我们去调用的

路径的来源如图:之前调用微信平台的下单接口时,发送过去的我们定义的一个回调支付通知地址。

当我们支付成功后哦,微信平台就会通过这个地址,把支付情况发送给我们。

2-3、结果:

如图,发来的数据是加密的,后续需要对微信发来的结果进行验签,验签成功再用对称加密的密钥对数据进行解密。

测试:应答不符合规范

支付通知:

当响应给微信支付系统的响应码是错误的201时,属于应答不符合规范。

微信认为通知失败,因为我们要么没收到通知,要么处理通知失败,所以微信会通过一定的策略定期重新发起通知

测试:应答出错
测试:应答超时

回调处理逻辑注意事项

让线程睡眠5秒,然后微信支付那边会因为超时没有收到商户的应答,而认为商户这边没有收到通知或通知处理失败,就会继续按规则重复发送通知

2-4、需求2:验签

APIv3证书与密钥使用说明:

自定义一个针对微信平台发来的request类型的支付通知的签名,进行验签操作的工具类。

因为微信支付端并没有给我们提供默认的集成在SDK内部的不用我们自己编写的签名验证,所以我们要自己编写一个【针对微信发来的请求的一个签名验证】。

就是做图片中的这一步。

先看一个jar包里面的针对响应的一个签名验证工具类,参考SDK源码中的 WechatPay2Validator 创建通知验签工具类 WechatPay2ValidatorForRequest

我们要弄一个针对微信支付系统发来的请求来进行验证的工具类,就可以模仿这个工具类

构造验签名串:

代码:
WxPayController
WechatPay2ValidatorForRequest

自定义验签工具类

验签工具类--验的是微信支付平台发给商户端的支付通知,

属于request请求




2-5、测试:

2-6、总结支付通知的流程:

流程:

1、商户端向微信平台发起支付请求。

2、微信平台则对该请求进行响应,响应的内容是一个订单编号和支付二维码的 URL 。

3、商户端这边接收到支付二维码的URL,通过这个URL获取支付二维码图片,并进行支付。

4、商户端进行支付之后,微信平台会自动发起一个请求,请求的内容是商户端支付的结果,就是支付通知。

5、商户端这边对该支付通知进行验签,得到该请求的数据并进行一些业务操作,然后给微信支付平台应答,告诉其自己已经成功收到该通知了。

应答的内容也包括:接收通知成功和接收通知失败两种。

6、如果给微信支付平台的应答是接受通知失败的状态码。那么微信支付平台就会根据规则,在一段事件内重复发送支付通知给客户端。当然,这个重复发送通知的作用,只是为了提高商户端那边能成功接收到通知的概率(比如商户端接收通知失败的原因可以有网络原因),但是不保证一定成功。

2-7、完整代码:

WxPayController
java 复制代码
@CrossOrigin //跨域
@RestController
@RequestMapping("/api/wx-pay")
@Api(tags = "网站微信支付API") //swagger 注解
@Slf4j
public class WxPayController
{
    @Resource
    private WxPayService wxPayService;

    //获取签名验证器需要的类
    @Resource
    private Verifier verifier;

    //调用统一下单API,生成支付二维码的链接和订单号
    //swagger注解
    @ApiOperation("调用统一下单API,生成支付二维码")
    @PostMapping("/native/{productId}")
    public R nativePay(@PathVariable Long productId) throws Exception
    {
        log.info("发起支付请求");
        //返回支付二维码的链接和订单号
        Map<String, Object> map = wxPayService.nativePay(productId);
        return R.ok().setData(map);
    }

    /*
     *  接收并处理完微信发来的通知后,需要给微信支付系统应答我们的处理情况
     *  告诉微信它发来的支付成功的通知,我们这边已经收到了,并且处理完自己的核心业务了,现在可以给微信一个成功的应答了。
     */
    //当我们支付后,微信支付系统会通过我们之前给的notify_url路径,把支付的结果响应回来
    //通知接口(回调通知)---接收微信服务器给我们发来的请求
    //request: 接收微信支付端发来的支付通知的请求的参数数据 , response: 响应回给微信支付端的应答
    @PostMapping("/native/notify")
    public String nativeNotify(HttpServletRequest request, HttpServletResponse response)
    {
        Gson gson = new Gson();
        //创建一个应答对象
        HashMap<String, String> map = new HashMap<>();
        try
        {
            //处理微信支付系统响应回来的通知参数
            String body = HttpUtils.readData(request);

            //gson.fromJson()是Gson库提供的一个方法,用于将 JSON 字符串转换为 Java 对象
            Map<String, Object> bodyMap = gson.fromJson(body, HashMap.class);
            //微信支付端发来的支付通知的请求的id
            String requestId = (String) bodyMap.get("id");

            log.info("支付通知的id ====> {}", bodyMap.get("id"));

            //log.info("支付通知的完整数据 ====> {}", body);
            //todo:签名的验证
            WechatPay2ValidatorForRequest wechatPay2ValidatorForRequest =
                    new WechatPay2ValidatorForRequest(verifier, requestId,body);
            //判断验签是否成功
            if (!wechatPay2ValidatorForRequest.validate(request))
            {
                log.error("通知验签失败");
                //如果验签不通过,就给个失败的应答
                response.setStatus(500);
                map.put("code", "ERROR");
                map.put("message", "微信支付系统发给商户的通知,验签失败");
                return gson.toJson(map);
            }
            log.info("签名验证成功");

            //todo :处理订单
            /*
             * 通知应答
             * 接收成功: HTTP应答状态码需返回200或204,无需返回应答报文。
             * 接收失败: HTTP应答状态码需返回5XX或4XX,同时需返回应答报文,格式如下
             */
            //Thread.sleep(10000); //单位:毫秒, 1000毫秒=1秒,这里是睡眠10秒钟
            //TimeUnit.SECONDS.sleep(6); //让线程睡眠超过5秒
            response.setStatus(200);
            map.put("code", "SUCCESS");
            map.put("message", "成功");
            return gson.toJson(map);
        } catch (Exception e)
        {
            e.printStackTrace();
            response.setStatus(500);
            map.put("code", "ERROR");
            map.put("message", "失败");
            return gson.toJson(map);
        }
    }


}
WechatPay2ValidatorForRequest

验签工具类--验的是微信支付平台发给商户端的支付通知,

属于request请求

java 复制代码
package cn.ljh.paymentdemo.util;

import com.wechat.pay.contrib.apache.httpclient.auth.Verifier;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.time.DateTimeException;
import java.time.Duration;
import java.time.Instant;

import static com.wechat.pay.contrib.apache.httpclient.constant.WechatPayHttpHeaders.*;

//这个类用来验证微信支付系统发给商户的支付通知的签名,就是验签
public class WechatPay2ValidatorForRequest
{
    protected static final Logger log = LoggerFactory.getLogger(WechatPay2ValidatorForRequest.class);
    /**
     * 应答超时时间,单位为分钟
     */
    protected static final long RESPONSE_EXPIRED_MINUTES = 5;
    protected final Verifier verifier;
    protected final String requestId;
    protected final String body;

    //参数1:获取签名验证器  参数2:微信支付系统发来的那个请求通知的id    参数3:微信支付系统响应回来的通知参数
    public WechatPay2ValidatorForRequest(Verifier verifier, String requestId, String body)
    {
        this.verifier = verifier;
        this.requestId = requestId;
        this.body = body;
    }

    protected static IllegalArgumentException parameterError(String message, Object... args)
    {
        message = String.format(message, args);
        return new IllegalArgumentException("parameter error: " + message);
    }

    protected static IllegalArgumentException verifyFail(String message, Object... args)
    {
        message = String.format(message, args);
        return new IllegalArgumentException("signature verify fail: " + message);
    }

    //验签方法
    public final boolean validate(HttpServletRequest request) throws IOException
    {
        try
        {
            //处理请求参数
            validateParameters(request);

            //调用构造验签名串方法
            String message = buildMessage(request);
            //从请求头当中拿到平台证书序列号
            String serial = request.getHeader(WECHAT_PAY_SERIAL);
            //从请求头当中拿到请求当中携带的签名
            String signature = request.getHeader(WECHAT_PAY_SIGNATURE);

            //利用verifier这个对象,进行验签的具体操作
            if (!verifier.verify(serial, message.getBytes(StandardCharsets.UTF_8), signature))
            {
                //如果验签失败,则抛异常
                throw verifyFail("serial=[%s] message=[%s] sign=[%s], request-id=[%s]",
                        serial, message, signature, requestId);
            }
        } catch (IllegalArgumentException e)
        {
            log.warn(e.getMessage());
            return false;
        }
        //验签成功
        return true;
    }

    //处理请求参数的方法-- 对参数进行判断的过程
    protected final void validateParameters(HttpServletRequest request)
    {

        // NOTE: ensure HEADER_WECHAT_PAY_TIMESTAMP at last
        String[] headers = {WECHAT_PAY_SERIAL, WECHAT_PAY_SIGNATURE, WECHAT_PAY_NONCE, WECHAT_PAY_TIMESTAMP};

        String header = null;
        //判空
        for (String headerName : headers)
        {
            header = request.getHeader(headerName);
            if (header == null)
            {
                throw parameterError("empty [%s], request-id=[%s]", headerName, requestId);
            }
        }

        //header 就是这个---> WECHAT_PAY_TIMESTAMP 时间戳,用于判断请求是否过期
        String timestampStr = header;
        try
        {
            //通过时间戳创建一个基于时间戳那个时间的时间对象
            Instant responseTime = Instant.ofEpochSecond(Long.parseLong(timestampStr));
            // 如果时间戳过期了,就拒绝过期请求,  创建一个基于此时此刻的时间对象--> Instant.now()
            //比较这两个时间对象 --> responseTimeh 和 Instant.now() ,如果比较的时间大于5分钟,就会认为这是一个过期的请求,那么就拒绝
            if (Duration.between(responseTime, Instant.now()).abs().toMinutes() >= RESPONSE_EXPIRED_MINUTES)
            {
                throw parameterError("timestamp=[%s] expires, request-id=[%s]", timestampStr, requestId);
            }
        } catch (DateTimeException | NumberFormatException e)
        {
            throw parameterError("invalid timestamp=[%s], request-id=[%s]", timestampStr, requestId);
        }
    }

    //构造验签名串方法
    protected final String buildMessage(HttpServletRequest request) throws IOException
    {
        //获取时间戳
        String timestamp = request.getHeader(WECHAT_PAY_TIMESTAMP);
        //随机数的字符串形式
        String nonce = request.getHeader(WECHAT_PAY_NONCE);
        return timestamp + "\n"
                + nonce + "\n"
                + body + "\n";
    }
}
相关推荐
用键盘当武器的秋刀鱼2 小时前
springBoot统一响应类型3.5.1版本
java·spring boot·后端
小李同学_LHY3 小时前
三.微服务架构中的精妙设计:服务注册/服务发现-Eureka
java·spring boot·spring·springcloud
爱喝醋的雷达4 小时前
Spring SpringBoot 细节总结
java·spring boot·spring
嘵奇6 小时前
深入解析 Spring Boot 测试核心注解
java·spring boot·后端
技术liul7 小时前
解决Spring Boot Configuration Annotation Processor not configured
java·spring boot·后端
腥臭腐朽的日子熠熠生辉10 小时前
解决maven失效问题(现象:maven中只有jdk的工具包,没有springboot的包)
java·spring boot·maven
绝顶少年12 小时前
Spring Boot 注解:深度解析与应用场景
java·spring boot·后端
西木风落12 小时前
springboot整合Thymeleaf web开发出现Whitelabel Error Page
spring boot·thymeleaf error·whitelabelerror
有来技术14 小时前
从0到1手撸企业级权限系统:基于 youlai-boot(开源) + Java17 + Spring Boot 3 完整实战
java·spring boot·后端
橘猫云计算机设计14 小时前
基于springboot微信小程序的旅游攻略系统(源码+lw+部署文档+讲解),源码可白嫖!
java·spring boot·后端·微信小程序·毕业设计·旅游