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";
    }
}
相关推荐
Yaml4几秒前
智能化健身房管理:Spring Boot与Vue的创新解决方案
前端·spring boot·后端·mysql·vue·健身房管理
LuckyLay21 分钟前
Spring学习笔记_27——@EnableLoadTimeWeaving
java·spring boot·spring
佳佳_1 小时前
Spring Boot 应用启动时打印配置类信息
spring boot·后端
程序媛小果2 小时前
基于java+SpringBoot+Vue的宠物咖啡馆平台设计与实现
java·vue.js·spring boot
狂放不羁霸4 小时前
idea | 搭建 SpringBoot 项目之配置 Maven
spring boot·maven·intellij-idea
计算机学长felix5 小时前
基于SpringBoot的“校园交友网站”的设计与实现(源码+数据库+文档+PPT)
数据库·spring boot·毕业设计·交友
码农派大星。5 小时前
Spring Boot 配置文件
java·spring boot·后端
江深竹静,一苇以航5 小时前
springboot3项目整合Mybatis-plus启动项目报错:Invalid bean definition with name ‘xxxMapper‘
java·spring boot
豪宇刘6 小时前
SpringBoot+Shiro权限管理
java·spring boot·spring
customer087 小时前
【开源免费】基于SpringBoot+Vue.JS医院管理系统(JAVA毕业设计)
java·vue.js·spring boot·后端·spring cloud·开源·intellij-idea