谈一下个人理解:比如交易,微信直连,那么你走的就是微信渠道,支付方式是微信,支付场景就可能是主扫,被扫,公众号等,三部分渠道,支付方式,支付场景
下面的微信支付产品就是包括这些,你是什么场景就直接使用什么产品,微信直连的方式渠道和支付方式都是固定的
一、微信支付产品和接入指引
1、支付产品
1.1、付款码支付(被扫,B扫C,authcode授权码)
用户展示微信钱包内的“付款码”给商家,商家扫描后直接完成支付,适用于线下面对面收银的场景。
1.2、JSAPI支付(主扫,C扫B,动态码)
线下场所:商户展示一个支付二维码,用户使用微信扫描二维码后,输入需要支付的金额,完成支付。
公众号场景:用户在微信内进入商家公众号,打开某个页面,选择某个产品,完成支付。
(比如微信公众号,这里微信直连肯定就是直接走的微信的渠道,但是我微信公众号也可以不走微信的渠道,可以走银联的渠道)
PC网站场景:在网站中展示二维码,用户使用微信扫描二维码,输入需要支付的金额,完成支付。
特点:用户在客户端输入支付金额
1.3、小程序支付
在微信小程序平台内实现支付的功能。
1.4、Native支付(主扫)
Native支付是指商户展示支付二维码,用户再用微信“扫一扫”完成支付的模式。这种方式适用于PC网站。
特点:商家预先指定支付金额
注意: JSAPI支付和Native支付都是主扫,但是前者是用户自己输入金额,后者是商家输入金额。其实没有毛病,但是实际上都是主扫,是否指定金额在于前端的设计
1.5、APP支付
商户通过在移动端独立的APP应用程序中集成微信支付模块,完成支付。
1.6、刷脸支付
用户在刷脸设备前通过摄像头刷脸、识别身份后进行的一种支付方式。
2、接入指引
无论前端千变万化,后端都是差不多的,后面的项目实战就是PC网站,也就是主扫
点击后,申请材料
商家经验的类目
申请流程
然后
然后准备各种资料
注意: 审核通过后,微信会给超级管理员的微信发生一个签约信息,超级管理员需要在微信上进行一个线上签约,签约完成后,就获取到了商户平台的商户号,然后就可以用获取到的商户号进行登录了。
来到刚刚界面的首页,超级管理员扫描登录即可。
然后就会进入商户平台后台管理首页了
2.1、获取商户号
微信商户平台:微信支付 - 中国领先的第三方支付平台 | 微信支付提供安全快捷的支付方式
场景:Native支付
步骤:提交资料 => 签署协议 => 获取商户号
然后点击账号中心,有一个登录账号的信息,就是微信支付开发中需要的第一个参数,也就是商户号
2.2、获取AppID
1、获取到微信商户号后 ,来到申请流程第三步,绑定场景,在这个步骤中需要一个AppID。
2、然后将这个AppID和申请到的商户号进行绑定。
3、AppID如何获取,需要申请一个微信公众号。
微信公众平台:微信公众平台
步骤:注册服务号 => 服务号认证 => 获取APPID => 绑定商户号
点击注册
点击服务号
填写基本信息后即可获取AppID了,但是还要我们再公众号中完成微信的企业认证,等待审核通过
就可以将这个步骤获取的AppID和上个步骤获取的商户号进行绑定
开发者ID(AppID),微信支付开发中需要的第二个参数
2.3、关联AppID
微信支付 - 中国领先的第三方支付平台 | 微信支付提供安全快捷的支付方式
来到微信商户平台,之前申请的商户号进行登录,然后关联AppID
2.4、获取API秘钥
APIv2版本的接口需要此秘钥
步骤:登录商户平台 => 选择 账户中心 => 安全中心 => API安全 => 设置API密钥
三个参数:API证书、API密钥(v2)、APIv3密钥【可以接入v2或者v3】
设置密钥可以用随机密码生成器:
在线随机密码生成器【设置密码需要超级管理员的操作密码进行验证】
注意:如果是接入的APIv3版本,在所有的API接口中都需要使用API证书。如果接入的是APIv2版本,只需要在一些高级接口中使用API证书(如退款、企业红包、企业付款会)证实商户身份。
2.5、获取APIv3秘钥
APIv3版本的接口需要此秘钥
步骤:登录商户平台 => 选择 账户中心 => 安全中心 => API安全 => 设置APIv3密钥
随机密码生成工具:生成随机密码 - 密码生成器 - 密码批量生成器
2.6、申请商户API证书
APIv3版本的所有接口都需要;APIv2版本的高级接口需要(如:退款、企业红包、企业付款等)步骤:登录商户平台 => 选择 账户中心 => 安全中心 => API安全 => 申请API证书
点击申请证书,然后点击下载证书工具
下载好之后,打开文件夹,双击
然后
然后填写商户号和商户名称(商户名称必须和营业执照保持一致)
然后获取到一个请求串,复制到商户平台中
也就是这下面方框中,然后点击下一步,输入管理员操作密码
然后复制证书串
然后粘贴证书串,点击下一步,生成证书
去电脑本地查看证书文件夹,这个压缩包就是证书文件
打开看,这三个文件有什么作用,后面会详细介绍
2.7、获取微信平台证书
可以预先下载,也可以通过编程的方式获取。后面的课程中,我们会通过编程的方式来获取。
注意:以上所有API秘钥和证书需妥善保管防止泄露
查看证书区别:商户API证书和微信平台证书区别!!!!-CSDN博客
二、支付安全(证书/秘钥/签名)
1、信息安全的基础 - 机密性
明文:加密前的消息叫“明文”(plain text)
密文:加密后的文本叫“密文”(cipher text)
密钥:只有掌握特殊“钥匙”的人,才能对加密的文本进行解密,这里的“钥匙”就叫做“密钥”(key)
“密钥”就是一个字符串,度量单位是“位”(bit),比如,密钥长度是 128,就是 16 字节的二进制串
加密:实现机密性最常用的手段是“加密”(encrypt)
按照密钥的使用方式,加密可以分为两大类: 。
对称加密和非对称加密
解密:使用密钥还原明文的过程叫“解密”(decrypt)
加密算法:加密解密的操作过程就是“加密算法”
所有的加密算法都是公开的,而算法使用的“密钥”则必须保密
2、对称加密和非对称加密
对称加密
特点:只使用一个密钥,密钥必须保密,常用的有 AES算法
优点:运算速度快
缺点:秘钥需要信息交换的双方共享,一旦被窃取,消息会被破解,无法做到安全的密钥交换
非对称加密
特点:使用两个密钥:公钥和私钥,公钥可以任意分发而私钥保密,常用的有 RSA
优点:黑客获取公钥无法破解密文,解决了密钥交换的问题
缺点:运算速度非常慢
微信支付中的非对称加密算法就是使用这种方式RSA
混合加密
实际场景中把对称加密和非对称加密结合起来使用。
3、身份认证
公钥加密,私钥解密的作用是加密信息
私钥加密,公钥解密的作用是身份认证(不是为了加密,因为很多人都有公钥,确认是谁发的消息,身份认证)
4、摘要算法(Digest Algorithm)
摘要算法就是我们常说的散列函数、哈希函数(Hash Function),它能够把任意长度的数据“压缩”成固定长度、而且独一无二的“摘要”字符串,就好像是给这段数据生成了一个数字“指纹”。
作用:保证信息的完整性
特性: 不可逆:只有算法,没有秘钥,只能加密,不能解密(不像前文中说到的对称加密和非对称加密一样)
难题友好性:想要破解,只能暴力枚举
发散性:只要对原文进行一点点改动,摘要就会发生剧烈变化(哪怕是一个标点符号)
抗碰撞性:原文不同,计算后的摘要也要不同(可能会有两份不同的原文对应同一份摘要)
常见摘要算法:
MD5、SHA1、SHA2(SHA224、SHA256、SHA384)
MD5和SHA1不具有强碰撞性,目前使用较多但是SHA2
在回答前文:
Bob:
第一步:Bob写完信后,用摘要算法生成信件原文的摘要MessageDigest
第二步:Bob将摘要附在信件原文的下面,一起发送给Pat
Pat:
第一步:Pat收到信后,也是和bob使用一样的摘要算法,加密信件的原文得到摘要
第二步:Pat将加密后的摘要和Bob在原文中附加的摘要做对比,如果一致说明信件没有被篡改
【有一个致命的漏洞:如果信件被黑客截获,并且黑客修改了原文,根据原文生成了新的摘要,附加到原文下面然后发送给Pat,Pat接受后是完全察觉不出来信件已经被篡改了的,所以说摘要算法不具有机密性】
所以说要加入密钥,确保信件的机密性,看下面的数字签名
5、数字签名
数字签名是使用私钥对摘要加密生成签名,需要由公钥将签名解密后进行验证,实现身份认证和不可否认
签名和验证签名的流程:
Bob
第一步:Bob写完信后,用摘要算法生成信件原文的摘要MessageDigest
第二步:Bob将使用自己的私钥摘要加密生成签名Signature
第三步:Bob将数字签名附在信件原文的下面,一起发送给Pat
Pat
第一步:Pat收到信后,用Bob的公钥解密得到信件的摘要,
第二步:Pat使用和Bob一样的摘要算法加密信件的原文,得到信件的摘要
第三步:Pat将前面两步的摘要进行对比,一致就是没有得到篡改,这个过程就是验签
【即使黑客修改了信件的原文,即使黑客通过摘要算法生成新的摘要,没有Bob的私钥,因此无法对摘要进行加密,无法生成只能有Bob才能生成的签名,所以这个信也就没有办法被篡改。】
微信支付中签名和验签就是这个原理!
6、数字证书
数字证书解决“公钥的信任”问题,可以防止黑客伪造公钥。
首先Doug想要欺骗Pat,谎称这是Bob的公钥,但是Pat实际拥有的是Doug的公钥。因此Doug就可以用自己的私钥做数字签名发送给Pat,Pat用假的Bob的公钥进行验签,可以成功。
Pat误以为是在和Bob通信,其实Doug。相当于你误以为和微信服务器进行通信,但实际上是和黑客通信。
谁都可以发公钥,怎么判断公钥是Bob的,解决这个办法就是数字证书
不能直接分发公钥,公钥的分发必须使用数字证书,数字证书由CA颁发
数字证书经过hash算法生成摘要,然后经过私钥对摘要加密生成签名,最后一起发布得到数字证书!!!
颁发证书的具体流程:CA根据证书中指定的哈希算法,根据证书信息计算整个证书的摘要,也就是证书的指纹。然后根据CA证书中的签名算法,用CA自己的私钥将摘要生成签名,最后一起发布得到数字证书。
先盖上数字证书,验证证书和验证信件!!!
Bob盖上数字证书,Pat收到信后,先取出证书,验签证书【Pat用证书中指定的哈希算法,根据证书信息计算整个证书的摘要,并且使用CA的公钥从数字证书的签名中解析出数字证书的摘要,然后将两个摘要进行比较,如果一致,说明验签通过】,如果验签通过,可以获取到Bob的公钥。
接下来就和前面一样了。对信件进行验签:先用Hash算法计算摘要,然后用刚刚获取的Bob的公钥对信件的签名进行解密,得到信件的摘要,然后对比两个摘要,一致则通过。
https协议中的数字证书:
相当于把HTTps换成前面的Bob,如果数字证书不可靠,浏览器会发出不安全的警告信息。
7、微信APIv3证书
包括商户证书和微信支付平台证书,整个应用程序目前是是谷粒学苑进行微信支付。信息交换的双方一个是谷粒学苑,一个是微信支付系统。信息交换的双方想要进行加密数据传输的话,两方都需要申请他们各种的证书。都要有各占的私钥和公钥。
所以商户证书这边解决商户这边的私钥、公钥以及证书的问题。(前面有申请过)
【第一个相当于安装版的证书、可以直接双击,将证书导入到我们的系统中。
第二个相当于一个文本版的一个证书,实际上是一个非常长的加密后的一个字符串,封装了商户的公钥,如果在编程里面想要读取公钥的话,必须读取这个证书,把这个公钥从证书中读取出来。
第三个文件就是商户平台中获取的私钥了。】
平台证书解决微信支付平台的公钥、私钥以及证书的问题。可以看下面
商户证书:(商户证书解决商户这边的公钥私钥,平台证书同理)
商户API证书是指由商户申请的,包含商户的商户号、公司名称、公钥信息的证书。
商户证书在商户后台申请:微信支付 - 中国领先的第三方支付平台 | 微信支付提供安全快捷的支付方式
平台证书(微信支付平台):
微信支付平台证书是指由微信支付负责申请的,包含微信支付平台标识、公钥信息的证书。商户可以使用平台证书中的公钥进行验签。
平台证书的获取:证书密钥使用说明-接口规则 | 微信支付商户平台文档中心
有两种方式:
1、使用证书下载工具下载下来的物理文件
2、通过API接口方式下载,通过程序的方式下载,避免工具下载下来的证书过期
8、API密钥和APIv3密钥
都是对称加密需要使用的加密和解密密钥,一定要保管好,不能泄露。平台证书里面用到非对称加密,有私钥
API密钥对应V2版本的API
APIv3密钥对应V3版本的API
经过一系列铺垫,实战!!!!
三、案例项目的创建
可以参考文章
这篇文章教你把项目前后端创建起来,然后继续下面的步骤
四、基础支付API V3
①支付参数包含商户号、APPID、API密钥、数字证书等,需要用代码的方式加载到应用程序中
②加载商户私钥、商户向微信平台发送请求的时候,商户需要用私钥进行签名,微信平台接受到商户的发送的请求之后,需要使用商户的公钥进行验签,这里的商户就是我们前后端的这个系统,谷粒学苑。
③获取平台证书和验签器,商户向微信平台发送请求的时候,反过来微信支付平台也会向我们的商户发送请求,在这个过程中,微信支付平台会用他的私钥进行签名,我们商户会使用微信支付平台的公钥进行验签。而平台的公钥是从平台的数字证书当中获取的,所以我们要获取平台的数字证书
并且创建我们的签名验证器
④获取httpclient对象,远程的请求的发送是建立在httpclient连接的基础上的,需要使用httpclient工具建立远程连接。
⑤API字典和接口规则,开发接口,首先需要熟悉API,熟悉接口规则,更好帮助我们完成接口的开发
⑥内网穿透,微信向我们的开发服务器发送请求的时候,我们开发服务器必须有一个微信可以访问的外网地址,而我们的开发机一般都是局域网环境的,是没有独立ip的,需要进行内网穿透。通过这样的方式将我们的开发机器映射到外网。
⑦整个开放过程都是围绕APIv3的所有接口来进行讲解的,我们会实现基础支付中所有的接口功能。包括支付、查询订单、关闭订单、申请退款等等
1、引入支付参数
1.1、定义微信支付相关参数
将资料文件夹中的 wxpay.properties 复制到resources目录中
这个文件定义了之前我们准备的微信支付相关的参数,例如商户号、APPID、API秘钥等等
再细将一遍:
1、商户号:前面说的账号
2、商户API证书序列号:
点击管理证书,一定要一一对应(磁盘下载的证书)
3、商户的私钥文件:(用私钥将请求签名,发送给微信服务端,微信服务器端根据证书序列号找到对应证书,从证书中再解密出公钥,用公钥对请求进行验签)请求发送和接收的过程,对应的是签名和验签的过程
4、APIv3的密钥(对称加密的密钥)
5、APPID,前面讲过
6、微信服务器地址:都是向远程服务器发起接口调用
https://api.mch.weixin.qq.com
7、接收结果通知地址:向微信发送请求,微信也要向商户端发送(谷粒学苑)发送请求,需要用到内网穿透,这里每个人的地址都是不一样的。
1.2、读取支付参数
将资料文件夹中的 config 目录中的 WxPayConfig.java 复制到源码目录中。
都是一一对应的
1、有@Configuration注解,因此在整个应用程序加载的时候,我们这个配置文件就会被创建出来
2、@PropertySource("classpath:wxpay.properties") //读取配置文件,自动读取配置文件中的信息,读取哪一部分信息:@ConfigurationProperties(prefix="wxpay") //读取wxpay节点
指定读取wxpay前缀的信息
3、用@Data set方法将值设置在了属性值当中(spring boot项目中配置文件中mch-id和mchId自动映射)
1.3、测试支付参数的获取
package com.atguigu.paymentdemo.controller;
import com.atguigu.paymentdemo.config.WxPayConfig;
import com.atguigu.paymentdemo.vo.R;
import io.swagger.annotations.Api;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
@Api(tags = "测试控制器")
@RestController
@RequestMapping("/api/test")
public class TestController {
@Resource
private WxPayConfig wxPayConfig;
@GetMapping
public R getWxPayConfig(){
String mchId = wxPayConfig.getMchId();
return R.ok().data("mchId",mchId);
}
}
然后启动服务器
启动服务器如果报错,需要在启动类上加@EnableConfigurationProperties
这个应该不是主要问题,可能是打包编译的问题
还报错的话
如果文件已正确放置在 resources
中,但在打包时未能正确包含,确保 pom.xml
文件中的构建配置正确,以确保所有资源文件都被正确打包。
对于 Maven 构建,确保 pom.xml
中有如下配置来确保资源文件被正确打包:
<resource>
<directory>src/main/resources</directory>
</resource>
测试成功:
1.4、在IDEA中设置 SpringBoot 配置文件及配置 Annotation Processor
点击跳转
但是下面点击跳转不了,因为这个配置文件在idea中,idea没有把它看成spring的配置文件,不影响应用程序运行,但是少了功能
只有一个配置文件,然后点击自定义spring配置
点击+号
然后点击ok,但是点击还是不能定位,还需要一个步骤
可以帮助我们生成自定义配置的元数据信息,让配置文件和Java代码之间的对应参数可以自动定位,方便开发。pom中添加依赖
<!--生成自定义配置的元数据信息-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
重新启动一下服务器,就可以跳转了
2、加载商户私钥
2.1、复制商户私钥
将下载的私钥文件复制到项目根目录下:
2.2、引入SDK
点击java
我们可以使用官方提供的 SDK,帮助我们完成开发。实现了请求签名的生成和应答签名的验证。
引入依赖
GitHub - wechatpay-apiv3/wechatpay-apache-httpclient: 微信支付 APIv3 Apache HttpClient装饰器(decorator)
<dependency>
<groupId>com.github.wechatpay-apiv3</groupId>
<artifactId>wechatpay-apache-httpclient</artifactId>
<version>0.3.0</version>
</dependency>
可以看包括了一些依赖,比如关于httpclient的依赖,已经被包括了
2.3、获取商户私钥
https://github.com/wechatpay-apiv3/wechatpay-apache-httpclient (如何加载商户私钥)
私钥以文件形式存在,用第一种
/**
* 获取商户的私钥文件
* @param filename
* @return
*/
private PrivateKey getPrivateKey(String filename){
try {
return PemUtil.loadPrivateKey(new FileInputStream(filename));
} catch (FileNotFoundException e) {
throw new RuntimeException("私钥文件不存在",e);
}
}
2.4、测试商户私钥的获取
在 PaymentDemoApplicationTests 测试类中添加如下方法,测试私钥对象是否能够获取出来。(将前面的方法改成public的再进行测试),也可以在controller层测试
@Test
void test() {
//获取私钥路径
String privateKeyPath = wxPayConfig.getPrivateKeyPath();
//获取私钥
PrivateKey privateKey = wxPayConfig.getPrivateKey(privateKeyPath);
System.out.println(privateKey);
}
如果打印的是内存
<!--fastjson-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.80</version>
</dependency>
需要
package com.atguigu.paymentdemo;
import com.alibaba.fastjson.JSON;
import com.atguigu.paymentdemo.config.WxPayConfig;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import javax.annotation.Resource;
import java.security.PrivateKey;
@SpringBootTest
class PaymentDemoApplicationTests {
@Resource
private WxPayConfig wxPayConfig;
@Test
void test() {
//获取私钥路径
String privateKeyPath = wxPayConfig.getPrivateKeyPath();
//获取私钥
PrivateKey privateKey = wxPayConfig.getPrivateKey(privateKeyPath);
System.out.println(JSON.toJSONString(privateKey));
}
}
打印结果:
记得测试完毕,方法改为private
3、获取签名验证器和HttpClient
3.1、证书密钥使用说明
1、商户请求:M是商户,W是微信支付平台,商户向微信支付平台发送请求,商户这边用商户的私钥对请求进行签名,微信支付平台用商户的公钥进行验签,非对称加密过程。
2、同步返回:返回响应数据之前,微信先用平台的私钥对响应的数据进行签名,然后发送到商户,商户这边用平台的公钥对响应的数据进行验证。验证成功才会做进一步处理。
通过SDK来进行计算机签名和验证签名。
3.2、获取签名验证器
https://github.com/wechatpay-apiv3/wechatpay-apache-httpclient (定时更新平台证书功能)
GitHub - wechatpay-apiv3/wechatpay-apache-httpclient: 微信支付 APIv3 Apache HttpClient装饰器(decorator)
平台证书:平台证书封装了微信的公钥,商户可以使用平台证书中的公钥进行验签。
签名验证器:帮助我们进行验签工作,我们单独将它定义出来,方便后面的开发。
核心代码在下面这块,这块通过builder对象创建一个httpclient对象,那么这个httpclient对象会执行一个execute方法,执行成功后会得到有一个响应,整个过程就是请求的发送和响应的过程。
在这短短的一行代码里隐含了商户端计算签名,返回的时候要对签名进行校验的过程。
之所以能做这么多,是因为前期做了很多准备工作
平台的公钥为了防止被冒用,不能再网上分发,以证书的形式分发,拿到微信平台公钥就是拿微信平台的证书,而且还有有效期,为什么不过期,下面sdk有定时更新平台证书的功能。使用ScheduledUpdateCertificatesVerifier这个类,就能定时更新下载(我们不用关心怎么下载),已经封装好了,提供必要的参数就行了。
这是上面代码,因为现在微信已经更新到0.5.0版本,,目前这里是0.3.0版本,用我下面的
// 使用定时更新的签名验证器,不需要传入证书
verifier=new ScheduledUpdateCertificatesVerifier(new WechatPay2Credentials(merchanId,new PrivateKeySigner(merchantSerialNumber,merchantPrivateKey)), apiV3Key.getBytes(StandardCharsets.UTF_8));
WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create()
.withMerchant(merchantId, merchantSerialNumber, merchantPrivateKey)
.withValidator(new WechatPay2Validator(verifier))
// ... 接下来,你仍然可以通过builder设置各种参数,来配置你的HttpClient
// 通过WechatPayHttpClientBuilder构造的HttpClient,会自动的处理签名和验签,并进行证书自动更新
CloseableHttpClient httpClient = builder.build();
// 后面跟使用Apache HttpClient一样
CloseableHttpResponse response = httpClient.execute(...);
3.3、获取 HttpClient 对象
https://github.com/wechatpay-apiv3/wechatpay-apache-httpclient (定时更新平台证书功能)
HttpClient 对象:是建立远程连接的基础,我们通过SDK创建这个对象。
这个可以进行签名验证的对象,其它地方也需要用,把它抽取出来
抽取出来如下:
想要这个方法自动执行,可以加一个@Bean,不希望执行多次,执行一次就好了
@Bean
public ScheduledUpdateCertificatesVerifier getVerifier(){
//使用定时更新的签名验证器,不需要传入证书
ScheduledUpdateCertificatesVerifier verifier=new ScheduledUpdateCertificatesVerifier(new WechatPay2Credentials(merchanId,new PrivateKeySigner(merchantSerialNumber,merchantPrivateKey)),
apiV3Key.getBytes(StandardCharsets.UTF_8));
return verifier;
}
上面的都是一系列过程,下面是关于签名验证器和HttpClient对象代码
签名验证器:
/**
* 获取签名验证器
* @return
*/
@Bean
public ScheduledUpdateCertificatesVerifier getVerifier(){
//获取商户私钥
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;
}
HttpClient对象:
/**
* 获取http请求对象
* @param verifier
* @return
* 加Bean是让CloseableHttpClient这个对象自动的被创建出来
*/
@Bean
public CloseableHttpClient getWXPayClient(ScheduledUpdateCertificatesVerifier verifier){
//获取商户私钥
PrivateKey privateKey = getPrivateKey(privateKeyPath);
WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create()
.withMerchant(mchId, mchSerialNo, privateKey)
.withValidator(new WechatPay2Validator(verifier));
// 通过WechatPayHttpClientBuilder构造的HttpClient,会自动的处理签名和验签,并进行证书自动更新
CloseableHttpClient httpClient = builder.build();
return httpClient;
}
4、API字典和相关工具
4.1、API列表
我们的项目中要实现以下所有API的功能。下面native所有的接口都要一一实现
实现过程中需要遵循一些接口规则:
关于签名有一些签名的介绍,用到了微信提供的sdk后,这个签名的生成过程对我们来说就不需要关注了,因为已经把这个过程给我们封装好了。
4.2、接口规则
微信支付 APIv3 使用 JSON 作为消息体的数据交换格式。在程序中添加JSON处理的工具
阿里和google都都行,这里我都使用一下
<!--json处理,被sprinboot管理,不用写版本-->
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
</dependency>
实现过程中需要遵循一些接口规则:
关于签名有一些签名的介绍,用到了微信提供的sdk后,这个签名的生成过程对我们来说就不需要关注了,因为已经把这个过程给我们封装好了。
4.3、定义枚举
将资料文件夹中的 enums 目录复制到源码目录中。
为了开发方便,我们预先在项目中定义一些枚举。枚举中定义的内容包括接口地址,支付状态等信息。
微信和支付宝,这里@AllArgsConstructor初始化
订单状态
api地址(主机地址https://api.mch.weixin.qq.com+上下面的地址,也就就是组装成完整的远程调用地址)
通知单独的定义出来,实际上需要我们商户平台来开发这些接口
退款状态
支付订单状态(这个支付订单是我们商户平台和微信之间会产生一笔要发起支付的支付交易)
刚刚看到的OrderStatus是用户和我们商户平台之间有一个具体的课程订单
4.4、添加工具类(回调通知)
将资料文件夹中的 util 目录复制到源码目录中,我们将会使用这些辅助工具简化项目的开发
订单号工具类
http请求的处理工具
正常情况下是我们的服务器给微信发请求,然后微信给我们一个响应
但是在我们开发的过程中,会有一个叫回调通知的概念:微信给我们发请求,和上面流程是相反的,我们会给微信发响应。那么微信给我们发来的请求,它的这个请求信息就是封装到了HttpServletRequest里面。
我们接受到这个HttpServletRequest后,我们需要把这里面的信息解析出来。下面的工具就是解析微信给我们的发送的请求的请求解析工具。
5、Native下单API
5.1、Native支付流程
我们选择一门课程,然后选择微信支付,然后点击确认支付,点击确认支付的一瞬间,在我们浏览器当中会弹出展示一个微信的二维码,这时候用户拿起手机,扫描这个二维码进行支付。
这下面描述的是整个微信支付一个完整的流程,先把整个流程串一下,
四个角色:微信支付用户、微信客户端(手机上的应用程序)、商户后台系统(谷粒学苑的前后端项目)、微信支付系统(远程的微信支付平台)
①首先用户在点击确认支付的时候,相当于他主观上想要发起一笔订单,所以商户后台系统在用户点击了确认支付的时候(前面前端界面的确认支付),首先生成订单,然后会在我们数据库中t_order_info中生成出一条订单记录,我们生成订单记录的时候要向数据库中t_order_info表中插入一条记录。
②生成完订单后,我们的商户后台系统会调用微信支付的统一下单api,生成一个预支付交易,这个统一下单api就是,这就是我们远程调用微信支付的第一个接口。
③紧接着微信支付系统收到我们请求后生成了预支付交易后会返回一个预支付交易链接,这个预支付交易链接实际上是一个code_url。
④code_url未来会被我们的二维码生成工具生成一个二维码,所以在我们商户后台会根据这个链接生成一个二维码图片。这个二维码图片用户就可以在商户后台系统看到了。
⑤用户看到这个二维码图片后,用户会打开他微信客户端,然后利用微信客户端的扫一扫进行扫码操作。扫码操作这个过程会直接提交给微信支付系统。
⑥微信支付系统收到会先验证这个链接的有效性。验证了这个链接的有效性后会要求用户授权。要求用户授权的过程实际上是输入支付密码的过程。
⑦接下来用户在微信的客户端输入支付密码并且确认支付。微信客户端就会把这个授权提交给微信支付系统。
⑧微信支付系统会验证这个授权,如果对面用户需要输入密码的话,支付系统就会校验这个密码,如果用户输入指纹的话,那么就会校验这个指纹,验证授权完成后,微信支付系统就会完成这笔支付交易。
紧接着然后下面的有几个并行操作:
9、微信支付系统完成这笔交易后,会将这个结果通过微信或者短信的形式提示给用户,微信客户端就会展示一个支付结果页面。
10、于此同时,我们这个谷粒学苑的这个系统也会收到通知。这个通知就是微信向谷粒学苑发起的,叫这异步通知。因为这个通知并不是向之前一样,请求然后响应,是微信主动发起的请求,并不是我们给微信发的请求的某一个响应,所以也叫回调通知。我们商户系统接受到这个回调通知后需要对我们系统当中的订单状态(商户和微信之间的)作一个修改,修改成功后我们会告诉微信支付系统我们这个回调通知已经成功接受。在回调通知发送和响应的过程中可能会出现网络异常的情况:有可能我们这边的回调由于某种原因没有成功接受到,那么你会发现用户收到了支付成功的通知,但是我们的商户系统没有收到支付成功的通知。这就会导致用户实际上已经支付成功了,但是我们的系统因为没有收到支付成功的通知,所以就没有办法在这个过程中去处理我们的订单状态,那我们系统当中很有可能有一个订单状态未更新的情况,用户支付成功了,商户系统写上未支付,那这样用户体验就不是特别好了。为了避免这样的问题发生:
11、如果我们的业务系统,商户系统,没有收到支付通知的时候,那么我们的商户平台会主动调用微信平台的一个查询订单的接口(目前是有两种,一种是微信支付订单号查询订单,一种是商户号查询订单)。那么在调用查询订单接口的过程中,我们查询到订单状态并且通过微信端返回的支付状态来修改订单。
12、后续就一些发货操作和其它操作了,对应我们这种虚拟产品就没有发货操作了,用户就直接可以查看课程了,
12、发货操作
5.2、Native下单API (*)
我们实现下面步骤的内容:1、生成订单,2、调用统一下单,3、微信支付端生成预支付交易,返回预支付交易链接code_url。
我们需要在程序中创建native统一下单的接口。
controller层:
package com.atguigu.paymentdemo.controller;
import io.swagger.annotations.Api;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@CrossOrigin //跨越注解
@RestController
@RequestMapping("/api/wx-pay")
@Api(tags = "网站微信支付API")
public class WxPayController {
}
service层:
package com.atguigu.paymentdemo.service.impl;
import com.atguigu.paymentdemo.service.WxPayService;
import org.springframework.stereotype.Service;
@Service
public class WxPayServiceImpl implements WxPayService {
}
package com.atguigu.paymentdemo.service;
public interface WxPayService {
}
然后创建好后开始写代码,下面我将直接给出代码:
加@Accessors(chain = true) //链式操作,是为了return R.ok().setData(map);
controller:
package com.atguigu.paymentdemo.controller;
import com.atguigu.paymentdemo.service.WxPayService;
import com.atguigu.paymentdemo.vo.R;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.Map;
@CrossOrigin //跨越注解
@RestController
@RequestMapping("/api/wx-pay")
@Api(tags = "网站微信支付API")
@Slf4j
public class WxPayController {
@Resource
private WxPayService wxPayService;
/*
* 前端是把商品的id传过去,数据库是bigint,下面用Long对象
* */
@ApiOperation("调用统一下单API,生成支付二维码")
@PostMapping("native/{productId}")
public R nativePay(@PathVariable Long productId){
log.info("发起支付请求");
//返回支付二维码链接和订单号
Map<String,Object> map=wxPayService.nativePay(productId);
return R.ok().setData(map);
}
}
然后把service的nativePay方法创建出来,这里就不多说。
package com.atguigu.paymentdemo.service.impl;
import com.atguigu.paymentdemo.entity.OrderInfo;
import com.atguigu.paymentdemo.enums.OrderStatus;
import com.atguigu.paymentdemo.service.WxPayService;
import com.atguigu.paymentdemo.util.OrderNoUtils;
import org.springframework.stereotype.Service;
import java.util.Map;
@Service
public class WxPayServiceImpl implements WxPayService {
@Override
public Map<String, Object> nativePay(Long productId) {
//生成订单
OrderInfo orderInfo = new OrderInfo();
orderInfo.setTitle("test");
orderInfo.setOrderNo(OrderNoUtils.getOrderNo());//订单号
orderInfo.setProductId(productId);
orderInfo.setTotalFee(1);//单位分
orderInfo.setOrderStatus(OrderStatus.NOTPAY.getType());
//有一个用户id这里就不用写了,因为这里主要是写支付,不用管用户认证了
//TODO 存入数据库
//调用统一下单API
return null;
}
}
然后写统一下单API,然后组装请求参数
这个接口怎么调用,我们需要http远程请求,怎么写,然后看微信指引文档,基础支付,native支付,中的开发指引。
最后总结一下serviceImpl:
package com.atguigu.paymentdemo.service.impl;
import com.atguigu.paymentdemo.config.WxPayConfig;
import com.atguigu.paymentdemo.entity.OrderInfo;
import com.atguigu.paymentdemo.enums.OrderStatus;
import com.atguigu.paymentdemo.enums.wxpay.WxApiType;
import com.atguigu.paymentdemo.enums.wxpay.WxNotifyType;
import com.atguigu.paymentdemo.service.WxPayService;
import com.atguigu.paymentdemo.util.OrderNoUtils;
import com.google.gson.Gson;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.util.EntityUtils;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
@Slf4j
@Service
public class WxPayServiceImpl implements WxPayService {
@Resource
private WxPayConfig wxPayConfig;
@Resource
private CloseableHttpClient wxPayClient;//标注了bean对象,可以把方法直接注入进来
/**
* 创建订单,调用native支付接口
* @param productId
* @return code_url和订单号
* @throws Exception
*/
@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());
//有一个用户id这里就不用写了,因为这里主要是写支付,不用管用户认证了
//TODO 存入数据库
log.info("调用统一下单API");
//调用统一下单API
HttpPost httpPost =new HttpPost(wxPayConfig.getDomain().concat(WxApiType.NATIVE_PAY.getType()));
//请求body参数
Gson gson = new Gson();
Map paramsMap = new HashMap();
paramsMap.put("appid",wxPayConfig.getAppid());
paramsMap.put("mchid",wxPayConfig.getMchId());
paramsMap.put("description",orderInfo.getTitle());
paramsMap.put("out_trade_no",orderInfo.getOrderNo());
paramsMap.put("notify_url",wxPayConfig.getNotifyDomain().concat(WxNotifyType.NATIVE_NOTIFY.getType()));
Map amountMap=new HashMap();
amountMap.put("total",orderInfo.getTotalFee());
amountMap.put("currency","CNY");
paramsMap.put("amount",amountMap);
//将参数转化为字符串
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");
//完成签名并执行请求,这个httpclient对象是之前在WxpayConfig里面设置过
CloseableHttpResponse response = wxPayClient.execute(httpPost);
try {
String bodyAsString = EntityUtils.toString(response.getEntity());//响应体
int statusCode = response.getStatusLine().getStatusCode();//响应状态码
if(statusCode == 200){//处理成功
log.info("成功,返回结果="+bodyAsString);
}else if(statusCode==204){//处理成功,无返回body
log.info("成功");
}else {
log.info("Native下单失败,响应码="+statusCode+",返回结果"+bodyAsString);
throw new IOException("request failed");
}
//响应结果
HashMap<String,String> resultMap = gson.fromJson(bodyAsString, HashMap.class);
//二维码
String codeUrl = resultMap.get("code_url");
Map<String, Object> map = new HashMap<>();
map.put("code_url",codeUrl);
map.put("orderNo",orderInfo.getOrderNo());
return map;
} finally {
response.close();
}
}
}
启动程序,swagger查看:
输入商品id,
然后成功获取codeUrl
然后启动前端:
报network是:把index.vue里面的注释掉,把这个定时器关闭掉。
// 启动定时器
this.timer = setInterval(() => {
this.queryOrderStatus();
}, 3000);
});
如果此时二维码扫码没有内容,可能是前端的问题,我们可以把
weixin://wxpay/bizpayurl?pr=6EykkROz3
发到微信,然后点击,就可以微信支付
这是我支付的过程,其实到这一步是可以进行支付了的:
5.3、前端解析
简要介绍一下前端的大致流程
这是后端的productList,返回的数据列表
这是对应的前端:
以及脚本:
如何赋值:created()方法在页面加载时执行
拿出data中的productList
axios执行:
后端接口:
引用的是一个模块,引用后可以直接调用方法
初始化了所有远程axios请求主机地址
点击确认支付的时候,点击后就要禁用按钮
如果是微信支付,走统一下单接口
回调后端返回的值
this.codeUrl = response.data.codeUrl;
this.orderNo = response.data.orderNo;
//打开二维码弹窗
this.codeDialogVisible = true;
"vue-qriously": "^1.1.1",这个模块展示二维码
引入:
注意,如果二维码用不了,可以用别的二维码生成工具:【支付之前记得刷新界面】
npm install qrcode
然后
import QRCode from "qrcode"; // 导入 qrcode 库
QRCode.toDataURL( codeUrl, { width: 300 }, (err, url) => { if (err) { console.error(err); } else { this.codeUrl = url; // 保存生成的二维码图片链接 this.codeDialogVisible = true; // 打开二维码弹窗 // 启动定时器 this.timer = setInterval(() => { this.queryOrderStatus(); }, 3000); } } );
<img v-if="codeUrl" :src="codeUrl" alt="微信支付二维码" style="width: 100%" />
5.4、签名和验签源码解析(了解即可)
1、签名解析
调用统一下单接口实际上是下面方宽中的流程,有一个完整的请求和响应,细节再第二张2图
首先商户发送一个请求的时候要计算一个签名,接受到微信支付给我们的响应我们要验证签名
开启debug日志
然后在WxPayConfig中log日志
1、获取签名器验证对象(应用程序启动就会被执行,加了@bean注解)
2、传入 验证对象获取httpclient对象(快捷键ctrl+F)
3、在WxPayController中打印“发起支付请求”
4、业务层中log.info("生成订单");log.info("请求参数:"+jsonParams);
5、//完成签名并执行请求,这个httpclient对象是之前在WxpayConfig里面设置过 CloseableHttpResponse response = wxPayClient.execute(httpPost);
在这个过程中完成了两个非常重要的过程:1、私钥签名的过程,2、公钥验签的过程
logging.level.root: debug
配置意味着所有日志记录器的日志级别都设置为 debug
,这会导致应用程序记录 debug
及以上级别的日志信息。
根日志记录器 的日志级别设置为
debug
。这意味着,除非你为某个特定的日志记录器(如某个类或包)单独设置了日志级别,否则所有日志记录器都将遵循这个设置,记录debug
级别及以上的日志(即debug
,info
,warn
,error
)
我们打印的日志和下面这个对应的
构造签名串:
前面五个字符
计算签名值:使用商户私钥对签名串进行SHA256 with RSA签名,并对签名结果进行Base64编码得到签名值 。SHA256是摘要算法,先把签名进行摘要计算,然后RSA是一个典型的非对称加密,用私钥对签名串生成的摘要进行非对称加密(和之前说的签名流程一样),多了一个步骤:对签名结果进行Base64编码得到签名值。
设置请求头:微信支付商户API v3要求通过HTTP Authorization头传递签名。Authorization由认证类型和签名信息两个部分组成。
Authorization: 认证类型 签名信息
Authorization: WECHATPAY2-SHA256-RSA2048 mchid="1900007291",nonce_str="593BEC0C930BF1AFEB40B4A08C8FB242",signature="C9PrZx8RTw7NF+e6SLmZxKgUBdXjH6EmUiu1i85Y6MApfWn4ueNpS4ED5no6uGObU0cfzTdaWyl6gAWDmyO2nG3MjHursadpzpNT8d+HaZapKis+boTHwJLgZXHXtacjX4zx2lOk/AONrKCLkjXRnh/DDp/kNsmDNYEiu+d/SeVvr+cL0XkL0CibAphyQSLYkv7Fh9uel89ax3ZGgVnBx+/MaBLCrYc1UqyYBDqfWPhS9fZf2OSghWMFp9c5dm+ORc97XbgzSOwAl8dcfLSrL/Sb4+L57+JZiq0iURjMXWzAD8FTUFYsJtJOYszRXJKLZNh4WGST39oplhhSdtxcoQ==",timestamp="1554208460",serial_no="408B07E79B8269FEC3D5D3E6AB8ED163A6A380DB"
签名信息:
认证类型及签名信息:
2、源码解析
【不懂可以看尚硅谷-微信支付-39】
3、平台证书的作用
源码看41
1、请求响应建议验证签名,封装的代码已经做了,下面第二步,第三步。
2、回调的过程必须验证签名,如下面的第10步。
微信支付API v3使用微信支付的平台私钥(不是商户私钥)进行应答签名。相应的商户的技术人员应该使用微信支付平台证书中的公钥验签。 下面第二步
5.5、创建课程订单
(1)保存订单
还有一个步骤需要完善,就是生成订单这个过程
先将日志级别调到info,debug打印的日志比较多
logging:
level:
root: info
之前生成的订单没有存入数据库 :
然后生成实现类:
最后改一下WxPayServiceImpl
然后重新启动服务器
http://localhost:8090/swagger-ui.html#!/
然后查看数据库,数据已经存入,不过目前都是未支付的状态
写下面两个是为了优化生成订单,
然后重新启应用程序
此时数据生成了一条新的订单记录(确保此时数据库订单为空)
然后在点击确认支付,发现数据库也只有一条记录,优化了创建订单【未支付的话永远一条】
(2)缓存二维码
保存二维码
修改了orderinfo中的codeurl字段
MyBatis-Plus 在执行更新操作时,默认只更新实体对象中被修改的字段(非空字段)。这里就是只更新codeUrl
(3)修改WxPayServiceImpl 的 nativePay 方法
重新启动服务
已经保存code_url
第一次点击确认支付,会创建订单以及保存codeurl
第二次点击确认支付,会直接返回订单以及保存的二维码
5.6、显示订单列表
在我的订单页面按时间倒序显示订单列表
(1)创建OrderInfoController
截图漏了@GetMapping("/list")
(2)定义 OrderInfoService 方法
重新启动
前端界面:
http://localhost:8080/
swagger界面进入:
http://localhost:8090/swagger-ui.html#!/
前端的代码
引入了orderInfo模块
调用list()方法response.data.list()
created() {
this.showOrderList()
},
确保一加载显示订单列表
list数据列表然后渲染
6、支付通知API(*)
前面四部分已经完成(商户)从5开始是用户手机微信扫码。
9:支付完成后,微信给用户发送支付成功的结果
例如:商户单号就是数据库中的订单号
交易单号(transaction_id):当用户通过微信支付成功完成交易时,微信支付平台会生成一个唯一的交易单号,称为
transaction_id
,用于标识这笔交易。
- 这个交易单号是由微信支付系统生成的,开发者无法自定义或修改。它是唯一的,并且与具体的支付订单一一对应。
商户订单号(order_no):这个是由商户(即你的系统)在发起支付时生成的唯一订单号。它是由商户自己定义的,通常与订单号相关,商户可以根据自己的业务需求设置。微信支付系统会将这个商户的订单号与交易成功的
transaction_id
关联起来。
10:异步通知商户支付结果
解决微信的服务端如何给我们的商户系统发送请求,都是基于局域网内网开发的
6.1、内网穿透
(1)访问cpolar官网
cpolar - secure introspectable tunnels to localhost
(2)注册登录
QQ邮箱密码即可
(3)下载内网穿透工具 (安装即可)
(4)设置你的 authToken 为本地计算机做授权配置
注意windows系统没有“./"
(5)启动服务
cpolar.exe http 8090
8080 后台服务端口
获取域名:建议使用https
或者直接面版创建
(6)测试外网访问
验证临时域名有效性 :
下次启动不用配置访问令牌了
6.2、接收通知和返回应答
也就是第10、11步
支付通知API: https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_5.shtml
(1)启动cpolar.exe http 8090
(建议用面板设置,启动cpolar web-ui)
(2)设置通知地址 wxpay.properties
我这里用面板设置的就是固定的域名
(3)创建通知接口
通知规则:用户支付完成后,微信会把相关支付结果和用户信息发送给商户,商户需要接收处理该消息,并返回应答。对后台通知交互时,如果微信收到商户的应答不符合规范或超时,微信认为通知失败,微信会通过一定的策略定期重新发起通知,尽可能提高通知的成功率,但微信不保证通知最终能成功。
(通知频率为 15s/15s/30s/3m/10m/20m/30m/30m/30m/60m/3h/3h/3h/6h/6h - 总计 24h4m)
先从请求体中拿到Json数据、之前有httpUtils工具类(将请求体内容转化为字符串)
然后启动服务(记得把数据库数据删除,后面会做超时自动关单的过程。目前支付了也没有做订单支付状态的处理)
微信向这个支付通知发起了回调
具体数据
对微信发的结果通知进行验签 ,验签完成后
ciphertext里面的加密数据还要用对称加密的密钥进行解密
(4)测试失败应答
用失败应答替换成功应答
然后按照规则发送通知
然后再启动
或者应答超时(5s) 可能会收到不一样的id,因为之前的通知微信也还在发送
6.3、验签(51)
已经对通知参数和通知应答进行了处理
(1)工具类
参考SDK源码中的 WechatPay2Validator 创建通知验签工具类 WechatPay2ValidatorForRequest
(2)验签
签名有两种,一种是之前的私钥公钥加密解密(对响应进行签名验证,respons获取请求头)
一种是微信服务端发送通知,我们对通知进行签名验证(和之前一样,不过是对请求进行签名验证,request获取请求头)【需要自己编写】
复制这个文件
改造后的:WechatPay2ValidatorForRequest
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package com.atguigu.paymentdemo.util;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.time.DateTimeException;
import java.time.Duration;
import java.time.Instant;
import com.wechat.pay.contrib.apache.httpclient.auth.Verifier;
import org.apache.http.Header;
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 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 = 5L;
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 {
//处理请求参数
this.validateParameters(request);
String message = this.buildMessage(request);
String serial = request.getHeader(WECHAT_PAY_SERIAL);
String signature = request.getHeader(WECHAT_PAY_SIGNATURE);
if (!this.verifier.verify(serial, message.getBytes(StandardCharsets.UTF_8), signature)) {
throw verifyFail("serial=[%s] message=[%s] sign=[%s], request-id=[%s]", serial, message, signature, requestId);
} else {
return true;
}
} catch (IllegalArgumentException var5) {
log.warn(var5.getMessage());
return false;
}
}
protected final void validateParameters(HttpServletRequest request) {
String[] headers = new String[]{"Wechatpay-Serial", "Wechatpay-Signature", "Wechatpay-Nonce", "Wechatpay-Timestamp"};
String header = null;
String[] var6 = headers;
int var7 = headers.length;
for(int var8 = 0; var8 < var7; ++var8) {
String headerName = var6[var8];
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() >= 5L) {
throw parameterError("timestamp=[%s] expires, request-id=[%s]", timestampStr, requestId);
}
} catch (NumberFormatException | DateTimeException var10) {
throw parameterError("invalid timestamp=[%s], request-id=[%s]", timestampStr, requestId);
}
}
protected final String buildMessage(HttpServletRequest request) throws IOException {
String timestamp = request.getHeader("Wechatpay-Timestamp");
String nonce = request.getHeader("Wechatpay-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) : "";
}
}
再次运行支付:
6.4、解密
微信端给我们发送通知之前,先用APIv3密钥进行参数的加密,加密后后对请求进行了签名。然后发送通知。
进行验签后再解密。
6.5、处理订单
@Override
public void processOrder(HashMap<String, Object> bodyMap) throws Exception {
log.info("处理订单");
String plainText = decryptFromResource(bodyMap);
}
/**
* 对称解密
* @param bodyMap
* @return
*/
private String decryptFromResource(HashMap<String, Object> bodyMap) throws Exception {
log.info("密文解密");
//通知数据
Map<String,String> resourceMap = (Map<String, String>) 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;
}
重新启动支付
明文:
这也是content字段内容:
{
"mchid": "1558950191", // 商户号
"appid": "wx74862e0dfcf69954", // 微信公众账号ID(应用ID)
"out_trade_no": "ORDER_20250107100601267", // 商户系统内部的订单号
"transaction_id": "4200002553202501079495711487", // 微信支付的交易单号
"trade_type": "NATIVE", // 支付方式,"NATIVE" 表示扫码支付
"trade_state": "SUCCESS", // 支付状态,"SUCCESS" 表示支付成功
"trade_state_desc": "支付成功", // 支付状态的描述信息,提供更详细的信息
"bank_type": "OTHERS", // 支付银行类型(如使用的支付渠道),这里是 "OTHERS" 可能代表其他类型
"attach": "", // 附加数据,商户可以携带一些自定义信息,支付回调时返回
"success_time": "2025-01-07T10:06:24+08:00", // 支付成功时间
"payer": {
"openid": "oHwsHuJXlJAbueTe7do_FRATykUw" // 支付者的用户 OpenID,用于标识该用户
},
"amount": {
"total": 1, // 总金额,单位为分,这里是 1 分
"payer_total": 1, // 支付者支付的金额,单位为分
"currency": "CNY", // 货币类型,"CNY" 表示人民币
"payer_currency": "CNY" // 支付者支付的货币类型,这里也是 "CNY" 表示人民币
}
}
然后,这里更新的订单状态是用枚举里面的值更新的,不是用明文里面的支付成功的值更新的
【这里其实可以加一个判断,判断支付通知返回的值,如果是suceess就更新订单状态支付成功。或者后通过查询订单的接口判断】
更新订单状态:
记录支付日志:
package com.atguigu.paymentdemo.service.impl;
import com.atguigu.paymentdemo.entity.PaymentInfo;
import com.atguigu.paymentdemo.enums.PayType;
import com.atguigu.paymentdemo.mapper.PaymentInfoMapper;
import com.atguigu.paymentdemo.service.PaymentInfoService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.google.gson.Gson;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Map;
@Slf4j
@Service
public class PaymentInfoServiceImpl extends ServiceImpl<PaymentInfoMapper, PaymentInfo> implements PaymentInfoService {
/**
* 记录日志
* @param plaintText
*/
@Override
public void createPaymentInfo(String plaintText) {
log.info("记录支付日志");
Gson gson = new Gson();
HashMap plainTextMap = gson.fromJson(plaintText, 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(plaintText);
baseMapper.insert(paymentInfo);
}
}
重新启动
6.6、处理重复通知
支付通知接口还存在一些问题,
商户系统=》微信支付系统,正常情况下商户系统向微信支付系统发送支付请求后,等待用户扫码支付,用户扫码支付后如果成功了,微信支付系统会给我们的商户系统发送通知,接下来我们会对这个通知进行处理。(前面的内容)
这个通知如果处理成功了,我们会给微信一个应答,这个应答必须是成功的应答(200)。
在这个应答的过程中,我们有一种情况微信会给我们重复发通知。超时情况【网络不稳定,超过5s了,微信就会重复给我们发通知】
所以我们需要重复接受通知,重复对订单进行处理,重复的记录日志。【可能积分会增加,日志会增加】
模拟应答超时
然后重新启动(数据库先清空)
然后查看数据库,因为微信会不停的发通知
处理重复的通知
重新启动支付
日志刷新也只有一条了
接口调用的幂等性 :无论接口被调用多少次,产生的结果是一致的
6.7、数据锁
对数据进行并发控制,对业务数据进行状态检查和处理之前,要采用数据锁进行并发控制,以避免函数重入造成的数据混乱。
什么意思:假设有两个通知同时到达,还是会写入两个日志。
模拟通知并发
数据库日志会有两条
解决方法:
尝试获取锁:成功获取则立即返回true,获取失败则立即返回false (获取失败就该干嘛干嘛了)
不必一直等待锁的释放
主动释放锁
/*对数据进行并发控制,对业务数据进行状态检查和处理之前,要采用数据锁进行并发控制,
以避免函数重入造成的数据混乱。*/
//尝试获取锁:成功获取则立即返回true,获取失败则立即返回false,不必一直等待锁的释放
if(lock.tryLock()){
try {
//接口调用的幂等性 :无论接口被调用多少次,产生的结果是一致的
//处理重复的通知
String orderStatus=orderInfoService.getOrderStatus(orderNo);
if(!OrderStatus.NOTPAY.getType().equals(orderStatus)){
log.info("订单已处理,无需重复处理");
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、后端定义商户查单接口
查询本地订单状态,如果返回是101,前端就一直定时查询
7.2、前端定时轮询查单
定时器每隔3秒执行函数里面的内容,也就是下面函数,去调用后端的接口
用户支付后,如果成功会跳转页面
8、用户取消订单API 实现用户主动取消订单的功能(*)
点击确认支付后,不扫码二维码。
8.1、定义取消订单接口
https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_9.shtml
取消订单
/**
* 关单接口的调用
* @param orderNo
*/
private void closeOrder(String orderNo) throws Exception {
log.info("调用微信关单API,订单号====>{}",orderNo);
//替换占位符
String url=String.format(WxApiType.CLOSE_ORDER_BY_NO.getType(),orderNo);
url=wxPayConfig.getDomain().concat(url);
HttpPost httpPost=new HttpPost(url);
//组装json请求体
Gson gson = new Gson();
HashMap<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");
//完成签名并执行请求,这个wxPayclient对象是之前在WxpayConfig里面设置过
CloseableHttpResponse response = wxPayClient.execute(httpPost);
try {
int statusCode = response.getStatusLine().getStatusCode();//响应状态码
if(statusCode == 200){//处理成功
log.info("成功200");
}else if(statusCode==204){//处理成功,无返回body
log.info("成功204");
}else {
log.info("Native关单失败,响应码="+statusCode);
throw new IOException("request failed");
}
} finally {
response.close();
}
}
然后重新启动:
9、微信支付查单API
目前已经完成接口:
不过Native调起支付是一整体流程(先Native下单,然后返回二维码,支付成功异步通知,或者商家查询订单)
9.1、查单接口的调用
如果商户后台迟迟没有收到异步通知的结果,商户应该主动的去调用微信支付的查单接口。商户端应该向微信支付系统发起一个查询订单的这样一个接口调用。查询这个订单是否支付成功了。
需要在商户端的后台程序中设置一个定时任务。例如五分钟之后没有收到异步通知的结果,进行查单操作。
实际上我们不用在controll层创建接口,创建业务方法就可以了,因为这个查单的功能是要被一个定时任务来调用的,而不是controller层接口。但是创建这个接口可以方便我们测试。
第一个是根据transaction_id来查,这个transaction_id是在微信的支付结果通知中给我们返回的一个微信端的支付订单的唯一业务编号。
第二个是根据商户订单号查询,也就是我们的orderno
编码WxPayController
这里请求方式不是前面的post,是get
编写代码:WxPayServiceImpl
/**
* 查询订单
* @param orderNo
* @return
*/
@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);
/**
* Accept 是一个 HTTP 请求头,用于告诉服务器,客户端希望接受的响应内容类型(即返回的数据格式)。
* "application/json" 表示客户端希望服务器返回的响应是 JSON 格式 的数据。
* 这里希望微信服务器返回json数据格式
*/
httpGet.setHeader("Accept","application/json");
//完成签名并执行请求,这个wxPayclient对象是之前在WxpayConfig里面设置过
CloseableHttpResponse response = wxPayClient.execute(httpGet);
try {
String bodyAsString = EntityUtils.toString(response.getEntity());//响应体
int statusCode = response.getStatusLine().getStatusCode();//响应状态码
if(statusCode == 200){//处理成功
log.info("成功,返回结果="+bodyAsString);
}else if(statusCode==204){//处理成功,无返回body
log.info("成功");
}else {
log.info("Native下单失败,响应码="+statusCode+",返回结果"+bodyAsString);
throw new IOException("request failed");
}
return bodyAsString;
} finally {
response.close();
}
}
然后启动测试一下
9.2、定时查询订单接口
用的是spring中默认的定时任务,Spring Task
然后测试一下
都是间隔1秒
规则
测试一下每隔3秒执行
更多内容请看:
从第0秒开始,每隔30秒执行一次,查询创建超过5分钟,并且未支付的订单
启动后发现,目前没有超时5分钟的订单
过了五分钟后:
看看这些超时的订单到底是未支付还是用户已支付但是我们的商户端没有接受到回调通知。
1、如果是已经支付没有接受回调通知:我们就要修改订单状态。
2、如果是用户未支付:我们就需要关单
/**
* 根据订单号查询微信支付查单接口,核实订单状态
*
* 1、如果订单已支付,则更新商户端订单状态
*
* 2、如果订单未支付,则调用关单接口,关闭订单并更新商户端订单状态
* @param orderNo
*/
@Override
public void checkOrderStatus(String orderNo) throws Exception {
log.info("核实订单状态====>{}",orderNo);
//调用微信支付查单接口
String result = this.queryOrder(orderNo);
Gson gson = new Gson();
HashMap resultMap = gson.fromJson(result, HashMap.class);
//获取微信支付端的订单状态
String tradeState = (String) resultMap.get("trade_state");
//处理订单状态
//本地订单状态和远程微信端订单状态是不一样的,
//例如如果我远程微信端的订单状态是关闭,再本地订单状态有可能是订单超时关闭,也有可能是用户取消的订单进行关闭。
if(WxTradeState.SUCCESS.getType().equals(tradeState)){
log.info("订单已支付,更新商户端订单状态");
//如果确认订单已支付则更新本地订单状态
orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.SUCCESS);
//记录支付日志
//查单接口返回的数据恰好和我们的支付通知返回的密文解密后的明文是一样的,所以我们可以将result直接赋值给它
paymentInfoService.createPaymentInfo(result);
}
if(WxTradeState.NOTPAY.getType().equals(tradeState)){
log.info("订单未支付,调用关单API");
this.closeOrder(orderNo);
//更新商户端的订单状态
//注意和之前取消订单不同的是,虽然在微信那里都是关单,
//但是之前是我们主动取消,这里是超时关闭【本地商户系统描述的说法不同】
orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.CLOSED);
//订单未支付不用记录支付日志
}
}
总结一下:就是先查询超时5分钟未支付的订单,然后执行:
1、查询微信端订单,如果微信端那边是已经支付的状态,则更新本地的订单状态以及记录日志。
2、如果是其实用户未支付,则进行超时关单。
然后重新启动来测试一下(用1分钟超时进行测试)
1、首先是用户确实没有支付:
数据库:
然后
2、如果是没有通知回调,比我改一下通知地址:用错误的通知地址
实际上我已经支付,但是由于通知地址改了,微信通知不到我的商户端,所以数据库的状态没有改变
然后我们等一下, 等商户系统开始主动查询订单。然后去修改数据库【注意这里时间改长一点,不然可能等一会订单就关闭了】
我改回五分钟,五分钟之后查看数据库发现订单状态已经改变:
10、申请退款API(*)
https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_9.shtml
到目前为止,应用程序的核心代码大部分已经完成了。还有一个退款功能,当用户点击退款进行退款。she
退款和下单是非常类似的
这个支付系统退款单号是我们向微信支付系统发起退款申请,微信支付端针对每一笔退款会生成一个退款记录,这个就是那个退款记录唯一的编号。和这个有点类似:
private String transactionId;//支付系统交易编号
退款金额,是支持部分退款的。
下面这个退款表相当于调用支付的两个表
10.1、创建退款单
(1)根据订单号查询订单
(2)创建退款单记录
10.2、申请退款
(1)WxPayController
下面是postmapping,写错了
(2)WxPayService
/**
* 申请退款
* @param orderNo
* @param reason
*/
@Override
public void refund(String orderNo, String reason) throws Exception{
log.info("创建退款单记录");
//根据订单编号创建退款单记录、
RefundInfo refundInfo=refundInfoService.createRefundByOrderNo(orderNo,reason);
log.info("调用退款API");
//调用申请退款API
String url=wxPayConfig.getDomain().concat(WxApiType.DOMESTIC_REFUNDS.getType());
HttpPost httpPost=new HttpPost(url);
//构造请求body参数
Gson gson = new Gson();
Map paramsMap = new HashMap<>();
paramsMap.put("out_trade_no",orderNo);//订单编号
paramsMap.put("out_refund_no",refundInfo.getRefundNo());//退款单编号
paramsMap.put("reason",reason);//退款原因
paramsMap.put("notify_url",wxPayConfig.getNotifyDomain().concat(WxNotifyType.REFUND_NOTIFY.getType()));//退款结果通知的回调地址
Map amountMap=new HashMap();
amountMap.put("refund",refundInfo.getRefund());//退款金额
amountMap.put("total",refundInfo.getTotalFee());//原订单金额
amountMap.put("currency","CNY");//退款币种
paramsMap.put("amount",amountMap);
//将参数转换成json字符串
String jsonParams = gson.toJson(paramsMap);
log.info("请求参数:{}",jsonParams);
StringEntity entity = new StringEntity(jsonParams,"utf-8");
entity.setContentType("application/json");//设置请求报文格式
httpPost.setEntity(entity);//将请求报文放入请求对象
httpPost.setHeader("Accept","application/json");//设置响应报文格式
//完成签名并执行请求,并完成验签
CloseableHttpResponse response = wxPayClient.execute(httpPost);
try {
//解析响应结果
String bodyAsString = EntityUtils.toString(response.getEntity());//响应体
int statusCode = response.getStatusLine().getStatusCode();//响应状态码
if(statusCode == 200){//处理成功
log.info("成功,退款返回结果="+bodyAsString);
}else if(statusCode==204){//处理成功,无返回body
log.info("成功");
}else {
log.info("退款异常,响应码="+statusCode+",退款返回结果"+bodyAsString);
throw new IOException("request failed");
}
//更新订单状态
orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.REFUND_PROCESSING);
//更新退款单
refundInfoService.updateRefund(bodyAsString);
} finally {
response.close();
}
}
1、创建退款单
2、然后调用退款api
3、更新订单状态(退款中)、更新退款订单(曾经创建了退款单记录,但是这个退款单记录只有一些初始化的信息,当我们的退款生气完成之后,这个退款申请api会给我们响应一些数据。这些数据有些非常有用,我们需要把他们存储在我们的退款单里面去,也就是存储到数据库中的refundinfo里面。
下面是更新退款订单记录
这里有查询退款和申请退款,两个都有返回值
其次是退款结果通知
目前是在申请退款中调用的updateRefund
后面还会在查询退款和退款回调中调用。
然后就可以重新启动程序了,点击退款
然后
数据库记录的也是退款处理中,因为还没有退款通知回调目前
虽然记录的是退款中,但是已经退款,只是退款通知回调还没有确认。
总结,目前应该由退款通知或者查询退款订单来修改最终状态【虽然目前的钱已经退了】
11、查询退款(*)
API 文档:https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_10.shtml
查询订单api是在我们的定时任务当中,当没有收到支付结果通知的时候,商户端会主动发起一个查询。
查询退款也是一样的。没有接受到退款结果通知的情况下,我们商户端会主动发起一个查询退款的一个请求,也是被定时任务调用的。
11.1、查单接口的调用
(1)WxPayController
(2)WxPayService
/**
* 查询退款接口调用
* @param refundNo
* @return
*/
@Override
public String queryRefund(String refundNo) throws Exception {
log.info("查询退款接口调用====》{}",refundNo);
String url=String.format(WxApiType.DOMESTIC_REFUNDS_QUERY.getType(),refundNo);
url=wxPayConfig.getDomain().concat(url);
//创建远程Get请求对象
HttpGet httpGet=new HttpGet(url);
httpGet.setHeader("Accept","application/json");
//完成签名并执行请求,并完成验签
CloseableHttpResponse response = wxPayClient.execute(httpGet);
try {
String bodyAsString = EntityUtils.toString(response.getEntity());
int statusCode = response.getStatusLine().getStatusCode();
if(statusCode == 200){//处理成功
log.info("成功,查询退款返回结果="+bodyAsString);
}else if(statusCode==204){//处理成功,无返回body
log.info("成功");
}else {
log.info("查询退款异常,响应码="+statusCode+",查询退款返回结果"+bodyAsString);
throw new IOException("request failed");
}
return bodyAsString;
}finally {
response.close();
}
}
然后在swagger里面进行测试
这里的状态变成success了,不是之前的处理中了
{
"amount": {
"currency": "CNY",
"discount_refund": 0,
"from": [],
"payer_refund": 1,
"payer_total": 1,
"refund": 1,
"refund_fee": 0,
"settlement_refund": 1,
"settlement_total": 1,
"total": 1
},
"channel": "ORIGINAL",
"create_time": "2025-01-07T17:19:30+08:00",
"funds_account": "UNAVAILABLE",
"out_refund_no": "REFUND_20250107171930808",
"out_trade_no": "ORDER_20250107153805078",
"promotion_detail": [],
"refund_id": "50300802002025010789569935403",
"status": "SUCCESS",
"success_time": "2025-01-07T17:19:33+08:00",
"transaction_id": "4200002559202501077952235928",
"user_received_account": "支付用户零钱"
}
11.2、定时查找退款中的订单
(1)WxPayTask
(2)RefundInfoService
11.3、处理超时未退款订单
我们可以启动测试一下
五分钟后
12、退款结果通知API(*)
文档: https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_11.shtml
12.1、接收退款通知
退款结果通知和支付结果通知几乎一模一样。
一个地方不一样
12.2、处理订单和退款单
/**
* 处理退款单
* @param bodyMap
*/
@Transactional(rollbackFor = Exception.class)
@Override
public void processRefund(HashMap<String, Object> bodyMap) throws Exception {
log.info("退款单");
//解密报文
String plainText = decryptFromResource(bodyMap);
//将明文转换成map
Gson gson = new Gson();
Map plainTextMap = gson.fromJson(plainText, Map.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);
//更新退款单
refundInfoService.updateRefund(plainText);
}finally {
//释放锁
lock.unlock();
}
}
}
然后重新启动
原来是退款中,现在是已退款。
13、账单
账单用于账号对账
13.1、申请交易账单和资金账单(*)(*)
(1)WxPayController
(2)WxPayService
/**
* 申请账单
* @param billDate
* @param type
* @return
*/
@Override
public String queryBill(String billDate, String type) throws Exception {
log.warn("申请账单接口调用====》{}",billDate);
String url="";
if("tradebill".equals(type)){
url=WxApiType.TRADE_BILLS.getType();
}else if("fundflowbill".equals(type)){
url=WxApiType.FUND_FLOW_BILLS.getType();
}else {
throw new RuntimeException("不支持的账单类型");
}
url=wxPayConfig.getDomain().concat(url).concat("?bill_date=").concat(billDate);
//创建远程Get请求对象
HttpGet httpGet=new HttpGet(url);
httpGet.addHeader("Accept","application/json");
//完成签名并执行请求,并完成验签
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();
}
}
然后启动测试 Swagger UI
这个url不能直接用,必须用作下面接口的请求url。
13.2、下载账单(*)
(1)WxPayController
(2)WxPayService
要调用这个
启动
或者
至此接口全部编写完毕
除了Native调起支付,10个接口都已经编写完毕(目录中打了*号的)