资料来源:https://www.bilibili.com/video/BV1US4y1D77m/?p=2&spm_id_from=pageDriver

微信支付

一、微信支付介绍和接入指引

1、微信支付产品介绍

1.1 付款码支付

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

1.2 JSAPI支付

  1. 线下场所:商户展示一个支付二维码,用户使用微信扫描二维码后,输入需要支付的金额,完成支付
  2. 公众号场景:用户在微信内进入商家公众号,打开某个页面,选择某个产品,完成支付。
  3. PC网站场景:在网站中展示二维码,用户使用微信扫描二维码,输入需要支付的金额,完成支付。
  4. 特点:用户在客户端输入支付金额

    1.3 小程序支付

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

    1.4 Native支付

    Native支付是指商户展示支付二维码,用户再用微信“扫一扫”完成支付的模式。这种方式适用于PC网 站。
    特点:商家预先指定支付金额

    1.5 APP支付

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

    1.6 刷脸支付

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

    2、接入指引

    2.1 获取商户号

    微信商户平台: https://pay.weixin.qq.com/
    场景: Native支付
    步骤:提交资料 => 签署协议 => 获取商户号

2.2 获取APPID

微信公众平台: https://mp.weixin.qq.com/
步骤:注册服务号 => 服务号认证 => 获取APPID => 绑定商户号

2.3 获取API秘钥

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

2.4 获取APIv3秘钥

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

2.5 申请商户API证书

APIv3版本的所有接口都需要; APIv2版本的高级接口需要(如:退款、企业红包、企业付款等)
步骤:登录商户平台=> 选择账户中心=> 安全中心=>API安全=> 申请API证书

2.6 获取微信平台证书

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

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

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

1、信息安全的基础 - 机密性

  1. 明文:加密前的消息叫“明文” (plain text)
  2. 密文:加密后的文本叫“密文” (cipher text)
  3. 密钥:只有掌握特殊“钥匙”的人,才能对加密的文本进行解密,这里的“钥匙”就叫做“密钥” (key)
    1. “密钥”就是一个字符串,度量单位是“位” (bit),比如,密钥长度是 128,就是16 字节的二进制串
  4. 加密:实现机密性最常用的手段是“加密” (encrypt)
    1. 按照密钥的使用方式,加密可以分为两大类: 对称加密和非对称加密。
  5. 解密:使用密钥还原明文的过程叫“解密” (decrypt)
  6. 加密算法:加密解密的操作过程就是“加密算法”

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

      2、对称加密和非对称加密

  7. 对称加密

    1. 特点:只使用一个密钥,密钥必须保密,常用的有AES算法
    2. 优点:运算速度快
    3. 缺点:秘钥需要信息交换的双方共享,一旦被窃取,消息会被破解,无法做到安全的密钥交换
  8. 非对称加密
    1. 特点:使用两个密钥:公钥和私钥,公钥可以任意分发而私钥保密,常用的有RSA
    2. 优点:黑客获取公钥无法破解密文,解决了密钥交换的问题
    3. 缺点:运算速度非常慢
  9. 混合加密
    1. 实际场景中把对称加密和非对称加密结合起来使用。

      3、身份认证

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

    4、摘要算法(Digest Algorithm)

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

    1. 不可逆:只有算法,没有秘钥,只能加密,不能解密
    2. 难题友好性:想要破解,只能暴力枚举
    3. 发散性:只要对原文进行一点点改动,摘要就会发生剧烈变化
    4. 抗碰撞性:原文不同,计算后的摘要也要不同

常见摘要算法:
MD5、SHA1、SHA2 ( SHA224、SHA256、SHA384)

5、数字签名

  1. 数字签名是使用私钥对摘要加密生成签名,需要由公钥将签名解密后进行验证,实现身份认证和不可否认。<br />签名和验证签名的流程:<br />![image.png](https://cdn.nlark.com/yuque/0/2022/png/22523384/1652002116798-e83f20d9-4c74-4d8c-894c-e549fdb82e02.png#clientId=u035f50c6-59a7-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=620&id=u6f8d1583&margin=%5Bobject%20Object%5D&name=image.png&originHeight=620&originWidth=1254&originalType=binary&ratio=1&rotation=0&showTitle=false&size=211703&status=done&style=none&taskId=u1ccf341f-c659-458d-bbd3-bc04358d022&title=&width=1254)

6、数字证书

数字证书解决“公钥的信任”问题,可以防止黑客伪造公钥。
不能直接分发公钥,公钥的分发必须使用数字证书,数字证书由CA颁发
https协议中的数字证书:
image.png

7、微信APIv3证书

商户证书:
商户API证书是指由商户申请的,包含商户的商户号、公司名称、公钥信息的证书。
商户证书在商户后台申请: https://pay.weixin.qq.com/index.php/core/cert/api_cert#/
image.png
平台证书(微信支付平台):
微信支付平台证书是指由微信支付负责申请的,包含微信支付平台标识、公钥信息的证书。商户可以使 用平台证书中的公钥进行验签。
平台证书的获取: https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay3_0.shtml
image.png

8、API密钥和APIv3密钥

都是对称加密需要使用的加密和解密密钥,一定要保管好,不能泄露。
API密钥对应V2版本的API
APIv3密钥对应V3版本的API

三、案例项目的创建

1、创建SpringBoot项目

1.1 新建项目
image.png
1.2 添加依赖

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

1.3 配置application.yml文件

  1. server:
  2. port: 8090 #服务端口
  3. spring:
  4. application:
  5. name: payment-demo # 应用名称

1.4 创建controller

  1. package com.atguigu.paymentdemo.controller;
  2. @CrossOrigin // 开放前端的跨域访问
  3. @Api(tags = "商品管理")
  4. @RestController
  5. @RequestMapping("/api/product")
  6. public class ProductController {
  7. @ApiOperation("测试接口")
  8. @GetMapping("/test")
  9. public String test(){
  10. return "hello";
  11. }
  12. }

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

2、引入Swagger

作用:自动生成接口文档和测试页面。
2.1 引入依赖

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

2.2 Swagger配置文件
创建config包,创建Swagger2Config类

  1. package com.atguigu.paymentdemo.config;
  2. @Configuration
  3. @EnableSwagger2
  4. public class Swagger2Config {
  5. @Bean
  6. public Docket docket(){
  7. return new Docket(DocumentationType.SWAGGER_2)
  8. .apiInfo(new ApiInfoBuilder().title("微信支付案例接口文档").build());
  9. }
  10. }

2.3 Swagger注解
controller中可以添加常用注解

  1. @Api(tags="商品管理") // 用在类上
  2. @ApiOperation("测试接口") // 用在方法上

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

3、定义统一结果

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

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

3.2 创建R类
创建统一结果类

  1. package com.atguigu.paymentdemo.vo;
  2. @Data
  3. @Accessors(chain = true)
  4. public class R {
  5. private Integer code; //响应码
  6. private String message; //响应消息
  7. private Map<String, Object> data = new HashMap<>();
  8. public static R ok(){
  9. R r = new R();
  10. r.setCode(0);
  11. r.setMessage("成功");
  12. return r;
  13. }
  14. public static R error(){
  15. R r = new R();
  16. r.setCode(-1);
  17. r.setMessage("失败");
  18. return r;
  19. }
  20. public R data(String key, Object value){
  21. this.data.put(key, value);
  22. return this;
  23. }
  24. }

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

  1. @CrossOrigin //开放前端的跨域访问
  2. @Api(tags = "商品管理")
  3. @RestController
  4. @RequestMapping("/api/product")
  5. public class ProductController {
  6. @ApiOperation("测试接口")
  7. @GetMapping("/test")
  8. public R test(){
  9. return R.ok()
  10. .data("message", "hello")
  11. .data("now", new Date());
  12. }
  13. }

3.4 配置json时间格式

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

4、创建数据库

4.1 创建数据库

  1. mysql -uroot -p
  2. mysql> create database payment_demo;

4.2 IDEA配置数据库连接
(1)打开数据库面板
image.png
(2)添加数据库
image.png
(3)配置数据库连接参数
image.png
4.3 执行SQL脚本
payment_demo.sql
image.png

5、集成MyBatis-Plus

5.1 引入依赖

  1. <!--mysql 驱动-->
  2. <dependency>
  3. <groupId>mysql</groupId>
  4. <artifactId>mysql-connector-java</artifactId>
  5. </dependency>
  6. <!--MyBatis-Plus:是MyBatis的增强-->
  7. <dependency>
  8. <groupId>com.baomidou</groupId>
  9. <artifactId>mybatis-plus-boot-starter</artifactId>
  10. <version>3.3.1</version>
  11. </dependency>

5.2 配置数据库连接

  1. spring:
  2. datasource: # mysql数据库连接
  3. driver-class-name: com.mysql.cj.jdbc.Driver
  4. url: jdbc:mysql://localhost:3306/payment_demo?serverTimezone=GMT%2B8&characterEncoding=utf-8
  5. username: root
  6. password: 123456

5.3 定义实体类
BaseEntity是父类,其他类继承BaseEntity
image.png
5.4 定义持久层
定义Mapper接口继承BaseMapper<>,定义xml配置文件
image.png
5.5 定义MyBatis-Plus的配置文件
在config包中创建配置文件MybatisPlusConfig

  1. package com.atguigu.paymentdemo.config;
  2. @Configuration
  3. @MapperScan("com.atguigu.paymentdemo.mapper") // 持久层扫描
  4. @EnableTransactionManagement // 启用事务管理
  5. public class MyBatisPlusConfig {
  6. }

5.6 定义yml配置文件
添加持久层日志和xml文件位置的配置

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

5.7 定义业务层
定义业务层接口继承IService<>
定义业务层接口的实现类,并继承ServiceImpl<,>
image.png
5.8 定义接口方法查询所有商品
在ProductController中添加一个方法

  1. package com.atguigu.paymentdemo.controller;
  2. @CrossOrigin //开放前端的跨域访问
  3. @Api(tags = "商品管理")
  4. @RestController
  5. @RequestMapping("/api/product")
  6. public class ProductController {
  7. @Resource
  8. private ProductService productService;
  9. @ApiOperation("商品列表")
  10. @GetMapping("/list")
  11. public R list(){
  12. List<Product> list = productService.list();
  13. return R.ok().data("productList", list);
  14. }
  15. }

5.9 pom中配置build节点
因为maven工程在默认情况下src/main/java 目录下的所有资源文件是不发布到target目录下的,我们 在 pom 文件的节点下配置一个资源发布过滤器

  1. <build>
  2. <!-- 项目打包时会将java目录中的*.xml文件也进行打包 -->
  3. <resources>
  4. <resource>
  5. <directory>src/main/java</directory>
  6. <includes>
  7. <include>**/*.xml</include>
  8. </includes>
  9. <filtering>false</filtering>
  10. </resource>
  11. </resources>
  12. </build>

6、搭建前端环境

6.1 安装Node.js
Node.js是一个基于JavaScript引擎的服务器端环境,前端项目在开发环境下要基于Node.js来运行
安装: node-v14.18.0-x64.msi

6.2 运行前端项目
将项目放在磁盘的一个目录中,例如D:\demo\payment-demo-front
进入项目目录,运行下面的命令启动项目:

  1. npm run serve

6.3 安装VSCode
如果你希望方便的查看和修改前端代码,可以安装一个VSCode
安装: VSCodeUserSetup-x64-1.56.2
安装插件:
image.png

7、Vue.js入门

官网: https://cn.vuejs.org/
Vue.js是一个前端框架,帮助我们快速构建前端项目。
使用vue有两种方式,一个是传统的在html 文件中引入js 脚本文件的方式,另一个是脚手架的方式。 我们的项目,使用的是脚手架的方式。
7.1 安装脚手架

  1. #经过下面的配置,所有的 npm install 都会经过淘宝的镜像地址下载
  2. npm config set registry https://registry.npm.taobao.org
  1. npm install -g @vue/cli

7.2 创建一个项目
先进入项目目录(Ctrl + ~),然后创建一个项目

  1. vue create vue-demo

7.3 运行项目

  1. npm run serve
  2. # 指定运行端口
  3. npm run serve -- --port 8888

7.4 数据绑定

  1. <!--定义页面结构-->
  2. <template>
  3. <div>
  4. <h1>Vue案例</h1>
  5. <!-- 插值 -->
  6. <p>{{course}}</p>
  7. </div>
  8. </template>
  9. <!--定义页面脚本-->
  10. <script>
  11. export default {
  12. // 定义数据
  13. data () {
  14. return {
  15. course: '微信支付'
  16. }
  17. }
  18. }
  19. </script>

7.5 安装Vue调试工具
在Chrome的扩展程序中安装:Vue.jsDevtools.zip
(1)扩展程序的安装
image.png
(2)扩展程序的使用
image.png7.6 双向数据绑定
数据会绑定到组件,组件的改变也会影响数据定义

  1. <p>
  2. <!-- 指令-->
  3. <input type="text" v-model="course">
  4. </p>

7.7 事件处理
(1)定义事件

  1. // 定义方法
  2. methods: {
  3. toPay(){
  4. console.log('去支付')
  5. }
  6. }

(2)调用事件

  1. <p>
  2. <!-- 事件-->
  3. <button @click="toPay()">去支付</button>
  4. </p>

四、基础支付API V3

1、引入支付参数

1.1 定义微信支付相关参数
这个文件定义了之前我们准备的微信支付相关的参数,例如商户号、APPID、API秘钥等等

  1. # 微信支付相关参数
  2. # 商户号
  3. wxpay.mch-id=1558950191
  4. # 商户API证书序列号
  5. wxpay.mch-serial-no=34345964330B66427E0D3D28826C4993C77E631F
  6. # 商户私钥文件
  7. wxpay.private-key-path=apiclient_key.pem
  8. # APIv3密钥
  9. wxpay.api-v3-key=UDuLFDcmy5Eb6o0nTNZdu6ek4DDh4K8B
  10. # APPID
  11. wxpay.appid=wx74862e0dfcf69954
  12. # 微信服务器地址
  13. wxpay.domain=https://api.mch.weixin.qq.com
  14. # 接收结果通知地址
  15. # 注意:每次重新启动ngrok,都需要根据实际情况修改这个配置
  16. wxpay.notify-domain=https://77ea-221-239-177-21.ngrok.io
  17. # APIv2密钥
  18. wxpay.partnerKey: T6m9iK73b0kn9g5v426MKfHQH7X8rKwb

1.2 读取支付参数

  1. package com.atguigu.paymentdemo.config;
  2. @Configuration
  3. @PropertySource("classpath:wxpay.properties") // 读取配置文件
  4. @ConfigurationProperties(prefix="wxpay") // 读取wxpay节点
  5. @Data // 使用set方法将wxpay节点中的值填充到当前类的属性中
  6. @Slf4j
  7. public class WxPayConfig {
  8. // 商户号
  9. private String mchId;
  10. // 商户API证书序列号
  11. private String mchSerialNo;
  12. // 商户私钥文件
  13. private String privateKeyPath;
  14. // APIv3密钥
  15. private String apiV3Key;
  16. // APPID
  17. private String appid;
  18. // 微信服务器地址
  19. private String domain;
  20. // 接收结果通知地址
  21. private String notifyDomain;
  22. // APIv2密钥
  23. private String partnerKey;
  24. /**
  25. * 获取商户的私钥文件
  26. * @param filename
  27. * @return
  28. */
  29. public PrivateKey getPrivateKey(String filename){
  30. try {
  31. return PemUtil.loadPrivateKey(new FileInputStream(filename));
  32. } catch (FileNotFoundException e) {
  33. throw new RuntimeException("私钥文件不存在", e);
  34. }
  35. }
  36. /**
  37. * 获取签名验证器
  38. * @return
  39. */
  40. @Bean
  41. public ScheduledUpdateCertificatesVerifier getVerifier(){
  42. log.info("获取签名验证器");
  43. //获取商户私钥
  44. PrivateKey privateKey = getPrivateKey(privateKeyPath);
  45. //私钥签名对象
  46. PrivateKeySigner privateKeySigner = new PrivateKeySigner(mchSerialNo, privateKey);
  47. //身份认证对象
  48. WechatPay2Credentials wechatPay2Credentials = new WechatPay2Credentials(mchId, privateKeySigner);
  49. // 使用定时更新的签名验证器,不需要传入证书
  50. ScheduledUpdateCertificatesVerifier verifier = new ScheduledUpdateCertificatesVerifier(
  51. wechatPay2Credentials,
  52. apiV3Key.getBytes(StandardCharsets.UTF_8));
  53. return verifier;
  54. }
  55. /**
  56. * 获取http请求对象
  57. * @param verifier
  58. * @return
  59. */
  60. @Bean(name = "wxPayClient")
  61. public CloseableHttpClient getWxPayClient(ScheduledUpdateCertificatesVerifier verifier){
  62. log.info("获取httpClient");
  63. //获取商户私钥
  64. PrivateKey privateKey = getPrivateKey(privateKeyPath);
  65. WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create()
  66. .withMerchant(mchId, mchSerialNo, privateKey)
  67. .withValidator(new WechatPay2Validator(verifier));
  68. // ... 接下来,你仍然可以通过builder设置各种参数,来配置你的HttpClient
  69. // 通过WechatPayHttpClientBuilder构造的HttpClient,会自动的处理签名和验签,并进行证书自动更新
  70. CloseableHttpClient httpClient = builder.build();
  71. return httpClient;
  72. }
  73. /**
  74. * 获取HttpClient,无需进行应答签名验证,跳过验签的流程
  75. */
  76. @Bean(name = "wxPayNoSignClient")
  77. public CloseableHttpClient getWxPayNoSignClient(){
  78. //获取商户私钥
  79. PrivateKey privateKey = getPrivateKey(privateKeyPath);
  80. //用于构造HttpClient
  81. WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create()
  82. //设置商户信息
  83. .withMerchant(mchId, mchSerialNo, privateKey)
  84. //无需进行签名验证、通过withValidator((response) -> true)实现
  85. .withValidator((response) -> true);
  86. // 通过WechatPayHttpClientBuilder构造的HttpClient,会自动的处理签名和验签,并进行证书自动更新
  87. CloseableHttpClient httpClient = builder.build();
  88. log.info("== getWxPayNoSignClient END ==");
  89. return httpClient;
  90. }
  91. }

1.3 测试支付参数的获取
在controller 包中创建TestController

  1. package com.atguigu.paymentdemo.controller;
  2. @Api(tags = "测试控制器")
  3. @RestController
  4. @RequestMapping("/api/test")
  5. public class TestController {
  6. @Resource
  7. private WxPayConfig wxPayConfig;
  8. @GetMapping
  9. public R getWxPayConfig(){
  10. String mchId = wxPayConfig.getMchId();
  11. return R.ok().data("mchId", mchId);
  12. }
  13. }

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

  1. <!-- 生成自定义配置的元数据信息 -->
  2. <dependency>
  3. <groupId>org.springframework.boot</groupId>
  4. <artifactId>spring-boot-configuration-processor</artifactId>
  5. <optional>true</optional>
  6. </dependency>

1.5 在IDEA中设置 SpringBoot 配置文件
让IDEA可以识别配置文件,将配置文件的图标展示成SpringBoot的图标,同时配置文件的内容可以高 亮显示
File -> Project Structure -> Modules -> 选择小叶子
image.png
点击(+)图标
image.png
选中配置文件:
image.png

2、加载商户私钥

2.1 复制商户私钥
将下载的私钥文件复制到项目根目录下:
image.png
2.2 引入SDK
https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay6_0.shtml
我们可以使用官方提供的SDK,帮助我们完成开发。实现了请求签名的生成和应答签名的验证。

  1. <!--微信支付SDK-->
  2. <dependency>
  3. <groupId>com.github.wechatpay-apiv3</groupId>
  4. <artifactId>wechatpay-apache-httpclient</artifactId>
  5. <version>0.3.0</version>
  6. </dependency>

2.3 获取商户私钥
https://github.com/wechatpay-apiv3/wechatpay-apache-httpclient (如何加载商户私钥)

  1. @Configuration
  2. @PropertySource("classpath:wxpay.properties") // 读取配置文件
  3. @ConfigurationProperties(prefix="wxpay") // 读取wxpay节点
  4. @Data // 使用set方法将wxpay节点中的值填充到当前类的属性中
  5. @Slf4j
  6. public class WxPayConfig {
  7. /**
  8. * 获取商户的私钥文件
  9. * @param filename
  10. * @return
  11. */
  12. public PrivateKey getPrivateKey(String filename){
  13. try {
  14. return PemUtil.loadPrivateKey(new FileInputStream(filename));
  15. } catch (FileNotFoundException e) {
  16. throw new RuntimeException("私钥文件不存在", e);
  17. }
  18. }
  19. }

2.4 测试商户私钥的获取
在PaymentDemoApplicationTests 测试类中添加如下方法,测试私钥对象是否能够获取出来。
(将前面的方法改成public的再进行测试)

3、获取签名验证器和HttpClient

3.1 证书密钥使用说明
https://pay.weixin.qq.com/wiki/doc/apiv3_partner/wechatpay/wechatpay3_0.shtml
image.png
3.2 获取签名验证器
https://github.com/wechatpay-apiv3/wechatpay-apache-httpclient (定时更新平台证书功能)
平台证书:平台证书封装了微信的公钥,商户可以使用平台证书中的公钥进行验签。
签名验证器:帮助我们进行验签工作,我们单独将它定义出来,方便后面的开发。

  1. @Configuration
  2. @PropertySource("classpath:wxpay.properties") // 读取配置文件
  3. @ConfigurationProperties(prefix="wxpay") // 读取wxpay节点
  4. @Data // 使用set方法将wxpay节点中的值填充到当前类的属性中
  5. @Slf4j
  6. public class WxPayConfig {
  7. /**
  8. * 获取签名验证器
  9. * @return
  10. */
  11. @Bean
  12. public ScheduledUpdateCertificatesVerifier getVerifier(){
  13. log.info("获取签名验证器");
  14. //获取商户私钥
  15. PrivateKey privateKey = getPrivateKey(privateKeyPath);
  16. //私钥签名对象
  17. PrivateKeySigner privateKeySigner = new PrivateKeySigner(mchSerialNo, privateKey);
  18. //身份认证对象
  19. WechatPay2Credentials wechatPay2Credentials = new WechatPay2Credentials(mchId, privateKeySigner);
  20. // 使用定时更新的签名验证器,不需要传入证书
  21. ScheduledUpdateCertificatesVerifier verifier = new ScheduledUpdateCertificatesVerifier(
  22. wechatPay2Credentials,
  23. apiV3Key.getBytes(StandardCharsets.UTF_8));
  24. return verifier;
  25. }
  26. }

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

  1. @Configuration
  2. @PropertySource("classpath:wxpay.properties") // 读取配置文件
  3. @ConfigurationProperties(prefix="wxpay") // 读取wxpay节点
  4. @Data // 使用set方法将wxpay节点中的值填充到当前类的属性中
  5. @Slf4j
  6. public class WxPayConfig {
  7. /**
  8. * 获取http请求对象
  9. * @param verifier
  10. * @return
  11. */
  12. @Bean(name = "wxPayClient")
  13. public CloseableHttpClient getWxPayClient(ScheduledUpdateCertificatesVerifier verifier){
  14. log.info("获取httpClient");
  15. //获取商户私钥
  16. PrivateKey privateKey = getPrivateKey(privateKeyPath);
  17. WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create()
  18. .withMerchant(mchId, mchSerialNo, privateKey)
  19. .withValidator(new WechatPay2Validator(verifier));
  20. // ... 接下来,你仍然可以通过builder设置各种参数,来配置你的HttpClient
  21. // 通过WechatPayHttpClientBuilder构造的HttpClient,会自动的处理签名和验签,并进行证书自动更新
  22. CloseableHttpClient httpClient = builder.build();
  23. return httpClient;
  24. }
  25. /**
  26. * 获取HttpClient,无需进行应答签名验证,跳过验签的流程
  27. */
  28. @Bean(name = "wxPayNoSignClient")
  29. public CloseableHttpClient getWxPayNoSignClient(){
  30. //获取商户私钥
  31. PrivateKey privateKey = getPrivateKey(privateKeyPath);
  32. //用于构造HttpClient
  33. WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create()
  34. //设置商户信息
  35. .withMerchant(mchId, mchSerialNo, privateKey)
  36. //无需进行签名验证、通过withValidator((response) -> true)实现
  37. .withValidator((response) -> true);
  38. // 通过WechatPayHttpClientBuilder构造的HttpClient,会自动的处理签名和验签,并进行证书自动更新
  39. CloseableHttpClient httpClient = builder.build();
  40. log.info("== getWxPayNoSignClient END ==");
  41. return httpClient;
  42. }
  43. }

4、API字典和相关工具

4.1 API列表
https://pay.weixin.qq.com/wiki/doc/apiv3/open/pay/chapter2_7_3.shtml
我们的项目中要实现以下所有API的功能。
image.png
4.2 接口规则
https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay2_0.shtml
微信支付APIv3 使用JSON作为消息体的数据交换格式。

  1. <!--json处理器-->
  2. <dependency>
  3. <groupId>com.google.code.gson</groupId>
  4. <artifactId>gson</artifactId>
  5. </dependency>

4.3 定义枚举
将资料文件夹中的enums 目录复制到源码目录中。
为了开发方便,我们预先在项目中定义一些枚举。枚举中定义的内容包括接口地址,支付状态等信息。
image.png
4.4 添加工具类

  1. public class HttpUtils {
  2. /**
  3. * 将通知参数转化为字符串
  4. * @param request
  5. * @return
  6. */
  7. public static String readData(HttpServletRequest request) {
  8. BufferedReader br = null;
  9. try {
  10. StringBuilder result = new StringBuilder();
  11. br = request.getReader();
  12. for (String line; (line = br.readLine()) != null; ) {
  13. if (result.length() > 0) {
  14. result.append("\n");
  15. }
  16. result.append(line);
  17. }
  18. return result.toString();
  19. } catch (IOException e) {
  20. throw new RuntimeException(e);
  21. } finally {
  22. if (br != null) {
  23. try {
  24. br.close();
  25. } catch (IOException e) {
  26. e.printStackTrace();
  27. }
  28. }
  29. }
  30. }
  31. }
  1. public class OrderNoUtils {
  2. /**
  3. * 获取订单编号
  4. * @return
  5. */
  6. public static String getOrderNo() {
  7. return "ORDER_" + getNo();
  8. }
  9. /**
  10. * 获取退款单编号
  11. * @return
  12. */
  13. public static String getRefundNo() {
  14. return "REFUND_" + getNo();
  15. }
  16. /**
  17. * 获取编号
  18. * @return
  19. */
  20. public static String getNo() {
  21. SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss");
  22. String newDate = sdf.format(new Date());
  23. String result = "";
  24. Random random = new Random();
  25. for (int i = 0; i < 3; i++) {
  26. result += random.nextInt(10);
  27. }
  28. return newDate + result;
  29. }
  30. }
  1. public class HttpClientUtils {
  2. private String url;
  3. private Map<String, String> param;
  4. private int statusCode;
  5. private String content;
  6. private String xmlParam;
  7. private boolean isHttps;
  8. public boolean isHttps() {
  9. return isHttps;
  10. }
  11. public void setHttps(boolean isHttps) {
  12. this.isHttps = isHttps;
  13. }
  14. public String getXmlParam() {
  15. return xmlParam;
  16. }
  17. public void setXmlParam(String xmlParam) {
  18. this.xmlParam = xmlParam;
  19. }
  20. public HttpClientUtils(String url, Map<String, String> param) {
  21. this.url = url;
  22. this.param = param;
  23. }
  24. public HttpClientUtils(String url) {
  25. this.url = url;
  26. }
  27. public void setParameter(Map<String, String> map) {
  28. param = map;
  29. }
  30. public void addParameter(String key, String value) {
  31. if (param == null)
  32. param = new HashMap<String, String>();
  33. param.put(key, value);
  34. }
  35. public void post() throws ClientProtocolException, IOException {
  36. HttpPost http = new HttpPost(url);
  37. setEntity(http);
  38. execute(http);
  39. }
  40. public void put() throws ClientProtocolException, IOException {
  41. HttpPut http = new HttpPut(url);
  42. setEntity(http);
  43. execute(http);
  44. }
  45. public void get() throws ClientProtocolException, IOException {
  46. if (param != null) {
  47. StringBuilder url = new StringBuilder(this.url);
  48. boolean isFirst = true;
  49. for (String key : param.keySet()) {
  50. if (isFirst) {
  51. url.append("?");
  52. isFirst = false;
  53. }else {
  54. url.append("&");
  55. }
  56. url.append(key).append("=").append(param.get(key));
  57. }
  58. this.url = url.toString();
  59. }
  60. HttpGet http = new HttpGet(url);
  61. execute(http);
  62. }
  63. /**
  64. * set http post,put param
  65. */
  66. private void setEntity(HttpEntityEnclosingRequestBase http) {
  67. if (param != null) {
  68. List<NameValuePair> nvps = new LinkedList<NameValuePair>();
  69. for (String key : param.keySet())
  70. nvps.add(new BasicNameValuePair(key, param.get(key))); // 参数
  71. http.setEntity(new UrlEncodedFormEntity(nvps, Consts.UTF_8)); // 设置参数
  72. }
  73. if (xmlParam != null) {
  74. http.setEntity(new StringEntity(xmlParam, Consts.UTF_8));
  75. }
  76. }
  77. private void execute(HttpUriRequest http) throws ClientProtocolException,
  78. IOException {
  79. CloseableHttpClient httpClient = null;
  80. try {
  81. if (isHttps) {
  82. SSLContext sslContext = new SSLContextBuilder()
  83. .loadTrustMaterial(null, new TrustStrategy() {
  84. // 信任所有
  85. public boolean isTrusted(X509Certificate[] chain,
  86. String authType)
  87. throws CertificateException {
  88. return true;
  89. }
  90. }).build();
  91. SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(
  92. sslContext);
  93. httpClient = HttpClients.custom().setSSLSocketFactory(sslsf)
  94. .build();
  95. } else {
  96. httpClient = HttpClients.createDefault();
  97. }
  98. CloseableHttpResponse response = httpClient.execute(http);
  99. try {
  100. if (response != null) {
  101. if (response.getStatusLine() != null)
  102. statusCode = response.getStatusLine().getStatusCode();
  103. HttpEntity entity = response.getEntity();
  104. // 响应内容
  105. content = EntityUtils.toString(entity, Consts.UTF_8);
  106. }
  107. } finally {
  108. response.close();
  109. }
  110. } catch (Exception e) {
  111. e.printStackTrace();
  112. } finally {
  113. httpClient.close();
  114. }
  115. }
  116. public int getStatusCode() {
  117. return statusCode;
  118. }
  119. public String getContent() throws ParseException, IOException {
  120. return content;
  121. }
  122. }
  1. public class WechatPay2ValidatorForRequest {
  2. protected static final Logger log = LoggerFactory.getLogger(WechatPay2ValidatorForRequest.class);
  3. /**
  4. * 应答超时时间,单位为分钟
  5. */
  6. protected static final long RESPONSE_EXPIRED_MINUTES = 5;
  7. protected final Verifier verifier;
  8. protected final String requestId;
  9. protected final String body;
  10. public WechatPay2ValidatorForRequest(Verifier verifier, String requestId, String body) {
  11. this.verifier = verifier;
  12. this.requestId = requestId;
  13. this.body = body;
  14. }
  15. protected static IllegalArgumentException parameterError(String message, Object... args) {
  16. message = String.format(message, args);
  17. return new IllegalArgumentException("parameter error: " + message);
  18. }
  19. protected static IllegalArgumentException verifyFail(String message, Object... args) {
  20. message = String.format(message, args);
  21. return new IllegalArgumentException("signature verify fail: " + message);
  22. }
  23. public final boolean validate(HttpServletRequest request) throws IOException {
  24. try {
  25. //处理请求参数
  26. validateParameters(request);
  27. //构造验签名串
  28. String message = buildMessage(request);
  29. String serial = request.getHeader(WECHAT_PAY_SERIAL);
  30. String signature = request.getHeader(WECHAT_PAY_SIGNATURE);
  31. //验签
  32. if (!verifier.verify(serial, message.getBytes(StandardCharsets.UTF_8), signature)) {
  33. throw verifyFail("serial=[%s] message=[%s] sign=[%s], request-id=[%s]",
  34. serial, message, signature, requestId);
  35. }
  36. } catch (IllegalArgumentException e) {
  37. log.warn(e.getMessage());
  38. return false;
  39. }
  40. return true;
  41. }
  42. protected final void validateParameters(HttpServletRequest request) {
  43. // NOTE: ensure HEADER_WECHAT_PAY_TIMESTAMP at last
  44. String[] headers = {WECHAT_PAY_SERIAL, WECHAT_PAY_SIGNATURE, WECHAT_PAY_NONCE, WECHAT_PAY_TIMESTAMP};
  45. String header = null;
  46. for (String headerName : headers) {
  47. header = request.getHeader(headerName);
  48. if (header == null) {
  49. throw parameterError("empty [%s], request-id=[%s]", headerName, requestId);
  50. }
  51. }
  52. //判断请求是否过期
  53. String timestampStr = header;
  54. try {
  55. Instant responseTime = Instant.ofEpochSecond(Long.parseLong(timestampStr));
  56. // 拒绝过期请求
  57. if (Duration.between(responseTime, Instant.now()).abs().toMinutes() >= RESPONSE_EXPIRED_MINUTES) {
  58. throw parameterError("timestamp=[%s] expires, request-id=[%s]", timestampStr, requestId);
  59. }
  60. } catch (DateTimeException | NumberFormatException e) {
  61. throw parameterError("invalid timestamp=[%s], request-id=[%s]", timestampStr, requestId);
  62. }
  63. }
  64. protected final String buildMessage(HttpServletRequest request) throws IOException {
  65. String timestamp = request.getHeader(WECHAT_PAY_TIMESTAMP);
  66. String nonce = request.getHeader(WECHAT_PAY_NONCE);
  67. return timestamp + "\n"
  68. + nonce + "\n"
  69. + body + "\n";
  70. }
  71. protected final String getResponseBody(CloseableHttpResponse response) throws IOException {
  72. HttpEntity entity = response.getEntity();
  73. return (entity != null && entity.isRepeatable()) ? EntityUtils.toString(entity) : "";
  74. }
  75. }

5、Native下单API

5.1 Native支付流程
https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_4.shtml
image.png
5.2 Native下单API
https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_1.shtml
商户端发起支付请求,微信端创建支付订单并生成支付二维码链接,微信端将支付二维码返回给商户 端,商户端显示支付二维码,用户使用微信客户端扫码后发起支付。
(1)创建 WxPayController

  1. @CrossOrigin //跨域
  2. @RestController
  3. @RequestMapping("/api/wx-pay")
  4. @Api(tags = "网站微信支付APIv3")
  5. @Slf4j
  6. public class WxPayController {
  7. @Resource
  8. private WxPayService wxPayService;
  9. @Resource
  10. private Verifier verifier;
  11. /**
  12. * Native下单
  13. * @param productId
  14. * @return
  15. * @throws Exception
  16. */
  17. @ApiOperation("调用统一下单API,生成支付二维码")
  18. @PostMapping("/native/{productId}")
  19. public R nativePay(@PathVariable Long productId) throws Exception {
  20. log.info("发起支付请求 v3");
  21. // 返回支付二维码连接和订单号
  22. Map<String, Object> map = wxPayService.nativePay(productId);
  23. return R.ok().setData(map);
  24. }
  25. }

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

  1. @Data
  2. @Accessors(chain = true) //链式操作
  3. public class R {
  4. }

(2)创建WxPayService

  1. public interface WxPayService {
  2. Map<String, Object> nativePay(Long productId) throws Exception;
  3. }

参考:
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

  1. @Service
  2. @Slf4j
  3. public class WxPayServiceImpl implements WxPayService {
  4. @Resource
  5. private WxPayConfig wxPayConfig;
  6. @Resource
  7. private CloseableHttpClient wxPayClient;
  8. @Resource
  9. private OrderInfoService orderInfoService;
  10. @Resource
  11. private PaymentInfoService paymentInfoService;
  12. @Resource
  13. private RefundInfoService refundsInfoService;
  14. @Resource
  15. private CloseableHttpClient wxPayNoSignClient; //无需应答签名
  16. private final ReentrantLock lock = new ReentrantLock();
  17. /**
  18. * 创建订单,调用Native支付接口
  19. * @param productId
  20. * @return code_url 和 订单号
  21. * @throws Exception
  22. */
  23. @Transactional(rollbackFor = Exception.class)
  24. @Override
  25. public Map<String, Object> nativePay(Long productId) throws Exception {
  26. log.info("生成订单");
  27. //生成订单
  28. OrderInfo orderInfo = orderInfoService.createOrderByProductId(productId, PayType.WXPAY.getType());
  29. String codeUrl = orderInfo.getCodeUrl();
  30. if(orderInfo != null && !StringUtils.isEmpty(codeUrl)){
  31. log.info("订单已存在,二维码已保存");
  32. //返回二维码
  33. Map<String, Object> map = new HashMap<>();
  34. map.put("codeUrl", codeUrl);
  35. map.put("orderNo", orderInfo.getOrderNo());
  36. return map;
  37. }
  38. log.info("调用统一下单API");
  39. //调用统一下单API
  40. HttpPost httpPost = new HttpPost(wxPayConfig.getDomain().concat(WxApiType.NATIVE_PAY.getType()));
  41. // 请求body参数
  42. Gson gson = new Gson();
  43. Map paramsMap = new HashMap();
  44. paramsMap.put("appid", wxPayConfig.getAppid());
  45. paramsMap.put("mchid", wxPayConfig.getMchId());
  46. paramsMap.put("description", orderInfo.getTitle());
  47. paramsMap.put("out_trade_no", orderInfo.getOrderNo());
  48. paramsMap.put("notify_url", wxPayConfig.getNotifyDomain().concat(WxNotifyType.NATIVE_NOTIFY.getType()));
  49. Map amountMap = new HashMap();
  50. amountMap.put("total", orderInfo.getTotalFee());
  51. amountMap.put("currency", "CNY");
  52. paramsMap.put("amount", amountMap);
  53. //将参数转换成json字符串
  54. String jsonParams = gson.toJson(paramsMap);
  55. log.info("请求参数 ===> {}" + jsonParams);
  56. StringEntity entity = new StringEntity(jsonParams,"utf-8");
  57. entity.setContentType("application/json");
  58. httpPost.setEntity(entity);
  59. httpPost.setHeader("Accept", "application/json");
  60. //完成签名并执行请求
  61. CloseableHttpResponse response = wxPayClient.execute(httpPost);
  62. try {
  63. String bodyAsString = EntityUtils.toString(response.getEntity());//响应体
  64. int statusCode = response.getStatusLine().getStatusCode();//响应状态码
  65. if (statusCode == 200) { //处理成功
  66. log.info("成功, 返回结果 = " + bodyAsString);
  67. } else if (statusCode == 204) { //处理成功,无返回Body
  68. log.info("成功");
  69. } else {
  70. log.info("Native下单失败,响应码 = " + statusCode+ ",返回结果 = " + bodyAsString);
  71. throw new IOException("request failed");
  72. }
  73. //响应结果
  74. Map<String, String> resultMap = gson.fromJson(bodyAsString, HashMap.class);
  75. //二维码
  76. codeUrl = resultMap.get("code_url");
  77. //保存二维码
  78. String orderNo = orderInfo.getOrderNo();
  79. orderInfoService.saveCodeUrl(orderNo, codeUrl);
  80. //返回二维码
  81. Map<String, Object> map = new HashMap<>();
  82. map.put("codeUrl", codeUrl);
  83. map.put("orderNo", orderInfo.getOrderNo());
  84. return map;
  85. } finally {
  86. response.close();
  87. }
  88. }
  89. }

5.3 签名和验签源码解析
(1)签名原理
开启debug日志

  1. logging:
  2. level:
  3. root: info

签名生成流程:https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay4_0.shtml
签名生成源码:
image.png
(2)验签原理
签名验证流程:https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay4_1.shtml
签名验证源码:
image.png
5.4 创建课程订单
(1)保存订单

  1. public interface OrderInfoService extends IService<OrderInfo> {
  2. OrderInfo createOrderByProductId(Long productId, String paymentType);
  3. }
  1. @Service
  2. @Slf4j
  3. public class OrderInfoServiceImpl extends ServiceImpl<OrderInfoMapper, OrderInfo> implements OrderInfoService {
  4. @Resource
  5. private ProductMapper productMapper;
  6. @Override
  7. public OrderInfo createOrderByProductId(Long productId, String paymentType) {
  8. // 查找已存在但未支付的订单
  9. OrderInfo orderInfo = this.getNoPayOrderByProductId(productId, paymentType);
  10. if( orderInfo != null){
  11. return orderInfo;
  12. }
  13. // 获取商品信息
  14. Product product = productMapper.selectById(productId);
  15. // 生成订单
  16. orderInfo = new OrderInfo();
  17. orderInfo.setTitle(product.getTitle());
  18. orderInfo.setOrderNo(OrderNoUtils.getOrderNo()); //订单号
  19. orderInfo.setProductId(productId);
  20. orderInfo.setTotalFee(product.getPrice()); //分
  21. orderInfo.setOrderStatus(OrderStatus.NOTPAY.getType()); //未支付
  22. orderInfo.setPaymentType(paymentType);
  23. baseMapper.insert(orderInfo);
  24. return orderInfo;
  25. }
  26. }

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

  1. /**
  2. * 根据商品id查询未支付订单
  3. * 防止重复创建订单对象
  4. * @param productId
  5. * @return
  6. */
  7. private OrderInfo getNoPayOrderByProductId(Long productId, String paymentType) {
  8. QueryWrapper<OrderInfo> queryWrapper = new QueryWrapper<>();
  9. queryWrapper.eq("product_id", productId);
  10. queryWrapper.eq("order_status", OrderStatus.NOTPAY.getType());
  11. queryWrapper.eq("payment_type", paymentType);
  12. // queryWrapper.eq("user_id", userId);
  13. OrderInfo orderInfo = baseMapper.selectOne(queryWrapper);
  14. return orderInfo;
  15. }

(2)缓存二维码

  1. public interface OrderInfoService extends IService<OrderInfo> {
  2. void saveCodeUrl(String orderNo, String codeUrl);
  3. }
  1. @Service
  2. @Slf4j
  3. public class OrderInfoServiceImpl extends ServiceImpl<OrderInfoMapper, OrderInfo> implements OrderInfoService {
  4. @Resource
  5. private ProductMapper productMapper;
  6. /**
  7. * 存储订单二维码
  8. * @param orderNo
  9. * @param codeUrl
  10. */
  11. @Override
  12. public void saveCodeUrl(String orderNo, String codeUrl) {
  13. QueryWrapper<OrderInfo> queryWrapper = new QueryWrapper<>();
  14. queryWrapper.eq("order_no", orderNo);
  15. OrderInfo orderInfo = new OrderInfo();
  16. orderInfo.setCodeUrl(codeUrl);
  17. baseMapper.update(orderInfo, queryWrapper);
  18. }
  19. }

(3)修改WxPayServiceImpl 的nativePay方法

  1. @Service
  2. @Slf4j
  3. public class WxPayServiceImpl implements WxPayService {
  4. @Resource
  5. private WxPayConfig wxPayConfig;
  6. @Resource
  7. private CloseableHttpClient wxPayClient;
  8. @Resource
  9. private OrderInfoService orderInfoService;
  10. @Resource
  11. private PaymentInfoService paymentInfoService;
  12. @Resource
  13. private RefundInfoService refundsInfoService;
  14. @Resource
  15. private CloseableHttpClient wxPayNoSignClient; //无需应答签名
  16. private final ReentrantLock lock = new ReentrantLock();
  17. /**
  18. * 创建订单,调用Native支付接口
  19. * @param productId
  20. * @return code_url 和 订单号
  21. * @throws Exception
  22. */
  23. @Transactional(rollbackFor = Exception.class)
  24. @Override
  25. public Map<String, Object> nativePay(Long productId) throws Exception {
  26. log.info("生成订单");
  27. //生成订单
  28. OrderInfo orderInfo = orderInfoService.createOrderByProductId(productId, PayType.WXPAY.getType());
  29. String codeUrl = orderInfo.getCodeUrl();
  30. if(orderInfo != null && !StringUtils.isEmpty(codeUrl)){
  31. log.info("订单已存在,二维码已保存");
  32. //返回二维码
  33. Map<String, Object> map = new HashMap<>();
  34. map.put("codeUrl", codeUrl);
  35. map.put("orderNo", orderInfo.getOrderNo());
  36. return map;
  37. }
  38. log.info("调用统一下单API");
  39. //调用统一下单API
  40. HttpPost httpPost = new HttpPost(wxPayConfig.getDomain().concat(WxApiType.NATIVE_PAY.getType()));
  41. // 请求body参数
  42. Gson gson = new Gson();
  43. Map paramsMap = new HashMap();
  44. paramsMap.put("appid", wxPayConfig.getAppid());
  45. paramsMap.put("mchid", wxPayConfig.getMchId());
  46. paramsMap.put("description", orderInfo.getTitle());
  47. paramsMap.put("out_trade_no", orderInfo.getOrderNo());
  48. paramsMap.put("notify_url", wxPayConfig.getNotifyDomain().concat(WxNotifyType.NATIVE_NOTIFY.getType()));
  49. Map amountMap = new HashMap();
  50. amountMap.put("total", orderInfo.getTotalFee());
  51. amountMap.put("currency", "CNY");
  52. paramsMap.put("amount", amountMap);
  53. //将参数转换成json字符串
  54. String jsonParams = gson.toJson(paramsMap);
  55. log.info("请求参数 ===> {}" + jsonParams);
  56. StringEntity entity = new StringEntity(jsonParams,"utf-8");
  57. entity.setContentType("application/json");
  58. httpPost.setEntity(entity);
  59. httpPost.setHeader("Accept", "application/json");
  60. //完成签名并执行请求
  61. CloseableHttpResponse response = wxPayClient.execute(httpPost);
  62. try {
  63. String bodyAsString = EntityUtils.toString(response.getEntity());//响应体
  64. int statusCode = response.getStatusLine().getStatusCode();//响应状态码
  65. if (statusCode == 200) { //处理成功
  66. log.info("成功, 返回结果 = " + bodyAsString);
  67. } else if (statusCode == 204) { //处理成功,无返回Body
  68. log.info("成功");
  69. } else {
  70. log.info("Native下单失败,响应码 = " + statusCode+ ",返回结果 = " + bodyAsString);
  71. throw new IOException("request failed");
  72. }
  73. //响应结果
  74. Map<String, String> resultMap = gson.fromJson(bodyAsString, HashMap.class);
  75. //二维码
  76. codeUrl = resultMap.get("code_url");
  77. //保存二维码
  78. String orderNo = orderInfo.getOrderNo();
  79. orderInfoService.saveCodeUrl(orderNo, codeUrl);
  80. //返回二维码
  81. Map<String, Object> map = new HashMap<>();
  82. map.put("codeUrl", codeUrl);
  83. map.put("orderNo", orderInfo.getOrderNo());
  84. return map;
  85. } finally {
  86. response.close();
  87. }
  88. }
  89. }

5.5 显示订单列表
在我的订单页面按时间倒序显示订单列表
(1)创建OrderInfoController

  1. @CrossOrigin //开放前端的跨域访问
  2. @Api(tags = "商品订单管理")
  3. @RestController
  4. @RequestMapping("/api/order-info")
  5. public class OrderInfoController {
  6. @Resource
  7. private OrderInfoService orderInfoService;
  8. @ApiOperation("订单列表")
  9. @GetMapping("/list")
  10. public R list(){
  11. List<OrderInfo> list = orderInfoService.listOrderByCreateTimeDesc();
  12. return R.ok().data("list", list);
  13. }
  14. }

(2)定义 OrderInfoService 方法

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

6、支付通知API

6.1 内网穿透
(1)访问ngrok官网
https://ngrok.com/
(2)注册账号、登录
(3)下载内网穿透工具
ngrok-stable-windows-amd64.zip
(4)设置你的authToken
为本地计算机做授权配置

  1. ngrok authtoken 6aYc6Kp7kpxVr8pY88LkG_6x9o18yMY8BASrXiDFMeS

(5)启动服务

  1. ngrok http 8090

(6)测试外网访问

  1. 你获得的外网地址/api/test

6.2 接收通知和返回应答
支付通知API:https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_5.shtml
(1)启动ngrok

  1. ngrok http 8090

(2)设置通知地址
wxpay.properties
注意:每次重新启动ngrok,都需要根据实际情况修改这个配置

  1. 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)

  1. /**
  2. * 支付通知
  3. * 微信支付通过支付通知接口将用户支付成功消息通知给商户
  4. */
  5. @ApiOperation("支付通知")
  6. @PostMapping("/native/notify")
  7. public String nativeNotify(HttpServletRequest request, HttpServletResponse response){
  8. Gson gson = new Gson();
  9. Map<String, String> map = new HashMap<>();//应答对象
  10. try {
  11. //处理通知参数
  12. String body = HttpUtils.readData(request);
  13. Map<String, Object> bodyMap = gson.fromJson(body, HashMap.class);
  14. String requestId = (String)bodyMap.get("id");
  15. log.info("支付通知的id ===> {}", requestId);
  16. //log.info("支付通知的完整数据 ===> {}", body);
  17. //int a = 9 / 0;
  18. //签名的验证
  19. WechatPay2ValidatorForRequest wechatPay2ValidatorForRequest
  20. = new WechatPay2ValidatorForRequest(verifier, requestId, body);
  21. if(!wechatPay2ValidatorForRequest.validate(request)){
  22. log.error("通知验签失败");
  23. //失败应答
  24. response.setStatus(500);
  25. map.put("code", "ERROR");
  26. map.put("message", "通知验签失败");
  27. return gson.toJson(map);
  28. }
  29. log.info("通知验签成功");
  30. //处理订单
  31. wxPayService.processOrder(bodyMap);
  32. //应答超时
  33. //模拟接收微信端的重复通知
  34. TimeUnit.SECONDS.sleep(5);
  35. //成功应答
  36. response.setStatus(200);
  37. map.put("code", "SUCCESS");
  38. map.put("message", "成功");
  39. return gson.toJson(map);
  40. } catch (Exception e) {
  41. e.printStackTrace();
  42. //失败应答
  43. response.setStatus(500);
  44. map.put("code", "ERROR");
  45. map.put("message", "失败");
  46. return gson.toJson(map);
  47. }
  48. }

(4)测试超时应答
回调通知注意事项: https://pay.weixin.qq.com/wiki/doc/apiv3/Practices/chapter1_1_5.shtml
商户系统收到支付结果通知,需要在 5秒内返回应答报文,否则微信支付认为通知失败,后续会重复发送通知。

  1. // 测试超时应答:添加睡眠时间使应答超时
  2. TimeUnit.SECONDS.sleep(5);

6.3 验签
(1)工具类
参考SDK源码中的WechatPay2Validator 创建通知验签工具类WechatPay2ValidatorForRequest
(2)验签
image.png
6.4 解密
image.png
image.png
image.png
(1)WxPayController
nativeNotify方法中添加处理订单的代码

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

(2)WxPayService

  1. @Transactional(rollbackFor = Exception.class)
  2. @Override
  3. public void processOrder(Map<String, Object> bodyMap) throws GeneralSecurityException {
  4. log.info("处理订单");
  5. //解密报文
  6. String plainText = decryptFromResource(bodyMap);
  7. //将明文转换成map
  8. Gson gson = new Gson();
  9. HashMap plainTextMap = gson.fromJson(plainText, HashMap.class);
  10. String orderNo = (String)plainTextMap.get("out_trade_no");
  11. /*在对业务数据进行状态检查和处理之前,
  12. 要采用数据锁进行并发控制,
  13. 以避免函数重入造成的数据混乱*/
  14. //尝试获取锁:
  15. // 成功获取则立即返回true,获取失败则立即返回false。不必一直等待锁的释放
  16. if(lock.tryLock()){
  17. try {
  18. //处理重复的通知
  19. //接口调用的幂等性:无论接口被调用多少次,产生的结果是一致的。
  20. String orderStatus = orderInfoService.getOrderStatus(orderNo);
  21. if(!OrderStatus.NOTPAY.getType().equals(orderStatus)){
  22. return;
  23. }
  24. //模拟通知并发
  25. try {
  26. TimeUnit.SECONDS.sleep(5);
  27. } catch (InterruptedException e) {
  28. e.printStackTrace();
  29. }
  30. //更新订单状态
  31. orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.SUCCESS);
  32. //记录支付日志
  33. paymentInfoService.createPaymentInfo(plainText);
  34. } finally {
  35. //要主动释放锁
  36. lock.unlock();
  37. }
  38. }
  39. }

