资料来源: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 />
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@EnableSwagger2public class Swagger2Config {@Beanpublic 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:sstime-zone: GMT+8
4、创建数据库
4.1 创建数据库
mysql -uroot -pmysql> 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.Driverurl: jdbc:mysql://localhost:3306/payment_demo?serverTimezone=GMT%2B8&characterEncoding=utf-8username: rootpassword: 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.StdOutImplmapper-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 {@Resourceprivate 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# APPIDwxpay.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节点中的值填充到当前类的属性中@Slf4jpublic class WxPayConfig {// 商户号private String mchId;// 商户API证书序列号private String mchSerialNo;// 商户私钥文件private String privateKeyPath;// APIv3密钥private String apiV3Key;// APPIDprivate 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*/@Beanpublic 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);//用于构造HttpClientWechatPayHttpClientBuilder 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 {@Resourceprivate WxPayConfig wxPayConfig;@GetMappingpublic 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节点中的值填充到当前类的属性中@Slf4jpublic 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节点中的值填充到当前类的属性中@Slf4jpublic class WxPayConfig {/*** 获取签名验证器* @return*/@Beanpublic 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节点中的值填充到当前类的属性中@Slf4jpublic 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);//用于构造HttpClientWechatPayHttpClientBuilder 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 lastString[] 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")@Slf4jpublic class WxPayController {@Resourceprivate WxPayService wxPayService;@Resourceprivate 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@Slf4jpublic class WxPayServiceImpl implements WxPayService {@Resourceprivate WxPayConfig wxPayConfig;@Resourceprivate CloseableHttpClient wxPayClient;@Resourceprivate OrderInfoService orderInfoService;@Resourceprivate PaymentInfoService paymentInfoService;@Resourceprivate RefundInfoService refundsInfoService;@Resourceprivate CloseableHttpClient wxPayNoSignClient; //无需应答签名private final ReentrantLock lock = new ReentrantLock();/*** 创建订单,调用Native支付接口* @param productId* @return code_url 和 订单号* @throws Exception*/@Transactional(rollbackFor = Exception.class)@Overridepublic 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");//调用统一下单APIHttpPost 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) { //处理成功,无返回Bodylog.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@Slf4jpublic class OrderInfoServiceImpl extends ServiceImpl<OrderInfoMapper, OrderInfo> implements OrderInfoService {@Resourceprivate ProductMapper productMapper;@Overridepublic 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@Slf4jpublic class OrderInfoServiceImpl extends ServiceImpl<OrderInfoMapper, OrderInfo> implements OrderInfoService {@Resourceprivate ProductMapper productMapper;/*** 存储订单二维码* @param orderNo* @param codeUrl*/@Overridepublic 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@Slf4jpublic class WxPayServiceImpl implements WxPayService {@Resourceprivate WxPayConfig wxPayConfig;@Resourceprivate CloseableHttpClient wxPayClient;@Resourceprivate OrderInfoService orderInfoService;@Resourceprivate PaymentInfoService paymentInfoService;@Resourceprivate RefundInfoService refundsInfoService;@Resourceprivate CloseableHttpClient wxPayNoSignClient; //无需应答签名private final ReentrantLock lock = new ReentrantLock();/*** 创建订单,调用Native支付接口* @param productId* @return code_url 和 订单号* @throws Exception*/@Transactional(rollbackFor = Exception.class)@Overridepublic 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");//调用统一下单APIHttpPost 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) { //处理成功,无返回Bodylog.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 {@Resourceprivate OrderInfoService orderInfoService;@ApiOperation("订单列表")@GetMapping("/list")public R list(){List<OrderInfo> list = orderInfoService.listOrderByCreateTimeDesc();return R.ok().data("list", list);}}
(2)定义 OrderInfoService 方法
/*** 查询订单列表,并倒序查询* @return*/@Overridepublic 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)@Overridepublic void processOrder(Map<String, Object> bodyMap) throws GeneralSecurityException {log.info("处理订单");//解密报文String plainText = decryptFromResource(bodyMap);//将明文转换成mapGson gson = new Gson();HashMap plainTextMap = gson.fromJson(plainText, HashMap.class);String orderNo = (String)plainTextMap.get("out_trade_no");/*在对业务数据进行状态检查和处理之前,要采用数据锁进行并发控制,以避免函数重入造成的数据混乱*///尝试获取锁:// 成功获取则立即返回true,获取失败则立即返回false。不必一直等待锁的释放if(lock.tryLock()){try {//处理重复的通知//接口调用的幂等性:无论接口被调用多少次,产生的结果是一致的。String orderStatus = orderInfoService.getOrderStatus(orderNo);if(!OrderStatus.NOTPAY.getType().equals(orderStatus)){return;}//模拟通知并发try {TimeUnit.SECONDS.sleep(5);} catch (InterruptedException e) {e.printStackTrace();}//更新订单状态orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.SUCCESS);//记录支付日志paymentInfoService.createPaymentInfo(plainText);} finally {//要主动释放锁lock.unlock();}}}
辅助方法:
/*** 对称解密* @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*/@Overridepublic void updateStatusByOrderNo(String orderNo, OrderStatus orderStatus) {log.info("更新订单状态 ===> {}", orderStatus.getType());QueryWrapper<OrderInfo> queryWrapper = new QueryWrapper<>();queryWrapper.eq("order_no", orderNo);OrderInfo orderInfo = new OrderInfo();orderInfo.setOrderStatus(orderStatus.getType());baseMapper.update(orderInfo, queryWrapper);}
(3)处理支付日志
/*** 记录支付日志:微信支付* @param plainText*/@Overridepublic 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*/@Overridepublic String getOrderStatus(String orderNo) {QueryWrapper<OrderInfo> queryWrapper = new QueryWrapper<>();queryWrapper.eq("order_no", orderNo);OrderInfo orderInfo = baseMapper.selectOne(queryWrapper);if(orderInfo == null){return null;}return orderInfo.getOrderStatus();}
6.7 数据锁
(1)测试通知并发
// 处理重复的通知// 模拟通知并发try {TimeUnit.SECONDS.sleep(5);} catch (InterruptedException e) {e.printStackTrace();}// 更新订单状态// 记录支付日志
(2)定义ReentrantLock
定义ReentrantLock 进行并发控制。注意,必须手动释放锁。
private final ReentrantLock lock = new ReentrantLock();
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*/@Overridepublic 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) { //处理成功,无返回Bodylog.info("成功204");} else {log.info("Native下单失败,响应码 = " + statusCode);throw new IOException("request failed");}} finally {response.close();}}
9、微信支付查单API
9.1 查单接口的调用
商户后台未收到异步支付结果通知时,商户应该主动调用《微信支付查单接口》,同步订单状态。
(1)WxPayController
/*** 查询订单* @param orderNo* @return* @throws 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
@Overridepublic String queryOrder(String orderNo) throws Exception {log.info("查单接口调用 ===> {}", orderNo);String url = String.format(WxApiType.ORDER_QUERY_BY_NO.getType(), orderNo);url = wxPayConfig.getDomain().concat(url).concat("?mchid=").concat(wxPayConfig.getMchId());HttpGet httpGet = new HttpGet(url);httpGet.setHeader("Accept", "application/json");//完成签名并执行请求CloseableHttpResponse response = wxPayClient.execute(httpGet);try {String bodyAsString = EntityUtils.toString(response.getEntity());//响应体int statusCode = response.getStatusLine().getStatusCode();//响应状态码if (statusCode == 200) { //处理成功log.info("成功, 返回结果 = " + bodyAsString);} else if (statusCode == 204) { //处理成功,无返回Bodylog.info("成功");} else {log.info("查单接口调用,响应码 = " + 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@Componentpublic 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@Componentpublic class WxPayTask {@Resourceprivate OrderInfoService orderInfoService;@Resourceprivate WxPayService wxPayService;/*** 从第0秒开始每隔30秒执行1次,查询创建超过5分钟,并且未支付的订单*/@Scheduled(cron = "0/30 * * * * ?")public void orderConfirm() throws Exception {log.info("orderConfirm 被执行......");List<OrderInfo> orderInfoList = orderInfoService.getNoPayOrderByDuration(1, PayType.WXPAY.getType());for (OrderInfo orderInfo : orderInfoList) {String orderNo = orderInfo.getOrderNo();log.warn("超时订单 ===> {}", orderNo);//核实订单状态:调用微信支付查单接口wxPayService.checkOrderStatus(orderNo);}}}
(2)OrderInfoServiceImpl
/*** 查询创建超过minutes分钟并且未支付的订单* @param minutes* @return*/@Overridepublic 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)@Overridepublic 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*/@Overridepublic 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*/@Overridepublic RefundInfo createRefundByOrderNo(String orderNo, String reason) {//根据订单号获取订单信息OrderInfo orderInfo = orderInfoService.getOrderByOrderNo(orderNo);//根据订单号生成退款订单RefundInfo refundInfo = new RefundInfo();refundInfo.setOrderNo(orderNo);//订单编号refundInfo.setRefundNo(OrderNoUtils.getRefundNo());//退款单编号refundInfo.setTotalFee(orderInfo.getTotalFee());//原订单金额(分)refundInfo.setRefund(orderInfo.getTotalFee());//退款金额(分)refundInfo.setReason(reason);//退款原因//保存退款订单baseMapper.insert(refundInfo);return refundInfo;}
11.2 更新退款单
/*** 记录退款记录* @param content*/@Overridepublic void updateRefund(String content) {//将json字符串转换成MapGson gson = new Gson();Map<String, String> resultMap = gson.fromJson(content, HashMap.class);//根据退款单编号修改退款单QueryWrapper<RefundInfo> queryWrapper = new QueryWrapper<>();queryWrapper.eq("refund_no", resultMap.get("out_refund_no"));//设置要修改的字段RefundInfo refundInfo = new RefundInfo();refundInfo.setRefundId(resultMap.get("refund_id"));//微信支付退款单号//查询退款和申请退款中的返回参数if(resultMap.get("status") != null){refundInfo.setRefundStatus(resultMap.get("status"));//退款状态refundInfo.setContentReturn(content);//将全部响应结果存入数据库的content字段}//退款回调中的回调参数if(resultMap.get("refund_status") != null){refundInfo.setRefundStatus(resultMap.get("refund_status"));//退款状态refundInfo.setContentNotify(content);//将全部响应结果存入数据库的content字段}//更新退款单baseMapper.update(refundInfo, queryWrapper);}
11.3 申请退款
(1) WxPayController
@ApiOperation("申请退款")@PostMapping("/refunds/{orderNo}/{reason}")public R refunds(@PathVariable String orderNo, @PathVariable String reason) throws Exception {log.info("申请退款");wxPayService.refund(orderNo, reason);return R.ok();}
(2)WxPayServiceImpl
/*** 退款* @param orderNo* @param reason* @throws IOException*/@Transactional(rollbackFor = Exception.class)@Overridepublic void refund(String orderNo, String reason) throws Exception {log.info("创建退款单记录");//根据订单编号创建退款单RefundInfo refundsInfo = refundsInfoService.createRefundByOrderNo(orderNo, reason);log.info("调用退款API");//调用统一下单APIString url = wxPayConfig.getDomain().concat(WxApiType.DOMESTIC_REFUNDS.getType());HttpPost httpPost = new HttpPost(url);// 请求body参数Gson gson = new Gson();Map paramsMap = new HashMap();paramsMap.put("out_trade_no", orderNo);//订单编号paramsMap.put("out_refund_no", refundsInfo.getRefundNo());//退款单编号paramsMap.put("reason",reason);//退款原因paramsMap.put("notify_url", wxPayConfig.getNotifyDomain().concat(WxNotifyType.REFUND_NOTIFY.getType()));//退款通知地址Map amountMap = new HashMap();amountMap.put("refund", refundsInfo.getRefund());//退款金额amountMap.put("total", refundsInfo.getTotalFee());//原订单金额amountMap.put("currency", "CNY");//退款币种paramsMap.put("amount", amountMap);//将参数转换成json字符串String jsonParams = gson.toJson(paramsMap);log.info("请求参数 ===> {}" + jsonParams);StringEntity entity = new StringEntity(jsonParams,"utf-8");entity.setContentType("application/json");//设置请求报文格式httpPost.setEntity(entity);//将请求报文放入请求对象httpPost.setHeader("Accept", "application/json");//设置响应报文格式//完成签名并执行请求,并完成验签CloseableHttpResponse response = wxPayClient.execute(httpPost);try {//解析响应结果String bodyAsString = EntityUtils.toString(response.getEntity());int statusCode = response.getStatusLine().getStatusCode();if (statusCode == 200) {log.info("成功, 退款返回结果 = " + bodyAsString);} else if (statusCode == 204) {log.info("成功");} else {throw new RuntimeException("退款异常, 响应码 = " + statusCode+ ", 退款返回结果 = " + bodyAsString);}//更新订单状态orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.REFUND_PROCESSING);//更新退款单refundsInfoService.updateRefund(bodyAsString);} finally {response.close();}}
12、查询退款API
文档: https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_10.shtml
12.1 查单接口的调用
(1) WxPayController
/*** 查询退款* @param refundNo* @return* @throws Exception*/@ApiOperation("查询退款:测试用")@GetMapping("/query-refund/{refundNo}")public R queryRefund(@PathVariable String refundNo) throws Exception {log.info("查询退款");String result = wxPayService.queryRefund(refundNo);return R.ok().setMessage("查询成功").data("result", result);}
(2) WxPayServiceImpl
/*** 查询退款接口调用* @param refundNo* @return*/@Overridepublic String queryRefund(String refundNo) throws Exception {log.info("查询退款接口调用 ===> {}", refundNo);String url = String.format(WxApiType.DOMESTIC_REFUNDS_QUERY.getType(), refundNo);url = wxPayConfig.getDomain().concat(url);//创建远程Get 请求对象HttpGet httpGet = new HttpGet(url);httpGet.setHeader("Accept", "application/json");//完成签名并执行请求CloseableHttpResponse response = wxPayClient.execute(httpGet);try {String bodyAsString = EntityUtils.toString(response.getEntity());int statusCode = response.getStatusLine().getStatusCode();if (statusCode == 200) {log.info("成功, 查询退款返回结果 = " + bodyAsString);} else if (statusCode == 204) {log.info("成功");} else {throw new RuntimeException("查询退款异常, 响应码 = " + statusCode+ ", 查询退款返回结果 = " + bodyAsString);}return bodyAsString;} finally {response.close();}}
12.2 定时查找退款中的订单
(1) WxPayTask
@Slf4j@Componentpublic class WxPayTask {@Resourceprivate OrderInfoService orderInfoService;@Resourceprivate WxPayService wxPayService;@Resourceprivate 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*/@Overridepublic List<RefundInfo> getNoRefundOrderByDuration(int minutes) {//minutes分钟之前的时间Instant instant = Instant.now().minus(Duration.ofMinutes(minutes));QueryWrapper<RefundInfo> queryWrapper = new QueryWrapper<>();queryWrapper.eq("refund_status", WxRefundStatus.PROCESSING.getType());queryWrapper.le("create_time", instant);List<RefundInfo> refundInfoList = baseMapper.selectList(queryWrapper);return refundInfoList;}
12.3 处理超时未退款订单
核实订单状态:
/*** 根据退款单号核实退款单状态* @param refundNo* @return*/@Transactional(rollbackFor = Exception.class)@Overridepublic void checkRefundStatus(String refundNo) throws Exception {log.warn("根据退款单号核实退款单状态 ===> {}", refundNo);//调用查询退款单接口String result = this.queryRefund(refundNo);//组装json请求体字符串Gson gson = new Gson();Map<String, String> resultMap = gson.fromJson(result, HashMap.class);//获取微信支付端退款状态String status = resultMap.get("status");String orderNo = resultMap.get("out_trade_no");if (WxRefundStatus.SUCCESS.getType().equals(status)) {log.warn("核实订单已退款成功 ===> {}", refundNo);//如果确认退款成功,则更新订单状态orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.REFUND_SUCCESS);//更新退款单refundsInfoService.updateRefund(result);}if (WxRefundStatus.ABNORMAL.getType().equals(status)) {log.warn("核实订单退款异常 ===> {}", refundNo);//如果确认退款成功,则更新订单状态orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.REFUND_ABNORMAL);//更新退款单refundsInfoService.updateRefund(result);}}
13、退款结果通知API
文档: [https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_11.shtml](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)@Overridepublic void processRefund(Map<String, Object> bodyMap) throws Exception {log.info("退款单");//解密报文String plainText = decryptFromResource(bodyMap);//将明文转换成mapGson gson = new Gson();HashMap plainTextMap = gson.fromJson(plainText, HashMap.class);String orderNo = (String)plainTextMap.get("out_trade_no");if(lock.tryLock()){try {String orderStatus = orderInfoService.getOrderStatus(orderNo);if (!OrderStatus.REFUND_PROCESSING.getType().equals(orderStatus)) {return;}//更新订单状态orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.REFUND_SUCCESS);//更新退款单refundsInfoService.updateRefund(plainText);} finally {//要主动释放锁lock.unlock();}}}
14、账单
14.1 申请交易账单和资金账单
(1) WxPayController
@ApiOperation("获取账单url:测试用")@GetMapping("/querybill/{billDate}/{type}")public R queryTradeBill(@PathVariable String billDate,@PathVariable String type) throws Exception {log.info("获取账单url");String downloadUrl = wxPayService.queryBill(billDate, type);return R.ok().setMessage("获取账单url成功").data("downloadUrl", downloadUrl);}
(2) WxPayServiceImpl
/*** 申请账单* @param billDate* @param type* @return* @throws Exception*/@Overridepublic String queryBill(String billDate, String type) throws Exception {log.warn("申请账单接口调用 {}", billDate);String url = "";if("tradebill".equals(type)){url = WxApiType.TRADE_BILLS.getType();}else if("fundflowbill".equals(type)){url = WxApiType.FUND_FLOW_BILLS.getType();}else{throw new RuntimeException("不支持的账单类型");}url = wxPayConfig.getDomain().concat(url).concat("?bill_date=").concat(billDate);//创建远程Get 请求对象HttpGet httpGet = new HttpGet(url);httpGet.addHeader("Accept", "application/json");//使用wxPayClient发送请求得到响应CloseableHttpResponse response = wxPayClient.execute(httpGet);try {String bodyAsString = EntityUtils.toString(response.getEntity());int statusCode = response.getStatusLine().getStatusCode();if (statusCode == 200) {log.info("成功, 申请账单返回结果 = " + bodyAsString);} else if (statusCode == 204) {log.info("成功");} else {throw new RuntimeException("申请账单异常, 响应码 = " + statusCode+ ", 申请账单返回结果 = " + bodyAsString);}//获取账单下载地址Gson gson = new Gson();Map<String, String> resultMap = gson.fromJson(bodyAsString, HashMap.class);return resultMap.get("download_url");} finally {response.close();}}
14.2 下载账单
(1) WxPayController
@ApiOperation("下载账单")@GetMapping("/downloadbill/{billDate}/{type}")public R downloadBill(@PathVariable String billDate,@PathVariable String type) throws Exception {log.info("下载账单");String result = wxPayService.downloadBill(billDate, type);return R.ok().data("result", result);}
(2) WxPayServiceImpl
/*** 下载账单* @param billDate* @param type* @return* @throws Exception*/@Overridepublic String downloadBill(String billDate, String type) throws Exception {log.warn("下载账单接口调用 {}, {}", billDate, type);//获取账单url地址String downloadUrl = this.queryBill(billDate, type);//创建远程Get 请求对象HttpGet httpGet = new HttpGet(downloadUrl);httpGet.addHeader("Accept", "application/json");//使用wxPayClient发送请求得到响应CloseableHttpResponse response = wxPayNoSignClient.execute(httpGet);try {String bodyAsString = EntityUtils.toString(response.getEntity());int statusCode = response.getStatusLine().getStatusCode();if (statusCode == 200) {log.info("成功, 下载账单返回结果 = " + bodyAsString);} else if (statusCode == 204) {log.info("成功");} else {throw new RuntimeException("下载账单异常, 响应码 = " + statusCode+ ", 下载账单返回结果 = " + bodyAsString);}return bodyAsString;} finally {response.close();}}
五、基础支付API V2
1、V2和V3的比较
2、引入依赖和工具
2.1 引入依赖
<!--微信支付 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")@Slf4jpublic class WxPayV2Controller {@Resourceprivate 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
@Overridepublic 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());//关联的公众号的appidparams.put("mch_id", wxPayConfig.getMchId());//商户号params.put("nonce_str", WXPayUtil.generateNonceStr());//生成随机字符串params.put("body", orderInfo.getTitle());params.put("out_trade_no", orderInfo.getOrderNo());//注意,这里必须使用字符串类型的参数(总金额:分)String totalFee = orderInfo.getTotalFee() + "";params.put("total_fee", totalFee);params.put("spbill_create_ip", remoteAddr);params.put("notify_url", wxPayConfig.getNotifyDomain().concat(WxNotifyType.NATIVE_NOTIFY_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")@Slf4jpublic class WxPayV2Controller {@Resourceprivate WxPayService wxPayService;@Resourceprivate WxPayConfig wxPayConfig;@Resourceprivate OrderInfoService orderInfoService;@Resourceprivate PaymentInfoService paymentInfoService;private final ReentrantLock lock = new ReentrantLock();/*** 支付通知* 微信支付通过支付通知接口将用户支付成功消息通知给商户*/@PostMapping("/native/notify")public String wxNotify(HttpServletRequest request) throws Exception {System.out.println("微信发送的回调");Map<String, String> returnMap = new HashMap<>();//应答对象//处理通知参数String body = HttpUtils.readData(request);//验签if(!WXPayUtil.isSignatureValid(body, wxPayConfig.getPartnerKey())) {log.error("通知验签失败");//失败应答returnMap.put("return_code", "FAIL");returnMap.put("return_msg", "验签失败");String returnXml = WXPayUtil.mapToXml(returnMap);return returnXml;}//解析xml数据Map<String, String> notifyMap = WXPayUtil.xmlToMap(body);//判断通信和业务是否成功if(!"SUCCESS".equals(notifyMap.get("return_code")) || !"SUCCESS".equals(notifyMap.get("result_code"))) {log.error("失败");//失败应答returnMap.put("return_code", "FAIL");returnMap.put("return_msg", "失败");String returnXml = WXPayUtil.mapToXml(returnMap);return returnXml;}//获取商户订单号String orderNo = notifyMap.get("out_trade_no");OrderInfo orderInfo = orderInfoService.getOrderByOrderNo(orderNo);//并校验返回的订单金额是否与商户侧的订单金额一致if (orderInfo != null && orderInfo.getTotalFee() != Long.parseLong(notifyMap.get("total_fee"))) {log.error("金额校验失败");//失败应答returnMap.put("return_code", "FAIL");returnMap.put("return_msg", "金额校验失败");String returnXml = WXPayUtil.mapToXml(returnMap);return returnXml;}//处理订单if(lock.tryLock()){try {//处理重复的通知//接口调用的幂等性:无论接口被调用多少次,产生的结果是一致的。String orderStatus = orderInfoService.getOrderStatus(orderNo);if(OrderStatus.NOTPAY.getType().equals(orderStatus)){//更新订单状态orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.SUCCESS);//记录支付日志paymentInfoService.createPaymentInfo(body);}} finally {//要主动释放锁lock.unlock();}}returnMap.put("return_code", "SUCCESS");returnMap.put("return_msg", "OK");String returnXml = WXPayUtil.mapToXml(returnMap);log.info("支付成功,已应答");return returnXml;}}
支付宝支付
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,卖家支付宝账号IDalipay.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 {@Resourceprivate 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 {@Resourceprivate Environment config;// 创建客户端对象AlipayClient。封装了签名和验签的整个功能@Beanpublic AlipayClient alipayClient() throws AlipayApiException {AlipayConfig alipayConfig = new AlipayConfig();// 设置网关地址alipayConfig.setServerUrl(config.getProperty("alipay.gateway-url"));// 设置应用IdalipayConfig.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);// 构造clientAlipayClient 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 = "网站支付宝支付")@Slf4jpublic class AliPayController {@Resourceprivate 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@Slf4jpublic class AliPayServiceImpl implements AliPayService {@Resourceprivate OrderInfoService orderInfoService;@Resourceprivate AlipayClient alipayClient;@Resourceprivate Environment config;@Transactional(rollbackFor = Exception.class)@Overridepublic 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@Slf4jpublic class OrderInfoServiceImpl extends ServiceImpl<OrderInfoMapper, OrderInfo> implements OrderInfoService {@Resourceprivate ProductMapper productMapper;@Overridepublic 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 = productIdconsole.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.codeUrlthis.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 = "网站支付宝支付")@Slf4jpublic class AliPayController {@Resourceprivate AliPayService aliPayService;@Resourceprivate Environment config;@Resourceprivate 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并继续商户自身业务处理,校验失败返回failureresult = "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@Slf4jpublic class AliPayServiceImpl implements AliPayService {@Resourceprivate OrderInfoService orderInfoService;@Resourceprivate AlipayClient alipayClient;@Resourceprivate Environment config;@Resourceprivate PaymentInfoService paymentInfoService;@Resourceprivate RefundInfoService refundsInfoService;private final ReentrantLock lock = new ReentrantLock();/*** 处理订单* @param params*/@Transactional(rollbackFor = Exception.class)@Overridepublic 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@Slf4jpublic class PaymentInfoServiceImpl extends ServiceImpl<PaymentInfoMapper, PaymentInfo> implements PaymentInfoService {/*** 记录支付日志:支付宝* @param params*/@Overridepublic 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@Slf4jpublic class AliPayServiceImpl implements AliPayService {@Resourceprivate OrderInfoService orderInfoService;@Resourceprivate AlipayClient alipayClient;@Resourceprivate Environment config;@Resourceprivate PaymentInfoService paymentInfoService;@Resourceprivate RefundInfoService refundsInfoService;private final ReentrantLock lock = new ReentrantLock();/*** 处理订单* @param params*/@Transactional(rollbackFor = Exception.class)@Overridepublic 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 = "网站支付宝支付")@Slf4jpublic class AliPayController {@Resourceprivate 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@Slf4jpublic class AliPayServiceImpl implements AliPayService {@Resourceprivate OrderInfoService orderInfoService;@Resourceprivate AlipayClient alipayClient;/*** 用户取消订单* @param orderNo*/@Overridepublic 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 = "网站支付宝支付")@Slf4jpublic class AliPayController {@Resourceprivate 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@Slf4jpublic class AliPayServiceImpl implements AliPayService {@Resourceprivate OrderInfoService orderInfoService;@Resourceprivate AlipayClient alipayClient;/*** 查询订单* @param orderNo* @return 返回订单查询结果,如果返回null则表示支付宝端尚未创建订单*/@Overridepublic 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@Componentpublic class AliPayTask {@Resourceprivate OrderInfoService orderInfoService;@Resourceprivate 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*/@Overridepublic 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@Slf4jpublic class AliPayServiceImpl implements AliPayService {@Resourceprivate OrderInfoService orderInfoService;@Resourceprivate AlipayClient alipayClient;/*** 根据订单号调用支付宝查单接口,核实订单状态* 如果订单未创建,则更新商户端订单状态(支付宝端订单未创建,商户端订单已创建。原因是下单操作时未登录支付宝,就不会在支付宝端创建订单)* 如果订单未支付,则调用关单接口关闭订单,并更新商户端订单状态* 如果订单已支付,则更新商户端订单状态,并记录支付日志(原因可能时支付宝端回调接口未成功发送、或商户端未成功接收造成的数据不一致)* @param orderNo*/@Overridepublic 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则表示支付宝端尚未创建订单*/@Overridepublic 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 = "网站支付宝支付")@Slf4jpublic class AliPayController {@Resourceprivate 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@Slf4jpublic class AliPayServiceImpl implements AliPayService {@Resourceprivate OrderInfoService orderInfoService;@Resourceprivate AlipayClient alipayClient;@Resourceprivate RefundInfoService refundsInfoService;/*** 退款* @param orderNo* @param reason*/@Transactional(rollbackFor = Exception.class)@Overridepublic 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("申请账单失败");
}
}
}
在线调试方法:











