SpringBoot整合微信支付

SpringBoot整合微信支付

本篇文章教给大家如何快速上手微信支付,没有太多的理论讲解。希望大家动手敲一遍代码,你会发现基于Native支付的微信对接没你想象的那么困难!

一. 微信支付热身

1.1 微信支付产品介绍
  • 付款码支付:用户展示微信钱包内的“付款码”给商家,商家扫描后直接完成支付,适用于线下面对面收银的场景。
  • JSAPI支付:1.线下场所:商户展示一个支付二维码,用户使用微信扫描二维码后,输入需要支付的金额,完成支付。 2.公众号场景:用户在微信内进入商家公众号,打开某个页面,选择某个产品,完成支付。3.PC网站场景:在网站中展示二维码,用户使用微信扫描二维码,输入需要支付的金额,完成支付。特点:用户在客户端输入支付金额。
  • 小程序支付:在微信小程序平台内实现支付的功能。
  • Native支付:Native支付是指商户展示支付二维码,用户再用微信“扫一扫”完成支付的模式。这种方式适用于PC网站。特点:商家预先指定支付金额。
  • APP支付:商户通过在移动端独立的APP应用程序中集成微信支付模块,完成支付。
  • 刷脸支付:用户在刷脸设备前通过摄像头刷脸、识别身份后进行的一种支付方式。
1.2 Native微信支付参数准备

上面的多种支付产品进行对接时,都需要相同的参数。这些参数是需要我们在微信上申请的,申请商户时,需要公司的营业执照,所以个人是无法申请或得到对接微信支付的参数。这里我给大家一份,大家后面学习时,用我给的这份参数即可(我这个也是网上找的,如果大家是通过公司申请的,请保护好个人隐私,不要对外公开!)

  • mch-id(商户号):1558950191
  • mch-serial-no(商户API证书序列号):34345964330B66427E0D3D28826C4993C77E631F
  • api-v3-key(APIv3密钥):UDuLFDcmy5Eb6o0nTNZdu6ek4DDh4K8B
  • appid(APPID):wx74862e0dfcf69954
  • private-key-path(商户私钥文件):apiclient_key.pem
  • domain(微信服务器地址):https://api.mch.weixin.qq.com
  • notify-domain(接收结果通知地址):这个需要自己本地进行内网穿透,微信回调才能打到本地,下面我会讲解。

注意:这里的apiclient_key.pem是一个文件,请大家下载到自己本地。链接: https://pan.baidu.com/s/1PoMVGLbNX7bXz7PN19Bbcg 提取码: pmma

1.3 内网穿透
  1. 访问ngrok官网:https://ngrok.com/
  2. 注册账号,然后去QQ邮箱验证。
  3. 点击QQ邮箱中链接跳转到ngrok官网
  4. 下载安装包到本地 这里直接下载我的。链接: https://pan.baidu.com/s/1dozNDO5wlvPoxEukQ7LOxQ 提取码: avsr
  5. 下载完成后,直接解压到当前文件夹。打开ngrok.exe,输入复制的命令。

SpringBoot整合微信支付_第1张图片
SpringBoot整合微信支付_第2张图片
SpringBoot整合微信支付_第3张图片

SpringBoot整合微信支付_第4张图片
SpringBoot整合微信支付_第5张图片
SpringBoot整合微信支付_第6张图片

回调接口参数 notify-domain(接收结果通知地址):https://573c-59-174-56-67.ngrok-free.app
注意:这里每个人的都不同,请大家自行配置。在微信支付对接中,该回调地址参数是必填的。你可以填一个错误的,但是订单支付或退款成功,你是无法感知的。所以大家还是要配一个,订单支付或退款成功,微信回调我们的接口,告知我们订单状态,同时,我们可以异步的对订单表进行其他的逻辑操作。比如修改订单状态,统计订单数据等等。

测试配置是否成功:

server:
  port: 9091 #这里的端口号与内网穿透配置的一样
package com.weige.javaskillpoint.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/ngrok")
public class TestController {
    @GetMapping("/test")
    public String test(){
        return "内网穿透成功";
    }
}

启动项目:

在这里插入图片描述

在浏览器上调用接口:
SpringBoot整合微信支付_第7张图片
SpringBoot整合微信支付_第8张图片

1.4 Native订单完整流程

微信官方文档链接:https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_4.shtml
SpringBoot整合微信支付_第9张图片

二. Native微信支付实战

2.1 项目搭建

项目目录:

SpringBoot整合微信支付_第10张图片

pom.xml文件

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.5.6</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.weige</groupId>
    <artifactId>java-skill-point</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>java-skill-point</name>
    <description>java-skill-point</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <!--Springboot项目自带 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <!--Springboot Web项目 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!--lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.22</version>
        </dependency>

        <!-- hutool  -->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.7.20</version>
        </dependency>

        <!--微信支付SDK-->
        <dependency>
            <groupId>com.github.wechatpay-apiv3</groupId>
            <artifactId>wechatpay-apache-httpclient</artifactId>
            <version>0.3.0</version>
        </dependency>

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

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

application.yml

server:
  port: 9091 #这里的端口号与内网穿透配置的一样

wxpay.properties

wxpay.mch-id=1558950191 #商户号

wxpay.mch-serial-no=34345964330B66427E0D3D28826C4993C77E631F #商户API证书序列号

wxpay.private-key-path=apiclient_key.pem #商户私钥文件

wxpay.api-v3-key=UDuLFDcmy5Eb6o0nTNZdu6ek4DDh4K8B #APIv3密钥

wxpay.appid=wx74862e0dfcf69954 #APPID

wxpay.domain=https://api.mch.weixin.qq.com #微信服务器地址

wxpay.notify-domain=https://573c-59-174-56-67.ngrok-free.app/ #接收结果通知地址

apiclient_key.pem

-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDnSAKI8sea8p+d
OBVPWlZmxqJfPbdhzZxdI5Kx1j5SJNZwXWtr43/giw38pwzSlBI+bubBcYlkFTI0
guigMZO/yueb1mZChaY/JG1vsT02Ubj0xkVvBwKNbYS48NEpZhK61Mia09R4n1iH
1vip9kt8J6Zrx+xIqwmuCNWigyivGrvY9AdevCNlNSVdHVOZUJiJ6UGtvVmgZb0u
RTwBzfkjnwTgEcsrZMmF15nFubFsyJLyF/zY4NhrISc8H/rbjgleqa8ybYL26iTS
gfPCXe4U9f8fNFF2bSA06GTiB2R93q2B0zHeUYrpgF4XOGlIAqH+Ea4Vn+aOj6I0
pduh03idAgMBAAECggEBAJ+4SB/hYd1szrPZhkXtwhtp87pIObtuLhzYMzdjGFjM
HdctfMDeNHKSNU+U4bMPFOZO2kcfLF2Ukb5X5WSzuDBMZNRnJOmtuJiEhJsM0JQR
reREhLDfK3EWAAFkNV4corSpu/vIbEP87zuoRsPBVnHgQ/rM7y1kCORKL5bycwcw
5BI4xhULKAu14LEcDL3+xDJo39w+WCFlxuP+6Bs7+vIeavs+AC3TJkA4kg2nyWd3
W07xPjHl64f17icqsFhuFZ+VuSf5CAgQGWDbC7BHqRkDStUDSiiUiFushouKCLdK
MpA0x4ogb2ZwfZDRhZHiLNAGe4QovYCcXWBydzuT0WECgYEA828Bo1JAHE5kdnsO
E9+enH/yMcOKTRnuYPiXsFXNvqofc5tZiXJmVE/+EKv7LFmtUA6qqKC7FDek8TpP
SkfXmSDAgfM6AdzT0YoHH23FRVewnFMEYumtogXsXJTyI5siBSJp16s9Rn/YwESt
JqjW5+9Ck1dkU+UJCZ4lOw4HeGkCgYEA8zho2BKQTh3P/xcFcoTcunVZpRayVkHM
g8Ef6RGGo4vM1oshQLvXyPqCmhAIf6j71I9WPqUwjmeGyaR7Hir0dbgTCm2fJPFW
lxAvgbCISxEPz10RYBcR2umMSlJLfZfhqv1CyfU4vfCTbdOimgsz2039E3oLTbzg
eDe/mdzu2BUCgYEAleKjf4wFLWiXMtxRrqrhXjrpRPrBDPgKbmqh+1DZfawB8YyV
dKublg4qwNkjrgsJS2G8cleE2M3qIR1l9LaHaSFhZqH79WmigkIaYJ+V9zwm4hm7
eaun3TsIbXjIHmRGbiLiSIiHEgFl0/x1IHiU2fnXZCFLBNzg06ssAVCCCQECgYA1
4BfxTONkOlxZgAr33BBcySPLWuS0EK0xvjTIVtaBIbWFDJqYEUPyQ/NsFwMa7B6k
bf/HrqW71ZjYz7Np8k/mR5kIJVIsR71Lhw1O6AC4yBW9dDsmEtYkrLkjuWj5cAxP
6PvDaqtf/4tYt5l8D+Ezwem+R7l7RcxfNNIfTf4mJQKBgE57dnRx+Ijx7VHjJvjl
X2jB/VSVGpK5OADykmmZ/wvHPlQcyzd+5kAIoJhSuY48CFeI1DOogR2p01LEFQEL
j4AI5FqOOQwRJvNmfoKcKwO36tSxSEGSM8POKOsa21PG/gvDpJjVFo2hn5QcMHWn
z5SjsgA/1YbXejubdLxT/3pl
-----END PRIVATE KEY-----

