互联网的发展极大地方便了人们的日常生活,深刻地改变了各行各业的面貌,尤其是在线支付(包括各种移动支付、指纹支付、刷脸支付等等),在前几年还和「高铁」一起,成了推动和见证中国高速发展的一张名片。现在,不管是买菜、买票、交电费还是电商网购,都已经离不开支付系统了。而且在目前的互联网应用中,不管是开发什么类型的系统,也几乎都会对接(第三方)支付系统实现订单付款。所以,了解支付系统是如何设计和运行的就显得很有必要了。而且,支付系统如果做得很耐撕,那么再去做其他的业务系统的话,技术层面能遇到的挑战就比较少了。
但是,想要设计、开发一个支付系统其实是一件极其困难的事情,因为它涉及到的场景太多太多,需要解决的问题也不计其数。别的不说,光是防黑灰产就够让人头疼的,更别说分布式事务问题、不同系统之间的认证授权问题、利益人之间的清结算、分账、分润问题等等......。
比如像下面这幅图:
图一:某互联网金融公司业务架构图(2018年)
另外,设计模式作为一项大杀招,在工程师的江湖中被「传扬」甚广,但就笔者自身经历而言,真正能把这个葵花宝典用好,用到实际开发项目中的还真不多。而能在支付系统中有目的、有计划、系统地使用设计模式的,在目前互联网中还没有这类公开的内容(各大支付公司的代码里肯定都有这种「祖传」代码,但一是它不可能公布出来,二是谁敢轻易对百万、千万级代码行的系统搞重构?)。
所以,本着「好奇害死猫」的探索精神,笔者在这里献个丑,抛个砖,通过分享自己一点点的思考和浅薄实践,给未来更多优秀的工程师们增添一些学习的材料和前行的铺路石。如果看过之后能有人说「还算有点用」,那也算没白忙活了。
不过,笔者在这里首先需要声明的是:
1、完整的支付系统涉及到的内容极其复杂(即使是图一,在整个大支付系统中也只是九牛一毫),任何人都不可能独自把这些都给实现了,既没能力,更没精力。所以,接下来要做的,就只是结合自己的工作实际把设计模式用到支付系统中;
2、只写核心代码。因为把核心问题讲清楚了,剩余的那些业务需求其实都比较通用。而且像调用支付接口,支付签名等动作,某种程度上都是可以「模板化」的,并没有什么技术含量,所以这里不会涉及到这些内容;
3、也不涉及到到抵御黑灰产的攻击,不涉及到分布式事务相关的内容。因为这两个要讲清楚的话,内容也是巨多,实在不是一个单独的专栏内容可以讲清楚的,后续笔者会接着这个主题继续讲这两部分。
不废话了,开干。
— 1 —
初始
在讲要怎么干之前,还是需要先把业务背景和基础代码讲清楚,不然都不知道是在什么基础上做的。
现在,咱们正在一家新成立不久的母婴用品的电商公司的研发部门工作。由于公司成立之初人少事多,需求迭代快速,所以导致开发工期非常紧张,电商平台的很多功能都相当于是赶工拼凑出来的,不仅问题多多,而且还经了几道手(至少有5位工程师经手开发过支付功能),稳定性、扩展性、易用性和可维护性极差,用「一团乱麻」和「命悬一线」都不为过。
为了解决这些潜在的隐患,老板下定决心要进行一次彻底的重构,一劳永逸地解决上面那些问题,让它能够满足并支撑后续业务肯能会到来的高速发展。而且老板还提出:要把现在的支付功能升格为支付系统,在新功能的开发继续进行的同时,同时还要保质保量地满足公司各种运营的需求,这有点像「给高速行驶中的汽车换轮子」——是在是太难了~
没办法,只能硬着头皮顶上去~
不过在动手之前,要先来看看现在支付功能这部分的「祖传代码」都传了些啥。
首先是账户类和订单类代码:
/**
* 用户账户
*
* @author 湘王
*/
public class Account {
// 账户编码
private String accid = "";
// 账户押金
private double deposit = 0;
// 账户余额
private double balance = 0;
public String getAccid() {
return accid;
}
public void setAccid(String accid) {
this.accid = accid;
}
public double getDeposit() {
return deposit;
}
public void setDeposit(double deposit) {
this.deposit = deposit;
}
public double getBalance() {
return balance;
}
public void setBalance(double balance) {
this.balance = balance;
}
}
/**
* 用户订单
*
* @author 湘王
*/
public class Order {
// 订单编码
private String oid = "";
// 订单金额
private double amount = 0;
public String getOid() {
return oid;
}
public void setOid(String oid) {
this.oid = oid;
}
public double getAmount() {
return amount;
}
public void setAmount(double amount) {
this.amount = amount;
}
}
然后是支付配置类:
/**
* 支付接口的设置
*
* @author 湘王
*/
public class Payload {
public enum CHANNEL {
// 余额支付
ACCOUNT,
// 支付宝支付
ALIPAY,
// 微信支付
WEIXIN
}
// 支付渠道
private CHANNEL channel;
// 支付备注
private String body;
// 手续费
private double charge;
// 交易编码
private String tradeNo;
// 回调地址
private String notifyUrl;
public CHANNEL getChannel() {
return channel;
}
public void setChannel(CHANNEL channel) {
this.channel = channel;
}
public String getBody() {
return body;
}
public void setBody(String body) {
this.body = body;
}
public double getCharge() {
return charge;
}
public void setCharge(double charge) {
this.charge = charge;
}
public String getTradeNo() {
return tradeNo;
}
public void setTradeNo(String tradeNo) {
this.tradeNo = tradeNo;
}
public String getNotifyUrl() {
return notifyUrl;
}
public void setNotifyUrl(String notifyUrl) {
this.notifyUrl = notifyUrl;
}
@Override
public String toString() {
return String.format("{\"body\":\"%s\", "
+ "\"charge\":%f, \"tradeNo\":\"%s\", "
+ "\"notifyUrl\":\"%s\"}", body,
charge, tradeNo, notifyUrl);
}
}
最后是支付类:
/**
* 支付类
*
* @author 湘王
*/
public class Payment {
private Payload payload;
/**
* 支付方法
*/
protected boolean pay(int type, final Account account, final Order order) {
// 支付接口的设置
payload = new Payload();
// 如果是支付宝
if (CHANNEL.ALIPAY.ordinal() == type) {
payload.setChannel(CHANNEL.ALIPAY);
// 千六手续费
payload.setCharge(order.getAmount() * 0.006);
payload.setNotifyUrl("http://localhost:7070");
}
// 如果是微信
if (CHANNEL.WEIXIN.ordinal() == type) {
payload.setChannel(CHANNEL.WEIXIN);
// 千六手续费
payload.setCharge(order.getAmount() * 0.006);
payload.setNotifyUrl("http://localhost:8080");
}
// 如果是余额
if (CHANNEL.ALIPAY.ordinal() == type) {
payload.setChannel(CHANNEL.ACCOUNT);
// 无手续费
payload.setCharge(0.0);
payload.setNotifyUrl("http://localhost:9090");
}
payload.setBody("订单备注");
payload.setTradeNo("202001230258863496515");
System.out.println("您需要支付的金额是:" + order.getAmount());
// 用余额支付
if (payload.getChannel() ==CHANNEL.ACCOUNT) {
// 如果可用余额不足
if (account.getBalance() < order.getAmount()) {
return false;
} else {
// 从可用余额中扣除
account.setBalance(account.getBalance() - order.getAmount());
}
}
return true;
}
}
以上就是目前支付功的基础代码了(为了更聚焦于说明咱们需要解决的问题,排除干扰信息,这里对实际代码略做了些简化,但不影响学习的效果)。
现在,老板提出,要把用户在APP中的余额分成两部分(假设这么做不违规且经用户调研后可行):
1、将账户分为可用余额与押金两部分。如果可用余额不足,就从押金中扣除一部分;
2、如果可用余额+ 押金仍不足以支付,那么支付失败;
3、为了支持产品和运营活动,业务系统在支付前和支付后需要完成不同的工作,例如支付前需要锁定账户,而支付成后要给账户增加积分;
4、另外,架构师说,公司的APP既有自己的钱包,也同时对接了多种不同的第三方接口,而且不同的支付接口(支付宝和微信)、不同的服务(支付和提现),接口的设置可能完全不同,这块的代码需要改善一下。
仔细思考一下需求,可以知道:
功能「1」纯粹属于产品设计的范畴,和技术架构无关。功能「2」属于是业务多了个分支条件,目前看来也和技术改造扯不上关系。而功能「3」,很明显,要做一个面向「切面」的拦截。也就是拦截所有支付请求,在支付钱执行某些动作,然后在支付后再完成另一些动作。
至于功能「4」,因为目前APP的后台是由咱们搭建并开发的,所以理所当然改造的重任就落到了自己肩上。而且,用「if...else」的方式设置不同的支付渠道,在将来系统更新时确实是难免会有一些隐患。而且每次配置属性参数的时候代码都比较丑陋,都是一堆set()方法,可能就像下面这样:
client.setAppID(......);
client.setAppSecret(......);
client.setRequest(......);
client.setPaymentModel(......);
client.setNotifyUrl(......);
......
这种写法确实不够「优雅」。
所以,很明显「3」和「4」就是需要根据业务需求来改造技术架构的点。那么,在GoF经典的23种设计模式中,哪几个匹配「3」和「4」的需求呢?
因为我们希望客户端的设置能够用一行代码搞定,就像这样:
client.setAppID(......).setAppSecret(......).setRequest(......).setPaymentModel(......).setNotifyUrl(......);
可能有的童鞋已经猜到了,它就是构造器(Builder)模式,它可以解决支付配置属性参数的问题,让代码更优雅。
对设计模式稍稍有点了解的小伙伴,可能已经想到了哪个模式比较适用于解决众多的「if...else」——对,就是策略(Strategy)模式——把不同的分支条件放在不同的类中而不是「if...else」条件里,这样至少对修改非常有好处,再也不会出现几个工程师集中修改一个类导致出现代码冲突了。而且从现在的支付代码来看,如果再增加其他支付接口,可能需要重新设置一遍,要改代码,这很不好,而且不符合软件设计中的开闭原则。
至于解决支付前和支付后要执行额外动作的问题,其实就是一个通用模板的问题,这个用模板方法(Template Method)模式就能完美解决。
下面来一个一个来实现。