一、微信支付介绍和接入指引
1、微信支付产品介绍
1.1、付款码支付
用户展示微信钱包内的
“
付款码
”
给商家,商家扫描后直接完成支付,适用于线下面对面收银的场景。
1.2、JSAPI支付
- 线下场所:商户展示一个支付二维码,用户使用微信扫描二维码后,输入需要支付的金额,完成支付。
- 公众号场景:用户在微信内进入商家公众号,打开某个页面,选择某个产品,完成支付。
- PC网站场景:在网站中展示二维码,用户使用微信扫描二维码,输入需要支付的金额,完成支付。
特点:用户在客户端输入支付金额
1.3、小程序支付
在微信小程序平台内实现支付的功能。
1.4、Native支付
Native
支付是指商户展示支付二维码,用户再用微信
“
扫一扫
”
完成支付的模式。这种方式适用于
PC
网站。
特点:商家预先指定支付金额
1.5、APP支付
商户通过在移动端独立的
APP
应用程序中集成微信支付模块,完成支付。
1.6、刷脸支付
用户在刷脸设备前通过摄像头刷脸、识别身份后进行的一种支付方式。
2、接入指引
2.1、获取商户号
微信商户平台:
https://pay.weixin.qq.com/
场景:
Native
支付
步骤:提交资料
=>
签署协议
=>
获取商户号
2.2、获取APPID
微信公众平台:
https://mp.weixin.qq.com/
步骤:注册服务号
=>
服务号认证
=>
获取
APPID =>
绑定商户号
2.3、获取API秘钥
APIv2
版本的接口需要此秘钥
步骤:登录商户平台
=>
选择 账户中心
=>
安全中心
=> API
安全
=>
设置
API
密钥
2.4、获取APIv3秘钥
APIv3
版本的接口需要此秘钥
步骤:登录商户平台
=>
选择 账户中心
=>
安全中心
=> API
安全
=>
设置
APIv3
密钥
随机密码生成工具:
https://suijimimashengcheng.bmcx.com/
2.5、申请商户API证书
APIv3
版本的所有接口都需要;
APIv2
版本的高级接口需要(如:退款、企业红包、企业付款等)
步骤:登录商户平台
=>
选择 账户中心
=>
安全中心
=> API
安全
=>
申请
API
证书
2.6、获取微信平台证书
可以预先下载,也可以通过编程的方式获取。后面的课程中,我们会通过编程的方式来获取。
注意:以上所有
API
秘钥和证书需妥善保管防止泄露
二、支付安全(证书/秘钥/签名)
1、信息安全的基础 - 机密性
- 明文:加密前的消息叫“明文”(plain text)
- 密文:加密后的文本叫“密文”(cipher text)
- 密钥:只有掌握特殊“钥匙”的人,才能对加密的文本进行解密,这里的“钥匙”就叫做“密钥”(key)
“
密钥
”
就是一个字符串,度量单位是
“
位
”
(
bit
),比如,密钥长度是
128
,就是
16
字节的二
进制串
- 加密:实现机密性最常用的手段是“加密”(encrypt)
按照密钥的使用方式,加密可以分为两大类:
对称加密和非对称加密
。
- 解密:使用密钥还原明文的过程叫“解密”(decrypt)
- 加密算法:加密解密的操作过程就是“加密算法”
所有的加密算法都是公开的,而算法使用的
“
密钥
”
则必须保密
2
、对称加密和非对称加密
- 对称加密
- 特点:只使用一个密钥,密钥必须保密,常用的有 AES算法
- 优点:运算速度快
- 缺点:秘钥需要信息交换的双方共享,一旦被窃取,消息会被破解,无法做到安全的密钥交换
- 非对称加密
- 特点:使用两个密钥:公钥和私钥,公钥可以任意分发而私钥保密,常用的有 RSA
- 优点:黑客获取公钥无法破解密文,解决了密钥交换的问题
- 缺点:运算速度非常慢
- 混合加密
实际场景中把对称加密和非对称加密结合起来使用。
3、身份认证
公钥加密,私钥解密的作用是加密信息
私钥加密,公钥解密的作用是身份认证
4、摘要算法(Digest Algorithm)
摘要算法就是我们常说的散列函数、哈希函数(
Hash Function
),它能够把任意长度的数据
“
压缩
”
成固定长度、而且独一无二的“
摘要
”
字符串,就好像是给这段数据生成了一个数字
“
指纹
”
。
作用
:
保证信息的完整性
特性
:
- 不可逆:只有算法,没有秘钥,只能加密,不能解密
- 难题友好性:想要破解,只能暴力枚举
- 发散性:只要对原文进行一点点改动,摘要就会发生剧烈变化
- 抗碰撞性:原文不同,计算后的摘要也要不同
常见摘要算法
:
MD5
、
SHA1
、
SHA2
(
SHA224
、
SHA256
、
SHA384
)
5、数字签名
数字签名是使用私钥对摘要加密生成签名,需要由公钥将签名解密后进行验证,实现身份认证和不可否认
签名和验证签名的流程
:
6、数字证书
数字证书解决
“
公钥的信任
”
问题,可以防止黑客伪造公钥。
不能直接分发公钥,公钥的分发必须使用数字证书,数字证书由
CA
颁发
https
协议中的数字证书:
7、微信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
8、API密钥和APIv3密钥
都是对称加密需要使用的加密和解密密钥,一定要保管好,不能泄露。
API
密钥对应
V2
版本的
API
APIv3
密钥对应
V3
版本的
API
三、案例项目的创建
1、创建SpringBoot项目
1.1、新建项目
注意:
Java
版本选择
8
1.2、添加依赖
添加
SpringBoot web
依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
1.3、配置application.yml文件
server:
port: 8090 #服务端口
spring:
application:
name: payment-demo # 应用名称
1.4、创建controller
创建
controller
包,创建
ProductController
类
package com.atguigu.paymentdemo.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")
@CrossOrigin //跨域
public class ProductController {
@GetMapping("/test")
public String test(){
return "hello";
}
}
1.5、测试
访问:
http://localhost:8090/api/product/test
2、引入Swagger
作用:自动生成接口文档和测试页面。
2.1、引入依赖
<!--swagger-->
<dependency><groupId> io.springfox </groupId><artifactId> springfox-swagger2 </artifactId><version> 2.7.0 </version></dependency><!--swagger ui--><dependency><groupId> io.springfox </groupId><artifactId> springfox-swagger-ui </artifactId><version> 2.7.0 </version></dependency>
2.2、Swagger配置文件
创建
confifig
包,创建
Swagger2Confifig
类
package com.atguigu.paymentdemo.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(){
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(new ApiInfoBuilder().title("微信支付案例接口文档").build());
}
}
2.3、Swagger注解
controller
中可以添加常用注解
@Api ( tags = " 商品管理 " ) // 用在类上
@ApiOperation ( " 测试接口 " ) // 用在方法上
2.4、测试
访问:
http://localhost:8090/swagger-ui.html
3、定义统一结果
作用:定义统一响应结果,为前端返回标准格式的数据。
3.1、引入lombok依赖
<!-- 实体对象工具类:低版本 idea 需要安装 lombok 插件 --><dependency><groupId> org.projectlombok </groupId><artifactId> lombok </artifactId></dependency>
3.2、创建R类
创建统一结果类
package com.atguigu.paymentdemo.vo;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.util.HashMap;
import java.util.Map;
@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;
}
}
3.3、修改controller
修改
test
方法,返回统一结果
@ApiOperation("测试接口")
@GetMapping("/test")
public R test(){
return R
.ok()
.data("message", "hello")
.data("now", new Date());
}
3.4、配置json时间格式
spring:
jackson : #json 时间格式date-format : yyyy-MM-dd HH : mm : sstime-zone : GMT+8
3.5、Swagger测试
4、创建数据库
4.1、创建数据库
mysql - uroot - pmysql > create database payment_demo;
4.2、IDEA配置数据库连接
(1)打开数据库面板
2
)添加数据库
(3)配置数据库连接参数
4.3、执行SQL脚本
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;
5、集成MyBatis-Plus
5.1、引入依赖
<!--mysql驱动-->
<dependency><groupId> mysql </groupId><artifactId> mysql-connector-java </artifactId></dependency><!-- 持久层 --><dependency><groupId> com.baomidou </groupId><artifactId> mybatis-plus-boot-starter </artifactId><version> 3.3.1 </version></dependency>
5.2、配置数据库连接
spring:
datasource : #mysql 数据库连接driver-class-name : com.mysql.cj.jdbc.Driverurl : jdbc : mysql : //localhost : 3306/payment_demo? serverTimezone=GMT%2B8&characterEncoding=utf-8username : rootpassword : 123456
5.3、定义实体类
BaseEntity
是父类,其他类继承
BaseEntity
BaseEntity
package com.atguigu.paymentdemo.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;
}
Product
package com.atguigu.paymentdemo.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
@Data
@TableName("t_product")
public class Product extends BaseEntity{
//标题
private String title;
//价格(分)
private Integer price;
}
Order
package com.atguigu.paymentdemo.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
@TableName("t_order_info")
@Data
public class OrderInfo extends BaseEntity{
//标题
private String title;
//商户订单编号
private String orderNo;
//用户id
private Long userId;
//支付产品Id
private Long productId;
//订单金额(分)
private Integer totalFee;
//订单二维码链接
private String codeUrl;
//订单状态
private String orderStatus;
}
PaymentInfo
package com.atguigu.paymentdemo.entity;
import com.baomidou.mybatisplus.annotation.TableName;
@TableName("t_payment_info")
public class PaymentInfo extends BaseEntity{
//商品订单编号
private String orderNo;
//支付系统交易编号
private String transactionId;
//支付类型
private String paymentType;
//交易类型
private String tradeType;
//交易状态
private String tradeStatus;
//支付金额
private Integer payerTotal;
//通知参数
private String content;
}
RefundInfo
package com.atguigu.paymentdemo.entity;
import com.baomidou.mybatisplus.annotation.TableName;
@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;
}
5.4、定义持久层
定义
Mapper
接口继承
BaseMapper<>
,
定义
xml
配置文件
5.5、定义MyBatis-Plus的配置文件
在
confifig
包中创建配置文件
MybatisPlusConfifig
package com.atguigu.paymentdemo.config;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.annotation.EnableTransactionManagement;
@Configuration
@MapperScan("com.atguigu.paymentdemo.mapper")//扫描持久层
@EnableTransactionManagement//开启事务管理
public class MybatisPlusConfig {
}
5.6、定义yml配置文件
添加持久层日志和
xml
文件位置的配置
mybatis-plus :configuration : #sql 日志log-impl : org.apache.ibatis.logging.stdout.StdOutImplmapper-locations : classpath : com/atguigu/paymentdemo/mapper/xml/*.xml
5.7、定义业务层
定义业务层接口继承
IService<>
ProductService
package com.atguigu.paymentdemo.service;
import com.atguigu.paymentdemo.entity.Product;
import com.baomidou.mybatisplus.extension.service.IService;
public interface ProductService extends IService<Product> {
}
OrderInfoService
package com.atguigu.paymentdemo.service;
import com.atguigu.paymentdemo.entity.OrderInfo;
import com.baomidou.mybatisplus.extension.service.IService;
public interface OrderInfoService extends IService<OrderInfo> {
}
PaymentInfoService
package com.atguigu.paymentdemo.service;
import com.atguigu.paymentdemo.entity.PaymentInfo;
import com.baomidou.mybatisplus.extension.service.IService;
public interface PaymentInfoService extends IService<PaymentInfo> {
}
RefundInfoService
package com.atguigu.paymentdemo.service;
import com.atguigu.paymentdemo.entity.RefundInfo;
import com.baomidou.mybatisplus.extension.service.IService;
public interface RefundInfoService extends IService<RefundInfo> {
}
定义业务层接口的实现类,并继承
ServiceImpl<,>
5.8、定义接口方法查询所有商品
在
public class ProductController
中添加一个方法
@Resource
private ProductService productService;
@ApiOperation("商品列表")
@GetMapping("/list")
public R test() {
List<Product> list = productService.list();
return R.ok().data("productList",list);
}
5.9、Swagger中测试
5.10、pom中配置build节点
因为
maven
工程在默认情况下
src/main/java
目录下的所有资源文件是不发布到
target
目录下的,我们在 pom
文件的 节点下配置一个资源发布过滤器
<!-- 项目打包时会将 java 目录中的 *.xml 文件也进行打包 --><resources><resource><directory> src/main/java </directory><includes><include> **/*.xml </include></includes><filtering> false </filtering></resource></resources>
6、搭建前端环境
6.1、安装Node.js
Node.js
是一个基于
JavaScript
引擎的服务器端环境,前端项目在开发环境下要基于
Node.js
来运行
安装:
node-v14.18.0-x64.msi
6.2、运行前端项目
将项目放在磁盘的一个目录中,例如
D:\demo\payment-demo-front
进入项目目录,运行下面的命令启动项目:
npm run serve
6.3、安装VSCode
如果你希望方便的查看和修改前端代码,可以安装一个
VSCode
安装:
VSCodeUserSetup-x64-1.56.2
安装插件:
7、Vue.js入门
官网:
https://cn.vuejs.org/
Vue.js
是一个前端框架,帮助我们快速构建前端项目。
使用
vue
有两种方式,一个是传统的在
html
文件中引入
js
脚本文件的方式,另一个是脚手架的方式。 我们的项目,使用的是脚手架的方式。
7.1、安装脚手架
配置淘宝镜像
# 经过下面的配置,所有的 npm install 都会经过淘宝的镜像地址下载npm config set registry https :// registry.npm.taobao.org
全局安装脚手架
npm install - g @vue / cli
7.2、创建一个项目
先进入项目目录(
Ctrl + ~
),然后创建一个项目
vue create vue-demo
7.3、运行项目
npm run serve
指定运行端口
npm run serve -- -- port 8888
7.4、数据绑定
修改
src/App.vue
<!-- 定义页面结构 --><template><div><h1> Vue 案例 </h1><!-- 插值 --><p> {{course}} </p></div></template><!-- 定义页面脚本 --><script>export default {// 定义数据data () {return {course : ' 微信支付 '}}}</script>
7.5、安装Vue调试工具
在
Chrome
的扩展程序中安装:
Vue.jsDevtools.zip
(
1
)扩展程序的安装
(
2
)扩展程序的使用
7.6、双向数据绑定
数据会绑定到组件,组件的改变也会影响数据定义
<p><!-- 指令 --><input type = "text" v-model = "course" ></p>
7.7、事件处理
(1)定义事件
// 定义方法methods : {toPay (){console . log ( ' 去支付 ' )}}
(
2
)调用事件
<p><!-- 事件 --><button @click = "toPay()" > 去支付 </button></p>
四、基础支付API V3
1、引入支付参数
1.1、定义微信支付相关参数
将资料文件夹中的
wxpay.properties
复制到
resources
目录中
这个文件定义了之前我们准备的微信支付相关的参数,例如商户号、
APPID
、
API
秘钥等等
1.2、读取支付参数
将资料文件夹中的
confifig
目录中的
WxPayConfifig.java
复制到源码目录中。
1.3、测试支付参数的获取
在 controller 包中创建 TestController
package com.atguigu.paymentdemo.controller;
import com.atguigu.paymentdemo.config.WxPayConfig;
import com.atguigu.paymentdemo.vo.R;
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 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);
}
}
1.4、配置 Annotation Processor
可以帮助我们生成自定义配置的元数据信息,让配置文件和
Java
代码之间的对应参数可以自动定位,方便开发。
<dependency><groupId> org.springframework.boot </groupId><artifactId> spring-boot-configuration-processor </artifactId><optional> true </optional></dependency>
1.5、在IDEA中设置 SpringBoot 配置文件
让
IDEA
可以识别配置文件,将配置文件的图标展示成
SpringBoot
的图标,同时配置文件的内容可以高亮显示
File -> Project Structure -> Modules ->
选择小叶子
点击(+) 图标
选中配置文件:
2、加载商户私钥
2.1、复制商户私钥
将下载的私钥文件复制到项目根目录下:
2.2、引入SDK
https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay6_0.shtml
我们可以使用官方提供的
SDK
,帮助我们完成开发。实现了请求签名的生成和应答签名的验证。
<dependency><groupId> com.github.wechatpay-apiv3 </groupId><artifactId> wechatpay-apache-httpclient </artifactId><version> 0.3.0 </version></dependency>
2.3、获取商户私钥
https://github.com/wechatpay-apiv3/wechatpay-apache-httpclient
(如何加载商户私钥)
/**
* 获取商户的私钥文件
* @param fileName
* @return
*/
private PrivateKey getPrivateKey(String fileName){
System.out.println("fileName = " + fileName);
// 加载商户私钥(privateKey:私钥字符串)
try {
return PemUtil.loadPrivateKey(new FileInputStream(fileName));
} catch (Exception e) {
throw new RuntimeException("私钥文件不存在",e);
}
}
2.4、测试商户私钥的获取
在
PaymentDemoApplicationTests
测试类中添加如下方法,测试私钥对象是否能够获取出来。
(将前面的方法改成
public
的再进行测试)
package com.atguigu.paymentdemo;
import com.atguigu.paymentdemo.config.WxPayConfig;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import javax.annotation.Resource;
import java.security.PrivateKey;
@SpringBootTest
class PaymentDemoApplicationTests {
@Resource
private WxPayConfig wxPayConfig;
/**
* 获取商户私钥
*/
@Test
public void testGetPrivateKey(){
//获取私钥路径
String privateKeyPath = wxPayConfig.getPrivateKeyPath();
//获取商户私钥
PrivateKey privateKey = wxPayConfig.getPrivateKey(privateKeyPath);
System.out.println(privateKey);
}
}
3、获取签名验证器和HttpClient
3.1、证书密钥使用说明
https://pay.weixin.qq.com/wiki/doc/apiv3_partner/wechatpay/wechatpay3_0.shtml
3.2、获取签名验证器
https://github.com/wechatpay-apiv3/wechatpay-apache-httpclient
(定时更新平台证书功能)
平台证书:平台证书封装了微信的公钥,商户可以使用平台证书中的公钥进行验签。
签名验证器:帮助我们进行验签工作,我们单独将它定义出来,方便后面的开发。
/**
* 获取签名验证器
* @return
*/
@Bean
public ScheduledUpdateCertificatesVerifier getVerifier() {
System.out.println("privateKeyPath = " + privateKeyPath);
log.info("获取签名验证器");
//获取商户私钥
PrivateKey privateKey = getPrivateKey(privateKeyPath);
//私钥签名对象
PrivateKeySigner privateKeySigner = new PrivateKeySigner(mchSerialNo, privateKey);
//身份认证对象
WechatPay2Credentials wechatPay2Credentials = new WechatPay2Credentials(mchId, privateKeySigner);
//使用定时更新的签名验证器,不需要传入证书
ScheduledUpdateCertificatesVerifier verifier = new ScheduledUpdateCertificatesVerifier(
wechatPay2Credentials, apiV3Key.getBytes(StandardCharsets.UTF_8));
return verifier;
}
3.4、获取 HttpClient 对象
https://github.com/wechatpay-apiv3/wechatpay-apache-httpclient
(定时更新平台证书功能)
HttpClient
对象:是建立远程连接的基础,我们通过
SDK
创建这个对象。
/**
* 获取http请求对象
* @param verifier
* @return
*/
@Bean(name = "wxPayClient")
public CloseableHttpClient getWxPayClient(ScheduledUpdateCertificatesVerifier verifier) {
log.info("获取httpClient");
//获取商户私钥
PrivateKey privateKey = getPrivateKey(privateKeyPath);
WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create()
.withMerchant(mchId, mchSerialNo, privateKey)
.withValidator(new WechatPay2Validator(verifier));
//接下来,你仍然可以通过builder设置各种参数,来配置你的HttpClient
//通过WechatPayHttpClientBuilder构造的HttpClient,会自动的处理签名和验签,并进行证书自动更新
CloseableHttpClient httpClient = builder.build();
return httpClient;
}
4、API字典和相关工具
4.1、API列表
https://pay.weixin.qq.com/wiki/doc/apiv3/open/pay/chapter2_7_3.shtml
我们的项目中要实现以下所有
API
的功能。
4.2、接口规则
https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay2_0.shtml
微信支付
APIv3
使用
JSON
作为消息体的数据交换格式。
<!--json 处理 --><dependency><groupId> com.google.code.gson </groupId><artifactId> gson </artifactId></dependency>
4.3、定义枚举
将资料文件夹中的
enums
目录复制到源码目录中。
为了开发方便,我们预先在项目中定义一些枚举。枚举中定义的内容包括接口地址,支付状态等信息。
OrderStatus
package com.atguigu.paymentdemo.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
@AllArgsConstructor
@Getter
public enum OrderStatus {
/**
* 未支付
*/
NOTPAY("未支付"),
/**
* 支付成功
*/
SUCCESS("支付成功"),
/**
* 已关闭
*/
CLOSED("超时已关闭"),
/**
* 已取消
*/
CANCEL("用户已取消"),
/**
* 退款中
*/
REFUND_PROCESSING("退款中"),
/**
* 已退款
*/
REFUND_SUCCESS("已退款"),
/**
* 退款异常
*/
REFUND_ABNORMAL("退款异常");
/**
* 类型
*/
private final String type;
}
PayType
package com.atguigu.paymentdemo.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
@AllArgsConstructor
@Getter
public enum PayType {
/**
* 微信
*/
WXPAY("微信"),
/**
* 支付宝
*/
ALIPAY("支付宝");
private final String type;
}
WxApiType
package com.atguigu.paymentdemo.enums.wxpay;
import lombok.AllArgsConstructor;
import lombok.Getter;
@AllArgsConstructor
@Getter
public enum WxApiType {
/**
* Native下单
*/
NATIVE_PAY("/v3/pay/transactions/native"),
/**
* Native下单
*/
NATIVE_PAY_V2("/pay/unifiedorder"),
/**
* 查询订单
*/
ORDER_QUERY_BY_NO("/v3/pay/transactions/out-trade-no/%s"),
/**
* 关闭订单
*/
CLOSE_ORDER_BY_NO("/v3/pay/transactions/out-trade-no/%s/close"),
/**
* 申请退款
*/
DOMESTIC_REFUNDS("/v3/refund/domestic/refunds"),
/**
* 查询单笔退款
*/
DOMESTIC_REFUNDS_QUERY("/v3/refund/domestic/refunds/%s"),
/**
* 申请交易账单
*/
TRADE_BILLS("/v3/bill/tradebill"),
/**
* 申请资金账单
*/
FUND_FLOW_BILLS("/v3/bill/fundflowbill");
/**
* 类型
*/
private final String type;
}
WxNotifyType
package com.atguigu.paymentdemo.enums.wxpay;
import lombok.AllArgsConstructor;
import lombok.Getter;
@AllArgsConstructor
@Getter
public enum WxNotifyType {
/**
* 支付通知
*/
NATIVE_NOTIFY("/api/wx-pay/native/notify"),
/**
* 支付通知
*/
NATIVE_NOTIFY_V2("/api/wx-pay-v2/native/notify"),
/**
* 退款结果通知
*/
REFUND_NOTIFY("/api/wx-pay/refunds/notify");
/**
* 类型
*/
private final String type;
}
WxRefundStatus
package com.atguigu.paymentdemo.enums.wxpay;
import lombok.AllArgsConstructor;
import lombok.Getter;
@AllArgsConstructor
@Getter
public enum WxRefundStatus {
/**
* 退款成功
*/
SUCCESS("SUCCESS"),
/**
* 退款关闭
*/
CLOSED("CLOSED"),
/**
* 退款处理中
*/
PROCESSING("PROCESSING"),
/**
* 退款异常
*/
ABNORMAL("ABNORMAL");
/**
* 类型
*/
private final String type;
}
WxTradeState
package com.atguigu.paymentdemo.enums.wxpay;
import lombok.AllArgsConstructor;
import lombok.Getter;
@AllArgsConstructor
@Getter
public enum WxTradeState {
/**
* 支付成功
*/
SUCCESS("SUCCESS"),
/**
* 未支付
*/
NOTPAY("NOTPAY"),
/**
* 已关闭
*/
CLOSED("CLOSED"),
/**
* 转入退款
*/
REFUND("REFUND");
/**
* 类型
*/
private final String type;
}
4.4、添加工具类
将资料文件夹中的
util
目录复制到源码目录中,我们将会使用这些辅助工具简化项目的开发
HttpUtils
package com.atguigu.paymentdemo.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();
}
}
}
}
}
OrderNoUtils
package com.atguigu.paymentdemo.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;
}
}
5、Native下单API
5.1、Native支付流程
https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_4.shtml
5.2、Native下单API
https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_1.shtml
商户端发起支付请求,微信端创建支付订单并生成支付二维码链接,微信端将支付二维码返回给商户端,商户端显示支付二维码,用户使用微信客户端扫码后发起支付。
(1)创建 WxPayController
package com.atguigu.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;
@CrossOrigin
@RestController
@RequestMapping("/api/wx-pay")
@Api(tags = "网站微信支付")
@Slf4j
public class WxPayController {
}
(2)创建 WxPayService
接口
package com.atguigu.paymentdemo.service;
public interface WxPayService {
}
实现
package com.atguigu.paymentdemo.service.impl;
import com.atguigu.paymentdemo.service.WxPayService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@Service
@Slf4j
public class WxPayServiceImpl implements WxPayService {
}
(
3
)定义
WxPayController
方法
package com.atguigu.paymentdemo.controller;
import com.atguigu.paymentdemo.service.WxPayService;
import com.atguigu.paymentdemo.vo.R;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.io.IOException;
import java.util.Map;
@Slf4j
@RestController
@RequestMapping("/api/wx-pay")
@Api(tags ="网站微信支付")
public class WxPayController {
@Resource
private WxPayService wxPayService;
@ApiOperation("调用统一下单API,生成支付二维码")
@PostMapping("/native/{productId}")
public R nativePay(@PathVariable Long productId) throws IOException {
log.info("发起支付请求");
//返回支付二维码链接和订单号
Map<String, Object> map = wxPayService.nativePay(productId);
return R.ok().setData(map);
}
}
R对象中添加 @Accessors(chain = true),使其可以链式操作
@Data
@Accessors(chain = true) //链式操作
public class R {
(
4
)定义
WxPayService
方法
参考:
API
字典
->
基础支付
-> Native
支付
-> Native
下单:
https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_1.shtml
指引文档
->
基础支付
-> Native
支付
->
开发指引
->
【服务端】
Native
下单:
https://pay.weixin.qq.com/wiki/doc/apiv3/open/pay/chapter2_7_2.shtml
接口
Map < String , Object > nativePay ( Long productId ) throws Exception ;
实现
package com.atguigu.paymentdemo.service.impl;
import com.atguigu.paymentdemo.config.WxPayConfig;
import com.atguigu.paymentdemo.entity.OrderInfo;
import com.atguigu.paymentdemo.enums.OrderStatus;
import com.atguigu.paymentdemo.enums.wxpay.WxApiType;
import com.atguigu.paymentdemo.enums.wxpay.WxNotifyType;
import com.atguigu.paymentdemo.service.WxPayService;
import com.atguigu.paymentdemo.util.OrderNoUtils;
import com.google.gson.Gson;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.util.EntityUtils;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
@Service
@Slf4j
public class WxPayServiceImpl implements WxPayService {
@Resource
private WxPayConfig wxPayConfig;
@Resource
private CloseableHttpClient wxPayClient;
@Override
public Map<String, Object> nativePay(Long productId) throws IOException {
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");
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){
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.3、签名和验签源码解析
(1)签名原理
开启
debug
日志
logging :level :root : info
签名生成流程:
https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay4_0.shtml
签名生成源码:
(2)验签原理
签名验证流程:
https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay4_1.shtml
签名验证源码:
5.4、创建课程订单
(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){
LambdaQueryWrapper<OrderInfo> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.eq(OrderInfo::getProductId, productId)
.eq(OrderInfo::getOrderStatus, OrderStatus.NOTPAY.getType());
OrderInfo orderInfo = baseMapper.selectOne(lambdaQueryWrapper);
return orderInfo;
}
(2)缓存二维码
OrderInfoService
接口:
void saveCodeUrl ( String orderNo , String codeUrl );
实现:
/**
* 存储订单二维码
* @param orderNo
* @param codeUrl
*/
@Override
public void saveCodeUrl(String orderNo, String codeUrl) {
LambdaQueryWrapper<OrderInfo> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.eq(OrderInfo::getOrderNo, orderNo);
OrderInfo orderInfo = new OrderInfo();
orderInfo.setCodeUrl(codeUrl);
baseMapper.update(orderInfo,lambdaQueryWrapper);
}
(
3
)修改
WxPayServiceImpl
的
nativePay
方法
package com.atguigu.paymentdemo.service.impl;
import com.atguigu.paymentdemo.config.WxPayConfig;
import com.atguigu.paymentdemo.entity.OrderInfo;
import com.atguigu.paymentdemo.enums.wxpay.WxApiType;
import com.atguigu.paymentdemo.enums.wxpay.WxNotifyType;
import com.atguigu.paymentdemo.service.OrderInfoService;
import com.atguigu.paymentdemo.service.WxPayService;
import com.google.gson.Gson;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.util.EntityUtils;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import javax.annotation.Resource;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
@Service
@Slf4j
public class WxPayServiceImpl implements WxPayService {
@Resource
private WxPayConfig wxPayConfig;
@Resource
private CloseableHttpClient wxPayClient;
@Resource
private OrderInfoService orderInfoService;
@Override
public Map<String, Object> nativePay(Long productId) throws IOException {
log.info("生成订单");
//生成订单
OrderInfo orderInfo = orderInfoService.createOrderByProductId(productId);
String codeUrl1 = orderInfo.getCodeUrl();
if (orderInfo != null && !StringUtils.isEmpty(codeUrl1)){
log.info("订单已存在,二维码已保存");
//返回二维码
Map<String, Object> map = new HashMap<>();
map.put("orderNo", orderInfo.getOrderNo());
map.put("codeUrl", codeUrl1);
return map;
}
log.info("调用统一下单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){
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");
//保存二维码
orderInfoService.saveCodeUrl(orderInfo.getOrderNo(), codeUrl);
Map<String, Object> map = new HashMap<>();
map.put("codeUrl",codeUrl);
map.put("orderNo",orderInfo.getOrderNo());
//返回二维码
return map;
} finally {
response.close();
}
}
}
5.5、显示订单列表
在我的订单页面按时间倒序显示订单列表
(1)创建OrderInfoController
package com.atguigu.paymentdemo.controller;
import com.atguigu.paymentdemo.entity.OrderInfo;
import com.atguigu.paymentdemo.service.OrderInfoService;
import com.atguigu.paymentdemo.vo.R;
import io.swagger.annotations.Api;
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 javax.annotation.Resource;
import java.util.List;
@CrossOrigin //开放前端的跨域访问
@Api(tags = "商品订单管理")
@RestController
@RequestMapping("/api/order-info")
public class OrderInfoController {
@Resource
private OrderInfoService orderInfoService;
@ApiOperation("订单列表")
@GetMapping("/list")
public R list(){
List<OrderInfo> list = orderInfoService.listOrderByCreateTimeDesc();
return R.ok().data("list", list);
}
}
(
2
)定义
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、内网穿透
(1)访问ngrok官网
https://ngrok.com/
(2)注册账号、登录
(3)下载内网穿透工具
ngrok-stable-windows-amd64.zip
(4)设置你的 authToken
为本地计算机做授权配置
ngrok authtoken 6 aYc6Kp7kpxVr8pY88LkG_6x9o18yMY8BASrXiDFMeS
(5)启动服务
ngrok http 8090
(6)测试外网访问
你获得的外网地址/api/test
6.2、接收通知和返回应答
支付通知
API
:
https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_5.shtml
(1)启动ngrok
ngrok http 8090
(2)设置通知地址
wxpay.properties
注意:每次重新启动
ngrok
,都需要根据实际情况修改这个配置
wxpay.notify-domain=https://7d92-115-171-63-135.ngrok.io
(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);
}
(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);
}
}
(5)测试超时应答
回调通知注意事项:
https://pay.weixin.qq.com/wiki/doc/apiv3/Practices/chapter1_1_5.shtml
商户系统收到支付结果通知,需要在
5
秒内
返回应答报文,否则微信支付认为通知失败,后续会
重复发送通知。
// 测试超时应答:添加睡眠时间使应答超时TimeUnit . SECONDS . sleep ( 5 );
6.3、验签
(1)工具类
参考
SDK
源码中的
WechatPay2Validator
创建通知验签工具类
WechatPay2ValidatorForRequest
(2)验签
@Resourceprivate Verifier verifier ;
// 签名的验证WechatPay2ValidatorForRequest validator= new WechatPay2ValidatorForRequest ( verifier , body , requestId );if ( ! validator . validate ( request )) {log . error ( " 通知验签失败 " );// 失败应答response . setStatus ( 500 );map . put ( "code" , "ERROR" );map . put ( "message" , " 通知验签失败 " );return gson . toJson ( map );}log . info ( " 通知验签成功 " );//TODO : 处理订单
6.4、解密
(1)WxPayController
nativeNotify
方法中添加处理订单的代码
// 处理订单wxPayService . processOrder ( bodyMap );
(1)WxPayService
接口:
void processOrder ( Map < String , Object > bodyMap ) throws GeneralSecurityException ;
实现:
@Overridepublic void processOrder ( Map < String , Object > bodyMap ) throwsGeneralSecurityException {log . info ( " 处理订单 " );String plainText = decryptFromResource ( bodyMap );// 转换明文// 更新订单状态// 记录支付日志}
辅助方法:
/*** 对称解密* @param bodyMap* @return*/private String decryptFromResource ( Map < String , Object > bodyMap ) throwsGeneralSecurityException {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 = newAesUtil ( 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、处理订单
(1)完善processOrder方法
@Resource
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);
}
(2)更新订单状态
OrderInfoService
接口:
void updateStatusByOrderNo ( String orderNo , OrderStatus orderStatus );
实现:
/*** 根据订单编号更新订单状态* @param orderNo* @param orderStatus*/@Overridepublic 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 );}
(3)处理支付日志
PaymentInfoService
接口:
void createPaymentInfo ( String plainText );
实现:
/*** 记录支付日志* @param plainText*/@Overridepublic 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、处理重复通知
(1)测试重复的通知
// 应答超时// 设置响应超时,可以接收到微信支付的重复的支付结果通知。// 通知重复,数据库会记录多余的支付日志TimeUnit . SECONDS . sleep ( 5 );
(2)处理重复通知
在
processOrder
方法中,更新订单状态之前,添加如下代码
// 处理重复通知// 保证接口调用的幂等性:无论接口被调用多少次,产生的结果是一致的String orderStatus = orderInfoService . getOrderStatus ( orderNo );if ( ! OrderStatus . NOTPAY . getType (). equals ( orderStatus )) {return ;}
OrderInfoService
接口:
String getOrderStatus ( String orderNo );
实现:
/*** 根据订单号获取订单状态* @param orderNo* @return*/@Overridepublic 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 ();}
6.7、数据锁
(1)测试通知并发
// 处理重复的通知// 模拟通知并发try {TimeUnit . SECONDS . sleep ( 5 );} catch ( InterruptedException e ) {e . printStackTrace ();}// 更新订单状态// 记录支付日志
(2)定义ReentrantLock
定义
ReentrantLock
进行并发控制。注意,必须手动释放锁。
private final ReentrantLock lock = new ReentrantLock ();
@Overridepublic void processOrder ( Map < String , Object > bodyMap ) throwsGeneralSecurityException {log . info ( " 处理订单 " );// 解密报文String plainText = decryptFromResource ( bodyMap );// 将明文转换成 mapGson 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、前端定时轮询查单
在二维码展示页面,前端定时轮询查询订单是否已支付,如果支付成功则跳转到订单页面
(1)定义定时器
// 启动定时器this . timer = setInterval (() => {// 查询订单是否支付成功this . queryOrderStatus ()}, 3000 )
(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.2
、
WxPayService
接口
void cancelOrder ( String orderNo ) throws Exception ;
实现
/*** 用户取消订单* @param orderNo*/@Overridepublic 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 ) { // 处理成功,无返回 Bodylog . info ( " 成功 204" );} else {log . info ( "Native 下单失败 , 响应码 = " + statusCode );throw new IOException ( "request failed" );}} finally {response . close ();}}
9、微信支付查单API
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 );}
(2)WxPayService
接口
String queryOrder ( String orderNo ) throws Exception ;
实现
/*** 查单接口调用*/@Overridepublic 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 ) { // 处理成功,无返回 Bodylog . 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
实现任务调度
(
1
)启动类添加注解
statistics
启动类添加注解
@EnableScheduling
(2)测试定时任务
创建
task
包,创建
WxPayTask.java
package com . atguigu . paymentdemo . task ;import lombok . extern . slf4j . Slf4j ;import org . springframework . scheduling . annotation . Scheduled ;import org . springframework . stereotype . Component ;@Slf4j@Componentpublic 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、定时查找超时订单
(1)WxPayTask
@Resourceprivate OrderInfoService orderInfoService ;@Resourceprivate 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 );}}
(2)OrderInfoService
接口
List < OrderInfo > getNoPayOrderByDuration ( int minutes );
实现
/*** 找出创建超过 minutes 分钟并且未支付的订单* @param minutes* @return*/@Overridepublic 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 ) throws Exception ;
实现:
/*** 根据订单号查询微信支付查单接口,核实订单状态* 如果订单已支付,则更新商户端订单状态,并记录支付日志* 如果订单未支付,则调用关单接口关闭订单,并更新商户端订单状态* @param orderNo*/@Overridepublic 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 );}}
6.8
接口:
OrderInfo getOrderByOrderNo ( String orderNo );
实现:
/*** 根据订单号获取订单* @param orderNo* @return*/@Overridepublic OrderInfo getOrderByOrderNo ( String orderNo ) {QueryWrapper < OrderInfo > queryWrapper = new QueryWrapper <> ();queryWrapper . eq ( "order_no" , orderNo );OrderInfo orderInfo = baseMapper . selectOne ( queryWrapper );return orderInfo ;}
11、申请退款API
文档:
https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_9.shtml
11.1、创建退款单
(1)根据订单号查询订单
OrderInfoService
接口:
OrderInfo getOrderByOrderNo ( String orderNo );
实现:
/*** 根据订单号获取订单* @param orderNo* @return*/@Overridepublic OrderInfo getOrderByOrderNo ( String orderNo ) {QueryWrapper < OrderInfo > queryWrapper = new QueryWrapper <> ();queryWrapper . eq ( "order_no" , orderNo );OrderInfo orderInfo = baseMapper . selectOne ( queryWrapper );return orderInfo ;}
(2)创建退款单记录
RefundsInfoService
接口:
RefundInfo createRefundByOrderNo ( String orderNo , String reason );
实现:
@Resourceprivate OrderInfoService orderInfoService ;/*** 根据订单号创建退款订单* @param orderNo* @return*/@Overridepublic 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 ;}
11.2、更新退款单
RefundInfoService
接口:
void updateRefund ( String content );
实现:
/*** 记录退款记录* @param content*/@Overridepublic void updateRefund ( String content ) {// 将 json 字符串转换成 MapGson 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 );}
11.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 ();}
(
2
)
WxPayService
接口:
void refund ( String orderNo , String reason ) throws Exception ;
实现:
@Resourceprivate RefundInfoService refundsInfoService ;/*** 退款* @param orderNo* @param reason* @throws IOException*/@Transactional ( rollbackFor = Exception . class )@Overridepublic void refund ( String orderNo , String reason ) throws Exception {log . info ( " 创建退款单记录 " );// 根据订单编号创建退款单RefundInfo refundsInfo = refundsInfoService . createRefundByOrderNo ( orderNo ,reason );log . info ( " 调用退款 API" );// 调用统一下单 APIString 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 ();}}
12、查询退款API
文档:
https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_10.shtml
12.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 );}
(2)WxPayService
接口:
String queryRefund ( String orderNo ) throws Exception ;
实现:
/*** 查询退款接口调用* @param refundNo* @return*/@Overridepublic String queryRefund ( String refundNo ) throws Exception {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 ();}}
12.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 );}}
(2)RefundInfoService
接口
List < RefundInfo > getNoRefundOrderByDuration ( int minutes );
实现
/*** 找出申请退款超过 minutes 分钟并且未成功的退款单* @param minutes* @return*/@Overridepublic 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 ;}
12.3、处理超时未退款订单
WxPayService
核实订单状态
接口:
void checkRefundStatus ( String refundNo );
实现:
/*** 根据退款单号核实退款单状态* @param refundNo* @return*/@Transactional ( rollbackFor = Exception . class )@Overridepublic void checkRefundStatus ( String refundNo ) throws Exception {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 );}}
13、退款结果通知API
文档:
https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_11.shtml
13.1、接收退款通知
WxPayController
/*** 退款结果通知* 退款状态改变后,微信会把相关退款结果发送给商户。*/@PostMapping ( "/refunds/notify" )public String refundsNotify ( HttpServletRequest request , HttpServletResponseresponse ){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 );}}
13.2、处理订单和退款单
WxPayService
接口:
void processRefund ( Map < String , Object > bodyMap ) throws Exception ;
实现:
/*** 处理退款单*/@Transactional ( rollbackFor = Exception . class )@Overridepublic void processRefund ( Map < String , Object > bodyMap ) throws Exception {log . info ( " 退款单 " );// 解密报文String plainText = decryptFromResource ( bodyMap );// 将明文转换成 mapGson 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 ();}}}
14、账单
14.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 );}
(2)WxPayService
接口:
String queryBill ( String billDate , String type ) throws Exception ;
实现
/*** 申请账单* @param billDate* @param type* @return* @throws Exception*/@Overridepublic String queryBill ( String billDate , String type ) throws Exception {log . warn ( " 申请账单接口调用 {}" , billDate );String url = "" ;if ( "tradebill" . equals ( type )){url = WxApiType . TRADE_BILLS . getType ();} else if ( "fundflowbill" . equals ( type )){url = WxApiType . FUND_FLOW_BILLS . getType ();} else {throw new RuntimeException ( " 不支持的账单类型 " );}url = wxPayConfig . getDomain (). concat ( url ). concat ( "?bill_date=" ). concat ( billDate );// 创建远程 Get 请求对象HttpGet httpGet = new HttpGet ( url );httpGet . addHeader ( "Accept" , "application/json" );// 使用 wxPayClient 发送请求得到响应CloseableHttpResponse response = wxPayClient . execute ( httpGet );try {String bodyAsString = EntityUtils . toString ( response . getEntity ());int statusCode = response . getStatusLine (). getStatusCode ();if ( statusCode == 200 ) {log . info ( " 成功 , 申请账单返回结果 = " + bodyAsString );} else if ( statusCode == 204 ) {log . info ( " 成功 " );} else {throw new RuntimeException ( " 申请账单异常 , 响应码 = " + statusCode + ",申请账单返回结果 = " + bodyAsString );}// 获取账单下载地址Gson gson = new Gson ();Map < String , String > resultMap = gson . fromJson ( bodyAsString ,HashMap . class );return resultMap . get ( "download_url" );} finally {response . close ();}}
14.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 );}
(2)WxPayService
接口:
String downloadBill ( String billDate , String type ) throws Exception ;
实现:
/*** 下载账单* @param billDate* @param type* @return* @throws Exception*/@Overridepublic String downloadBill ( String billDate , String type ) throws Exception {log . warn ( " 下载账单接口调用 {}, {}" , billDate , type );// 获取账单 url 地址String downloadUrl = this . queryBill ( billDate , type );// 创建远程 Get 请求对象HttpGet httpGet = new HttpGet ( downloadUrl );httpGet . addHeader ( "Accept" , "application/json" );// 使用 wxPayClient 发送请求得到响应CloseableHttpResponse response = wxPayNoSignClient . execute ( httpGet );try {String bodyAsString = EntityUtils . toString ( response . getEntity ());int statusCode = response . getStatusLine (). getStatusCode ();if ( statusCode == 200 ) {log . info ( " 成功 , 下载账单返回结果 = " + bodyAsString );} else if ( statusCode == 204 ) {log . info ( " 成功 " );} else {throw new RuntimeException ( " 下载账单异常 , 响应码 = " + statusCode + ",下载账单返回结果 = " + bodyAsString );}return bodyAsString ;} finally {response . close ();}}
五、基础支付API V2
1、V2和V3的比较
2、引入依赖和工具
2.1、引入依赖
<!-- 微信支付 --><dependency><groupId> com.github.wxpay </groupId><artifactId> wxpay-sdk </artifactId><version> 0.0.3 </version></dependency>
2.2、复制工具类
2.3、添加商户APIv2 key
yml
文件
# APIv2 密钥wxpay.partnerKey : T6m9iK73b0kn9g5v426MKfHQH7X8rKwb
WxPayConfifig.java
private String partnerKey ;
2.4、添加枚举
enum WxApiType
/*** Native 下单 V2*/NATIVE_PAY_V2 ( "/pay/unifiedorder" ),
enum WxNotifyType
/*** 支付通知 V2*/NATIVE_NOTIFY_V2 ( "/api/wx-pay-v2/native/notify" ),
3、统一下单
3.1、创建WxPayV2Controller
package com . atguigu . paymentdemo . controller ;import com . atguigu . paymentdemo . service . WxPayService ;import com . atguigu . paymentdemo . vo . R ;import io . swagger . annotations . Api ;import io . swagger . annotations . ApiOperation ;import lombok . extern . slf4j . Slf4j ;import org . springframework . web . bind . annotation . * ;import javax . annotation . Resource ;import javax . servlet . http . HttpServletRequest ;import java . util . Map ;@CrossOrigin // 跨域@RestController@RequestMapping ( "/api/wx-pay-v2" )@Api ( tags = " 网站微信支付 APIv2" )@Slf4jpublic class WxPayV2Controller {@Resourceprivate WxPayService wxPayService ;/*** Native 下单* @param productId* @return* @throws Exception*/@ApiOperation ( " 调用统一下单 API ,生成支付二维码 " )@PostMapping ( "/native/{productId}" )public R createNative ( @PathVariable Long productId , HttpServletRequestrequest ) throws Exception {log . info ( " 发起支付请求 v2" );String remoteAddr = request . getRemoteAddr ();Map < String , Object > map = wxPayService . nativePayV2 ( productId ,remoteAddr );return R . ok (). setData ( map );}}
3.2、WxPayService
接口:
Map < String , Object > nativePayV2 ( Long productId , String remoteAddr ) throwsException ;
实现:
@Overridepublic Map < String , Object > nativePayV2 ( Long productId , String remoteAddr ) throwsException {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" );HttpClientUtils client = newHttpClientUtils ( "https://api.mch.weixin.qq.com/pay/unifiedorder" );// 组装接口参数Map < String , String > params = new HashMap <> ();params . put ( "appid" , wxPayConfig . getAppid ()); // 关联的公众号的 appidparams . put ( "mch_id" , wxPayConfig . getMchId ()); // 商户号params . put ( "nonce_str" , WXPayUtil . generateNonceStr ()); // 生成随机字符串params . put ( "body" , orderInfo . getTitle ());params . put ( "out_trade_no" , orderInfo . getOrderNo ());// 注意,这里必须使用字符串类型的参数(总金额:分)String totalFee = orderInfo . getTotalFee () + "" ;params . put ( "total_fee" , totalFee );params . put ( "spbill_create_ip" , remoteAddr );params . put ( "notify_url" ,wxPayConfig . getNotifyDomain (). concat ( WxNotifyType . NATIVE_NOTIFY . getType ()));params . put ( "trade_type" , "NATIVE" );// 将参数转换成 xml 字符串格式:生成带有签名的 xml 格式字符串String xmlParams = WXPayUtil . generateSignedXml ( params ,wxPayConfig . getPartnerKey ());log . info ( "\n xmlParams : \n" + xmlParams );client . setXmlParam ( xmlParams ); // 将参数放入请求对象的方法体client . setHttps ( true ); // 使用 https 形式发送client . post (); // 发送请求String resultXml = client . getContent (); // 得到响应结果log . info ( "\n resultXml : \n" + resultXml );// 将 xml 响应结果转成 map 对象Map < String , String > resultMap = WXPayUtil . xmlToMap ( resultXml );// 错误处理if ( "FAIL" . equals ( resultMap . get ( "return_code" )) ||"FAIL" . equals ( resultMap . get ( "result_code" ))){log . error ( " 微信支付统一下单错误 ===> {} " , resultXml );throw new RuntimeException ( " 微信支付统一下单错误 " );}// 二维码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 ;}
4、支付回调
@Resourceprivate WxPayService wxPayService ;@Resourceprivate WxPayConfig wxPayConfig ;@Resourceprivate OrderInfoService orderInfoService ;@Resourceprivate PaymentInfoService paymentInfoService ;private final ReentrantLock lock = new ReentrantLock ();/*** 支付通知* 微信支付通过支付通知接口将用户支付成功消息通知给商户*/@PostMapping ( "/native/notify" )public String wxNotify ( HttpServletRequest request ) throws Exception {System . out . println ( " 微信发送的回调 " );Map < String , String > returnMap = new HashMap <> (); // 应答对象// 处理通知参数String body = HttpUtils . readData ( request );// 验签if ( ! WXPayUtil . isSignatureValid ( body , wxPayConfig . getPartnerKey ())) {log . error ( " 通知验签失败 " );// 失败应答returnMap . put ( "return_code" , "FAIL" );returnMap . put ( "return_msg" , " 验签失败 " );String returnXml = WXPayUtil . mapToXml ( returnMap );return returnXml ;}// 解析 xml 数据Map < String , String > notifyMap = WXPayUtil . xmlToMap ( body );// 判断通信和业务是否成功if ( ! "SUCCESS" . equals ( notifyMap . get ( "return_code" )) ||! "SUCCESS" . equals ( notifyMap . get ( "result_code" ))) {log . error ( " 失败 " );// 失败应答returnMap . put ( "return_code" , "FAIL" );returnMap . put ( "return_msg" , " 失败 " );String returnXml = WXPayUtil . mapToXml ( returnMap );return returnXml ;}// 获取商户订单号String orderNo = notifyMap . get ( "out_trade_no" );OrderInfo orderInfo = orderInfoService . getOrderByOrderNo ( orderNo );// 并校验返回的订单金额是否与商户侧的订单金额一致if ( orderInfo != null && orderInfo . getTotalFee () !=Long . parseLong ( notifyMap . get ( "total_fee" ))) {log . error ( " 金额校验失败 " );// 失败应答returnMap . put ( "return_code" , "FAIL" );returnMap . put ( "return_msg" , " 金额校验失败 " );String returnXml = WXPayUtil . mapToXml ( returnMap );return returnXml ;}// 处理订单if ( lock . tryLock ()){try {// 处理重复的通知// 接口调用的幂等性:无论接口被调用多少次,产生的结果是一致的。String orderStatus = orderInfoService . getOrderStatus ( orderNo );if ( OrderStatus . NOTPAY . getType (). equals ( orderStatus )){// 更新订单状态orderInfoService . updateStatusByOrderNo ( orderNo ,OrderStatus . SUCCESS );// 记录支付日志paymentInfoService . createPaymentInfo ( body );}} finally {// 要主动释放锁lock . unlock ();}}returnMap . put ( "return_code" , "SUCCESS" );returnMap . put ( "return_msg" , "OK" );String returnXml = WXPayUtil . mapToXml ( returnMap );log . info ( " 支付成功,已应答 " );return returnXml ;}