一 背景
我们的业务中,经常会遇到多渠道+多场景的的需求。多渠道我们可以理解为,多个服务商同时为我们某一个功能提供服务,多场景就是同一个服务商下面的多种服务,我们通过几个例子来具体看下:
我们有个电商平台,系统在付款时候弹出选项,让用户自主选择支付宝/微信/京东白条等支付,那么这里的支付宝、微信、京东我们都可以理解为他是我们的渠道。
同时,既然接入了支付宝/微信等,我们的系统就不再会单单使用他们的一个支付功能,还会涉及到对账、退款等等操作,那么这些操作,我们称之为场景。
类似的,有的公司需要将订单进行推送,根据订单类型,推送到淘宝、拼多多等。那么淘宝和拼多多也属于渠道,同样的,有了下单,那么还会有退单、订单对账等都属于场景。通过这样的案例分析下来,我们会发现,我们身边有很多这样的需求。如果针对这类需求,我们的代码缺少一些架构设计,那么就会带来很多问题,比如:
- 代码复用度不够,新增需要会产生大量重复的冗余代码;
- 代码耦合度过高,业务逻辑和三方交互代码夹杂在一起,每次改动牵一发而动全身,影响面不好评估。
此类问题应该存在一些通用的解决方案,可以实现以下特点:
- 面向接口编程:业务方调用三方服务的时候,不需要关心具体服务商的实现,只针对接口调用;
- 对拓展开放,对修改关闭:每次新增一个渠道,或者新增一个场景,对已有的代码应该是无冲突的,不需要改动的。
二 案例分析
2.1 下单支付案例
我们以下单支付为案例,通过时序图的流程来梳理下我们的思路:
关于和三方请求的操作,一般就分为两类:
- 请求三方系统;
- 三方系统处理完成,回调通知。
2.2 请求类
请求三方系统的具体流程如下:
不同场景下,流程相同,但是还会存在一些差异点,我们总结一下:
- 通用:数据加签/加密,重试+告警,返回结果解密,操作日志记录等;
- 差异:模型转换字段不一样,发起请求的方式和路径不一样;
2.3 回调类
不同回调场景也可能会存在一些差异,我们总结一下:
- 通用:回调日志记录,数据解密,响应三方系统;
- 差异:模型转换字段不一样,执行业务操作不一样;
2.4 渠道商的拓展
多渠道多业务通过以上分析基本上都是可以枚举的,可以通过以下表格进行拓展:
三 代码实现
代码已开源到Github:https://github.com/Shiyajian/mall-example
代码可能运行有问题,主要展示的是思路,下面是针对项目的讲解。
3.1 设计思路
通过案例分析,我们分析出来了通用和差异点,对于通用的部分,我们采用封装成标准流程,差异的地方,我们定义拓展点接口,不同渠道方各自实现,每个渠道方的代码要物理隔离。
3.2 代码结构分包
代码主要分为三部分:核心业务代码、架构代码、拓展实现代码。
- 核心业务代码:就是我们平常写的业务代码,应该和具体的三方系统交互解耦,面向接口编程,不感知三方系统模型,也不感知具体的渠道实现。
- 架构代码:根据不同的渠道和场景,找到对应的实现,并且提供部分流程编排能力
- 拓展点接口定义:抽象的能力接口和公用;
- 拓展点实现代码:不同场景不同渠道的实现。
3.3 spi 接口定义
主要使用策略模式,这里通过接口进行抽象定义,代码主要分为三个部分:
- call :表示调用第三方渠道商的抽象服务接口;
- callback:表示第三方渠道商回调业务系统的接口,每个场景按包进行细分
- common:一些公用的枚举和标记性接口定义
拓展方式:
- 如果需要新增调用渠道商的接口,那么在 ChannelCaller中定义方法,在 spi-impl 中实现;
- 如果需要新增回调方法,首先在 ChannelCallbackSceneEnum 中增加类型,在 callback 中新增一个包,新建一个 request 模型和 parser 模型,request 表示渠道方传入的参数,parser 表示如何转换成我们业务系统模型;
3.4 spi 实现方式
每个渠道商为一个package,里面实现 call 和 callback 定义的接口即可,按需自己增加常量和工具类等;
3.5 spi的发现及路由
通过 Manger 管理类进行查找具体实现,并进行一定业务逻辑的编排,处理日志记录,错误处理等通用流程。
3.6 业务代码使用
需要调用渠道商接口的时候,通过以下方式进行调用:
@Override
public String pay(ChannelCodeEnum channelCode, Object args) {
// 1、创建支付单;
PayOrder payOrder = new PayOrder();
// 2、支付单入库;
// 3、根据不同支付渠道,调用三方的支付单创建
ChannelCaller channelCaller = channelCallerManager.of(channelCode);
channelCaller.submitPay(payOrder);
// 4、更新三方订单号入库
// 5、返回前端唤醒参数
return payOrder.getPayParams();
}
渠道商接口回调时候,通过下面方式进行参数的转换,转换完成后执行自己系统逻辑:
@RequestMapping("/pay/{channelCode}/{bizNo}")
public ResponseEntity> paySuccessCallback(HttpServletRequest request,
@PathVariable(value = "channelCode") String channelCode,
@PathVariable(value = "bizNo") String bizNo) {
log.info("进入[" + channelCode + "]支付成功回调:bizNo:[" + bizNo + "] ");
ChannelCodeEnum channelCodeEnum = ChannelCodeEnum.ofCode(channelCode);
return channelCallbackHandlerManager.run(channelCodeEnum, () -> {
CallbackPayRequest payRequest = channelRequestParserManager.parse(channelCodeEnum, ChannelCallbackSceneEnum.PAY_CALLBACK, request, bizNo);
return payService.paySuccess(payRequest);
});
}
结束语
上面就是我在项目开发中,针对多渠道+多场景的一些思考及个人的代码设计。由于经验有限,此方案不一定是最优方案,欢迎大家批评指正,感谢。