SpringBoot+Vue 微信支付API V3

SpringBoot+Vue 微信支付

gitee仓库链接,前后端和辅助资料都上传了

wxpaydemo

1.微信支付产品介绍

微信支付主要包括付款码支付,JSAPI支付,小程序支付,Native支付,APP支付,刷脸支付等场景,本次主要学习Native支付,适用于pc网站,常见的微信扫一扫支付方式.

2.前置工作

(1)获取微信商户号

微信商户平台:https://pay.weixin.qq.com/

场景:Native支付 步骤:提交资料 => 签署协议 => 获取商户号

(2)在微信开放平台获取APPID

微信公众平台:https://mp.weixin.qq.com/

步骤:注册服务号 => 服务号认证 => 获取APPID => 绑定商户号

(3)获取API密钥

APIv2版本的接口需要此秘钥 步骤:登录商户平台 => 选择 账户中心 => 安全中心 => API安全 => 设置API密钥

(4)获取APIV3密钥

APIv3版本的接口需要此秘钥

步骤:登录商户平台 => 选择 账户中心 => 安全中心 => API安全 => 设置APIv3密钥 随机密码生成工具:https://suijimimashengcheng.bmcx.com/

(5)申请商户API证书

APIv3版本的所有接口都需要

APIv2版本的高级接口需要(如:退款、企业红包、企业付款等)

步骤:登录商户平台 => 选择 账户中心 => 安全中心 => API安全 => 申请API证书

(6)获取微信怕平台证书

可以预先下载,也可以通过编程的方式获取。后面的课程中,我们会通过编程的方式来获取。

3.创建Springboot微信支付案例

(1)创建sprigboot项目

(2)添加依赖

<dependency>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-webartifactId>
dependency>

(3)创建application.yml配置文件

server:
  port: 8090
spring:
  application:
    name: payment-demo

(4)创建controller包

创建controller包,创建ProductController类

@Api(tags = "微信支付")
@RestController
@RequestMapping("/api/product")
@CrossOrigin //跨域
public class ProductController {

    @ApiOperation(value = "test")
    @GetMapping("/test")
    public String test() {
        return "hello";
    }
}

(5)引入swagger3依赖

        
		<dependency>
            <groupId>io.springfoxgroupId>
            <artifactId>springfox-boot-starterartifactId>
            <version>3.0.0version>
        dependency>
        
        <dependency>
            <groupId>com.github.xiaoymingroupId>
            <artifactId>knife4j-spring-uiartifactId>
            <version>3.0.3version>
        dependency>

(6)swagger3配置类

@Configuration
@EnableOpenApi
//@EnableSwaggerBootstrapUI
public class SwaggerConfig {
    @Value("${swagger.enabled}")
    private boolean enable;

    /**
     * 创建API
     * http:IP:端口号/swagger-ui/index.html 原生地址
     * http:IP:端口号/doc.html bootStrap-UI地址
     */
    @Bean
    public Docket createRestApi() {
        return new Docket(DocumentationType.OAS_30).pathMapping("/")
                // 用来创建该API的基本信息,展示在文档的页面中(自定义展示的信息)
                /*.enable(enable)*/
                .apiInfo(apiInfo())
                // 设置哪些接口暴露给Swagger展示
                .select()
                // 扫描所有有注解的api,用这种方式更灵活
                .apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class))
                // 扫描指定包中的swagger注解
                .apis(RequestHandlerSelectors.basePackage("com.acerola.paymentdemo.controller"))
                // 扫描所有 .apis(RequestHandlerSelectors.any())
                .paths(PathSelectors.regex("(?!/ApiError.*).*"))
                .paths(PathSelectors.any())
                .build()
                // 支持的通讯协议集合
                .protocols(newHashSet("https", "http"))
                .securitySchemes(securitySchemes())
                .securityContexts(securityContexts());

    }

    /**
     * 支持的通讯协议集合
     * @param type1
     * @param type2
     * @return
     */
    private Set<String> newHashSet(String type1, String type2){
        Set<String> set = new HashSet<>();
        set.add(type1);
        set.add(type2);
        return set;
    }

    /**
     * 认证的安全上下文
     */
    private List<SecurityScheme> securitySchemes() {
        List<SecurityScheme> securitySchemes = new ArrayList<>();
        securitySchemes.add((SecurityScheme) new ApiKey("token", "token", "header"));
        return securitySchemes;
    }

    /**
     * 授权信息全局应用
     */
    private List<SecurityContext> securityContexts() {
        List<SecurityContext> securityContexts = new ArrayList<>();
        securityContexts.add(SecurityContext.builder()
                .securityReferences(defaultAuth())
                .forPaths(PathSelectors.any()).build());
        return securityContexts;
    }

    private List<SecurityReference> defaultAuth() {
        AuthorizationScope authorizationScope = new AuthorizationScope("global", "accessEverything");
        AuthorizationScope[] authorizationScopes = new AuthorizationScope[1];
        authorizationScopes[0] = authorizationScope;
        List<SecurityReference> securityReferences = new ArrayList<>();
        securityReferences.add(new SecurityReference("Authorization", authorizationScopes));
        return securityReferences;
    }

    /**
     * 添加摘要信息
     */
    private ApiInfo apiInfo() {
        // 用ApiInfoBuilder进行定制
        return new ApiInfoBuilder()
                // 设置标题
                .title("微信支付")
                // 描述
                .description("微信支付")
                // 作者信息
                .contact(new Contact("doctorCloud", null, null))
                // 版本
                .version("版本号:V.1")
                //协议
                .license("The Apache License")
                //协议url
                .licenseUrl("http://www.baidu.com")
                .build();
    }
}

(7)测试swagger文档

http://localhost:8090/doc.html#/

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-noQdPbG7-1648467247291)(C:\Users\Acerola\AppData\Roaming\Typora\typora-user-images\image-20220326104137315.png)]

(8)引入lombok,简化实体开发

        <dependency>
            <groupId>org.projectlombokgroupId>
            <artifactId>lombokartifactId>
            <optional>trueoptional>
        dependency>

(9)创建R类,统一返回结果

@Data //生成set、get等方法
public class R {
    private Integer code;
    private String message;
    private Map<String, Object> data = new HashMap<>();

    public static R ok() {
        R r = new R();
        r.setCode(0);
        r.setMessage("成功");
        return r;
    }

    public static R error() {
        R r = new R();
        r.setCode(-1);
        r.setMessage("失败");
        return r;
    }

    public R data(String key, Object value) {
        this.data.put(key, value);
        return this;
    }
}

(10)配置json时间格式

spring:
  jackson: #json时间格式
    date-format: yyyy-MM-dd HH:mm:ss
    time-zone: GMT+8

(11)创建mysql数据库

创建payment_demo数据库,并执行以下sql

USE `payment_demo`;

/*Table structure for table `t_order_info` */

CREATE TABLE `t_order_info` (
  `id` bigint(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '订单id',
  `title` varchar(256) DEFAULT NULL COMMENT '订单标题',
  `order_no` varchar(50) DEFAULT NULL COMMENT '商户订单编号',
  `user_id` bigint(20) DEFAULT NULL COMMENT '用户id',
  `product_id` bigint(20) DEFAULT NULL COMMENT '支付产品id',
  `total_fee` int(11) DEFAULT NULL COMMENT '订单金额(分)',
  `code_url` varchar(50) DEFAULT NULL COMMENT '订单二维码连接',
  `order_status` varchar(10) DEFAULT NULL COMMENT '订单状态',
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;


/*Table structure for table `t_payment_info` */

CREATE TABLE `t_payment_info` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '支付记录id',
  `order_no` varchar(50) DEFAULT NULL COMMENT '商户订单编号',
  `transaction_id` varchar(50) DEFAULT NULL COMMENT '支付系统交易编号',
  `payment_type` varchar(20) DEFAULT NULL COMMENT '支付类型',
  `trade_type` varchar(20) DEFAULT NULL COMMENT '交易类型',
  `trade_state` varchar(50) DEFAULT NULL COMMENT '交易状态',
  `payer_total` int(11) DEFAULT NULL COMMENT '支付金额(分)',
  `content` text COMMENT '通知参数',
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;


/*Table structure for table `t_product` */

CREATE TABLE `t_product` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '商品id',
  `title` varchar(20) DEFAULT NULL COMMENT '商品名称',
  `price` int(11) DEFAULT NULL COMMENT '价格(分)',
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;

/*Data for the table `t_product` */

insert  into `t_product`(`title`,`price`) values ('Java课程',1);
insert  into `t_product`(`title`,`price`) values ('大数据课程',1);
insert  into `t_product`(`title`,`price`) values ('前端课程',1);
insert  into `t_product`(`title`,`price`) values ('UI课程',1);

/*Table structure for table `t_refund_info` */

CREATE TABLE `t_refund_info` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '退款单id',
  `order_no` varchar(50) DEFAULT NULL COMMENT '商户订单编号',
  `refund_no` varchar(50) DEFAULT NULL COMMENT '商户退款单编号',
  `refund_id` varchar(50) DEFAULT NULL COMMENT '支付系统退款单号',
  `total_fee` int(11) DEFAULT NULL COMMENT '原订单金额(分)',
  `refund` int(11) DEFAULT NULL COMMENT '退款金额(分)',
  `reason` varchar(50) DEFAULT NULL COMMENT '退款原因',
  `refund_status` varchar(10) DEFAULT NULL COMMENT '退款状态',
  `content_return` text COMMENT '申请退款返回参数',
  `content_notify` text COMMENT '退款结果通知参数',
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;

(12) 集成mybatis-plus

引入依赖


<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
dependency>

<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-boot-starterartifactId>
<version>3.3.1version>
dependency>

(13) 配置数据库连接

datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/payment_demo?serverTimezone=GMT%2B8&characterEncoding=utf-8
    username: root
    password: 123456

(14)定义实体类

BaseEntity:

@Data
public class BaseEntity {

    //定义主键策略:跟随数据库的主键自增
    @TableId(value = "id", type = IdType.AUTO)
    private String id; //主键

    private Date createTime;//创建时间

    private Date updateTime;//更新时间
}

OrderInfo:

@Data
@TableName("t_order_info")
public class OrderInfo  extends BaseEntity{

    private String title;//订单标题

    private String orderNo;//商户订单编号

    private Long userId;//用户id

    private Long productId;//支付产品id

    private Integer totalFee;//订单金额(分)

    private String codeUrl;//订单二维码连接

    private String orderStatus;//订单状态
}

PaymentInfo:

@Data
@TableName("t_payment_info")
public class PaymentInfo extends BaseEntity{

    private String orderNo;//商品订单编号

    private String transactionId;//支付系统交易编号

    private String paymentType;//支付类型

    private String tradeType;//交易类型

    private String tradeState;//交易状态

    private Integer payerTotal;//支付金额(分)

    private String content;//通知参数
}

Product:

@Data
@TableName("t_product")
public class Product extends BaseEntity{

    private String title; //商品名称

    private Integer price; //价格(分)
}

RefundInfo:

@Data
@TableName("t_refund_info")
public class RefundInfo extends BaseEntity{

    private String orderNo;//商品订单编号

    private String refundNo;//退款单编号

    private String refundId;//支付系统退款单号

    private Integer totalFee;//原订单金额(分)

    private Integer refund;//退款金额(分)

    private String reason;//退款原因

    private String refundStatus;//退款单状态

    private String contentReturn;//申请退款返回参数

    private String contentNotify;//退款结果通知参数
}

(15)定义持久层

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iISHg9Ga-1648467247295)(C:\Users\Acerola\AppData\Roaming\Typora\typora-user-images\image-20220326150017853.png)]

(16)定义mybatis-plus的yml文件配置

mybatis-plus:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  mapper-locations:
    - classpath: src/main/resources/mapper/*.xml

(17) 定义业务层

定义业务层接口继承 IService<>

定义业务层接口的实现类,并继承 ServiceImpl<,>

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-O9NP08o5-1648467247297)(C:\Users\Acerola\AppData\Roaming\Typora\typora-user-images\image-20220326150316641.png)]

(18) 查询所有商品测试

    @ApiOperation("商品列表")
    @GetMapping("/list")
    public R list() {
        List<Product> list = productService.list();
        return R.ok().data("productList", list);
    }

swagger测试:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-b6b9wTzl-1648467247298)(C:\Users\Acerola\AppData\Roaming\Typora\typora-user-images\image-20220326151342914.png)]

4. 搭建前端环境

此处为vue基础,略过

5. 基础支付API V3

(1) 引入支付参数

1.1定义微信支付相关参数

将资料文件夹中的 wxpay.properties 复制到resources目录中 这个文件定义了之前我们准备的微信支付相关的参数,例如商户号、APPID、API秘钥等等

wxpay.properties:

# 微信支付相关参数
# 商户号
wxpay.mch-id=1558950191
# 商户API证书序列号
wxpay.mch-serial-no=34345964330B66427E0D3D28826C4993C77E631F
# 商户私钥文件
wxpay.private-key-path=apiclient_key.pem
# APIv3密钥
wxpay.api-v3-key=UDuLFDcmy5Eb6o0nTNZdu6ek4DDh4K8B
# APPID
wxpay.appid=wx74862e0dfcf69954
# 微信服务器地址
wxpay.domain=https://api.mch.weixin.qq.com
# 接收结果通知地址
wxpay.notify-domain=https://7d92-115-171-63-135.ngrok.io

1.2 读取支付参数

WxPayConfig.java:

@Configuration
@PropertySource("classpath:wxpay.properties") //读取配置文件
@ConfigurationProperties(prefix="wxpay") //读取wxpay节点
@Data //使用set方法将wxpay节点中的值填充到当前类的属性中
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;

}

1.3 测试支付参数的获取

在controller包建立TestController类

@Api(tags = "测试控制器")
@RestController
@RequestMapping("/api/test")
public class TestController {
    @Autowired
    private WxPayConfig wxPayConfig;

    @ApiOperation(value = "测试支付参数的获取")
    @GetMapping("/get-wx-pay-config")
    public R getWxPayConfig() {
        String mchId = wxPayConfig.getMchId();
        return R.ok().data("mchId", mchId);
    }
}

1.4 配置 Annotation Processor

可以帮助我们生成自定义配置的元数据信息,让配置文件和Java代码之间的对应参数可以自动定位,方便开发。

  <dependency>
       <groupId>org.springframework.bootgroupId>
       <artifactId>spring-boot-configuration-processorartifactId>
       <optional>trueoptional>
  dependency>

1.5 设置wxpay.properties为springboot配置文件

让IDEA可以识别配置文件,将配置文件的图标展示成SpringBoot的图标,同时配置文件的内容可以高亮显示

File -> Project Structure -> Modules -> 选择小叶子

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-q2UbX2qu-1648467247299)(C:\Users\Acerola\AppData\Roaming\Typora\typora-user-images\image-20220326163120508.png)]

(2) 加载商户私钥

2.1复制商户私钥

2.2 引入SDK

        <dependency>
            <groupId>com.github.wechatpay-apiv3groupId>
            <artifactId>wechatpay-apache-httpclientartifactId>
            <version>0.3.0version>
        dependency>

2.3测试商户私钥的获取

在WxPayConfig.java中添加获取商户私钥文件的方法

    /**
     * 获取商户私钥文件
     *
     * @param filename
     * @return
     * @throws FileNotFoundException
     */
     public PrivateKey getPrivateKey(String filename){
         try {
             return PemUtil.loadPrivateKey(new FileInputStream(filename));
         } catch (FileNotFoundException e) {
             throw new RuntimeException("私钥不存在",e);
         }
     }

在 PaymentDemoApplicationTests 测试类中添加如下方法,测试私钥对象是否能够获取出来。

   @Test
    public void testGetPrivateKey() {
        //获取私钥路径
        String privateKeyPath = wxPayConfig.getPrivateKeyPath();
        //获取商户私钥
        PrivateKey privateKey = wxPayConfig.getPrivateKey(privateKeyPath);
        System.out.println(privateKey);
    }

(3)获取签名验证器和HttpClient

3.1 证书密钥使用说明

SpringBoot+Vue 微信支付API V3_第1张图片

3.2 获取签名验证器