辅助方法:

  1. /**
  2. * 对称解密
  3. * @param bodyMap
  4. * @return
  5. */
  6. private String decryptFromResource(Map<String, Object> bodyMap) throws GeneralSecurityException {
  7. log.info("密文解密");
  8. //通知数据
  9. Map<String, String> resourceMap = (Map) bodyMap.get("resource");
  10. //数据密文
  11. String ciphertext = resourceMap.get("ciphertext");
  12. //随机串
  13. String nonce = resourceMap.get("nonce");
  14. //附加数据
  15. String associatedData = resourceMap.get("associated_data");
  16. log.info("密文 ===> {}", ciphertext);
  17. AesUtil aesUtil = new AesUtil(wxPayConfig.getApiV3Key().getBytes(StandardCharsets.UTF_8));
  18. String plainText = aesUtil.decryptToString(associatedData.getBytes(StandardCharsets.UTF_8),
  19. nonce.getBytes(StandardCharsets.UTF_8),
  20. ciphertext);
  21. log.info("明文 ===> {}", plainText);
  22. return plainText;
  23. }

6.5 处理订单
(1)完善processOrder方法
(2)更新订单状态

  1. /**
  2. * 根据订单号更新订单状态
  3. * @param orderNo
  4. * @param orderStatus
  5. */
  6. @Override
  7. public void updateStatusByOrderNo(String orderNo, OrderStatus orderStatus) {
  8. log.info("更新订单状态 ===> {}", orderStatus.getType());
  9. QueryWrapper<OrderInfo> queryWrapper = new QueryWrapper<>();
  10. queryWrapper.eq("order_no", orderNo);
  11. OrderInfo orderInfo = new OrderInfo();
  12. orderInfo.setOrderStatus(orderStatus.getType());
  13. baseMapper.update(orderInfo, queryWrapper);
  14. }

(3)处理支付日志

  1. /**
  2. * 记录支付日志:微信支付
  3. * @param plainText
  4. */
  5. @Override
  6. public void createPaymentInfo(String plainText) {
  7. log.info("记录支付日志");
  8. Gson gson = new Gson();
  9. HashMap plainTextMap = gson.fromJson(plainText, HashMap.class);
  10. //订单号
  11. String orderNo = (String)plainTextMap.get("out_trade_no");
  12. //业务编号
  13. String transactionId = (String)plainTextMap.get("transaction_id");
  14. //支付类型
  15. String tradeType = (String)plainTextMap.get("trade_type");
  16. //交易状态
  17. String tradeState = (String)plainTextMap.get("trade_state");
  18. //用户实际支付金额
  19. Map<String, Object> amount = (Map)plainTextMap.get("amount");
  20. Integer payerTotal = ((Double) amount.get("payer_total")).intValue();
  21. PaymentInfo paymentInfo = new PaymentInfo();
  22. paymentInfo.setOrderNo(orderNo);
  23. paymentInfo.setPaymentType(PayType.WXPAY.getType());
  24. paymentInfo.setTransactionId(transactionId);
  25. paymentInfo.setTradeType(tradeType);
  26. paymentInfo.setTradeState(tradeState);
  27. paymentInfo.setPayerTotal(payerTotal);
  28. paymentInfo.setContent(plainText);
  29. baseMapper.insert(paymentInfo);
  30. }

6.6 处理重复通知
image.png
(1)测试重复的通知

  1. // 应答超时
  2. // 设置响应超时,可以接收到微信支付的重复的支付结果通知。
  3. // 通知重复,数据库会记录多余的支付日志
  4. TimeUnit.SECONDS.sleep(5);

(2)处理重复通知
在 processOrder 方法中,更新订单状态之前,添加如下代码

  1. // 处理重复通知
  2. // 保证接口调用的幂等性:无论接口被调用多少次,产生的结果是一致的
  3. String orderStatus = orderInfoService.getOrderStatus(orderNo);
  4. if (!OrderStatus.NOTPAY.getType().equals(orderStatus)) {
  5. return;
  6. }
  1. /**
  2. * 根据订单号获取订单状态
  3. * @param orderNo
  4. * @return
  5. */
  6. @Override
  7. public String getOrderStatus(String orderNo) {
  8. QueryWrapper<OrderInfo> queryWrapper = new QueryWrapper<>();
  9. queryWrapper.eq("order_no", orderNo);
  10. OrderInfo orderInfo = baseMapper.selectOne(queryWrapper);
  11. if(orderInfo == null){
  12. return null;
  13. }
  14. return orderInfo.getOrderStatus();
  15. }

6.7 数据锁
image.png
(1)测试通知并发

  1. // 处理重复的通知
  2. // 模拟通知并发
  3. try {
  4. TimeUnit.SECONDS.sleep(5);
  5. } catch (InterruptedException e) {
  6. e.printStackTrace();
  7. }
  8. // 更新订单状态
  9. // 记录支付日志

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

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

7、商户定时查询本地订单

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

  1. /**
  2. * 查询本地订单状态
  3. * @param orderNo
  4. * @return
  5. */
  6. @ApiOperation("查询本地订单状态")
  7. @GetMapping("/query-order-status/{orderNo}")
  8. public R queryOrderStatus(@PathVariable String orderNo){
  9. String orderStatus = orderInfoService.getOrderStatus(orderNo);
  10. if(OrderStatus.SUCCESS.getType().equals(orderStatus)){
  11. return R.ok().setMessage("支付成功"); //支付成功
  12. }
  13. return R.ok().setCode(101).setMessage("支付中......");
  14. }

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

  1. //启动定时器
  2. this.timer = setInterval(() => {
  3. //查询订单是否支付成功
  4. this.queryOrderStatus()
  5. }, 3000)

image.png
(2)查询订单

  1. // 查询订单状态
  2. queryOrderStatus() {
  3. orderInfoApi.queryOrderStatus(this.orderNo).then((response) => {
  4. console.log('查询订单状态:' + response.code)
  5. // 支付成功后的页面跳转
  6. if (response.code === 0) {
  7. console.log('清除定时器')
  8. clearInterval(this.timer)
  9. // 三秒后跳转到支付成功页面
  10. setTimeout(() => {
  11. this.$router.push({ path: '/success' })
  12. }, 3000)
  13. }
  14. })
  15. }

8、用户取消订单API

实现用户主动取消订单的功能
8.1 定义取消订单接口

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

8.2 具体实现取消订单

  1. /**
  2. * 用户取消订单
  3. * @param orderNo
  4. */
  5. @Override
  6. public void cancelOrder(String orderNo) throws Exception {
  7. //调用微信支付的关单接口
  8. this.closeOrder(orderNo);
  9. //更新商户端的订单状态
  10. orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.CANCEL);
  11. }
  1. private void closeOrder(String orderNo) throws Exception {
  2. log.info("关单接口的调用,订单号 ===> {}", orderNo);
  3. //创建远程请求对象
  4. String url = String.format(WxApiType.CLOSE_ORDER_BY_NO.getType(), orderNo);
  5. url = wxPayConfig.getDomain().concat(url);
  6. HttpPost httpPost = new HttpPost(url);
  7. //组装json请求体
  8. Gson gson = new Gson();
  9. Map<String, String> paramsMap = new HashMap<>();
  10. paramsMap.put("mchid", wxPayConfig.getMchId());
  11. String jsonParams = gson.toJson(paramsMap);
  12. log.info("请求参数 ===> {}", jsonParams);
  13. //将请求参数设置到请求对象中
  14. StringEntity entity = new StringEntity(jsonParams,"utf-8");
  15. entity.setContentType("application/json");
  16. httpPost.setEntity(entity);
  17. httpPost.setHeader("Accept", "application/json");
  18. //完成签名并执行请求
  19. CloseableHttpResponse response = wxPayClient.execute(httpPost);
  20. try {
  21. int statusCode = response.getStatusLine().getStatusCode();//响应状态码
  22. if (statusCode == 200) { //处理成功
  23. log.info("成功200");
  24. } else if (statusCode == 204) { //处理成功,无返回Body
  25. log.info("成功204");
  26. } else {
  27. log.info("Native下单失败,响应码 = " + statusCode);
  28. throw new IOException("request failed");
  29. }
  30. } finally {
  31. response.close();
  32. }
  33. }

9、微信支付查单API

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

  1. /**
  2. * 查询订单
  3. * @param orderNo
  4. * @return
  5. * @throws Exception
  6. */
  7. @ApiOperation("查询订单:测试订单状态用")
  8. @GetMapping("/query/{orderNo}")
  9. public R queryOrder(@PathVariable String orderNo) throws Exception {
  10. log.info("查询订单");
  11. String result = wxPayService.queryOrder(orderNo);
  12. return R.ok().setMessage("查询成功").data("result", result);
  13. }

(2)WxPayServiceImpl

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

9.2 集成Spring Task
Spring 3.0后提供Spring Task实现任务调度
(1)启动类添加注解

  1. @EnableScheduling

(2)测试定时任务
创建task 包,创建WxPayTask.java

  1. @Slf4j
  2. @Component
  3. public class WxPayTask {
  4. /**
  5. * 秒 分 时 日 月 周
  6. * 以秒为例
  7. * *:每秒都执行
  8. * 1-3:从第1秒开始执行,到第3秒结束执行
  9. * 0/3:从第0秒开始,每隔3秒执行1次
  10. * 1,2,3:在指定的第1、2、3秒执行
  11. * ?:不指定
  12. * 日和周不能同时制定,指定其中之一,则另一个设置为?
  13. */
  14. @Scheduled(cron = "0/3 * * * * ?")
  15. public void task1(){
  16. log.info("task1 被执行......");
  17. }
  18. }

9.3 定时查找超时订单
(1)WxPayTask

  1. @Slf4j
  2. @Component
  3. public class WxPayTask {
  4. @Resource
  5. private OrderInfoService orderInfoService;
  6. @Resource
  7. private WxPayService wxPayService;
  8. /**
  9. * 从第0秒开始每隔30秒执行1次,查询创建超过5分钟,并且未支付的订单
  10. */
  11. @Scheduled(cron = "0/30 * * * * ?")
  12. public void orderConfirm() throws Exception {
  13. log.info("orderConfirm 被执行......");
  14. List<OrderInfo> orderInfoList = orderInfoService.getNoPayOrderByDuration(1, PayType.WXPAY.getType());
  15. for (OrderInfo orderInfo : orderInfoList) {
  16. String orderNo = orderInfo.getOrderNo();
  17. log.warn("超时订单 ===> {}", orderNo);
  18. //核实订单状态:调用微信支付查单接口
  19. wxPayService.checkOrderStatus(orderNo);
  20. }
  21. }
  22. }

(2)OrderInfoServiceImpl

  1. /**
  2. * 查询创建超过minutes分钟并且未支付的订单
  3. * @param minutes
  4. * @return
  5. */
  6. @Override
  7. public List<OrderInfo> getNoPayOrderByDuration(int minutes, String paymentType) {
  8. Instant instant = Instant.now().minus(Duration.ofMinutes(minutes));
  9. QueryWrapper<OrderInfo> queryWrapper = new QueryWrapper<>();
  10. queryWrapper.eq("order_status", OrderStatus.NOTPAY.getType());
  11. queryWrapper.le("create_time", instant);
  12. queryWrapper.eq("payment_type", paymentType);
  13. List<OrderInfo> orderInfoList = baseMapper.selectList(queryWrapper);
  14. return orderInfoList;
  15. }

9.4 处理超时订单

  1. /**
  2. * 根据订单号查询微信支付查单接口,核实订单状态
  3. * 如果订单已支付,则更新商户端订单状态,并记录支付日志
  4. * 如果订单未支付,则调用关单接口关闭订单,并更新商户端订单状态
  5. * @param orderNo
  6. */
  7. @Transactional(rollbackFor = Exception.class)
  8. @Override
  9. public void checkOrderStatus(String orderNo) throws Exception {
  10. log.warn("根据订单号核实订单状态 ===> {}", orderNo);
  11. //调用微信支付查单接口
  12. String result = this.queryOrder(orderNo);
  13. Gson gson = new Gson();
  14. Map<String, String> resultMap = gson.fromJson(result, HashMap.class);
  15. //获取微信支付端的订单状态
  16. String tradeState = resultMap.get("trade_state");
  17. //判断订单状态
  18. if(WxTradeState.SUCCESS.getType().equals(tradeState)){
  19. log.warn("核实订单已支付 ===> {}", orderNo);
  20. //如果确认订单已支付则更新本地订单状态
  21. orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.SUCCESS);
  22. //记录支付日志
  23. paymentInfoService.createPaymentInfo(result);
  24. }
  25. if(WxTradeState.NOTPAY.getType().equals(tradeState)){
  26. log.warn("核实订单未支付 ===> {}", orderNo);
  27. //如果订单未支付,则调用关单接口
  28. this.closeOrder(orderNo);
  29. //更新本地订单状态
  30. orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.CLOSED);
  31. }
  32. }

11、申请退款API

文档: https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_9.shtml
11.1 创建退款单
(1)根据订单号查询订单

  1. /**
  2. * 根据订单号获取订单
  3. * @param orderNo
  4. * @return
  5. */
  6. @Override
  7. public OrderInfo getOrderByOrderNo(String orderNo) {
  8. QueryWrapper<OrderInfo> queryWrapper = new QueryWrapper<>();
  9. queryWrapper.eq("order_no", orderNo);
  10. OrderInfo orderInfo = baseMapper.selectOne(queryWrapper);
  11. return orderInfo;
  12. }

(2)创建退款单记录

  1. /**
  2. * 根据订单号创建退款订单
  3. * @param orderNo
  4. * @return
  5. */
  6. @Override
  7. public RefundInfo createRefundByOrderNo(String orderNo, String reason) {
  8. //根据订单号获取订单信息
  9. OrderInfo orderInfo = orderInfoService.getOrderByOrderNo(orderNo);
  10. //根据订单号生成退款订单
  11. RefundInfo refundInfo = new RefundInfo();
  12. refundInfo.setOrderNo(orderNo);//订单编号
  13. refundInfo.setRefundNo(OrderNoUtils.getRefundNo());//退款单编号
  14. refundInfo.setTotalFee(orderInfo.getTotalFee());//原订单金额(分)
  15. refundInfo.setRefund(orderInfo.getTotalFee());//退款金额(分)
  16. refundInfo.setReason(reason);//退款原因
  17. //保存退款订单
  18. baseMapper.insert(refundInfo);
  19. return refundInfo;
  20. }

11.2 更新退款单

  1. /**
  2. * 记录退款记录
  3. * @param content
  4. */
  5. @Override
  6. public void updateRefund(String content) {
  7. //将json字符串转换成Map
  8. Gson gson = new Gson();
  9. Map<String, String> resultMap = gson.fromJson(content, HashMap.class);
  10. //根据退款单编号修改退款单
  11. QueryWrapper<RefundInfo> queryWrapper = new QueryWrapper<>();
  12. queryWrapper.eq("refund_no", resultMap.get("out_refund_no"));
  13. //设置要修改的字段
  14. RefundInfo refundInfo = new RefundInfo();
  15. refundInfo.setRefundId(resultMap.get("refund_id"));//微信支付退款单号
  16. //查询退款和申请退款中的返回参数
  17. if(resultMap.get("status") != null){
  18. refundInfo.setRefundStatus(resultMap.get("status"));//退款状态
  19. refundInfo.setContentReturn(content);//将全部响应结果存入数据库的content字段
  20. }
  21. //退款回调中的回调参数
  22. if(resultMap.get("refund_status") != null){
  23. refundInfo.setRefundStatus(resultMap.get("refund_status"));//退款状态
  24. refundInfo.setContentNotify(content);//将全部响应结果存入数据库的content字段
  25. }
  26. //更新退款单
  27. baseMapper.update(refundInfo, queryWrapper);
  28. }

11.3 申请退款
(1) WxPayController

  1. @ApiOperation("申请退款")
  2. @PostMapping("/refunds/{orderNo}/{reason}")
  3. public R refunds(@PathVariable String orderNo, @PathVariable String reason) throws Exception {
  4. log.info("申请退款");
  5. wxPayService.refund(orderNo, reason);
  6. return R.ok();
  7. }

(2)WxPayServiceImpl

  1. /**
  2. * 退款
  3. * @param orderNo
  4. * @param reason
  5. * @throws IOException
  6. */
  7. @Transactional(rollbackFor = Exception.class)
  8. @Override
  9. public void refund(String orderNo, String reason) throws Exception {
  10. log.info("创建退款单记录");
  11. //根据订单编号创建退款单
  12. RefundInfo refundsInfo = refundsInfoService.createRefundByOrderNo(orderNo, reason);
  13. log.info("调用退款API");
  14. //调用统一下单API
  15. String url = wxPayConfig.getDomain().concat(WxApiType.DOMESTIC_REFUNDS.getType());
  16. HttpPost httpPost = new HttpPost(url);
  17. // 请求body参数
  18. Gson gson = new Gson();
  19. Map paramsMap = new HashMap();
  20. paramsMap.put("out_trade_no", orderNo);//订单编号
  21. paramsMap.put("out_refund_no", refundsInfo.getRefundNo());//退款单编号
  22. paramsMap.put("reason",reason);//退款原因
  23. paramsMap.put("notify_url", wxPayConfig.getNotifyDomain().concat(WxNotifyType.REFUND_NOTIFY.getType()));//退款通知地址
  24. Map amountMap = new HashMap();
  25. amountMap.put("refund", refundsInfo.getRefund());//退款金额
  26. amountMap.put("total", refundsInfo.getTotalFee());//原订单金额
  27. amountMap.put("currency", "CNY");//退款币种
  28. paramsMap.put("amount", amountMap);
  29. //将参数转换成json字符串
  30. String jsonParams = gson.toJson(paramsMap);
  31. log.info("请求参数 ===> {}" + jsonParams);
  32. StringEntity entity = new StringEntity(jsonParams,"utf-8");
  33. entity.setContentType("application/json");//设置请求报文格式
  34. httpPost.setEntity(entity);//将请求报文放入请求对象
  35. httpPost.setHeader("Accept", "application/json");//设置响应报文格式
  36. //完成签名并执行请求,并完成验签
  37. CloseableHttpResponse response = wxPayClient.execute(httpPost);
  38. try {
  39. //解析响应结果
  40. String bodyAsString = EntityUtils.toString(response.getEntity());
  41. int statusCode = response.getStatusLine().getStatusCode();
  42. if (statusCode == 200) {
  43. log.info("成功, 退款返回结果 = " + bodyAsString);
  44. } else if (statusCode == 204) {
  45. log.info("成功");
  46. } else {
  47. throw new RuntimeException("退款异常, 响应码 = " + statusCode+ ", 退款返回结果 = " + bodyAsString);
  48. }
  49. //更新订单状态
  50. orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.REFUND_PROCESSING);
  51. //更新退款单
  52. refundsInfoService.updateRefund(bodyAsString);
  53. } finally {
  54. response.close();
  55. }
  56. }

