Java微信支付接入(6) - API V3 Native 支付通知API

        官方文档:https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_5.shtml

        通知规则:用户支付完成后,微信会把相关支付结果和用户信息发送给商户,商户需要接收处理该消息,并返回应答。对后台通知交互时,如果微信收到商户的应答不符合规范或超时,微信认为通知失败,微信会通过一定的策略定期重新发起通知,尽可能提高通知的成功率,但微信不保 证通知最终能成功。(通知频率为 15s/15s/30s/3m/10m/20m/30m/30m/30m/60m/3h/3h/3h/6h/6h - 总计 24h4m)

1.创建通知接口

/**
     * 支付通知
     * 微信支付通过支付通知接口将用户支付成功消息通知给商户
     */
@ApiOperation("支付通知")
@PostMapping("/native/notify")
public String nativeNotify(HttpServletRequest request, HttpServletResponse 
response){
    Gson gson = new Gson();
    Map<String, String> map = new HashMap<>();//应答对象
    //处理通知参数
    String body = HttpUtils.readData(request);
    Map<String, Object> bodyMap = gson.fromJson(body, HashMap.class);
    log.info("支付通知的id ===> {}", bodyMap.get("id"));
    log.info("支付通知的完整数据 ===> {}", body);
    //TODO : 签名的验证
    //TODO : 处理订单
    //成功应答:成功应答必须为200或204,否则就是失败应答
    response.setStatus(200);
    map.put("code", "SUCCESS");
    map.put("message", "成功");
    return gson.toJson(map);
}

2.失败应答

@PostMapping("/native/notify")
public String nativeNotify(HttpServletRequest request, HttpServletResponse 
response) throws Exception {
    Gson gson = new Gson();
    Map<String, String> map = new HashMap<>();
    try {
   } catch (Exception e) {
        
        e.printStackTrace();
        // 测试错误应答
        response.setStatus(500);
        map.put("code", "ERROR");
        map.put("message", "系统错误");
        return gson.toJson(map);
   }
}

3.验签

        微信发来的通知请求我们要进行验签,确定是微信传来的

        这里提供一个验证请求签名的工具类

import com.wechat.pay.contrib.apache.httpclient.auth.Verifier;
import org.apache.http.HttpEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.util.EntityUtils;
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.*;

/**
 * @author xy-peng
 */
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;


    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);

            //验签
            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);
            }
        }

        //判断请求是否过期
        String timestampStr = header;
        try {
            Instant responseTime = Instant.ofEpochSecond(Long.parseLong(timestampStr));
            // 拒绝过期请求
            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";
    }

    protected final String getResponseBody(CloseableHttpResponse response) throws IOException {
        HttpEntity entity = response.getEntity();
        return (entity != null && entity.isRepeatable()) ? EntityUtils.toString(entity) : "";
    }

}

        签名的验证:

        verifier 是Java微信支付接入(3) - API V3 获取签名验证器和HttpClient_java 微信支付v3 签名-CSDN博客

中提到的签名验证器,

        requestId 是微信通知请求中携带的通知 id,在微信通知请求参数中对应的 key 是“id” 

        body 是请求体,请求中的数据内容(字符串形式)

@Resource
private Verifier verifier;



//签名的验证
WechatPay2ValidatorForRequest validator 
    = new WechatPay2ValidatorForRequest(verifier, body, requestId);
if (!validator.validate(request)) {
    log.error("通知验签失败");
    //失败应答
    response.setStatus(500);
    map.put("code", "ERROR");
    map.put("message", "通知验签失败");
    return gson.toJson(map);
}
log.info("通知验签成功");
//TODO : 处理订单

4.解密

https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_5.shtml

        验签成功,确定该请求是微信传来的,我们要对请求中的订单信息进行解密,因为微信发来的请求中已经将订单数据用对称密钥进行加密了

        这里提供一个解密的参考方法(传入请求包含的数据内容返回解密后得到的商品信息):

/**
     * 对称解密
     * @param bodyMap
     * @return
     */