https://github.com/wechatpay-apiv3/wechatpay-apache-httpclient (定时更新平台证书功能) 平台证书:平台证书封装了微信的公钥,商户可以使用平台证书中的公钥进行验签。 签名验证器:帮助我们进行验签工作,我们单独将它定义出来,方便后面的开发。

@Bean
    public ScheduledUpdateCertificatesVerifier getVerifier() {
//获取商户私钥
        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;
    }

3.3 获取 HttpClient 对象

https://github.com/wechatpay-apiv3/wechatpay-apache-httpclient (定时更新平台证书功能) HttpClient 对象:是建立远程连接的基础,我们通过SDK创建这个对象。

 /**
     * 获取HttpClient对象
     *
     * @param verifier
     * @return
     */
    @Bean
    public CloseableHttpClient getWxPayClient(ScheduledUpdateCertificatesVerifier
                                                      verifier) {
        //获取商户私钥
        PrivateKey privateKey = getPrivateKey(privateKeyPath);
        //用于构造HttpClient
        WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create()
                .withMerchant(mchId, mchSerialNo, privateKey)
                .withValidator(new WechatPay2Validator(verifier));
        // ... 接下来,你仍然可以通过builder设置各种参数,来配置你的HttpClient
        // 通过WechatPayHttpClientBuilder构造的HttpClient,会自动的处理签名和验签,并进行证书自动更新
        CloseableHttpClient httpClient = builder.build();
        return httpClient;
    }

(4)API字典和相关工具

4.1 API列表

模块名称 功能列表 描述
Native支付 Native下单 通过本接口提交微信支付Native支付订单
查询订单 通过此接口查询订单状态
关闭订单 通过此接口关闭待支付订单
Native调起支付 商户后台系统先调用微信支付的Native支付接口,微信后台系统返回链接参数code_url,商户后台系统将code_url值生成二维码图片,用户使用微信客户端扫码后发起支付。
支付结果通知 微信支付通过支付通知接口将用户支付成功消息通知给商户
申请退款 商户可以通过该接口将支付金额退还给买家
查询单笔退款 提交退款申请后,通过调用该接口查询退款状态
退款结果通知 微信支付通过退款通知接口将用户退款成功消息通知给商户
申请交易账单 商户可以通过该接口获取交易账单文件的下载地址
申请资金账单 商户可以通过该接口获取资金账单文件的下载地址
下载账单 通过申请交易/资金账单获取到download_url在该接口获取到对应的账单。

4.2 接口规则

https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay2_0.shtml 微信支付 APIv3 使用 JSON 作为消息体的数据交换格式。

我们使用谷歌的json处理


<dependency>
	<groupId>com.google.code.gsongroupId>
	<artifactId>gsonartifactId>
dependency>

4.3定义枚举

为了开发方便,我们预先在项目中定义一些枚举。枚举中定义的内容包括接口地址,支付状态等信息。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ybh6aBlk-1648467247302)(C:\Users\Acerola\AppData\Roaming\Typora\typora-user-images\image-20220326174737976.png)]

4.4添加工具类

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-H0L2h6Zk-1648467247303)(C:\Users\Acerola\AppData\Roaming\Typora\typora-user-images\image-20220326174822867.png)]

(5)Native下单API

5.1Native下单流程

微信支付-开发者文档 (qq.com)

SpringBoot+Vue 微信支付API V3_第2张图片

5.2 Native下单API

微信支付-开发者文档 (qq.com)

5.2.1 创建WxPayController
package com.acerola.paymentdemo.controller;

import io.swagger.annotations.Api;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @program: payment-demo
 * @description:
 * @author: Acerola
 * @create: 2022-03-26 17:53
 **/
@CrossOrigin
@RestController
@RequestMapping("/api/wx-pay")
@Api(tags = "网站微信支付")
@Slf4j
public class WxPayController {

}

5.2.2 创建service层

接口:

package com.acerola.paymentdemo.service;

public interface WxPayService {
}

实现:

package com.acerola.paymentdemo.service.impl;

import com.acerola.paymentdemo.service.WxPayService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

/**
 * @program: payment-demo
 * @description:
 * @author: Acerola
 * @create: 2022-03-26 17:55
 **/
@Service
@Slf4j
public class WxPayServiceImpl implements WxPayService {
}

5.2.3 定义WxPayController方法

R对象中添加 @Accessors(chain = true),使其可以链式操作

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

    }
5.2.4 定义WxPayService方法

实现

@Autowired
    private WxPayConfig wxPayConfig;
    @Autowired
    private CloseableHttpClient wxPayClient;

    /**
     * 创建订单,调用Native支付接口
     *
     * @param productId
     * @return code_url 和 订单号
     */
    @SneakyThrows
    @Override
    public Map<String, Object> nativePay(Long productId) {
        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");
        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");
            Map<String, Object> map = new HashMap<>();
            map.put("codeUrl", codeUrl);
            map.put("orderNo", orderInfo.getOrderNo());
            return map;
        } finally {
            response.close();
        }
    }
5.2.5 测试调用统一下单API,生成支付二维码

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BDPFpSkB-1648467247306)(C:\Users\Acerola\AppData\Roaming\Typora\typora-user-images\image-20220326181237729.png)]

5.3 创建课程订单

5.3.1 保存订单

OrderInfoService

接口:

 OrderInfo createOrderByProductId(Long productId);

实现:

  @Resource
    private ProductMapper productMapper;

    @Override
    public OrderInfo createOrderByProductId(Long productId) {
        //查找已存在但未支付的订单
        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());
        baseMapper.insert(orderInfo);
        return orderInfo;

    }