12、查询退款API

文档: https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_10.shtml
12.1 查单接口的调用
(1) WxPayController

  1. /**
  2. * 查询退款
  3. * @param refundNo
  4. * @return
  5. * @throws Exception
  6. */
  7. @ApiOperation("查询退款:测试用")
  8. @GetMapping("/query-refund/{refundNo}")
  9. public R queryRefund(@PathVariable String refundNo) throws Exception {
  10. log.info("查询退款");
  11. String result = wxPayService.queryRefund(refundNo);
  12. return R.ok().setMessage("查询成功").data("result", result);
  13. }

(2) WxPayServiceImpl

  1. /**
  2. * 查询退款接口调用
  3. * @param refundNo
  4. * @return
  5. */
  6. @Override
  7. public String queryRefund(String refundNo) throws Exception {
  8. log.info("查询退款接口调用 ===> {}", refundNo);
  9. String url = String.format(WxApiType.DOMESTIC_REFUNDS_QUERY.getType(), refundNo);
  10. url = wxPayConfig.getDomain().concat(url);
  11. //创建远程Get 请求对象
  12. HttpGet httpGet = new HttpGet(url);
  13. httpGet.setHeader("Accept", "application/json");
  14. //完成签名并执行请求
  15. CloseableHttpResponse response = wxPayClient.execute(httpGet);
  16. try {
  17. String bodyAsString = EntityUtils.toString(response.getEntity());
  18. int statusCode = response.getStatusLine().getStatusCode();
  19. if (statusCode == 200) {
  20. log.info("成功, 查询退款返回结果 = " + bodyAsString);
  21. } else if (statusCode == 204) {
  22. log.info("成功");
  23. } else {
  24. throw new RuntimeException("查询退款异常, 响应码 = " + statusCode+ ", 查询退款返回结果 = " + bodyAsString);
  25. }
  26. return bodyAsString;
  27. } finally {
  28. response.close();
  29. }
  30. }

12.2 定时查找退款中的订单
(1) WxPayTask

  1. @Slf4j
  2. @Component
  3. public class WxPayTask {
  4. @Resource
  5. private OrderInfoService orderInfoService;
  6. @Resource
  7. private WxPayService wxPayService;
  8. @Resource
  9. private RefundInfoService refundInfoService;
  10. /**
  11. * 从第0秒开始每隔30秒执行1次,查询创建超过5分钟,并且未成功的退款单
  12. */
  13. @Scheduled(cron = "0/30 * * * * ?")
  14. public void refundConfirm() throws Exception {
  15. log.info("refundConfirm 被执行......");
  16. // 找出申请退款超过5分钟并且未成功的退款单
  17. List<RefundInfo> refundInfoList = refundInfoService.getNoRefundOrderByDuration(1);
  18. for (RefundInfo refundInfo : refundInfoList) {
  19. String refundNo = refundInfo.getRefundNo();
  20. log.warn("超时未退款的退款单号 ===> {}", refundNo);
  21. // 核实订单状态:调用微信支付查询退款接口
  22. wxPayService.checkRefundStatus(refundNo);
  23. }
  24. }
  25. }

(2) RefundInfoServiceImpl

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

12.3 处理超时未退款订单
核实订单状态:

  1. /**
  2. * 根据退款单号核实退款单状态
  3. * @param refundNo
  4. * @return
  5. */
  6. @Transactional(rollbackFor = Exception.class)
  7. @Override
  8. public void checkRefundStatus(String refundNo) throws Exception {
  9. log.warn("根据退款单号核实退款单状态 ===> {}", refundNo);
  10. //调用查询退款单接口
  11. String result = this.queryRefund(refundNo);
  12. //组装json请求体字符串
  13. Gson gson = new Gson();
  14. Map<String, String> resultMap = gson.fromJson(result, HashMap.class);
  15. //获取微信支付端退款状态
  16. String status = resultMap.get("status");
  17. String orderNo = resultMap.get("out_trade_no");
  18. if (WxRefundStatus.SUCCESS.getType().equals(status)) {
  19. log.warn("核实订单已退款成功 ===> {}", refundNo);
  20. //如果确认退款成功,则更新订单状态
  21. orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.REFUND_SUCCESS);
  22. //更新退款单
  23. refundsInfoService.updateRefund(result);
  24. }
  25. if (WxRefundStatus.ABNORMAL.getType().equals(status)) {
  26. log.warn("核实订单退款异常 ===> {}", refundNo);
  27. //如果确认退款成功,则更新订单状态
  28. orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.REFUND_ABNORMAL);
  29. //更新退款单
  30. refundsInfoService.updateRefund(result);
  31. }
  32. }

13、退款结果通知API

  1. 文档: [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 接收退款通知**
  1. /**
  2. * 退款结果通知
  3. * 退款状态改变后,微信会把相关退款结果发送给商户。
  4. */
  5. @ApiOperation("退款结果通知")
  6. @PostMapping("/refunds/notify")
  7. public String refundsNotify(HttpServletRequest request, HttpServletResponse response){
  8. log.info("退款通知执行");
  9. Gson gson = new Gson();
  10. Map<String, String> map = new HashMap<>();//应答对象
  11. try {
  12. //处理通知参数
  13. String body = HttpUtils.readData(request);
  14. Map<String, Object> bodyMap = gson.fromJson(body, HashMap.class);
  15. String requestId = (String)bodyMap.get("id");
  16. log.info("支付通知的id ===> {}", requestId);
  17. //签名的验证
  18. WechatPay2ValidatorForRequest wechatPay2ValidatorForRequest
  19. = new WechatPay2ValidatorForRequest(verifier, requestId, body);
  20. if(!wechatPay2ValidatorForRequest.validate(request)){
  21. log.error("通知验签失败");
  22. //失败应答
  23. response.setStatus(500);
  24. map.put("code", "ERROR");
  25. map.put("message", "通知验签失败");
  26. return gson.toJson(map);
  27. }
  28. log.info("通知验签成功");
  29. //处理退款单
  30. wxPayService.processRefund(bodyMap);
  31. //成功应答
  32. response.setStatus(200);
  33. map.put("code", "SUCCESS");
  34. map.put("message", "成功");
  35. return gson.toJson(map);
  36. } catch (Exception e) {
  37. e.printStackTrace();
  38. //失败应答
  39. response.setStatus(500);
  40. map.put("code", "ERROR");
  41. map.put("message", "失败");
  42. return gson.toJson(map);
  43. }
  44. }

13.2 处理订单和退款单

  1. /**
  2. * 处理退款单
  3. */
  4. @Transactional(rollbackFor = Exception.class)
  5. @Override
  6. public void processRefund(Map<String, Object> bodyMap) throws Exception {
  7. log.info("退款单");
  8. //解密报文
  9. String plainText = decryptFromResource(bodyMap);
  10. //将明文转换成map
  11. Gson gson = new Gson();
  12. HashMap plainTextMap = gson.fromJson(plainText, HashMap.class);
  13. String orderNo = (String)plainTextMap.get("out_trade_no");
  14. if(lock.tryLock()){
  15. try {
  16. String orderStatus = orderInfoService.getOrderStatus(orderNo);
  17. if (!OrderStatus.REFUND_PROCESSING.getType().equals(orderStatus)) {
  18. return;
  19. }
  20. //更新订单状态
  21. orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.REFUND_SUCCESS);
  22. //更新退款单
  23. refundsInfoService.updateRefund(plainText);
  24. } finally {
  25. //要主动释放锁
  26. lock.unlock();
  27. }
  28. }
  29. }

14、账单

14.1 申请交易账单和资金账单
(1) WxPayController

  1. @ApiOperation("获取账单url:测试用")
  2. @GetMapping("/querybill/{billDate}/{type}")
  3. public R queryTradeBill(
  4. @PathVariable String billDate,
  5. @PathVariable String type) throws Exception {
  6. log.info("获取账单url");
  7. String downloadUrl = wxPayService.queryBill(billDate, type);
  8. return R.ok().setMessage("获取账单url成功").data("downloadUrl", downloadUrl);
  9. }

(2) WxPayServiceImpl

  1. /**
  2. * 申请账单
  3. * @param billDate
  4. * @param type
  5. * @return
  6. * @throws Exception
  7. */
  8. @Override
  9. public String queryBill(String billDate, String type) throws Exception {
  10. log.warn("申请账单接口调用 {}", billDate);
  11. String url = "";
  12. if("tradebill".equals(type)){
  13. url = WxApiType.TRADE_BILLS.getType();
  14. }else if("fundflowbill".equals(type)){
  15. url = WxApiType.FUND_FLOW_BILLS.getType();
  16. }else{
  17. throw new RuntimeException("不支持的账单类型");
  18. }
  19. url = wxPayConfig.getDomain().concat(url).concat("?bill_date=").concat(billDate);
  20. //创建远程Get 请求对象
  21. HttpGet httpGet = new HttpGet(url);
  22. httpGet.addHeader("Accept", "application/json");
  23. //使用wxPayClient发送请求得到响应
  24. CloseableHttpResponse response = wxPayClient.execute(httpGet);
  25. try {
  26. String bodyAsString = EntityUtils.toString(response.getEntity());
  27. int statusCode = response.getStatusLine().getStatusCode();
  28. if (statusCode == 200) {
  29. log.info("成功, 申请账单返回结果 = " + bodyAsString);
  30. } else if (statusCode == 204) {
  31. log.info("成功");
  32. } else {
  33. throw new RuntimeException("申请账单异常, 响应码 = " + statusCode+ ", 申请账单返回结果 = " + bodyAsString);
  34. }
  35. //获取账单下载地址
  36. Gson gson = new Gson();
  37. Map<String, String> resultMap = gson.fromJson(bodyAsString, HashMap.class);
  38. return resultMap.get("download_url");
  39. } finally {
  40. response.close();
  41. }
  42. }

14.2 下载账单
(1) WxPayController

  1. @ApiOperation("下载账单")
  2. @GetMapping("/downloadbill/{billDate}/{type}")
  3. public R downloadBill(
  4. @PathVariable String billDate,
  5. @PathVariable String type) throws Exception {
  6. log.info("下载账单");
  7. String result = wxPayService.downloadBill(billDate, type);
  8. return R.ok().data("result", result);
  9. }

(2) WxPayServiceImpl

  1. /**
  2. * 下载账单
  3. * @param billDate
  4. * @param type
  5. * @return
  6. * @throws Exception
  7. */
  8. @Override
  9. public String downloadBill(String billDate, String type) throws Exception {
  10. log.warn("下载账单接口调用 {}, {}", billDate, type);
  11. //获取账单url地址
  12. String downloadUrl = this.queryBill(billDate, type);
  13. //创建远程Get 请求对象
  14. HttpGet httpGet = new HttpGet(downloadUrl);
  15. httpGet.addHeader("Accept", "application/json");
  16. //使用wxPayClient发送请求得到响应
  17. CloseableHttpResponse response = wxPayNoSignClient.execute(httpGet);
  18. try {
  19. String bodyAsString = EntityUtils.toString(response.getEntity());
  20. int statusCode = response.getStatusLine().getStatusCode();
  21. if (statusCode == 200) {
  22. log.info("成功, 下载账单返回结果 = " + bodyAsString);
  23. } else if (statusCode == 204) {
  24. log.info("成功");
  25. } else {
  26. throw new RuntimeException("下载账单异常, 响应码 = " + statusCode+ ", 下载账单返回结果 = " + bodyAsString);
  27. }
  28. return bodyAsString;
  29. } finally {
  30. response.close();
  31. }
  32. }

五、基础支付API V2

1、V2和V3的比较

image.png

2、引入依赖和工具

2.1 引入依赖

  1. <!--微信支付 APIv2 SDK-->
  2. <dependency>
  3. <groupId>com.github.wxpay</groupId>
  4. <artifactId>wxpay-sdk</artifactId>
  5. <version>0.0.3</version>
  6. </dependency>

2.2 复制工具类

  1. public class HttpClientUtils {
  2. private String url;
  3. private Map<String, String> param;
  4. private int statusCode;
  5. private String content;
  6. private String xmlParam;
  7. private boolean isHttps;
  8. public boolean isHttps() {
  9. return isHttps;
  10. }
  11. public void setHttps(boolean isHttps) {
  12. this.isHttps = isHttps;
  13. }
  14. public String getXmlParam() {
  15. return xmlParam;
  16. }
  17. public void setXmlParam(String xmlParam) {
  18. this.xmlParam = xmlParam;
  19. }
  20. public HttpClientUtils(String url, Map<String, String> param) {
  21. this.url = url;
  22. this.param = param;
  23. }
  24. public HttpClientUtils(String url) {
  25. this.url = url;
  26. }
  27. public void setParameter(Map<String, String> map) {
  28. param = map;
  29. }
  30. public void addParameter(String key, String value) {
  31. if (param == null)
  32. param = new HashMap<String, String>();
  33. param.put(key, value);
  34. }
  35. public void post() throws ClientProtocolException, IOException {
  36. HttpPost http = new HttpPost(url);
  37. setEntity(http);
  38. execute(http);
  39. }
  40. public void put() throws ClientProtocolException, IOException {
  41. HttpPut http = new HttpPut(url);
  42. setEntity(http);
  43. execute(http);
  44. }
  45. public void get() throws ClientProtocolException, IOException {
  46. if (param != null) {
  47. StringBuilder url = new StringBuilder(this.url);
  48. boolean isFirst = true;
  49. for (String key : param.keySet()) {
  50. if (isFirst) {
  51. url.append("?");
  52. isFirst = false;
  53. }else {
  54. url.append("&");
  55. }
  56. url.append(key).append("=").append(param.get(key));
  57. }
  58. this.url = url.toString();
  59. }
  60. HttpGet http = new HttpGet(url);
  61. execute(http);
  62. }
  63. /**
  64. * set http post,put param
  65. */
  66. private void setEntity(HttpEntityEnclosingRequestBase http) {
  67. if (param != null) {
  68. List<NameValuePair> nvps = new LinkedList<NameValuePair>();
  69. for (String key : param.keySet())
  70. nvps.add(new BasicNameValuePair(key, param.get(key))); // 参数
  71. http.setEntity(new UrlEncodedFormEntity(nvps, Consts.UTF_8)); // 设置参数
  72. }
  73. if (xmlParam != null) {
  74. http.setEntity(new StringEntity(xmlParam, Consts.UTF_8));
  75. }
  76. }
  77. private void execute(HttpUriRequest http) throws ClientProtocolException,
  78. IOException {
  79. CloseableHttpClient httpClient = null;
  80. try {
  81. if (isHttps) {
  82. SSLContext sslContext = new SSLContextBuilder()
  83. .loadTrustMaterial(null, new TrustStrategy() {
  84. // 信任所有
  85. public boolean isTrusted(X509Certificate[] chain,
  86. String authType)
  87. throws CertificateException {
  88. return true;
  89. }
  90. }).build();
  91. SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(
  92. sslContext);
  93. httpClient = HttpClients.custom().setSSLSocketFactory(sslsf)
  94. .build();
  95. } else {
  96. httpClient = HttpClients.createDefault();
  97. }
  98. CloseableHttpResponse response = httpClient.execute(http);
  99. try {
  100. if (response != null) {
  101. if (response.getStatusLine() != null)
  102. statusCode = response.getStatusLine().getStatusCode();
  103. HttpEntity entity = response.getEntity();
  104. // 响应内容
  105. content = EntityUtils.toString(entity, Consts.UTF_8);
  106. }
  107. } finally {
  108. response.close();
  109. }
  110. } catch (Exception e) {
  111. e.printStackTrace();
  112. } finally {
  113. httpClient.close();
  114. }
  115. }
  116. public int getStatusCode() {
  117. return statusCode;
  118. }
  119. public String getContent() throws ParseException, IOException {
  120. return content;
  121. }
  122. }

2.3 添加商户APIv2 key

  1. # APIv2密钥
  2. wxpay.partnerKey: T6m9iK73b0kn9g5v426MKfHQH7X8rKwb
  1. private String partnerKey;

2.4 添加枚举

  1. /**
  2. * Native下单V2
  3. */
  4. NATIVE_PAY_V2("/pay/unifiedorder"),
  1. /**
  2. * 支付通知V2
  3. */
  4. NATIVE_NOTIFY_V2("/api/wx-pay-v2/native/notify"),

3、统一下单

3.1 创建WxPayV2Controller

  1. @CrossOrigin //跨域
  2. @RestController
  3. @RequestMapping("/api/wx-pay-v2")
  4. @Api(tags = "网站微信支付APIv2")
  5. @Slf4j
  6. public class WxPayV2Controller {
  7. @Resource
  8. private WxPayService wxPayService;
  9. /**
  10. * Native下单
  11. * @param productId
  12. * @return
  13. * @throws Exception
  14. */
  15. @ApiOperation("调用统一下单API,生成支付二维码")
  16. @PostMapping("/native/{productId}")
  17. public R createNative(@PathVariable Long productId, HttpServletRequest request) throws Exception {
  18. log.info("发起支付请求 v2");
  19. String remoteAddr = request.getRemoteAddr();
  20. Map<String, Object> map = wxPayService.nativePayV2(productId, remoteAddr);
  21. return R.ok().setData(map);
  22. }
  23. }

