微信支付v3

文章目录


前言

本篇文章基于PC网站的情景来实现微信支付
学习自尚硅谷


1. 微信支付产品介绍

官网
https://pay.weixin.qq.com/static/product/product_index.shtml#payment_product
在这里插入图片描述

2 接入指引

2.1 获取商户号

接入引链接
https://pay.weixin.qq.com/static/applyment_guide/applyment_index.shtml
选择支付产品后接入微信支付

2.2 获取appid

来到微信公众号平台选择注册小程序或公众号等
获取appid
https://mp.weixin.qq.com

然后到微信支付商户平台中
选择关联appid
这样你的商户号和appid就联系起来了
在这里插入图片描述
接着在公众号平台中选择同意绑定

2.3 获取密钥和证书

进入微信商户平台
选择 账户中心 安全中心 api安全
对这三项进行设置
在这里插入图片描述

3 支付安全

3.1 对称加密和非对称加密

在这里插入图片描述

3.2 身份认证

私钥加密 公钥解密的目的是为了身份认证

3.3 数字证书

在这里插入图片描述在这里插入图片描述

3.4 https中的数字证书

在这里插入图片描述

3.5 微信支付中的证书密钥和签名

因为在微信支付的过程中是我们商户和微信平台的数据信息互通
所以需要商户的证书,公钥私钥 和 微信平台的证书公钥私钥

1 之前我们生成的商户的api证书中包括了
商户的公钥(在证书cert中)和私钥(在key中)
在这里插入图片描述
2 我们可以通过api接口获取的方式获取微信平台证书
https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay3_2.shtml
在这里插入图片描述

4 基础支付apiv3

4.1 基础支付APly3-引入支付参数

1 在resource中定义如下配置文件

在这里插入图片描述

# 微信支付相关参数
# 商户号
wxpay.mch-id=1xxxxxx
# 商户API证书序列号
wxpay.mch-serial-no=3xxxxxxxxxxxxx

# 商户私钥文件
wxpay.private-key-path=apiclient_key.pem
# APIv3密钥
wxpay.api-v3-key=UDuLFxxxxxxxxxxx
# APPID
wxpay.appid=wx748xxxxxxx
# 微信服务器地址
wxpay.domain=https://api.mch.weixin.qq.com
# 接收结果通知地址
# 注意:每次重新启动ngrok,都需要根据实际情况修改这个配置
wxpay.notify-domain=https://500c-219-1xxxxxxxxx

## APIv2密钥
#wxpay.partnerKey: T6m9xxxxxxxxx

2 定义config加载在resource中定义的资源

在这里插入图片描述

package com.atguigu.paymentdemo.config;

//import com.wechat.pay.contrib.apache.httpclient.WechatPayHttpClientBuilder;
//import com.wechat.pay.contrib.apache.httpclient.auth.*;
//import com.wechat.pay.contrib.apache.httpclient.util.PemUtil;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
//import org.apache.http.impl.client.CloseableHttpClient;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.nio.charset.StandardCharsets;
import java.security.PrivateKey;


@Configuration
@PropertySource("classpath:wxpay.properties") //读取配置文件
@ConfigurationProperties(prefix="wxpay") //读取wxpay节点
@Data //使用set方法将wxpay节点中的值填充到当前类的属性中
@Slf4j
public class WxPayConfig {
    
    

    // 商户号
    private String mchId;

    // 商户API证书序列号
    private String mchSerialNo;

    // 商户私钥文件
    private String privateKeyPath;

    // APIv3密钥
    private String apiV3Key;

    // APPID
    private String appid;

    // 微信服务器地址
    private String domain;

    // 接收结果通知地址
    private String notifyDomain;

    // APIv2密钥
    private String partnerKey;

//    /**
//     * 获取商户的私钥文件
//     * @param filename
//     * @return
//     */
//    private PrivateKey getPrivateKey(String filename){
    
    
//
//        try {
    
    
//            return PemUtil.loadPrivateKey(new FileInputStream(filename));
//        } catch (FileNotFoundException e) {
    
    
//            throw new RuntimeException("私钥文件不存在", e);
//        }
//    }
//
//    /**
//     * 获取签名验证器
//     * @return
//     */
//    @Bean
//    public ScheduledUpdateCertificatesVerifier getVerifier(){
    
    
//
//        log.info("获取签名验证器");
//
//        //获取商户私钥
//        PrivateKey privateKey = getPrivateKey(privateKeyPath);
//
//        //私钥签名对象
//        PrivateKeySigner privateKeySigner = new PrivateKeySigner(mchSerialNo, privateKey);
//
//        //身份认证对象
//        WechatPay2Credentials wechatPay2Credentials = new WechatPay2Credentials(mchId, privateKeySigner);
//
//        // 使用定时更新的签名验证器,不需要传入证书
//        ScheduledUpdateCertificatesVerifier verifier = new ScheduledUpdateCertificatesVerifier(
//                wechatPay2Credentials,
//                apiV3Key.getBytes(StandardCharsets.UTF_8));
//
//        return verifier;
//    }
//
//
//    /**
//     * 获取http请求对象
//     * @param verifier
//     * @return
//     */
//    @Bean(name = "wxPayClient")
//    public CloseableHttpClient getWxPayClient(ScheduledUpdateCertificatesVerifier verifier){
    
    
//
//        log.info("获取httpClient");
//
//        //获取商户私钥
//        PrivateKey privateKey = getPrivateKey(privateKeyPath);
//
//        WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create()
//                .withMerchant(mchId, mchSerialNo, privateKey)
//                .withValidator(new WechatPay2Validator(verifier));
//        // ... 接下来,你仍然可以通过builder设置各种参数,来配置你的HttpClient
//
//        // 通过WechatPayHttpClientBuilder构造的HttpClient,会自动的处理签名和验签,并进行证书自动更新
//        CloseableHttpClient httpClient = builder.build();
//
//        return httpClient;
//    }
//
//    /**
//     * 获取HttpClient,无需进行应答签名验证,跳过验签的流程
//     */
//    @Bean(name = "wxPayNoSignClient")
//    public CloseableHttpClient getWxPayNoSignClient(){
    
    
//
//        //获取商户私钥
//        PrivateKey privateKey = getPrivateKey(privateKeyPath);
//
//        //用于构造HttpClient
//        WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create()
//                //设置商户信息
//                .withMerchant(mchId, mchSerialNo, privateKey)
//                //无需进行签名验证、通过withValidator((response) -> true)实现
//                .withValidator((response) -> true);
//
//        // 通过WechatPayHttpClientBuilder构造的HttpClient,会自动的处理签名和验签,并进行证书自动更新
//        CloseableHttpClient httpClient = builder.build();
//
//        log.info("== getWxPayNoSignClient END ==");
//
//        return httpClient;
//    }

}

