参考视频:【尚硅谷】微信支付开发 https://www.bilibili.com/video/BV1hh411D7sb?p=1

微信支付介绍和接入指引

微信支付产品介绍

https://pay.weixin.qq.com/static/product/product_intro.shtml?name=qrcode

付款码支付:

  • 用户展示微信钱包内的“付款码”给商家,商家扫描后直接完成支付,适用于线下面对面收银的场景。

JSAPI支付:

  • 线下场所:商户展示一个支付二维码,用户使用微信扫描二维码后,输入需要支付的金额,完成支付。

  • 公众号场景:用户在微信内进入商家公众号,打开某个页面,选择某个产品,完成支付。

  • PC网站场景:在网站中展示二维码,用户使用微信扫描二维码,输入需要支付的金额,完成支付。

特点:用户在客户端输入支付金额

小程序支付:

  • 在微信小程序平台内实现支付的功能。

Native支付:

  • Native支付是指商户展示支付二维码,用户再用微信“扫一扫”完成支付的模式。这种方式适用于PC网站。

特点:商家预先指定支付金额

APP支付:

  • 商户通过在移动端独立的APP应用程序中集成微信支付模块,完成支付。

刷脸支付:

  • 用户在刷脸设备前通过摄像头刷脸、识别身份后进行的一种支付方式。

接入指引

官网:https://pay.weixin.qq.com/wiki/doc/apiv3/open/pay/chapter2_7_1.shtml

获取商户号:

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

  • 场景:Native支付

  • 步骤:提交资料 => 签署协议 => 获取商户号(账户中心的个人信息页面的登录账号)

获取APPID:

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

  • 步骤:注册服务号 => 服务号认证 => 获取APPID (基本配置中的开发者ID)=> 绑定商户号

获取API秘钥:

  • APIv2版本的接口需要此秘钥

  • 步骤:登录商户平台 => 选择 账户中心 => 安全中心 => API安全 => 设置API密钥

获取APIv3秘钥:

  • APIv3版本的接口需要此秘钥

  • 步骤:登录商户平台 => 选择 账户中心 => 安全中心 => API安全 => 设置APIv3密钥

随机密码生成工具:https://suijimimashengcheng.bmcx.com/

申请商户API证书以及对应证书序列号:

  • APIv3版本的所有接口都需要;APIv2版本的高级接口需要(如:退款、企业红包、企业付款等)

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

获取微信平台证书:

  • 可以预先下载,也可以通过编程的方式获取。建议通过编程的方式来获取避免证书过期。

注意:以上所有API秘钥和证书需妥善保管防止泄露


支付安全(证书/秘钥/签名)

信息安全的基础 - 机密性

  • 明文:加密前的消息叫“明文”(plain text)

  • 密文:加密后的文本叫“密文”(cipher text)

  • 密钥:只有掌握特殊“钥匙”的人,才能对加密的文本进行解密,这里的“钥匙”就叫做“密钥”(key)

    • “密钥”就是一个字符串,度量单位是“位”(bit),比如,密钥长度是 128,就是 16 字节的二进制串
  • 加密:实现机密性最常用的手段是“加密”(encrypt)

    • 按照密钥的使用方式,加密可以分为两大类:对称加密和非对称加密。
  • 解密:使用密钥还原明文的过程叫“解密”(decrypt)

  • 加密算法:加密解密的操作过程就是“加密算法”

    • 所有的加密算法都是公开的,而算法使用的“密钥”则必须保密

对称加密和非对称加密

  • 对称加密

    • 特点:只使用一个密钥,密钥必须保密,常用的有 AES算法

    • 优点:运算速度快

    • 缺点:秘钥需要信息交换的双方共享,一旦被窃取,消息会被破解,无法做到安全的密钥交换

  • 非对称加密

    • 特点:使用两个密钥:公钥和私钥,公钥可以任意分发而私钥保密,常用的有 RSA算法

    • 优点:黑客获取公钥无法破解密文,解决了密钥交换的问题

    • 缺点:运算速度非常慢

  • 混合加密

    • 实际场景中把对称加密和非对称加密结合起来使用。

身份认证

  • 公钥加密,私钥解密的作用是加密信息

  • 私钥加密,公钥解密的作用是身份认证

公钥加密:

私钥加密:

摘要算法(Digest Algorithm)

摘要算法就是我们常说的散列函数、哈希函数(Hash Function),它能够把任意长度的数据“压缩”成固定长度而且独一无二的“摘要”字符串,就好像是给这段数据生成了一个数字“指纹”。

作用:保证信息的完整性

特性

  • 不可逆:只有算法,没有秘钥,只能加密,不能解密

  • 难题友好性:想要破解,只能暴力枚举

  • 发散性:只要对原文进行一点点改动,摘要就会发生剧烈变化(这种现象也可以称为雪崩效应)

  • 抗碰撞性:原文不同,计算后的摘要也要不同

常见摘要算法:MD5、SHA1、SHA2(SHA224、SHA256、SHA384)【SHA2用的比较多,具有强的抗碰撞性】

注意:这样还是无法鉴别出信息传输的完整性,黑客可以更改明文和摘要达到欺骗的目的,所以还需要数字签名

数字签名

数字签名是使用私钥对摘要加密生成签名,需要由公钥将签名解密后进行验证,实现身份认证和不可否认。

签名和验证签名的流程

数字证书

数字证书解决“公钥的信任”问题,可以防止黑客伪造公钥。

不能直接分发公钥,公钥的分发必须使用数字证书,数字证书由CA颁发:


通过数字证书获取公钥进行验签流程:

https协议中的数字证书:


微信APIv3证书

商户证书

商户API证书是指由商户申请的,包含商户的商户号、公司名称、公钥信息的证书。

商户证书在商户后台申请:https://pay.weixin.qq.com/index.php/core/cert/api_cert#/

平台证书(微信支付平台):

微信支付平台证书是指由微信支付负责申请的,包含微信支付平台标识、公钥信息的证书。商户可以使用平台证书中的公钥进行验签。

平台证书的获取:https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay3_0.shtml

注意:API密钥和APIv3密钥都是对称加密需要使用的加密和解密密钥,一定要保管好,不能泄露。

  • API密钥对应V2版本的API

  • APIv3密钥对应V3版本的API


创建案例项目

初始化SpringBoot项目

IDEA->NEW Project->Spring Initializer

注意:Java版本选择8,Server URL可以用阿里云的: http://start.aliyun.com,SringBoot版本是2.3.7

添加依赖:

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

配置yaml:

1
2
3
4
5
6
7
# 服务端口
server:
port: 8090
# 应用名称
spring:
application:
name: Wechat-Payment

创建Controller:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package top.nanzx.learn_wechatpayment.controller;

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

@RestController
@RequestMapping("/api/product")
public class ProductController {

@GetMapping("/test")
public String test() {
return "Hello World";
}
}

测试访问:http://localhost:8090/api/product/test

引入Swagger

作用:自动生成接口文档和测试页面。

添加依赖:

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.7.0</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.7.0</version>
</dependency>

创建Swagger2的配置类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package top.nanzx.learn_wechatpayment.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

@Configuration
@EnableSwagger2
public class Swagger2Config {

@Bean
public Docket docket() {
Docket docket = new Docket(DocumentationType.SWAGGER_2);
docket.apiInfo(new ApiInfoBuilder().title("微信支付案例接口文档").build());
return docket;
}
}

使用Swagger常用注解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package top.nanzx.learn_wechatpayment.controller;

import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Api(tags = "商品管理")//用在类上
@RestController
@RequestMapping("/api/product")
public class ProductController {

@ApiOperation(value = "测试接口") //用在方法上
@GetMapping("/test")
public String test() {
return "Hello World";
}
}

测试访问:http://localhost:8090/swagger-ui.html

注意:spring-boot-starter-web版本号如果太高会跟swagger匹配不上,可以配置如下:

1
2
3
4
5
6
spring:
application:
name:payment
mvc:
pathmatch:
matching-strategy: ant_path_matcher

定义统一结果

作用:定义统一响应结果,为前端返回标准格式的数据。

引入lombok依赖,简化实体类的开发:

1
2
3
4
5
      <!--实体对象工具类:低版本idea需要安装lombok插件-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>

创建统一结果类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package top.nanzx.learn_wechatpayment.vo;

import lombok.Data;

import java.util.HashMap;
import java.util.Map;

@Data
@Accessors(chain = true)//链式操作,set方法的返回值从void变成R
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;
}
}

修改test方法,返回统一结果:

1
2
3
4
5
@ApiOperation(value = "测试接口")
@GetMapping("/test")
public R test() {
return new R().data("message", "Hello World").data("dataTime", new Date());
}

配置json时间格式:

1
2
3
4
5
6
7
8
9
10
# 服务端口
server:
port: 8090
# 应用名称
spring:
application:
name: Wechat-Payment
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: GMT+8

Swagger测试接口:http://localhost:8090/swagger-ui.html