WxPayConfig文件 微信支付对接参数类

package com.weige.javaskillpoint.config;

import com.wechat.pay.contrib.apache.httpclient.WechatPayHttpClientBuilder;
import com.wechat.pay.contrib.apache.httpclient.auth.PrivateKeySigner;
import com.wechat.pay.contrib.apache.httpclient.auth.ScheduledUpdateCertificatesVerifier;
import com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Credentials;
import com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Validator;
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;


    /**
     * 获取商户的私钥文件
     *
     * @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() {

        log.info("无需进行应答签名验证,获取httpClient");


        //获取商户私钥
        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();

        return httpClient;
    }

}

WxPayController文件 订单流程操作类

package com.weige.javaskillpoint.controller;

import com.google.gson.Gson;
import com.weige.javaskillpoint.config.WxPayConfig;
import com.weige.javaskillpoint.enums.wxpay.WxApiType;
import com.weige.javaskillpoint.enums.wxpay.WxNotifyType;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.util.EntityUtils;
import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

@RestController
@RequestMapping("/api/wx-pay")
@Slf4j
public class WxPayController {

    @Resource
    private CloseableHttpClient wxPayClient;

    @Resource
    //无需应答签名
    private CloseableHttpClient wxPayNoSignClient;

    @Resource
    private WxPayConfig wxPayConfig;

    /**
     * Native下单
     */
    @PostMapping("/native")
    public void nativePay() throws Exception {

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

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

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

        // 请求body参数
        Gson gson = new Gson();
        HashMap<String, Object> paramsMap = new HashMap<String, Object>();
        paramsMap.put("appid", wxPayConfig.getAppid());// APPID
        paramsMap.put("mchid", wxPayConfig.getMchId());// 商户id
        paramsMap.put("description", "魏凯的小商铺"); // 订单描述
        paramsMap.put("out_trade_no", "123456789987"); // 订单号
        paramsMap.put("notify_url", wxPayConfig.getNotifyDomain().concat(WxNotifyType.NATIVE_NOTIFY.getType()));// 二维码扫描支付成功后进行回调

        Map<String, Object> amountMap = new HashMap<>();
        amountMap.put("total", 1); // 金额 以分为单位 这里写1分钱 订单号123456789987对应的金额为1分
        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");
        httpPost.setEntity(entity);
        httpPost.setHeader("Accept", "application/json");

        //完成签名并执行请求(这个地方就是mchId与商户证书密钥进行校验)
        try (CloseableHttpResponse response = wxPayClient.execute(httpPost)) {
            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");
            }
            //响应结果
            HashMap resultMap = gson.fromJson(bodyAsString, HashMap.class);
            //二维码
            String codeUrl = (String) resultMap.get("code_url");

            log.info("二维码为: " + codeUrl);

        }
    }

    /**
     * 查询订单
     */
    @GetMapping("/query/{orderNo}")
    public void queryOrder(@PathVariable String orderNo) throws Exception {

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

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

        // 拼接url
        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");

        //完成签名并执行请求
        try (CloseableHttpResponse response = wxPayClient.execute(httpGet)) {
            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");
            }
        }
    }


    /**
     * 用户取消订单
     */
    @PostMapping("/cancel/{orderNo}")
    public void cancel(@PathVariable 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");

        //完成签名并执行请求
        try (CloseableHttpResponse response = wxPayClient.execute(httpPost)) {
            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");
            }
        }
    }

    /**
     * 用户申请退款
     */
    @PostMapping("/refunds/{orderNo}")
    public void refunds(@PathVariable String orderNo) throws Exception {

        log.info("申请退款");
        log.info("调用退款API");
        //调用统一下单API
        String url = wxPayConfig.getDomain().concat(WxApiType.DOMESTIC_REFUNDS.getType());
        HttpPost httpPost = new HttpPost(url);

        // 请求body参数
        Gson gson = new Gson();
        Map<String, Object> paramsMap = new HashMap<>();
        paramsMap.put("out_trade_no", orderNo);//订单编号
        paramsMap.put("out_refund_no", "123456");//退款单编号 随便填
        paramsMap.put("reason", "随便填一个");//退款原因
        paramsMap.put("notify_url", wxPayConfig.getNotifyDomain().concat(WxNotifyType.REFUND_NOTIFY.getType()));//退款成功通知地址

        Map<String, java.io.Serializable> amountMap = new HashMap<String, java.io.Serializable>();
        amountMap.put("refund", 1);//退款金额 这里的金额应该根据订单id查询出来
        amountMap.put("total", 1);//原订单金额 这里的金额应该根据订单id查询出来
        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");//设置请求报文格式
        httpPost.setEntity(entity);//将请求报文放入请求对象
        httpPost.setHeader("Accept", "application/json");//设置响应报文格式

        //完成签名并执行请求,并完成验签
        try (CloseableHttpResponse response = wxPayClient.execute(httpPost)) {
            //解析响应结果
            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);
            }
        }
    }

    /**
     * 查询退款
     */
    @GetMapping("/query-refund/{refundNo}")
    public void queryRefund(@PathVariable String refundNo) throws Exception {

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

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

        //完成签名并执行请求
        try (CloseableHttpResponse response = wxPayClient.execute(httpGet)) {
            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);
            }
        }
    }

    /**
     * 获取账单url 这个url无法在浏览器打开
     */
    @GetMapping("/querybill/{billDate}/{type}")
    public void queryTradeBill(@PathVariable String billDate, @PathVariable String type) throws Exception {

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

        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发送请求得到响应
        try (CloseableHttpResponse response = wxPayClient.execute(httpGet)) {
            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);
            log.info("账单链接;" + resultMap.get("download_url"));
        }
    }

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

        log.info("下载账单");
        log.warn("下载账单接口调用 {}, {}", billDate, type);

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

        //使用wxPayClient发送请求得到响应
        try (CloseableHttpResponse response = wxPayNoSignClient.execute(httpGet)) {
            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);
            }
        }

    }

    /**
     * 申请账单
     */
    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发送请求得到响应
        try (CloseableHttpResponse response = wxPayClient.execute(httpGet)) {
            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");
        }
    }

}