4.2 基础支付APly3-加载用户私钥

1 导入SDK
https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay6_0.shtml
在这里插入图片描述
先导入依赖

<dependency>
    <groupId>com.github.wechatpay-apiv3</groupId>
    <artifactId>wechatpay-apache-httpclient</artifactId>
    <version>0.4.8</version>
</dependency>

在文档中搜索私钥

在这里插入图片描述

# 示例:私钥存储在文件
PrivateKey merchantPrivateKey = PemUtil.loadPrivateKey(
        new FileInputStream("/path/to/apiclient_key.pem"));

# 示例:私钥为String字符串
PrivateKey merchantPrivateKey = PemUtil.loadPrivateKey(
        new ByteArrayInputStream(privateKey.getBytes("utf-8")));
    /**
     * 获取商户的私钥文件
     * @param filename
     * @return
     */
    public PrivateKey getPrivateKey(String filename){
    
    

        try {
    
    
            //通过导入的SDK 和 商户的数字证书路径filename 获得 PrivateKey 并且返回
            return PemUtil.loadPrivateKey(new FileInputStream(filename));


        } catch (FileNotFoundException e )  {
    
    
            throw new RuntimeException("私钥文件不存在", e);
        }
    }

4.3 基础支付APIv3-获取验签器和HttpClient

我们所要做的是使用商户私钥加密和
使用微信支付平台的公钥进行解密

在这里插入图片描述


/**
     * 获取签名验证器
     * @return
     */
    @Bean
    public ScheduledUpdateCertificatesVerifier getVerifier(){
    
    

        log.info("获取签名验证器");

        //获取商户私钥
        PrivateKey privateKey = getPrivateKey(privateKeyPath);

        //私钥签名对象
        PrivateKeySigner privateKeySigner = new PrivateKeySigner(mchSerialNo, privateKey);

        //身份认证对象
        WechatPay2Credentials wechatPay2Credentials = new WechatPay2Credentials(mchId, privateKeySigner);

        // 使用定时更新的签名验证器,不需要传入证书
        ScheduledUpdateCertificatesVerifier verifier = new ScheduledUpdateCertificatesVerifier(
                wechatPay2Credentials,
                apiV3Key.getBytes(StandardCharsets.UTF_8));

        return verifier;
    }


    /**
     * 获取http请求对象
     * @param verifier
     * @return
     */
    @Bean(name = "wxPayClient")
    public CloseableHttpClient getWxPayClient(ScheduledUpdateCertificatesVerifier verifier){
    
    

        log.info("获取httpClient");

        //获取商户私钥
        PrivateKey privateKey = getPrivateKey(privateKeyPath);

        WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create()
                .withMerchant(mchId, mchSerialNo, privateKey)
                .withValidator(new WechatPay2Validator(verifier));
        // ... 接下来,你仍然可以通过builder设置各种参数,来配置你的HttpClient

        // 通过WechatPayHttpClientBuilder构造的HttpClient,会自动的处理签名和验签,并进行证书自动更新
        CloseableHttpClient httpClient = builder.build();

        return httpClient;
    }

4.4 基础支付APIv3-API字典和相关工具

由于
在这里插入图片描述

所以我们引入处理json的工具

        <!--json处理器-->
        <dependency>
            <groupId>com.google.code.gson</groupId>
            <artifactId>gson</artifactId>
        </dependency>

接着配置一些enum枚举
方便开发

在这里插入图片描述

4.5 基础支付APIv3-Native支付流程

在这里插入图片描述

4.6 基础支付APIv3-Native返回二维码链接

完成了支付流程的1-5步骤

1 Controller

    /**
     * Native下单
     * @param productId
     * @return
     * @throws Exception
     */
    @ApiOperation("调用统一下单API,生成支付二维码")
    @PostMapping("/native/{productId}")
    public R nativePay(@PathVariable Long productId) throws Exception {
    
    

        log.info("发起支付请求 v3");

        //返回支付二维码连接和订单号
        //接受前端发来的productid
        //返回code_url
        Map<String, Object> map = wxPayService.nativePay(productId);

        return R.ok().setData(map);
    }

2 service

Map<String, Object> nativePay(Long productId) throws Exception;

3 serviceimpl

/**
     * 创建订单,调用Native支付接口
     * @param productId
     * @return code_url 和 订单号
     * @throws Exception
     */
    @Transactional(rollbackFor = Exception.class)
    @Override
    public Map<String, Object> nativePay(Long productId) throws Exception {
    
    

        log.info("生成订单");

        //生成订单
        OrderInfo orderInfo = new OrderInfo();
        orderInfo.setTitle("test");
        orderInfo.setOrderNo(OrderNoUtils.getOrderNo());
        orderInfo.setProductId(productId);
        orderInfo.setTotalFee(1);
        orderInfo.setOrderStatus(OrderStatus.NOTPAY.getType());


        //TODO: 把生成的订单对象存入数据库


        log.info("调用统一下单API");

        //调用统一下单api
        HttpPost httpPost = new HttpPost(wxPayConfig.getDomain().concat(WxApiType.NATIVE_PAY.getType()));

        // 请求body参数

        Gson gson = new Gson();
        Map paramsMap = new HashMap();
        paramsMap.put("appid", wxPayConfig.getAppid());
        paramsMap.put("mchid", wxPayConfig.getMchId());
        paramsMap.put("description", orderInfo.getTitle());
        paramsMap.put("out_trade_no", orderInfo.getOrderNo());
        paramsMap.put("notify_url", wxPayConfig.getNotifyDomain().concat(WxNotifyType.NATIVE_NOTIFY.getType()));

        Map amountMap = new HashMap();
        amountMap.put("total", orderInfo.getTotalFee());
        amountMap.put("currency", "CNY");

        paramsMap.put("amount", amountMap);

        //将参数转换成json字符串
        String jsonParams = gson.toJson(paramsMap);

        log.info("请求参数 ===> {}" + jsonParams);


        StringEntity entity = new StringEntity(jsonParams,"utf-8");
        entity.setContentType("application/json");
        //把处理好的实体类放到post请求对象中
        httpPost.setEntity(entity);
        httpPost.setHeader("Accept", "application/json");

        //完成签名并执行请求
        CloseableHttpResponse response = wxPayClient.execute(httpPost);

        try {
    
    
            String bodyAsString = EntityUtils.toString(response.getEntity());//响应体
            int statusCode = response.getStatusLine().getStatusCode();//响应状态码
            if (statusCode == 200) {
    
     //处理成功
                log.info("成功, 返回结果 = " + bodyAsString);
            } else if (statusCode == 204) {
    
     //处理成功,无返回Body
                log.info("成功");
            } else {
    
    
                log.info("Native下单失败,响应码 = " + statusCode+ ",返回结果 = " + bodyAsString);
                throw new IOException("request failed");
            }

            //响应结果
            Map<String, String> resultMap = gson.fromJson(bodyAsString, HashMap.class);
            //二维码
            String codeUrl = resultMap.get("code_url");

            //保存二维码
//            String orderNo = orderInfo.getOrderNSo();
//            orderInfoService.saveCodeUrl(orderNo, codeUrl);

            //返回二维码
            Map<String, Object> map = new HashMap<>();
            map.put("codeUrl", codeUrl);
            map.put("orderNo", orderInfo.getOrderNo());

            return map;

        } finally {
    
    
            response.close();
        }

    }

