前言
阅读本文大概需要6分钟
最近在项目中对接了第三方支付,对于第三方支付来说,比较复杂功能的就是支付、退款、对账。
本篇文章我们只介绍支付相关的接口设计。
一笔支付流水可能涉及到的节点包括:支付、支付结果查询、支付结果通知、撤单、关单、退款、对账。
拿支付宝举例,支付宝提供了非常丰富的支付能力:app支付、扫码支付、网站支付等等。不同的支付方式之间的区别不大。
对接第三方支付的流程大同小异。按照官方提供的文档可以迅速完成对接,所以这篇文章我们不讨论如何对接第三方支付,我们要聊的是对接以外的事。
我在如何设计好一个接口?里有写到,设计一个接口要考虑五点:安全性、稳定性、高效性、可维护性、可读性。
下面我们就围绕这几个特性来讨论下如何设计一个支付接口。
安全性
支付接口涉及资金的流转,那么其安全性不言而喻。
支付宝规定,接入支付能力的时,数据传输接口用公私钥的方式进行加密。
那我们自己接口间是如何保证安全性的呢?
SHA256
或者RSA2
。
具体的加密算法此处不详细说明,网上一找一大堆。
本文只简单说一下加密的流程,如下图:
具体来说就是:
- 在App端首先用
SHA256
或者RSA2
将支付报文进行加密,然后传给后台服务; - 后台解析密文,并进行验证。
- 解析通过则进行下一步逻辑,否则提示密文解析失败。
稳定性
对于支付接口来说幂等性是极为重要的。
支付业务涉及到的数据库操作包括:保存支付流水、同步订单状态、更新库存数量等等。
由于新增操作天然的非幂等性,所以我们需要在设计层面来保证。
我在项目中使用Redis分布式锁实现支付接口的幂等。
- 有关使用Redis实现分布式锁的原理,我会在下一篇文章和大家分享。
我在项目中实现的方式:Redisson
。
Redisson原理:
- 线程去获取锁,获取成功:执行lua脚本,保存数据到redis数据库。
- 线程去获取锁,获取失败:一直通过while循环尝试获取锁,获取成功后执行lua脚本,保存数据到redis数据库。
- 支持看门狗自动延期机制。
代码实例:
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.13.6</version>
</dependency>
public void testReentrantLock(RedissonClient redisson){
RLock lock = redisson.getLock("anyLock");
try{
// 1. 最常见的使用方法
//lock.lock();
// 2. 支持过期解锁功能,10秒钟以后自动解锁, 无需调用unlock方法手动解锁
//lock.lock(10, TimeUnit.SECONDS);
// 3. 尝试加锁,最多等待3秒,上锁以后10秒自动解锁
boolean res = lock.tryLock(3, 10, TimeUnit.SECONDS);
if(res){ //成功
// do your business
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
除了幂等性之外,支付接口还要考虑的一个问题就是订单超时关闭。这个问题先留给各位思考一下,我们会在后期文章中详细介绍。
事务一致性
支付成功同时要更新订单状态、库存数量。
在微服务的背景下,各业务的数据库都是独立的,为了保证事务的一致性就需要用上分布式事务。
常见的分布式事务解决方案:
- XA 两阶段提交
- TCC模式:支持 TCC 事务的开源框架有:ByteTCC、Himly、TCC-transaction。
- Saga事务
- 基于消息的分布式事务:基于事务消息的方案、基于本地消息的方案
- 分布式事务中间件:Seata
可维护性
我们项目中目前只对接了支付宝和微信,以后还可能对接银联等等。支付方式会随着业务的增长不断增加。
但是每个支付方式的流程大致都是一样的:支付信息解密、支付、修改订单、修改库存。
如此一来,使用if else
判断就会导致支付功能和系统业务功能高度耦合。
if (payType.equals ("WeiXin")) {
//dosomething
} else if (payType.equals ("AliPay")) {
//dosomething
} else if (payType.equals ("UnionPay")) {
//dosomething
}
所以我在项目中用到了策略模式,来为不同的支付方式定义不同的实现。
-
首先定义一个抽象类,封装公共的方法。
-
然后自定义注解
ServiceRoute
,标注在具体的支付实现接口,项目启动时自动把标注了ServiceRoute
注解的服务注入到容器; -
在支付的时候根据具体的通道编号,调用不同的支付实现功能。
自定义注解:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface ServiceRoute {
/**
* 支付通道编号
*
* @return
*/
String value();
}
服务注册:
public class RegisterService implements ApplicationContextAware {
private static final Logger logger = LoggerFactory.getLogger(RegisterService.class);
private Map<String, Object> servicesMap = new ConcurrentHashMap<String, Object>();
private static ApplicationContext applicationCtx = null;
/**
* 注册服务接口
*/
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
applicationCtx = applicationContext;
//扫描添加了ServiceRoute注解的类
Map<String, Object> allWebResBeans = applicationCtx.getBeansWithAnnotation(ServiceRoute.class);
for (Object bean : allWebResBeans.values()) {
String routeName = getServiceRoute(bean);
if (routeName != null) {
servicesMap.put(routeName, bean);
logger.debug("register route,routeName={},bean={}", new Object[] {routeName,bean});
}
}
}
private String getServiceRoute(Object bean) {
if (bean != null) {
Annotation anno = AnnotationUtils.getAnnotation(bean.getClass(), ServiceRoute.class);
if (anno != null) {
return anno.getClass().getAnnotation(ServiceRoute.class).value();
}
}
return null;
}
public Object getServiceByAnnoName(String name) {
if (StringUtils.isNotEmpty(name)) {
return servicesMap.get(name);
}
return null;
}
}