WxPayNotifyController文件 支付成功或者退款成功回调类

package com.weige.javaskillpoint.controller;

import com.google.gson.Gson;
import com.wechat.pay.contrib.apache.httpclient.auth.Verifier;
import com.weige.javaskillpoint.util.HttpUtils;
import com.weige.javaskillpoint.util.WechatPay2ValidatorForRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;

@RestController
@RequestMapping("/api/wx-pay/notify")
@Slf4j
public class WxPayNotifyController {
    @Resource
    private Verifier verifier;

    /**
     * 订单支付成功回调
     */
    @PostMapping("/native")
    public String nativeNotify(HttpServletRequest request, HttpServletResponse response) {

        Gson gson = new Gson();
        Map<String, String> map = new HashMap<>();//应答对象

        try {
            //处理通知参数
            String body = HttpUtils.readData(request);
            Map<String, Object> bodyMap = gson.fromJson(body, HashMap.class);
            String requestId = (String) bodyMap.get("id");
            log.info("支付通知的id ===> {}", requestId);

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

            //处理订单 这里可以对订单进行处理 比如订单支付成功 修改数据库订单表订单状态为已支付
//            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);
        }
    }

    @PostMapping("/refunds")
    public String refundsNotify(HttpServletRequest request, HttpServletResponse response){

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

        try {
            //处理通知参数
            String body = HttpUtils.readData(request);
            Map<String, Object> bodyMap = gson.fromJson(body, HashMap.class);
            String requestId = (String)bodyMap.get("id");
            log.info("支付通知的id ===> {}", requestId);

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

            //处理退款单 订单退款成功 修改订单表中订单状态为已退款
//            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);
        }
    }
}

WxApiType文件 对接微信支付接口后缀