3.2 WxPayServiceImpl

  1. @Override
  2. public Map<String, Object> nativePayV2(Long productId, String remoteAddr) throws Exception {
  3. log.info("生成订单");
  4. //生成订单
  5. OrderInfo orderInfo = orderInfoService.createOrderByProductId(productId, PayType.WXPAY.getType());
  6. String codeUrl = orderInfo.getCodeUrl();
  7. if(orderInfo != null && !StringUtils.isEmpty(codeUrl)){
  8. log.info("订单已存在,二维码已保存");
  9. //返回二维码
  10. Map<String, Object> map = new HashMap<>();
  11. map.put("codeUrl", codeUrl);
  12. map.put("orderNo", orderInfo.getOrderNo());
  13. return map;
  14. }
  15. log.info("调用统一下单API");
  16. HttpClientUtils client = new HttpClientUtils(wxPayConfig.getDomain().concat(WxApiType.NATIVE_PAY_V2.getType()));
  17. //组装接口参数
  18. Map<String, String> params = new HashMap<>();
  19. params.put("appid", wxPayConfig.getAppid());//关联的公众号的appid
  20. params.put("mch_id", wxPayConfig.getMchId());//商户号
  21. params.put("nonce_str", WXPayUtil.generateNonceStr());//生成随机字符串
  22. params.put("body", orderInfo.getTitle());
  23. params.put("out_trade_no", orderInfo.getOrderNo());
  24. //注意,这里必须使用字符串类型的参数(总金额:分)
  25. String totalFee = orderInfo.getTotalFee() + "";
  26. params.put("total_fee", totalFee);
  27. params.put("spbill_create_ip", remoteAddr);
  28. params.put("notify_url", wxPayConfig.getNotifyDomain().concat(WxNotifyType.NATIVE_NOTIFY_V2.getType()));
  29. params.put("trade_type", "NATIVE");
  30. //将参数转换成xml字符串格式:生成带有签名的xml格式字符串
  31. String xmlParams = WXPayUtil.generateSignedXml(params, wxPayConfig.getPartnerKey());
  32. log.info("\n xmlParams:\n" + xmlParams);
  33. client.setXmlParam(xmlParams);//将参数放入请求对象的方法体
  34. client.setHttps(true);//使用https形式发送
  35. client.post();//发送请求
  36. String resultXml = client.getContent();//得到响应结果
  37. log.info("\n resultXml:\n" + resultXml);
  38. //将xml响应结果转成map对象
  39. Map<String, String> resultMap = WXPayUtil.xmlToMap(resultXml);
  40. //错误处理
  41. if("FAIL".equals(resultMap.get("return_code")) || "FAIL".equals(resultMap.get("result_code"))){
  42. log.error("微信支付统一下单错误 ===> {} ", resultXml);
  43. throw new RuntimeException("微信支付统一下单错误");
  44. }
  45. //二维码
  46. codeUrl = resultMap.get("code_url");
  47. //保存二维码
  48. String orderNo = orderInfo.getOrderNo();
  49. orderInfoService.saveCodeUrl(orderNo, codeUrl);
  50. //返回二维码
  51. Map<String, Object> map = new HashMap<>();
  52. map.put("codeUrl", codeUrl);
  53. map.put("orderNo", orderInfo.getOrderNo());
  54. return map;
  55. }

4、支付回调

  1. @CrossOrigin //跨域
  2. @RestController
  3. @RequestMapping("/api/wx-pay-v2")
  4. @Api(tags = "网站微信支付APIv2")
  5. @Slf4j
  6. public class WxPayV2Controller {
  7. @Resource
  8. private WxPayService wxPayService;
  9. @Resource
  10. private WxPayConfig wxPayConfig;
  11. @Resource
  12. private OrderInfoService orderInfoService;
  13. @Resource
  14. private PaymentInfoService paymentInfoService;
  15. private final ReentrantLock lock = new ReentrantLock();
  16. /**
  17. * 支付通知
  18. * 微信支付通过支付通知接口将用户支付成功消息通知给商户
  19. */
  20. @PostMapping("/native/notify")
  21. public String wxNotify(HttpServletRequest request) throws Exception {
  22. System.out.println("微信发送的回调");
  23. Map<String, String> returnMap = new HashMap<>();//应答对象
  24. //处理通知参数
  25. String body = HttpUtils.readData(request);
  26. //验签
  27. if(!WXPayUtil.isSignatureValid(body, wxPayConfig.getPartnerKey())) {
  28. log.error("通知验签失败");
  29. //失败应答
  30. returnMap.put("return_code", "FAIL");
  31. returnMap.put("return_msg", "验签失败");
  32. String returnXml = WXPayUtil.mapToXml(returnMap);
  33. return returnXml;
  34. }
  35. //解析xml数据
  36. Map<String, String> notifyMap = WXPayUtil.xmlToMap(body);
  37. //判断通信和业务是否成功
  38. if(!"SUCCESS".equals(notifyMap.get("return_code")) || !"SUCCESS".equals(notifyMap.get("result_code"))) {
  39. log.error("失败");
  40. //失败应答
  41. returnMap.put("return_code", "FAIL");
  42. returnMap.put("return_msg", "失败");
  43. String returnXml = WXPayUtil.mapToXml(returnMap);
  44. return returnXml;
  45. }
  46. //获取商户订单号
  47. String orderNo = notifyMap.get("out_trade_no");
  48. OrderInfo orderInfo = orderInfoService.getOrderByOrderNo(orderNo);
  49. //并校验返回的订单金额是否与商户侧的订单金额一致
  50. if (orderInfo != null && orderInfo.getTotalFee() != Long.parseLong(notifyMap.get("total_fee"))) {
  51. log.error("金额校验失败");
  52. //失败应答
  53. returnMap.put("return_code", "FAIL");
  54. returnMap.put("return_msg", "金额校验失败");
  55. String returnXml = WXPayUtil.mapToXml(returnMap);
  56. return returnXml;
  57. }
  58. //处理订单
  59. if(lock.tryLock()){
  60. try {
  61. //处理重复的通知
  62. //接口调用的幂等性:无论接口被调用多少次,产生的结果是一致的。
  63. String orderStatus = orderInfoService.getOrderStatus(orderNo);
  64. if(OrderStatus.NOTPAY.getType().equals(orderStatus)){
  65. //更新订单状态
  66. orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.SUCCESS);
  67. //记录支付日志
  68. paymentInfoService.createPaymentInfo(body);
  69. }
  70. } finally {
  71. //要主动释放锁
  72. lock.unlock();
  73. }
  74. }
  75. returnMap.put("return_code", "SUCCESS");
  76. returnMap.put("return_msg", "OK");
  77. String returnXml = WXPayUtil.mapToXml(returnMap);
  78. log.info("支付成功,已应答");
  79. return returnXml;
  80. }
  81. }

支付宝支付

1、支付宝开放能力介绍

1.1 开放平台账号注册

https://open.alipay.com/
image.png
image.png

1.2 能力地图

https://opendocs.alipay.com/open/270/105898
⽀付能力、⽀付扩展、资⾦能力、⼝碑能力、营销能力、会员能力、⾏业能力、安全能力、基础能力
image.png

2、接入准备

2.1 常规接入流程

https://opendocs.alipay.com/open/01bxlm

第一步:创建应用

选择应⽤类型、填写应⽤基本信息、添加应⽤功能、配置应⽤环境(获取⽀付宝公 钥、应⽤公钥、应⽤私钥、⽀付宝⽹关地址,配置接⼝内容加密⽅式)、查看 APPID
image.png

第二步:绑定应用

将开发者账号中的APPID和商家账号PID进行绑定
https://opendocs.alipay.com/open/0128wr
在商家中心获取商家账号并绑定APPID:https://b.alipay.com/page/store-management/infomanage
image.png

第三步:配置秘钥

即创建应用中的“配置应用环境”步骤
image.png
image.png
image.png

第四步:上线应用

将应用提交审核
image.png

第五步:签约功能

在商家中心上传营业执照、已备案网站信息等,提交审核进行签约
电脑网站支付需要签约完成后才会生效。在商家中心的产品中心中完成签约(需要真实的企业材料)
https://b.alipay.com/signing/productSetV2.htm?mrchportalwebServer=https%3A%2F%2Fmrchportalweb.alipay.com
image.png
image.png

2.2 沙箱接入流程

直接使用沙箱提供的开发参数,无需进行应用创建、绑定、上线和签约
沙箱环境介绍:https://opendocs.alipay.com/common/02kkv7
沙箱环境主页:https://openhome.alipay.com/develop/sandbox/app

第一步:获取相关参数

image.png

第二步:获取沙箱账号

image.png

3、创建客户端连接对象

3.1 配置支付宝支付参数

  1. # 支付宝支付相关参数
  2. # 应用ID,您的APPID,收款账号既是您的APPID对应支付宝账号
  3. alipay.app-id=2088622958601234
  4. # 商户PID,卖家支付宝账号ID
  5. alipay.seller-id=2088621958581234
  6. # 支付宝网关
  7. alipay.gateway-url=https://openapi.alipaydev.com/gateway.do
  8. # 商户私钥,您的PKCS8格式RSA2私钥
  9. 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=
  10. # 支付宝公钥,查看地址:https://openhome.alipay.com/platform/keyManage.htm 对应APPID下的支付宝公钥
  11. alipay.alipay-public-key=MIIBIjANBgkqhkiG9w0BAQEQEAldW1hLDeN/yMYItGH0+KcJEyXJtFqVNIJZYjXDY2Yes1etG1qFvpT1fOYOAQ6qfasSJOcEGSgQ6oBqGhfaDZGS7ctbX1RlAUFDtM99HhLbKTx5DRSae5HDmSfhTJdOY41DbbOSY9qi8c9zwsGYvhfTq0yvwsXRlYfCZGAFTblyfXi4EynxJdkhnxCFBUwwMFzjbGxELodJYZjPXrO886zHSnUlnUWOm2f3Ol2cBHmGcKNCkVG9w8hHR9qjakSy3Ub+y8h6sDAWazQL+VKs8IrG6SLr8Amhjvxjh7S7OoPDqpU5CXvEEj1Vtc+1MK6yvKJwxbloCuoWywIDAQAB
  12. # 接口内容加密秘钥,对称秘钥
  13. alipay.content-key= 8fnvoe8X6Q==
  14. # 页面跳转同步通知页面路径
  15. alipay.return-url=http://localhost:8080/#/success
  16. # 服务器异步通知页面路径 http://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问
  17. # 注意:每次重新启动ngrok,都需要根据实际情况修改这个配置
  18. alipay.notify-url=https://a863-180-174-204-123.ngrok.io/api/ali-pay/trade/notify

将alipay-sandbox.properties添加为SpringBoot的标准配置文件
image.png

3.2 读取支付参数配置文件

  1. package com.atguigu.paymentdemo.config;
  2. @Configuration
  3. @PropertySource("classpath:alipay-sandbox.properties")
  4. public class AlipayClientConfig {
  5. @Resource
  6. private Environment config;
  7. }

3.3 创建支付宝客户端连接对象

3.3.1 引入支付宝SDK

参考文档:开放平台 => 文档 => 开发⼯具 => 服务端SDK => Java => 通用版 => Maven项目依赖
https://opendocs.alipay.com/mini/02c6he
image.png

  1. <!--支付宝 SDK-->
  2. <dependency>
  3. <groupId>com.alipay.sdk</groupId>
  4. <artifactId>alipay-sdk-java</artifactId>
  5. <version>4.22.57.ALL</version>
  6. </dependency>

3.3.2 创建客户端对象

客户端对象封装了签名和验签功能
https://opendocs.alipay.com/common/02kf5q
image.png

  1. package com.atguigu.paymentdemo.config;
  2. @Configuration
  3. @PropertySource("classpath:alipay-sandbox.properties")
  4. public class AlipayClientConfig {
  5. @Resource
  6. private Environment config;
  7. // 创建客户端对象AlipayClient。封装了签名和验签的整个功能
  8. @Bean
  9. public AlipayClient alipayClient() throws AlipayApiException {
  10. AlipayConfig alipayConfig = new AlipayConfig();
  11. // 设置网关地址
  12. alipayConfig.setServerUrl(config.getProperty("alipay.gateway-url"));
  13. // 设置应用Id
  14. alipayConfig.setAppId(config.getProperty("alipay.app-id"));
  15. // 设置应用私钥
  16. alipayConfig.setPrivateKey(config.getProperty("alipay.merchant-private-key"));
  17. // 设置请求格式,固定值json
  18. // alipayConfig.setFormat("json");
  19. alipayConfig.setFormat(AlipayConstants.FORMAT_JSON);
  20. // 设置字符集
  21. alipayConfig.setCharset("utf8");
  22. // 设置支付宝公钥
  23. alipayConfig.setAlipayPublicKey(config.getProperty("alipay.alipay-public-key"));
  24. // 设置签名类型
  25. alipayConfig.setSignType(AlipayConstants.SIGN_TYPE_RSA2);
  26. // 构造client
  27. AlipayClient alipayClient = new DefaultAlipayClient(alipayConfig);
  28. return alipayClient;
  29. }
  30. }

4、支付功能开发

支付宝支付API列表
https://opendocs.alipay.com/open/028r8t?scene=22
image.png

4.1 统⼀收单下单并支付页面接口

4.1.1 支付调用流程

image.png

4.1.2 接口说明

https://opendocs.alipay.com/apis/028r8t?scene=22
公共请求参数:所有接口都需要的参数。在AlipayClient对象向开放平台发起接口请求调用之前,sign签名这个参数会被自动生成,所以不需要手动设置
image.png
请求参数:当前接口需要的参数
image.png
公共响应参数:所有接口的响应中都包含的数据
响应参数:当前接口的响应中包含的数据
响应示例:

  1. // 响应为表单格式,可嵌入页面,具体以返回的结果为准。浏览器会自动执行该表单
  2. <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&notify_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&timestamp=2021-02-02+14%3A11%3A40&alipay_sdk=alipay-sdk-java-dynamicVersionNo&format=json">
  3. <input type="submit" value="提交" style="display:none" >
  4. </form>
  5. <script>document.forms[0].submit();</script>

接口调用说明:业务前端调用业务后端,业务后端调用开放平台的统一收单下单接口,开放平台接口返回form表单给业务后端,业务后端返回给业务前端,前端接收到form表单后,浏览器自动执行表单提交,提交内容至开放平台,开放平台给用户展示支付登录页面,显示登录二维码和登录账号密码表单
image.png

4.1.3 发起支付请求

(1)创建AliPayController
  1. package com.atguigu.paymentdemo.controller;
  2. @CrossOrigin
  3. @RestController
  4. @RequestMapping("/api/ali-pay")
  5. @Api(tags = "网站支付宝支付")
  6. @Slf4j
  7. public class AliPayController {
  8. @Resource
  9. private AliPayService aliPayService;
  10. @ApiOperation("统一收单下单并支付页面接口的调用")
  11. @PostMapping("/trade/page/pay/{productId}")
  12. public R tradePagePay(@PathVariable Long productId){
  13. log.info("统一收单下单并支付页面接口的调用");
  14. // 支付宝开放平台接受 request 请求对象后
  15. // 会为开发者生成一个html形式的 form表单,包含自动提交的脚本
  16. String formStr = aliPayService.tradeCreate(productId);
  17. // 我们将form表单字符串返回给前端程序,之后前端将会调用自动提交脚本,进行表单的提交
  18. // 此时,表单会自动提交到action属性所指向的支付宝开放平台中,从而为用户展示一个支付页面
  19. return R.ok().data("formStr", formStr);
  20. }
  21. }

(2)AliPayService接口
  1. package com.atguigu.paymentdemo.service;
  2. public interface AliPayService {
  3. String tradeCreate(Long productId);
  4. }

(3)实现类AliPayServiceImpl

image.png

  1. package com.atguigu.paymentdemo.service.impl;
  2. @Service
  3. @Slf4j
  4. public class AliPayServiceImpl implements AliPayService {
  5. @Resource
  6. private OrderInfoService orderInfoService;
  7. @Resource
  8. private AlipayClient alipayClient;
  9. @Resource
  10. private Environment config;
  11. @Transactional(rollbackFor = Exception.class)
  12. @Override
  13. public String tradeCreate(Long productId) {
  14. try {
  15. // 生成订单
  16. log.info("生成订单");
  17. OrderInfo orderInfo = orderInfoService.createOrderByProductId(productId, PayType.ALIPAY.getType());
  18. // 调用支付宝接口
  19. AlipayTradePagePayRequest request = new AlipayTradePagePayRequest();
  20. // 配置需要的公共请求参数
  21. // 支付完成后,支付宝向谷粒学院发起异步通知的地址
  22. request.setNotifyUrl(config.getProperty("alipay.notify-url"));
  23. // 支付完成后,我们想让页面跳转回谷粒学院的页面,配置returnUrl(同步返回)
  24. request.setReturnUrl(config.getProperty("alipay.return-url"));
  25. // 组装当前业务方法的请求参数
  26. JSONObject bizContent = new JSONObject();
  27. bizContent.put("out_trade_no", orderInfo.getOrderNo()); // 商户订单号
  28. BigDecimal total = new BigDecimal(orderInfo.getTotalFee().toString()).divide(new BigDecimal("100"));
  29. bizContent.put("total_amount", total); // 订单金额
  30. bizContent.put("subject", orderInfo.getTitle());
  31. bizContent.put("product_code", "FAST_INSTANT_TRADE_PAY"); // 销售产品码,与支付宝签约的产品码名称
  32. request.setBizContent(bizContent.toString());
  33. // 执行请求,调用支付宝接口
  34. AlipayTradePagePayResponse response = alipayClient.pageExecute(request);
  35. if(response.isSuccess()){
  36. log.info("调用成功,返回结果 ===> " + response.getBody());
  37. return response.getBody();
  38. } else {
  39. log.info("调用失败,返回码 ===> " + response.getCode() + ", 返回描述 ===> " + response.getMsg());
  40. throw new RuntimeException("创建支付交易失败");
  41. }
  42. } catch (AlipayApiException e) {
  43. e.printStackTrace();
  44. throw new RuntimeException("创建支付交易失败");
  45. }
  46. }
  47. }

(4)创建订单createOrderByProductId
  1. package com.atguigu.paymentdemo.service.impl;
  2. @Service
  3. @Slf4j
  4. public class OrderInfoServiceImpl extends ServiceImpl<OrderInfoMapper, OrderInfo> implements OrderInfoService {
  5. @Resource
  6. private ProductMapper productMapper;
  7. @Override
  8. public OrderInfo createOrderByProductId(Long productId, String paymentType) {
  9. //查找已存在但未支付的订单
  10. OrderInfo orderInfo = this.getNoPayOrderByProductId(productId, paymentType);
  11. if( orderInfo != null){
  12. return orderInfo;
  13. }
  14. //获取商品信息
  15. Product product = productMapper.selectById(productId);
  16. //生成订单
  17. orderInfo = new OrderInfo();
  18. orderInfo.setTitle(product.getTitle());
  19. orderInfo.setOrderNo(OrderNoUtils.getOrderNo()); //订单号
  20. orderInfo.setProductId(productId);
  21. orderInfo.setTotalFee(product.getPrice()); //分
  22. orderInfo.setOrderStatus(OrderStatus.NOTPAY.getType()); //未支付
  23. orderInfo.setPaymentType(paymentType);
  24. baseMapper.insert(orderInfo);
  25. return orderInfo;
  26. }
  27. /**
  28. * 根据商品id查询未支付订单
  29. * 防止重复创建订单对象
  30. * @param productId
  31. * @return
  32. */
  33. private OrderInfo getNoPayOrderByProductId(Long productId, String paymentType) {
  34. QueryWrapper<OrderInfo> queryWrapper = new QueryWrapper<>();
  35. queryWrapper.eq("product_id", productId);
  36. queryWrapper.eq("order_status", OrderStatus.NOTPAY.getType());
  37. queryWrapper.eq("payment_type", paymentType);
  38. // queryWrapper.eq("user_id", userId);
  39. OrderInfo orderInfo = baseMapper.selectOne(queryWrapper);
  40. return orderInfo;
  41. }
  42. }

(5)测试

image.png
image.png
注意点:由于支付宝沙箱环境的限制,每次使用浏览器访问沙箱环境时,当前浏览器窗口只能保留测试环境的菜单。否则会出现异常
image.png

4.1.4 前端支付按钮

(1)index.vue
  1. methods: {
  2. // 选择商品
  3. selectItem(productId) {
  4. console.log('商品id:' + productId)
  5. this.payOrder.productId = productId
  6. console.log(this.payOrder)
  7. //this.$router.push({ path: '/order' })
  8. },
  9. // 选择支付方式
  10. selectPayType(type) {
  11. console.log('支付方式:' + type)
  12. this.payOrder.payType = type
  13. //this.$router.push({ path: '/order' })
  14. },
  15. // 确认支付
  16. toPay() {
  17. // 禁用按钮,防止重复提交
  18. this.payBtnDisabled = true
  19. // 微信支付
  20. if (this.payOrder.payType === 'wxpay') {
  21. //调用统一下单接口
  22. wxPayApi.nativePay(this.payOrder.productId).then((response) => {
  23. this.codeUrl = response.data.codeUrl
  24. this.orderNo = response.data.orderNo
  25. //打开二维码弹窗
  26. this.codeDialogVisible = true
  27. //启动定时器
  28. this.timer = setInterval(() => {
  29. //查询订单是否支付成功
  30. this.queryOrderStatus()
  31. }, 3000)
  32. })
  33. //支付宝支付
  34. } else if (this.payOrder.payType === 'alipay') {
  35. // 调用支付宝统一收单下单并支付页面接口
  36. aliPayApi.tradePagePay(this.payOrder.productId).then((response) => {
  37. // 将支付宝返回的表单字符串写在浏览器中,表单会自动触发submit提交
  38. document.write(response.data.formStr)
  39. })
  40. }
  41. },

(2)aliPay.js
  1. // axios 发送ajax请求
  2. import request from '@/utils/request'
  3. export default{
  4. //发起支付请求
  5. tradePagePay(productId) {
  6. return request({
  7. url: '/api/ali-pay/trade/page/pay/' + productId,
  8. method: 'post'
  9. })
  10. },
  11. cancel(orderNo) {
  12. return request({
  13. url: '/api/ali-pay/trade/close/' + orderNo,
  14. method: 'post'
  15. })
  16. },
  17. refunds(orderNo, reason) {
  18. return request({
  19. url: '/api/ali-pay/trade/refund/' + orderNo + '/' + reason,
  20. method: 'post'
  21. })
  22. }
  23. }

(3)启动前端服务

image.png

4.2 支付结果通知

4.2.1 启动内网穿透ngrok

  1. ngrok http 8090

image.png
image.png
image.png
image.png

4.2.2 修改内网穿透配置

根据ngrok每次启动的情况,修改 alipay-sandbox.properties 文件中的 alipay.notify-url

  1. # 服务器异步通知页面路径 http://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常 访问
  2. # 注意:每次重新启动ngrok,都需要根据实际情况修改这个配置
  3. alipay.notify-url=https://a863-180-174-204-169.ngrok.io/api/ali-pay/trade/notify

image.png

4.2.3 设置异步通知地址

在 AliPayServiceImpl 的 tradeCreate 方法中设置异步通知地址

  1. // 配置需要的公共请求参数
  2. // 支付完成后,支付宝向谷粒学院发起异步通知的地址
  3. request.setNotifyUrl(config.getProperty("alipay.notify-url"));

image.png

  1. {
  2. gmt_create=2022-05-08 10:53:10,
  3. charset=UTF-8,
  4. gmt_payment=2022-05-08 10:53:21,
  5. notify_time=2022-05-08 10:53:23,
  6. subject=前端课程,
  7. sign=XJ/6o2bPe0246sYh8MXfSmW2rYb3pM9xw1T7bkI1JJP7kzRvFampFTeCfCaHhfxo3jtiID+6+/2QTT9xnGG8d417wz5chytnBvMXmMNSClNi6YZXWywCMBaNBz4PEQkvI6j6HupWKO20CtNSbo3bIB7+4t9gWTAFyorRv8TY3zMMeyLt2JQbxi/EC9N3jEz70i/SPMND98xaIO5YgvJVrjAAcXd6FizohplMuYA+d3wFKYipIoTyfbmIQsI071sAIJ/RUg+DmJi0b5NV5H8k1QjlhFnv08rzj7wfR5vzWbzcHFjXfAabpMiBMJyfG8ToXajf3VPXT/Yn5LORwVPfNw==,
  8. buyer_id=2088622958601474,
  9. invoice_amount=0.01,
  10. version=1.0,
  11. notify_id=2022050800222105322001470523533608,
  12. fund_bill_list=[
  13. {
  14. "amount":"0.01",
  15. "fundChannel":"ALIPAYACCOUNT"
  16. }
  17. ],
  18. notify_type=trade_status_sync,
  19. out_trade_no=ORDER_20220508105248532,
  20. total_amount=0.01,
  21. trade_status=TRADE_SUCCESS, // 触发通知
  22. trade_no=2022050822001401470502515966,
  23. auth_app_id=2021000119683440,
  24. receipt_amount=0.01,
  25. point_amount=0.00,
  26. app_id=2021000119683440,
  27. buyer_pay_amount=0.01,
  28. sign_type=RSA2,
  29. seller_id=2088621958584044
  30. }

image.png

4.2.4 开发异步通知接口

https://opendocs.alipay.com/open/270/105902
image.png

(1)创建AliPayController
  1. package com.atguigu.paymentdemo.controller;
  2. @CrossOrigin
  3. @RestController
  4. @RequestMapping("/api/ali-pay")
  5. @Api(tags = "网站支付宝支付")
  6. @Slf4j
  7. public class AliPayController {
  8. @Resource
  9. private AliPayService aliPayService;
  10. @Resource
  11. private Environment config;
  12. @Resource
  13. private OrderInfoService orderInfoService;
  14. @ApiOperation("统一收单下单并支付页面接口的调用")
  15. @PostMapping("/trade/page/pay/{productId}")
  16. public R tradePagePay(@PathVariable Long productId){
  17. log.info("统一收单下单并支付页面接口的调用");
  18. // 支付宝开放平台接受 request 请求对象后
  19. // 会为开发者生成一个html 形式的 form表单,包含自动提交的脚本
  20. String formStr = aliPayService.tradeCreate(productId);
  21. //我们将form表单字符串返回给前端程序,之后前端将会调用自动提交脚本,进行表单的提交
  22. //此时,表单会自动提交到action属性所指向的支付宝开放平台中,从而为用户展示一个支付页面
  23. return R.ok().data("formStr", formStr);
  24. }
  25. // 获取HttpServelt的request请求参数(支付宝提交的请求参数),并存放到params
  26. @ApiOperation("支付通知")
  27. @PostMapping("/trade/notify")
  28. public String tradeNotify(@RequestParam Map<String, String> params){
  29. log.info("支付通知正在执行");
  30. log.info("通知参数 ===> {}", params);
  31. String result = "failure";
  32. try {
  33. // 异步通知验签。调用SDK验证签名:验证支付宝请求参数params中的sign签名是否正确
  34. boolean signVerified = AlipaySignature.rsaCheckV1(
  35. params,
  36. config.getProperty("alipay.alipay-public-key"),
  37. AlipayConstants.CHARSET_UTF8,
  38. AlipayConstants.SIGN_TYPE_RSA2);
  39. if(!signVerified){
  40. // 验签失败则记录异常日志,并在response中返回failure.
  41. log.error("支付成功异步通知验签失败!");
  42. return result;
  43. }
  44. // 验签成功后
  45. log.info("支付成功异步通知验签成功!");
  46. // 按照支付结果异步通知中的描述,对支付结果中的业务内容进行二次校验,校验成功后在response中返回success并继续商户自身业务处理,校验失败返回failure
  47. // 1、商户需要验证该通知数据中的 out_trade_no 是否为商户系统中创建的订单号
  48. String outTradeNo = params.get("out_trade_no");
  49. OrderInfo order = orderInfoService.getOrderByOrderNo(outTradeNo);
  50. if(order == null){
  51. log.error("订单不存在");
  52. return result;
  53. }
  54. // 2、判断 total_amount 是否确实为该订单的实际金额(即商户订单创建时的金额)
  55. String totalAmount = params.get("total_amount");
  56. int totalAmountInt = new BigDecimal(totalAmount).multiply(new BigDecimal("100")).intValue();
  57. int totalFeeInt = order.getTotalFee().intValue();
  58. if(totalAmountInt != totalFeeInt){
  59. log.error("金额校验失败");
  60. return result;
  61. }
  62. // 3、校验通知中的 seller_id(或者 seller_email) 是否为 out_trade_no 这笔单据的对应的操作方
  63. String sellerId = params.get("seller_id");
  64. String sellerIdProperty = config.getProperty("alipay.seller-id");
  65. if(!sellerId.equals(sellerIdProperty)){
  66. log.error("商家pid校验失败");
  67. return result;
  68. }
  69. //4、验证 app_id 是否为该商户本身
  70. String appId = params.get("app_id");
  71. String appIdProperty = config.getProperty("alipay.app-id");
  72. if(!appId.equals(appIdProperty)){
  73. log.error("appid校验失败");
  74. return result;
  75. }
  76. // 在支付宝的业务通知中,只有交易通知状态为TRADE_SUCCESS时,支付宝才会认定为买家付款成功。
  77. String tradeStatus = params.get("trade_status");
  78. if(!"TRADE_SUCCESS".equals(tradeStatus)){
  79. log.error("支付未成功");
  80. return result;
  81. }
  82. // 处理业务 修改订单状态 记录支付日志
  83. aliPayService.processOrder(params);
  84. // 校验成功后在response中返回success并继续商户自身业务处理,校验失败返回failure
  85. result = "success";
  86. } catch (AlipayApiException e) {
  87. e.printStackTrace();
  88. }
  89. return result;
  90. }
  91. }

(2)AliPayService接口
  1. void processOrder(Map<String, String> params);

(3)实现类AliPayServiceImpl
  1. package com.atguigu.paymentdemo.service.impl;
  2. @Service
  3. @Slf4j
  4. public class AliPayServiceImpl implements AliPayService {
  5. @Resource
  6. private OrderInfoService orderInfoService;
  7. @Resource
  8. private AlipayClient alipayClient;
  9. @Resource
  10. private Environment config;
  11. @Resource
  12. private PaymentInfoService paymentInfoService;
  13. @Resource
  14. private RefundInfoService refundsInfoService;
  15. private final ReentrantLock lock = new ReentrantLock();
  16. /**
  17. * 处理订单
  18. * @param params
  19. */
  20. @Transactional(rollbackFor = Exception.class)
  21. @Override
  22. public void processOrder(Map<String, String> params) {
  23. log.info("处理订单");
  24. // 获取订单号
  25. String orderNo = params.get("out_trade_no");
  26. // 在对业务数据进行状态检查和处理之前,要采用数据锁进行并发控制,以避免函数重入造成的数据混乱
  27. // 尝试获取锁:
  28. // 成功获取则立即返回true,获取失败则立即返回false。不必一直等待锁的释放
  29. if(lock.tryLock()) {
  30. try {
  31. // 处理重复通知
  32. // 接口调用的幂等性:无论接口被调用多少次,以下业务执行一次
  33. String orderStatus = orderInfoService.getOrderStatus(orderNo);
  34. if (!OrderStatus.NOTPAY.getType().equals(orderStatus)) {
  35. return;
  36. }
  37. // 更新订单状态
  38. orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.SUCCESS);
  39. // 记录支付日志
  40. paymentInfoService.createPaymentInfoForAliPay(params);
  41. } finally {
  42. // 要主动释放锁
  43. lock.unlock();
  44. }
  45. }
  46. }
  47. }

4.2.5 记录支付日志

  1. public interface PaymentInfoService {
  2. void createPaymentInfoForAliPay(Map<String, String> params);
  3. }
  1. package com.atguigu.paymentdemo.service.impl;
  2. @Service
  3. @Slf4j
  4. public class PaymentInfoServiceImpl extends ServiceImpl<PaymentInfoMapper, PaymentInfo> implements PaymentInfoService {
  5. /**
  6. * 记录支付日志:支付宝
  7. * @param params
  8. */
  9. @Override
  10. public void createPaymentInfoForAliPay(Map<String, String> params) {
  11. log.info("记录支付日志");
  12. // 获取订单号
  13. String orderNo = params.get("out_trade_no");
  14. // 业务编号
  15. String transactionId = params.get("trade_no");
  16. // 交易状态
  17. String tradeStatus = params.get("trade_status");
  18. // 交易金额
  19. String totalAmount = params.get("total_amount");
  20. int totalAmountInt = new BigDecimal(totalAmount).multiply(new BigDecimal("100")).intValue();
  21. PaymentInfo paymentInfo = new PaymentInfo();
  22. paymentInfo.setOrderNo(orderNo);
  23. paymentInfo.setPaymentType(PayType.ALIPAY.getType());
  24. paymentInfo.setTransactionId(transactionId);
  25. paymentInfo.setTradeType("电脑网站支付");
  26. paymentInfo.setTradeState(tradeStatus);
  27. paymentInfo.setPayerTotal(totalAmountInt);
  28. Gson gson = new Gson();
  29. String json = gson.toJson(params, HashMap.class);
  30. paymentInfo.setContent(json);
  31. baseMapper.insert(paymentInfo);
  32. }
  33. }

image.png

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

https://opendocs.alipay.com/open/270/105902#%E5%BC%82%E6%AD%A5%E8%BF%94%E5%9B%9E%E7%BB%93%E6%9E%9C%E7%9A%84%E9%AA%8C%E7%AD%BE
image.png
通知重复的原因:

  1. 商户给支付宝返回的不是“success”
  2. 支付宝未收到商户发送的消息

此时支付状态已更新、支付日志已记录,但是反馈没有正确发送给支付宝。支付宝就会给服务端不断重发通知,此后支付状态又要被修改(再次更新对业务基本无影响)、支付日志又要被记录(影响业务:一笔业务记录多条记录)

如何过滤重复通知:

  1. 业务服务器在没有成功接收支付宝回调通知时,我们才希望支付宝给我们发送重复通知
  2. 若业务服务器收到支付宝回调通知,但服务器未成功给支付宝反馈消息“success”时,我们不希望接收到支付宝给我们发送的重复通知

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

  1. // 接口调用的幂等性:无论回调通知接口被调用多少次,以下业务只执行一次
  2. // 1、当支付状态为待支付时,需要执行后续的更新订单状态和记录支付日志
  3. // 2、当支付状态不为待支付时,不需要重复执行后续操作
  4. String orderStatus = orderInfoService.getOrderStatus(orderNo);
  5. if (!OrderStatus.NOTPAY.getType().equals(orderStatus)) {
  6. return;
  7. }
  8. // ① 更新订单状态
  9. orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.SUCCESS);
  10. // ② 记录支付日志
  11. paymentInfoService.createPaymentInfoForAliPay(params);

4.2.7 数据锁

在 AliPayServiceImpl 中定义 ReentrantLock 进行并发控制(避免多个回调通知同时到达,从而产生多条日志)。注意,必须⼿动释放锁。

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

