一、微信支付接入与介绍
1、微信支付产品介绍
付款码支付
用户展示微信钱包内的“付款码”给商家,商家扫描后直接完成支付,适用于线下面对面收银的场景。
JSAPI支付
- 线下场所:商户展示一个支付二维码,用户使用微信扫描二维码后,输入需要支付的金额,完成支 付。
- 公众号场景:用户在微信内进入商家公众号,打开某个页面,选择某个产品,完成支付。
- PC网站场景:在网站中展示二维码,用户使用微信扫描二维码,输入需要支付的金额,完成支付。
特点:用户在客户端输入支付金额
小程序支付
在微信小程序平台内实现支付的功能。
Native支付
Native支付是指商户展示支付二维码,用户再用微信“扫一扫”完成支付的模式。这种方式适用于PC网站。
特点:商家预先指定支付金额
APP支付
商户通过在移动端独立的APP应用程序中集成微信支付模块,完成支付。
刷脸支付
用户在刷脸设备前通过摄像头刷脸、识别身份后进行的一种支付方式。
2、接入指引
1、获取商户号
微信商户平台:https://pay.weixin.qq.com/ 场景:Native支付
步骤:提交资料 => 签署协议 => 获取商户号
2、获取APPID
微信公众平台:https://mp.weixin.qq.com/
步骤:注册服务号 => 服务号认证 => 获取APPID => 绑定商户号
3、获取API秘钥
APIv2版本的接口需要此秘钥
步骤:登录商户平台 => 选择 账户中心 => 安全中心 => API安全=>设置API密钥
4、获取APIv3秘钥
APIv3版本的接口需要此秘钥
步骤:登录商户平台 => 选择 账户中心 => 安全中心 =>API安全 => 设置APIv3密钥
5、申请商户API证书
商户API证书是指由商户申请的,包含商户的商户号、公司名称、公钥信息的证书。
APIv3版本的所有接口都需要;APIv2版本的高级接口需要(如:退款、企业红包、企业付款等)
步骤:登录商户平台 =>选择 账户中心 => 安全中心 => API安全 => 申请API证书
商户证书在商户后台申请:https://pay.weixin.qq.com/index.php/core/cert/api_cert#/
6、获取微信平台证书
微信支付平台证书是指由微信支付 负责申请的,包含微信支付平台标识、公钥信息的证书。商户可以使用平台证书中的公钥进行验签。
可以预先下载,也可以通过编程的方式获取。后面的课程中,我们会通过编程的方式来获取
平台证书的获取:https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay3_0.shtml
注意:以上所有API秘钥和证书需妥善保管防止泄露
二、支付安全基础(证书/秘钥/签名)
1、安全基础
**明文:**加密前的消息叫“明文”(plain text)
**密文:**加密后的文本叫“密文”(cipher text)
**密钥:**只有掌握特殊“钥匙”的人,才能对加密的文本进行解密,这里的“钥匙”就叫做“密钥”(key)
“密钥”就是一个字符串,度量单位是“位”(bit),比如,密钥长度是 128,就是 16 字节的二进制串
**加密:**实现机密性最常用的手段是“加密”(encrypt)
按照密钥的使用方式,加密可以分为两大类:对称加密和非对称加密。
**解密:**使用密钥还原明文的过程叫“解密”(decrypt)
**加密算法:**加密解密的操作过程就是“加密算法”
所有的加密算法都是公开的,而算法使用的“密钥”则必须保密
2、对称加密和非对称加密
对称加密
- 特点:只使用一个密钥,密钥必须保密,常用的有AES算法
- 优点:运算速度快
- 缺点:秘钥需要信息交换的双方共享,一旦被窃取,消息会被破解,无法做到安全的密钥交换
非对称加密
- 特点:使用两个密钥:公钥和私钥,公钥可以任意分发而私钥保密,常用的有RSA
- 优点:黑客获取公钥无法破解密文,解决了密钥交换的问题
- 缺点:运算速度非常慢
混合加密
实际场景中把对称加密和非对称加密结合起来使用
身份认证
- 公钥加密,私钥解密的作用是加密信息
- 私钥加密,公钥解密的作用是身份认证
3、摘要算法
摘要算法就是我们常说的散列函数、哈希函数(Hash Function),它能够把任意长度的数据“压缩”成固定长度、而且独一无二的“摘要”字符串,就好像是给这段数据生成了一个数字“指纹”。
摘要算法的作用是保证信息的完整性
特性:
- 不可逆:只有算法,没有秘钥,只能加密,不能解密难题友好性:想要破解,只能暴力枚举
- 发散性:只要对原文进行一点点改动,摘要就会发生剧烈变化抗碰撞性:原文不同,计算后的摘要也要不同
常见摘要算法:
MD5、SHA1、SHA2(SHA224、SHA256、SHA384)
4、数字签名与证书
4.1 数字签名
数字签名是使用私钥对摘要加密生成签名,需要由公钥将签名解密后进行验证,实现身份认证和不可否认。但是黑客可以伪造公钥与客户进行通信
4.2 数字证书
数字证书解决“公钥的信任”问题,可以防止黑客伪造公钥。个人不能直接分发公钥,公钥的分发必须使用数字证书,数字证书由CA(证书颁发机构)颁发
4.3 https协议中的数字证书
三、基础支付API V3
1、支付配置准备
1.1 引入支付参数
注:这里使用了尚硅谷的资料
新建 wxpay.properties
文件并放置在resources文件夹下
# 微信支付相关参数
# 商户号
wxpay.mch-id=1558950191
# 商户API证书序列号
wxpay.mch-serial-no=34345964330B66427E0D3D28826C4993C77E631F
# 商户私钥文件,放在工程目录下
wxpay.private-key-path=apiclient_key.pem
# APIv3密钥
wxpay.api-v3-key=UDuLFDcmy5Eb6o0nTNZdu6ek4DDh4K8B
# APPID
wxpay.appid=wx74862e0dfcf69954
# 微信服务器地址
wxpay.domain=https://api.mch.weixin.qq.com
# 接收结果通知地址
# 注意:每次重新启动ngrok,都需要根据实际情况修改这个配置
wxpay.notify-domain=https://500c-219-143-130-12.ngrok.io
# APIv2密钥
wxpay.partnerKey: T6m9iK73b0kn9g5v426MKfHQH7X8rKwb
新建 WxPayConfig.java
获取配置文件信息
@Configuration
@PropertySource("classpath:wxpay.properties") //读取配置文件
@ConfigurationProperties(prefix="wxpay") //读取wxpay节点
@Data //使用set方法将wxpay节点中的值填充到当前类的属性中
@Slf4j
public class WxPayConfig {
// 商户号
private String mchId;
// 商户API证书序列号
private String mchSerialNo;
// 商户私钥文件
private String privateKeyPath;
// APIv3密钥
private String apiV3Key;
// APPID
private String appid;
// 微信服务器地址
private String domain;
// 接收结果通知地址
private String notifyDomain;
// APIv2密钥
private String partnerKey;
/**
* 获取商户的私钥文件
* @param filename
* @return
*/
private PrivateKey getPrivateKey(String filename){
try {
return PemUtil.loadPrivateKey(new FileInputStream(filename));
} catch (FileNotFoundException e) {
throw new RuntimeException("私钥文件不存在", e);
}
}
/**
* 获取签名验证器
* @return
*/
@Bean
public ScheduledUpdateCertificatesVerifier getVerifier(){
log.info("获取签名验证器");
//获取商户私钥
PrivateKey privateKey = getPrivateKey(privateKeyPath);
//私钥签名对象
PrivateKeySigner privateKeySigner = new PrivateKeySigner(mchSerialNo, privateKey);
//身份认证对象
WechatPay2Credentials wechatPay2Credentials = new WechatPay2Credentials(mchId, privateKeySigner);
// 使用定时更新的签名验证器,不需要传入证书
ScheduledUpdateCertificatesVerifier verifier = new ScheduledUpdateCertificatesVerifier(
wechatPay2Credentials,
apiV3Key.getBytes(StandardCharsets.UTF_8));
return verifier;
}
/**
* 获取http请求对象
* @param verifier
* @return
*/
@Bean(name = "wxPayClient")
public CloseableHttpClient getWxPayClient(ScheduledUpdateCertificatesVerifier verifier){
log.info("获取httpClient");
//获取商户私钥
PrivateKey privateKey = getPrivateKey(privateKeyPath);
WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create()
.withMerchant(mchId, mchSerialNo, privateKey)
.withValidator(new WechatPay2Validator(verifier));
// ... 接下来,你仍然可以通过builder设置各种参数,来配置你的HttpClient
// 通过WechatPayHttpClientBuilder构造的HttpClient,会自动的处理签名和验签,并进行证书自动更新
CloseableHttpClient httpClient = builder.build();
return httpClient;
}
/**
* 获取HttpClient,无需进行应答签名验证,跳过验签的流程
*/
@Bean(name = "wxPayNoSignClient")
public CloseableHttpClient getWxPayNoSignClient(){
//获取商户私钥
PrivateKey privateKey = getPrivateKey(privateKeyPath);
//用于构造HttpClient
WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create()
//设置商户信息
.withMerchant(mchId, mchSerialNo, privateKey)
//无需进行签名验证、通过withValidator((response) -> true)实现
.withValidator((response) -> true);
// 通过WechatPayHttpClientBuilder构造的HttpClient,会自动的处理签名和验签,并进行证书自动更新
CloseableHttpClient httpClient = builder.build();
log.info("== getWxPayNoSignClient END ==");
return httpClient;
}
}
最后为了让IDEA可以识别配置文件,将配置文件的图标展示成SpringBoot的图标,同时配置文件的内容可以高亮显示,让配置文件和Java代码之间的对应参数可以自动定位,方便开发,这里需要配置Annotation Processor
maven依赖,同时进入File -> Project Structure -> Modules -> 选择小叶子- > 选择+号- >选中配置文件
,即可配置成功
<!-- 生成自定义配置的元数据信息 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
1.2 加载商户私钥
https://github.com/wechatpay-apiv3/wechatpay-apache-httpclient
引入微信SDK详情查看官网,搜索如何加载商户私钥,实现了请求签名的生成和应答签名的验证
<dependency>
<groupId>com.github.wechatpay-apiv3</groupId>
<artifactId>wechatpay-apache-httpclient</artifactId>
<version>0.4.1</version>
</dependency>
1.3 获取签名验证器和HttpClient
流程如图所示,其余可查看Github官网,1.1中已经具体写明
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0Po2JQaN-1649921278437)(https://pay.weixin.qq.com/wiki/doc/apiv3/assets/img/common/ico-guide/chapter1_5_1.png “”)]
1.4 API字典和相关工具
https://pay.weixin.qq.com/wiki/doc/apiv3/open/pay/chapter2_7_3.shtml
微信Native支付的API列表
同时微信支付 APIv3 使用 JSON 作为消息体的数据交换格式,因此需要引入json转换依赖
<!--json处理-->
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
</dependency>
最后将Native支付接口写成枚举类,方便调用,以及HttpUtil工具类
//举例
@AllArgsConstructor
@Getter
public enum PayType {
/**
* 微信
*/
WXPAY("微信"),
/**
* 支付宝
*/
ALIPAY("支付宝");
/**
* 类型
*/
private final String type;
}
public class HttpUtils {
/**
* 将通知参数转化为字符串
* @param request
* @return
*/
public static String readData(HttpServletRequest request) {
BufferedReader br = null;
try {
StringBuilder result = new StringBuilder();
br = request.getReader();
for (String line; (line = br.readLine()) != null; ) {
if (result.length() > 0) {
result.append("\n");
}
result.append(line);
}
return result.toString();
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
if (br != null) {
try {
br.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
1.5 设置全局返回类
// 全局返回
@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;
}
}
2、签名和验签解析
2.1 签名
https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay4_0.shtml
2.2 验签
https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay4_1.shtml
3、Native支付
3.1 Native支付流程
https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_4.shtml
3.2 Native下单Api
https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_1.shtml
https://pay.weixin.qq.com/wiki/doc/apiv3/open/pay/chapter2_7_2.shtml
商户端发起支付请求,微信端创建支付订单并生成支付二维码链接,微信端将支付二维码返回给商户 端,商户端显示支付二维码,用户使用微信客户端扫码后发起支付
/**
* 创建订单,调用Native支付接口
* @param productId
* @return code_url 和 订单号
* @throws Exception
*/
@Transactional(rollbackFor = Exception.class)
@Override
public Map<String, Object> nativePay(Long productId) throws Exception {
log.info("生成订单");
//生成订单
OrderInfo orderInfo = orderInfoService.createOrderByProductId(productId);
String codeUrl = orderInfo.getCodeUrl();
if(orderInfo != null && !StringUtils.isEmpty(codeUrl)){
log.info("订单已存在,二维码已保存");
//返回二维码
Map<String, Object> map = new HashMap<>();
map.put("codeUrl", codeUrl);
map.put("orderNo", orderInfo.getOrderNo());
return map;
}
log.info("调用统一下单API");
//调用统一下单API
HttpPost httpPost = new HttpPost(wxPayConfig.getDomain().concat(WxApiType.NATIVE_PAY.getType()));
// 请求body参数
Gson gson = new Gson();
Map paramsMap = new HashMap();
paramsMap.put("appid", wxPayConfig.getAppid());
paramsMap.put("mchid", wxPayConfig.getMchId());
paramsMap.put("description", orderInfo.getTitle());
paramsMap.put("out_trade_no", orderInfo.getOrderNo());
paramsMap.put("notify_url", wxPayConfig.getNotifyDomain().concat(WxNotifyType.NATIVE_NOTIFY.getType()));
Map amountMap = new HashMap();
amountMap.put("total", orderInfo.getTotalFee());
amountMap.put("currency", "CNY");
paramsMap.put("amount", amountMap);
//将参数转换成json字符串
String jsonParams = gson.toJson(paramsMap);
log.info("请求参数 ===> {}" + jsonParams);
StringEntity entity = new StringEntity(jsonParams,"utf-8");
entity.setContentType("application/json");
httpPost.setEntity(entity);
httpPost.setHeader("Accept", "application/json");
//完成签名并执行请求
CloseableHttpResponse response = wxPayClient.execute(httpPost);
try {
String bodyAsString = EntityUtils.toString(response.getEntity());//响应体
int statusCode = response.getStatusLine().getStatusCode();//响应状态码
if (statusCode == 200) { //处理成功
log.info("成功, 返回结果 = " + bodyAsString);
} else if (statusCode == 204) { //处理成功,无返回Body
log.info("成功");
} else {
log.info("Native下单失败,响应码 = " + statusCode+ ",返回结果 = " + bodyAsString);
throw new IOException("request failed");
}
//响应结果
Map<String, String> resultMap = gson.fromJson(bodyAsString, HashMap.class);
//二维码
codeUrl = resultMap.get("code_url");
//保存二维码
String orderNo = orderInfo.getOrderNo();
orderInfoService.saveCodeUrl(orderNo, codeUrl);
//返回二维码
Map<String, Object> map = new HashMap<>();
map.put("codeUrl", codeUrl);
map.put("orderNo", orderInfo.getOrderNo());
return map;
} finally {
response.close();
}
}
使用微信SDK自带的验签和签名函数,进行订单创建,同时为了减轻服务器压力,需要缓存二维码,同时定时检查二维吗url是否已经过期,并同步到数据库
3.3 支付通知API
https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_5.shtml
微信支付通过支付通知接口将用户支付成功消息通知给商户
- 内网穿透
这里使用了ngrok作为内网穿透工具,原因是微信支付回调函数需要通知地址,而我们的程序一般都部署在内网;同时针对微信的回调函数,需要考虑失败、超时等情况,否则微信会持续发送回调通知,直到成功返回 - 验签工具
参考SDK源码中的 WechatPay2Validator 创建通知验签工具类 WechatPay2ValidatorForRequest - 解密工具
验签成功的签名串经过对称加密,需要进行api-v3-key
秘钥进行解密,证书和回调解密的AesGcm解密参考AesUtil.java
- 处理订单
获取微信支付回调函数后,需要更新订单状态以及插入支付记录,同时使用可重入锁来处理并发、重复请求,保证订单记录的唯一性与可靠性
4、微信支付查单API
https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_2.shtml
查单接口是为了确定用户已经完成支付但却未回调到系统的情况,需要增加定时任务,定时查找超时订单与处理超时订单等
@Slf4j
@Component
public class WxPayTask {
@Resource
private OrderInfoService orderInfoService;
@Resource
private WxPayService wxPayService;
@Resource
private RefundInfoService refundInfoService;
/**
* 秒 分 时 日 月 周
* 以秒为例
* *:每秒都执行
* 1-3:从第1秒开始执行,到第3秒结束执行
* 0/3:从第0秒开始,每隔3秒执行1次
* 1,2,3:在指定的第1、2、3秒执行
* ?:不指定
* 日和周不能同时制定,指定其中之一,则另一个设置为?
*/
//@Scheduled(cron = "0/3 * * * * ?")
public void task1(){
log.info("task1 被执行......");
}
/**
* 从第0秒开始每隔30秒执行1次,查询创建超过5分钟,并且未支付的订单
*/
//@Scheduled(cron = "0/30 * * * * ?")
public void orderConfirm() throws Exception {
log.info("orderConfirm 被执行......");
List<OrderInfo> orderInfoList = orderInfoService.getNoPayOrderByDuration(1, PayType.WXPAY.getType());
for (OrderInfo orderInfo : orderInfoList) {
String orderNo = orderInfo.getOrderNo();
log.warn("超时订单 ===> {}", orderNo);
//核实订单状态:调用微信支付查单接口
wxPayService.checkOrderStatus(orderNo);
}
}
/**
* 从第0秒开始每隔30秒执行1次,查询创建超过5分钟,并且未成功的退款单
*/
//@Scheduled(cron = "0/30 * * * * ?")
public void refundConfirm() throws Exception {
log.info("refundConfirm 被执行......");
//找出申请退款超过5分钟并且未成功的退款单
List<RefundInfo> refundInfoList = refundInfoService.getNoRefundOrderByDuration(1);
for (RefundInfo refundInfo : refundInfoList) {
String refundNo = refundInfo.getRefundNo();
log.warn("超时未退款的退款单号 ===> {}", refundNo);
//核实订单状态:调用微信支付查询退款接口
wxPayService.checkRefundStatus(refundNo);
}
}
}
5、微信支付退款API
退款回调和通知回调类似
6、微信支付账单下载
https://pay.weixin.qq.com/wiki/doc/apiv3_partner/apis/chapter4_4_8.shtml
前端Vue下载函数
//下载账单
downloadBill(type){
//获取账单内容
billApi.downloadBill(this.billDate, type).then(response => {
console.log(response)
//response.data.result是后端传输的数据流,后端已经从微信支付下载完毕数据,
//将其变成base64data,前端进行excel下载
const element = document.createElement('a')
element.setAttribute('href', 'data:application/vnd.ms-excel;charset=utf-8,' + encodeURIComponent(response.data.result))
element.setAttribute('download', this.billDate + '-' + type)
element.style.display = 'none'
element.click()
})
}
7、基础支付APIv2
四、支付宝支付介绍与环境准备
1、接入介绍
支付宝开发者开发平台:https://open.alipay.com/
1.1 常规接入流程
例如网页&移动应用:https://opendocs.alipay.com/open/200
- 创建应⽤:选择应⽤类型、填写应⽤基本信息、添加应⽤功能、配置应⽤环境(获取⽀付宝公钥、应⽤公钥、应⽤私钥、⽀付宝⽹关地址,配置接⼝内容加密⽅式)、查看 APPID
- 绑定应⽤:将开发者账号中的APPID和商家账号PID进⾏绑定
- 配置秘钥:即创建应⽤中的“配置应⽤环境”步骤
- 上线应⽤:将应⽤提交审核
- 签约功能:在商家中⼼上传营业执照、已备案⽹站信息等,提交审核进⾏签约
1.2 使用沙箱
- 沙箱环境配置:https://opendocs.alipay.com/common/02kkv7
- 沙箱版支付宝下载与登录:https://open.alipay.com/platform/appDaily.htm?tab=tool
2、支付参数引入
2.1 引入配置文件
创建alipay.properties
文件,里面存放支付宝相关参数,这里使用了尚硅谷的真实支付宝参数,沙箱版自行替换参数
# 支付宝支付相关参数
# 应用ID,您的APPID,收款账号既是您的APPID对应支付宝账号
alipay.app-id=2021003124617201
# 商户PID,卖家支付宝账号ID
alipay.seller-id=2088102040215494
# 支付宝网关
alipay.gateway-url=https://openapi.alipay.com/gateway.do
# 商户私钥,您的PKCS8格式RSA2私钥
alipay.merchant-private-key=MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDmMcbk+ezXjrwG2kTIQugXLpMJnl8b112Bq+TV/yQgL2oMC2alYCXDzyHWyjXLHhpeb9MVrKKqqdifcv3+r0U2rclsmoBVIdmlpk9E3Hi0Ulb7qIoYwgrPUMpSQssCPnyCqoN7Xg+y7PqZhgHQpBOF34lGokc1hAVyHHIt+JybTvDaWr00Em9NwGslw3oV8mDXA1rpoPSEpMyxrGW6kFaOlX08zsYoLJUgL9VkQAJ/WUm5gMpQArSzO9MFL3VwnyEBg2KBNlxuDwj+PJ6D3RB6o4SWID8X7y/UCscNLwTmVr2qLf6zSf3GtX+/jnXSVLTtqqL8bnZNFXKNbfoWNH5ZAgMBAAECggEAVEROkg3npLVMoZmPalwLyEi1bOT73h5Fza1WVPxUhi+1O3mE9u8ug/K0aYOWk6eOcZmwBRQwbBdHBH+8+VnCFZUi0k3wwrlkil5KUGQBD8nAq9lzzEJkYKYrmld3J3gmblLrVOMHDjHwPvkueulFeFFvWFsZhD6zG6XMKoYDFlrq1k4yCfimMrPTSLRxhUtkdbPXt0V8vTgrX9D5z9wFiAebjzRhRRhbxWH4xkdy5WaPI7z+zakIjjrWECdXEVVF7BvahBv1dGtKCGjwRwPw15Bc336yZ2Gqfa9Il06PHY3XCB5YCtE31T1s+Wm620hbtJ1Dk3sDnspoYpV8ZnPHLQKBgQD0QOHErtE6OcFMezWLWjbtak1QWUuxdCYtmVCgWqpPpe/JuFSKUYy6AF6Bd5RBJOa4i+RtrZuMAjM0QzZ/6gCZyWzMo0aZB9KhtC3ftpAKWvlmFdvH08I0GKd/PLY/Wcz5YIgByhcV2VZ410NH2u1nVqKL+jIfpzJOzM3zsLFGdwKBgQDxQ87dsVPxdwqcvAkv4WwFY0Dv1932uscyb5ZjkasWtiQ6TPrfzzkT13Zu81vqXL9eaiTgaP8bYeVUQVVScFiO0r2Ylp6TJzp3nl3qEm8N4DmoA/Vwo5TXoR55HgDUMBv7DMzcBa/PU5J1ErZ1pt6k2uBD0U3sMxIRP+VBpsMFrwKBgQCENM4/GGS1gGdpT1NXHziV3zED6aF35qd3jQHAGfMPc4DMDdLsn2FtmB+PMjtz21Zq04WL/Ckyakpu4maQbAdxNj6GsWXYFQzka9NcwMNMZ5uQrwosKil261VWIHWA6slwvdhAJ7PBJseQVuva69wOUC1hWMZirawkTOS5H42E1wKBgQCuDt6CgDlwXhKQ6vOx0G6vIGEr58/h/fRSBcE4ylHlS7itOvZPW1/xWaO+/eFVHl6NzgQWxokthx39ADl/BUBOoelY2WlD/qwmumFEytHF7/uIpHqBLfLm8f1bIfM1IhQ9tYliPtQMvl1OCxcJoD7GLoZXRvxxqJKjUTaje5z9TwKBgQDul1tsUHKpuWyoXoHQ6j0zqzMPgPohvFGN1fV6MBpHHFDDCLajuCrpj/gQPWcwRpOYArZK7u0cyvn/sWijbdL1v6RtjYt7sASiDRbf2FFraLeRJOu/KKkFZ5UZbXWiR2fs02+sTkLBD2MI16HquJzsS3rK+8Hitgdd/U3PyHDSyQ==
# 支付宝公钥,查看地址:https://openhome.alipay.com/platform/keyManage.htm 对应APPID下的支付宝公钥
alipay.alipay-public-key=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAguAlv4mG5/uvo5VjyU2ZAJuZR3VbSbhUN11DKYuNbtaGh9lzysF0N7ZbjWLb8E3TLYsvzSYiDwPUJyVU1uPLld9mXiLQ/k8FOAma1rCG7OFMhWtBWglZq2LxLN68qz8aIsUPbuaqiuIfF+zqg9dQ4y9CrhC18U5cCzpYmqoSbxZPMLGRE28qgs7m9FxmGBjttpa+oHHg2Jf1i79DOtbUCHTrK9Mr6Cfd47dAMQf0OdIuvD+fxYD3fF8tVeJWH+GyibMCojYn66lFUhR1TqKoKZtUxFCcnGbodEhoWr2iTlPTMQzm24EiYODod0xn3GWwigZcJma2tLruW51de6U+dwIDAQAB
# 接口内容加密秘钥,对称秘钥
alipay.content-key=pdF//ropAEqHXxI360iwUg==
# 页面跳转同步通知页面路径
alipay.return-url=http://localhost:8080/#/success
# 服务器异步通知页面路径 需http://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问
# 注意:每次重新启动ngrok,都需要根据实际情况修改这个配置
alipay.notify-url=https://a863-180-174-204-169.ngrok.io/api/ali-pay/trade/notify
2.2 创建配置文件
在config包创建AlipayClientConfig,这里使用Environment 获取参数,和上面微信支付获取参数方法不同
@Configuration
//加载配置文件
@PropertySource("classpath:alipay.properties")
public class AlipayClientConfig {
@Resource
private Environment config;
// 可以通过以下方式获取config.getProperty("alipay.app-id")
}
3、引入服务端SDK
3.1 引入依赖
参考⽂档:开放平台 => ⽂档 => 开发⼯具 => 服务端SDK => Java => 通⽤版 => Maven项⽬依赖https://search.maven.org/artifact/com.alipay.sdk/alipay-sdk-java
<dependency>
<groupId>com.alipay.sdk</groupId>
<artifactId>alipay-sdk-java</artifactId>
<version>4.23.21.ALL</version>
</dependency>
3.2 创建客户端连接对象
参考⽂档:开放平台 => ⽂档 => 开发⼯具 => 技术接⼊指南 => 数据签名
https://opendocs.alipay.com/common/02kf5q
参考⽂档中 公钥方式 完善 AlipayClientConfig
类,添加 alipayClient()
⽅法 初始化 AlipayClient
对象
@Configuration
//加载配置文件
@PropertySource("classpath:alipay.properties")
public class AlipayClientConfig {
@Resource
private Environment config;
@Bean
public AlipayClient alipayClient() throws AlipayApiException {
AlipayConfig alipayConfig = new AlipayConfig();
//设置网关地址
alipayConfig.setServerUrl(config.getProperty("alipay.gateway-url"));
//设置应用Id
alipayConfig.setAppId(config.getProperty("alipay.app-id"));
//设置应用私钥
alipayConfig.setPrivateKey(config.getProperty("alipay.merchant-private-key"));
//设置请求格式,固定值json
alipayConfig.setFormat(AlipayConstants.FORMAT_JSON);
//设置字符集
alipayConfig.setCharset(AlipayConstants.CHARSET_UTF8);
//设置支付宝公钥
alipayConfig.setAlipayPublicKey(config.getProperty("alipay.alipay-public-key"));
//设置签名类型
alipayConfig.setSignType(AlipayConstants.SIGN_TYPE_RSA2);
//构造client
AlipayClient alipayClient = new DefaultAlipayClient(alipayConfig);
return alipayClient;
}
}
五、支付宝支付功能开发
1、统一收单下单与支付
1.1 支付调用流程
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mWwD20cH-1653225486947)(https://secure2.wostatic.cn/static/uxiuZngjTEt6V1s3cK7vcG/image.png)]
1.2 接口说明
- 公共请求参数:所有接⼝都需要的参数
- 请求参数:当前接⼝需要的参数
- 公共响应参数:所有接⼝的响应中都包含的数据
- 响应参数:当前接⼝的响应中包含的数据
1.3 发起支付请求
后端核心
@Service
@Slf4j
public class AliPayServiceImpl implements AliPayService {
@Resource
private OrderInfoService orderInfoService;
@Resource
private AlipayClient alipayClient;
@Resource
private Environment config;
/**支付宝开放平台接受 request 请求对象后
* 会为开发者生成一个html 形式的 form表单,包含自动提交的脚本
* 我们将form表单字符串返回给前端程序,之后前端将会调用自动提交脚本,进行表单的提交
* 此时,表单会自动提交到action属性所指向的支付宝开放平台中,从而为用户展示一个支付页面
*/
@Transactional(rollbackFor = Exception.class)
@Override
public String tradeCreate(Long productId) {
try {
//生成订单
log.info("生成订单");
// 先查存在就直接返回,不存在就插入
OrderInfo orderInfo = orderInfoService.createOrderByProductId(productId, PayType.ALIPAY.getType());
//调用支付宝接口
AlipayTradePagePayRequest request = new AlipayTradePagePayRequest();
//配置需要的公共请求参数
//支付完成后,支付宝向谷粒学院发起异步通知的地址
request.setNotifyUrl(config.getProperty("alipay.notify-url"));
//支付完成后,我们想让页面跳转回谷粒学院的页面,配置returnUrl
request.setReturnUrl(config.getProperty("alipay.return-url"));
//组装当前业务方法的请求参数
JSONObject bizContent = new JSONObject();
bizContent.put("out_trade_no", orderInfo.getOrderNo());
BigDecimal total = new BigDecimal(orderInfo.getTotalFee().toString()).divide(new BigDecimal("100"));
bizContent.put("total_amount", total);
bizContent.put("subject", orderInfo.getTitle());
bizContent.put("product_code", "FAST_INSTANT_TRADE_PAY");
request.setBizContent(bizContent.toString());
//执行请求,调用支付宝接口
AlipayTradePagePayResponse response = alipayClient.pageExecute(request);
if(response.isSuccess()){
log.info("调用成功,返回结果 ===> " + response.getBody());
return response.getBody();
} else {
log.info("调用失败,返回码 ===> " + response.getCode() + ", 返回描述 ===> " + response.getMsg());
throw new RuntimeException("创建支付交易失败");
}
} catch (AlipayApiException e) {
e.printStackTrace();
throw new RuntimeException("创建支付交易失败");
}
}
}
前端核心
//确认支付
toPay() {
//禁用按钮,防止重复提交
this.payBtnDisabled = true
//微信支付
if (this.payOrder.payType === 'wxpay') {
...
//支付宝支付
} else if (this.payOrder.payType === 'alipay') {
//调用支付宝统一收单下单并支付页面接口
aliPayApi.tradePagePay(this.payOrder.productId).then((response) => {
//将支付宝返回的表单字符串写在浏览器中,表单会自动触发submit提交
document.write(response.data.formStr)
})
}
},
前端API方法
import request from '@/utils/request'
export default{
//发起支付请求
tradePagePay(productId) {
return request({
url: '/api/ali-pay/trade/page/pay/' + productId,
method: 'post'
})
}
}
2、支付结果通知
2.1 环境配置
在 AliPayServiceImpl
的 tradeCreate
⽅法中设置异步通知地址。同时启动ngrok内网穿透,上面微信支付已经说明,注意要修改成自己的配置
2.2 开发异步通知接口
AliPayController
@ApiOperation("支付通知")
@PostMapping("/trade/notify")
public String tradeNotify(@RequestParam Map<String, String> params){
log.info("支付通知正在执行");
log.info("通知参数 ===> {}", params);
String result = "failure";
try {
//异步通知验签
boolean signVerified = AlipaySignature.rsaCheckV1(
params,
config.getProperty("alipay.alipay-public-key"),
AlipayConstants.CHARSET_UTF8,
AlipayConstants.SIGN_TYPE_RSA2); //调用SDK验证签名
if(!signVerified){
//验签失败则记录异常日志,并在response中返回failure.
log.error("支付成功异步通知验签失败!");
return result;
}
// 验签成功后
log.info("支付成功异步通知验签成功!");
//按照支付结果异步通知中的描述,对支付结果中的业务内容进行二次校验,
//1 商户需要验证该通知数据中的 out_trade_no 是否为商户系统中创建的订单号
String outTradeNo = params.get("out_trade_no");
OrderInfo order = orderInfoService.getOrderByOrderNo(outTradeNo);
if(order == null){
log.error("订单不存在");
return result;
}
//2 判断 total_amount 是否确实为该订单的实际金额(即商户订单创建时的金额)
String totalAmount = params.get("total_amount");
int totalAmountInt = new BigDecimal(totalAmount).multiply(new BigDecimal("100")).intValue();
int totalFeeInt = order.getTotalFee().intValue();
if(totalAmountInt != totalFeeInt){
log.error("金额校验失败");
return result;
}
//3 校验通知中的 seller_id(或者 seller_email) 是否为 out_trade_no 这笔单据的对应的操作方
String sellerId = params.get("seller_id");
String sellerIdProperty = config.getProperty("alipay.seller-id");
if(!sellerId.equals(sellerIdProperty)){
log.error("商家pid校验失败");
return result;
}
//4 验证 app_id 是否为该商户本身
String appId = params.get("app_id");
String appIdProperty = config.getProperty("alipay.app-id");
if(!appId.equals(appIdProperty)){
log.error("appid校验失败");
return result;
}
//在支付宝的业务通知中,只有交易通知状态为 TRADE_SUCCESS时,
// 支付宝才会认定为买家付款成功。
String tradeStatus = params.get("trade_status");
if(!"TRADE_SUCCESS".equals(tradeStatus)){
log.error("支付未成功");
return result;
}
//处理业务 修改订单状态 记录支付日志
aliPayService.processOrder(params);
//校验成功后在response中返回success并继续商户自身业务处理,校验失败返回failure
result = "success";
} catch (AlipayApiException e) {
e.printStackTrace();
}
return result;
}
AliPayService
/**
* 处理订单
* @param params
*/
@Transactional(rollbackFor = Exception.class)
@Override
public void processOrder(Map<String, String> params) {
log.info("处理订单");
//获取订单号
String orderNo = params.get("out_trade_no");
/*在对业务数据进行状态检查和处理之前,
要采用数据锁进行并发控制,
以避免函数重入造成的数据混乱*/
//尝试获取锁:
// 成功获取则立即返回true,获取失败则立即返回false。不必一直等待锁的释放
if(lock.tryLock()) {
try {
//处理重复通知
//接口调用的幂等性:无论接口被调用多少次,以下业务执行一次
String orderStatus = orderInfoService.getOrderStatus(orderNo);
if (!OrderStatus.NOTPAY.getType().equals(orderStatus)) {
return;
}
//更新订单状态
orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.SUCCESS);
//记录支付日志
paymentInfoService.createPaymentInfoForAliPay(params);
} finally {
//要主动释放锁
lock.unlock();
}
}
}
3、统一收单交易关闭
/**
* 用户取消订单
* @param orderNo
*/
@Override
public void cancelOrder(String orderNo) {
//调用支付宝提供的统一收单交易关闭接口
this.closeOrder(orderNo);
//更新用户订单状态
orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.CANCEL);
}
/**
* 关单接口的调用
* @param orderNo 订单号
*/
private void closeOrder(String orderNo) {
try {
log.info("关单接口的调用,订单号 ===> {}", orderNo);
AlipayTradeCloseRequest request = new AlipayTradeCloseRequest();
JSONObject bizContent = new JSONObject();
bizContent.put("out_trade_no", orderNo);
request.setBizContent(bizContent.toString());
AlipayTradeCloseResponse response = alipayClient.execute(request);
if(response.isSuccess()){
log.info("调用成功,返回结果 ===> " + response.getBody());
} else {
log.info("调用失败,返回码 ===> " + response.getCode() + ", 返回描述 ===> " + response.getMsg());
//throw new RuntimeException("关单接口的调用失败");
}
} catch (AlipayApiException e) {
e.printStackTrace();
throw new RuntimeException("关单接口的调用失败");
}
}
注意:针对⼆维码⽀付,只有经过扫码的订单才在⽀付宝端有交易记录。针对⽀付宝账号⽀付,只有经过登录的订单才在⽀付宝端有交易记录。
4、统一收单交易查询
4.1 查单接口调用
商户后台未收到异步⽀付结果通知时,商户应该主动调⽤《统⼀收单线下交易查询接⼝》,同步订单状态。
/**
* 查询订单
* @param orderNo
* @return 返回订单查询结果,如果返回null则表示支付宝端尚未创建订单
*/
@Override
public String queryOrder(String orderNo) {
try {
log.info("查单接口调用 ===> {}", orderNo);
AlipayTradeQueryRequest request = new AlipayTradeQueryRequest();
JSONObject bizContent = new JSONObject();
bizContent.put("out_trade_no", orderNo);
request.setBizContent(bizContent.toString());
AlipayTradeQueryResponse response = alipayClient.execute(request);
if(response.isSuccess()){
log.info("调用成功,返回结果 ===> " + response.getBody());
return response.getBody();
} else {
log.info("调用失败,返回码 ===> " + response.getCode() + ", 返回描述 ===> " + response.getMsg());
//throw new RuntimeException("查单接口的调用失败");
return null;//订单不存在
}
} catch (AlipayApiException e) {
e.printStackTrace();
throw new RuntimeException("查单接口的调用失败");
}
}
4.2 定时查单
创建定时任务
@Slf4j
@Component
public class AliPayTask {
@Resource
private OrderInfoService orderInfoService;
@Resource
private AliPayService aliPayService;
/**
* 从第0秒开始每隔30秒执行1次,查询创建超过5分钟,并且未支付的订单
*/
@Scheduled(cron = "0/30 * * * * ?")
public void orderConfirm(){
log.info("orderConfirm 被执行......");
List<OrderInfo> orderInfoList = orderInfoService.getNoPayOrderByDuration(1, PayType.ALIPAY.getType());
for (OrderInfo orderInfo : orderInfoList) {
String orderNo = orderInfo.getOrderNo();
log.warn("超时订单 ===> {}", orderNo);
//核实订单状态:调用支付宝查单接口
aliPayService.checkOrderStatus(orderNo);
}
}
}
4.3 处理查询到的订单
/**
* 根据订单号调用支付宝查单接口,核实订单状态
* 如果订单未创建,则更新商户端订单状态
* 如果订单未支付,则调用关单接口关闭订单,并更新商户端订单状态
* 如果订单已支付,则更新商户端订单状态,并记录支付日志
* @param orderNo
*/
@Override
public void checkOrderStatus(String orderNo) {
log.warn("根据订单号核实订单状态 ===> {}", orderNo);
String result = this.queryOrder(orderNo);
//订单未创建
if(result == null){
log.warn("核实订单未创建 ===> {}", orderNo);
//更新本地订单状态
orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.CLOSED);
}
//解析查单响应结果
Gson gson = new Gson();
HashMap<String, LinkedTreeMap> resultMap = gson.fromJson(result, HashMap.class);
LinkedTreeMap alipayTradeQueryResponse = resultMap.get("alipay_trade_query_response");
String tradeStatus = (String)alipayTradeQueryResponse.get("trade_status");
if(AliPayTradeState.NOTPAY.getType().equals(tradeStatus)){
log.warn("核实订单未支付 ===> {}", orderNo);
//如果订单未支付,则调用关单接口关闭订单
this.closeOrder(orderNo);
// 并更新商户端订单状态
orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.CLOSED);
}
if(AliPayTradeState.SUCCESS.getType().equals(tradeStatus)){
log.warn("核实订单已支付 ===> {}", orderNo);
//如果订单已支付,则更新商户端订单状态
orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.SUCCESS);
//并记录支付日志
paymentInfoService.createPaymentInfoForAliPay(alipayTradeQueryResponse);
}
}
5、统一交易退款
/**
* 退款
* @param orderNo
* @param reason
*/
@Transactional(rollbackFor = Exception.class)
@Override
public void refund(String orderNo, String reason) {
try {
log.info("调用退款API");
//创建退款单
RefundInfo refundInfo = refundsInfoService.createRefundByOrderNoForAliPay(orderNo, reason);
//调用统一收单交易退款接口
AlipayTradeRefundRequest request = new AlipayTradeRefundRequest ();
//组装当前业务方法的请求参数
JSONObject bizContent = new JSONObject();
bizContent.put("out_trade_no", orderNo);//订单编号
BigDecimal refund = new BigDecimal(refundInfo.getRefund().toString()).divide(new BigDecimal("100"));
//BigDecimal refund = new BigDecimal("2").divide(new BigDecimal("100"));
bizContent.put("refund_amount", refund);//退款金额:不能大于支付金额
bizContent.put("refund_reason", reason);//退款原因(可选)
request.setBizContent(bizContent.toString());
//执行请求,调用支付宝接口
AlipayTradeRefundResponse response = alipayClient.execute(request);
if(response.isSuccess()){
log.info("调用成功,返回结果 ===> " + response.getBody());
//更新订单状态
orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.REFUND_SUCCESS);
//更新退款单
refundsInfoService.updateRefundForAliPay(
refundInfo.getRefundNo(),
response.getBody(),
AliPayTradeState.REFUND_SUCCESS.getType()); //退款成功
} else {
log.info("调用失败,返回码 ===> " + response.getCode() + ", 返回描述 ===> " + response.getMsg());
//更新订单状态
orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.REFUND_ABNORMAL);
//更新退款单
refundsInfoService.updateRefundForAliPay(
refundInfo.getRefundNo(),
response.getBody(),
AliPayTradeState.REFUND_ERROR.getType()); //退款失败
}
} catch (AlipayApiException e) {
e.printStackTrace();
throw new RuntimeException("创建退款申请失败");
}
}
6、收单退款冲退完成通知
退款存在退到银⾏卡场景下时,收单会根据银⾏回执消息发送退款完成信息。开发流程类似⽀付结果通知。
7、对账
查询对账单下载地址接⼝
/**
* 申请账单
* @param billDate
* @param type
* @return
*/
@Override
public String queryBill(String billDate, String type) {
try {
AlipayDataDataserviceBillDownloadurlQueryRequest request = new AlipayDataDataserviceBillDownloadurlQueryRequest();
JSONObject bizContent = new JSONObject();
bizContent.put("bill_type", type);
bizContent.put("bill_date", billDate);
request.setBizContent(bizContent.toString());
AlipayDataDataserviceBillDownloadurlQueryResponse response = alipayClient.execute(request);
if(response.isSuccess()){
log.info("调用成功,返回结果 ===> " + response.getBody());
//获取账单下载地址
Gson gson = new Gson();
HashMap<String, LinkedTreeMap> resultMap = gson.fromJson(response.getBody(), HashMap.class);
LinkedTreeMap billDownloadurlResponse = resultMap.get("alipay_data_dataservice_bill_downloadurl_query_response");
String billDownloadUrl = (String)billDownloadurlResponse.get("bill_download_url");
return billDownloadUrl;
} else {
log.info("调用失败,返回码 ===> " + response.getCode() + ", 返回描述 ===> " + response.getMsg());
throw new RuntimeException("申请账单失败");
}
} catch (AlipayApiException e) {
e.printStackTrace();
throw new RuntimeException("申请账单失败");
}
}
参考文章