package com.weige.javaskillpoint.enums.wxpay;

import lombok.AllArgsConstructor;
import lombok.Getter;

@AllArgsConstructor
@Getter
public enum WxApiType {

	/**
	 * Native下单
	 */
	NATIVE_PAY("/v3/pay/transactions/native"),

	/**
	 * 查询订单
	 */
	ORDER_QUERY_BY_NO("/v3/pay/transactions/out-trade-no/%s"),

	/**
	 * 关闭订单
	 */
	CLOSE_ORDER_BY_NO("/v3/pay/transactions/out-trade-no/%s/close"),

	/**
	 * 申请退款
	 */
	DOMESTIC_REFUNDS("/v3/refund/domestic/refunds"),

	/**
	 * 查询单笔退款
	 */
	DOMESTIC_REFUNDS_QUERY("/v3/refund/domestic/refunds/%s"),

	/**
	 * 申请交易账单
	 */
	TRADE_BILLS("/v3/bill/tradebill"),

	/**
	 * 申请资金账单
	 */
	FUND_FLOW_BILLS("/v3/bill/fundflowbill");


	/**
	 * 类型
	 */
	private final String type;
}

WxNotifyType文件 接口回调地址后缀

package com.weige.javaskillpoint.enums.wxpay;

import lombok.AllArgsConstructor;
import lombok.Getter;

@AllArgsConstructor
@Getter
public enum WxNotifyType {

	/**
	 * 支付通知
	 */
	NATIVE_NOTIFY("/api/wx-pay/notify/native"),


	/**
	 * 退款结果通知
	 */
	REFUND_NOTIFY("/api/wx-pay/notify/refunds");

	/**
	 * 类型
	 */
	private final String type;
}

HttpUtils文件 工具类(将通知参数转化为字符串)

package com.weige.javaskillpoint.util;

import javax.servlet.http.HttpServletRequest;
import java.io.BufferedReader;
import java.io.IOException;

/**
 * @author weikai
 */
public class HttpUtils {