新建数据库和表

在Mysql的查询窗口中执行以下命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
CREATE DATABASE wechat_payment;

USE `wechat_payment`;

/*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;

集成MyBatis-Plus

引入依赖:

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.25</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.3.1</version>
</dependency>

配置数据库连接:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 服务端口
server:
port: 8090
# 应用名称
spring:
application:
name: Wechat-Payment
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: GMT+8
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/wechat_payment?serverTimezone=GMT%2B8&characterEncoding=utf-8
username: root
password: 123456

定义实体类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package top.nanzx.learn_wechatpayment.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.Data;

import java.util.Date;

@Data
public class BaseEntity {

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

private Date createTime;//创建时间

private Date updateTime;//更新时间
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package top.nanzx.learn_wechatpayment.entity;

import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;

@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;//退款结果通知参数
}

定义Dao层

1
2
3
4
5
6
7
8
package top.nanzx.learn_wechatpayment.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import top.nanzx.learn_wechatpayment.entity.RefundInfo;

public interface RefundInfoMapper extends BaseMapper<RefundInfo> {

}
1
2
3
4
5
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="top.nanzx.learn_wechatpayment.mapper.RefundInfoMapper">

</mapper>

定义Service层

1
2
3
4
5
6
7
8
9
package top.nanzx.learn_wechatpayment.service;


import com.baomidou.mybatisplus.extension.service.IService;
import top.nanzx.learn_wechatpayment.entity.RefundInfo;

public interface RefundInfoService extends IService<RefundInfo> {

}
1
2
3
4
5
6
7
8
9
10
11
12
package top.nanzx.learn_wechatpayment.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
import top.nanzx.learn_wechatpayment.entity.RefundInfo;
import top.nanzx.learn_wechatpayment.mapper.RefundInfoMapper;
import top.nanzx.learn_wechatpayment.service.RefundInfoService;

@Service
public class RefundInfoServiceImpl extends ServiceImpl<RefundInfoMapper, RefundInfo> implements RefundInfoService {

}

配置Mybatis-Plus的config

1
2
3
4
5
6
7
8
9
10
11
12
package top.nanzx.learn_wechatpayment.config;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.annotation.EnableTransactionManagement;

@Configuration
@EnableTransactionManagement
@MapperScan("top.nanzx.learn_wechatpayment.mapper")
public class MybatisPlusConfig {
}


测试:http://localhost:8090/api/product/list

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package top.nanzx.learn_wechatpayment.controller;

import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import top.nanzx.learn_wechatpayment.entity.Product;
import top.nanzx.learn_wechatpayment.service.ProductService;
import top.nanzx.learn_wechatpayment.vo.R;

import java.util.Date;
import java.util.List;

@Api(tags = "商品管理")
@RestController
@RequestMapping("/api/product")
@CrossOrigin
public class ProductController {

@Autowired
private ProductService productService;

@ApiOperation(value = "测试接口")
@GetMapping("/test")
public R test() {
return new R().data("message", "Hello World").data("dataTime", new Date());
}

@GetMapping("/list")
public R list() {
List<Product> list = productService.list();
return new R().data("productList", list);
}
}

扩展

一般情况下,java目录下的xml文件不会被打包,所以target里会找不到xml文件,可以在pom.xml中的build标签中添加如下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<resources>
<!--打包src/main/java下面的属性文件和xml文件-->
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.properties</include>
<include>**/*.xml</include>
</includes>
<!-- 是否替换资源中的属性-->
<filtering>false</filtering>
</resource>
<!--打包src/main/resources目录下的属性文件和xml文件-->
<resource>
<directory>src/main/resources</directory>
<includes>
<include>**/*.properties</include>
<include>**/*.xml</include>
<include>**/*.yml</include>
</includes>
<filtering>true</filtering>
</resource>
</resources>

添加持久层日志和xml文件位置的配置:

1
2
3
4
mybatis-plus:
configuration: #sql日志
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
mapper-locations: classpath:top/nanzx/learn_wechatpayment/mapper/xml/*.xml

基础支付API v3

官网开发指引:https://pay.weixin.qq.com/wiki/doc/apiv3/open/pay/chapter2_7_2.shtml

APIv3证书与密钥使用说明

引入支付参数

将wxpay.properties 复制到resources目录中【参考1.2 接入指引 获取】:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 微信支付相关参数
# 商户号
wxpay.mch-id=1558950191
# 商户API证书序列号
wxpay.mch-serial-no=34345964330B66427E0D3D28826C4993C77E631F
# 商户私钥文件
wxpay.private-key-path=D:\\javaProjects\\MyLearn\\Learn_WechatPayment\\apiclient_key.pem
# 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

配置WxPayConfifig.java读取支付参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package top.nanzx.learn_wechatpayment.config;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;


@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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package top.nanzx.learn_wechatpayment.controller;

import io.swagger.annotations.Api;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import top.nanzx.learn_wechatpayment.config.WxPayConfig;
import top.nanzx.learn_wechatpayment.vo.R;

import javax.annotation.Resource;

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

@Resource
private WxPayConfig wxPayConfig;

@GetMapping("/get-wx-pay-config")
public R getWxPayConfig() {
String mchId = wxPayConfig.getMchId();
return R.ok().data("mchId", mchId);
}
}

扩展

  • 在IDEA中设置 SpringBoot 配置文件,让IDEA可以识别配置文件,将配置文件的图标展示成SpringBoot的图标,同时配置文件的内容可以高亮显示:File -> Project Structure -> Modules -> 展开对应项目文件夹 -> Spring -> 选择小叶子 -> 点击+号选择wxpay.properties

  • 配置 Annotation Processor可以帮助我们生成自定义配置的元数据信息,让配置文件和Java代码之间的对应参数可以自动定位(ctrl+鼠标左键),方便开发。

1
2
3
4
5
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>

加载商户私钥

将下载的私钥文件apiclient_key.pem复制到项目根目录下。

我们可以使用官方提供的 SDK,帮助我们完成开发。实现请求签名的生成和应答签名的验证。

SDK 就是 Software Development Kit 的缩写,中文意思就是“软件开发工具包”。这是一个覆盖面相当广泛的名词,可以这么说:辅助开发某一类软件的相关文档、范例和工具的集合都可以叫做“SDK”

微信官方Java SDK地址:https://github.com/wechatpay-apiv3/wechatpay-apache-httpclient

引入依赖:

1
2
3
4
5
<dependency>
<groupId>com.github.wechatpay-apiv3</groupId>
<artifactId>wechatpay-apache-httpclient</artifactId>
<version>0.4.0</version>
</dependency>

加载商户私钥 Link

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
@Configuration
@PropertySource("classpath:wxpay.properties") //读取配置文件
@ConfigurationProperties(pre fix = "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;

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

测试商户私钥的获取(将前面的方法改成public的再进行测试,测试私钥对象是否能够获取出来):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package top.nanzx.learn_wechatpayment;

import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import top.nanzx.learn_wechatpayment.config.WxPayConfig;

import javax.annotation.Resource;
import java.security.PrivateKey;

@SpringBootTest
class LearnWechatPaymentApplicationTests {

@Resource
private WxPayConfig wxPayConfig;

@Test
void contextLoads() {
}

@Test
void getPrivateKey() {
String privateKeyPath = wxPayConfig.getPrivateKeyPath();
PrivateKey privateKey = wxPayConfig.getPrivateKey(privateKeyPath);
System.out.println(privateKey);
}
}

注意:

  • 如果获取的私钥只有一行短短的字符串,可以更换更高版本的jdk,运行时看第一句是在哪个jdk目录下
  • 如果后面启动 项目报Caused by: java.io.FileNotFoundException: apiclient_key.pem (系统找不到指定的文件。),建议将wxpay.properties的私钥路径换成绝对路径

获取签名验证器和HttpClient

定时更新平台证书功能 Link

签名验证器:SDK版本>=0.4.0可使用 CertificatesManager.getVerifier(mchId) 得到的验签器替代默认的验签器。它可以帮助我们进行签名和验签工作,同时也会定时下载和更新商户对应的微信支付平台证书 (默认下载间隔为UPDATE_INTERVAL_MINUTE)。我们单独将它定义出来,方便后面的开发。

HttpClient 对象:是建立远程连接的基础,我们通过SDK中配置了验签器(Verifier)的WechatPayHttpClientBuilder构造的HttpClient,会自动的处理签名和验签,并进行证书自动更新。

视频中老师使用的是0.3.0的ScheduledUpdateCertificatesVerifier类替代默认的验签器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
package top.nanzx.learn_wechatpayment.config;

import com.wechat.pay.contrib.apache.httpclient.WechatPayHttpClientBuilder;
...

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

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

/**
* 获取签名验证器
* @return
* @throws GeneralSecurityException
* @throws IOException
* @throws HttpCodeException
* @throws NotFoundException
*/
@Bean(name = "verifer")
public Verifier getVerifier() throws GeneralSecurityException, IOException, HttpCodeException, NotFoundException {
// 获取证书管理器实例
CertificatesManager certificatesManager = CertificatesManager.getInstance();
//获取商户私钥
PrivateKey privateKey = getPrivateKey(privateKeyPath);
//私钥签名对象(签名)
PrivateKeySigner privateKeySigner = new PrivateKeySigner(mchSerialNo, privateKey);
//身份认证对象(验签)
WechatPay2Credentials wechatPay2Credentials = new WechatPay2Credentials(mchId, privateKeySigner);
// 向证书管理器增加需要自动更新平台证书的商户信息
certificatesManager.putMerchant(mchId, wechatPay2Credentials, apiV3Key.getBytes(StandardCharsets.UTF_8));
// ... 若有多个商户号,可继续调用putMerchant添加商户信息

// 从证书管理器中获取verifier
return certificatesManager.getVerifier(mchId);
}