private String decryptFromResource(Map<String, Object> bodyMap) throws 
GeneralSecurityException {
    log.info("密文解密");
    //通知数据
    Map<String, String> resourceMap = (Map) bodyMap.get("resource");
    //数据密文
    String ciphertext = resourceMap.get("ciphertext");
    //随机串
    String nonce = resourceMap.get("nonce");
    //附加数据
    String associatedData = resourceMap.get("associated_data");
    log.info("密文 ===> {}", ciphertext);
    AesUtil aesUtil = new 
AesUtil(wxPayConfig.getApiV3Key().getBytes(StandardCharsets.UTF_8));
    String plainText = 
aesUtil.decryptToString(associatedData.getBytes(StandardCharsets.UTF_8),
                                               
nonce.getBytes(StandardCharsets.UTF_8),
                                               ciphertext);
    log.info("明文 ===> {}", plainText);
    return plainText;
}

5.处理订单

        解密得到明文以后,明文中的订单信息包含订单号,我们可以使用订单号去更新我们商户数据库中的订单信息了

@Override
public void processOrder(Map<String, Object> bodyMap) throws 
GeneralSecurityException {
    log.info("处理订单");
    String plainText = decryptFromResource(bodyMap);
    //转换明文
    Gson gson = new Gson();
    Map<String, Object> plainTextMap = gson.fromJson(plainText, HashMap.class);
    String orderNo = (String)plainTextMap.get("out_trade_no");
    //更新订单状态
    orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.SUCCESS);
    //记录支付日志
    paymentInfoService.createPaymentInfo(plainText);
}

        提供一个记录支付日志(订单信息)的一个参考方法:

/**
 * 记录支付日志
 * @param plainText
 */
@Override
public void createPaymentInfo(String plainText) {
    log.info("记录支付日志");
    Gson gson = new Gson();
    Map<String, Object> plainTextMap = gson.fromJson(plainText, HashMap.class);
    String orderNo = (String)plainTextMap.get("out_trade_no");
    String transactionId = (String)plainTextMap.get("transaction_id");
    String tradeType = (String)plainTextMap.get("trade_type");
    String tradeState = (String)plainTextMap.get("trade_state");
    Map<String, Object> amount = (Map)plainTextMap.get("amount");
    Integer payerTotal = ((Double) amount.get("payer_total")).intValue();
    PaymentInfo paymentInfo = new PaymentInfo();
    paymentInfo.setOrderNo(orderNo);
    paymentInfo.setPaymentType(PayType.WXPAY.getType());
    paymentInfo.setTransactionId(transactionId);
    paymentInfo.setTradeType(tradeType);
    paymentInfo.setTradeState(tradeState);
    paymentInfo.setPayerTotal(payerTotal);
    paymentInfo.setContent(plainText);
    baseMapper.insert(paymentInfo);
}

处理重复通知

        根据订单编号查询订单状态,如果订单状态是未支付,才处理订单

//处理重复通知
//保证接口调用的幂等性:无论接口被调用多少次,产生的结果是一致的
String orderStatus = orderInfoService.getOrderStatus(orderNo);
if (!OrderStatus.NOTPAY.getType().equals(orderStatus)) {
    return;
}

数据锁

        定义ReentrantLock

        定义 ReentrantLock进行并发控制。注意,必须手动释放锁

private final ReentrantLock lock = new ReentrantLock();