查找未支付订单:OrderInfoService中添加辅助方法

 /**
     * 根据商品id查询未支付订单
     * 防止重复创建订单对象
     *
     * @param productId
     * @return
     */
    private OrderInfo getNoPayOrderByProductId(Long productId) {
        QueryWrapper<OrderInfo> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("product_id", productId);
        queryWrapper.eq("order_status", OrderStatus.NOTPAY.getType());
        // queryWrapper.eq("user_id", userId);
        OrderInfo orderInfo = baseMapper.selectOne(queryWrapper);
        return orderInfo;
    }
5.3.2 缓存二维码

OrderInfoService

接口:

OrderInfo createOrderByProductId(Long productId);

实现:

    /**
     * 存储订单二维码
     * @param orderNo
     * @param codeUrl
     */
    @Override
    public void saveCodeUrl(String orderNo, String codeUrl) {
        QueryWrapper<OrderInfo> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("order_no", orderNo);
        OrderInfo orderInfo = new OrderInfo();
        orderInfo.setCodeUrl(codeUrl);
        baseMapper.update(orderInfo, queryWrapper);
    }
5.3.3 修改WxPayServiceImpl 的 nativePay 方法
 @Autowired
    private OrderInfoService orderInfoService;


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

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

        //生成订单
        OrderInfo orderInfo = orderInfoService.createOrderByProductId(productId);
        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;
        }


        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");
        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);
            //二维码
            codeUrl = resultMap.get("code_url");

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

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

            return map;

        } finally {
            response.close();
        }
    }

5.4 显示订单列表

在我的订单页面按时间倒序显示订单列表

5.4.1 创建OrderInfoController
package com.acerola.paymentdemo.controller;

import com.acerola.paymentdemo.common.R;
import com.acerola.paymentdemo.entity.OrderInfo;
import com.acerola.paymentdemo.service.OrderInfoService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

/**
 * @program: payment-demo
 * @description:
 * @author: Acerola
 * @create: 2022-03-27 17:57
 **/


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

    @ApiOperation("订单列表")
    @GetMapping("/list")
    public R list() {
        List<OrderInfo> list = orderInfoService.listOrderByCreateTimeDesc();
        return R.ok().data("list", list);
    }
}
5.4.2 定义 OrderInfoService 方法

接口:

List<OrderInfo> listOrderByCreateTimeDesc();

实现:

    /**
     * 查询订单列表,并倒序查询
     * @return
     */
    @Override
    public List<OrderInfo> listOrderByCreateTimeDesc() {
        QueryWrapper<OrderInfo> queryWrapper = new QueryWrapper<OrderInfo>
                ().orderByDesc("create_time");
        return baseMapper.selectList(queryWrapper);
    }

(6)支付通知API

6.1 内网穿透

6.1.1下载内网穿透工具

下载ngrok

6.1.2 设置authtoken
ngrok authtoken 26y23wgoy8AEsyn4CC9qfLbEvK0_2A4okRhSWNU6GprLQjyHZ
6.1.3 启动内网穿透
ngrok http 8090

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mHnBKosO-1648467247307)(C:\Users\Acerola\AppData\Roaming\Typora\typora-user-images\image-20220327201945354.png)]

6.1.4 测试外网访问
http://922c-117-158-127-32.ngrok.io/api/order-info/list

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3aEg2FgA-1648467247308)(C:\Users\Acerola\AppData\Roaming\Typora\typora-user-images\image-20220327232105872.png)]

6.2 接收通知和返回应答

支付通知API:https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_5.shtml

6.2.1启动ngrok
ngrok http 8090
6.2.2设置通知地址

wxpay.properties

注意:每次重新启动ngrok,都需要根据实际情况修改这个配置

wxpay.notify-domain=https://7d92-115-171-63-135.ngrok.io
6.2.3创建通知接口

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

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

用失败应答替换成功应答

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

6.3验签

6.3.1 工具类

参考SDK源码中的 WechatPay2Validator 创建通知验签工具类 WechatPay2ValidatorForRequest

6.3.2 验签
@Resource
private Verifier verifier;

更新WxPayController中nativeNotify方法

 /**
     * 支付通知
     * 微信支付通过支付通知接口将用户支付成功消息通知给商户
     */
    @ApiOperation("支付通知")
    @PostMapping("/native/notify")
    public String nativeNotify(HttpServletRequest request, HttpServletResponse
            response) throws IOException {
        Gson gson = new Gson();
        Map<String, String> map = new HashMap<>();//应答对象
        //处理通知参数
        String body = HttpUtils.readData(request);
        Map<String, Object> bodyMap = gson.fromJson(body, HashMap.class);
        String requestId = (String) bodyMap.get("id");
        log.info("支付通知的id ===> {}", bodyMap.get("id"));
        log.info("支付通知的完整数据 ===> {}", body);
        //TODO : 签名的验证
        //签名的验证
        WechatPay2ValidatorForRequest validator
                = new WechatPay2ValidatorForRequest(verifier, body, requestId);
        if (!validator.validate(request)) {
            log.error("通知验签失败");
            //失败应答
            response.setStatus(500);
            map.put("code", "ERROR");
            map.put("message", "通知验签失败");
            return gson.toJson(map);
        }
        log.info("通知验签成功");

        //TODO : 处理订单
        //成功应答:成功应答必须为200或204,否则就是失败应答
        response.setStatus(200);
        map.put("code", "SUCCESS");
        map.put("message", "成功");
        return gson.toJson(map);
    }

6.4解密

6.4.1 WxPayController