效果
在这里插入图片描述

4.7 基础支付APIv3-签名原理实现流程分析

在这里插入图片描述
在这里插入图片描述

4.8 基础支付Apiv3生成订单-存入数据库

        //调用方法生成订单对象(这个方法中会将生成的订单对象加入数据库)
        OrderInfo orderInfo = orderInfoService.createOrderByProductId(productId);
        
//        OrderInfo orderInfo = new OrderInfo();
//        orderInfo.setTitle("test");
//        orderInfo.setOrderNo(OrderNoUtils.getOrderNo());
//        orderInfo.setProductId(productId);
//        orderInfo.setTotalFee(1);
//        orderInfo.setOrderStatus(OrderStatus.NOTPAY.getType());

Service层

package com.atguigu.paymentdemo.service;

import com.atguigu.paymentdemo.entity.OrderInfo;
import com.baomidou.mybatisplus.extension.service.IService;

public interface OrderInfoService extends IService<OrderInfo> {
    
    
    OrderInfo createOrderByProductId(Long productId);
}

ServiceImpl层

@Service
public class OrderInfoServiceImpl extends ServiceImpl<OrderInfoMapper, OrderInfo> implements OrderInfoService {
    
    

    @Autowired
    ProductMapper productMapper;

    @Override
    public OrderInfo createOrderByProductId(Long productId) {
    
    
        //获取商品信息
        Product product = productMapper.selectById(productId);

        //生成订单
        OrderInfo orderInfo = new OrderInfo();
        orderInfo.setTitle(product.getTitle());
        orderInfo.setOrderNo(OrderNoUtils.getOrderNo()); //订单号
        orderInfo.setProductId(productId);
        orderInfo.setTotalFee(product.getPrice()); //分
        orderInfo.setOrderStatus(OrderStatus.NOTPAY.getType());

        //把订单对象添加到数据库中(使用了mybaits-plus)
        baseMapper.insert(orderInfo);

        return orderInfo;
    }
}

4.9 基础支付APy3i–生成订单–获取已存在订单(结合项目实际使用 非必要)

如果按照之前那样写
用户无论是否支付在数据库中都会创建新的订单对象
我们对其进行优化

@Override
    public OrderInfo createOrderByProductId(Long productId) {
    
    

        //查找已存在但未支付的订单(没有验证userid 正式记得验证)
        //有这样的订单直接返回这个orderinfo
        //不再创建新的订单对象 节省数据库空间
        OrderInfo orderInfo = this.getNoPayOrderByProductId(productId);
        if( orderInfo != null){
    
    
            return orderInfo;
        }

        //获取商品信息
        Product product = productMapper.selectById(productId);

        //生成订单
        orderInfo = new OrderInfo();
        orderInfo.setTitle(product.getTitle());
        orderInfo.setOrderNo(OrderNoUtils.getOrderNo()); //订单号
        orderInfo.setProductId(productId);
        orderInfo.setTotalFee(product.getPrice()); //分
        orderInfo.setOrderStatus(OrderStatus.NOTPAY.getType());

        //把订单对象添加到数据库中(使用了mybaits-plus)
        baseMapper.insert(orderInfo);

        return orderInfo;
    }

    /**
     * 根据商品id查询未支付订单
     * 防止重复创建订单对象
     * @param productId
     * @return
     */
    private OrderInfo getNoPayOrderByProductId(Long productId) {
    
    

        //生成查询OrderInfo的queryWrapper
        QueryWrapper<OrderInfo> queryWrapper = new QueryWrapper<>();
        //组装queryWrapper
        queryWrapper.eq("product_id", productId);
        queryWrapper.eq("order_status", OrderStatus.NOTPAY.getType());
        //正式项目记得验证user_id
//        queryWrapper.eq("user_id", userId);

        //使用selectone查询是否有这样的订单对象
        OrderInfo orderInfo = baseMapper.selectOne(queryWrapper);
        return orderInfo;
    }

4.10 基础支付ARV3生成讶单-存储二维码地址(结合项目实际使用 非必要)

1 新建一个service

public interface OrderInfoService extends IService<OrderInfo> {
    
    
    //根据产品id创建订单
    OrderInfo createOrderByProductId(Long productId);

    //保存订单的二维码
    void saveCodeUrl(String orderNo, String codeUrl);
}

2 对应的serviceimpl

/**
     * 存储订单二维码
     * @param orderNo
     * @param codeUrl
     */
    @Override
    public void saveCodeUrl(String orderNo, String codeUrl) {
    
    

        //建立一个queryWrapper 方便后续查询
        QueryWrapper<OrderInfo> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("order_no", orderNo);

        //创建一个orderInfo对象  并设置其二维码url
        OrderInfo orderInfo = new OrderInfo();
        orderInfo.setCodeUrl(codeUrl);

        //mybatis-plus  根据订单号更新状态
        baseMapper.update(orderInfo, queryWrapper);
    }

3 payseviceimpl中对应使用 实现保存二维码

           //响应结果
            Map<String, String> resultMap = gson.fromJson(bodyAsString, HashMap.class);
            //二维码
            codeUrl = resultMap.get("code_url");

            //保存二维码
            String orderNo = orderInfo.getOrderNo();
            orderInfoService.saveCodeUrl(orderNo, codeUrl);

4 在 开始时候 校验是否有code_url 有的话直接返回这个结果
不再调用api