/**
* 获取HttpClient对象
* @return
* @throws GeneralSecurityException
* @throws NotFoundException
* @throws IOException
* @throws HttpCodeException
*/
@Bean(name = "wxPayClient")
public CloseableHttpClient getWxPayClient() throws GeneralSecurityException, NotFoundException, IOException, HttpCodeException {
PrivateKey privateKey = getPrivateKey(privateKeyPath);
Verifier verifier = getVerifier();

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

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

API字典和相关工具

Native支付模块

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

微信支付 APIv3 使用 JSON 作为消息体的数据交换格式,引入gson依赖处理json数据:

1
2
3
4
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
</dependency>

定义枚举

内容包括接口地址,支付状态等信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
package top.nanzx.learn_wechatpayment.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;
}

添加工具类

简化项目的开发:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package top.nanzx.learn_wechatpayment.util;

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


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();
}
}
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
package top.nanzx.learn_wechatpayment.util;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Random;

/**
* 订单号工具类
*
* @author qy
* @since 1.0
*/
public class OrderNoUtils {

/**
* 获取订单编号
* @return
*/
public static String getOrderNo() {
return "ORDER_" + getNo();
}

/**
* 获取退款单编号
* @return
*/
public static String getRefundNo() {
return "REFUND_" + getNo();
}

/**
* 获取编号
* @return
*/
public static String getNo() {
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss");
String newDate = sdf.format(new Date());
String result = "";
Random random = new Random();
for (int i = 0; i < 3; i++) {
result += random.nextInt(10);
}
return newDate + result;
}
}