nativeNotify 方法中添加处理订单的代码

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

接口:

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

实现:

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

6.5处理订单

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

OrderInfoService

接口:

void updateStatusByOrderNo(String orderNo, OrderStatus orderStatus);

实现:

/**
     * 根据订单编号更新订单状态
     *
     * @param orderNo
     * @param orderStatus
     */
    @Override
    public void updateStatusByOrderNo(String orderNo, OrderStatus orderStatus) {
        log.info("更新订单状态 ===> {}", orderStatus.getType());
        QueryWrapper<OrderInfo> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("order_no", orderNo);
        OrderInfo orderInfo = new OrderInfo();
        orderInfo.setOrderStatus(orderStatus.getType());
        baseMapper.update(orderInfo, queryWrapper);
    }
6.5.3处理支付日志

PaymentInfoService

接口:

void createPaymentInfo(String plainText);

实现:

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

6.6处理重复通知

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JIRw5qzC-1648467247310)(C:\Users\Acerola\AppData\Roaming\Typora\typora-user-images\image-20220328104343532.png)]

在 processOrder 方法中,更新订单状态之前,添加如下代码

OrderInfoService

接口:

String getOrderStatus(String orderNo);

实现:

 /**
     * 根据订单号获取订单状态
     *
     * @param orderNo
     * @return
     */
    @Override
    public String getOrderStatus(String orderNo) {
        QueryWrapper<OrderInfo> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("order_no", orderNo);
        OrderInfo orderInfo = baseMapper.selectOne(queryWrapper);
        //防止被删除的订单的回调通知的调用
        if (orderInfo == null) {
            return null;
        }
        return orderInfo.getOrderStatus();
    }

在 processOrder 方法中,更新订单状态之前,添加如下代码

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

6.7数据锁

6.7.1定义ReentrantLock

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

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

(7)商户定时查询本地订单

7.1 后端定义商户查单接口

支付成功后,商户侧查询本地数据库,订单是否支付成功

 /**
     * 查询本地订单状态
     */
    @ApiOperation("查询本地订单状态")
    @GetMapping("/query-order-status/{orderNo}")
    public R queryOrderStatus(@PathVariable String orderNo) {
        String orderStatus = orderInfoService.getOrderStatus(orderNo);
        if (OrderStatus.SUCCESS.getType().equals(orderStatus)) {//支付成功
            return R.ok();
        }
        return R.ok().setCode(101).setMessage("支付中...");
    }

7.2 前端定时轮询查单

在二维码展示页面,前端定时轮询查询订单是否已支付,如果支付成功则跳转到订单页面

7.2.1定义定时器
//启动定时器
this.timer = setInterval(() => {
//查询订单是否支付成功
this.queryOrderStatus()
}, 3000)
7.2.2 查询订单
// 查询订单状态
queryOrderStatus() {
orderInfoApi.queryOrderStatus(this.orderNo).then(response => {
console.log('查询订单状态:' + response.code)
// 支付成功后的页面跳转
if (response.code === 0) {
console.log('清除定时器')
clearInterval(this.timer)
// 三秒后跳转到订单列表
setTimeout(() => {
this.$router.push({ path: '/success' })
}, 3000)
}
})
}

(8)用户取消订单API

实现用户主动取消订单的功能

8.1定义取消订单接口

WxPayController中添加接口方法

 /**
     * 用户取消订单
     *
     * @param orderNo
     * @return
     * @throws Exception
     */
    @ApiOperation("用户取消订单")
    @PostMapping("/cancel/{orderNo}")
    public R cancel(@PathVariable String orderNo) throws Exception {
        log.info("取消订单");
        wxPayService.cancelOrder(orderNo);
        return R.ok().setMessage("订单已取消");
    }

8.2WxPayService

接口:

void cancelOrder(String orderNo) throws Exception;

实现:

    /**
     * 用户取消订单
     *
     * @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();
        }
    }

(9)微信支付查单API

9.1 查单接口的调用

商户后台未收到异步支付结果通知时,商户应该主动调用《微信支付查单接口》,同步订单状态

9.1.1 WxPayController
 /**
     * 查询订单
     *
     * @param orderNo
     * @return
     * @throws URISyntaxException
     * @throws IOException
     */
    @ApiOperation("查询订单:测试订单状态用")
    @GetMapping("query/{orderNo}")
    public R queryOrder(@PathVariable String orderNo) throws Exception {
        log.info("查询订单");
        String bodyAsString = wxPayService.queryOrder(orderNo);
        return R.ok().setMessage("查询成功").data("bodyAsString", bodyAsString);
    }
9.1.2 WxPayService

接口:

String queryOrder(String orderNo) throws Exception;

实现:

  /**
     * 查单接口调用
     */
    @SneakyThrows
    @Override
    public String queryOrder(String orderNo) {
        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("Native下单失败,响应码 = " + statusCode + ",返回结果 = " +
                        bodyAsString);
                throw new IOException("request failed");
            }
            return bodyAsString;
        } finally {
            response.close();
        }
    }

9.2 集成Spring Task

Spring 3.0后提供Spring Task实现任务调度

9.2.1 启动类添加注解
@EnableScheduling
9.2.2 测试定时任务

创建 task 包,创建 WxPayTask.java

package com.acerola.paymentdemo.task;

import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

/**
 * @program: payment-demo
 * @description:
 * @author: Acerola
 * @create: 2022-03-28 17:12
 **/