public Map<String, Object> nativePay(Long productId) throws Exception {
    
    

        log.info("生成订单");

        //调用方法生成订单对象(这个方法中会将生成的订单对象加入数据库)
        OrderInfo orderInfo = orderInfoService.createOrderByProductId(productId);

        //如果返回的订单对象中有code_url 直接返回结果 非必须代码
        String codeUrl = orderInfo.getCodeUrl();
        if(orderInfo != null && !StringUtils.isEmpty(codeUrl)){
    
    
            log.info("订单已存在,二维码已保存");
            //返回二维码
            Map<String, Object> map = new HashMap<>();
            map.put("codeUrl", codeUrl);
            map.put("orderNo", orderInfo.getOrderNo());
            return map;
        }

4.11 基础支付APlv3-生成订单-显示订单

1 controller

@CrossOrigin //开放前端的跨域访问
@Api(tags = "商品订单管理")
@RestController
@RequestMapping("/api/order-info")
public class OrderInfoController {
    
    

    @Resource
    private OrderInfoService orderInfoService;

    @ApiOperation("订单列表")
    @GetMapping("/list")
    public R list(){
    
    

        List<OrderInfo> list = orderInfoService.listOrderByCreateTimeDesc();
        return R.ok().data("list", list);
    }




}

2 service

public interface OrderInfoService extends IService<OrderInfo> {
    
    
    //根据产品id创建订单
    OrderInfo createOrderByProductId(Long productId);

    //保存订单的二维码
    void saveCodeUrl(String orderNo, String codeUrl);

    //根据倒叙创建事件查询订单对象
    List<OrderInfo> listOrderByCreateTimeDesc();
}

3 serviceimpl

    /**
     * 查询订单列表,并倒序查询
     * @return
     */
    @Override
    public List<OrderInfo> listOrderByCreateTimeDesc() {
    
    

        //创建一个倒叙时间查询所有订单对象
        QueryWrapper<OrderInfo> queryWrapper = new QueryWrapper<OrderInfo>().orderByDesc("create_time");

        //调用Mapper层查询
        return baseMapper.selectList(queryWrapper);
    }

4.12 基础支付APlv3-实现内网穿透

可以在网上搜索cpolar 免费实现内网穿透

4.13 基础支付APIv3-支付通知-接收通知和返回应答

官方文档
https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_5.shtml
在这里插入图片描述
1 在配置文件中配置好我们的外网服务器ip地址 方便后续支付成功后微信服务器的访问

在这里插入图片描述

2 我们之前在基础下单接口中的请求参数 notify_url中设置了回调地址
微信服务器在完成支付交易之后会访问这个地址给我们发送请求
在这里插入图片描述
3 编写controller接口
(签名的验证 处理订单 超时等在后面做处理)

/**
     * 支付通知
     * 微信支付通过支付通知接口将用户支付成功消息通知给商户
     */
    @ApiOperation("支付通知")
    @PostMapping("/native/notify")
    public String nativeNotify(HttpServletRequest request, HttpServletResponse response){
    
    

        Gson gson = new Gson();

        //创建响应应答对象
        Map<String, String> map = new HashMap<>();

        try {
    
    

            //处理通知参数  把接收到的request处理成String
            String body = HttpUtils.readData(request);
            //调用工具类把String 转化为 hashmap
            Map<String, Object> bodyMap = gson.fromJson(body, HashMap.class);

            String requestId = (String)bodyMap.get("id");
            log.info("支付通知的id ===> {}", requestId);
            log.info("支付通知的完整数据 ===> {}", body);
            //int a = 9 / 0;

            //签名的验证
//            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("通知验签成功");

            //处理订单
//            wxPayService.processOrder(bodyMap);

            //应答超时
            //模拟接收微信端的重复通知
//            TimeUnit.SECONDS.sleep(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);
        }

    }

4 当支付成功后微信访问了我们提供的接口
这里使用了日志打印
在这里插入图片描述

4.14 基础支付APIv3-支付通知-应答异常和应答超时

对后台通知交互时,如果微信收到商户的应答不符合规范或超时,微信认为通知失败,微信会通过一定的策略定期重新发起通知,尽可能提高通知的成功率,但微信不保证通知最终能成功。(通知频率为15s/15s/30s/3m/10m/20m/30m/30m/30m/60m/3h/3h/3h/6h/6h - 总计 24h4m)

4.15 基础支付API3—支付通知–验签

之前的基础通知我们针对response进行验签
现在我们针对request进行验签

1 自定义请求验签器
在这里插入图片描述

package com.atguigu.paymentdemo.util;


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) : "";
   }

}

2 调用请求验签器验证

在支付通知接口中进行验证
在这里插入图片描述

//签名的验证
            //调用我们自定义的请求延签器 实现签名的验证 验证Request消息的真实性
            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("通知验签成功");

4.16 基础支付APlv3-支付通知-报文解密

在验签成功后我们可以对密文的信息进行解密
在这里插入图片描述

在这里插入图片描述
1 调用service层方法

            }
            log.info("通知验签成功");

            //处理订单(调用service层方法 传递接收到的body)
            wxPayService.processOrder(bodyMap);

2 service层

public interface WxPayService {
    
    

    Map<String, Object> nativePay(Long productId) throws Exception;

    void processOrder(Map<String, Object> bodyMap) throws GeneralSecurityException;

3 impl层


@Transactional(rollbackFor = Exception.class)
    @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");


    }
/**
     * 对称解密
     * @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;
    }

在这里插入图片描述

4.17 基础支付Apaly3m支付通知–更新订单状态–记录支付日志

当我们解密回调报文后
我们对订单状态更新
并且记录支付日志
在这里插入图片描述
1 更改订单状态

//更新订单状态
        orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.SUCCESS);

Service

public interface OrderInfoService extends IService<OrderInfo> {
    
    
    //根据产品id创建订单
    OrderInfo createOrderByProductId(Long productId);

    //保存订单的二维码
    void saveCodeUrl(String orderNo, String codeUrl);

    //根据倒叙创建事件查询订单对象
    List<OrderInfo> listOrderByCreateTimeDesc();

    void updateStatusByOrderNo(String orderNo, OrderStatus success);
}

serviceimpl


    /**
     * 根据订单号更新订单状态
     * @param orderNo
     * @param orderStatus
     */
    @Override
    public void updateStatusByOrderNo(String orderNo, OrderStatus orderStatus) {
    
    

        log.info("更新订单状态 ===> {}", orderStatus.getType());

        //创建OrderInfo的queryWrapper
        QueryWrapper<OrderInfo> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("order_no", orderNo);

        //构造更新对象 准备更改订单状态
        OrderInfo orderInfo = new OrderInfo();
        orderInfo.setOrderStatus(orderStatus.getType());

        //执行更新操作
        baseMapper.update(orderInfo, queryWrapper);
    }

2 记录支付日志

//记录支付日志
        paymentInfoService.createPaymentInfo(plainText);

service

public interface PaymentInfoService {
    
    

    void createPaymentInfo(String plainText);
}

