资料来源:https://www.bilibili.com/video/BV1US4y1D77m/?p=2&spm_id_from=pageDriver
微信支付
一、微信支付介绍和接入指引
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 获取微信平台证书
可以预先下载,也可以通过编程的方式获取。后面的课程中,我们会通过编程的方式来获取。
二、支付安全(证书/秘钥/签名)
1、信息安全的基础 - 机密性
- 明文:加密前的消息叫“明文” (plain text)
- 密文:加密后的文本叫“密文” (cipher text)
- 密钥:只有掌握特殊“钥匙”的人,才能对加密的文本进行解密,这里的“钥匙”就叫做“密钥” (key)
- “密钥”就是一个字符串,度量单位是“位” (bit),比如,密钥长度是 128,就是16 字节的二进制串
- 加密:实现机密性最常用的手段是“加密” (encrypt)
- 按照密钥的使用方式,加密可以分为两大类: 对称加密和非对称加密。
- 解密:使用密钥还原明文的过程叫“解密” (decrypt)
加密算法:加密解密的操作过程就是“加密算法”
对称加密
- 特点:只使用一个密钥,密钥必须保密,常用的有AES算法
- 优点:运算速度快
- 缺点:秘钥需要信息交换的双方共享,一旦被窃取,消息会被破解,无法做到安全的密钥交换
- 非对称加密
- 特点:使用两个密钥:公钥和私钥,公钥可以任意分发而私钥保密,常用的有RSA
- 优点:黑客获取公钥无法破解密文,解决了密钥交换的问题
- 缺点:运算速度非常慢
- 混合加密
- 公钥加密,私钥解密的作用是加密信息
-
4、摘要算法(Digest Algorithm)
摘要算法就是我们常说的散列函数、哈希函数(Hash Function),它能够把任意长度的数据“压缩”成 固定长度、而且独一无二的“摘要”字符串,就好像是给这段数据生成了一个数字“指纹”。
作用:保证信息的完整性
特性:- 不可逆:只有算法,没有秘钥,只能加密,不能解密
- 难题友好性:想要破解,只能暴力枚举
- 发散性:只要对原文进行一点点改动,摘要就会发生剧烈变化
- 抗碰撞性:原文不同,计算后的摘要也要不同
常见摘要算法:
MD5、SHA1、SHA2 ( SHA224、SHA256、SHA384)
5、数字签名
数字签名是使用私钥对摘要加密生成签名,需要由公钥将签名解密后进行验证,实现身份认证和不可否认。<br />签名和验证签名的流程:<br />![image.png](https://cdn.nlark.com/yuque/0/2022/png/22523384/1652002116798-e83f20d9-4c74-4d8c-894c-e549fdb82e02.png#clientId=u035f50c6-59a7-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=620&id=u6f8d1583&margin=%5Bobject%20Object%5D&name=image.png&originHeight=620&originWidth=1254&originalType=binary&ratio=1&rotation=0&showTitle=false&size=211703&status=done&style=none&taskId=u1ccf341f-c659-458d-bbd3-bc04358d022&title=&width=1254)
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 新建项目
1.2 添加依赖
<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
package com.atguigu.paymentdemo.controller;
@CrossOrigin // 开放前端的跨域访问
@Api(tags = "商品管理")
@RestController
@RequestMapping("/api/product")
public class ProductController {
@ApiOperation("测试接口")
@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配置文件
创建config包,创建Swagger2Config类
package com.atguigu.paymentdemo.config;
@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;
@Data
@Accessors(chain = true)
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方法,返回统一结果
@CrossOrigin //开放前端的跨域访问
@Api(tags = "商品管理")
@RestController
@RequestMapping("/api/product")
public class ProductController {
@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:ss
time-zone: GMT+8
4、创建数据库
4.1 创建数据库
mysql -uroot -p
mysql> create database payment_demo;
4.2 IDEA配置数据库连接
(1)打开数据库面板
(2)添加数据库
(3)配置数据库连接参数
4.3 执行SQL脚本
payment_demo.sql
5、集成MyBatis-Plus
5.1 引入依赖
<!--mysql 驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--MyBatis-Plus:是MyBatis的增强-->
<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.Driver
url: jdbc:mysql://localhost:3306/payment_demo?serverTimezone=GMT%2B8&characterEncoding=utf-8
username: root
password: 123456
5.3 定义实体类
BaseEntity是父类,其他类继承BaseEntity
5.4 定义持久层
定义Mapper接口继承BaseMapper<>,定义xml配置文件
5.5 定义MyBatis-Plus的配置文件
在config包中创建配置文件MybatisPlusConfig
package com.atguigu.paymentdemo.config;
@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.StdOutImpl
mapper-locations: classpath:com/atguigu/paymentdemo/mapper/xml/*.xml
5.7 定义业务层
定义业务层接口继承IService<>
定义业务层接口的实现类,并继承ServiceImpl<,>
5.8 定义接口方法查询所有商品
在ProductController中添加一个方法
package com.atguigu.paymentdemo.controller;
@CrossOrigin //开放前端的跨域访问
@Api(tags = "商品管理")
@RestController
@RequestMapping("/api/product")
public class ProductController {
@Resource
private ProductService productService;
@ApiOperation("商品列表")
@GetMapping("/list")
public R list(){
List<Product> list = productService.list();
return R.ok().data("productList", list);
}
}
5.9 pom中配置build节点
因为maven工程在默认情况下src/main/java 目录下的所有资源文件是不发布到target目录下的,我们 在 pom 文件的节点下配置一个资源发布过滤器
<build>
<!-- 项目打包时会将java目录中的*.xml文件也进行打包 -->
<resources>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.xml</include>
</includes>
<filtering>false</filtering>
</resource>
</resources>
</build>
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 数据绑定
<!--定义页面结构-->
<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 定义微信支付相关参数
这个文件定义了之前我们准备的微信支付相关的参数,例如商户号、APPID、API秘钥等等
# 微信支付相关参数
# 商户号
wxpay.mch-id=1558950191
# 商户API证书序列号
wxpay.mch-serial-no=34345964330B66427E0D3D28826C4993C77E631F
# 商户私钥文件
wxpay.private-key-path=apiclient_key.pem
# APIv3密钥
wxpay.api-v3-key=UDuLFDcmy5Eb6o0nTNZdu6ek4DDh4K8B
# APPID
wxpay.appid=wx74862e0dfcf69954
# 微信服务器地址
wxpay.domain=https://api.mch.weixin.qq.com
# 接收结果通知地址
# 注意:每次重新启动ngrok,都需要根据实际情况修改这个配置
wxpay.notify-domain=https://77ea-221-239-177-21.ngrok.io
# APIv2密钥
wxpay.partnerKey: T6m9iK73b0kn9g5v426MKfHQH7X8rKwb
1.2 读取支付参数
package com.atguigu.paymentdemo.config;
@Configuration
@PropertySource("classpath:wxpay.properties") // 读取配置文件
@ConfigurationProperties(prefix="wxpay") // 读取wxpay节点
@Data // 使用set方法将wxpay节点中的值填充到当前类的属性中
@Slf4j
public class WxPayConfig {
// 商户号
private String mchId;
// 商户API证书序列号
private String mchSerialNo;
// 商户私钥文件
private String privateKeyPath;
// APIv3密钥
private String apiV3Key;
// APPID
private String appid;
// 微信服务器地址
private String domain;
// 接收结果通知地址
private String notifyDomain;
// APIv2密钥
private String partnerKey;
/**
* 获取商户的私钥文件
* @param filename
* @return
*/
public PrivateKey getPrivateKey(String filename){
try {
return PemUtil.loadPrivateKey(new FileInputStream(filename));
} catch (FileNotFoundException e) {
throw new RuntimeException("私钥文件不存在", e);
}
}
/**
* 获取签名验证器
* @return
*/
@Bean
public ScheduledUpdateCertificatesVerifier getVerifier(){
log.info("获取签名验证器");
//获取商户私钥
PrivateKey privateKey = getPrivateKey(privateKeyPath);
//私钥签名对象
PrivateKeySigner privateKeySigner = new PrivateKeySigner(mchSerialNo, privateKey);
//身份认证对象
WechatPay2Credentials wechatPay2Credentials = new WechatPay2Credentials(mchId, privateKeySigner);
// 使用定时更新的签名验证器,不需要传入证书
ScheduledUpdateCertificatesVerifier verifier = new ScheduledUpdateCertificatesVerifier(
wechatPay2Credentials,
apiV3Key.getBytes(StandardCharsets.UTF_8));
return verifier;
}
/**
* 获取http请求对象
* @param verifier
* @return
*/
@Bean(name = "wxPayClient")
public CloseableHttpClient getWxPayClient(ScheduledUpdateCertificatesVerifier verifier){
log.info("获取httpClient");
//获取商户私钥
PrivateKey privateKey = getPrivateKey(privateKeyPath);
WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create()
.withMerchant(mchId, mchSerialNo, privateKey)
.withValidator(new WechatPay2Validator(verifier));
// ... 接下来,你仍然可以通过builder设置各种参数,来配置你的HttpClient
// 通过WechatPayHttpClientBuilder构造的HttpClient,会自动的处理签名和验签,并进行证书自动更新
CloseableHttpClient httpClient = builder.build();
return httpClient;
}
/**
* 获取HttpClient,无需进行应答签名验证,跳过验签的流程
*/
@Bean(name = "wxPayNoSignClient")
public CloseableHttpClient getWxPayNoSignClient(){
//获取商户私钥
PrivateKey privateKey = getPrivateKey(privateKeyPath);
//用于构造HttpClient
WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create()
//设置商户信息
.withMerchant(mchId, mchSerialNo, privateKey)
//无需进行签名验证、通过withValidator((response) -> true)实现
.withValidator((response) -> true);
// 通过WechatPayHttpClientBuilder构造的HttpClient,会自动的处理签名和验签,并进行证书自动更新
CloseableHttpClient httpClient = builder.build();
log.info("== getWxPayNoSignClient END ==");
return httpClient;
}
}
1.3 测试支付参数的获取
在controller 包中创建TestController
package com.atguigu.paymentdemo.controller;
@Api(tags = "测试控制器")
@RestController
@RequestMapping("/api/test")
public class TestController {
@Resource
private WxPayConfig wxPayConfig;
@GetMapping
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,帮助我们完成开发。实现了请求签名的生成和应答签名的验证。
<!--微信支付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 (如何加载商户私钥)
@Configuration
@PropertySource("classpath:wxpay.properties") // 读取配置文件
@ConfigurationProperties(prefix="wxpay") // 读取wxpay节点
@Data // 使用set方法将wxpay节点中的值填充到当前类的属性中
@Slf4j
public class WxPayConfig {
/**
* 获取商户的私钥文件
* @param filename
* @return
*/
public PrivateKey getPrivateKey(String filename){
try {
return PemUtil.loadPrivateKey(new FileInputStream(filename));
} catch (FileNotFoundException e) {
throw new RuntimeException("私钥文件不存在", e);
}
}
}
2.4 测试商户私钥的获取
在PaymentDemoApplicationTests 测试类中添加如下方法,测试私钥对象是否能够获取出来。
(将前面的方法改成public的再进行测试)
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 (定时更新平台证书功能)
平台证书:平台证书封装了微信的公钥,商户可以使用平台证书中的公钥进行验签。
签名验证器:帮助我们进行验签工作,我们单独将它定义出来,方便后面的开发。
@Configuration
@PropertySource("classpath:wxpay.properties") // 读取配置文件
@ConfigurationProperties(prefix="wxpay") // 读取wxpay节点
@Data // 使用set方法将wxpay节点中的值填充到当前类的属性中
@Slf4j
public class WxPayConfig {
/**
* 获取签名验证器
* @return
*/
@Bean
public ScheduledUpdateCertificatesVerifier getVerifier(){
log.info("获取签名验证器");
//获取商户私钥
PrivateKey privateKey = getPrivateKey(privateKeyPath);
//私钥签名对象
PrivateKeySigner privateKeySigner = new PrivateKeySigner(mchSerialNo, privateKey);
//身份认证对象
WechatPay2Credentials wechatPay2Credentials = new WechatPay2Credentials(mchId, privateKeySigner);
// 使用定时更新的签名验证器,不需要传入证书
ScheduledUpdateCertificatesVerifier verifier = new ScheduledUpdateCertificatesVerifier(
wechatPay2Credentials,
apiV3Key.getBytes(StandardCharsets.UTF_8));
return verifier;
}
}
3.4 获取 HttpClient 对象
https://github.com/wechatpay-apiv3/wechatpay-apache-httpclient (定时更新平台证书功能)
HttpClient 对象:是建立远程连接的基础,我们通过SDK创建这个对象。
@Configuration
@PropertySource("classpath:wxpay.properties") // 读取配置文件
@ConfigurationProperties(prefix="wxpay") // 读取wxpay节点
@Data // 使用set方法将wxpay节点中的值填充到当前类的属性中
@Slf4j
public class WxPayConfig {
/**
* 获取http请求对象
* @param verifier
* @return
*/
@Bean(name = "wxPayClient")
public CloseableHttpClient getWxPayClient(ScheduledUpdateCertificatesVerifier verifier){
log.info("获取httpClient");
//获取商户私钥
PrivateKey privateKey = getPrivateKey(privateKeyPath);
WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create()
.withMerchant(mchId, mchSerialNo, privateKey)
.withValidator(new WechatPay2Validator(verifier));
// ... 接下来,你仍然可以通过builder设置各种参数,来配置你的HttpClient
// 通过WechatPayHttpClientBuilder构造的HttpClient,会自动的处理签名和验签,并进行证书自动更新
CloseableHttpClient httpClient = builder.build();
return httpClient;
}
/**
* 获取HttpClient,无需进行应答签名验证,跳过验签的流程
*/
@Bean(name = "wxPayNoSignClient")
public CloseableHttpClient getWxPayNoSignClient(){
//获取商户私钥
PrivateKey privateKey = getPrivateKey(privateKeyPath);
//用于构造HttpClient
WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create()
//设置商户信息
.withMerchant(mchId, mchSerialNo, privateKey)
//无需进行签名验证、通过withValidator((response) -> true)实现
.withValidator((response) -> true);
// 通过WechatPayHttpClientBuilder构造的HttpClient,会自动的处理签名和验签,并进行证书自动更新
CloseableHttpClient httpClient = builder.build();
log.info("== getWxPayNoSignClient END ==");
return httpClient;
}
}
4、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 目录复制到源码目录中。
为了开发方便,我们预先在项目中定义一些枚举。枚举中定义的内容包括接口地址,支付状态等信息。
4.4 添加工具类
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();
}
}
}
}
}
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;
}
}
public class HttpClientUtils {
private String url;
private Map<String, String> param;
private int statusCode;
private String content;
private String xmlParam;
private boolean isHttps;
public boolean isHttps() {
return isHttps;
}
public void setHttps(boolean isHttps) {
this.isHttps = isHttps;
}
public String getXmlParam() {
return xmlParam;
}
public void setXmlParam(String xmlParam) {
this.xmlParam = xmlParam;
}
public HttpClientUtils(String url, Map<String, String> param) {
this.url = url;
this.param = param;
}
public HttpClientUtils(String url) {
this.url = url;
}
public void setParameter(Map<String, String> map) {
param = map;
}
public void addParameter(String key, String value) {
if (param == null)
param = new HashMap<String, String>();
param.put(key, value);
}
public void post() throws ClientProtocolException, IOException {
HttpPost http = new HttpPost(url);
setEntity(http);
execute(http);
}
public void put() throws ClientProtocolException, IOException {
HttpPut http = new HttpPut(url);
setEntity(http);
execute(http);
}
public void get() throws ClientProtocolException, IOException {
if (param != null) {
StringBuilder url = new StringBuilder(this.url);
boolean isFirst = true;
for (String key : param.keySet()) {
if (isFirst) {
url.append("?");
isFirst = false;
}else {
url.append("&");
}
url.append(key).append("=").append(param.get(key));
}
this.url = url.toString();
}
HttpGet http = new HttpGet(url);
execute(http);
}
/**
* set http post,put param
*/
private void setEntity(HttpEntityEnclosingRequestBase http) {
if (param != null) {
List<NameValuePair> nvps = new LinkedList<NameValuePair>();
for (String key : param.keySet())
nvps.add(new BasicNameValuePair(key, param.get(key))); // 参数
http.setEntity(new UrlEncodedFormEntity(nvps, Consts.UTF_8)); // 设置参数
}
if (xmlParam != null) {
http.setEntity(new StringEntity(xmlParam, Consts.UTF_8));
}
}
private void execute(HttpUriRequest http) throws ClientProtocolException,
IOException {
CloseableHttpClient httpClient = null;
try {
if (isHttps) {
SSLContext sslContext = new SSLContextBuilder()
.loadTrustMaterial(null, new TrustStrategy() {
// 信任所有
public boolean isTrusted(X509Certificate[] chain,
String authType)
throws CertificateException {
return true;
}
}).build();
SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(
sslContext);
httpClient = HttpClients.custom().setSSLSocketFactory(sslsf)
.build();
} else {
httpClient = HttpClients.createDefault();
}
CloseableHttpResponse response = httpClient.execute(http);
try {
if (response != null) {
if (response.getStatusLine() != null)
statusCode = response.getStatusLine().getStatusCode();
HttpEntity entity = response.getEntity();
// 响应内容
content = EntityUtils.toString(entity, Consts.UTF_8);
}
} finally {
response.close();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
httpClient.close();
}
}
public int getStatusCode() {
return statusCode;
}
public String getContent() throws ParseException, IOException {
return content;
}
}
public class WechatPay2ValidatorForRequest {
protected static final Logger log = LoggerFactory.getLogger(WechatPay2ValidatorForRequest.class);
/**
* 应答超时时间,单位为分钟
*/
protected static final long RESPONSE_EXPIRED_MINUTES = 5;
protected final Verifier verifier;
protected final String requestId;
protected final String body;
public WechatPay2ValidatorForRequest(Verifier verifier, String requestId, String body) {
this.verifier = verifier;
this.requestId = requestId;
this.body = body;
}
protected static IllegalArgumentException parameterError(String message, Object... args) {
message = String.format(message, args);
return new IllegalArgumentException("parameter error: " + message);
}
protected static IllegalArgumentException verifyFail(String message, Object... args) {
message = String.format(message, args);
return new IllegalArgumentException("signature verify fail: " + message);
}
public final boolean validate(HttpServletRequest request) throws IOException {
try {
//处理请求参数
validateParameters(request);
//构造验签名串
String message = buildMessage(request);
String serial = request.getHeader(WECHAT_PAY_SERIAL);
String signature = request.getHeader(WECHAT_PAY_SIGNATURE);
//验签
if (!verifier.verify(serial, message.getBytes(StandardCharsets.UTF_8), signature)) {
throw verifyFail("serial=[%s] message=[%s] sign=[%s], request-id=[%s]",
serial, message, signature, requestId);
}
} catch (IllegalArgumentException e) {
log.warn(e.getMessage());
return false;
}
return true;
}
protected final void validateParameters(HttpServletRequest request) {
// NOTE: ensure HEADER_WECHAT_PAY_TIMESTAMP at last
String[] headers = {WECHAT_PAY_SERIAL, WECHAT_PAY_SIGNATURE, WECHAT_PAY_NONCE, WECHAT_PAY_TIMESTAMP};
String header = null;
for (String headerName : headers) {
header = request.getHeader(headerName);
if (header == null) {
throw parameterError("empty [%s], request-id=[%s]", headerName, requestId);
}
}
//判断请求是否过期
String timestampStr = header;
try {
Instant responseTime = Instant.ofEpochSecond(Long.parseLong(timestampStr));
// 拒绝过期请求
if (Duration.between(responseTime, Instant.now()).abs().toMinutes() >= RESPONSE_EXPIRED_MINUTES) {
throw parameterError("timestamp=[%s] expires, request-id=[%s]", timestampStr, requestId);
}
} catch (DateTimeException | NumberFormatException e) {
throw parameterError("invalid timestamp=[%s], request-id=[%s]", timestampStr, requestId);
}
}
protected final String buildMessage(HttpServletRequest request) throws IOException {
String timestamp = request.getHeader(WECHAT_PAY_TIMESTAMP);
String nonce = request.getHeader(WECHAT_PAY_NONCE);
return timestamp + "\n"
+ nonce + "\n"
+ body + "\n";
}
protected final String getResponseBody(CloseableHttpResponse response) throws IOException {
HttpEntity entity = response.getEntity();
return (entity != null && entity.isRepeatable()) ? EntityUtils.toString(entity) : "";
}
}
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
@CrossOrigin //跨域
@RestController
@RequestMapping("/api/wx-pay")
@Api(tags = "网站微信支付APIv3")
@Slf4j
public class WxPayController {
@Resource
private WxPayService wxPayService;
@Resource
private Verifier verifier;
/**
* Native下单
* @param productId
* @return
* @throws Exception
*/
@ApiOperation("调用统一下单API,生成支付二维码")
@PostMapping("/native/{productId}")
public R nativePay(@PathVariable Long productId) throws Exception {
log.info("发起支付请求 v3");
// 返回支付二维码连接和订单号
Map<String, Object> map = wxPayService.nativePay(productId);
return R.ok().setData(map);
}
}
R对象中添加@Accessors(chain = true),使其可以链式操作
@Data
@Accessors(chain = true) //链式操作
public class R {
}
(2)创建WxPayService
public interface WxPayService {
Map<String, Object> nativePay(Long productId) throws Exception;
}
参考:
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
@Service
@Slf4j
public class WxPayServiceImpl implements WxPayService {
@Resource
private WxPayConfig wxPayConfig;
@Resource
private CloseableHttpClient wxPayClient;
@Resource
private OrderInfoService orderInfoService;
@Resource
private PaymentInfoService paymentInfoService;
@Resource
private RefundInfoService refundsInfoService;
@Resource
private CloseableHttpClient wxPayNoSignClient; //无需应答签名
private final ReentrantLock lock = new ReentrantLock();
/**
* 创建订单,调用Native支付接口
* @param productId
* @return code_url 和 订单号
* @throws Exception
*/
@Transactional(rollbackFor = Exception.class)
@Override
public Map<String, Object> nativePay(Long productId) throws Exception {
log.info("生成订单");
//生成订单
OrderInfo orderInfo = orderInfoService.createOrderByProductId(productId, PayType.WXPAY.getType());
String codeUrl = orderInfo.getCodeUrl();
if(orderInfo != null && !StringUtils.isEmpty(codeUrl)){
log.info("订单已存在,二维码已保存");
//返回二维码
Map<String, Object> map = new HashMap<>();
map.put("codeUrl", codeUrl);
map.put("orderNo", orderInfo.getOrderNo());
return map;
}
log.info("调用统一下单API");
//调用统一下单API
HttpPost httpPost = new HttpPost(wxPayConfig.getDomain().concat(WxApiType.NATIVE_PAY.getType()));
// 请求body参数
Gson gson = new Gson();
Map paramsMap = new HashMap();
paramsMap.put("appid", wxPayConfig.getAppid());
paramsMap.put("mchid", wxPayConfig.getMchId());
paramsMap.put("description", orderInfo.getTitle());
paramsMap.put("out_trade_no", orderInfo.getOrderNo());
paramsMap.put("notify_url", wxPayConfig.getNotifyDomain().concat(WxNotifyType.NATIVE_NOTIFY.getType()));
Map amountMap = new HashMap();
amountMap.put("total", orderInfo.getTotalFee());
amountMap.put("currency", "CNY");
paramsMap.put("amount", amountMap);
//将参数转换成json字符串
String jsonParams = gson.toJson(paramsMap);
log.info("请求参数 ===> {}" + jsonParams);
StringEntity entity = new StringEntity(jsonParams,"utf-8");
entity.setContentType("application/json");
httpPost.setEntity(entity);
httpPost.setHeader("Accept", "application/json");
//完成签名并执行请求
CloseableHttpResponse response = wxPayClient.execute(httpPost);
try {
String bodyAsString = EntityUtils.toString(response.getEntity());//响应体
int statusCode = response.getStatusLine().getStatusCode();//响应状态码
if (statusCode == 200) { //处理成功
log.info("成功, 返回结果 = " + bodyAsString);
} else if (statusCode == 204) { //处理成功,无返回Body
log.info("成功");
} else {
log.info("Native下单失败,响应码 = " + statusCode+ ",返回结果 = " + bodyAsString);
throw new IOException("request failed");
}
//响应结果
Map<String, String> resultMap = gson.fromJson(bodyAsString, HashMap.class);
//二维码
codeUrl = resultMap.get("code_url");
//保存二维码
String orderNo = orderInfo.getOrderNo();
orderInfoService.saveCodeUrl(orderNo, codeUrl);
//返回二维码
Map<String, Object> map = new HashMap<>();
map.put("codeUrl", codeUrl);
map.put("orderNo", orderInfo.getOrderNo());
return map;
} finally {
response.close();
}
}
}
5.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)保存订单
public interface OrderInfoService extends IService<OrderInfo> {
OrderInfo createOrderByProductId(Long productId, String paymentType);
}
@Service
@Slf4j
public class OrderInfoServiceImpl extends ServiceImpl<OrderInfoMapper, OrderInfo> implements OrderInfoService {
@Resource
private ProductMapper productMapper;
@Override
public OrderInfo createOrderByProductId(Long productId, String paymentType) {
// 查找已存在但未支付的订单
OrderInfo orderInfo = this.getNoPayOrderByProductId(productId, paymentType);
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()); //未支付
orderInfo.setPaymentType(paymentType);
baseMapper.insert(orderInfo);
return orderInfo;
}
}
查找未支付订单:OrderInfoService中添加辅助方法
/**
* 根据商品id查询未支付订单
* 防止重复创建订单对象
* @param productId
* @return
*/
private OrderInfo getNoPayOrderByProductId(Long productId, String paymentType) {
QueryWrapper<OrderInfo> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("product_id", productId);
queryWrapper.eq("order_status", OrderStatus.NOTPAY.getType());
queryWrapper.eq("payment_type", paymentType);
// queryWrapper.eq("user_id", userId);
OrderInfo orderInfo = baseMapper.selectOne(queryWrapper);
return orderInfo;
}
(2)缓存二维码
public interface OrderInfoService extends IService<OrderInfo> {
void saveCodeUrl(String orderNo, String codeUrl);
}
@Service
@Slf4j
public class OrderInfoServiceImpl extends ServiceImpl<OrderInfoMapper, OrderInfo> implements OrderInfoService {
@Resource
private ProductMapper productMapper;
/**
* 存储订单二维码
* @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);
}
}
(3)修改WxPayServiceImpl 的nativePay方法
@Service
@Slf4j
public class WxPayServiceImpl implements WxPayService {
@Resource
private WxPayConfig wxPayConfig;
@Resource
private CloseableHttpClient wxPayClient;
@Resource
private OrderInfoService orderInfoService;
@Resource
private PaymentInfoService paymentInfoService;
@Resource
private RefundInfoService refundsInfoService;
@Resource
private CloseableHttpClient wxPayNoSignClient; //无需应答签名
private final ReentrantLock lock = new ReentrantLock();
/**
* 创建订单,调用Native支付接口
* @param productId
* @return code_url 和 订单号
* @throws Exception
*/
@Transactional(rollbackFor = Exception.class)
@Override
public Map<String, Object> nativePay(Long productId) throws Exception {
log.info("生成订单");
//生成订单
OrderInfo orderInfo = orderInfoService.createOrderByProductId(productId, PayType.WXPAY.getType());
String codeUrl = orderInfo.getCodeUrl();
if(orderInfo != null && !StringUtils.isEmpty(codeUrl)){
log.info("订单已存在,二维码已保存");
//返回二维码
Map<String, Object> map = new HashMap<>();
map.put("codeUrl", codeUrl);
map.put("orderNo", orderInfo.getOrderNo());
return map;
}
log.info("调用统一下单API");
//调用统一下单API
HttpPost httpPost = new HttpPost(wxPayConfig.getDomain().concat(WxApiType.NATIVE_PAY.getType()));
// 请求body参数
Gson gson = new Gson();
Map paramsMap = new HashMap();
paramsMap.put("appid", wxPayConfig.getAppid());
paramsMap.put("mchid", wxPayConfig.getMchId());
paramsMap.put("description", orderInfo.getTitle());
paramsMap.put("out_trade_no", orderInfo.getOrderNo());
paramsMap.put("notify_url", wxPayConfig.getNotifyDomain().concat(WxNotifyType.NATIVE_NOTIFY.getType()));
Map amountMap = new HashMap();
amountMap.put("total", orderInfo.getTotalFee());
amountMap.put("currency", "CNY");
paramsMap.put("amount", amountMap);
//将参数转换成json字符串
String jsonParams = gson.toJson(paramsMap);
log.info("请求参数 ===> {}" + jsonParams);
StringEntity entity = new StringEntity(jsonParams,"utf-8");
entity.setContentType("application/json");
httpPost.setEntity(entity);
httpPost.setHeader("Accept", "application/json");
//完成签名并执行请求
CloseableHttpResponse response = wxPayClient.execute(httpPost);
try {
String bodyAsString = EntityUtils.toString(response.getEntity());//响应体
int statusCode = response.getStatusLine().getStatusCode();//响应状态码
if (statusCode == 200) { //处理成功
log.info("成功, 返回结果 = " + bodyAsString);
} else if (statusCode == 204) { //处理成功,无返回Body
log.info("成功");
} else {
log.info("Native下单失败,响应码 = " + statusCode+ ",返回结果 = " + bodyAsString);
throw new IOException("request failed");
}
//响应结果
Map<String, String> resultMap = gson.fromJson(bodyAsString, HashMap.class);
//二维码
codeUrl = resultMap.get("code_url");
//保存二维码
String orderNo = orderInfo.getOrderNo();
orderInfoService.saveCodeUrl(orderNo, codeUrl);
//返回二维码
Map<String, Object> map = new HashMap<>();
map.put("codeUrl", codeUrl);
map.put("orderNo", orderInfo.getOrderNo());
return map;
} finally {
response.close();
}
}
}
5.5 显示订单列表
在我的订单页面按时间倒序显示订单列表
(1)创建OrderInfoController
@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 方法
/**
* 查询订单列表,并倒序查询
* @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 6aYc6Kp7kpxVr8pY88LkG_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<>();//应答对象
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);
//int a = 9 / 0;
//签名的验证
WechatPay2ValidatorForRequest wechatPay2ValidatorForRequest
= new WechatPay2ValidatorForRequest(verifier, requestId, body);
if(!wechatPay2ValidatorForRequest.validate(request)){
log.error("通知验签失败");
//失败应答
response.setStatus(500);
map.put("code", "ERROR");
map.put("message", "通知验签失败");
return gson.toJson(map);
}
log.info("通知验签成功");
//处理订单
wxPayService.processOrder(bodyMap);
//应答超时
//模拟接收微信端的重复通知
TimeUnit.SECONDS.sleep(5);
//成功应答
response.setStatus(200);
map.put("code", "SUCCESS");
map.put("message", "成功");
return gson.toJson(map);
} catch (Exception e) {
e.printStackTrace();
//失败应答
response.setStatus(500);
map.put("code", "ERROR");
map.put("message", "失败");
return gson.toJson(map);
}
}
(4)测试超时应答
回调通知注意事项: 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)验签
6.4 解密
(1)WxPayController
nativeNotify方法中添加处理订单的代码
// 处理订单
wxPayService.processOrder(bodyMap);
(2)WxPayService
@Transactional(rollbackFor = Exception.class)
@Override
public void processOrder(Map<String, Object> bodyMap) throws GeneralSecurityException {
log.info("处理订单");
//解密报文
String plainText = decryptFromResource(bodyMap);
//将明文转换成map
Gson gson = new Gson();
HashMap plainTextMap = gson.fromJson(plainText, HashMap.class);
String orderNo = (String)plainTextMap.get("out_trade_no");
/*在对业务数据进行状态检查和处理之前,
要采用数据锁进行并发控制,
以避免函数重入造成的数据混乱*/
//尝试获取锁:
// 成功获取则立即返回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();
}
}
}
辅助方法:
/**
* 对称解密
* @param bodyMap
* @return
*/
private String decryptFromResource(Map<String, Object> bodyMap) throws GeneralSecurityException {
log.info("密文解密");
//通知数据
Map<String, String> resourceMap = (Map) bodyMap.get("resource");
//数据密文
String ciphertext = resourceMap.get("ciphertext");
//随机串
String nonce = resourceMap.get("nonce");
//附加数据
String associatedData = resourceMap.get("associated_data");
log.info("密文 ===> {}", ciphertext);
AesUtil aesUtil = new AesUtil(wxPayConfig.getApiV3Key().getBytes(StandardCharsets.UTF_8));
String plainText = aesUtil.decryptToString(associatedData.getBytes(StandardCharsets.UTF_8),
nonce.getBytes(StandardCharsets.UTF_8),
ciphertext);
log.info("明文 ===> {}", plainText);
return plainText;
}
6.5 处理订单
(1)完善processOrder方法
(2)更新订单状态
/**
* 根据订单号更新订单状态
* @param orderNo
* @param orderStatus
*/
@Override
public void updateStatusByOrderNo(String orderNo, OrderStatus orderStatus) {
log.info("更新订单状态 ===> {}", orderStatus.getType());
QueryWrapper<OrderInfo> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("order_no", orderNo);
OrderInfo orderInfo = new OrderInfo();
orderInfo.setOrderStatus(orderStatus.getType());
baseMapper.update(orderInfo, queryWrapper);
}
(3)处理支付日志
/**
* 记录支付日志:微信支付
* @param plainText
*/
@Override
public void createPaymentInfo(String plainText) {
log.info("记录支付日志");
Gson gson = new Gson();
HashMap plainTextMap = gson.fromJson(plainText, HashMap.class);
//订单号
String orderNo = (String)plainTextMap.get("out_trade_no");
//业务编号
String transactionId = (String)plainTextMap.get("transaction_id");
//支付类型
String tradeType = (String)plainTextMap.get("trade_type");
//交易状态
String tradeState = (String)plainTextMap.get("trade_state");
//用户实际支付金额
Map<String, Object> amount = (Map)plainTextMap.get("amount");
Integer payerTotal = ((Double) amount.get("payer_total")).intValue();
PaymentInfo paymentInfo = new PaymentInfo();
paymentInfo.setOrderNo(orderNo);
paymentInfo.setPaymentType(PayType.WXPAY.getType());
paymentInfo.setTransactionId(transactionId);
paymentInfo.setTradeType(tradeType);
paymentInfo.setTradeState(tradeState);
paymentInfo.setPayerTotal(payerTotal);
paymentInfo.setContent(plainText);
baseMapper.insert(paymentInfo);
}
6.6 处理重复通知
(1)测试重复的通知
// 应答超时
// 设置响应超时,可以接收到微信支付的重复的支付结果通知。
// 通知重复,数据库会记录多余的支付日志
TimeUnit.SECONDS.sleep(5);
(2)处理重复通知
在 processOrder 方法中,更新订单状态之前,添加如下代码
// 处理重复通知
// 保证接口调用的幂等性:无论接口被调用多少次,产生的结果是一致的
String orderStatus = orderInfoService.getOrderStatus(orderNo);
if (!OrderStatus.NOTPAY.getType().equals(orderStatus)) {
return;
}
/**
* 根据订单号获取订单状态
* @param orderNo
* @return
*/
@Override
public String getOrderStatus(String orderNo) {
QueryWrapper<OrderInfo> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("order_no", orderNo);
OrderInfo orderInfo = baseMapper.selectOne(queryWrapper);
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();
7、商户定时查询本地订单
7.1 后端定义商户查单接口
支付成功后,商户侧查询本地数据库,订单是否支付成功
/**
* 查询本地订单状态
* @param orderNo
* @return
*/
@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("支付中......");
}
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 定义取消订单接口
/**
* 用户取消订单
* @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 具体实现取消订单
/**
* 用户取消订单
* @param orderNo
*/
@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();
}
}
9、微信支付查单API
9.1 查单接口的调用
商户后台未收到异步支付结果通知时,商户应该主动调用《微信支付查单接口》,同步订单状态。
(1)WxPayController
/**
* 查询订单
* @param orderNo
* @return
* @throws Exception
*/
@ApiOperation("查询订单:测试订单状态用")
@GetMapping("/query/{orderNo}")
public R queryOrder(@PathVariable String orderNo) throws Exception {
log.info("查询订单");
String result = wxPayService.queryOrder(orderNo);
return R.ok().setMessage("查询成功").data("result", result);
}
(2)WxPayServiceImpl
@Override
public String queryOrder(String orderNo) throws Exception {
log.info("查单接口调用 ===> {}", orderNo);
String url = String.format(WxApiType.ORDER_QUERY_BY_NO.getType(), orderNo);
url = wxPayConfig.getDomain().concat(url).concat("?mchid=").concat(wxPayConfig.getMchId());
HttpGet httpGet = new HttpGet(url);
httpGet.setHeader("Accept", "application/json");
//完成签名并执行请求
CloseableHttpResponse response = wxPayClient.execute(httpGet);
try {
String bodyAsString = EntityUtils.toString(response.getEntity());//响应体
int statusCode = response.getStatusLine().getStatusCode();//响应状态码
if (statusCode == 200) { //处理成功
log.info("成功, 返回结果 = " + bodyAsString);
} else if (statusCode == 204) { //处理成功,无返回Body
log.info("成功");
} else {
log.info("查单接口调用,响应码 = " + statusCode+ ",返回结果 = " + bodyAsString);
throw new IOException("request failed");
}
return bodyAsString;
} finally {
response.close();
}
}
9.2 集成Spring Task
Spring 3.0后提供Spring Task实现任务调度
(1)启动类添加注解
@EnableScheduling
(2)测试定时任务
创建task 包,创建WxPayTask.java
@Slf4j
@Component
public class WxPayTask {
/**
* 秒 分 时 日 月 周
* 以秒为例
* *:每秒都执行
* 1-3:从第1秒开始执行,到第3秒结束执行
* 0/3:从第0秒开始,每隔3秒执行1次
* 1,2,3:在指定的第1、2、3秒执行
* ?:不指定
* 日和周不能同时制定,指定其中之一,则另一个设置为?
*/
@Scheduled(cron = "0/3 * * * * ?")
public void task1(){
log.info("task1 被执行......");
}
}
9.3 定时查找超时订单
(1)WxPayTask
@Slf4j
@Component
public class WxPayTask {
@Resource
private OrderInfoService orderInfoService;
@Resource
private WxPayService wxPayService;
/**
* 从第0秒开始每隔30秒执行1次,查询创建超过5分钟,并且未支付的订单
*/
@Scheduled(cron = "0/30 * * * * ?")
public void orderConfirm() throws Exception {
log.info("orderConfirm 被执行......");
List<OrderInfo> orderInfoList = orderInfoService.getNoPayOrderByDuration(1, PayType.WXPAY.getType());
for (OrderInfo orderInfo : orderInfoList) {
String orderNo = orderInfo.getOrderNo();
log.warn("超时订单 ===> {}", orderNo);
//核实订单状态:调用微信支付查单接口
wxPayService.checkOrderStatus(orderNo);
}
}
}
(2)OrderInfoServiceImpl
/**
* 查询创建超过minutes分钟并且未支付的订单
* @param minutes
* @return
*/
@Override
public List<OrderInfo> getNoPayOrderByDuration(int minutes, String paymentType) {
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);
queryWrapper.eq("payment_type", paymentType);
List<OrderInfo> orderInfoList = baseMapper.selectList(queryWrapper);
return orderInfoList;
}
9.4 处理超时订单
/**
* 根据订单号查询微信支付查单接口,核实订单状态
* 如果订单已支付,则更新商户端订单状态,并记录支付日志
* 如果订单未支付,则调用关单接口关闭订单,并更新商户端订单状态
* @param orderNo
*/
@Transactional(rollbackFor = Exception.class)
@Override
public void checkOrderStatus(String orderNo) throws Exception {
log.warn("根据订单号核实订单状态 ===> {}", orderNo);
//调用微信支付查单接口
String result = this.queryOrder(orderNo);
Gson gson = new Gson();
Map<String, String> resultMap = gson.fromJson(result, HashMap.class);
//获取微信支付端的订单状态
String 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);
}
}
11、申请退款API
文档: https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_9.shtml
11.1 创建退款单
(1)根据订单号查询订单
/**
* 根据订单号获取订单
* @param orderNo
* @return
*/
@Override
public OrderInfo getOrderByOrderNo(String orderNo) {
QueryWrapper<OrderInfo> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("order_no", orderNo);
OrderInfo orderInfo = baseMapper.selectOne(queryWrapper);
return orderInfo;
}
(2)创建退款单记录
/**
* 根据订单号创建退款订单
* @param orderNo
* @return
*/
@Override
public RefundInfo createRefundByOrderNo(String orderNo, String reason) {
//根据订单号获取订单信息
OrderInfo orderInfo = orderInfoService.getOrderByOrderNo(orderNo);
//根据订单号生成退款订单
RefundInfo refundInfo = new RefundInfo();
refundInfo.setOrderNo(orderNo);//订单编号
refundInfo.setRefundNo(OrderNoUtils.getRefundNo());//退款单编号
refundInfo.setTotalFee(orderInfo.getTotalFee());//原订单金额(分)
refundInfo.setRefund(orderInfo.getTotalFee());//退款金额(分)
refundInfo.setReason(reason);//退款原因
//保存退款订单
baseMapper.insert(refundInfo);
return refundInfo;
}
11.2 更新退款单
/**
* 记录退款记录
* @param content
*/
@Override
public void updateRefund(String content) {
//将json字符串转换成Map
Gson gson = new Gson();
Map<String, String> resultMap = gson.fromJson(content, HashMap.class);
//根据退款单编号修改退款单
QueryWrapper<RefundInfo> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("refund_no", resultMap.get("out_refund_no"));
//设置要修改的字段
RefundInfo refundInfo = new RefundInfo();
refundInfo.setRefundId(resultMap.get("refund_id"));//微信支付退款单号
//查询退款和申请退款中的返回参数
if(resultMap.get("status") != null){
refundInfo.setRefundStatus(resultMap.get("status"));//退款状态
refundInfo.setContentReturn(content);//将全部响应结果存入数据库的content字段
}
//退款回调中的回调参数
if(resultMap.get("refund_status") != null){
refundInfo.setRefundStatus(resultMap.get("refund_status"));//退款状态
refundInfo.setContentNotify(content);//将全部响应结果存入数据库的content字段
}
//更新退款单
baseMapper.update(refundInfo, queryWrapper);
}
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)WxPayServiceImpl
/**
* 退款
* @param orderNo
* @param reason
* @throws IOException
*/
@Transactional(rollbackFor = Exception.class)
@Override
public void refund(String orderNo, String reason) throws Exception {
log.info("创建退款单记录");
//根据订单编号创建退款单
RefundInfo refundsInfo = refundsInfoService.createRefundByOrderNo(orderNo, reason);
log.info("调用退款API");
//调用统一下单API
String url = wxPayConfig.getDomain().concat(WxApiType.DOMESTIC_REFUNDS.getType());
HttpPost httpPost = new HttpPost(url);
// 请求body参数
Gson gson = new Gson();
Map paramsMap = new HashMap();
paramsMap.put("out_trade_no", orderNo);//订单编号
paramsMap.put("out_refund_no", refundsInfo.getRefundNo());//退款单编号
paramsMap.put("reason",reason);//退款原因
paramsMap.put("notify_url", wxPayConfig.getNotifyDomain().concat(WxNotifyType.REFUND_NOTIFY.getType()));//退款通知地址
Map amountMap = new HashMap();
amountMap.put("refund", refundsInfo.getRefund());//退款金额
amountMap.put("total", refundsInfo.getTotalFee());//原订单金额
amountMap.put("currency", "CNY");//退款币种
paramsMap.put("amount", amountMap);
//将参数转换成json字符串
String jsonParams = gson.toJson(paramsMap);
log.info("请求参数 ===> {}" + jsonParams);
StringEntity entity = new StringEntity(jsonParams,"utf-8");
entity.setContentType("application/json");//设置请求报文格式
httpPost.setEntity(entity);//将请求报文放入请求对象
httpPost.setHeader("Accept", "application/json");//设置响应报文格式
//完成签名并执行请求,并完成验签
CloseableHttpResponse response = wxPayClient.execute(httpPost);
try {
//解析响应结果
String bodyAsString = EntityUtils.toString(response.getEntity());
int statusCode = response.getStatusLine().getStatusCode();
if (statusCode == 200) {
log.info("成功, 退款返回结果 = " + bodyAsString);
} else if (statusCode == 204) {
log.info("成功");
} else {
throw new RuntimeException("退款异常, 响应码 = " + statusCode+ ", 退款返回结果 = " + bodyAsString);
}
//更新订单状态
orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.REFUND_PROCESSING);
//更新退款单
refundsInfoService.updateRefund(bodyAsString);
} finally {
response.close();
}
}
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) WxPayServiceImpl
/**
* 查询退款接口调用
* @param refundNo
* @return
*/
@Override
public 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
@Slf4j
@Component
public class WxPayTask {
@Resource
private OrderInfoService orderInfoService;
@Resource
private WxPayService wxPayService;
@Resource
private RefundInfoService refundInfoService;
/**
* 从第0秒开始每隔30秒执行1次,查询创建超过5分钟,并且未成功的退款单
*/
@Scheduled(cron = "0/30 * * * * ?")
public void refundConfirm() throws Exception {
log.info("refundConfirm 被执行......");
// 找出申请退款超过5分钟并且未成功的退款单
List<RefundInfo> refundInfoList = refundInfoService.getNoRefundOrderByDuration(1);
for (RefundInfo refundInfo : refundInfoList) {
String refundNo = refundInfo.getRefundNo();
log.warn("超时未退款的退款单号 ===> {}", refundNo);
// 核实订单状态:调用微信支付查询退款接口
wxPayService.checkRefundStatus(refundNo);
}
}
}
(2) RefundInfoServiceImpl
/**
* 找出申请退款超过minutes分钟并且未成功的退款单
* @param minutes
* @return
*/
@Override
public List<RefundInfo> getNoRefundOrderByDuration(int minutes) {
//minutes分钟之前的时间
Instant instant = Instant.now().minus(Duration.ofMinutes(minutes));
QueryWrapper<RefundInfo> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("refund_status", WxRefundStatus.PROCESSING.getType());
queryWrapper.le("create_time", instant);
List<RefundInfo> refundInfoList = baseMapper.selectList(queryWrapper);
return refundInfoList;
}
12.3 处理超时未退款订单
核实订单状态:
/**
* 根据退款单号核实退款单状态
* @param refundNo
* @return
*/
@Transactional(rollbackFor = Exception.class)
@Override
public 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](https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_11.shtml)<br />**13.1 接收退款通知**
/**
* 退款结果通知
* 退款状态改变后,微信会把相关退款结果发送给商户。
*/
@ApiOperation("退款结果通知")
@PostMapping("/refunds/notify")
public String refundsNotify(HttpServletRequest request, HttpServletResponse response){
log.info("退款通知执行");
Gson gson = new Gson();
Map<String, String> map = new HashMap<>();//应答对象
try {
//处理通知参数
String body = HttpUtils.readData(request);
Map<String, Object> bodyMap = gson.fromJson(body, HashMap.class);
String requestId = (String)bodyMap.get("id");
log.info("支付通知的id ===> {}", requestId);
//签名的验证
WechatPay2ValidatorForRequest wechatPay2ValidatorForRequest
= new WechatPay2ValidatorForRequest(verifier, requestId, body);
if(!wechatPay2ValidatorForRequest.validate(request)){
log.error("通知验签失败");
//失败应答
response.setStatus(500);
map.put("code", "ERROR");
map.put("message", "通知验签失败");
return gson.toJson(map);
}
log.info("通知验签成功");
//处理退款单
wxPayService.processRefund(bodyMap);
//成功应答
response.setStatus(200);
map.put("code", "SUCCESS");
map.put("message", "成功");
return gson.toJson(map);
} catch (Exception e) {
e.printStackTrace();
//失败应答
response.setStatus(500);
map.put("code", "ERROR");
map.put("message", "失败");
return gson.toJson(map);
}
}
13.2 处理订单和退款单
/**
* 处理退款单
*/
@Transactional(rollbackFor = Exception.class)
@Override
public void processRefund(Map<String, Object> bodyMap) throws Exception {
log.info("退款单");
//解密报文
String plainText = decryptFromResource(bodyMap);
//将明文转换成map
Gson gson = new Gson();
HashMap plainTextMap = gson.fromJson(plainText, HashMap.class);
String orderNo = (String)plainTextMap.get("out_trade_no");
if(lock.tryLock()){
try {
String orderStatus = orderInfoService.getOrderStatus(orderNo);
if (!OrderStatus.REFUND_PROCESSING.getType().equals(orderStatus)) {
return;
}
//更新订单状态
orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.REFUND_SUCCESS);
//更新退款单
refundsInfoService.updateRefund(plainText);
} finally {
//要主动释放锁
lock.unlock();
}
}
}
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) WxPayServiceImpl
/**
* 申请账单
* @param billDate
* @param type
* @return
* @throws Exception
*/
@Override
public String queryBill(String billDate, String type) throws Exception {
log.warn("申请账单接口调用 {}", billDate);
String url = "";
if("tradebill".equals(type)){
url = WxApiType.TRADE_BILLS.getType();
}else if("fundflowbill".equals(type)){
url = WxApiType.FUND_FLOW_BILLS.getType();
}else{
throw new RuntimeException("不支持的账单类型");
}
url = wxPayConfig.getDomain().concat(url).concat("?bill_date=").concat(billDate);
//创建远程Get 请求对象
HttpGet httpGet = new HttpGet(url);
httpGet.addHeader("Accept", "application/json");
//使用wxPayClient发送请求得到响应
CloseableHttpResponse response = wxPayClient.execute(httpGet);
try {
String bodyAsString = EntityUtils.toString(response.getEntity());
int statusCode = response.getStatusLine().getStatusCode();
if (statusCode == 200) {
log.info("成功, 申请账单返回结果 = " + bodyAsString);
} else if (statusCode == 204) {
log.info("成功");
} else {
throw new RuntimeException("申请账单异常, 响应码 = " + statusCode+ ", 申请账单返回结果 = " + bodyAsString);
}
//获取账单下载地址
Gson gson = new Gson();
Map<String, String> resultMap = gson.fromJson(bodyAsString, HashMap.class);
return resultMap.get("download_url");
} finally {
response.close();
}
}
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) WxPayServiceImpl
/**
* 下载账单
* @param billDate
* @param type
* @return
* @throws Exception
*/
@Override
public String downloadBill(String billDate, String type) throws Exception {
log.warn("下载账单接口调用 {}, {}", billDate, type);
//获取账单url地址
String downloadUrl = this.queryBill(billDate, type);
//创建远程Get 请求对象
HttpGet httpGet = new HttpGet(downloadUrl);
httpGet.addHeader("Accept", "application/json");
//使用wxPayClient发送请求得到响应
CloseableHttpResponse response = wxPayNoSignClient.execute(httpGet);
try {
String bodyAsString = EntityUtils.toString(response.getEntity());
int statusCode = response.getStatusLine().getStatusCode();
if (statusCode == 200) {
log.info("成功, 下载账单返回结果 = " + bodyAsString);
} else if (statusCode == 204) {
log.info("成功");
} else {
throw new RuntimeException("下载账单异常, 响应码 = " + statusCode+ ", 下载账单返回结果 = " + bodyAsString);
}
return bodyAsString;
} finally {
response.close();
}
}
五、基础支付API V2
1、V2和V3的比较
2、引入依赖和工具
2.1 引入依赖
<!--微信支付 APIv2 SDK-->
<dependency>
<groupId>com.github.wxpay</groupId>
<artifactId>wxpay-sdk</artifactId>
<version>0.0.3</version>
</dependency>
2.2 复制工具类
public class HttpClientUtils {
private String url;
private Map<String, String> param;
private int statusCode;
private String content;
private String xmlParam;
private boolean isHttps;
public boolean isHttps() {
return isHttps;
}
public void setHttps(boolean isHttps) {
this.isHttps = isHttps;
}
public String getXmlParam() {
return xmlParam;
}
public void setXmlParam(String xmlParam) {
this.xmlParam = xmlParam;
}
public HttpClientUtils(String url, Map<String, String> param) {
this.url = url;
this.param = param;
}
public HttpClientUtils(String url) {
this.url = url;
}
public void setParameter(Map<String, String> map) {
param = map;
}
public void addParameter(String key, String value) {
if (param == null)
param = new HashMap<String, String>();
param.put(key, value);
}
public void post() throws ClientProtocolException, IOException {
HttpPost http = new HttpPost(url);
setEntity(http);
execute(http);
}
public void put() throws ClientProtocolException, IOException {
HttpPut http = new HttpPut(url);
setEntity(http);
execute(http);
}
public void get() throws ClientProtocolException, IOException {
if (param != null) {
StringBuilder url = new StringBuilder(this.url);
boolean isFirst = true;
for (String key : param.keySet()) {
if (isFirst) {
url.append("?");
isFirst = false;
}else {
url.append("&");
}
url.append(key).append("=").append(param.get(key));
}
this.url = url.toString();
}
HttpGet http = new HttpGet(url);
execute(http);
}
/**
* set http post,put param
*/
private void setEntity(HttpEntityEnclosingRequestBase http) {
if (param != null) {
List<NameValuePair> nvps = new LinkedList<NameValuePair>();
for (String key : param.keySet())
nvps.add(new BasicNameValuePair(key, param.get(key))); // 参数
http.setEntity(new UrlEncodedFormEntity(nvps, Consts.UTF_8)); // 设置参数
}
if (xmlParam != null) {
http.setEntity(new StringEntity(xmlParam, Consts.UTF_8));
}
}
private void execute(HttpUriRequest http) throws ClientProtocolException,
IOException {
CloseableHttpClient httpClient = null;
try {
if (isHttps) {
SSLContext sslContext = new SSLContextBuilder()
.loadTrustMaterial(null, new TrustStrategy() {
// 信任所有
public boolean isTrusted(X509Certificate[] chain,
String authType)
throws CertificateException {
return true;
}
}).build();
SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(
sslContext);
httpClient = HttpClients.custom().setSSLSocketFactory(sslsf)
.build();
} else {
httpClient = HttpClients.createDefault();
}
CloseableHttpResponse response = httpClient.execute(http);
try {
if (response != null) {
if (response.getStatusLine() != null)
statusCode = response.getStatusLine().getStatusCode();
HttpEntity entity = response.getEntity();
// 响应内容
content = EntityUtils.toString(entity, Consts.UTF_8);
}
} finally {
response.close();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
httpClient.close();
}
}
public int getStatusCode() {
return statusCode;
}
public String getContent() throws ParseException, IOException {
return content;
}
}
2.3 添加商户APIv2 key
# APIv2密钥
wxpay.partnerKey: T6m9iK73b0kn9g5v426MKfHQH7X8rKwb
private String partnerKey;
2.4 添加枚举
/**
* Native下单V2
*/
NATIVE_PAY_V2("/pay/unifiedorder"),
/**
* 支付通知V2
*/
NATIVE_NOTIFY_V2("/api/wx-pay-v2/native/notify"),
3、统一下单
3.1 创建WxPayV2Controller
@CrossOrigin //跨域
@RestController
@RequestMapping("/api/wx-pay-v2")
@Api(tags = "网站微信支付APIv2")
@Slf4j
public class WxPayV2Controller {
@Resource
private WxPayService wxPayService;
/**
* Native下单
* @param productId
* @return
* @throws Exception
*/
@ApiOperation("调用统一下单API,生成支付二维码")
@PostMapping("/native/{productId}")
public R createNative(@PathVariable Long productId, HttpServletRequest request) throws Exception {
log.info("发起支付请求 v2");
String remoteAddr = request.getRemoteAddr();
Map<String, Object> map = wxPayService.nativePayV2(productId, remoteAddr);
return R.ok().setData(map);
}
}
3.2 WxPayServiceImpl
@Override
public Map<String, Object> nativePayV2(Long productId, String remoteAddr) throws Exception {
log.info("生成订单");
//生成订单
OrderInfo orderInfo = orderInfoService.createOrderByProductId(productId, PayType.WXPAY.getType());
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 = new HttpClientUtils(wxPayConfig.getDomain().concat(WxApiType.NATIVE_PAY_V2.getType()));
//组装接口参数
Map<String, String> params = new HashMap<>();
params.put("appid", wxPayConfig.getAppid());//关联的公众号的appid
params.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_V2.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、支付回调
@CrossOrigin //跨域
@RestController
@RequestMapping("/api/wx-pay-v2")
@Api(tags = "网站微信支付APIv2")
@Slf4j
public class WxPayV2Controller {
@Resource
private WxPayService wxPayService;
@Resource
private WxPayConfig wxPayConfig;
@Resource
private OrderInfoService orderInfoService;
@Resource
private 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;
}
}
支付宝支付
1、支付宝开放能力介绍
1.1 开放平台账号注册
1.2 能力地图
https://opendocs.alipay.com/open/270/105898
⽀付能力、⽀付扩展、资⾦能力、⼝碑能力、营销能力、会员能力、⾏业能力、安全能力、基础能力
2、接入准备
2.1 常规接入流程
https://opendocs.alipay.com/open/01bxlm
第一步:创建应用
选择应⽤类型、填写应⽤基本信息、添加应⽤功能、配置应⽤环境(获取⽀付宝公 钥、应⽤公钥、应⽤私钥、⽀付宝⽹关地址,配置接⼝内容加密⽅式)、查看 APPID
第二步:绑定应用
将开发者账号中的APPID和商家账号PID进行绑定
https://opendocs.alipay.com/open/0128wr
在商家中心获取商家账号并绑定APPID:https://b.alipay.com/page/store-management/infomanage
第三步:配置秘钥
第四步:上线应用
第五步:签约功能
在商家中心上传营业执照、已备案网站信息等,提交审核进行签约
电脑网站支付需要签约完成后才会生效。在商家中心的产品中心中完成签约(需要真实的企业材料)
https://b.alipay.com/signing/productSetV2.htm?mrchportalwebServer=https%3A%2F%2Fmrchportalweb.alipay.com
2.2 沙箱接入流程
直接使用沙箱提供的开发参数,无需进行应用创建、绑定、上线和签约
沙箱环境介绍:https://opendocs.alipay.com/common/02kkv7
沙箱环境主页:https://openhome.alipay.com/develop/sandbox/app
第一步:获取相关参数
第二步:获取沙箱账号
3、创建客户端连接对象
3.1 配置支付宝支付参数
# 支付宝支付相关参数
# 应用ID,您的APPID,收款账号既是您的APPID对应支付宝账号
alipay.app-id=2088622958601234
# 商户PID,卖家支付宝账号ID
alipay.seller-id=2088621958581234
# 支付宝网关
alipay.gateway-url=https://openapi.alipaydev.com/gateway.do
# 商户私钥,您的PKCS8格式RSA2私钥
alipay.merchant-private-key=MIIEvQIBADANBgkqhkiCBKcwggSjAgECTMO1Dp6VCa8zhSTbtz201HuOZbo5zAtgiQl1uJt60T7SXwyIUoVse4M5WeJf7WR0ttLLh1N+yImHthDjJ7Ux3a6HgxnrttLnQ7YZ1+wsO/6CUSPRLdf+x18bybN/P+b/2vJ9cPPjoJACq4Fcxp1qAtSKmHG2Oswfs7T6yFjRGiQLoPUO9jlcSA2+sLW1+pNvXN7gAlVU7qHSycp+eEADJ10SZAUDn1sbI2P3yOnyHkzIGubR6ML9dX1KEbMdBGgKHnkGMrhEbcAkN7e0bHuv6dRHER0+yy2vxdB8AT1/4mDYCaYX/1rSx7cWnhHNzXSO6vPnvQxXeU1RAgMBAAECggEAJuQ7egfMt1qm94ks0uR3mxAaSwCOGxRl89vnC7xENtYllyjkj6IfiOnWpLsRnrV+NNv2QbyNi/7WIvtRdJzjzUwd1uKcZdKojW0fholCoNp2j1ouD7jN0QoVjU2JyXC1uGjmH4vsOd+xIOV0fEMcbz/+S9I3aGp6SQ15DQie3mgC8mUl9Ux2OoOSgkGh9YU+3G50zoldxZuGCDNyDmDbx7i0yp96P+hwAgj7ZSV5+B/tvFv555IgZmhdSXuYoHM5HaeV+oDmyNsznCkZSjp4dqMIAK3DV7+Jm/N2eTZEhWxhy7ivlm5Ucs998YcP8jFe00GFezdOBDtDUq2nud0H9QKBgQC6Q4TVHB5ReuGMYh0y7SDdTS51HpPbnB+4E2RP2fELy3JtZmknkHObk+VORmnAEAui5x7UyvHHYtlxcypK5K9lxHufkBUAA+mGY1DSw2MCVziQ/06r5RyiNXU3Lg1G03idXo9I/chuKpgI9yLDOdR8S4l3ZeVRnO6mg5ofSQmbqwKBgQCyuIQsNV0NLng3KjN+KIgvXRPz6bg88E7NE6xsgSFRUpOoVODjj5HxckHim5szxAFozE8mzcLL1lLzV1cX57hlo3xaE41KqtKBGJnclFXlWMOqQdjWrnYxzRr8Rci7hD+ge5eoKu+vgVbNvVBk2pad9DQigBWs9lTBFRTGsNWe8wKBgQC4JL4yzbyvgxNu7SS+zy35ey9dvGAi8eNt4UX/p5Alv/mdbGyzH50bhwhg7T3pEjPe1i1l5ElJfFFKVrfOGO3KZ4hsRE5umG+LW65w35eHneEfgDgvuq8nMkBy0AVyKukIc46mc7duKo/p9SGZ7hdC1o7Nyp/+om5RkrOREGz+RwKBgEqUHPOlwRLDFX9PqPU4TLwXB0B9g1hKn1eMoiMVL6YT43IXGVFck/ZBS6UYcgeD/2KP/2ed7W/KHAtXowxisdwYAMhF6GwEJJuifHJDpCR0ihH+MFJFsyTNBjnHlSBK65I4gOy4HhUK0AJCwc2UOc3oHelXsbPfhzabaXQQTtRpAoGAOxaGM4SDwF5d4VQ07i/RpatXI6dkf7N3kqu1UeMCFIsSWVMOy2VD1HDfxHffxEiYpdJhJmqFVKnQUfCzQMsxlmi2lbdhHH1ahoWForA2KHG0gr2sdqr3YZ3jjVqaOiyiys7zAvfC0oTDSpmEsA3+dzMVwZKivTufJV8QzovuN5k=
# 支付宝公钥,查看地址:https://openhome.alipay.com/platform/keyManage.htm 对应APPID下的支付宝公钥
alipay.alipay-public-key=MIIBIjANBgkqhkiG9w0BAQEQEAldW1hLDeN/yMYItGH0+KcJEyXJtFqVNIJZYjXDY2Yes1etG1qFvpT1fOYOAQ6qfasSJOcEGSgQ6oBqGhfaDZGS7ctbX1RlAUFDtM99HhLbKTx5DRSae5HDmSfhTJdOY41DbbOSY9qi8c9zwsGYvhfTq0yvwsXRlYfCZGAFTblyfXi4EynxJdkhnxCFBUwwMFzjbGxELodJYZjPXrO886zHSnUlnUWOm2f3Ol2cBHmGcKNCkVG9w8hHR9qjakSy3Ub+y8h6sDAWazQL+VKs8IrG6SLr8Amhjvxjh7S7OoPDqpU5CXvEEj1Vtc+1MK6yvKJwxbloCuoWywIDAQAB
# 接口内容加密秘钥,对称秘钥
alipay.content-key= 8fnvoe8X6Q==
# 页面跳转同步通知页面路径
alipay.return-url=http://localhost:8080/#/success
# 服务器异步通知页面路径 需http://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问
# 注意:每次重新启动ngrok,都需要根据实际情况修改这个配置
alipay.notify-url=https://a863-180-174-204-123.ngrok.io/api/ali-pay/trade/notify
将alipay-sandbox.properties添加为SpringBoot的标准配置文件
3.2 读取支付参数配置文件
package com.atguigu.paymentdemo.config;
@Configuration
@PropertySource("classpath:alipay-sandbox.properties")
public class AlipayClientConfig {
@Resource
private Environment config;
}
3.3 创建支付宝客户端连接对象
3.3.1 引入支付宝SDK
参考文档:开放平台 => 文档 => 开发⼯具 => 服务端SDK => Java => 通用版 => Maven项目依赖
https://opendocs.alipay.com/mini/02c6he
<!--支付宝 SDK-->
<dependency>
<groupId>com.alipay.sdk</groupId>
<artifactId>alipay-sdk-java</artifactId>
<version>4.22.57.ALL</version>
</dependency>
3.3.2 创建客户端对象
客户端对象封装了签名和验签功能
https://opendocs.alipay.com/common/02kf5q
package com.atguigu.paymentdemo.config;
@Configuration
@PropertySource("classpath:alipay-sandbox.properties")
public class AlipayClientConfig {
@Resource
private Environment config;
// 创建客户端对象AlipayClient。封装了签名和验签的整个功能
@Bean
public AlipayClient alipayClient() throws AlipayApiException {
AlipayConfig alipayConfig = new AlipayConfig();
// 设置网关地址
alipayConfig.setServerUrl(config.getProperty("alipay.gateway-url"));
// 设置应用Id
alipayConfig.setAppId(config.getProperty("alipay.app-id"));
// 设置应用私钥
alipayConfig.setPrivateKey(config.getProperty("alipay.merchant-private-key"));
// 设置请求格式,固定值json
// alipayConfig.setFormat("json");
alipayConfig.setFormat(AlipayConstants.FORMAT_JSON);
// 设置字符集
alipayConfig.setCharset("utf8");
// 设置支付宝公钥
alipayConfig.setAlipayPublicKey(config.getProperty("alipay.alipay-public-key"));
// 设置签名类型
alipayConfig.setSignType(AlipayConstants.SIGN_TYPE_RSA2);
// 构造client
AlipayClient alipayClient = new DefaultAlipayClient(alipayConfig);
return alipayClient;
}
}
4、支付功能开发
支付宝支付API列表
https://opendocs.alipay.com/open/028r8t?scene=22
4.1 统⼀收单下单并支付页面接口
4.1.1 支付调用流程
4.1.2 接口说明
https://opendocs.alipay.com/apis/028r8t?scene=22
公共请求参数:所有接口都需要的参数。在AlipayClient对象向开放平台发起接口请求调用之前,sign签名这个参数会被自动生成,所以不需要手动设置
请求参数:当前接口需要的参数
公共响应参数:所有接口的响应中都包含的数据
响应参数:当前接口的响应中包含的数据
响应示例:
// 响应为表单格式,可嵌入页面,具体以返回的结果为准。浏览器会自动执行该表单
<form name="submit_form" method="post" action="https://openapi.alipay.com/gateway.do?charset=UTF-8&method=alipay.trade.page.pay&sign=k0w1DePFqNMQWyGBwOaEsZEJuaIEQufjoPLtwYBYgiX%2FRSkBFY38VuhrNumXpoPY9KgLKtm4nwWz4DEQpGXOOLaqRZg4nDOGOyCmwHmVSV5qWKDgWMiW%2BLC2f9Buil%2BEUdE8CFnWhM8uWBZLGUiCrAJA14hTjVt4BiEyiPrtrMZu0o6%2FXsBu%2Fi6y4xPR%2BvJ3KWU8gQe82dIQbowLYVBuebUMc79Iavr7XlhQEFf%2F7WQcWgdmo2pnF4tu0CieUS7Jb0FfCwV%2F8UyrqFXzmCzCdI2P5FlMIMJ4zQp%2BTBYsoTVK6tg12stpJQGa2u3%2BzZy1r0KNzxcGLHL%2BwWRTx%2FCU%2Fg%3D%3D¬ify_url=http%3A%2F%2F114.55.81.185%2Fopendevtools%2Fnotify%2Fdo%2Fbf70dcb4-13c9-4458-a547-3a5a1e8ead04&version=1.0&app_id=2014100900013222&sign_type=RSA×tamp=2021-02-02+14%3A11%3A40&alipay_sdk=alipay-sdk-java-dynamicVersionNo&format=json">
<input type="submit" value="提交" style="display:none" >
</form>
<script>document.forms[0].submit();</script>
接口调用说明:业务前端调用业务后端,业务后端调用开放平台的统一收单下单接口,开放平台接口返回form表单给业务后端,业务后端返回给业务前端,前端接收到form表单后,浏览器自动执行表单提交,提交内容至开放平台,开放平台给用户展示支付登录页面,显示登录二维码和登录账号密码表单
4.1.3 发起支付请求
(1)创建AliPayController
package com.atguigu.paymentdemo.controller;
@CrossOrigin
@RestController
@RequestMapping("/api/ali-pay")
@Api(tags = "网站支付宝支付")
@Slf4j
public class AliPayController {
@Resource
private AliPayService aliPayService;
@ApiOperation("统一收单下单并支付页面接口的调用")
@PostMapping("/trade/page/pay/{productId}")
public R tradePagePay(@PathVariable Long productId){
log.info("统一收单下单并支付页面接口的调用");
// 支付宝开放平台接受 request 请求对象后
// 会为开发者生成一个html形式的 form表单,包含自动提交的脚本
String formStr = aliPayService.tradeCreate(productId);
// 我们将form表单字符串返回给前端程序,之后前端将会调用自动提交脚本,进行表单的提交
// 此时,表单会自动提交到action属性所指向的支付宝开放平台中,从而为用户展示一个支付页面
return R.ok().data("formStr", formStr);
}
}
(2)AliPayService接口
package com.atguigu.paymentdemo.service;
public interface AliPayService {
String tradeCreate(Long productId);
}
(3)实现类AliPayServiceImpl
package com.atguigu.paymentdemo.service.impl;
@Service
@Slf4j
public class AliPayServiceImpl implements AliPayService {
@Resource
private OrderInfoService orderInfoService;
@Resource
private AlipayClient alipayClient;
@Resource
private Environment config;
@Transactional(rollbackFor = Exception.class)
@Override
public String tradeCreate(Long productId) {
try {
// 生成订单
log.info("生成订单");
OrderInfo orderInfo = orderInfoService.createOrderByProductId(productId, PayType.ALIPAY.getType());
// 调用支付宝接口
AlipayTradePagePayRequest request = new AlipayTradePagePayRequest();
// 配置需要的公共请求参数
// 支付完成后,支付宝向谷粒学院发起异步通知的地址
request.setNotifyUrl(config.getProperty("alipay.notify-url"));
// 支付完成后,我们想让页面跳转回谷粒学院的页面,配置returnUrl(同步返回)
request.setReturnUrl(config.getProperty("alipay.return-url"));
// 组装当前业务方法的请求参数
JSONObject bizContent = new JSONObject();
bizContent.put("out_trade_no", orderInfo.getOrderNo()); // 商户订单号
BigDecimal total = new BigDecimal(orderInfo.getTotalFee().toString()).divide(new BigDecimal("100"));
bizContent.put("total_amount", total); // 订单金额
bizContent.put("subject", orderInfo.getTitle());
bizContent.put("product_code", "FAST_INSTANT_TRADE_PAY"); // 销售产品码,与支付宝签约的产品码名称
request.setBizContent(bizContent.toString());
// 执行请求,调用支付宝接口
AlipayTradePagePayResponse response = alipayClient.pageExecute(request);
if(response.isSuccess()){
log.info("调用成功,返回结果 ===> " + response.getBody());
return response.getBody();
} else {
log.info("调用失败,返回码 ===> " + response.getCode() + ", 返回描述 ===> " + response.getMsg());
throw new RuntimeException("创建支付交易失败");
}
} catch (AlipayApiException e) {
e.printStackTrace();
throw new RuntimeException("创建支付交易失败");
}
}
}
(4)创建订单createOrderByProductId
package com.atguigu.paymentdemo.service.impl;
@Service
@Slf4j
public class OrderInfoServiceImpl extends ServiceImpl<OrderInfoMapper, OrderInfo> implements OrderInfoService {
@Resource
private ProductMapper productMapper;
@Override
public OrderInfo createOrderByProductId(Long productId, String paymentType) {
//查找已存在但未支付的订单
OrderInfo orderInfo = this.getNoPayOrderByProductId(productId, paymentType);
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()); //未支付
orderInfo.setPaymentType(paymentType);
baseMapper.insert(orderInfo);
return orderInfo;
}
/**
* 根据商品id查询未支付订单
* 防止重复创建订单对象
* @param productId
* @return
*/
private OrderInfo getNoPayOrderByProductId(Long productId, String paymentType) {
QueryWrapper<OrderInfo> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("product_id", productId);
queryWrapper.eq("order_status", OrderStatus.NOTPAY.getType());
queryWrapper.eq("payment_type", paymentType);
// queryWrapper.eq("user_id", userId);
OrderInfo orderInfo = baseMapper.selectOne(queryWrapper);
return orderInfo;
}
}
(5)测试
注意点:由于支付宝沙箱环境的限制,每次使用浏览器访问沙箱环境时,当前浏览器窗口只能保留测试环境的菜单。否则会出现异常
4.1.4 前端支付按钮
(1)index.vue
methods: {
// 选择商品
selectItem(productId) {
console.log('商品id:' + productId)
this.payOrder.productId = productId
console.log(this.payOrder)
//this.$router.push({ path: '/order' })
},
// 选择支付方式
selectPayType(type) {
console.log('支付方式:' + type)
this.payOrder.payType = type
//this.$router.push({ path: '/order' })
},
// 确认支付
toPay() {
// 禁用按钮,防止重复提交
this.payBtnDisabled = true
// 微信支付
if (this.payOrder.payType === 'wxpay') {
//调用统一下单接口
wxPayApi.nativePay(this.payOrder.productId).then((response) => {
this.codeUrl = response.data.codeUrl
this.orderNo = response.data.orderNo
//打开二维码弹窗
this.codeDialogVisible = true
//启动定时器
this.timer = setInterval(() => {
//查询订单是否支付成功
this.queryOrderStatus()
}, 3000)
})
//支付宝支付
} else if (this.payOrder.payType === 'alipay') {
// 调用支付宝统一收单下单并支付页面接口
aliPayApi.tradePagePay(this.payOrder.productId).then((response) => {
// 将支付宝返回的表单字符串写在浏览器中,表单会自动触发submit提交
document.write(response.data.formStr)
})
}
},
(2)aliPay.js
// axios 发送ajax请求
import request from '@/utils/request'
export default{
//发起支付请求
tradePagePay(productId) {
return request({
url: '/api/ali-pay/trade/page/pay/' + productId,
method: 'post'
})
},
cancel(orderNo) {
return request({
url: '/api/ali-pay/trade/close/' + orderNo,
method: 'post'
})
},
refunds(orderNo, reason) {
return request({
url: '/api/ali-pay/trade/refund/' + orderNo + '/' + reason,
method: 'post'
})
}
}
(3)启动前端服务
4.2 支付结果通知
4.2.1 启动内网穿透ngrok
ngrok http 8090
4.2.2 修改内网穿透配置
根据ngrok每次启动的情况,修改 alipay-sandbox.properties 文件中的 alipay.notify-url
# 服务器异步通知页面路径 需http://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常 访问
# 注意:每次重新启动ngrok,都需要根据实际情况修改这个配置
alipay.notify-url=https://a863-180-174-204-169.ngrok.io/api/ali-pay/trade/notify
4.2.3 设置异步通知地址
在 AliPayServiceImpl 的 tradeCreate 方法中设置异步通知地址
// 配置需要的公共请求参数
// 支付完成后,支付宝向谷粒学院发起异步通知的地址
request.setNotifyUrl(config.getProperty("alipay.notify-url"));
{
gmt_create=2022-05-08 10:53:10,
charset=UTF-8,
gmt_payment=2022-05-08 10:53:21,
notify_time=2022-05-08 10:53:23,
subject=前端课程,
sign=XJ/6o2bPe0246sYh8MXfSmW2rYb3pM9xw1T7bkI1JJP7kzRvFampFTeCfCaHhfxo3jtiID+6+/2QTT9xnGG8d417wz5chytnBvMXmMNSClNi6YZXWywCMBaNBz4PEQkvI6j6HupWKO20CtNSbo3bIB7+4t9gWTAFyorRv8TY3zMMeyLt2JQbxi/EC9N3jEz70i/SPMND98xaIO5YgvJVrjAAcXd6FizohplMuYA+d3wFKYipIoTyfbmIQsI071sAIJ/RUg+DmJi0b5NV5H8k1QjlhFnv08rzj7wfR5vzWbzcHFjXfAabpMiBMJyfG8ToXajf3VPXT/Yn5LORwVPfNw==,
buyer_id=2088622958601474,
invoice_amount=0.01,
version=1.0,
notify_id=2022050800222105322001470523533608,
fund_bill_list=[
{
"amount":"0.01",
"fundChannel":"ALIPAYACCOUNT"
}
],
notify_type=trade_status_sync,
out_trade_no=ORDER_20220508105248532,
total_amount=0.01,
trade_status=TRADE_SUCCESS, // 触发通知
trade_no=2022050822001401470502515966,
auth_app_id=2021000119683440,
receipt_amount=0.01,
point_amount=0.00,
app_id=2021000119683440,
buyer_pay_amount=0.01,
sign_type=RSA2,
seller_id=2088621958584044
}
4.2.4 开发异步通知接口
https://opendocs.alipay.com/open/270/105902
(1)创建AliPayController
package com.atguigu.paymentdemo.controller;
@CrossOrigin
@RestController
@RequestMapping("/api/ali-pay")
@Api(tags = "网站支付宝支付")
@Slf4j
public class AliPayController {
@Resource
private AliPayService aliPayService;
@Resource
private Environment config;
@Resource
private OrderInfoService orderInfoService;
@ApiOperation("统一收单下单并支付页面接口的调用")
@PostMapping("/trade/page/pay/{productId}")
public R tradePagePay(@PathVariable Long productId){
log.info("统一收单下单并支付页面接口的调用");
// 支付宝开放平台接受 request 请求对象后
// 会为开发者生成一个html 形式的 form表单,包含自动提交的脚本
String formStr = aliPayService.tradeCreate(productId);
//我们将form表单字符串返回给前端程序,之后前端将会调用自动提交脚本,进行表单的提交
//此时,表单会自动提交到action属性所指向的支付宝开放平台中,从而为用户展示一个支付页面
return R.ok().data("formStr", formStr);
}
// 获取HttpServelt的request请求参数(支付宝提交的请求参数),并存放到params
@ApiOperation("支付通知")
@PostMapping("/trade/notify")
public String tradeNotify(@RequestParam Map<String, String> params){
log.info("支付通知正在执行");
log.info("通知参数 ===> {}", params);
String result = "failure";
try {
// 异步通知验签。调用SDK验证签名:验证支付宝请求参数params中的sign签名是否正确
boolean signVerified = AlipaySignature.rsaCheckV1(
params,
config.getProperty("alipay.alipay-public-key"),
AlipayConstants.CHARSET_UTF8,
AlipayConstants.SIGN_TYPE_RSA2);
if(!signVerified){
// 验签失败则记录异常日志,并在response中返回failure.
log.error("支付成功异步通知验签失败!");
return result;
}
// 验签成功后
log.info("支付成功异步通知验签成功!");
// 按照支付结果异步通知中的描述,对支付结果中的业务内容进行二次校验,校验成功后在response中返回success并继续商户自身业务处理,校验失败返回failure
// 1、商户需要验证该通知数据中的 out_trade_no 是否为商户系统中创建的订单号
String outTradeNo = params.get("out_trade_no");
OrderInfo order = orderInfoService.getOrderByOrderNo(outTradeNo);
if(order == null){
log.error("订单不存在");
return result;
}
// 2、判断 total_amount 是否确实为该订单的实际金额(即商户订单创建时的金额)
String totalAmount = params.get("total_amount");
int totalAmountInt = new BigDecimal(totalAmount).multiply(new BigDecimal("100")).intValue();
int totalFeeInt = order.getTotalFee().intValue();
if(totalAmountInt != totalFeeInt){
log.error("金额校验失败");
return result;
}
// 3、校验通知中的 seller_id(或者 seller_email) 是否为 out_trade_no 这笔单据的对应的操作方
String sellerId = params.get("seller_id");
String sellerIdProperty = config.getProperty("alipay.seller-id");
if(!sellerId.equals(sellerIdProperty)){
log.error("商家pid校验失败");
return result;
}
//4、验证 app_id 是否为该商户本身
String appId = params.get("app_id");
String appIdProperty = config.getProperty("alipay.app-id");
if(!appId.equals(appIdProperty)){
log.error("appid校验失败");
return result;
}
// 在支付宝的业务通知中,只有交易通知状态为TRADE_SUCCESS时,支付宝才会认定为买家付款成功。
String tradeStatus = params.get("trade_status");
if(!"TRADE_SUCCESS".equals(tradeStatus)){
log.error("支付未成功");
return result;
}
// 处理业务 修改订单状态 记录支付日志
aliPayService.processOrder(params);
// 校验成功后在response中返回success并继续商户自身业务处理,校验失败返回failure
result = "success";
} catch (AlipayApiException e) {
e.printStackTrace();
}
return result;
}
}
(2)AliPayService接口
void processOrder(Map<String, String> params);
(3)实现类AliPayServiceImpl
package com.atguigu.paymentdemo.service.impl;
@Service
@Slf4j
public class AliPayServiceImpl implements AliPayService {
@Resource
private OrderInfoService orderInfoService;
@Resource
private AlipayClient alipayClient;
@Resource
private Environment config;
@Resource
private PaymentInfoService paymentInfoService;
@Resource
private RefundInfoService refundsInfoService;
private final ReentrantLock lock = new ReentrantLock();
/**
* 处理订单
* @param params
*/
@Transactional(rollbackFor = Exception.class)
@Override
public void processOrder(Map<String, String> params) {
log.info("处理订单");
// 获取订单号
String orderNo = params.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.createPaymentInfoForAliPay(params);
} finally {
// 要主动释放锁
lock.unlock();
}
}
}
}
4.2.5 记录支付日志
public interface PaymentInfoService {
void createPaymentInfoForAliPay(Map<String, String> params);
}
package com.atguigu.paymentdemo.service.impl;
@Service
@Slf4j
public class PaymentInfoServiceImpl extends ServiceImpl<PaymentInfoMapper, PaymentInfo> implements PaymentInfoService {
/**
* 记录支付日志:支付宝
* @param params
*/
@Override
public void createPaymentInfoForAliPay(Map<String, String> params) {
log.info("记录支付日志");
// 获取订单号
String orderNo = params.get("out_trade_no");
// 业务编号
String transactionId = params.get("trade_no");
// 交易状态
String tradeStatus = params.get("trade_status");
// 交易金额
String totalAmount = params.get("total_amount");
int totalAmountInt = new BigDecimal(totalAmount).multiply(new BigDecimal("100")).intValue();
PaymentInfo paymentInfo = new PaymentInfo();
paymentInfo.setOrderNo(orderNo);
paymentInfo.setPaymentType(PayType.ALIPAY.getType());
paymentInfo.setTransactionId(transactionId);
paymentInfo.setTradeType("电脑网站支付");
paymentInfo.setTradeState(tradeStatus);
paymentInfo.setPayerTotal(totalAmountInt);
Gson gson = new Gson();
String json = gson.toJson(params, HashMap.class);
paymentInfo.setContent(json);
baseMapper.insert(paymentInfo);
}
}
4.2.6 更新订单状态记录支付日志
- 商户给支付宝返回的不是“success”
- 支付宝未收到商户发送的消息
此时支付状态已更新、支付日志已记录,但是反馈没有正确发送给支付宝。支付宝就会给服务端不断重发通知,此后支付状态又要被修改(再次更新对业务基本无影响)、支付日志又要被记录(影响业务:一笔业务记录多条记录)
如何过滤重复通知:
- 业务服务器在没有成功接收支付宝回调通知时,我们才希望支付宝给我们发送重复通知
- 若业务服务器收到支付宝回调通知,但服务器未成功给支付宝反馈消息“success”时,我们不希望接收到支付宝给我们发送的重复通知
在 processOrder 方法中,更新订单状态之前,添加如下代码:
// 接口调用的幂等性:无论回调通知接口被调用多少次,以下业务只执行一次
// 1、当支付状态为待支付时,需要执行后续的更新订单状态和记录支付日志
// 2、当支付状态不为待支付时,不需要重复执行后续操作
String orderStatus = orderInfoService.getOrderStatus(orderNo);
if (!OrderStatus.NOTPAY.getType().equals(orderStatus)) {
return;
}
// ① 更新订单状态
orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.SUCCESS);
// ② 记录支付日志
paymentInfoService.createPaymentInfoForAliPay(params);
4.2.7 数据锁
在 AliPayServiceImpl 中定义 ReentrantLock 进行并发控制(避免多个回调通知同时到达,从而产生多条日志)。注意,必须⼿动释放锁。
private final ReentrantLock lock = new ReentrantLock();
完整的 processOrder 方法:
package com.atguigu.paymentdemo.service.impl;
@Service
@Slf4j
public class AliPayServiceImpl implements AliPayService {
@Resource
private OrderInfoService orderInfoService;
@Resource
private AlipayClient alipayClient;
@Resource
private Environment config;
@Resource
private PaymentInfoService paymentInfoService;
@Resource
private RefundInfoService refundsInfoService;
private final ReentrantLock lock = new ReentrantLock();
/**
* 处理订单
* @param params
*/
@Transactional(rollbackFor = Exception.class)
@Override
public void processOrder(Map<String, String> params) {
log.info("处理订单");
// 获取订单号
String orderNo = params.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.createPaymentInfoForAliPay(params);
} finally {
// 要主动释放锁
lock.unlock();
}
}
}
}
4.3 订单表优化
4.3.1 修改表
t_order_info 表中添加 payment_type 字段(区别两种支付方式:支付宝、微信)
4.3.2 业务修改
(1)修改⽀付业务代码
修改AliPayServiceImpl、WxPayServiceImpl代码中对如下方法的调用,添加参数PayType.ALIPAY.getType()
log.info("生成订单");
OrderInfo orderInfo = orderInfoService.createOrderByProductId(productId, PayType.ALIPAY.getType());
(2)修改OrderInfoService
接口的createOrderByProductId方法中添加参数 String paymentType
实现类createOrderByProductId方法中添加参数 String paymentType
对getNoPayOrderByProductId方法的调用时添加参数 paymentType
生成订单的过程中添加orderInfo.setPaymentType(paymentType);
对 getNoPayOrderByProductId 方法的定义时添加参数 paymentType
添加查询条件queryWrapper.eq(“payment_type”, paymentType);
4.4 统一收单交易关闭
4.4.1 定义用户取消订单接口
package com.atguigu.paymentdemo.controller;
@CrossOrigin
@RestController
@RequestMapping("/api/ali-pay")
@Api(tags = "网站支付宝支付")
@Slf4j
public class AliPayController {
@Resource
private AliPayService aliPayService;
/**
* 用户取消订单
* @param orderNo
* @return
*/
@ApiOperation("用户取消订单")
@PostMapping("/trade/close/{orderNo}")
public R cancel(@PathVariable String orderNo){
log.info("取消订单");
aliPayService.cancelOrder(orderNo);
return R.ok().setMessage("订单已取消");
}
}
4.4.2 关单并修改订单状态
void cancelOrder(String orderNo);
package com.atguigu.paymentdemo.service.impl;
@Service
@Slf4j
public class AliPayServiceImpl implements AliPayService {
@Resource
private OrderInfoService orderInfoService;
@Resource
private AlipayClient alipayClient;
/**
* 用户取消订单
* @param orderNo
*/
@Override
public void cancelOrder(String orderNo) {
// 调用支付宝提供的统一收单交易关闭接口
this.closeOrder(orderNo);
// 更新用户订单状态
orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.CANCEL);
}
/**
* 关单接口的调用
* @param orderNo 订单号
*/
private void closeOrder(String orderNo) {
try {
log.info("关单接口的调用,订单号 ===> {}", orderNo);
AlipayTradeCloseRequest request = new AlipayTradeCloseRequest();
JSONObject bizContent = new JSONObject();
bizContent.put("out_trade_no", orderNo);
request.setBizContent(bizContent.toString());
AlipayTradeCloseResponse response = alipayClient.execute(request);
if(response.isSuccess()){
log.info("调用成功,返回结果 ===> " + response.getBody());
} else {
log.info("调用失败,返回码 ===> " + response.getCode() + ", 返回描述 ===> " + response.getMsg());
//throw new RuntimeException("关单接口的调用失败");
// 此处不要抛异常,原因是下单操作时,首先在本地添加订单数据,但需要登录支付宝后才会在支付宝端生成订单数据。
// 当本地存在数据,而支付宝端不存在订单数据时,调用取消订单接口,支付宝关单接口会报错提示“交易不存在”,此时就退出整个流程了
// 所以此处不抛异常的作用是,支付宝端关单接口报错,直接忽略,然后执行本地关单方法orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.CANCEL);关闭本地数据即可
}
} catch (AlipayApiException e) {
e.printStackTrace();
throw new RuntimeException("关单接口的调用失败");
}
}
}
4.4.3 测试
注意:针对⼆维码支付,只有经过扫码的订单才在⽀付宝端有交易记录。针对支付宝账号支付,只有经过登录的订单才在支付宝端有交易记录。
4.5 统一收单线下交易查询
4.5.1 查单接口的调用
商户后台未收到异步支付结果通知时(支付宝未成功发生回调通知,或者商户由于网络问题未成功收到回调通知),商户应该主动调用《统⼀收单线下交易查询接口》,查看交易状态,并同步本地的订单状态
(1)AliPayController
package com.atguigu.paymentdemo.controller;
@CrossOrigin
@RestController
@RequestMapping("/api/ali-pay")
@Api(tags = "网站支付宝支付")
@Slf4j
public class AliPayController {
@Resource
private AliPayService aliPayService;
/**
* 查询订单
* @param orderNo
* @return
*/
@ApiOperation("查询订单:测试订单状态用")
@GetMapping("/trade/query/{orderNo}")
public R queryOrder(@PathVariable String orderNo) {
log.info("查询订单");
String result = aliPayService.queryOrder(orderNo);
return R.ok().setMessage("查询成功").data("result", result);
}
}
(2)AliPayService
String queryOrder(String orderNo);
package com.atguigu.paymentdemo.service.impl;
@Service
@Slf4j
public class AliPayServiceImpl implements AliPayService {
@Resource
private OrderInfoService orderInfoService;
@Resource
private AlipayClient alipayClient;
/**
* 查询订单
* @param orderNo
* @return 返回订单查询结果,如果返回null则表示支付宝端尚未创建订单
*/
@Override
public String queryOrder(String orderNo) {
try {
log.info("查单接口调用 ===> {}", orderNo);
AlipayTradeQueryRequest request = new AlipayTradeQueryRequest();
JSONObject bizContent = new JSONObject();
bizContent.put("out_trade_no", orderNo);
request.setBizContent(bizContent.toString());
AlipayTradeQueryResponse response = alipayClient.execute(request);
if(response.isSuccess()){
log.info("调用成功,返回结果 ===> " + response.getBody());
return response.getBody();
} else {
log.info("调用失败,返回码 ===> " + response.getCode() + ", 返回描述 ===> " + response.getMsg());
//throw new RuntimeException("查单接口的调用失败");
return null; // 订单不存在
}
} catch (AlipayApiException e) {
e.printStackTrace();
throw new RuntimeException("查单接口的调用失败");
}
}
}
4.5.2 定时查单
(1)创建AliPayTask
package com.atguigu.paymentdemo.task;
@Slf4j
@Component
public class AliPayTask {
@Resource
private OrderInfoService orderInfoService;
@Resource
private AliPayService aliPayService;
/**
* 从第0秒开始每隔30秒执行1次,查询创建超过5分钟,并且未支付的订单
*/
@Scheduled(cron = "0/30 * * * * ?")
public void orderConfirm(){
log.info("orderConfirm 被执行......");
// 查询本地未支付的订单
List<OrderInfo> orderInfoList = orderInfoService.getNoPayOrderByDuration(1, PayType.ALIPAY.getType());
for (OrderInfo orderInfo : orderInfoList) {
String orderNo = orderInfo.getOrderNo();
log.warn("超时订单 ===> {}", orderNo);
// 核实订单状态:调用支付宝查单接口
aliPayService.checkOrderStatus(orderNo);
}
}
}
(2)修改OrderInfoService
List<OrderInfo> getNoPayOrderByDuration(int minutes, String paymentType);
实现添加参数 String paymentType , 添加查询条件queryWrapper.eq(“payment_type”, paymentType);
/**
* 查询创建超过minutes分钟并且未支付的订单
* @param minutes
* @return
*/
@Override
public List<OrderInfo> getNoPayOrderByDuration(int minutes, String paymentType) {
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);
queryWrapper.eq("payment_type", paymentType);
List<OrderInfo> orderInfoList = baseMapper.selectList(queryWrapper);
return orderInfoList;
}
4.5.3 处理查询到的订单
(1)AliPayTask
在定时任务的for循环最后添加以下代码
// 核实订单状态:调用支付宝查单接口
aliPayService.checkOrderStatus(orderNo);
(2)AliPayService
void checkOrderStatus(String orderNo);
package com.atguigu.paymentdemo.service.impl;
@Service
@Slf4j
public class AliPayServiceImpl implements AliPayService {
@Resource
private OrderInfoService orderInfoService;
@Resource
private AlipayClient alipayClient;
/**
* 根据订单号调用支付宝查单接口,核实订单状态
* 如果订单未创建,则更新商户端订单状态(支付宝端订单未创建,商户端订单已创建。原因是下单操作时未登录支付宝,就不会在支付宝端创建订单)
* 如果订单未支付,则调用关单接口关闭订单,并更新商户端订单状态
* 如果订单已支付,则更新商户端订单状态,并记录支付日志(原因可能时支付宝端回调接口未成功发送、或商户端未成功接收造成的数据不一致)
* @param orderNo
*/
@Override
public void checkOrderStatus(String orderNo) {
log.warn("根据订单号核实订单状态 ===> {}", orderNo);
String result = this.queryOrder(orderNo);
// 订单未创建
if(result == null){
log.warn("核实订单未创建 ===> {}", orderNo);
// 更新本地订单状态
orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.CLOSED);
}
// 解析查单响应结果
Gson gson = new Gson();
HashMap<String, LinkedTreeMap> resultMap = gson.fromJson(result, HashMap.class);
LinkedTreeMap alipayTradeQueryResponse = resultMap.get("alipay_trade_query_response");
String tradeStatus = (String)alipayTradeQueryResponse.get("trade_status");
if(AliPayTradeState.NOTPAY.getType().equals(tradeStatus)){ // 支付宝端订单未支付
log.warn("核实订单未支付 ===> {}", orderNo);
//如果订单未支付,则调用关单接口关闭订单
this.closeOrder(orderNo);
// 并更新商户端订单状态
orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.CLOSED);
}
if(AliPayTradeState.SUCCESS.getType().equals(tradeStatus)){ // 支付宝端订单已支付
log.warn("核实订单已支付 ===> {}", orderNo);
// 如果订单已支付,则更新商户端订单状态
orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.SUCCESS);
// 并记录支付日志
paymentInfoService.createPaymentInfoForAliPay(alipayTradeQueryResponse);
}
}
/**
* 查询订单
* @param orderNo
* @return 返回订单查询结果,如果返回null则表示支付宝端尚未创建订单
*/
@Override
public String queryOrder(String orderNo) {
try {
log.info("查单接口调用 ===> {}", orderNo);
AlipayTradeQueryRequest request = new AlipayTradeQueryRequest();
JSONObject bizContent = new JSONObject();
bizContent.put("out_trade_no", orderNo);
request.setBizContent(bizContent.toString());
AlipayTradeQueryResponse response = alipayClient.execute(request);
if(response.isSuccess()){
log.info("调用成功,返回结果 ===> " + response.getBody());
return response.getBody();
} else {
log.info("调用失败,返回码 ===> " + response.getCode() + ", 返回描述 ===> " + response.getMsg());
//throw new RuntimeException("查单接口的调用失败");
return null; // 订单不存在
}
} catch (AlipayApiException e) {
e.printStackTrace();
throw new RuntimeException("查单接口的调用失败");
}
}
}
4.6 统一收单交易退款
https://opendocs.alipay.com/open/028sm9
4.6.1 退款接口
(1)AliPayController
package com.atguigu.paymentdemo.controller;
@CrossOrigin
@RestController
@RequestMapping("/api/ali-pay")
@Api(tags = "网站支付宝支付")
@Slf4j
public class AliPayController {
@Resource
private AliPayService aliPayService;
/**
* 申请退款
* @param orderNo
* @param reason
* @return
*/
@ApiOperation("申请退款")
@PostMapping("/trade/refund/{orderNo}/{reason}")
public R refunds(@PathVariable String orderNo, @PathVariable String reason){
log.info("申请退款");
aliPayService.refund(orderNo, reason);
return R.ok();
}
}
(2)AliPayService
void refund(String orderNo, String reason);
package com.atguigu.paymentdemo.service.impl;
@Service
@Slf4j
public class AliPayServiceImpl implements AliPayService {
@Resource
private OrderInfoService orderInfoService;
@Resource
private AlipayClient alipayClient;
@Resource
private RefundInfoService refundsInfoService;
/**
* 退款
* @param orderNo
* @param reason
*/
@Transactional(rollbackFor = Exception.class)
@Override
public void refund(String orderNo, String reason) {
try {
log.info("调用退款API");
// 创建退款单
RefundInfo refundInfo = refundsInfoService.createRefundByOrderNoForAliPay(orderNo, reason);
// 调用统一收单交易退款接口
AlipayTradeRefundRequest request = new AlipayTradeRefundRequest ();
// 组装当前业务方法的请求参数
JSONObject bizContent = new JSONObject();
bizContent.put("out_trade_no", orderNo); // 订单编号
BigDecimal refund = new BigDecimal(refundInfo.getRefund().toString()).divide(new BigDecimal("100"));
//BigDecimal refund = new BigDecimal("2").divide(new BigDecimal("100"));
bizContent.put("refund_amount", refund); // 退款金额:不能大于支付金额。一笔订单可以发起多笔退款
bizContent.put("refund_reason", reason); // 退款原因(可选)
request.setBizContent(bizContent.toString());
// 执行请求,调用支付宝接口
AlipayTradeRefundResponse response = alipayClient.execute(request);
if(response.isSuccess()){
log.info("调用成功,返回结果 ===> " + response.getBody());
// 更新订单状态
orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.REFUND_SUCCESS);
// 更新退款单
refundsInfoService.updateRefundForAliPay(refundInfo.getRefundNo(),
response.getBody(),
AliPayTradeState.REFUND_SUCCESS.getType()); // 退款成功
} else {
log.info("调用失败,返回码 ===> " + response.getCode() + ", 返回描述 ===> " + response.getMsg());
// 更新订单状态
orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.REFUND_ABNORMAL);
// 更新退款单
refundsInfoService.updateRefundForAliPay(refundInfo.getRefundNo(),
response.getBody(),
AliPayTradeState.REFUND_ERROR.getType()); // 退款失败
}
} catch (AlipayApiException e) {
e.printStackTrace();
throw new RuntimeException("创建退款申请失败");
}
}
}
4.6.2 创建退款记录
RefundInfo createRefundByOrderNoForAliPay(String orderNo, String reason);
/**
* 根据订单号创建退款订单
* @param orderNo
* @return
*/
@Override
public RefundInfo createRefundByOrderNoForAliPay(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;
}
4.6.3 更新退款记录
void updateRefundForAliPay(String refundNo, String content, String refundStatus);
/**
* 更新退款记录
* @param refundNo
* @param content
* @param refundStatus
*/
@Override
public void updateRefundForAliPay(String refundNo, String content, String refundStatus) {
//根据退款单编号修改退款单
QueryWrapper<RefundInfo> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("refund_no", refundNo);
//设置要修改的字段
RefundInfo refundInfo = new RefundInfo();
refundInfo.setRefundStatus(refundStatus);//退款状态
refundInfo.setContentReturn(content);//将全部响应结果存入数据库的content字段
//更新退款单
baseMapper.update(refundInfo, queryWrapper);
}
4.7 统一收单交易退款查询
https://opendocs.alipay.com/open/028sma
若退款接口由于网络等原因返回异常,商户可调用退款查询接口 alipay.trade.fastpay.refund.query(统一收单交易退款查询接口)查询指定交易的退款信息。
支付宝退款支持单笔交易分多次退款,多次退款需要提交原支付订单的商户订单号和设置不同的退款单号。
可以设置补偿机制,三次调用退款查询接口,只要有一次返回REFUND_SUCCESS,表示退款处理成功。则更新本地订单状态和退款单状态
(1)AliPayController
package com.atguigu.paymentdemo.controller;
@CrossOrigin
@RestController
@RequestMapping("/api/ali-pay")
@Api(tags = "网站支付宝支付")
@Slf4j
public class AliPayController {
@Resource
private AliPayService aliPayService;
/**
* 查询退款
* @param orderNo
* @return
* @throws Exception
*/
@ApiOperation("查询退款:测试用")
@GetMapping("/trade/fastpay/refund/{orderNo}")
public R queryRefund(@PathVariable String orderNo) throws Exception {
log.info("查询退款");
String result = aliPayService.queryRefund(orderNo);
return R.ok().setMessage("查询成功").data("result", result);
}
}
(2)AliPayService
String queryRefund(String orderNo);
package com.atguigu.paymentdemo.service.impl;
@Service
@Slf4j
public class AliPayServiceImpl implements AliPayService {
@Resource
private OrderInfoService orderInfoService;
@Resource
private AlipayClient alipayClient;
@Resource
private RefundInfoService refundsInfoService;
/**
* 查询退款
* @param orderNo
* @return
*/
@Override
public String queryRefund(String orderNo) {
try {
log.info("查询退款接口调用 ===> {}", orderNo);
AlipayTradeFastpayRefundQueryRequest request = new AlipayTradeFastpayRefundQueryRequest();
JSONObject bizContent = new JSONObject();
bizContent.put("out_trade_no", orderNo);
bizContent.put("out_request_no", orderNo);
request.setBizContent(bizContent.toString());
AlipayTradeFastpayRefundQueryResponse response = alipayClient.execute(request);
if(response.isSuccess()){
log.info("调用成功,返回结果 ===> " + response.getBody());
return response.getBody();
} else {
log.info("调用失败,返回码 ===> " + response.getCode() + ", 返回描述 ===> " + response.getMsg());
// throw new RuntimeException("查单接口的调用失败");
return null; // 订单不存在
}
} catch (AlipayApiException e) {
e.printStackTrace();
throw new RuntimeException("查单接口的调用失败");
}
}
}
4.8 收单退款冲退完成通知
退款存在退到银⾏卡场景下时,收单会根据银⾏回执消息发送退款完成信息。
开发流程类似⽀付结果通知。
4.9 查询对账单下载地址接口
https://opendocs.alipay.com/open/028woc
(1)AliPayController
package com.atguigu.paymentdemo.controller;
@CrossOrigin
@RestController
@RequestMapping("/api/ali-pay")
@Api(tags = "网站支付宝支付")
@Slf4j
public class AliPayController {
@Resource
private AliPayService aliPayService;
/**
* 根据账单类型和日期获取账单url地址
*
* @param billDate
* @param type
* @return
*/
@ApiOperation("获取账单url")
@GetMapping("/bill/downloadurl/query/{billDate}/{type}")
public R queryTradeBill(
@PathVariable String billDate,
@PathVariable String type) {
log.info("获取账单url");
String downloadUrl = aliPayService.queryBill(billDate, type);
return R.ok().setMessage("获取账单url成功").data("downloadUrl", downloadUrl);
}
}
(2)AliPayService
String queryBill(String billDate, String type);
package com.atguigu.paymentdemo.service.impl;
@Service
@Slf4j
public class AliPayServiceImpl implements AliPayService {
@Resource
private AlipayClient alipayClient;
/**
* 申请账单
* @param billDate
* @param type
* @return
*/
@Override
public String queryBill(String billDate, String type) {
try {
AlipayDataDataserviceBillDownloadurlQueryRequest request = new AlipayDataDataserviceBillDownloadurlQueryRequest();
JSONObject bizContent = new JSONObject();
bizContent.put("bill_type", type);
bizContent.put("bill_date", billDate);
request.setBizContent(bizContent.toString());
AlipayDataDataserviceBillDownloadurlQueryResponse response = alipayClient.execute(request);
if(response.isSuccess()){
log.info("调用成功,返回结果 ===> " + response.getBody());
// 获取账单下载地址
Gson gson = new Gson();
HashMap<String, LinkedTreeMap> resultMap = gson.fromJson(response.getBody(), HashMap.class);
LinkedTreeMap billDownloadurlResponse = resultMap.get("alipay_data_dataservice_bill_downloadurl_query_response");
String billDownloadUrl = (String)billDownloadurlResponse.get("bill_download_url");
return billDownloadUrl;
} else {
log.info("调用失败,返回码 ===> " + response.getCode() + ", 返回描述 ===> " + response.getMsg());
throw new RuntimeException("申请账单失败");
}
} catch (AlipayApiException e) {
e.printStackTrace();
throw new RuntimeException("申请账单失败");
}
}
}
在线调试方法: