在线支付微信支付
1.微信支付介绍和接入指引
2.证书/密钥/签名
3.案例项目的创建
SpringBoot+Vue Java mp Mysql HTML JS Vue
4.基础支付API V3
微信支付的支付介绍
产品中心 - 微信支付商户平台
具体适用场景可以自己到官网进行查看
Native支付是用户扫描二维码后直接展示由商家指定的支付金额
JSAPI 是由用户自己输入金额
本质一样的只是用于不同的前端中,对于后端来说接入机制都是一样的,我们得学习会以PC网站来实现
微信支付接入指引 - 微信支付商户平台
在接入指引中选择我有PC网站
在我们收钱得过程中微信会收取一定得费用 跟具体类目有关
入驻结算规则、行业属性及特殊资质 - 腾讯客服
申请流程: 提交资料 =》 签署协议 =》 绑定场景
一、微信支付介绍和接入指引
1、微信支付产品介绍
1.1、付款码支付
用户展示微信钱包内的“付款码”给商家,商家扫描后直接完成支付,适用于线下面对面收银的场景。
1.2、JSAPI支付
线下场所:商户展示一个支付二维码,用户使用微信扫描二维码后,输入需要支付的金额,完成支
付。
公众号场景:用户在微信内进入商家公众号,打开某个页面,选择某个产品,完成支付。
PC网站场景:在网站中展示二维码,用户使用微信扫描二维码,输入需要支付的金额,完成支
付。
特点:用户在客户端输入支付金额
1.3、小程序支付
在微信小程序平台内实现支付的功能。
1.4、Native支付
Native支付是指商户展示支付二维码,用户再用微信“扫一扫”完成支付的模式。这种方式适用于PC网
站。
特点:商家预先指定支付金额
1.5、APP支付
商户通过在移动端独立的APP应用程序中集成微信支付模块,完成支付。
1.6、刷脸支付
用户在刷脸设备前通过摄像头刷脸、识别身份后进行的一种支付方式。
2、接入指引
2.1、获取商户号
微信商户平台:https://pay.weixin.qq.com/
场景:Native支付
步骤:提交资料 => 签署协议 => 获取商户号
2.2、获取APPID
微信公众平台:https://mp.weixin.qq.com/
步骤:注册服务号 => 服务号认证 => 获取APPID => 绑定商户号
2.3、获取API秘钥
APIv2版本的接口需要此秘钥
步骤:登录商户平台 => 选择 账户中心 => 安全中心 => API安全 => 设置API密钥
2.4、获取APIv3秘钥
APIv3版本的接口需要此秘钥
步骤:登录商户平台 => 选择 账户中心 => 安全中心 => API安全 => 设置APIv3密钥
随机密码生成工具:https://suijimimashengcheng.bmcx.com/
2.5、申请商户API证书
APIv3版本的所有接口都需要;APIv2版本的高级接口需要(如:退款、企业红包、企业付款等)
步骤:登录商户平台 => 选择 账户中心 => 安全中心 => API安全 => 申请API证书
2.6、获取微信平台证书
可以预先下载,也可以通过编程的方式获取。后面的课程中,我们会通过编程的方式来获取。
注意:以上所有API秘钥和证书需妥善保管防止泄露
这个过程怎么保证安全呢?
• 密钥的度量单位是位 bit,如,秘钥长度128,就是16字节的二进制串
• 按照密钥的使用方式,加密可以分为两大类:对称加密和非对称加密
• AES加密算法,密钥长度128、192或256,安全强度很高,性能很好
• 加密分组模式:将明文分组加密,微信支付中使用 AEAD_AES_256_GCM
• 使用公钥加密后只能用私钥解密,反过来,私钥加密后也只能用公钥解密
• RSA加密算法:最著名的非对称加密算法
对称加密
• 优点:运算速度快
• 缺点秘钥需要信息交换的双方共享, —旦被窃取, 消息会被破解
非对称加密
• 优点私钥严格保密, 公钥任意分发, 黑客获取公钥无法破解密文
• 缺点:运算速度非常慢
有人可能会问了:
那可不可以抛弃对称加密呢? 很遗憾,非对称加密是基于非常复杂得数学算法,因此呢它得运行速度非常慢,如果我们在互联网上传输信息的话,通信速度呢是无法保证的,所以一般情况下,如果要保证信息传输的安全性,一般呢都是对称加密和非对称加密相结合的
例如我们可以先用非对称加密的方式先传输对称加密需要的密钥,这样呢可以保证密钥被安全的传递,那后期的信息交换的过程呢就可以安全地使用对称加密进行了。这样既能保证对称加密需要地密钥呢不会再传输地过程当中被拦截被窃取,又能保证再后续地信息传输地过程当中加密和解密的效率,https的底层使用的就是这个原理。
下面是一个例子:
Bob有一对密钥,他给他的朋友们公钥。只有自己有私钥,Susan想给Bob写信,信的内容需要加密,那么她可以使用Bob的公钥将内容进行加密然后发送给Bob,Bob用自己的私钥解密查看。那如果Bob也行给Susan写一封加密的信,应该如何保证数据的安全呢?我们可以想到Susan就要也拥有自己的公钥和私钥然后将公钥分发给她的朋友们
如果私钥加密公钥解密有什么效果呢?
他的朋友们都可以知道这封信写了什么
所以这到底有什么效果呢?
朋友们可以确定这封信是Bob发出的,其实就是身份认证。
摘要算法和数据的完整性
那么如何保证信件不给篡改,即信息的完整性?
特点:
1.不可逆:只有算法,没有密钥,只能加密,不能解密
2.难题友好性:想要破解,只能暴力枚举
3.发散性:只要对原文进行一点点改动,摘要就会发生剧烈变化
4.抗碰撞性:原文不同,计算后的摘要也要不同
常见的摘要算法:MD5 ,SHA1,SHA2(SHA224,SHA256,SHA484) 生成xx字节的摘要
使用最多的时候SHA2
Bob发文章给Pat,Bob先用摘要算法生成一段哈希值,然后把这段哈希值附带在文章的下边。Pat收到后,他也用对应的摘要算法生成一段哈希值看看是否一样,如果一样证明没有被篡改过。但是如果过程中被黑客劫持,并且黑客直接修改了原文,并且根据原文生成了新的摘要放在原文的下面,伪装成Bob将信件发送给Pat那么Pat接收后呢是完全差距不出来的。这样还是没有办法鉴别出信息传输的完整性。如果保证无法被篡改呢?
答案是一定要加入密钥确保机密性
数字签名
过程是这样的:Bob先用摘要算法生成一段摘要,然后Bob用自己的私钥来生成签名(认证身份),附加在文章下方,Pat拿到信件之后先用Bob的公钥对签名进行解密得到摘要,然后用相应的摘要算法对文章进行摘要,比对两者是否一致如果一致的话那么信件就算Bob发出的并且没有篡改过。这个过程叫验证签名。
那么微信支付中的签名和验签的过程呢就是这个原理了
数字签名可以保证信息传递的过程中不可以被篡改以及信息传递者身份的认证。
数字证书
Doug想欺骗Pat,他把自己的公钥给了Pat但是跟她说这是Bob的公钥,当Doug用自己的私钥发送信件给Pat,Pat用Doug给的公钥发现可以解开,她会以为她正在跟Bob对话。
这里其实就是一个公钥信息的问题:黑客可以伪造公钥,怎么判断公钥是真实的?
解决这个问题的答案就是数字证书
• 公钥:Bob的公钥
• 所有者:Bob
• 颁发者:CA(Certificate Authority,证书认证机构)
• 有效期:证书的使用期限
• 签名哈希算法:指定摘要算法,用来计算证书的摘要
• 指纹:证书的摘要,保证证书的完整性
• 签名算法:用于生成签名,确保证书是由CA签发
序列号:证书的唯一标识
那么下面我们来看看数字证书的颁发过程
CA先用数字证书的信息用摘要算法生成一段摘要,然后用CA自己的密钥进行加密,生成签名,把这个签名放在数字证书后边,也就是对应的数字证书。
数字证书- https协议
还有一些情况比如数字证书过期或者数字证书被吊销或者是颁发数字证书的CA机构不是特别正规,那么这些情况我们的浏览器就会发出一些网站不安全的一些警告信息,如果数字证书是可靠的那么客户端呢就可以顺利地从证书中获取到网站地公钥了
二、支付安全(证书/秘钥/签名)
1、信息安全的基础 - 机密性
明文:加密前的消息叫“明文”(plain text)
密文:加密后的文本叫“密文”(cipher text)
密钥:只有掌握特殊“钥匙”的人,才能对加密的文本进行解密,这里的“钥匙”就叫做“密钥”(key)
“密钥”就是一个字符串,度量单位是“位”(bit),比如,密钥长度是 128,就是 16 字节的二
进制串
加密:实现机密性最常用的手段是“加密”(encrypt)
按照密钥的使用方式,加密可以分为两大类:对称加密和非对称加密。
解密:使用密钥还原明文的过程叫“解密”(decrypt)
加密算法:加密解密的操作过程就是“加密算法”
所有的加密算法都是公开的,而算法使用的“密钥”则必须保密
2、对称加密和非对称加密
对称加密
特点:只使用一个密钥,密钥必须保密,常用的有 AES算法
优点:运算速度快
缺点:秘钥需要信息交换的双方共享,一旦被窃取,消息会被破解,无法做到安全的密钥交
换
非对称加密
特点:使用两个密钥:公钥和私钥,公钥可以任意分发而私钥保密,常用的有 RSA
优点:黑客获取公钥无法破解密文,解决了密钥交换的问题
缺点:运算速度非常慢
混合加密
实际场景中把对称加密和非对称加密结合起来使用。
3、身份认证
公钥加密,私钥解密的作用是加密信息
私钥加密,公钥解密的作用是身份认证
4、摘要算法(Digest Algorithm)
摘要算法就是我们常说的散列函数、哈希函数(Hash Function),它能够把任意长度的数据“压缩”成
固定长度、而且独一无二的“摘要”字符串,就好像是给这段数据生成了一个数字“指纹”。
作用:
保证信息的完整性
特性:
不可逆:只有算法,没有秘钥,只能加密,不能解密
难题友好性:想要破解,只能暴力枚举
发散性:只要对原文进行一点点改动,摘要就会发生剧烈变化
抗碰撞性:原文不同,计算后的摘要也要不同
常见摘要算法:
MD5、SHA1、SHA2(SHA224、SHA256、SHA384)
5、数字签名
数字签名是使用私钥对摘要加密生成签名,需要由公钥将签名解密后进行验证,实现身份认证和不可否
认
签名和验证签名的流程:
6、数字证书
数字证书解决“公钥的信任”问题,可以防止黑客伪造公钥。
不能直接分发公钥,公钥的分发必须使用数字证书,数字证书由CA颁发
https协议中的数字证书:
在线微信支付案例
7.微信APIv3证书
商户证书:
商户API证书是指由商户申请的,包含商户的商户号、公司名称、公钥信息的证书。
商户证书在商户后台申请:https://pay.weixin.qq.com/index.php/core/cert/api_cert#/
平台证书(微信支付平台):
微信支付平台证书是指由微信支付 负责申请的,包含微信支付平台标识、公钥信息的证书。商户可以使
用平台证书中的公钥进行验签。
平台证书的获取:https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay3_0.shtml
8、API密钥和APIv3密钥
都是对称加密需要使用的加密和解密密钥,一定要保管好,不能泄露。
API密钥对应V2版本的API
APIv3密钥对应V3版本的API
1、创建SpringBoot项目
1.1、新建项目
注意:Java版本选择8
1.2、添加依赖
添加SpringBoot web依赖
1 2 3 4 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-web</artifactId > </dependency >
1.3、配置application.yml文件
1 2 3 4 5 server: port: 8090 spring: application: name: payment-demo
1.4、创建controller
创建controller包,创建ProductController类
1.5、测试
访问:http://localhost:8090/api/product/test
2、引入Swagger
作用:自动生成接口文档和测试页面。
2.1、引入依赖
2.2、Swagger配置文件
1 2 3 4 5 6 7 8 9 10 11 12 package com.yjy.paymentdemo.controller;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;@RestController @RequestMapping("/api/product") @CrossOrigin public class ProductController { @GetMapping("/test") public String test () { return "hello" ; } }
1.5、测试
访问:http://localhost:8090/api/product/test
2、引入Swagger
作用:自动生成接口文档和测试页面。
2.1、引入依赖
1 2 3 4 5 6 7 8 9 10 11 12 <dependency > <groupId > io.springfox</groupId > <artifactId > springfox-swagger2</artifactId > <version > 2.7.0</version > </dependency > <dependency > <groupId > io.springfox</groupId > <artifactId > springfox-swagger-ui</artifactId > <version > 2.7.0</version > </dependency >
2.2、Swagger配置文件
创建config包,创建Swagger2Config类
1 2 3 4 5 6 7 8 9 10 11 12 @Configuration @EnableSwagger2 public class Swagger2Config { @Bean public Docket docket () { return new Docket (DocumentationType.SWAGGER_2) .apiInfo(new ApiInfoBuilder ().title("微信支付案例接口文档" ).build()); } }
2.3、Swagger注解
controller中可以添加常用注解
1 2 @Api(tags="商品管理") @ApiOperation("测试接口")
2.4、测试
访问:http://localhost:8090/swagger-ui.html
3、定义统一结果
作用:定义统一响应结果,为前端返回标准格式的数据。
3.1、引入lombok依赖
简化实体类的开发
1 2 3 4 5 <dependency > <groupId > org.projectlombok</groupId > <artifactId > lombok</artifactId > </dependency >
3.2、创建R类
创建统一结果类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 @Data @Accessors(chain = true) public class R { private Integer code; private String message; private Map<String, Object> data = new HashMap <>(); public static R ok () { R r = new R (); r.setCode(0 ); r.setMessage("成功" ); return r; } public static R error () { R r = new R (); r.setCode(-1 ); r.setMessage("失败" ); return r; } public R data (String key, Object value) { this .data.put(key, value); return this ; } }
3.3、修改controller
修改test方法,返回统一结果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Api(tags = "测试控制器") @RestController @RequestMapping("/api/test") public class TestController { @Resource private WxPayConfig wxPayConfig; @GetMapping public R getWxPayConfig () { String mchId = wxPayConfig.getMchId(); return R.ok().data("mchId" , mchId); } }
3.4、配置json时间格式
1 2 3 4 5 6 7 spring: application: name: payment-demo jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8
3.5、Swagger测试
4、创建数据库
4.1、创建数据库
4.2、IDEA配置数据库连接
(1)打开数据库面板
payment_demo.sql
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 USE `payment_demo`; CREATE TABLE `t_order_info` ( `id` bigint (11 ) unsigned NOT NULL AUTO_INCREMENT COMMENT '订单id' , `title` varchar (256 ) DEFAULT NULL COMMENT '订单标题' , `order_no` varchar (50 ) DEFAULT NULL COMMENT '商户订单编号' , `user_id` bigint (20 ) DEFAULT NULL COMMENT '用户id' , `product_id` bigint (20 ) DEFAULT NULL COMMENT '支付产品id' , `total_fee` int (11 ) DEFAULT NULL COMMENT '订单金额(分)' , `code_url` varchar (50 ) DEFAULT NULL COMMENT '订单二维码连接' , `order_status` varchar (10 ) DEFAULT NULL COMMENT '订单状态' , `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间' , `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间' , PRIMARY KEY (`id`) ) ENGINE= InnoDB AUTO_INCREMENT= 1 DEFAULT CHARSET= utf8mb4; CREATE TABLE `t_payment_info` ( `id` bigint (20 ) unsigned NOT NULL AUTO_INCREMENT COMMENT '支付记录id' , `order_no` varchar (50 ) DEFAULT NULL COMMENT '商户订单编号' , `transaction_id` varchar (50 ) DEFAULT NULL COMMENT '支付系统交易编号' , `payment_type` varchar (20 ) DEFAULT NULL COMMENT '支付类型' , `trade_type` varchar (20 ) DEFAULT NULL COMMENT '交易类型' , `trade_state` varchar (50 ) DEFAULT NULL COMMENT '交易状态' , `payer_total` int (11 ) DEFAULT NULL COMMENT '支付金额(分)' , `content` text COMMENT '通知参数' , `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间' , `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间' , PRIMARY KEY (`id`) ) ENGINE= InnoDB AUTO_INCREMENT= 1 DEFAULT CHARSET= utf8mb4; CREATE TABLE `t_product` ( `id` bigint (20 ) NOT NULL AUTO_INCREMENT COMMENT '商品id' , `title` varchar (20 ) DEFAULT NULL COMMENT '商品名称' , `price` int (11 ) DEFAULT NULL COMMENT '价格(分)' , `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间' , `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间' , PRIMARY KEY (`id`) ) ENGINE= InnoDB AUTO_INCREMENT= 1 DEFAULT CHARSET= utf8mb4; insert into `t_product`(`title`,`price`) values ('Java课程' ,1 );insert into `t_product`(`title`,`price`) values ('大数据课程' ,1 );insert into `t_product`(`title`,`price`) values ('前端课程' ,1 );insert into `t_product`(`title`,`price`) values ('UI课程' ,1 );CREATE TABLE `t_refund_info` ( `id` bigint (20 ) unsigned NOT NULL AUTO_INCREMENT COMMENT '退款单id' , `order_no` varchar (50 ) DEFAULT NULL COMMENT '商户订单编号' , `refund_no` varchar (50 ) DEFAULT NULL COMMENT '商户退款单编号' , `refund_id` varchar (50 ) DEFAULT NULL COMMENT '支付系统退款单号' , `total_fee` int (11 ) DEFAULT NULL COMMENT '原订单金额(分)' , `refund` int (11 ) DEFAULT NULL COMMENT '退款金额(分)' , `reason` varchar (50 ) DEFAULT NULL COMMENT '退款原因' , `refund_status` varchar (10 ) DEFAULT NULL COMMENT '退款状态' , `content_return` text COMMENT '申请退款返回参数' , `content_notify` text COMMENT '退款结果通知参数' , `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间' , `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间' , PRIMARY KEY (`id`) ) ENGINE= InnoDB AUTO_INCREMENT= 1 DEFAULT CHARSET= utf8mb4;
5、集成MyBatis-Plus
5.1、引入依赖
1 2 3 4 5 6 7 8 9 10 11 12 13 <dependency > <groupId > mysql</groupId > <artifactId > mysql-connector-java</artifactId > </dependency > <dependency > <groupId > com.baomidou</groupId > <artifactId > mybatis-plus-boot-starter</artifactId > <version > 3.3.1</version > </dependency >
5.2、配置数据库连接
1 2 3 4 5 datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/payment_demo?serverTimezone=GMT%2B8&characterEncoding=utf-8 username: root password: root
5.3、定义实体类
BaseEntity是父类,其他类继承BaseEntity
1 2 3 4 5 6 7 8 9 10 11 12 @Data public class BaseEntity { @TableId(value = "id", type = IdType.AUTO) private String id; private Date createTime; private Date updateTime; }
5.4、定义持久层
定义Mapper接口继承 BaseMapper<>,
定义xml配置文件
5.5、定义MyBatis-Plus的配置文件
在config包中创建配置文件 MybatisPlusConfig
1 2 3 4 5 6 7 8 @Configuration @MapperScan("com.atguigu.paymentdemo.mapper") @EnableTransactionManagement public class MyBatisPlusConfig {}
5.6、定义yml配置文件
添加持久层日志和xml文件位置的配置
1 2 3 4 5 6 7 8 mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl mapper-locations: classpath:com/atguigu/paymentdemo/mapper/xml/*.xml logging: level: root: info
5.7、定义业务层
定义业务层接口继承 IService<>
定义业务层接口的实现类,并继承 ServiceImpl<,>
5.8、定义接口方法查询所有商品
在 public class ProductController 中添加一个方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 @CrossOrigin @Api(tags = "商品管理") @RestController @RequestMapping("/api/product") public class ProductController { @Resource private ProductService productService; @ApiOperation("测试接口") @GetMapping("/test") public R test () { return R.ok().data("message" , "hello" ).data("now" , new Date ()); } @ApiOperation("商品列表") @GetMapping("/list") public R list () { List<Product> list = productService.list(); return R.ok().data("productList" , list); } }
5.9、Swagger中测试
5.10、pom中配置build节点
因为maven工程在默认情况下 src/main/java 目录下的所有资源文件是不发布到 target 目录下的,我们
在 pom 文件的 节点下配置一个资源发布过滤器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 <build > <resources > <resource > <directory > src/main/java</directory > <includes > <include > **/*.yml</include > <include > **/*.properties</include > <include > **/*.xml</include > </includes > <filtering > false</filtering > </resource > <resource > <directory > src/main/resources</directory > <includes > <include > **/*.yml</include > <include > **/*.properties</include > <include > **/*.xml</include > </includes > <filtering > false</filtering > </resource > </resources >
6、搭建前端环境
6.1、安装Node.js
Node.js是一个基于JavaScript引擎的服务器端环境,前端项目在开发环境下要基于Node.js来运行
安装:node-v14.18.0-x64.msi
6.2、运行前端项目
将项目放在磁盘的一个目录中,例如 D:\demo\payment-demo-front
进入项目目录,运行下面的命令启动项目:
内容:
引入支付参数
加载商户私钥
获取平台证书和验签器
获取HttpClient对象
API字典和接口规则
内网穿透
API v3
wxpay.properties
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 wxpay.mch-id =1558950191 wxpay.mch-serial-no =34345964330B66427E0D3D28826C4993C77E631F wxpay.private-key-path =apiclient_key.pem wxpay.api-v3-key =UDuLFDcmy5Eb6o0nTNZdu6ek4DDh4K8B wxpay.appid =wx74862e0dfcf69954 wxpay.domain =https://api.mch.weixin.qq.com wxpay.notify-domain =https://fb8a-183-36-237-242.ngrok-free.app/ wxpay.partnerKey : T6m9iK73b0kn9g5v426MKfHQH7X8rKwb
商户的私钥文件加载到应用程序的目的是为了做签名,我们用私钥对我们得请求做签名,然后把我们得请求发送给微信得服务器端,微信得服务器端会根据商户API证书序列号找到这个序列号对应的证书,从证书当中解析出公钥,用公钥对我们得请求进行验签,这样就是请求和接收得过程,也是就签名和验签的过程。
1.2、读取支付参数
将资料文件夹中的 config 目录中的 WxPayConfig.java 复制到源码目录中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 @Configuration @PropertySource("classpath:wxpay.properties") @ConfigurationProperties(prefix="wxpay") @Data @Slf4j public class WxPayConfig { private String mchId; private String mchSerialNo; private String privateKeyPath; private String apiV3Key; private String appid; private String domain; private String notifyDomain; private String partnerKey; private PrivateKey getPrivateKey (String filename) { try { return PemUtil.loadPrivateKey(new FileInputStream (filename)); } catch (FileNotFoundException e) { throw new RuntimeException ("私钥文件不存在" , e); } } @Bean public ScheduledUpdateCertificatesVerifier getVerifier () { log.info("获取签名验证器" ); PrivateKey privateKey = getPrivateKey(privateKeyPath); PrivateKeySigner privateKeySigner = new PrivateKeySigner (mchSerialNo, privateKey); WechatPay2Credentials wechatPay2Credentials = new WechatPay2Credentials (mchId, privateKeySigner); ScheduledUpdateCertificatesVerifier verifier = new ScheduledUpdateCertificatesVerifier ( wechatPay2Credentials, apiV3Key.getBytes(StandardCharsets.UTF_8)); return verifier; } @Bean(name = "wxPayClient") public CloseableHttpClient getWxPayClient (ScheduledUpdateCertificatesVerifier verifier) { log.info("获取httpClient" ); PrivateKey privateKey = getPrivateKey(privateKeyPath); WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create() .withMerchant(mchId, mchSerialNo, privateKey) .withValidator(new WechatPay2Validator (verifier)); CloseableHttpClient httpClient = builder.build(); return httpClient; } @Bean(name = "wxPayNoSignClient") public CloseableHttpClient getWxPayNoSignClient () { PrivateKey privateKey = getPrivateKey(privateKeyPath); WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create() .withMerchant(mchId, mchSerialNo, privateKey) .withValidator((response) -> true ); CloseableHttpClient httpClient = builder.build(); log.info("== getWxPayNoSignClient END ==" ); return httpClient; } }
1.3、测试支付参数的获取
在 controller 包中创建 TestController
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Api(tags = "测试控制器") @RestController @RequestMapping("/api/test") public class TestController { @Resource private WxPayConfig wxPayConfig; @GetMapping public R getWxPayConfig () { String mchId = wxPayConfig.getMchId(); return R.ok().data("mchId" , mchId); } }
1.4、配置 Annotation Processor
可以帮助我们生成自定义配置的元数据信息,让配置文件和Java代码之间的对应参数可以自动定位,方
便开发。也就是我们可以从properties中点进去使用到的位置
1 2 3 4 5 6 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-configuration-processor</artifactId > <optional > true</optional > </dependency >
1.5、在IDEA中设置 SpringBoot 配置文件
让IDEA可以识别配置文件,将配置文件的图标展示成SpringBoot的图标,同时配置文件的内容可以高
亮显示
File -> Project Structure -> Modules -> 选择小叶子
点击(+) 图标
选中配置文件:
2、加载商户私钥
2.1、复制商户私钥
将下载的私钥文件复制到项目根目录下
2.2、引入SDK
https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay6_0.shtml
我们可以使用官方提供的 SDK,帮助我们完成开发。实现了请求签名的生成和应答签名的验证。
1 2 3 4 5 6 <dependency > <groupId > com.github.wechatpay-apiv3</groupId > <artifactId > wechatpay-apache-httpclient</artifactId > <version > 0.3.0</version > </dependency >
2.3、获取商户私钥
https://github.com/wechatpay-apiv3/wechatpay-apache-httpclient (如何加载商户私钥)
1 2 3 4 5 6 7 8 9 10 11 12 13 private PrivateKey getPrivateKey (String filename) { try { return PemUtil.loadPrivateKey(new FileInputStream (filename)); } catch (FileNotFoundException e) { throw new RuntimeException ("私钥文件不存在" , e); } }
2.4、测试商户私钥的获取
在 PaymentDemoApplicationTests 测试类中添加如下方法,测试私钥对象是否能够获取出来。
(将前面的方法改成public的再进行测试)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 @SpringBootTest class PaymentDemoApplicationTests { @Resource private WxPayConfig wxPayConfig; @Resource private CloseableHttpClient wxPayClient; }
3、获取签名验证器和HttpClient
3.1、证书密钥使用说明
https://pay.weixin.qq.com/wiki/doc/apiv3_partner/wechatpay/wechatpay3_0.shtml
对于我们编程来说要实现 M(密钥)计算签名和W(公钥)验证签名
我们之前引入的SDK可以帮助我们完成这个任务我们需要给一些参数给它,比如签名得给私钥等
3.2、获取签名验证器
https://github.com/wechatpay-apiv3/wechatpay-apache-httpclient (定时更新平台证书功能)
平台证书:平台证书封装了微信的公钥,商户可以使用平台证书中的公钥进行验签。
签名验证器:帮助我们进行验签工作,我们单独将它定义出来,方便后面的开发。
3.4、获取 HttpClient 对象
https://github.com/wechatpay-apiv3/wechatpay-apache-httpclient (定时更新平台证书功能)
HttpClient 对象:是建立远程连接的基础,我们通过SDK创建这个对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 @Bean public ScheduledUpdateCertificatesVerifier getVerifier () { log.info("获取签名验证器" ); PrivateKey privateKey = getPrivateKey(privateKeyPath); PrivateKeySigner privateKeySigner = new PrivateKeySigner (mchSerialNo, privateKey); WechatPay2Credentials wechatPay2Credentials = new WechatPay2Credentials (mchId, privateKeySigner); ScheduledUpdateCertificatesVerifier verifier = new ScheduledUpdateCertificatesVerifier ( wechatPay2Credentials, apiV3Key.getBytes(StandardCharsets.UTF_8)); return verifier; } @Bean(name = "wxPayClient") public CloseableHttpClient getWxPayClient (ScheduledUpdateCertificatesVerifier verifier) { log.info("获取httpClient" ); PrivateKey privateKey = getPrivateKey(privateKeyPath); WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create() .withMerchant(mchId, mchSerialNo, privateKey) .withValidator(new WechatPay2Validator (verifier)); CloseableHttpClient httpClient = builder.build(); return httpClient; } @Bean(name = "wxPayNoSignClient") public CloseableHttpClient getWxPayNoSignClient () { PrivateKey privateKey = getPrivateKey(privateKeyPath); WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create() .withMerchant(mchId, mchSerialNo, privateKey) .withValidator((response) -> true ); CloseableHttpClient httpClient = builder.build(); log.info("== getWxPayNoSignClient END ==" ); return httpClient; }
后续传输比较大的数据的时候需要使用对称加密
4、API字典和相关工具
4.1、API列表
https://pay.weixin.qq.com/wiki/doc/apiv3/open/pay/chapter2_7_3.shtml
我们的项目中要实现以下所有API的功能。
4.2、接口规则
Native下单_Native支付|微信支付合作伙伴文档中心
微信支付 APIv3 使用 JSON 作为消息体的数据交换格式。
1 2 3 4 5 <dependency > <groupId > com.google.code.gson</groupId > <artifactId > gson</artifactId > </dependency >
4.3、定义枚举
将资料文件夹中的 enums 目录复制到源码目录中。
为了开发方便,我们预先在项目中定义一些枚举。枚举中定义的内容包括接口地址,支付状态等信息。
在enums下面还有两个枚举类:
OrderStatus:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 @AllArgsConstructor @Getter public enum OrderStatus { NOTPAY("未支付" ), SUCCESS("支付成功" ), CLOSED("超时已关闭" ), CANCEL("用户已取消" ), REFUND_PROCESSING("退款中" ), REFUND_SUCCESS("已退款" ), REFUND_ABNORMAL("退款异常" ); private final String type; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @AllArgsConstructor @Getter public enum PayType { WXPAY("微信" ), ALIPAY("支付宝" ); private final String type; }
下面还有一个包wxpay
官网中有不同Api的地址:
1 2 3 4 5 6 Native下单 请求方式:【POST】/v3/pay/partner/transactions/native 商户订单号查询订单 请求方式:【GET】/v3/pay/partner/transactions/out-trade-no/{out_trade_no} ...
那么我们也开发了一个对应的枚举类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 @AllArgsConstructor @Getter public enum WxApiType { NATIVE_PAY("/v3/pay/transactions/native" ), NATIVE_PAY_V2("/pay/unifiedorder" ), ORDER_QUERY_BY_NO("/v3/pay/transactions/out-trade-no/%s" ), CLOSE_ORDER_BY_NO("/v3/pay/transactions/out-trade-no/%s/close" ), DOMESTIC_REFUNDS("/v3/refund/domestic/refunds" ), DOMESTIC_REFUNDS_QUERY("/v3/refund/domestic/refunds/%s" ), TRADE_BILLS("/v3/bill/tradebill" ), FUND_FLOW_BILLS("/v3/bill/fundflowbill" ); private final String type; }
还有几个通知,这几个通知需要我们在商户平台上进行开发
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 @AllArgsConstructor @Getter public enum WxNotifyType { NATIVE_NOTIFY("/api/wx-pay/native/notify" ), NATIVE_NOTIFY_V2("/api/wx-pay-v2/native/notify" ), REFUND_NOTIFY("/api/wx-pay/refunds/notify" ); private final String type; }
前面的主机地址我们在wxpay.properties中配置了,倒时候进行组装即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 @AllArgsConstructor @Getter public enum WxRefundStatus { SUCCESS("SUCCESS" ), CLOSED("CLOSED" ), PROCESSING("PROCESSING" ), ABNORMAL("ABNORMAL" ); private final String type; }
支付订单状态,这是我们得商户平台和微信会产生一笔要发起支付得支付交易,这是这个订单状态
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 @AllArgsConstructor @Getter public enum WxTradeState { SUCCESS("SUCCESS" ), NOTPAY("NOTPAY" ), CLOSED("CLOSED" ), REFUND("REFUND" ); private final String type; }
前面得OrderStatus是发生在用户和商户平台之间。
4.4、添加工具类
将资料文件夹中的 util 目录复制到源码目录中,我们将会使用这些辅助工具简化项目的开发
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 public class OrderNoUtils { public static String getOrderNo () { return "ORDER_" + getNo(); } public static String getRefundNo () { return "REFUND_" + getNo(); } public static String getNo () { SimpleDateFormat sdf = new SimpleDateFormat ("yyyyMMddHHmmss" ); String newDate = sdf.format(new Date ()); String result = "" ; Random random = new Random (); for (int i = 0 ; i < 3 ; i++) { result += random.nextInt(10 ); } return newDate + result; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 public class HttpUtils { public static String readData (HttpServletRequest request) { BufferedReader br = null ; try { StringBuilder result = new StringBuilder (); br = request.getReader(); for (String line; (line = br.readLine()) != null ; ) { if (result.length() > 0 ) { result.append("\n" ); } result.append(line); } return result.toString(); } catch (IOException e) { throw new RuntimeException (e); } finally { if (br != null ) { try { br.close(); } catch (IOException e) { e.printStackTrace(); } } } } }
5、Native下单API
5.1、Native支付流程
https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_4.shtml
5.2、Native下单API
https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_1.shtml
商户端发起支付请求,微信端创建支付订单并生成支付二维码链接,微信端将支付二维码返回给商户
端,商户端显示支付二维码,用户使用微信客户端扫码后发起支付。
(1)创建 WxPayController
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 @CrossOrigin @RestController @RequestMapping("/api/wx-pay") @Api(tags = "网站微信支付APIv3") @Slf4j public class WxPayController { @Resource private WxPayService wxPayService; @ApiOperation("调用统一下单API,生成支付二维码") @PostMapping("/native/{productId}") public R nativePay (@PathVariable Long productId) throws Exception { log.info("发起支付请求 v3" ); Map<String, Object> map = wxPayService.nativePay(productId); return R.ok().setData(map); }
(2)创建 WxPayService
接口
(4)定义WxPayService方法
参考:
API字典 -> 基础支付 -> Native支付 -> Native下单:
https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_1.shtml
指引文档 -> 基础支付 -> Native支付 -> 开发指引 ->【服务端】Native下单:
https://pay.weixin.qq.com/wiki/doc/apiv3/open/pay/chapter2_7_2.shtml
接口
实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 @Data @Accessors(chain = true) public class R { Map<String, Object> nativePay (Long productId) throws Exception; @Resource private WxPayConfig wxPayConfig; @Resource private CloseableHttpClient wxPayClient; } ======================================== @Override public Map<String, Object> nativePay (Long productId) throws Exception { log.info("生成订单" ); OrderInfo orderInfo = new OrderInfo (); orderInfo.setTitle("test" ); orderInfo.setOrderNo(OrderNoUtils.getOrderNo()); orderInfo.setProductId(productId); orderInfo.setTotalFee(1 ); orderInfo.setOrderStatus(OrderStatus.NOTPAY.getType()); log.info("调用统一下单API" ); HttpPost httpPost = new HttpPost (wxPayConfig.getDomain().concat(WxApiType.NATIVE_PAY.getType())); Gson gson = new Gson (); Map paramsMap = new HashMap (); paramsMap.put("appid" , wxPayConfig.getAppid()); paramsMap.put("mchid" , wxPayConfig.getMchId()); paramsMap.put("description" , orderInfo.getTitle()); paramsMap.put("out_trade_no" , orderInfo.getOrderNo()); paramsMap.put("notify_url" , wxPayConfig.getNotifyDomain().concat(WxNotifyType.NATIVE_NOTIFY.getType())); Map amountMap = new HashMap (); amountMap.put("total" , orderInfo.getTotalFee()); amountMap.put("currency" , "CNY" ); paramsMap.put("amount" , amountMap); String jsonParams = gson.toJson(paramsMap); log.info("请求参数:" + jsonParams); StringEntity entity = new StringEntity (jsonParams,"utf-8" ); entity.setContentType("application/json" ); httpPost.setEntity(entity); httpPost.setHeader("Accept" , "application/json" ); CloseableHttpResponse response = wxPayClient.execute(httpPost); try { String bodyAsString = EntityUtils.toString(response.getEntity()); int statusCode = response.getStatusLine().getStatusCode(); if (statusCode == 200 ) { log.info("成功, 返回结果 = " + bodyAsString); } else if (statusCode == 204 ) { log.info("成功" ); } else { log.info("Native下单失败,响应码 = " + statusCode+ ",返回结果 = " + bodyAsString); throw new IOException ("request failed" ); } Map<String, String> resultMap = gson.fromJson(bodyAsString, HashMap.class); String codeUrl = resultMap.get("code_url" ); Map<String, Object> map = new HashMap <>(); map.put("codeUrl" , codeUrl); map.put("orderNo" , orderInfo.getOrderNo()); return map; } finally { response.close(); } }
前端
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import request from '@/utils/request' export default { list ( ) { return request ({ url : '/api/product/list' , method : 'get' }) } }
request.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 import axios from 'axios' import { Message } from 'element-ui' const service = axios.create ({ baseURL : 'http://localhost:8090' , timeout : 20000 }) service.interceptors .request .use ( config => { return config }, error => { Promise .reject (error) } ) service.interceptors .response .use ( response => { const res = response.data if (res.code < 0 ) { Message ({ message : res.message , type : 'error' , duration : 5 * 1000 }) return Promise .reject ('error' ) } else { return response.data } }, error => { Message ({ message : error.message , type : 'error' , duration : 5 * 1000 }) return Promise .reject (error) } ) export default service
wxpay.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 import request from '@/utils/request' export default { nativePay (productId ) { return request ({ url : '/api/wx-pay/native/' + productId, method : 'post' }) }, nativePayV2 (productId ) { return request ({ url : '/api/wx-pay-v2/native/' + productId, method : 'post' }) }, cancel (orderNo ) { return request ({ url : '/api/wx-pay/cancel/' + orderNo, method : 'post' }) }, refunds (orderNo, reason ) { return request ({ url : '/api/wx-pay/refunds/' + orderNo + '/' + reason, method : 'post' }) } }
要引入二维码组件:qriously
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 <template> <div class="bg-fa of"> <section id="index" class="container"> <header class="comm-title"> <h2 class="fl tac"> <span class="c-333">课程列表</span> </h2> </header> <ul> <li v-for="product in productList" :key="product.id"> <a :class="['orderBtn', {current:payOrder.productId === product.id}]" @click="selectItem(product.id)" href="javascript:void(0);" > {{product.title}} ¥{{product.price / 100}} </a> </li> </ul> <div class="PaymentChannel_payment-channel-panel"> <h3 class="PaymentChannel_title"> 选择支付方式 </h3> <div class="PaymentChannel_channel-options" > <!-- 选择微信 --> <div :class="['ChannelOption_payment-channel-option', {current:payOrder.payType === 'wxpay'}]" @click="selectPayType('wxpay')"> <div class="ChannelOption_channel-icon"> <img src="../assets/img/wxpay.png" class="ChannelOption_icon"> </div> <div class="ChannelOption_channel-info"> <div class="ChannelOption_channel-label"> <div class="ChannelOption_label">微信支付</div> <div class="ChannelOption_sub-label"></div> <div class="ChannelOption_check-option"></div> </div> </div> </div> <!-- 选择支付宝 --> <div :class="['ChannelOption_payment-channel-option', {current:payOrder.payType === 'alipay'}]" @click="selectPayType('alipay')"> <div class="ChannelOption_channel-icon"> <img src="../assets/img/alipay.png" class="ChannelOption_icon"> </div> <div class="ChannelOption_channel-info"> <div class="ChannelOption_channel-label"> <div class="ChannelOption_label">支付宝</div> <div class="ChannelOption_sub-label"></div> <div class="ChannelOption_check-option"></div> </div> </div> </div> </div> </div> <div class="payButtom"> <el-button :disabled="payBtnDisabled" type="warning" round style="width: 180px;height: 44px;font-size: 18px;" @click="toPay()"> 确认支付V3 </el-button> <el-button :disabled="payBtnDisabled" type="warning" round style="width: 180px;height: 44px;font-size: 18px;" @click="toPayV2()"> 确认支付V2 </el-button> </div> </section> <!-- 微信支付二维码 --> <el-dialog :visible.sync="codeDialogVisible" :show-close="false" @close="closeDialog" width="350px" center> <qriously :value="codeUrl" :size="300"/> <!-- <img src="../assets/img/code.png" alt="" style="width:100%"><br> --> 使用微信扫码支付 </el-dialog> </div> </template> <script> import productApi from '../api/product' import wxPayApi from '../api/wxPay' import orderInfoApi from '../api/orderInfo' export default { data () { return { payBtnDisabled: false, //确认支付按钮是否禁用 codeDialogVisible: false, //微信支付二维码弹窗 productList: [], //商品列表 payOrder: { //订单信息 productId: '', //商品id payType: 'wxpay' //支付方式 }, codeUrl: '', // 二维码 orderNo: '', //订单号 timer: null // 定时器 } }, //页面加载时执行 created () { //获取商品列表 productApi.list().then(response => { this.productList = response.data.productList this.payOrder.productId = this.productList[0].id }) }, methods: { //选择商品 selectItem(productId) { console.log('商品id:' + productId) this.payOrder.productId = productId console.log(this.payOrder) //this.$router.push({ path: '/order' }) }, //选择支付方式 selectPayType(type) { console.log('支付方式:' + type) this.payOrder.payType = type //this.$router.push({ path: '/order' }) }, //确认支付 toPay(){ //禁用按钮,防止重复提交 this.payBtnDisabled = true //微信支付 if(this.payOrder.payType === 'wxpay'){ //调用统一下单接口 wxPayApi.nativePay(this.payOrder.productId).then(response => { this.codeUrl = response.data.codeUrl this.orderNo = response.data.orderNo //打开二维码弹窗 this.codeDialogVisible = true //启动定时器 this.timer = setInterval(() => { //查询订单是否支付成功 this.queryOrderStatus() }, 3000) }) } }, //确认支付 toPayV2(){ //禁用按钮,防止重复提交 this.payBtnDisabled = true //微信支付 if(this.payOrder.payType === 'wxpay'){ //调用统一下单接口 wxPayApi.nativePayV2(this.payOrder.productId).then(response => { this.codeUrl = response.data.codeUrl this.orderNo = response.data.orderNo //打开二维码弹窗 this.codeDialogVisible = true //启动定时器 this.timer = setInterval(() => { //查询订单是否支付成功 this.queryOrderStatus() }, 3000) }) } }, //关闭微信支付二维码对话框时让“确认支付”按钮可用 closeDialog(){ console.log('close.................') this.payBtnDisabled = false console.log('清除定时器') clearInterval(this.timer) }, // 查询订单状态 queryOrderStatus() { orderInfoApi.queryOrderStatus(this.orderNo).then(response => { console.log('查询订单状态:' + response.code) // 支付成功后的页面跳转 if (response.code === 0) { console.log('清除定时器') clearInterval(this.timer) // 三秒后跳转到订单列表 setTimeout(() => { this.$router.push({ path: '/success' }) }, 3000) } }) } } } </script>
5.3、签名和验签源码解析
(1)签名原理
开启debug日志
1 2 3 logging: level: root: info
签名生成流程:
https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay4_0.shtml
签名生成源码:
(2)验签原理
签名验证流程:
https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay4_1.shtml
签名验证源码:
获取平台证书:
签名验证的前提就是获取平台证书
源码: 在SDK中
验签:相应超时时间的情况
正常验签流程:
5.4、创建课程订单
(1)保存订单
OrderInfoService
接口:
1 OrderInfo createOrderByProductId (Long productId) ;
实现:
OrderInfoService.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 @Override public OrderInfo createOrderByProductId (Long productId) { OrderInfo orderInfo = this .getNoPayOrderByProductId(productId); if ( orderInfo != null ){ return orderInfo; } Product product = productMapper.selectById(productId); orderInfo = new OrderInfo (); orderInfo.setTitle(product.getTitle()); orderInfo.setOrderNo(OrderNoUtils.getOrderNo()); orderInfo.setProductId(productId); orderInfo.setTotalFee(product.getPrice()); orderInfo.setOrderStatus(OrderStatus.NOTPAY.getType()); baseMapper.insert(orderInfo); return orderInfo; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 private OrderInfo getNoPayOrderByProductId (Long productId) { QueryWrapper<OrderInfo> queryWrapper = new QueryWrapper <>(); queryWrapper.eq("product_id" , productId); queryWrapper.eq("order_status" , OrderStatus.NOTPAY.getType()); OrderInfo orderInfo = baseMapper.selectOne(queryWrapper); return orderInfo; }
修改:nativePay
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 @Transactional(rollbackFor = Exception.class) @Override public Map<String, Object> nativePay (Long productId) throws Exception { log.info("生成订单" ); OrderInfo orderInfo = orderInfoService.createOrderByProductId(productId); String codeUrl = orderInfo.getCodeUrl(); if (orderInfo != null && !StringUtils.isEmpty(codeUrl)){ log.info("订单已存在,二维码已保存" ); Map<String, Object> map = new HashMap <>(); map.put("codeUrl" , codeUrl); map.put("orderNo" , orderInfo.getOrderNo()); return map; } log.info("调用统一下单API" ); HttpPost httpPost = new HttpPost (wxPayConfig.getDomain().concat(WxApiType.NATIVE_PAY.getType())); Gson gson = new Gson (); Map paramsMap = new HashMap (); paramsMap.put("appid" , wxPayConfig.getAppid()); paramsMap.put("mchid" , wxPayConfig.getMchId()); paramsMap.put("description" , orderInfo.getTitle()); paramsMap.put("out_trade_no" , orderInfo.getOrderNo()); paramsMap.put("notify_url" , wxPayConfig.getNotifyDomain().concat(WxNotifyType.NATIVE_NOTIFY.getType())); Map amountMap = new HashMap (); amountMap.put("total" , orderInfo.getTotalFee()); amountMap.put("currency" , "CNY" ); paramsMap.put("amount" , amountMap); String jsonParams = gson.toJson(paramsMap); log.info("请求参数 ===> {}" + jsonParams); StringEntity entity = new StringEntity (jsonParams,"utf-8" ); entity.setContentType("application/json" ); httpPost.setEntity(entity); httpPost.setHeader("Accept" , "application/json" ); CloseableHttpResponse response = wxPayClient.execute(httpPost); try { String bodyAsString = EntityUtils.toString(response.getEntity()); int statusCode = response.getStatusLine().getStatusCode(); if (statusCode == 200 ) { log.info("成功, 返回结果 = " + bodyAsString); } else if (statusCode == 204 ) { log.info("成功" ); } else { log.info("Native下单失败,响应码 = " + statusCode+ ",返回结果 = " + bodyAsString); throw new IOException ("request failed" ); } Map<String, String> resultMap = gson.fromJson(bodyAsString, HashMap.class); codeUrl = resultMap.get("code_url" ); String orderNo = orderInfo.getOrderNo(); orderInfoService.saveCodeUrl(orderNo, codeUrl); Map<String, Object> map = new HashMap <>(); map.put("codeUrl" , codeUrl); map.put("orderNo" , orderInfo.getOrderNo()); return map; } finally { response.close(); } }
(2)缓存二维码
code_url 为两个小时
OrderInfoService
接口:
1 2 3 4 5 6 public interface OrderInfoService extends IService <OrderInfo> { OrderInfo createOrderByProductId (Long productId) ; void saveCodeUrl (String orderNo, String codeUrl) ; }
impl:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Override public void saveCodeUrl (String orderNo, String codeUrl) { QueryWrapper<OrderInfo> queryWrapper = new QueryWrapper <>(); queryWrapper.eq("order_no" , orderNo); OrderInfo orderInfo = new OrderInfo (); orderInfo.setCodeUrl(codeUrl); baseMapper.update(orderInfo, queryWrapper); }
在nativePay哪里保存
5.5、显示订单列表
在我的订单页面按时间倒序显示订单列表
(1)创建OrderInfoController
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @CrossOrigin @Api(tags = "商品订单管理") @RestController @RequestMapping("/api/order-info") public class OrderInfoController { @Resource private OrderInfoService orderInfoService; @ApiOperation("订单列表") @GetMapping("/list") public R list () { List<OrderInfo> list = orderInfoService.listOrderByCreateTimeDesc(); return R.ok().data("list" , list); }
1 List<OrderInfo> listOrderByCreateTimeDesc () ;
1 2 3 4 5 6 7 8 9 10 @Override public List<OrderInfo> listOrderByCreateTimeDesc () { QueryWrapper<OrderInfo> queryWrapper = new QueryWrapper <OrderInfo>().orderByDesc("create_time" ); return baseMapper.selectList(queryWrapper); }
6、支付通知API
6.1、内网穿透
(1)访问ngrok官网
也可以用cpolar
异步通知
6.2、接收通知和返回应答
支付通知API:https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_5.shtml
(2)设置通知地址
wxpay.properties
注意:每次重新启动ngrok,都需要根据实际情况修改这个配置
1 wxpay.notify-domain=https:
1 2 3 4 5 6 (3 )创建通知接口 通知规则:用户支付完成后,微信会把相关支付结果和用户信息发送给商户,商户需要接收处理 该消息,并返回应答。对后台通知交互时,如果微信收到商户的应答不符合规范或超时,微信认 为通知失败,微信会通过一定的策略定期重新发起通知,尽可能提高通知的成功率,但微信不保 证通知最终能成功。(通知频率为 15s/15s/30s/3m/10m/20m/30m/30m/30m/60m/3h/3h/3h/6h/6h - 总计 24h4m)
(4)测试失败应答
用失败应答替换成功应答
(5)测试超时应答
回调通知注意事项:https://pay.weixin.qq.com/wiki/doc/apiv3/Practices/chapter1_1_5.shtml
商户系统收到支付结果通知,需要在5秒内返回应答报文,否则微信支付认为通知失败,后续会
重复发送通知。
6.3、验签
(1)工具类
参考SDK源码中的 WechatPay2Validator 创建通知验签工具类 WechatPay2ValidatorForRequest
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 package com.yjy.paymentdemo.util;import com.wechat.pay.contrib.apache.httpclient.auth.Verifier;import org.apache.http.HttpEntity;import org.apache.http.client.methods.CloseableHttpResponse;import org.apache.http.util.EntityUtils;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import javax.servlet.http.HttpServletRequest;import java.io.IOException;import java.nio.charset.StandardCharsets;import java.time.DateTimeException;import java.time.Duration;import java.time.Instant;import static com.wechat.pay.contrib.apache.httpclient.constant.WechatPayHttpHeaders.*;public class WechatPay2ValidatorForRequest { protected static final Logger log = LoggerFactory.getLogger(WechatPay2ValidatorForRequest.class); protected static final long RESPONSE_EXPIRED_MINUTES = 5 ; protected final Verifier verifier; protected final String requestId; protected final String body; public WechatPay2ValidatorForRequest (Verifier verifier, String requestId, String body) { this .verifier = verifier; this .requestId = requestId; this .body = body; } protected static IllegalArgumentException parameterError (String message, Object... args) { message = String.format(message, args); return new IllegalArgumentException ("parameter error: " + message); } protected static IllegalArgumentException verifyFail (String message, Object... args) { message = String.format(message, args); return new IllegalArgumentException ("signature verify fail: " + message); } public final boolean validate (HttpServletRequest request) throws IOException { try { validateParameters(request); String message = buildMessage(request); String serial = request.getHeader(WECHAT_PAY_SERIAL); String signature = request.getHeader(WECHAT_PAY_SIGNATURE); if (!verifier.verify(serial, message.getBytes(StandardCharsets.UTF_8), signature)) { throw verifyFail("serial=[%s] message=[%s] sign=[%s], request-id=[%s]" , serial, message, signature, requestId); } } catch (IllegalArgumentException e) { log.warn(e.getMessage()); return false ; } return true ; } protected final void validateParameters (HttpServletRequest request) { String[] headers = {WECHAT_PAY_SERIAL, WECHAT_PAY_SIGNATURE, WECHAT_PAY_NONCE, WECHAT_PAY_TIMESTAMP}; String header = null ; for (String headerName : headers) { header = request.getHeader(headerName); if (header == null ) { throw parameterError("empty [%s], request-id=[%s]" , headerName, requestId); } } String timestampStr = header; try { Instant responseTime = Instant.ofEpochSecond(Long.parseLong(timestampStr)); if (Duration.between(responseTime, Instant.now()).abs().toMinutes() >= RESPONSE_EXPIRED_MINUTES) { throw parameterError("timestamp=[%s] expires, request-id=[%s]" , timestampStr, requestId); } } catch (DateTimeException | NumberFormatException e) { throw parameterError("invalid timestamp=[%s], request-id=[%s]" , timestampStr, requestId); } } protected final String buildMessage (HttpServletRequest request) throws IOException { String timestamp = request.getHeader(WECHAT_PAY_TIMESTAMP); String nonce = request.getHeader(WECHAT_PAY_NONCE); return timestamp + "\n" + nonce + "\n" + body + "\n" ; } protected final String getResponseBody (CloseableHttpResponse response) throws IOException { HttpEntity entity = response.getEntity(); return (entity != null && entity.isRepeatable()) ? EntityUtils.toString(entity) : "" ; } }
(2)验签
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 @Resource private Verifier verifier; @ApiOperation("支付通知") @PostMapping("/native/notify") public String nativeNotify (HttpServletRequest request, HttpServletResponse response) { Gson gson = new Gson (); Map<String, String> map = new HashMap <>(); try { String body = HttpUtils.readData(request); Map<String, Object> bodyMap = gson.fromJson(body, HashMap.class); String requestId = (String)bodyMap.get("id" ); log.info("支付通知的id ===> {}" , requestId); WechatPay2ValidatorForRequest wechatPay2ValidatorForRequest = new WechatPay2ValidatorForRequest (verifier, requestId, body); if (!wechatPay2ValidatorForRequest.validate(request)){ log.error("通知验签失败" ); response.setStatus(500 ); map.put("code" , "ERROR" ); map.put("message" , "通知验签失败" ); return gson.toJson(map); } log.info("通知验签成功" ); wxPayService.processOrder(bodyMap); TimeUnit.SECONDS.sleep(5 ); response.setStatus(200 ); map.put("code" , "SUCCESS" ); map.put("message" , "成功" ); return gson.toJson(map); } catch (Exception e) { e.printStackTrace(); response.setStatus(500 ); map.put("code" , "ERROR" ); map.put("message" , "失败" ); return gson.toJson(map); } }
wxpayService
1 void processOrder (Map<String, Object> bodyMap) throws GeneralSecurityException;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 private final ReentrantLock lock = new ReentrantLock ();@Transactional(rollbackFor = Exception.class) @Override public void processOrder (Map<String, Object> bodyMap) throws GeneralSecurityException { log.info("处理订单" ); String plainText = decryptFromResource(bodyMap); Gson gson = new Gson (); HashMap plainTextMap = gson.fromJson(plainText, HashMap.class); String orderNo = (String)plainTextMap.get("out_trade_no" ); if (lock.tryLock()){ try { String orderStatus = orderInfoService.getOrderStatus(orderNo); if (!OrderStatus.NOTPAY.getType().equals(orderStatus)){ return ; } try { TimeUnit.SECONDS.sleep(5 ); } catch (InterruptedException e) { e.printStackTrace(); } orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.SUCCESS); paymentInfoService.createPaymentInfo(plainText); } finally { lock.unlock(); } } }
6.4、解密
报文解密:
(1)WxPayController
nativeNotify 方法中添加处理订单的代码
1 2 wxPayService.processOrder(bodyMap);
(1)WxPayService
接口:
1 void processOrder (Map<String, Object> bodyMap) throws GeneralSecurityException;
实现:
1 2 3 4 5 6 7 8 9 @Override public void processOrder (Map<String, Object> bodyMap) throws GeneralSecurityException { log.info("处理订单" ); String plainText = decryptFromResource(bodyMap);}
辅助方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 private String decryptFromResource (Map<String, Object> bodyMap) throws GeneralSecurityException { log.info("密文解密" ); Map<String, String> resourceMap = (Map) bodyMap.get("resource" ); String ciphertext = resourceMap.get("ciphertext" ); String nonce = resourceMap.get("nonce" ); String associatedData = resourceMap.get("associated_data" ); log.info("密文 ===> {}" , ciphertext); AesUtil aesUtil = new AesUtil (wxPayConfig.getApiV3Key().getBytes(StandardCharsets.UTF_8)); String plainText = aesUtil.decryptToString(associatedData.getBytes(StandardCharsets.UTF_8), nonce.getBytes(StandardCharsets.UTF_8), ciphertext); log.info("明文 ===> {}" , plainText); return plainText; }
6.5、处理订单
(1)完善processOrder方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Resource private PaymentInfoService paymentInfoService;@Override public void processOrder (Map<String, Object> bodyMap) throws GeneralSecurityException { log.info("处理订单" ); String plainText = decryptFromResource(bodyMap);Gson gson = new Gson ();Map<String, Object> plainTextMap = gson.fromJson(plainText, HashMap.class); String orderNo = (String)plainTextMap.get("out_trade_no" );orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.SUCCESS); paymentInfoService.createPaymentInfo(plainText); }
(2)更新订单状态
OrderInfoService
接口:
1 void updateStatusByOrderNo (String orderNo, OrderStatus orderStatus) ;
实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @Override public void updateStatusByOrderNo (String orderNo, OrderStatus orderStatus) { log.info("更新订单状态 ===> {}" , orderStatus.getType()); QueryWrapper<OrderInfo> queryWrapper = new QueryWrapper <>(); queryWrapper.eq("order_no" , orderNo); OrderInfo orderInfo = new OrderInfo (); orderInfo.setOrderStatus(orderStatus.getType()); baseMapper.update(orderInfo, queryWrapper); }
(3)处理支付日志
PaymentInfoService
接口:
1 2 3 4 5 public interface PaymentInfoService { void createPaymentInfo (String plainText) ; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 @Service @Slf4j public class PaymentInfoServiceImpl extends ServiceImpl <PaymentInfoMapper, PaymentInfo> implements PaymentInfoService { @Override public void createPaymentInfo (String plainText) { log.info("记录支付日志" ); Gson gson = new Gson (); HashMap plainTextMap = gson.fromJson(plainText, HashMap.class); String orderNo = (String)plainTextMap.get("out_trade_no" ); String transactionId = (String)plainTextMap.get("transaction_id" ); String tradeType = (String)plainTextMap.get("trade_type" ); String tradeState = (String)plainTextMap.get("trade_state" ); Map<String, Object> amount = (Map)plainTextMap.get("amount" ); Integer payerTotal = ((Double) amount.get("payer_total" )).intValue(); PaymentInfo paymentInfo = new PaymentInfo (); paymentInfo.setOrderNo(orderNo); paymentInfo.setPaymentType(PayType.WXPAY.getType()); paymentInfo.setTransactionId(transactionId); paymentInfo.setTradeType(tradeType); paymentInfo.setTradeState(tradeState); paymentInfo.setPayerTotal(payerTotal); paymentInfo.setContent(plainText); baseMapper.insert(paymentInfo); } }
6.6、处理重复通知
(1)测试重复的通知
1 2 3 4 TimeUnit.SECONDS.sleep(5 );
(2)处理重复通知
在 processOrder 方法中,更新订单状态之前,添加如下代码
1 2 3 4 5 6 String orderStatus = orderInfoService.getOrderStatus(orderNo);if (!OrderStatus.NOTPAY.getType().equals(orderStatus)) {return ;}
OrderInfoService
接口:
1 String getOrderStatus (String orderNo) ;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Override public String getOrderStatus (String orderNo) { QueryWrapper<OrderInfo> queryWrapper = new QueryWrapper <>(); queryWrapper.eq("order_no" , orderNo); OrderInfo orderInfo = baseMapper.selectOne(queryWrapper); if (orderInfo == null ){ return null ; } return orderInfo.getOrderStatus(); }
6.7、数据锁
(1)测试通知并发
1 2 3 4 5 6 7 8 9 try {TimeUnit.SECONDS.sleep(5 ); } catch (InterruptedException e) { e.printStackTrace(); }
(2)定义ReentrantLock
定义 ReentrantLock 进行并发控制。注意,必须手动释放锁。
1 private final ReentrantLock lock = new ReentrantLock ();
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 @Transactional(rollbackFor = Exception.class) @Override public void processOrder (Map<String, Object> bodyMap) throws GeneralSecurityException { log.info("处理订单" ); String plainText = decryptFromResource(bodyMap); Gson gson = new Gson (); HashMap plainTextMap = gson.fromJson(plainText, HashMap.class); String orderNo = (String)plainTextMap.get("out_trade_no" ); if (lock.tryLock()){ try { String orderStatus = orderInfoService.getOrderStatus(orderNo); if (!OrderStatus.NOTPAY.getType().equals(orderStatus)){ return ; } try { TimeUnit.SECONDS.sleep(5 ); } catch (InterruptedException e) { e.printStackTrace(); } orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.SUCCESS); paymentInfoService.createPaymentInfo(plainText); } finally { lock.unlock(); } } }
打开支付二维码后用户如何判断是否支付成功了?如果没有成功就停留,如果成功应该告诉他成功啦!
所以需要定时查询
7、商户定时查询本地订单
7.1、后端定义商户查单接口
支付成功后,商户侧查询本地数据库,订单是否支付成功
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 @CrossOrigin @Api(tags = "商品订单管理") @RestController @RequestMapping("/api/order-info") public class OrderInfoController { @Resource private OrderInfoService orderInfoService; @ApiOperation("订单列表") @GetMapping("/list") public R list () { List<OrderInfo> list = orderInfoService.listOrderByCreateTimeDesc(); return R.ok().data("list" , list); } @ApiOperation("查询本地订单状态") @GetMapping("/query-order-status/{orderNo}") public R queryOrderStatus (@PathVariable String orderNo) { String orderStatus = orderInfoService.getOrderStatus(orderNo); if (OrderStatus.SUCCESS.getType().equals(orderStatus)){ return R.ok().setMessage("支付成功" ); } return R.ok().setCode(101 ).setMessage("支付中......" ); } }
前端:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 toPayV2 ( ){ this .payBtnDisabled = true if (this .payOrder .payType === 'wxpay' ){ wxPayApi.nativePayV2 (this .payOrder .productId ).then (response => { this .codeUrl = response.data .codeUrl this .orderNo = response.data .orderNo this .codeDialogVisible = true this .timer = setInterval (() => { this .queryOrderStatus () }, 3000 ) }) } }, closeDialog ( ){ console .log ('close.................' ) this .payBtnDisabled = false console .log ('清除定时器' ) clearInterval (this .timer ) },
8、用户取消订单API
实现用户主动取消订单的功能
8.1、定义取消订单接口
WxPayController中添加接口方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @ApiOperation("用户取消订单") @PostMapping("/cancel/{orderNo}") public R cancel (@PathVariable String orderNo) throws Exception { log.info("取消订单" ); wxPayService.cancelOrder(orderNo); return R.ok().setMessage("订单已取消" ); }
1 2 3 4 5 6 7 8 9 10 11 12 13 @Override public void cancelOrder (String orderNo) throws Exception { this .closeOrder(orderNo); orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.CANCEL); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 private void closeOrder (String orderNo) throws Exception { log.info("关单接口的调用,订单号 ===> {}" , orderNo); String url = String.format(WxApiType.CLOSE_ORDER_BY_NO.getType(), orderNo); url = wxPayConfig.getDomain().concat(url); HttpPost httpPost = new HttpPost (url); Gson gson = new Gson (); Map<String, String> paramsMap = new HashMap <>(); paramsMap.put("mchid" , wxPayConfig.getMchId()); String jsonParams = gson.toJson(paramsMap); log.info("请求参数 ===> {}" , jsonParams); StringEntity entity = new StringEntity (jsonParams,"utf-8" ); entity.setContentType("application/json" ); httpPost.setEntity(entity); httpPost.setHeader("Accept" , "application/json" ); CloseableHttpResponse response = wxPayClient.execute(httpPost); try { int statusCode = response.getStatusLine().getStatusCode(); if (statusCode == 200 ) { log.info("成功200" ); } else if (statusCode == 204 ) { log.info("成功204" ); } else { log.info("Native下单失败,响应码 = " + statusCode); throw new IOException ("request failed" ); } } finally { response.close(); } }
9、微信支付查单API
9.1、查单接口的调用
商户后台未收到异步支付结果通知时,商户应该主动调用《微信支付查单接口》商户订单号查询订单_Native支付|微信支付合作伙伴文档中心 ,同步订单状态。
(1)WxPayController
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @ApiOperation("查询订单:测试订单状态用") @GetMapping("/query/{orderNo}") public R queryOrder (@PathVariable String orderNo) throws Exception { log.info("查询订单" ); String result = wxPayService.queryOrder(orderNo); return R.ok().setMessage("查询成功" ).data("result" , result); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 @Override public String queryOrder (String orderNo) throws Exception { log.info("查单接口调用 ===> {}" , orderNo); String url = String.format(WxApiType.ORDER_QUERY_BY_NO.getType(), orderNo); url = wxPayConfig.getDomain().concat(url).concat("?mchid=" ).concat(wxPayConfig.getMchId()); HttpGet httpGet = new HttpGet (url); httpGet.setHeader("Accept" , "application/json" ); CloseableHttpResponse response = wxPayClient.execute(httpGet); try { String bodyAsString = EntityUtils.toString(response.getEntity()); int statusCode = response.getStatusLine().getStatusCode(); if (statusCode == 200 ) { log.info("成功, 返回结果 = " + bodyAsString); } else if (statusCode == 204 ) { log.info("成功" ); } else { log.info("查单接口调用,响应码 = " + statusCode+ ",返回结果 = " + bodyAsString); throw new IOException ("request failed" ); } return bodyAsString; } finally { response.close(); } }
9.2、集成Spring Task
Spring 3.0后提供Spring Task实现任务调度
(1)启动类添加注解
statistics启动类添加注解
(2)测试定时任务
创建 task 包,创建 WxPayTask.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 @Slf4j @Component public class WxPayTask { @Resource private OrderInfoService orderInfoService; @Resource private WxPayService wxPayService; @Resource private RefundInfoService refundInfoService; public void task1 () { log.info("task1 被执行......" ); }
9.3、定时查找超时订单
(1)WxPayTask
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Scheduled(cron = "0/30 * * * * ?") public void orderConfirm () throws Exception { log.info("orderConfirm 被执行......" ); List<OrderInfo> orderInfoList = orderInfoService.getNoPayOrderByDuration(1 ); for (OrderInfo orderInfo : orderInfoList) { String orderNo = orderInfo.getOrderNo(); log.warn("超时订单 ===> {}" , orderNo); wxPayService.checkOrderStatus(orderNo); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @Override public List<OrderInfo> getNoPayOrderByDuration (int minutes) { Instant instant = Instant.now().minus(Duration.ofMinutes(minutes)); QueryWrapper<OrderInfo> queryWrapper = new QueryWrapper <>(); queryWrapper.eq("order_status" , OrderStatus.NOTPAY.getType()); queryWrapper.le("create_time" , instant); List<OrderInfo> orderInfoList = baseMapper.selectList(queryWrapper); return orderInfoList; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 @Transactional(rollbackFor = Exception.class) @Override public void checkOrderStatus (String orderNo) throws Exception { log.warn("根据订单号核实订单状态 ===> {}" , orderNo); String result = this .queryOrder(orderNo); Gson gson = new Gson (); Map<String, String> resultMap = gson.fromJson(result, HashMap.class); String tradeState = resultMap.get("trade_state" ); if (WxTradeState.SUCCESS.getType().equals(tradeState)){ log.warn("核实订单已支付 ===> {}" , orderNo); orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.SUCCESS); paymentInfoService.createPaymentInfo(result); } if (WxTradeState.NOTPAY.getType().equals(tradeState)){ log.warn("核实订单未支付 ===> {}" , orderNo); this .closeOrder(orderNo); orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.CLOSED); } }
11、申请退款API
文档:https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_9.shtml
11.1、创建退款单
(1)根据订单号查询订单
OrderInfoService
1 2 3 4 5 6 7 8 @ApiOperation("申请退款") @PostMapping("/refunds/{orderNo}/{reason}") public R refunds (@PathVariable String orderNo, @PathVariable String reason) throws Exception { log.info("申请退款" ); wxPayService.refund(orderNo, reason); return R.ok(); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @Data @TableName("t_refund_info") public class RefundInfo extends BaseEntity { private String orderNo; private String refundNo; private String refundId; private Integer totalFee; private Integer refund; private String reason; private String refundStatus; private String contentReturn; private String contentNotify; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 @Transactional(rollbackFor = Exception.class) @Override public void refund (String orderNo, String reason) throws Exception { log.info("创建退款单记录" ); RefundInfo refundsInfo = refundsInfoService.createRefundByOrderNo(orderNo, reason); log.info("调用退款API" ); String url = wxPayConfig.getDomain().concat(WxApiType.DOMESTIC_REFUNDS.getType()); HttpPost httpPost = new HttpPost (url); Gson gson = new Gson (); Map paramsMap = new HashMap (); paramsMap.put("out_trade_no" , orderNo); paramsMap.put("out_refund_no" , refundsInfo.getRefundNo()); paramsMap.put("reason" ,reason); paramsMap.put("notify_url" , wxPayConfig.getNotifyDomain().concat(WxNotifyType.REFUND_NOTIFY.getType())); Map amountMap = new HashMap (); amountMap.put("refund" , refundsInfo.getRefund()); amountMap.put("total" , refundsInfo.getTotalFee()); amountMap.put("currency" , "CNY" ); paramsMap.put("amount" , amountMap); String jsonParams = gson.toJson(paramsMap); log.info("请求参数 ===> {}" + jsonParams); StringEntity entity = new StringEntity (jsonParams,"utf-8" ); entity.setContentType("application/json" ); httpPost.setEntity(entity); httpPost.setHeader("Accept" , "application/json" ); CloseableHttpResponse response = wxPayClient.execute(httpPost); try { String bodyAsString = EntityUtils.toString(response.getEntity()); int statusCode = response.getStatusLine().getStatusCode(); if (statusCode == 200 ) { log.info("成功, 退款返回结果 = " + bodyAsString); } else if (statusCode == 204 ) { log.info("成功" ); } else { throw new RuntimeException ("退款异常, 响应码 = " + statusCode+ ", 退款返回结果 = " + bodyAsString); } orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.REFUND_PROCESSING); refundsInfoService.updateRefund(bodyAsString); } finally { response.close(); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 @Override public void updateRefund (String content) { Gson gson = new Gson (); Map<String, String> resultMap = gson.fromJson(content, HashMap.class); QueryWrapper<RefundInfo> queryWrapper = new QueryWrapper <>(); queryWrapper.eq("refund_no" , resultMap.get("out_refund_no" )); RefundInfo refundInfo = new RefundInfo (); refundInfo.setRefundId(resultMap.get("refund_id" )); if (resultMap.get("status" ) != null ){ refundInfo.setRefundStatus(resultMap.get("status" )); refundInfo.setContentReturn(content); } if (resultMap.get("refund_status" ) != null ){ refundInfo.setRefundStatus(resultMap.get("refund_status" )); refundInfo.setContentNotify(content); } baseMapper.update(refundInfo, queryWrapper); }
11.3、申请退款
(1)WxPayController
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @ApiOperation("查询退款:测试用") @GetMapping("/query-refund/{refundNo}") public R queryRefund (@PathVariable String refundNo) throws Exception { log.info("查询退款" ); String result = wxPayService.queryRefund(refundNo); return R.ok().setMessage("查询成功" ).data("result" , result); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 @Override public String queryRefund (String refundNo) throws Exception { log.info("查询退款接口调用 ===> {}" , refundNo); String url = String.format(WxApiType.DOMESTIC_REFUNDS_QUERY.getType(), refundNo); url = wxPayConfig.getDomain().concat(url); HttpGet httpGet = new HttpGet (url); httpGet.setHeader("Accept" , "application/json" ); CloseableHttpResponse response = wxPayClient.execute(httpGet); try { String bodyAsString = EntityUtils.toString(response.getEntity()); int statusCode = response.getStatusLine().getStatusCode(); if (statusCode == 200 ) { log.info("成功, 查询退款返回结果 = " + bodyAsString); } else if (statusCode == 204 ) { log.info("成功" ); } else { throw new RuntimeException ("查询退款异常, 响应码 = " + statusCode+ ", 查询退款返回结果 = " + bodyAsString); } return bodyAsString; } finally { response.close(); } }
退款结果通知
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 @ApiOperation("退款结果通知") @PostMapping("/refunds/notify") public String refundsNotify (HttpServletRequest request, HttpServletResponse response) { log.info("退款通知执行" ); Gson gson = new Gson (); Map<String, String> map = new HashMap <>(); try { String body = HttpUtils.readData(request); Map<String, Object> bodyMap = gson.fromJson(body, HashMap.class); String requestId = (String)bodyMap.get("id" ); log.info("支付通知的id ===> {}" , requestId); WechatPay2ValidatorForRequest wechatPay2ValidatorForRequest = new WechatPay2ValidatorForRequest (verifier, requestId, body); if (!wechatPay2ValidatorForRequest.validate(request)){ log.error("通知验签失败" ); response.setStatus(500 ); map.put("code" , "ERROR" ); map.put("message" , "通知验签失败" ); return gson.toJson(map); } log.info("通知验签成功" ); wxPayService.processRefund(bodyMap); response.setStatus(200 ); map.put("code" , "SUCCESS" ); map.put("message" , "成功" ); return gson.toJson(map); } catch (Exception e) { e.printStackTrace(); response.setStatus(500 ); map.put("code" , "ERROR" ); map.put("message" , "失败" ); return gson.toJson(map); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 @Transactional(rollbackFor = Exception.class) @Override public void processRefund (Map<String, Object> bodyMap) throws Exception { log.info("退款单" ); String plainText = decryptFromResource(bodyMap); Gson gson = new Gson (); HashMap plainTextMap = gson.fromJson(plainText, HashMap.class); String orderNo = (String)plainTextMap.get("out_trade_no" ); if (lock.tryLock()){ try { String orderStatus = orderInfoService.getOrderStatus(orderNo); if (!OrderStatus.REFUND_PROCESSING.getType().equals(orderStatus)) { return ; } orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.REFUND_SUCCESS); refundsInfoService.updateRefund(plainText); } finally { lock.unlock(); } } }
12.2、定时查找退款中的订单
(1)WxPayTask
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @Scheduled(cron = "0/30 * * * * ?") public void refundConfirm () throws Exception { log.info("refundConfirm 被执行......" ); List<RefundInfo> refundInfoList = refundInfoService.getNoRefundOrderByDuration(1 ); for (RefundInfo refundInfo : refundInfoList) { String refundNo = refundInfo.getRefundNo(); log.warn("超时未退款的退款单号 ===> {}" , refundNo); wxPayService.checkRefundStatus(refundNo); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 @Transactional(rollbackFor = Exception.class) @Override public void checkRefundStatus (String refundNo) throws Exception { log.warn("根据退款单号核实退款单状态 ===> {}" , refundNo); String result = this .queryRefund(refundNo); Gson gson = new Gson (); Map<String, String> resultMap = gson.fromJson(result, HashMap.class); String status = resultMap.get("status" ); String orderNo = resultMap.get("out_trade_no" ); if (WxRefundStatus.SUCCESS.getType().equals(status)) { log.warn("核实订单已退款成功 ===> {}" , refundNo); orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.REFUND_SUCCESS); refundsInfoService.updateRefund(result); } if (WxRefundStatus.ABNORMAL.getType().equals(status)) { log.warn("核实订单退款异常 ===> {}" , refundNo); orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.REFUND_ABNORMAL); refundsInfoService.updateRefund(result); } }
12.3、处理超时未退款订单
WxPayService
核实订单状态
接口:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 @Transactional(rollbackFor = Exception.class) @Override public void checkRefundStatus (String refundNo) throws Exception { log.warn("根据退款单号核实退款单状态 ===> {}" , refundNo); String result = this .queryRefund(refundNo); Gson gson = new Gson (); Map<String, String> resultMap = gson.fromJson(result, HashMap.class); String status = resultMap.get("status" ); String orderNo = resultMap.get("out_trade_no" ); if (WxRefundStatus.SUCCESS.getType().equals(status)) { log.warn("核实订单已退款成功 ===> {}" , refundNo); orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.REFUND_SUCCESS); refundsInfoService.updateRefund(result); } if (WxRefundStatus.ABNORMAL.getType().equals(status)) { log.warn("核实订单退款异常 ===> {}" , refundNo); orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.REFUND_ABNORMAL); refundsInfoService.updateRefund(result); } }
14、账单
14.1、申请交易账单和资金账单
(1)WxPayController
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 @ApiOperation("获取账单url:测试用") @GetMapping("/querybill/{billDate}/{type}") public R queryTradeBill ( @PathVariable String billDate, @PathVariable String type) throws Exception { log.info("获取账单url" ); String downloadUrl = wxPayService.queryBill(billDate, type); return R.ok().setMessage("获取账单url成功" ).data("downloadUrl" , downloadUrl); } @ApiOperation("下载账单") @GetMapping("/downloadbill/{billDate}/{type}") public R downloadBill ( @PathVariable String billDate, @PathVariable String type) throws Exception { log.info("下载账单" ); String result = wxPayService.downloadBill(billDate, type); return R.ok().data("result" , result); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 @Override public String queryBill (String billDate, String type) throws Exception { log.warn("申请账单接口调用 {}" , billDate); String url = "" ; if ("tradebill" .equals(type)){ url = WxApiType.TRADE_BILLS.getType(); }else if ("fundflowbill" .equals(type)){ url = WxApiType.FUND_FLOW_BILLS.getType(); }else { throw new RuntimeException ("不支持的账单类型" ); } url = wxPayConfig.getDomain().concat(url).concat("?bill_date=" ).concat(billDate); HttpGet httpGet = new HttpGet (url); httpGet.addHeader("Accept" , "application/json" ); CloseableHttpResponse response = wxPayClient.execute(httpGet); try { String bodyAsString = EntityUtils.toString(response.getEntity()); int statusCode = response.getStatusLine().getStatusCode(); if (statusCode == 200 ) { log.info("成功, 申请账单返回结果 = " + bodyAsString); } else if (statusCode == 204 ) { log.info("成功" ); } else { throw new RuntimeException ("申请账单异常, 响应码 = " + statusCode+ ", 申请账单返回结果 = " + bodyAsString); } Gson gson = new Gson (); Map<String, String> resultMap = gson.fromJson(bodyAsString, HashMap.class); return resultMap.get("download_url" ); } finally { response.close(); } } @Override public String downloadBill (String billDate, String type) throws Exception { log.warn("下载账单接口调用 {}, {}" , billDate, type); String downloadUrl = this .queryBill(billDate, type); HttpGet httpGet = new HttpGet (downloadUrl); httpGet.addHeader("Accept" , "application/json" ); CloseableHttpResponse response = wxPayNoSignClient.execute(httpGet); try { String bodyAsString = EntityUtils.toString(response.getEntity()); int statusCode = response.getStatusLine().getStatusCode(); if (statusCode == 200 ) { log.info("成功, 下载账单返回结果 = " + bodyAsString); } else if (statusCode == 204 ) { log.info("成功" ); } else { throw new RuntimeException ("下载账单异常, 响应码 = " + statusCode+ ", 下载账单返回结果 = " + bodyAsString); } return bodyAsString; } finally { response.close(); } }