serviceimpl

    /**
     * 记录支付日志
     * @param plainText
     */
    @Override
    public void createPaymentInfo(String plainText) {
    
    

        log.info("记录支付日志");

        Gson gson = new Gson();
        HashMap 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);
    }

4.18 基础支付APlv3-支付通知-处理重复通知和接口调用的幂等性

在这里插入图片描述
如果因为网络波动等问题
我们没有及时给微信的回调请求发送结果
微信会重复给我们发送回调通知
这时我们必须正确的处理重复的通知
在这里插入图片描述
在这里插入图片描述

        //处理重复的通知
        //接口调用的幂等性:无论接口被调用多少次,产生的结果是一致的。
        String orderStatus = orderInfoService.getOrderStatus(orderNo);
        //如果订单的状态不是未支付(即订单的状态已经被处理 代表之前已经处理过 直接return不再处理)
        if(!OrderStatus.NOTPAY.getType().equals(orderStatus)){
    
    
            return;
        }
    /**
     * 根据订单号获取订单状态
     * @param orderNo
     * @return
     */
    @Override
    public String getOrderStatus(String orderNo) {
    
    

        QueryWrapper<OrderInfo> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("order_no", orderNo);
        OrderInfo orderInfo = baseMapper.selectOne(queryWrapper);
        //如果没查到数据 返回null 防止空指针异常
        if(orderInfo == null){
    
    
            return null;
        }
        return orderInfo.getOrderStatus();
    }

4.19 基础支付APIv3-支付通知-数据锁

要是用数据锁进行并发控制
在这里插入图片描述

如果两个请求并发来到处理重复通知的这段代码
则会有并发性的问题
更新订单状态和记录支付日志
都会执行两遍

我们使用锁进行优化

    //锁
    private final ReentrantLock lock = new ReentrantLock();


       /*在对业务数据进行状态检查和处理之前,
        要采用数据锁进行并发控制,
        以避免函数重入造成的数据混乱*/
        //尝试获取锁:
        // 成功获取则立即返回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();
            }
        }

这样确保无论重复通知发送多少次
能够正确的处理重复通知
更新订单状态和记录支付日志只执行一遍

4.20 基础支付AP3-关闭订单(调用关单接口)-用户取消订单

1 controller

    /**
     * 用户取消订单
     * @param orderNo
     * @return
     * @throws Exception
     */
    @ApiOperation("用户取消订单")
    @PostMapping("/cancel/{orderNo}")
    public R cancel(@PathVariable String orderNo) throws Exception {
    
    

        log.info("取消订单");

        //调用关单service
        wxPayService.cancelOrder(orderNo);
        return R.ok().setMessage("订单已取消");
    }

2 service

void cancelOrder(String orderNo) throws Exception;

3 serviceimpl

    /**
     * 用户取消订单
     * @param orderNo
     */
    @Override
    public void cancelOrder(String orderNo) throws Exception {
    
    

        //调用微信支付的关单接口
        this.closeOrder(orderNo);

        //更新商户端的订单状态
        orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.CANCEL);
    }


    /**
     * 关单接口的调用
     * @param orderNo
     */
    private void closeOrder(String orderNo) throws Exception {
    
    

        log.info("关单接口的调用,订单号 ===> {}", orderNo);

        //创建远程请求对象
        String url = String.format(WxApiType.CLOSE_ORDER_BY_NO.getType(), orderNo);
        url = wxPayConfig.getDomain().concat(url);
        HttpPost httpPost = new HttpPost(url);

        //组装json请求体
        Gson gson = new Gson();
        Map<String, String> paramsMap = new HashMap<>();
        paramsMap.put("mchid", wxPayConfig.getMchId());
        String jsonParams = gson.toJson(paramsMap);
        log.info("请求参数 ===> {}", jsonParams);

        //将请求参数设置到请求对象中
        StringEntity entity = new StringEntity(jsonParams,"utf-8");
        entity.setContentType("application/json");
        httpPost.setEntity(entity);
        httpPost.setHeader("Accept", "application/json");

        //完成签名并执行请求
        CloseableHttpResponse response = wxPayClient.execute(httpPost);

        try {
    
    
            int statusCode = response.getStatusLine().getStatusCode();//响应状态码
            if (statusCode == 200) {
    
     //处理成功
                log.info("成功200");
            } else if (statusCode == 204) {
    
     //处理成功,无返回Body
                log.info("成功204");
            } else {
    
    
                log.info("Native下单失败,响应码 = " + statusCode);
                throw new IOException("request failed");
            }

        } finally {
    
    
            response.close();
        }
    }

4.21 基础支付APlv3-查询订单API-微信支付查询订单

在这里插入图片描述
在这里插入图片描述
如果我们没有接收到通知的商户支付结果
我们应该主动调用查询订单api
查询订单状态
1 controller

    /**
     * 查询订单
     * @param orderNo
     * @return
     * @throws Exception
     */
    @ApiOperation("查询订单:测试订单状态用")
    @GetMapping("/query/{orderNo}")
    public R queryOrder(@PathVariable String orderNo) throws Exception {
    
    

        log.info("查询订单");

        String result = wxPayService.queryOrder(orderNo);
        return R.ok().setMessage("查询成功").data("result", result);

    }

2 service

String queryOrder(String orderNo) throws Exception;

3 serviceimpl

@Override
    public String queryOrder(String orderNo) throws Exception {
    
    

        log.info("查单接口调用 ===> {}", orderNo);

        String url = String.format(WxApiType.ORDER_QUERY_BY_NO.getType(), orderNo);
        url = wxPayConfig.getDomain().concat(url).concat("?mchid=").concat(wxPayConfig.getMchId());

        HttpGet httpGet = new HttpGet(url);
        httpGet.setHeader("Accept", "application/json");

        //完成签名并执行请求
        CloseableHttpResponse response = wxPayClient.execute(httpGet);

        try {
    
    
            String bodyAsString = EntityUtils.toString(response.getEntity());//响应体
            int statusCode = response.getStatusLine().getStatusCode();//响应状态码
            if (statusCode == 200) {
    
     //处理成功
                log.info("成功, 返回结果 = " + bodyAsString);
            } else if (statusCode == 204) {
    
     //处理成功,无返回Body
                log.info("成功");
            } else {
    
    
                log.info("查单接口调用,响应码 = " + statusCode+ ",返回结果 = " + bodyAsString);
                throw new IOException("request failed");
            }

            return bodyAsString;

        } finally {
    
    
            response.close();
        }

    }

在这里插入图片描述

4.22 基础支付APIv3-查询订单API-引入定时任务

1 首先在启动类中