业务流程时序图

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

生成订单

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
package top.nanzx.learn_wechatpayment.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
...

import javax.annotation.Resource;

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

@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;
}

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

/**
* 存储订单二维码
*
* @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);
}
}

Native下单API

官方Api指引:https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_1.shtml

定义Controller:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package top.nanzx.learn_wechatpayment.controller;

import io.swagger.annotations.Api;
...

@CrossOrigin
@RestController
@RequestMapping("api/wx-pay")
@Api(tags = "网站的微信支付API")
@Slf4j
public class WxPayController {

@Autowired
private WxPayService wxPayService;

@RequestMapping("/native/{productId}")
@ApiOperation("调用统一下单API,生成支付二维码")
public R nativePay(@PathVariable Long productId) throws Exception {
log.info("发起支付请求");

//返回订单编号以及支付二维码
Map<String, Object> map = wxPayService.nativePay(productId);

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

定义Service:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
package top.nanzx.learn_wechatpayment.service.impl;

import com.google.gson.Gson;
...

@Service
@Slf4j
public class WxPayServiceImpl implements WxPayService {

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

/**
* 创建订单,调用Native支付接口
*
* @param productId
* @return code_url, orderNo
* @throws Exception
*/
@Override
public Map<String, Object> nativePay(Long productId) throws Exception {
log.info("生成订单"); //生成订单

OrderInfo orderInfo = orderInfoService.createOrderByProductId(productId);
String codeUrl = orderInfo.getCodeUrl();
if (!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

Gson gson = new Gson();
Map<String, Object> 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<String, Object> 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);

HttpPost httpPost = new HttpPost(wxPayConfig.getDomain().concat(WxApiType.NATIVE_PAY.getType()));
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();
String bodyAsString = EntityUtils.toString(response.getEntity());
if (statusCode == 200) { //处理成功
log.info("成功, 返回结果 = " + bodyAsString);
} else if (statusCode == 204) { //处理成功,无返回Body
log.info("成功");
} else {
log.info("失败, 响应状态码 = " + statusCode + ",返回结果 = " + bodyAsString);
throw new IOException("请求失败");
}
//响应结果
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;
}
}
}