@Slf4j
@Component
public class WxPayTask {
    /**
     * 测试
     * (cron="秒 分 时 日 月 周")
     * *:每隔一秒执行
     * 0/3:从第0秒开始,每隔3秒执行一次
     * 1-3: 从第1秒开始执行,到第3秒结束执行
     * 1,2,3:第1、2、3秒执行
     * ?:不指定,若指定日期,则不指定周,反之同理
     */
    @Scheduled(cron = "0/3 * * * * ?")
    public void task1() {
        log.info("task1 执行");
    }
}

9.3定时查找超时订单

9.3.1WxPayTask
    @Autowired
    private OrderInfoService orderInfoService;
    @Autowired
    private WxPayService wxPayService;

    /**
     * 从第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);
        }
    }
9.3.2 OrderInfoService

接口:

    List<OrderInfo> getNoPayOrderByDuration(int i);

实现:

    /**
     * 找出创建超过minutes分钟并且未支付的订单
     *
     * @param minutes
     * @return
     */
    @Override
    public List<OrderInfo> getNoPayOrderByDuration(int minutes) {
        //minutes分钟之前的时间
        Instant instant = Instant.now().minus(Duration.ofMinutes(minutes));
        QueryWrapper<OrderInfo> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("order_status", OrderStatus.NOTPAY.getType());
        queryWrapper.le("create_time", instant);
        List<OrderInfo> orderInfoList = baseMapper.selectList(queryWrapper);
        return orderInfoList;
    }

9.4 处理超时订单

WxPayService

接口:

    void checkOrderStatus(String orderNo);

实现:

  /**
     * 根据订单号查询微信支付查单接口,核实订单状态
     * 如果订单已支付,则更新商户端订单状态,并记录支付日志
     * 如果订单未支付,则调用关单接口关闭订单,并更新商户端订单状态
     *
     * @param orderNo
     */
    @SneakyThrows
    @Override
    public void checkOrderStatus(String orderNo)   {
        log.warn("根据订单号核实订单状态 ===> {}", orderNo);
        //调用微信支付查单接口
        String result = this.queryOrder(orderNo);
        Gson gson = new Gson();
        Map resultMap = gson.fromJson(result, HashMap.class);
        //获取微信支付端的订单状态
        Object tradeState = resultMap.get("trade_state");
        //判断订单状态
        if (WxTradeState.SUCCESS.getType().equals(tradeState)) {
            log.warn("核实订单已支付 ===> {}", orderNo);
        //如果确认订单已支付则更新本地订单状态
            orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.SUCCESS);
        //记录支付日志
            paymentInfoService.createPaymentInfo(result);
        }
        if (WxTradeState.NOTPAY.getType().equals(tradeState)) {
            log.warn("核实订单未支付 ===> {}", orderNo);
        //如果订单未支付,则调用关单接口
            this.closeOrder(orderNo);
        //更新本地订单状态
            orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.CLOSED);
        }
    }
9.4.1 根据订单号获取订单

接口:

OrderInfo getOrderByOrderNo(String orderNo);

实现:

/**
     * 根据订单号获取订单
     * @param orderNo
     * @return
     */
    @Override
    public OrderInfo getOrderByOrderNo(String orderNo) {
        QueryWrapper<OrderInfo> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("order_no", orderNo);
        OrderInfo orderInfo = baseMapper.selectOne(queryWrapper);
        return orderInfo;
    }

(10) 申请退款API

文档:https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_9.shtml

10.1 创建退款单

RefundsInfoService

接口:

RefundInfo createRefundByOrderNo(String orderNo, String reason);

实现:

@Autowired
    private OrderInfoService orderInfoService;

    /**
     * 根据订单号创建退款订单
     *
     * @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;
    }

10.2 更新退款单

RefundInfoService

接口:

void updateRefund(String content);

实现:

/**
     * 记录退款记录
     *
     * @param content
     */
    @Override
    public void updateRefund(String content) {
        //将json字符串转换成Map
        Gson gson = new Gson();
        Map<String, String> resultMap = gson.fromJson(content, HashMap.class);
        //根据退款单编号修改退款单
        QueryWrapper<RefundInfo> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("refund_no", resultMap.get("out_refund_no"));
        //设置要修改的字段
        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);
    }

10.3 申请退款

10.3.1 WxPayController
@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();
    }
10.3.2 WxPayService

接口:

  void refund(String orderNo, String reason);

实现:

 @Autowired
    private RefundInfoService refundsInfoService;
/**
     * 退款
     *
     * @param orderNo
     * @param reason
     * @throws IOException
     */
    @SneakyThrows
    @Transactional(rollbackFor = Exception.class)
    @Override
    public void refund(String orderNo, String reason) {
        log.info("创建退款单记录");
        //根据订单编号创建退款单
        RefundInfo refundsInfo = refundsInfoService.createRefundByOrderNo(orderNo,
                reason);
        log.info("调用退款API");
        //调用统一下单API
        String url =
                wxPayConfig.getDomain().concat(WxApiType.DOMESTIC_REFUNDS.getType());
        HttpPost httpPost = new HttpPost(url);
        // 请求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);
        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 {
            //解析响应结果
            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);
            }
            //更新订单状态
            orderInfoService.updateStatusByOrderNo(orderNo,
                    OrderStatus.REFUND_PROCESSING);
            //更新退款单
            refundsInfoService.updateRefund(bodyAsString);
        } finally {
            response.close();
        }
    }

(11)查询退款API

文档:https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_10.shtml

11.1 查单接口的调用