@SpringBootApplication
//引入Spring Task 执行定时任务
@EnableScheduling
public class PaymentDemoApplication {
    
    

    public static void main(String[] args) {
    
    
        SpringApplication.run(PaymentDemoApplication.class, args);
    }

}

2 进行测试

@Slf4j
@Component
public class WxPayTask {
    
    
/**
     * 秒 分 时 日 月 周
     * 以秒为例
     * *:每秒都执行
     * 1-3:从第1秒开始执行,到第3秒结束执行
     * 0/3:从第0秒开始,每隔3秒执行1次
     * 1,2,3:在指定的第1、2、3秒执行
     * ?:不指定
     * 日和周不能同时制定,指定其中之一,则另一个设置为?
     */
    @Scheduled(cron = "0/3 * * * * ?")
    public void task1(){
    
    
        log.info("task1 被执行......");
     }
    }

在这里插入图片描述

4.23 基础支付APl3-查询订单APi–定时查找超时订单

1 controller

    /**
     * 从第0秒开始每隔30秒执行1次,查询创建超过5分钟,并且未支付的订单
     */
    @Scheduled(cron = "0/30 * * * * ?")
    public void orderConfirm() throws Exception {
    
    
        log.info("orderConfirm 被执行......");

        List<OrderInfo> orderInfoList = orderInfoService.getNoPayOrderByDuration(5);



        for (OrderInfo orderInfo : orderInfoList) {
    
    
            String orderNo = orderInfo.getOrderNo();
            log.warn("超时订单 ===> {}", orderNo);

            //核实订单状态:调用微信支付查单接口
//            wxPayService.checkOrderStatus(orderNo);
        }
    }

2 service

//获取在一定时间间隔内没有支付的订单列表
    List<OrderInfo> getNoPayOrderByDuration(int minutes);

3 serviceimpl

    /**
     * 查询创建超过minutes分钟并且未支付的订单
     * @param minutes
     * @return
     */
    @Override
    public List<OrderInfo> getNoPayOrderByDuration(int minutes) {
    
    
        Instant now = Instant.now();

        log.info("instant.now产生的时间是====》{}",now);

        Instant instant = Instant.now().minus(Duration.ofMinutes(minutes));

        log.info("instant.now减去传来的时间后结果是===》{}",instant);

        //创建一个queryWrapper
        QueryWrapper<OrderInfo> queryWrapper = new QueryWrapper<>();

        //设置查询条件为未支付
        queryWrapper.eq("order_status", OrderStatus.NOTPAY.getType());

        //设置查询条件 小于等于instant
        queryWrapper.le("create_time", instant);

        //查询list
        List<OrderInfo> orderInfoList = baseMapper.selectList(queryWrapper);

        return orderInfoList;
    }

在这里插入图片描述

4.24 其础支付APIv3-查询订单APi-处理超时订单

我们首先模拟没有接收到回调的情况
将自己的配置文件中接收结果通知地址进行更改

这样我们就无法接收回调了

1 controller
在这里插入图片描述

    /**
     * 从第0秒开始每隔30秒执行1次,查询创建超过5分钟,并且未支付的订单
     */
    @Scheduled(cron = "0/30 * * * * ?")
    public void orderConfirm() throws Exception {
    
    
        log.info("orderConfirm 被执行......");

        //查询创建超过1分钟,并且未支付的订单
        List<OrderInfo> orderInfoList = orderInfoService.getNoPayOrderByDuration(1);


        //对查询出来的这些未支付订单进行核查
        for (OrderInfo orderInfo : orderInfoList) {
    
    
            String orderNo = orderInfo.getOrderNo();
            log.warn("超时订单 ===> {}", orderNo);

            //核实订单状态:调用微信支付查单接口
            wxPayService.checkOrderStatus(orderNo);
        }
    }

2 service

    //检查订单状态 (确定到底是否支付钱)
    void checkOrderStatus(String orderNo) throws Exception;

3 serviceimpl

    /**
     * 根据订单号查询微信支付查单接口,核实订单状态
     * 如果订单已支付,则更新商户端订单状态,并记录支付日志
     * 如果订单未支付,则调用关单接口关闭订单,并更新商户端订单状态
     * @param orderNo
     */
    @Transactional(rollbackFor = Exception.class)
    @Override
    public void checkOrderStatus(String orderNo) throws Exception {
    
    

        log.warn("根据订单号核实订单状态 ===> {}", orderNo);

        //调用微信支付查单接口  获取调用查单接口后返回的结果
        String result = this.queryOrder(orderNo);

        //把String类型的返回结果通过Gson处理为HashMap
        Gson gson = new Gson();
        Map<String, String> resultMap = gson.fromJson(result, HashMap.class);

        //从hashmap中获取微信支付端的订单状态
        String tradeState = resultMap.get("trade_state");

        //判断订单状态
        //1 如果支付状态成功的话 更新本地订单状态并且记录支付日志
        if(WxTradeState.SUCCESS.getType().equals(tradeState)){
    
    

            log.warn("核实订单已支付 ===> {}", orderNo);

            //如果确认订单已支付则更新本地订单状态
            orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.SUCCESS);
            //记录支付日志(调用查单接口返回的数据和 回调数据中的密文解密后的数据一致)
            paymentInfoService.createPaymentInfo(result);
        }

        //2 如果订单未支付的话 调用关单接口 并更新本地订单状态
        if(WxTradeState.NOTPAY.getType().equals(tradeState)){
    
    
            log.warn("核实订单未支付 ===> {}", orderNo);

            //如果订单未支付,则调用关单接口
            this.closeOrder(orderNo);

            //更新本地订单状态
            orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.CLOSED);
        }

    }

在这里插入图片描述

4.25 基础支付APIv3-申请退款APl

1 整体逻辑

1 controller

    @ApiOperation("申请退款")
    @PostMapping("/refunds/{orderNo}/{reason}")
    public R refunds(@PathVariable String orderNo, @PathVariable String reason) throws Exception {
    
    

        log.info("申请退款");
        wxPayService.refund(orderNo, reason);
        return R.ok();
    }

2 service

void refund(String orderNo, String reason) throws Exception;