显示订单列表

定义Controller:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package top.nanzx.learn_wechatpayment.controller;

import io.swagger.annotations.Api;
...

@Api(tags = "商品订单管理")
@RestController
@RequestMapping("/api/order-info")
@CrossOrigin
public class OrderInfoController {

@Autowired
private OrderInfoService orderInfoService;

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

定义Service:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package top.nanzx.learn_wechatpayment.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
...

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

@Resource
private ProductMapper productMapper;

...

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

支付通知API

官方Api指引:https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_5.shtml

内网穿透

  1. 访问ngrok的官网:https://ngrok.com/
  2. 注册账号并登录,也可以用Github账号登录
  3. 下载ngrok工具
  4. 设置 authToken,为本地计算机做授权配置,每个人令牌不同:ngrok authtoken 6aYc6Kp7kpxVr8pY88LkG_6x9o18yMY8BASrXiDFMeS
  5. 启动服务:ngrok http 8090
  6. 测试外网访问:你获得的外网地址/api/product/test

接收通知和返回应答

  1. 启动ngrok服务:ngrok http 8090

  2. 在wxpay.properties设置通知地址(注意:每次重新启动ngrok,都需要根据实际情况修改这个配置):wxpay.notify-domain=https://7d92-115-171-63-135.ngrok.io

  3. 创建通知接口

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    package top.nanzx.learn_wechatpayment.controller;

    import com.google.gson.Gson;
    ...

    @CrossOrigin
    @RestController
    @RequestMapping("api/wx-pay")
    @Api(tags = "网站的微信支付API")
    @Slf4j
    public class WxPayController {

    @Autowired
    private WxPayService wxPayService;

    @ApiOperation("支付通知")
    @PostMapping("/native/notify")
    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);
    log.info("支付通知的完整数据 ===> {}", body);
    // TODO : 签名的验证
    // TODO : 处理订单
    // 成功应答:成功应答必须为200或204,否则就是失败应答
    response.setStatus(200);
    map.put("code", "SUCCESS");
    map.put("message", "成功");
    } catch (Exception e) {
    e.printStackTrace();
    response.setStatus(500);
    map.put("code", "ERROR");
    map.put("message", "系统错误");
    return gson.toJson(map);
    }
    return gson.toJson(map);
    }
    }

验签

https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay4_1.shtml

商户系统对于开启结果通知的内容一定要做签名验证,并校验通知的信息是否与商户侧的信息一致,防止数据泄露导致出现“假通知”,造成资金损失。

  • 如果验证商户的请求签名正确,微信支付会在应答的HTTP头部中包括应答签名。微信建议商户验证应答签名。(注解:这里的应答对应图中的第二步,由于使用微信SDK中httpClient发送的请求,所以会自动对响应进行验签)

    1
    2
    3
    4
    CloseableHttpClient httpClient= WechatPayHttpClientBuilder.create()
    .withMerchant(mchId, mchSerialNo, privateKey)//签名
    .withValidator(new WechatPay2Validator(verifier))//验签
    .build();
  • 同样的,微信支付会在回调的HTTP头部中包括回调报文的签名。商户必须验证回调的签名,以确保回调是由微信支付发送。(注解:这里的应答对应图中的第三步,并没有通过httpclient发送请求,所以不会自动验签)我们可以参考SDK源码中的 WechatPay2Validator 创建通知验签工具类 WechatPay2ValidatorForRequest:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    package top.nanzx.learn_wechatpayment.util;


    import com.wechat.pay.contrib.apache.httpclient.auth.Verifier;
    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 body;
    protected final String requestId;

    public WechatPay2ValidatorForRequest(Verifier verifier, String body, String requestId) {
    this.verifier = verifier;
    this.body = body;
    this.requestId = requestId;
    }

    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, request.getHeader(REQUEST_ID));
    }
    } 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";
    }
    }

    通知接口添加验签:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    package top.nanzx.learn_wechatpayment.controller;

    import com.google.gson.Gson;
    ...

    @CrossOrigin
    @RestController
    @RequestMapping("api/wx-pay")
    @Api(tags = "网站的微信支付API")
    @Slf4j
    public class WxPayController {

    @Autowired
    private WxPayService wxPayService;
    @Resource
    private Verifier verifer;

    @RequestMapping("/native/{productId}")
    @ApiOperation("调用统一下单API,生成支付二维码")
    public R nativePay(@PathVariable Long productId) throws Exception {
    log.info("发起支付请求");

    //返回订单编号以及支付二维码
    Map<String, Object> map = wxPayService.nativePay(productId);

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

    @ApiOperation("支付通知")
    @PostMapping("/native/notify")
    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);
    log.info("支付通知的完整数据 ===> {}", body);

    //签名的验证
    WechatPay2ValidatorForRequest wechatPay2ValidatorForRequest
    = new WechatPay2ValidatorForRequest(verifer, body, requestId);
    if (!wechatPay2ValidatorForRequest.validate(request)) {
    log.error("通知验签失败");
    //失败应答
    response.setStatus(500);
    map.put("code", "ERROR");
    map.put("message", "系统错误");
    return gson.toJson(map);
    }
    // TODO : 处理订单
    // 成功应答:成功应答必须为200或204,否则就是失败应答
    response.setStatus(200);
    map.put("code", "SUCCESS");
    map.put("message", "成功");
    } catch (Exception e) {
    e.printStackTrace();
    response.setStatus(500);
    map.put("code", "ERROR");
    map.put("message", "系统错误");
    return gson.toJson(map);
    }
    return gson.toJson(map);
    }
    }

    参数解密