11.1.1 WxPayController
    /**
     * 查询退款
     *
     * @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);
    }
11.1.2 WxPayService

接口:

    String queryRefund(String refundNo);

实现:

 @SneakyThrows
    @Override
    public String queryRefund(String refundNo) {
        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");
        //完成签名并执行请求
        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();
        }
    }

11.2 定时查找退款中的订单

11.2.1 WxPayTask
    /**
     * 从第0秒开始每隔30秒执行1次,查询创建超过5分钟,并且未成功的退款单
     */
    @Scheduled(cron = "0/30 * * * * ?")
    public void refundConfirm() throws Exception {
        log.info("refundConfirm 被执行......");
        //找出申请退款超过5分钟并且未成功的退款单
        List<RefundInfo> refundInfoList =
                refundInfoService.getNoRefundOrderByDuration(5);
        for (RefundInfo refundInfo : refundInfoList) {
            String refundNo = refundInfo.getRefundNo();
            log.warn("超时未退款的退款单号 ===> {}", refundNo);
            //核实订单状态:调用微信支付查询退款接口
            wxPayService.checkRefundStatus(refundNo);
        }
    }
11.2.2 RefundInfoService

接口:

List<RefundInfo> getNoRefundOrderByDuration(int minutes);

实现:

    /**
     * 找出申请退款超过minutes分钟并且未成功的退款单
     *
     * @param minutes
     * @return
     */
    @Override
    public List<RefundInfo> getNoRefundOrderByDuration(int minutes) {
        //minutes分钟之前的时间
        Instant instant = Instant.now().minus(Duration.ofMinutes(minutes));
        QueryWrapper<RefundInfo> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("refund_status", WxRefundStatus.PROCESSING.getType());
        queryWrapper.le("create_time", instant);
        List<RefundInfo> refundInfoList = baseMapper.selectList(queryWrapper);
        return refundInfoList;
    }

11.3 处理超时未退款订单

WxPayService

核实订单状态

接口:

void checkRefundStatus(String refundNo);

实现:

    /**
     * 根据退款单号核实退款单状态
     *
     * @param refundNo
     * @return
     */
    @Transactional(rollbackFor = Exception.class)
    @Override
    public void checkRefundStatus(String refundNo) {
        log.warn("根据退款单号核实退款单状态 ===> {}", refundNo);
        //调用查询退款单接口
        String result = this.queryRefund(refundNo);
        //组装json请求体字符串
        Gson gson = new Gson();
        Map<String, String> resultMap = gson.fromJson(result, HashMap.class);
        //获取微信支付端退款状态
        String status = resultMap.get("status");
        String orderNo = resultMap.get("out_trade_no");
        if (WxRefundStatus.SUCCESS.getType().equals(status)) {
            log.warn("核实订单已退款成功 ===> {}", refundNo);
            //如果确认退款成功,则更新订单状态
            orderInfoService.updateStatusByOrderNo(orderNo,
                    OrderStatus.REFUND_SUCCESS);
            //更新退款单
            refundsInfoService.updateRefund(result);
        }
        if (WxRefundStatus.ABNORMAL.getType().equals(status)) {
            log.warn("核实订单退款异常 ===> {}", refundNo);
            //如果确认退款成功,则更新订单状态
            orderInfoService.updateStatusByOrderNo(orderNo,
                    OrderStatus.REFUND_ABNORMAL);
            //更新退款单
            refundsInfoService.updateRefund(result);
        }
    }

(12) 退款结果通知API

文档:https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_11.shtml

12.1 接收退款通知

WxPayController

/**
     * 退款结果通知
     * 退款状态改变后,微信会把相关退款结果发送给商户。
     */
    @PostMapping("/refunds/notify")
    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("通知验签成功");
            //处理退款单
            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);
        }
    }

12.2 处理订单和退款单

WxPayService

接口:

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

实现:

 /**
     * 处理退款单
     */
    @SneakyThrows
    @Transactional(rollbackFor = Exception.class)
    @Override
    public void processRefund(Map<String, Object> bodyMap)  {
        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();
            }
        }
    }

(13) 账单

13.1 申请交易账单和资金账单

13.1.1 WxPayController
 @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);
    }
13.1.2 WxPayService

接口:

String queryBill(String billDate, String type);

实现:

 /**
     * 申请账单
     *
     * @param billDate
     * @param type
     * @return
     * @throws Exception
     */
    @SneakyThrows
    @Override
    public String queryBill(String billDate, String type)  {
        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();
        }
    }

13.2 下载账单

13.2.1 WxPayController
   @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);
    }
13.2.2 WxPayService

接口:

String downloadBill(String billDate, String type);

实现:

    @Autowired
    private CloseableHttpClient wxPayNoSignClient;

/**
     * 下载账单
     *
     * @param billDate
     * @param type
     * @return
     * @throws Exception
     */
    @SneakyThrows
    @Override
    public String downloadBill(String billDate, String type)  {
        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();
        }
    }

(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 resultMap = gson.fromJson(bodyAsString,
HashMap.class);
return resultMap.get(“download_url”);
} finally {
response.close();
}
}


#### 13.2 下载账单

##### 13.2.1 WxPayController

```java
   @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);
    }
13.2.2 WxPayService

接口:

String downloadBill(String billDate, String type);

实现:

    @Autowired
    private CloseableHttpClient wxPayNoSignClient;

/**
     * 下载账单
     *
     * @param billDate
     * @param type
     * @return
     * @throws Exception
     */
    @SneakyThrows
    @Override
    public String downloadBill(String billDate, String type)  {
        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();
        }
    }

你可能感兴趣的:(菜鸡上路,java,开发语言,后端)