3 serviceimpl

   /**
     * 退款
     * @param orderNo
     * @param reason
     * @throws IOException
     */
    @Transactional(rollbackFor = Exception.class)
    @Override
    public void refund(String orderNo, String reason) throws Exception {
    
    

        log.info("创建退款单记录");
        //根据订单编号创建退款单
        RefundInfo refundsInfo = refundsInfoService.createRefundByOrderNo(orderNo, reason);

        log.info("调用退款API");

        //调用统一下单API
        //1 构造url 创建HttpPost对象
        String url = wxPayConfig.getDomain().concat(WxApiType.DOMESTIC_REFUNDS.getType());
        HttpPost httpPost = new HttpPost(url);

        // 2
        // 请求body参数
        Gson gson = new Gson();
        Map paramsMap = new HashMap();
        paramsMap.put("out_trade_no", orderNo);//订单编号
        paramsMap.put("out_refund_no", refundsInfo.getRefundNo());//退款单编号
        paramsMap.put("reason",reason);//退款原因
        paramsMap.put("notify_url", wxPayConfig.getNotifyDomain().concat(WxNotifyType.REFUND_NOTIFY.getType()));//退款通知地址

        Map amountMap = new HashMap();
        amountMap.put("refund", refundsInfo.getRefund());//退款金额
        amountMap.put("total", refundsInfo.getTotalFee());//原订单金额
        amountMap.put("currency", "CNY");//退款币种
        paramsMap.put("amount", amountMap);

        //将参数转换成json字符串
        String jsonParams = gson.toJson(paramsMap);
        log.info("请求参数 ===> {}" + jsonParams);


        //3 将请求参数设置到httpPost中并发送
        StringEntity entity = new StringEntity(jsonParams,"utf-8");
        entity.setContentType("application/json");//设置请求报文格式
        httpPost.setEntity(entity);//将请求报文放入请求对象
        httpPost.setHeader("Accept", "application/json");//设置响应报文格式

        //完成签名并执行请求,并完成验签
        CloseableHttpResponse response = wxPayClient.execute(httpPost);

        try {
    
    

            //1 解析响应结果
            String bodyAsString = EntityUtils.toString(response.getEntity());
            //获取响应code
            int statusCode = response.getStatusLine().getStatusCode();
            if (statusCode == 200) {
    
    
                log.info("成功, 退款返回结果 = " + bodyAsString);
            } else if (statusCode == 204) {
    
    
                log.info("成功");
            } else {
    
    
                throw new RuntimeException("退款异常, 响应码 = " + statusCode+ ", 退款返回结果 = " + bodyAsString);
            }

            //2 更新状态

            //更新订单状态
            orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.REFUND_PROCESSING);

            //更新退款单 (传递的是处理过后的String 接口响应结果)
            refundsInfoService.updateRefund(bodyAsString);

        } finally {
    
    
            response.close();
        }
    }

2 createRefundByOrderNo逻辑

2 service

//管理退款的service层
public interface RefundInfoService extends IService<RefundInfo> {
    
    

    //创建退款对象
    RefundInfo createRefundByOrderNo(String orderNo, String reason);

    //更新退款  (传递过来的是调用退款接口后返回的数据)
    void updateRefund(String content);
}

3 serviceimpl

/**
     * 根据订单号创建退款订单
     * @param orderNo
     * @return
     */
    @Override
    public RefundInfo createRefundByOrderNo(String orderNo, String reason) {
    
    

        //根据订单号获取订单对象信息
        OrderInfo orderInfo = orderInfoService.getOrderByOrderNo(orderNo);

        //根据获取的订单对象生成退款订单对象
        RefundInfo refundInfo = new RefundInfo();
        refundInfo.setOrderNo(orderNo);//订单编号
        refundInfo.setRefundNo(OrderNoUtils.getRefundNo());//退款单编号
        refundInfo.setTotalFee(orderInfo.getTotalFee());//原订单金额(分)
        refundInfo.setRefund(orderInfo.getTotalFee());//退款金额(分)
        refundInfo.setReason(reason);//退款原因

        //保存退款订单对象到数据库
        baseMapper.insert(refundInfo);

        return refundInfo;
    }

3 refundsInfoService.updateRefund(bodyAsString); 逻辑

2 service

//管理退款的service层
public interface RefundInfoService extends IService<RefundInfo> {
    
    

    //创建退款对象
    RefundInfo createRefundByOrderNo(String orderNo, String reason);

    //更新退款  (传递过来的是调用退款接口后返回的数据)
    void updateRefund(String content);
}

3 serviceimpl

    /**
     * 记录退款记录
     * @param content
     */
    //传递来的是调用退款接口后返回的结果
    @Override
    public void updateRefund(String content) {
    
    

        //1 将json字符串转换成Map
        Gson gson = new Gson();
        Map<String, String> resultMap = gson.fromJson(content, HashMap.class);

        //2 根据退款单编号修改退款单
        QueryWrapper<RefundInfo> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("refund_no", resultMap.get("out_refund_no"));

        //3 设置要修改的字段
        //这些字段从 接口返回的结果中获取
        RefundInfo refundInfo = new RefundInfo();

        refundInfo.setRefundId(resultMap.get("refund_id"));//微信支付退款单号

        //查询退款和申请退款中的返回参数
        if(resultMap.get("status") != null){
    
    
            refundInfo.setRefundStatus(resultMap.get("status"));//退款状态
            refundInfo.setContentReturn(content);//将全部响应结果存入数据库的content字段
        }
        //退款回调中的回调参数
        if(resultMap.get("refund_status") != null){
    
    
            refundInfo.setRefundStatus(resultMap.get("refund_status"));//退款状态
            refundInfo.setContentNotify(content);//将全部响应结果存入数据库的content字段
        }

        //更新退款单
        baseMapper.update(refundInfo, queryWrapper);
    }

4.26 基础支付APIv3-查询退款API

1 controller

    /**
     * 查询退款
     * @param refundNo
     * @return
     * @throws Exception
     */
    @ApiOperation("查询退款:测试用")
    @GetMapping("/query-refund/{refundNo}")
    public R queryRefund(@PathVariable String refundNo) throws Exception {
    
    

        log.info("查询退款");

        String result = wxPayService.queryRefund(refundNo);
        return R.ok().setMessage("查询成功").data("result", result);
    }

2 service

    //查询退款
    String queryRefund(String orderNo) throws Exception;

3 serviceimpl

    /**
     * 查询退款接口调用
     * @param refundNo
     * @return
     */
    @Override
    public String queryRefund(String refundNo) throws Exception {
    
    

        log.info("查询退款接口调用 ===> {}", refundNo);

        //1 根据refundNo退款单号 构建url
        String url =  String.format(WxApiType.DOMESTIC_REFUNDS_QUERY.getType(), refundNo);
        url = wxPayConfig.getDomain().concat(url);

        //创建远程Get 请求对象
        HttpGet httpGet = new HttpGet(url);
        httpGet.setHeader("Accept", "application/json");

        //完成签名并执行请求
        CloseableHttpResponse response = wxPayClient.execute(httpGet);

        try {
    
    
            String bodyAsString = EntityUtils.toString(response.getEntity());
            int statusCode = response.getStatusLine().getStatusCode();
            if (statusCode == 200) {
    
    
                log.info("成功, 查询退款返回结果 = " + bodyAsString);
            } else if (statusCode == 204) {
    
    
                log.info("成功");
            } else {
    
    
                throw new RuntimeException("查询退款异常, 响应码 = " + statusCode+ ", 查询退款返回结果 = " + bodyAsString);
            }

            return bodyAsString;

        } finally {
    
    
            response.close();
        }
    }