    /**
     * 将通知参数转化为字符串
     * @param request
     * @return
     */
    public static String readData(HttpServletRequest request) {
        BufferedReader br = null;
        try {
            StringBuilder result = new StringBuilder();
            br = request.getReader();
            for (String line; (line = br.readLine()) != null; ) {
                if (result.length() > 0) {
                    result.append("\n");
                }
                result.append(line);
            }
            return result.toString();
        } catch (IOException e) {
            throw new RuntimeException(e);
        } finally {
            if (br != null) {
                try {
                    br.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

WechatPay2ValidatorForRequest文件 工具类

package com.weige.javaskillpoint.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.*;

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

JavaSkillPointApplication 启动类

package com.weige.javaskillpoint;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class JavaSkillPointApplication {

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

项目搭建完成,接下来讲解订单从创建到结束的整个流程:

2.2 项目初始化(获取签名验证器与HttpClient对象)

要想与微信进行支付对接,首先第一步我们需要获取签名验证器和HttpClient,如果这个成功不了,则无法与微信对接
证书密钥使用说明:https://pay.weixin.qq.com/wiki/doc/apiv3_partner/wechatpay/wechatpay3_0.shtml
我把获取签名验证器的代码写到WxPayConfig类中,这个类在项目启动时,会执行下面几步:

  • @PropertySource("classpath:wxpay.properties") //读取配置文件
  • @ConfigurationProperties(prefix = "wxpay") //读取wxpay节点
  • @Data //使用set方法将wxpay节点中的值填充到当前类的属性中
  • 根据读取配置文件的微信对接参数,获取签名验证器与http请求对象
 /**
     * 获取签名验证器
     *
     * @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;
    }

SpringBoot整合微信支付_第11张图片
微信支付对接参数正确,则微信签名成功:

SpringBoot整合微信支付_第12张图片
微信支付对接参数不正确,则微信签名失败(我们这里将wxpay.mch-id修改为1558950192):

在这里插入图片描述
微信支付对接参数不正确,则微信签名失败(我们这里将wxpay.mch-serial-no修改为34345964330B66427E0D3D28826C4993C77E631F1):

2.3 生成订单收款码(Native支付对接中创建订单是得到收款码 需用户扫码支付)

代码:

package com.weige.javaskillpoint.controller;

import com.google.gson.Gson;
import com.weige.javaskillpoint.config.WxPayConfig;
import com.weige.javaskillpoint.enums.wxpay.WxApiType;
import com.weige.javaskillpoint.enums.wxpay.WxNotifyType;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.util.EntityUtils;
import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

@RestController
@RequestMapping("/api/wx-pay")
@Slf4j
public class WxPayController {

    @Resource
    private CloseableHttpClient wxPayClient; // 对应WxPayConfig类中的@Bean(name = "wxPayClient") 获取http请求对象

    @Resource
    private WxPayConfig wxPayConfig;

    /**
     * Native下单
     */
    @PostMapping("/native")
    public void nativePay() throws Exception {

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

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

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

        // 请求body参数
        Gson gson = new Gson();
        HashMap<String, Object> paramsMap = new HashMap<String, Object>();
        paramsMap.put("appid", wxPayConfig.getAppid());// APPID
        paramsMap.put("mchid", wxPayConfig.getMchId());// 商户id
        paramsMap.put("description", "魏凯的小商铺"); // 订单描述
        paramsMap.put("out_trade_no", "123456789988"); // 订单号
        paramsMap.put("notify_url", wxPayConfig.getNotifyDomain().concat(WxNotifyType.NATIVE_NOTIFY.getType()));// 二维码扫描支付成功后进行回调

        Map<String, Object> amountMap = new HashMap<>();
        amountMap.put("total", 1); // 金额 以分为单位 这里写1分钱 订单号123456789987对应的金额为1分
        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");
        httpPost.setEntity(entity);
        httpPost.setHeader("Accept", "application/json");

        //完成签名并执行请求(这个地方就是mchId与商户证书密钥进行校验)
        try (CloseableHttpResponse response = wxPayClient.execute(httpPost)) {
            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");
            }
            //响应结果
            HashMap resultMap = gson.fromJson(bodyAsString, HashMap.class);
            //二维码
            String codeUrl = (String) resultMap.get("code_url");

            log.info("二维码为: " + codeUrl);

        }
    }   
}

请求微信下单接口:wxPayConfig.getDomain().concat(WxApiType.NATIVE_PAY.getType()) -> https://api.mch.weixin.qq.com/v3/pay/transactions/native
扫码收款码支付成功回调:wxPayConfig.getNotifyDomain().concat(WxNotifyType.NATIVE_NOTIFY.getType()) -> https://573c-59-174-56-67.ngrok-free.app/api/wx-pay/notify/native
订单金额以分为单位,可以前端传入。这里我统一写为1分。请求微信下单接口参数按我代码封装即可。
参数详情可以查看微信文档:https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_1.shtml

SpringBoot整合微信支付_第13张图片
收款码:weixin://wxpay/bizpayurl?pr=Y4vn02Lzz 根据url生成收款码

SpringBoot整合微信支付_第14张图片
扫描图中二维码进行微信付款,付款成功后,微信会回调我们的支付成功接口

SpringBoot整合微信支付_第15张图片
回调成功:

SpringBoot整合微信支付_第16张图片
在这里插入图片描述

2.4 查询订单状态

代码:

package com.weige.javaskillpoint.controller;

import com.google.gson.Gson;
import com.weige.javaskillpoint.config.WxPayConfig;
import com.weige.javaskillpoint.enums.wxpay.WxApiType;
import com.weige.javaskillpoint.enums.wxpay.WxNotifyType;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.util.EntityUtils;
import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

@RestController
@RequestMapping("/api/wx-pay")
@Slf4j
public class WxPayController {

    @Resource
    private CloseableHttpClient wxPayClient;

    @Resource
    //无需应答签名
    private CloseableHttpClient wxPayNoSignClient;

    @Resource
    private WxPayConfig wxPayConfig;

    /**
     * 查询订单
     */
    @GetMapping("/query/{orderNo}")
    public void queryOrder(@PathVariable String orderNo) throws Exception {

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

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

        // 拼接url
        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");

        //完成签名并执行请求
        try (CloseableHttpResponse response = wxPayClient.execute(httpGet)) {
            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");
            }
        }
    }   
}

查询刚刚支付的订单号:123456789988

SpringBoot整合微信支付_第17张图片

2.5 订单退款(这里退款成功会有回调)

代码:

package com.weige.javaskillpoint.controller;

import com.google.gson.Gson;
import com.weige.javaskillpoint.config.WxPayConfig;
import com.weige.javaskillpoint.enums.wxpay.WxApiType;
import com.weige.javaskillpoint.enums.wxpay.WxNotifyType;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.util.EntityUtils;
import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

@RestController
@RequestMapping("/api/wx-pay")
@Slf4j
public class WxPayController {

    @Resource
    private CloseableHttpClient wxPayClient;

    @Resource
    //无需应答签名
    private CloseableHttpClient wxPayNoSignClient;

    @Resource
    private WxPayConfig wxPayConfig;

    @PostMapping("/refunds/{orderNo}")
    public void refunds(@PathVariable String orderNo) throws Exception {

        log.info("申请退款");
        log.info("调用退款API");
        //调用统一下单API
        String url = wxPayConfig.getDomain().concat(WxApiType.DOMESTIC_REFUNDS.getType());
        HttpPost httpPost = new HttpPost(url);

        // 请求body参数
        Gson gson = new Gson();
        Map<String, Object> paramsMap = new HashMap<>();
        paramsMap.put("out_trade_no", orderNo);//订单编号
        paramsMap.put("out_refund_no", "123456");//退款单编号 随便填
        paramsMap.put("reason", "随便填一个");//退款原因
        paramsMap.put("notify_url", wxPayConfig.getNotifyDomain().concat(WxNotifyType.REFUND_NOTIFY.getType()));//退款成功通知地址

        Map<String, java.io.Serializable> amountMap = new HashMap<String, java.io.Serializable>();
        amountMap.put("refund", 1);//退款金额 这里的金额应该根据订单id查询出来
        amountMap.put("total", 1);//原订单金额 这里的金额应该根据订单id查询出来
        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");//设置请求报文格式
        httpPost.setEntity(entity);//将请求报文放入请求对象
        httpPost.setHeader("Accept", "application/json");//设置响应报文格式

        //完成签名并执行请求,并完成验签
        try (CloseableHttpResponse response = wxPayClient.execute(httpPost)) {
            //解析响应结果
            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);
            }
        }
    } 
}

请求微信退款接口:wxPayConfig.getDomain().concat(WxApiType.NATIVE_PAY.getType()) -> https://api.mch.weixin.qq.com/v3/refund/domestic/refunds
微信退款成功回调:wxPayConfig.getNotifyDomain().concat(WxNotifyType.NATIVE_NOTIFY.getType()) -> https://573c-59-174-56-67.ngrok-free.app/api/wx-pay/notify/refunds

SpringBoot整合微信支付_第18张图片
SpringBoot整合微信支付_第19张图片
SpringBoot整合微信支付_第20张图片
在这里插入图片描述
再次查询订单状态

SpringBoot整合微信支付_第21张图片

2.6 下载账单

代码:

package com.weige.javaskillpoint.controller;

import com.google.gson.Gson;
import com.weige.javaskillpoint.config.WxPayConfig;
import com.weige.javaskillpoint.enums.wxpay.WxApiType;
import com.weige.javaskillpoint.enums.wxpay.WxNotifyType;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.util.EntityUtils;
import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

@RestController
@RequestMapping("/api/wx-pay")
@Slf4j
public class WxPayController {

