前言

阅读本文大概需要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;
    }
}
Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