在Swagger中测试
在这里插入图片描述

4.27 基础支付APIv3-处理退款通知

1 controller

/**
     * 退款结果通知
     * 退款状态改变后,微信会把相关退款结果发送给商户。
     */
    @ApiOperation("退款结果通知")
    @PostMapping("/refunds/notify")
    public String refundsNotify(HttpServletRequest request, HttpServletResponse response){
    
    

        log.info("退款通知执行");
        Gson gson = new Gson();
        Map<String, String> map = new HashMap<>();//应答对象

        try {
    
    

            // 1 处理收到的request
            //处理通知参数
            String body = HttpUtils.readData(request);

            //把收到的数据包从Json变为hashmap
            Map<String, Object> bodyMap = gson.fromJson(body, HashMap.class);
            String requestId = (String)bodyMap.get("id");

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

            //2
            //签名的验证
            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("通知验签成功");


            //3 验签通过后处理退款单
            //处理退款单
            wxPayService.processRefund(bodyMap);

            //成功应答
            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);
        }
    }

2 wxPayService.processRefund(bodyMap)

void processRefund(Map<String, Object> bodyMap) throws Exception;

3 serviceimpl

/**
     * 处理退款单
     */
    @Transactional(rollbackFor = Exception.class)
    @Override
    public void processRefund(Map<String, Object> bodyMap) throws Exception {
    
    

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

        if(lock.tryLock()){
    
    
            try {
    
    

                String orderStatus = orderInfoService.getOrderStatus(orderNo);
                if (!OrderStatus.REFUND_PROCESSING.getType().equals(orderStatus)) {
    
    
                    return;
                }

                //更新订单状态
                orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.REFUND_SUCCESS);

                //更新退款单
                refundsInfoService.updateRefund(plainText);

            } finally {
    
    
                //要主动释放锁
                lock.unlock();
            }
        }
    }

4.28 基础支付APIv3-账单-1获取账单的下载地址

1 controller

    @ApiOperation("获取账单url:测试用")
    @GetMapping("/querybill/{billDate}/{type}")
    public R queryTradeBill(
            @PathVariable String billDate,
            @PathVariable String type) throws Exception {
    
    

        log.info("获取账单url");

        String downloadUrl = wxPayService.queryBill(billDate, type);
        return R.ok().setMessage("获取账单url成功").data("downloadUrl", downloadUrl);
    }

2 service

String queryBill(String billDate, String type) throws Exception;

3 serviceimpl

/**
     * 申请账单
     * @param billDate
     * @param type
     * @return
     * @throws Exception
     */
    @Override
    public String queryBill(String billDate, String type) throws Exception {
    
    
        log.warn("申请账单接口调用 {}", billDate);

        String url = "";
        if("tradebill".equals(type)){
    
    
            url =  WxApiType.TRADE_BILLS.getType();
        }else if("fundflowbill".equals(type)){
    
    
            url =  WxApiType.FUND_FLOW_BILLS.getType();
        }else{
    
    
            throw new RuntimeException("不支持的账单类型");
        }

        url = wxPayConfig.getDomain().concat(url).concat("?bill_date=").concat(billDate);

        //创建远程Get 请求对象
        HttpGet httpGet = new HttpGet(url);
        httpGet.addHeader("Accept", "application/json");

        //使用wxPayClient发送请求得到响应
        CloseableHttpResponse response = wxPayClient.execute(httpGet);

        try {
    
    

            String bodyAsString = EntityUtils.toString(response.getEntity());

            int statusCode = response.getStatusLine().getStatusCode();
            if (statusCode == 200) {
    
    
                log.info("成功, 申请账单返回结果 = " + bodyAsString);
            } else if (statusCode == 204) {
    
    
                log.info("成功");
            } else {
    
    
                throw new RuntimeException("申请账单异常, 响应码 = " + statusCode+ ", 申请账单返回结果 = " + bodyAsString);
            }

            //获取账单下载地址
            Gson gson = new Gson();
            Map<String, String> resultMap = gson.fromJson(bodyAsString, HashMap.class);
            return resultMap.get("download_url");

        } finally {
    
    
            response.close();
        }
    }

在这里插入图片描述

在这里插入图片描述

4.29 基础支付APIv3-账单-2 下载账单

1 controller

@ApiOperation("下载账单")
    @GetMapping("/downloadbill/{billDate}/{type}")
    public R downloadBill(
            @PathVariable String billDate,
            @PathVariable String type) throws Exception {
    
    

        log.info("下载账单");
        String result = wxPayService.downloadBill(billDate, type);


        return R.ok().data("result", result);
    }

2 service

//下载账单
    String downloadBill(String billDate, String type) throws Exception;

3 serviceimpl

/**
     * 下载账单
     * @param billDate
     * @param type
     * @return
     * @throws Exception
     */
    @Override
    public String downloadBill(String billDate, String type) throws Exception {
    
    
        log.warn("下载账单接口调用 {}, {}", billDate, type);

        //获取账单url地址
        String downloadUrl = this.queryBill(billDate, type);
        //创建远程Get 请求对象
        HttpGet httpGet = new HttpGet(downloadUrl);
        httpGet.addHeader("Accept", "application/json");

        //使用wxPayClient发送请求得到响应
        CloseableHttpResponse response = wxPayNoSignClient.execute(httpGet);

        try {
    
    

            String bodyAsString = EntityUtils.toString(response.getEntity());

            int statusCode = response.getStatusLine().getStatusCode();
            if (statusCode == 200) {
    
    
                log.info("成功, 下载账单返回结果 = " + bodyAsString);
            } else if (statusCode == 204) {
    
    
                log.info("成功");
            } else {
    
    
                throw new RuntimeException("下载账单异常, 响应码 = " + statusCode+ ", 下载账单返回结果 = " + bodyAsString);
            }

            return bodyAsString;

        } finally {
    
    
            response.close();
        }
    }

总结

提示:这里对文章进行总结:
例如:以上就是今天要讲的内容,本文仅仅简单介绍了pandas的使用,而pandas提供了大量能使我们快速便捷地处理数据的函数和方法。

猜你喜欢

转载自blog.csdn.net/weixin_51751186/article/details/126568398