    @Resource
    private CloseableHttpClient wxPayClient;

    @Resource
    //无需应答签名
    private CloseableHttpClient wxPayNoSignClient;

    @Resource
    private WxPayConfig wxPayConfig;

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

        log.info("下载账单");
        log.warn("下载账单接口调用 {}, {}", billDate, type);

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

        //使用wxPayClient发送请求得到响应
        try (CloseableHttpResponse response = wxPayNoSignClient.execute(httpGet)) {
            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);
            }
        }

    }

    /**
     * 申请账单
     */
    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发送请求得到响应
        try (CloseableHttpResponse response = wxPayClient.execute(httpGet)) {
            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");
        }
    }
}

这里的账单有两种类型。一种是申请交易账单,接口后缀为/v3/bill/tradebill;一种是申请资金账单,接口后缀为/v3/bill/fundflowbill
这里我分别演示两种不同账单,并打印到控制台中:
第一种交易账单:

SpringBoot整合微信支付_第22张图片
第二种资金账单:

SpringBoot整合微信支付_第23张图片

2.7 微信支付接口网站

大家在学习微信对接或准备微信对接时,一定要多看微信官方文档。
官方文档:https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_1.shtml
项目代码: https://pan.baidu.com/s/1FRGfYmujzdpUtl4RwSlylA 提取码: 1999

你可能感兴趣的:(高质量JAVA博客,spring,boot,微信,微信小程序)