https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay4_2.shtml

下面详细描述对通知数据进行解密的流程:

  1. 用商户平台上设置的APIv3密钥【微信商户平台—>账户设置—>API安全—>设置APIv3密钥】,记为key;
  2. 针对resource.algorithm中描述的算法(目前为AEAD_AES_256_GCM),取得对应的参数nonce和associated_data;
  3. 使用key、nonce和associated_data,对数据密文resource.ciphertext进行解密,得到JSON形式的资源对象;

我们可以通过参考AesUtil.Java对证书和回调解密。

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

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

实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
package top.nanzx.learn_wechatpayment.service.impl;

import com.google.gson.Gson;
...

@Service
@Slf4j
public class WxPayServiceImpl implements WxPayService {

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

...

@Override
public void processOrder(Map<String, Object> bodyMap) throws GeneralSecurityException {
log.info("处理订单");
//转换明文
String plainText = decryptFromResource(bodyMap);
//更新订单状态
//记录支付日志
}

/**
* 使用Api v3密钥进行对称解密
*
* @param bodyMap
* @return
* @throws GeneralSecurityException
*/
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;
}
}

更新订单状态和记录支付日志

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package top.nanzx.learn_wechatpayment.service.impl;

import com.google.gson.Gson;
...

@Service
@Slf4j
public class WxPayServiceImpl implements WxPayService {

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

...

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

更新订单状态:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package top.nanzx.learn_wechatpayment.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
...

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

@Resource
private ProductMapper productMapper;

...

/**
* 更新订单状态
*
* @param orderNo
* @param orderStatus
*/
@Override
public void updateStatusByOrderNo(String orderNo, OrderStatus orderStatus) {
log.info("更新订单状态 ===> {}", orderStatus.getType());
QueryWrapper<OrderInfo> wrapper = new QueryWrapper<>();
wrapper.eq("order_no", orderNo);

OrderInfo orderInfo = new OrderInfo();
orderInfo.setOrderStatus(orderStatus.getType());
baseMapper.update(orderInfo, wrapper);
}
}

记录支付日志:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package top.nanzx.learn_wechatpayment.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
...