@Override
public void processOrder(Map<String, Object> bodyMap) throws 
GeneralSecurityException {
    log.info("处理订单");
    //解密报文
    String plainText = decryptFromResource(bodyMap);
    //将明文转换成map
    Gson gson = new Gson();
    HashMap plainTextMap = gson.fromJson(plainText, HashMap.class);
    String orderNo = (String)plainTextMap.get("out_trade_no");
    /*在对业务数据进行状态检查和处理之前,
    要采用数据锁进行并发控制,
    以避免函数重入造成的数据混乱*/
    //尝试获取锁:
    // 成功获取则立即返回true,获取失败则立即返回false。不必一直等待锁的释放
    if(lock.tryLock()){
        try {
            //处理重复的通知
            //接口调用的幂等性:无论接口被调用多少次,产生的结果是一致的。
            String orderStatus = orderInfoService.getOrderStatus(orderNo);
            if(!OrderStatus.NOTPAY.getType().equals(orderStatus)){
                return;
           }
            //模拟通知并发
            try {
                TimeUnit.SECONDS.sleep(5);
           } catch (InterruptedException e) {
                e.printStackTrace();
           }
            //更新订单状态
            orderInfoService.updateStatusByOrderNo(orderNo, 
OrderStatus.SUCCESS);
            //记录支付日志
            paymentInfoService.createPaymentInfo(plainText);
       } finally {
            //要主动释放锁
            lock.unlock();
       }
   }
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/890395.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

如何解决 Vim 中的 “E212: Can‘t open file for writing“ 错误:从编辑到权限管理(sudo)

个人名片 &#x1f393;作者简介&#xff1a;java领域优质创作者 &#x1f310;个人主页&#xff1a;码农阿豪 &#x1f4de;工作室&#xff1a;新空间代码工作室&#xff08;提供各种软件服务&#xff09; &#x1f48c;个人邮箱&#xff1a;[2435024119qq.com] &#x1f4f1…

第十五届蓝桥杯C++B组省赛

文章目录 1.握手问题解题思路1&#xff08;组合数学&#xff09;解题思路2&#xff08;暴力枚举&#xff09; 2.小球反弹做题思路 3.好数算法思路&#xff08;暴力解法&#xff09;---不会超时 4.R格式算法思路 5.宝石组合算法思路---唯一分解定理 6.数字接龙算法思路----DFS 7…

TinyOS 点对基站通信

文章目录 一、前言1.1 发包的BlinkToRadio的数据包格式 二、混淆基站源码分析2.1 Makefile2.2 组件连接2.3 主逻辑代码 一、前言 1.1 发包的BlinkToRadio的数据包格式 如下&#xff0c;注意&#xff1a;AM层类型(1byte)即handlerID使可以在组件中修改的。 二、混淆基站源码…

uniapp学习(004-1 组件 Part.2生命周期)

零基础入门uniapp Vue3组合式API版本到咸虾米壁纸项目实战&#xff0c;开发打包微信小程序、抖音小程序、H5、安卓APP客户端等 总时长 23:40:00 共116P 此文章包含第31p-第p35的内容 文章目录 组件生命周期我们主要使用的三种生命周期setup(创建组件时执行)不可以操作dom节点…

使用 three.js和 shader 实现一个五星红旗 飘扬得着色器

使用 three.js和 shader 实现一个五星红旗 飘扬得着色器 源链接&#xff1a;https://threehub.cn/#/codeMirror?navigationThreeJS&classifyshader&idchinaFlag 国内站点预览&#xff1a;http://threehub.cn github地址: https://github.com/z2586300277/three-ce…

python异常检测 - 随机离群选择Stochastic Outlier Selection (SOS)

python异常检测 - Stochastic Outlier Selection (SOS) 前言 随机离群选择SOS算法全称stochastic outlier selection algorithm. 该算法的作者是jeroen janssens. SOS算法是一种无监督的异常检测算法. 随机离群选择SOS算法原理 随机离群选择SOS算法的输入: 特征矩阵(featu…

【代码】集合set

哈喽大家好&#xff0c;我是学霸小羊&#xff0c;今天来讲一讲集合&#xff08;set&#xff09;。 在数学上&#xff0c;集合长这样&#xff1a; 那今天就来讲一讲编程上的集合。 集合的定义&#xff1a;把一些元素按照某些规律放在一起&#xff0c;就形成了一个集合。比如说…

stm32单片机个人学习笔记10(TIM编码器接口)

前言 本篇文章属于stm32单片机&#xff08;以下简称单片机&#xff09;的学习笔记&#xff0c;来源于B站教学视频。下面是这位up主的视频链接。本文为个人学习笔记&#xff0c;只能做参考&#xff0c;细节方面建议观看视频&#xff0c;肯定受益匪浅。 STM32入门教程-2023版 细…

论文笔记:Template-Based Named Entity Recognition Using BART

论文来源&#xff1a;ACL 2021 Finding 论文链接&#xff1a;https://aclanthology.org/2021.findings-acl.161.pdf 论文代码&#xff1a;GitHub - Nealcly/templateNER: Source code for template-based NER 笔记仅供参考&#xff0c;撰写不易&#xff0c;请勿恶意转载抄袭…

D35【python 接口自动化学习】- python基础之输入输出与文件操作

day35 文件合并 学习日期&#xff1a;20241012 学习目标&#xff1a;输入输出与文件操作&#xfe63;-47 如何使用python合并多个文件&#xff1f; 学习笔记&#xff1a; 合并文件需求分析 合并两个文件 代码实现 # 合并两个文件 with open(demo1.txt) as f1:file_data_1f…

机器学习(10.7-10.13)(Pytorch LSTM和LSTMP的原理及其手写复现)

文章目录 摘要Abstract1 LSTM1.1 使用Pytorch LSTM1.1.1 LSTM API代码实现1.1.2 LSTMP代码实现 1.2 手写一个lstm_forward函数 实现单向LSTM的计算原理1.3 手写一个lstmp_forward函数 实现单向LSTMP的计算原理总结 摘要 LSTM是RNN的一个优秀的变种模型&#xff0c;继承了大部分…

鸿蒙--知乎评论

这里我们将采用组件化的思想进行开发 在开发中默认展示的是首页也就是 pages/Index.ets页面 这里存放的是所有页面的配置文件,类似与uniapp中的pages.json 如果我们此时要更改默认显示Zh

jmeter入门: 安装

前提&#xff1a; 安装jdk1.8&#xff0c; 并设置java_home 和path环境变量。 ​​​​​​1. download Apache JMeter - Download Apache JMeter 2. 解压jmeter包 3. 安装插件Install :: JMeter-Plugins.org 下载jar包&#xff0c;放到lib/ext目录 4. 打开jmeter &#xff0…

安装Node.js环境,安装vue工具

一、安装Node.js 去官方网站自行安装自己所需求的安装包 这是下载的官方网站 下载 | Node.js 中文网 给I accept the terms in the License Agreement打上勾然后点击Next 把安装包放到自己所知道的位置,后面一直点Next即可 等待它安装好 然后winr打开命令提示符cmd 二、安装…

解决报错:Invalid number of channels [PaErrorCode -9998]

继昨天重装了树莓派系统后&#xff0c;今天开始重新安装语音助手。在测试录音代码时遇到了报错“Invalid number of channels [PaErrorCode -9998]”&#xff0c;这是怎么回事&#xff1f; 有人说这是因为pyaudio没有安装成功造成的。于是&#xff0c;我pip3 install –upgrad…

难点:Linux 死机定位(进程虚拟地址空间耗尽)

死机定位(进程虚拟地址空间耗尽) 一、死机现象 内存富裕,但内存申请失败。 死机时打印: 怀疑是: 1、内存碎片原因导致。 2、进程虚拟地址空间耗尽导致。 3、进程资源限制导致。 二、内存碎片分析 1、理论知识:如何分析内存碎片化情况 使用 /proc/buddyinfo: /proc/…

数据结构-串

串的定义 串的操作 字符集编码 串的顺序存储 串的链式存储 模式匹配

完成Sentinel-Dashboard控制台数据的持久化-同步到Nacos

本次案例采用的是Sentinel1.8.8版本 一、Sentinel源码环境搭建 1、下载Sentinel源码工程 git clone https://github.com/alibaba/Sentinel.git 2、导入到idea 这里可以先运行DashboardApplication.java试一下是否运行成功&#xff0c;若成功&#xff0c;源码环境搭建完毕&a…

树莓派应用--AI项目实战篇来啦-11.OpenCV定位物体的实时位置

1. 介绍 本项目通过PCA9685舵机控制模块控制二自由度舵机云台固定在零点位置&#xff0c;然后通OpenCV检测到黄色小熊&#xff0c;找到中心位置并打印出中心位置的坐标&#xff0c;通过双色LED灯进行指示是否检测到目标&#xff0c;本项目为后面二维云台追踪物体和追踪人脸提供…

图论day56|广度优先搜索理论基础 、bfs与dfs的对比(思维导图)、 99.岛屿数量(卡码网)、100.岛屿的最大面积(卡码网)

图论day56|广度优先搜索理论基础 、bfs与dfs的对比&#xff08;思维导图&#xff09;、 99.岛屿数量&#xff08;卡码网&#xff09;、100.岛屿的最大面积&#xff08;卡码网&#xff09;&#xff09; 广度优先搜索理论基础bfs与dfs的对比&#xff08;思维导图&#xff09;&…