三方系统多渠道多场景的思考及代码分享

一 背景

我们的业务中,经常会遇到多渠道+多场景的的需求。多渠道我们可以理解为,多个服务商同时为我们某一个功能提供服务,多场景就是同一个服务商下面的多种服务,我们通过几个例子来具体看下:

我们有个电商平台,系统在付款时候弹出选项,让用户自主选择支付宝/微信/京东白条等支付,那么这里的支付宝、微信、京东我们都可以理解为他是我们的渠道。
同时,既然接入了支付宝/微信等,我们的系统就不再会单单使用他们的一个支付功能,还会涉及到对账、退款等等操作,那么这些操作,我们称之为场景。

类似的,有的公司需要将订单进行推送,根据订单类型,推送到淘宝、拼多多等。那么淘宝和拼多多也属于渠道,同样的,有了下单,那么还会有退单、订单对账等都属于场景。通过这样的案例分析下来,我们会发现,我们身边有很多这样的需求。如果针对这类需求,我们的代码缺少一些架构设计,那么就会带来很多问题,比如:

  • 代码复用度不够,新增需要会产生大量重复的冗余代码;
  • 代码耦合度过高,业务逻辑和三方交互代码夹杂在一起,每次改动牵一发而动全身,影响面不好评估。

此类问题应该存在一些通用的解决方案,可以实现以下特点:

  • 面向接口编程:业务方调用三方服务的时候,不需要关心具体服务商的实现,只针对接口调用;
  • 对拓展开放,对修改关闭:每次新增一个渠道,或者新增一个场景,对已有的代码应该是无冲突的,不需要改动的。

二 案例分析

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);
    });
}

结束语

上面就是我在项目开发中,针对多渠道+多场景的一些思考及个人的代码设计。由于经验有限,此方案不一定是最优方案,欢迎大家批评指正,感谢。

posted @ 2023-07-20 23:49  史亚健  阅读(324)  评论(0编辑  收藏  举报