序
经常有读者后台问我有没有开源的支付系统可以推荐,今天聊一款国内公司开源的支付系统。
内容主要包括:推荐理由、系统介绍、源码走读、几点建议。
声明:这不是广告,纯个人兴趣做了评测,我和系统关联方没有利益往来。
1. 前言
俗话说,适合自己的才是最好的。支付系统也是如此。
一些大型支付公司的系统极其复杂,开发运维成本也是巨高,因为其业务诉求也极高,两者匹配也没有问题。而一些小型公司,比如某个垂直行业的分销系统,只是对接微信、支付宝等,业务相对也比较简单,那就只需要一套简单的支付系统就足够用。而对于个人用户来说,如果想学习支付系统的开发,最好的办法之一就是部署一套相对简单的系统,先跑起来,边debug边学,速度是最快的。
开源的支付系统不少,国外国内都有,因国情原因,很多国外的系统移植到国内需要还需要做很多研发工作。
今天主要介绍国内团队维护的一套开源支付系统:Jeepay。
后面有时间也聊聊国内其它几款开源支付系统。
2. 开源与商业
Jeepay是由计全科技开发的一款开源支付系统,系统已经对接了微信支付、支付宝和云闪付等接口,支持docker部署,提供齐全的开发和使用文档,在线演示能力,开源代码库持续有更新,说明生态是稳定的。
除了开源版本,还提供商业版本,在商业版本中提供更为丰富的能力,包括分账能力,硬件支持等。具体可以参考其官网介绍。
根据多年从业经验看,在中国做纯开源基本是走不下去的,比如早年在中国互联网支付行业非常知名的“凤凰牌老熊”,也曾经发起过一个“jigsaw-projects”开源支付系统项目,最后也没有做起来。不过论述支付系统设计相关的文档还在,有兴趣的读者可以去看看,里面的材料很全,有很多至今仍然有借鉴意义:https://doc.cocolian.cn/essay/。
3. 推荐理由
通过上面的开源与商业的简单分析,给出我为什么首选推荐Jeepay几个理由:
有商业版支撑,可持续发展,不用担心过几天就没人维护。
如果使用开源版,一旦业务发展壮大,可选择招人自己升级 + 付费咨询,也可以选择直接升级功能更强大的商业版,可平滑过渡,充分利用前期投入。
文档齐全,部署方便。
当前在gitee上有12.3K Star,在github上有5.1K Star,都是不错的成绩。
4. 系统介绍
整体看下来,jeepay有以下几个特点:
多渠道对接:支持微信支付、支付宝、云闪付等多种支付渠道。
部署方便:支持docker部署,官方发布一键部署脚本,10分钟部署完成。
技术栈:基于Spring Boot、Ant Design Vue等现代开发框架,前后端分离架构,方便二次开发。
接口市场:提供中国市场上主流的三方支付接口对接代码,可按需购买。
提供对接SDK:提供java和python的sdk。
这是开源版架构图:
这是商业版架构图:
5. 简单试用
官方提供在线支付体验:https://www.jeequan.com/demo/jeepay_cashier.html。
支付是可以成功的,要注册后登录才能退款,我没有继续尝试。有兴趣的同学可以深度体验一下。
6. 源码走读
分析一个开源系统,对于专业的程序员,需要读下源码。
只读了部分源码,整体感受如下:
优点:整体架构比较简洁明了。
缺点:安全性不足,接口不够规范,领域区分度不够,代码规范执行不到位。
下面重点说说建议改进的点。有兴趣采用这套开源代码的读者需要重点留意一下。
6.1. 代码库整体结构
看代码前要有一个全局观,这是jeepay官方给出的项目代码结构。
jeepay-ui -- https://gitee.com/jeequan/jeepay-ui
jeepay
├── conf -- 存放系统部署使用的.yml文件
├── docker -- 存放docker相关文件
└── docs -- 存放项目相关文档说明
├── intsll -- 项目部署shell脚本
├── script -- 项目启动shell脚本
└── sql -- 初始化sql文件
└── jeepay-components -- 公共组件目录
├── jeepay-components-mq -- mq组件
└── jeepay-components-oss -- oss组件
├── jeepay-core -- 核心依赖包
├── jeepay-manager -- 运营平台服务端[9217]
├── jeepay-merchant -- 商户系统服务端[9218]
├── jeepay-payment -- 支付网关[9216]
├── jeepay-service -- 业务层代码
└── jeepay-z-codegen -- mybatis代码生成
6.2. 建议加固安全
代码直接使用密钥明文。
在接口中使用MD5做签名和验签。
从安全角度出发,密钥明文一定要加密保存到数据库。
另外,MD5已经不推荐使用,建议升级到公私钥模式的RSA2048。再差也建议是SHA256。
因为MD5和SHA256都是哈希算法,虽然加上了密钥,但是密钥需要双方共享,一旦泄漏或有伪造情况发生,说不清楚是哪方出了安全问题。
而公私钥模式的RSA2048,都是拿私钥签名,公钥验签,一旦有泄漏或有伪造情况发生,一定是私钥持有人出了安全问题,责任清晰。
代码:com.jeequan.jeepay.core.utils.JeepayKit#getSign
6.3. 建议提高编码规范
仍然拿上面的md5签名代码来说,还可以再优化一下。一个就是把Map转成TreeMap,解决排序问题,同时加上一些适当的换行,请代码更简洁易读。
原代码:
优化后如下:
魔法值就不说了,能不用尽量不要用。
还有,md5出错应该抛出自定义的异常,而不是直接使用e.printStackTrace(),这种不良习惯会在生产系统出问题时找不到原因,后果很严重。
优化方案:要不自己打印error日志(需要把关键参数也打印出来),要不抛出去,让外层处理。
优化后如下:
还有像这种,只打印堆栈信息的,出问题很不好定位问题,建议是把关键参数也打印出来。
6.4. 建议升级订单状态机
在很多代码中,都是直接更新状态,而不是根据状态机来推进。在高并发情况下,容易出问题。具体怎么使用状态机,可以参考我以前写的《图解支付系统状态机设计与实现》那篇文章。
简单地说,就是通过预先定义好几组:【当前状态】 + 【事件】 可推进到【目标状态】,在使用时就通过当前状态+事件,拿到目标状态去推进和更新。而不是通过if else 来写。
com.jeequan.jeepay.service.impl.PayOrderService#updateIng2SuccessOrFail
以及:
6.5. 建议升级Money类
在很多代码里面,金额处理不是使用String,就是Long,要不就是BigDecimal。这都不是规范的,容易有资损出来,建议构建一个Money类,所有金额在入口处转成Money类,一旦进入到系统内部,全部使用Money来处理金额。具体怎么构建和使用Money类,可参考以前写的《资损防控:搞定交易系统中金额处理规范》。
com.jeequan.jeepay.core.ctrls.AbstractCtrl#getRequiredAmountL
6.6. 建议升级校验框架
在内部很多服务的业务参数校验,使用了if来判断,建议使用断言校验。
com.jeequan.jeepay.pay.ctrl.ApiController#getRQByWithMchSign(里面有很多这种判断,只截取了2个做为示例)
抛个“参数有误!”,使用方怎么知识是哪个参数有误呢?
优化为:
更好的优化,是再加一个自己的定义的错误码一起抛出去,这样更彻底。
6.7. 建议升级接口契约
主要有2个方面:
提供给外部商户调用的接口,要清晰,不要缺少一些关键参数。
外部参数转内部参数,应该提炼出内部需要的参数,然后在网关对接层面,转成内部参数,而不是使用Object这种难以理解的定义。
这就是所谓的接口契约,一定要清晰,容易理解,不要缺少关键参数,也不要有Object, JSONObject,Map这样的定义。
外部接口不清晰示例:
com.jeequan.jeepay.mgr.ctrl.order.PayOrderController类中有一个refund(退款)方法,同时还存在一个com.jeequan.jeepay.mgr.ctrl.order.RefundOrderController类,注释也是退款订单类,但是只提供查询能力。
缺少关键参数示例:
com.jeequan.jeepay.mgr.ctrl.order.PayOrderController#refund退款接口缺少退款请求号,那就无法实现幂等能力。如果出现订单重放,或网络包数据重传,有可能一笔请求被重复执行多次,容易导致资损。比如用户支付100块,退款50块,因为网络包数据重传2次,导致退款100块,资损50块。
外部参数内部参数使用Object定义示例:
比如下面这个返回值,谁能一眼看出返回的具体是什么,应该如何处理?
com.jeequan.jeepay.pay.channel.IChannelNoticeService#parseParams
使用的时候特别为难,一不小心就容易搞错。
com.jeequan.jeepay.pay.ctrl.payorder.ChannelNoticeController#doNotify
6.8. 建议提升领域区分度
从现有的代码看,创建订单、支付、支付回调等业务主流程相关的模块在:payment模块和service模块。
payment模块主要包含了以下内容:
商户下单请求入口,主要在:com.jeequan.jeepay.pay.ctrl.payorder这个包下面的代码。
外发给渠道(微信支付、支付宝等)出口,主要在:com.jeequan.jeepay.pay.channel这个包下面的代码。
状态推进,主要在:com.jeequan.jeepay.pay.service这个包下面的代码。
service模块:主要是数据库操作,主要是com.jeequan.jeepay.service.impl这个包下面的代码。
存在的问题:没有为订单或支付能力提供一个高内聚的领域服务,而是把对订单的操作分散在各个类中。以下单(创建订单)举例:
全部业务处理在Controller层做掉com.jeequan.jeepay.pay.ctrl.payorder.UnifiedOrderController,包括参数校验、调用payOrderService保存到数据库,再调用com.jeequan.jeepay.pay.channel.IPaymentService#pay去渠道下单。
渠道回调入口也在Controller层com.jeequan.jeepay.pay.ctrl.payorder.ChannelNoticeController,包括更新订单状态等,但是又多了一个com.jeequan.jeepay.pay.service.PayOrderProcessService#confirmSuccess的调用,这个类注释是“订单处理通用逻辑”,方法confirmSuccess的注释是“明确成功的处理逻辑(除更新订单其他业务)”,而大部分订单操作服务全部在Controller层已经完成,所以显得有点混乱。
推荐做法:Controller只负责接收外部的请求(包括商户下单和渠道结果通知等请求),也就是一个API入口,只做基础的校验,然后调用OrderDomainService,所有核心的业务操作放在OrderDomainService,在OrderDomainService里面做状态推进,保存数据库,调用商户通知服务等。
这样做的好处非常明显:领域内的能力是非常内聚的,只向外暴露服务,不暴露细节。
而现在ChannelNoticeController里面还有很多重复代码,同步通知doReturn和异步通知doNofity的实现里面,查询订单,查询商户信息,保存数据库等代码都是重复的。如果Controller只是做基础校验和参数转换,然后由OrderDomainService来负责核心业务处理,就可以避免这些重复代码,一旦有修改也不需要动多个地方。
下面是一些代码截图,领域内聚程度不足。
下单入口:com.jeequan.jeepay.pay.ctrl.payorder.UnifiedOrderController#unifiedOrder
参数校验,保存订单,调用渠道下单在:com.jeequan.jeepay.pay.ctrl.payorder.AbstractPayOrderController#unifiedOrder(java.lang.String, com.jeequan.jeepay.pay.rqrs.payorder.UnifiedOrderRQ, com.jeequan.jeepay.core.entity.PayOrder)。代码太长,整整4屏才能显示完。(一个方法有146行,建议参考《代码整洁之道》里面的方法做些重构,保持主方法在20行左右)
回调通知的Controller也有很多订单相关的操作代码,这里就不贴了,有兴趣的读者可以自己下载下来看看。
7. 源码及官方介绍
开源代码:https://gitee.com/jeequan/jeepay
官方文档:https://docs.jeequan.com/docs/jeepay/index
商业版:https://www.jeequan.com/product/jeepay4plus.html#more
8. 小结
Jeepay做为一款发展多年且还在持续更新迭代的开源支付系统,整体来说还是不错的,尤其还拥有活跃的社区支持和详细的文档资源,说明维护团队持续有投入,值得推荐给想基于开源支付解决方案快速拉起一个四方支付系统的初创团队,也适合个人开发者debug学习完整的支付流程。
代码里面也仍然存在一些安全性不够、容易资损、编码不够规范等问题,使用的时候需要留意。
上述内容纯属个人不成熟见解,仅供参考。
欢迎关注公众号“隐墨星辰”,和我一起深入解码支付系统设计与实现的方方面面。