完整的 processOrder 方法:

  1. package com.atguigu.paymentdemo.service.impl;
  2. @Service
  3. @Slf4j
  4. public class AliPayServiceImpl implements AliPayService {
  5. @Resource
  6. private OrderInfoService orderInfoService;
  7. @Resource
  8. private AlipayClient alipayClient;
  9. @Resource
  10. private Environment config;
  11. @Resource
  12. private PaymentInfoService paymentInfoService;
  13. @Resource
  14. private RefundInfoService refundsInfoService;
  15. private final ReentrantLock lock = new ReentrantLock();
  16. /**
  17. * 处理订单
  18. * @param params
  19. */
  20. @Transactional(rollbackFor = Exception.class)
  21. @Override
  22. public void processOrder(Map<String, String> params) {
  23. log.info("处理订单");
  24. // 获取订单号
  25. String orderNo = params.get("out_trade_no");
  26. // 在对业务数据进行状态检查和处理之前,要采用数据锁进行并发控制,以避免函数重入造成的数据混乱
  27. // 尝试获取锁:
  28. // 成功获取则立即返回true,获取失败则立即返回false。不必一直等待锁的释放
  29. if(lock.tryLock()) {
  30. try {
  31. // 处理重复通知
  32. // 接口调用的幂等性:无论接口被调用多少次,以下业务执行一次
  33. String orderStatus = orderInfoService.getOrderStatus(orderNo);
  34. if (!OrderStatus.NOTPAY.getType().equals(orderStatus)) {
  35. return;
  36. }
  37. // 更新订单状态
  38. orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.SUCCESS);
  39. // 记录支付日志
  40. paymentInfoService.createPaymentInfoForAliPay(params);
  41. } finally {
  42. // 要主动释放锁
  43. lock.unlock();
  44. }
  45. }
  46. }
  47. }

4.3 订单表优化

4.3.1 修改表

t_order_info 表中添加 payment_type 字段(区别两种支付方式:支付宝、微信)

4.3.2 业务修改

(1)修改⽀付业务代码

修改AliPayServiceImpl、WxPayServiceImpl代码中对如下方法的调用,添加参数PayType.ALIPAY.getType()

  1. log.info("生成订单");
  2. 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 定义用户取消订单接口

  1. package com.atguigu.paymentdemo.controller;
  2. @CrossOrigin
  3. @RestController
  4. @RequestMapping("/api/ali-pay")
  5. @Api(tags = "网站支付宝支付")
  6. @Slf4j
  7. public class AliPayController {
  8. @Resource
  9. private AliPayService aliPayService;
  10. /**
  11. * 用户取消订单
  12. * @param orderNo
  13. * @return
  14. */
  15. @ApiOperation("用户取消订单")
  16. @PostMapping("/trade/close/{orderNo}")
  17. public R cancel(@PathVariable String orderNo){
  18. log.info("取消订单");
  19. aliPayService.cancelOrder(orderNo);
  20. return R.ok().setMessage("订单已取消");
  21. }
  22. }

4.4.2 关单并修改订单状态

  1. void cancelOrder(String orderNo);
  1. package com.atguigu.paymentdemo.service.impl;
  2. @Service
  3. @Slf4j
  4. public class AliPayServiceImpl implements AliPayService {
  5. @Resource
  6. private OrderInfoService orderInfoService;
  7. @Resource
  8. private AlipayClient alipayClient;
  9. /**
  10. * 用户取消订单
  11. * @param orderNo
  12. */
  13. @Override
  14. public void cancelOrder(String orderNo) {
  15. // 调用支付宝提供的统一收单交易关闭接口
  16. this.closeOrder(orderNo);
  17. // 更新用户订单状态
  18. orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.CANCEL);
  19. }
  20. /**
  21. * 关单接口的调用
  22. * @param orderNo 订单号
  23. */
  24. private void closeOrder(String orderNo) {
  25. try {
  26. log.info("关单接口的调用,订单号 ===> {}", orderNo);
  27. AlipayTradeCloseRequest request = new AlipayTradeCloseRequest();
  28. JSONObject bizContent = new JSONObject();
  29. bizContent.put("out_trade_no", orderNo);
  30. request.setBizContent(bizContent.toString());
  31. AlipayTradeCloseResponse response = alipayClient.execute(request);
  32. if(response.isSuccess()){
  33. log.info("调用成功,返回结果 ===> " + response.getBody());
  34. } else {
  35. log.info("调用失败,返回码 ===> " + response.getCode() + ", 返回描述 ===> " + response.getMsg());
  36. //throw new RuntimeException("关单接口的调用失败");
  37. // 此处不要抛异常,原因是下单操作时,首先在本地添加订单数据,但需要登录支付宝后才会在支付宝端生成订单数据。
  38. // 当本地存在数据,而支付宝端不存在订单数据时,调用取消订单接口,支付宝关单接口会报错提示“交易不存在”,此时就退出整个流程了
  39. // 所以此处不抛异常的作用是,支付宝端关单接口报错,直接忽略,然后执行本地关单方法orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.CANCEL);关闭本地数据即可
  40. }
  41. } catch (AlipayApiException e) {
  42. e.printStackTrace();
  43. throw new RuntimeException("关单接口的调用失败");
  44. }
  45. }
  46. }

image.png

4.4.3 测试

注意:针对⼆维码支付,只有经过扫码的订单才在⽀付宝端有交易记录。针对支付宝账号支付,只有经过登录的订单才在支付宝端有交易记录。

4.5 统一收单线下交易查询

4.5.1 查单接口的调用

商户后台未收到异步支付结果通知时(支付宝未成功发生回调通知,或者商户由于网络问题未成功收到回调通知),商户应该主动调用《统⼀收单线下交易查询接口》,查看交易状态,并同步本地的订单状态
image.png

(1)AliPayController
  1. package com.atguigu.paymentdemo.controller;
  2. @CrossOrigin
  3. @RestController
  4. @RequestMapping("/api/ali-pay")
  5. @Api(tags = "网站支付宝支付")
  6. @Slf4j
  7. public class AliPayController {
  8. @Resource
  9. private AliPayService aliPayService;
  10. /**
  11. * 查询订单
  12. * @param orderNo
  13. * @return
  14. */
  15. @ApiOperation("查询订单:测试订单状态用")
  16. @GetMapping("/trade/query/{orderNo}")
  17. public R queryOrder(@PathVariable String orderNo) {
  18. log.info("查询订单");
  19. String result = aliPayService.queryOrder(orderNo);
  20. return R.ok().setMessage("查询成功").data("result", result);
  21. }
  22. }

(2)AliPayService
  1. String queryOrder(String orderNo);
  1. package com.atguigu.paymentdemo.service.impl;
  2. @Service
  3. @Slf4j
  4. public class AliPayServiceImpl implements AliPayService {
  5. @Resource
  6. private OrderInfoService orderInfoService;
  7. @Resource
  8. private AlipayClient alipayClient;
  9. /**
  10. * 查询订单
  11. * @param orderNo
  12. * @return 返回订单查询结果,如果返回null则表示支付宝端尚未创建订单
  13. */
  14. @Override
  15. public String queryOrder(String orderNo) {
  16. try {
  17. log.info("查单接口调用 ===> {}", orderNo);
  18. AlipayTradeQueryRequest request = new AlipayTradeQueryRequest();
  19. JSONObject bizContent = new JSONObject();
  20. bizContent.put("out_trade_no", orderNo);
  21. request.setBizContent(bizContent.toString());
  22. AlipayTradeQueryResponse response = alipayClient.execute(request);
  23. if(response.isSuccess()){
  24. log.info("调用成功,返回结果 ===> " + response.getBody());
  25. return response.getBody();
  26. } else {
  27. log.info("调用失败,返回码 ===> " + response.getCode() + ", 返回描述 ===> " + response.getMsg());
  28. //throw new RuntimeException("查单接口的调用失败");
  29. return null; // 订单不存在
  30. }
  31. } catch (AlipayApiException e) {
  32. e.printStackTrace();
  33. throw new RuntimeException("查单接口的调用失败");
  34. }
  35. }
  36. }

image.png

4.5.2 定时查单

(1)创建AliPayTask
  1. package com.atguigu.paymentdemo.task;
  2. @Slf4j
  3. @Component
  4. public class AliPayTask {
  5. @Resource
  6. private OrderInfoService orderInfoService;
  7. @Resource
  8. private AliPayService aliPayService;
  9. /**
  10. * 从第0秒开始每隔30秒执行1次,查询创建超过5分钟,并且未支付的订单
  11. */
  12. @Scheduled(cron = "0/30 * * * * ?")
  13. public void orderConfirm(){
  14. log.info("orderConfirm 被执行......");
  15. // 查询本地未支付的订单
  16. List<OrderInfo> orderInfoList = orderInfoService.getNoPayOrderByDuration(1, PayType.ALIPAY.getType());
  17. for (OrderInfo orderInfo : orderInfoList) {
  18. String orderNo = orderInfo.getOrderNo();
  19. log.warn("超时订单 ===> {}", orderNo);
  20. // 核实订单状态:调用支付宝查单接口
  21. aliPayService.checkOrderStatus(orderNo);
  22. }
  23. }
  24. }

(2)修改OrderInfoService
  1. List<OrderInfo> getNoPayOrderByDuration(int minutes, String paymentType);

实现添加参数 String paymentType , 添加查询条件queryWrapper.eq(“payment_type”, paymentType);

  1. /**
  2. * 查询创建超过minutes分钟并且未支付的订单
  3. * @param minutes
  4. * @return
  5. */
  6. @Override
  7. public List<OrderInfo> getNoPayOrderByDuration(int minutes, String paymentType) {
  8. Instant instant = Instant.now().minus(Duration.ofMinutes(minutes));
  9. QueryWrapper<OrderInfo> queryWrapper = new QueryWrapper<>();
  10. queryWrapper.eq("order_status", OrderStatus.NOTPAY.getType());
  11. queryWrapper.le("create_time", instant);
  12. queryWrapper.eq("payment_type", paymentType);
  13. List<OrderInfo> orderInfoList = baseMapper.selectList(queryWrapper);
  14. return orderInfoList;
  15. }

image.png

4.5.3 处理查询到的订单

(1)AliPayTask

在定时任务的for循环最后添加以下代码

  1. // 核实订单状态:调用支付宝查单接口
  2. aliPayService.checkOrderStatus(orderNo);

(2)AliPayService
  1. void checkOrderStatus(String orderNo);
  1. package com.atguigu.paymentdemo.service.impl;
  2. @Service
  3. @Slf4j
  4. public class AliPayServiceImpl implements AliPayService {
  5. @Resource
  6. private OrderInfoService orderInfoService;
  7. @Resource
  8. private AlipayClient alipayClient;
  9. /**
  10. * 根据订单号调用支付宝查单接口,核实订单状态
  11. * 如果订单未创建,则更新商户端订单状态(支付宝端订单未创建,商户端订单已创建。原因是下单操作时未登录支付宝,就不会在支付宝端创建订单)
  12. * 如果订单未支付,则调用关单接口关闭订单,并更新商户端订单状态
  13. * 如果订单已支付,则更新商户端订单状态,并记录支付日志(原因可能时支付宝端回调接口未成功发送、或商户端未成功接收造成的数据不一致)
  14. * @param orderNo
  15. */
  16. @Override
  17. public void checkOrderStatus(String orderNo) {
  18. log.warn("根据订单号核实订单状态 ===> {}", orderNo);
  19. String result = this.queryOrder(orderNo);
  20. // 订单未创建
  21. if(result == null){
  22. log.warn("核实订单未创建 ===> {}", orderNo);
  23. // 更新本地订单状态
  24. orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.CLOSED);
  25. }
  26. // 解析查单响应结果
  27. Gson gson = new Gson();
  28. HashMap<String, LinkedTreeMap> resultMap = gson.fromJson(result, HashMap.class);
  29. LinkedTreeMap alipayTradeQueryResponse = resultMap.get("alipay_trade_query_response");
  30. String tradeStatus = (String)alipayTradeQueryResponse.get("trade_status");
  31. if(AliPayTradeState.NOTPAY.getType().equals(tradeStatus)){ // 支付宝端订单未支付
  32. log.warn("核实订单未支付 ===> {}", orderNo);
  33. //如果订单未支付,则调用关单接口关闭订单
  34. this.closeOrder(orderNo);
  35. // 并更新商户端订单状态
  36. orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.CLOSED);
  37. }
  38. if(AliPayTradeState.SUCCESS.getType().equals(tradeStatus)){ // 支付宝端订单已支付
  39. log.warn("核实订单已支付 ===> {}", orderNo);
  40. // 如果订单已支付,则更新商户端订单状态
  41. orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.SUCCESS);
  42. // 并记录支付日志
  43. paymentInfoService.createPaymentInfoForAliPay(alipayTradeQueryResponse);
  44. }
  45. }
  46. /**
  47. * 查询订单
  48. * @param orderNo
  49. * @return 返回订单查询结果,如果返回null则表示支付宝端尚未创建订单
  50. */
  51. @Override
  52. public String queryOrder(String orderNo) {
  53. try {
  54. log.info("查单接口调用 ===> {}", orderNo);
  55. AlipayTradeQueryRequest request = new AlipayTradeQueryRequest();
  56. JSONObject bizContent = new JSONObject();
  57. bizContent.put("out_trade_no", orderNo);
  58. request.setBizContent(bizContent.toString());
  59. AlipayTradeQueryResponse response = alipayClient.execute(request);
  60. if(response.isSuccess()){
  61. log.info("调用成功,返回结果 ===> " + response.getBody());
  62. return response.getBody();
  63. } else {
  64. log.info("调用失败,返回码 ===> " + response.getCode() + ", 返回描述 ===> " + response.getMsg());
  65. //throw new RuntimeException("查单接口的调用失败");
  66. return null; // 订单不存在
  67. }
  68. } catch (AlipayApiException e) {
  69. e.printStackTrace();
  70. throw new RuntimeException("查单接口的调用失败");
  71. }
  72. }
  73. }

image.png
image.png
image.png

4.6 统一收单交易退款

https://opendocs.alipay.com/open/028sm9

4.6.1 退款接口

(1)AliPayController
  1. package com.atguigu.paymentdemo.controller;
  2. @CrossOrigin
  3. @RestController
  4. @RequestMapping("/api/ali-pay")
  5. @Api(tags = "网站支付宝支付")
  6. @Slf4j
  7. public class AliPayController {
  8. @Resource
  9. private AliPayService aliPayService;
  10. /**
  11. * 申请退款
  12. * @param orderNo
  13. * @param reason
  14. * @return
  15. */
  16. @ApiOperation("申请退款")
  17. @PostMapping("/trade/refund/{orderNo}/{reason}")
  18. public R refunds(@PathVariable String orderNo, @PathVariable String reason){
  19. log.info("申请退款");
  20. aliPayService.refund(orderNo, reason);
  21. return R.ok();
  22. }
  23. }

(2)AliPayService
  1. void refund(String orderNo, String reason);
  1. package com.atguigu.paymentdemo.service.impl;
  2. @Service
  3. @Slf4j
  4. public class AliPayServiceImpl implements AliPayService {
  5. @Resource
  6. private OrderInfoService orderInfoService;
  7. @Resource
  8. private AlipayClient alipayClient;
  9. @Resource
  10. private RefundInfoService refundsInfoService;
  11. /**
  12. * 退款
  13. * @param orderNo
  14. * @param reason
  15. */
  16. @Transactional(rollbackFor = Exception.class)
  17. @Override
  18. public void refund(String orderNo, String reason) {
  19. try {
  20. log.info("调用退款API");
  21. // 创建退款单
  22. RefundInfo refundInfo = refundsInfoService.createRefundByOrderNoForAliPay(orderNo, reason);
  23. // 调用统一收单交易退款接口
  24. AlipayTradeRefundRequest request = new AlipayTradeRefundRequest ();
  25. // 组装当前业务方法的请求参数
  26. JSONObject bizContent = new JSONObject();
  27. bizContent.put("out_trade_no", orderNo); // 订单编号
  28. BigDecimal refund = new BigDecimal(refundInfo.getRefund().toString()).divide(new BigDecimal("100"));
  29. //BigDecimal refund = new BigDecimal("2").divide(new BigDecimal("100"));
  30. bizContent.put("refund_amount", refund); // 退款金额:不能大于支付金额。一笔订单可以发起多笔退款
  31. bizContent.put("refund_reason", reason); // 退款原因(可选)
  32. request.setBizContent(bizContent.toString());
  33. // 执行请求,调用支付宝接口
  34. AlipayTradeRefundResponse response = alipayClient.execute(request);
  35. if(response.isSuccess()){
  36. log.info("调用成功,返回结果 ===> " + response.getBody());
  37. // 更新订单状态
  38. orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.REFUND_SUCCESS);
  39. // 更新退款单
  40. refundsInfoService.updateRefundForAliPay(refundInfo.getRefundNo(),
  41. response.getBody(),
  42. AliPayTradeState.REFUND_SUCCESS.getType()); // 退款成功
  43. } else {
  44. log.info("调用失败,返回码 ===> " + response.getCode() + ", 返回描述 ===> " + response.getMsg());
  45. // 更新订单状态
  46. orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.REFUND_ABNORMAL);
  47. // 更新退款单
  48. refundsInfoService.updateRefundForAliPay(refundInfo.getRefundNo(),
  49. response.getBody(),
  50. AliPayTradeState.REFUND_ERROR.getType()); // 退款失败
  51. }
  52. } catch (AlipayApiException e) {
  53. e.printStackTrace();
  54. throw new RuntimeException("创建退款申请失败");
  55. }
  56. }
  57. }

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
微信支付与支付宝支付 - 图76
若退款接口由于网络等原因返回异常,商户可调用退款查询接口 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);
    }
}

2AliPayService

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("申请账单失败");
        }
    }
}

在线调试方法:
image.png
image.png