@Service
@Slf4j
public class PaymentInfoServiceImpl extends ServiceImpl<PaymentInfoMapper, PaymentInfo> implements PaymentInfoService {

/**
* 记录支付日志
*
* @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);
}
}

接口幂等性

微信支付通过支付通知接口将用户支付成功消息通知给商户

注意:同样的通知可能会多次发送给商户系统。商户系统必须能够正确处理重复的通知。 推荐的做法是,当商户系统收到通知进行处理时,先检查对应业务数据的状态,并判断该通知是否已经处理。如果未处理,则再进行处理;如果已处理,则直接返回结果成功。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@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");

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

// 更新订单状态
orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.SUCCESS);
// 记录支付日志
paymentInfoService.createPaymentInfo(plainText);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 根据订单号获取订单状态
*
* @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();
}

数据锁

在对业务数据进行状态检查和处理之前,要采用数据锁进行并发控制,以避免函数重入造成的数据混乱。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
package top.nanzx.learn_wechatpayment.service.impl;

import com.google.gson.Gson;
...

@Service
@Slf4j
public class WxPayServiceImpl implements WxPayService {

...
private final ReentrantLock lock = new ReentrantLock();

...

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

// 尝试获取锁:
// 成功获取则立即返回true,获取失败则立即返回false。不必一直等待锁的释放
if(lock.tryLock()) {
try {
//处理重复通知,保证接口调用的幂等性:无论接口被调用多少次,产生的结果是一致的
String orderStatus = orderInfoService.getOrderStatus(orderNo);
if (!OrderStatus.NOTPAY.getType().equals(orderStatus)) {
return;
}

// 更新订单状态
orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.SUCCESS);
// 记录支付日志
paymentInfoService.createPaymentInfo(plainText);
} finally {
lock.unlock();
}
}
}
}

查单接口

后端定义商户查单接口(支付成功后,商户侧查询本地数据库,订单是否支付成功):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package top.nanzx.learn_wechatpayment.controller;

import io.swagger.annotations.Api;
...

@Api(tags = "商品订单管理")
@RestController
@RequestMapping("/api/order-info")
@CrossOrigin
public class OrderInfoController {

@Autowired
private OrderInfoService orderInfoService;

...

@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().setMessage("支付成功");
}
return R.ok().setCode(101).setMessage("支付中...");
}
}

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

  • 定义定时器

    • //启动定时器 
      this.timer = setInterval(() => { 
          //查询订单是否支付成功 
          this.queryOrderStatus() 
      }, 3000)
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19

      - 查询订单

      - ```javascript
      // 查询订单状态
      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)
      }
      })
      }

关闭订单API

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

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

WxPayController:

1
2
3
4
5
6
7
@ApiOperation("用户取消订单")
@PostMapping("/cancel/{orderNo}")
public R cancel(@PathVariable String orderNo) throws Exception {
log.info("取消订单");
wxPayService.cancelOrder(orderNo);
return R.ok().setMessage("订单已取消");
}

WxPayServiceImpl:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
@Override
public void cancelOrder(String orderNo) throws Exception {
//调用微信支付的关单接口
this.closeOrder(orderNo);
//更新商户端的订单状态
orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.CANCEL);
}

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

查询订单API

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

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

WxPayController:

1
2
3
4
5
6
7
@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);
}

WxPayServiceImpl:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@Override
public String queryOrder(String orderNo) throws Exception {
log.info("查单接口调用 ===> {}", orderNo);
String url = String.format(WxApiType.ORDER_QUERY_BY_NO.getType(), orderNo);
url = wxPayConfig.getDomain().concat(url).concat("? mchid=").concat(wxPayConfig.getMchId());
HttpGet httpGet = new HttpGet(url);
httpGet.setHeader("Accept", "application/json");
//完成签名并执行请求
CloseableHttpResponse response = wxPayClient.execute(httpGet);
try {
String bodyAsString = EntityUtils.toString(response.getEntity());
//响应体
int statusCode = response.getStatusLine().getStatusCode();
//响应状态码
if (statusCode == 200) {
//处理成功
log.info("成功, 返回结果 = " + bodyAsString);
} else if (statusCode == 204) {
//处理成功,无返回Body
log.info("成功");
} else {
log.info("Native查单失败,响应码 = " + statusCode + ",返回结果 = " + bodyAsString);
throw new IOException("request failed");
}
return bodyAsString;
} finally {
response.close();
}
}

集成Spring Task

Spring 3.0后提供Spring Task实现任务调度,启动类添加注解:@EnableScheduling

在线Cron表达式生成器:https://cron.qqe2.com/,其时间有**分、时、日、月、周**五种,操作符有

  • ***** 取值范围内的所有数字
  • / 每过多少个数字
  • - 从X到Z
  • 散列数字

定时查找超时订单

WxPayTask:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package top.nanzx.learn_wechatpayment.task;

import lombok.extern.slf4j.Slf4j;
...

@Slf4j
public class WxPayTask {
@Autowired
private OrderInfoService orderInfoService;
@Autowired
private WxPayService wxPayService;

/**
* 从第0秒开始每隔30秒执行1次,查询创建超过5分钟,并且未支付的订单
*
* @throws Exception
*/
@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);
}
}
}

OrderInfoServiceImpl:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 找出创建超过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);
return baseMapper.selectList(queryWrapper);
}

处理超时订单

WxPayServiceImpl:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/**
* 根据订单号查询微信支付查单接口,核实订单状态
* 如果订单已支付,则更新商户端订单状态,并记录支付日志
* 如果订单未支付,则调用关单接口关闭订单,并更新商户端订单状态
*
* @param orderNo
* @throws Exception
*/
@Override
public void checkOrderStatus(String orderNo) throws Exception {
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);
}
}

其他API类似,参考官方文档即可。