设计模式专栏(五):设计模式在实际项目中的应用 —— 支付系统扩展与回调处理案例

57 阅读13分钟

设计模式专栏(五):设计模式在实际项目中的应用 —— 支付系统扩展与回调处理案例(关注“Beyond Code 程序员”订阅号查看更多文章)

在真实业务开发中,设计模式并不是教科书上的概念,而是解决实际问题的一种思维方式。下面我结合我在项目中的真实经验,分享支付场景下如何运用设计模式来提升代码的扩展性和可维护性。

⚠️ 提示:设计模式是一种编程思想,其实现方式并非唯一。本文示例代码仅作参考,并不意味着设计模式必须按照示例中的方式实现。


1. 背景故事

刚开始项目只接入了微信支付支付宝支付,业务也很单一。 后来业务扩展:

  • 接入了苹果内购、聚合支付(A聚合、B聚合等,它们内部还是调用微信或支付宝)。

  • 支付场景分为多种:

    1. 充值金币(虚拟币)
    2. 购买 VIP 套餐(直接人民币购买)
    3. 后续更多场景

关键点:无论下单时的业务是什么,真正的业务处理都在支付回调时完成。 因此,需要同时解决:

  • 多支付方式接入的可扩展性
  • 支付回调业务逻辑的解耦

为了演示代码,下面简单创建了订单表、充值产品表-金币,充值产品表-vip,由于两张充值产品表字段有区别,我并没有放在一张表

// 订单表
CREATE TABLE `order` (
  `id` bigint unsigned NOT NULL AUTO_INCREMENT,
  `user_id` bigint unsigned NOT NULL DEFAULT '0' COMMENT '用户user表id',
  `recharge_product_id` bigint NOT NULL DEFAULT '0' COMMENT '充值产品id,不同场景对应不同的表',
  `order_id` varchar(64CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '系统订单号',
  `third_order_sn` varchar(64CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '第三方订单支付流水号',
  `amount` bigint unsigned NOT NULL DEFAULT '0' COMMENT '订单金额,单位分',
  `pay_platform` varchar(32CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '支付厂家平台,alipay 支付宝,wechat 微信,apple 苹果',
  `pay_way` varchar(32CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '支付方式,app 客户端原生支付,h5 网页端支付,apple 苹果内购',
  `pay_scene` varchar(32CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '充值场景,icon 金币,vip 购买vip',
  `state` varchar(10CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '支付状态,wait 待支付,ok 支付成功,fail 支付失败,refund 退款',
  `pay_time` datetime DEFAULT NULL COMMENT '成功支付的时间',
  `create_time` datetime DEFAULT NULL COMMENT '订单创建时间',
  `update_time` datetime DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE,
  UNIQUE KEY `uqx_order_sn` (`order_sn`) USING BTREE,
  KEY `idx_third_order_sn` (`third_order_sn`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='充值订单表';

// 充值产品表-金币
CREATE TABLE `charge_product_coin` (
  `id` bigint unsigned NOT NULL AUTO_INCREMENT,
  `app_platform` varchar(64CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT 'app平台,Android 安卓,iOS 苹果',
  `amount` bigint unsigned NOT NULL DEFAULT '0' COMMENT '金额,单位分',
  `coin` int unsigned NOT NULL DEFAULT '0' COMMENT '金币',
  `state` varchar(10CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '状态,Y 上架,N 下架',
  `apple_product_id` varchar(32CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '苹果商店的产品id',
  `create_time` datetime DEFAULT NULL COMMENT '订单创建时间',
  `update_time` datetime DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='充值产品表-金币';

// 充值产品表-vip
CREATE TABLE `charge_product_vip` (
  `id` bigint unsigned NOT NULL AUTO_INCREMENT,
  `app_platform` varchar(64CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT 'app平台,Android 安卓,iOS 苹果',
  `type` varchar(64CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '类型,vip, svip',
  `amount` bigint unsigned NOT NULL DEFAULT '0' COMMENT '金额,单位分',
  `days` int unsigned NOT NULL DEFAULT '0' COMMENT '获取的天数',
  `state` varchar(10CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '状态,Y 上架,N 下架',
  `apple_product_id` varchar(32CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '苹果商店的产品id',
  `create_time` datetime DEFAULT NULL COMMENT '订单创建时间',
  `update_time` datetime DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='充值产品表-vip';

2. 实现方式与问题场景

统一下单接口 url:/api/charge/prepare/order method:POST 参数: payScene 充值场景,ICON 充值金币,VIP 购买vip; payPlatform 充值平台,WECHAT 微信,ALIPAY 支付宝, APPLE 苹果内购; chargeProductId: 产品id(在不同的充值场景,会在充值页面返回不同的产品列表,如充值金币返回 charge_product_coin 表的数据,产品id则是 charge_product_coin 表id,购买vip返回 charge_product_vip 表的数据,产品id则是 charge_product_vip 表的id)

// 首先通过参数中的 payScene,获取不同表的数据(charge_product_coin,charge_product_vip)
if ("ICON".equals(payScene)) {
    // 获取产品对应的金额,金币等else if ("VIP".equals(payScene)) {
    // 获取产品对应的金额,天数等
}

if ("WECHAT".equals(payPlatform)) {
    // 调用微信下单的接口else if ("ALIPAY".equals(payPlatform)) {
    // 调用支付宝下单的接口else if ("APPLE".equals(payPlatform)) {
    // 返回苹果内购需要的参数else if (...) // 其它支付方式

// 回调处理,先查找订单出来,通过支付场景处理不同的业务逻辑
if ("ICON".equals(rechargeScene)) {
    // 加金币else if ("VIP".equals(rechargeScene)) {
    // 设置vip过期时间else if (...) // 其它场景

问题:

  • 支付场景和支付方式增加要改 if else
  • 回调处理逻辑直接写死在支付回调里,扩展业务场景时容易改坏原逻辑

3. 模式介入

下单阶段:

  • 策略模式 + 工厂模式:封装不同支付方式的下单逻辑

回调阶段:

  • 策略模式(CallbackSceneStrategy) :封装不同业务类型的回调处理逻辑
  • 工厂模式:根据业务类型选择回调策略

这样:

  • 新增支付方式 → 新增一个支付策略类
  • 新增业务类型 → 新增一个回调策略类
  • 核心流程代码不动

4. 核心代码实现(Java 简化版)

⚠️ 提示:代码直接用文本编辑器手打,可能存在拼写、语法、引用不正确的地方,欢迎指正。同时案例代码仅供参考,切勿直接复制到业务生产环境中使用!!!


// 支付平台枚举
enum PayPlatform {
    // 微信
    WECHAT,
    // 支付宝
    ALIPAY,
    // 苹果内购
    APPLE;
    // 更多的聚合支付
}

// ===== 下单策略接口 =====
interface PayStrategy {
    String pay(String orderId, long amount);

    PayPlatform getPayPlatform();
}

// 微信支付
class WechatPay implements PayStrategy {
    public String pay(String orderId, long amount) {
        System.out.println("调用微信支付接口 -> 订单: " + orderId + ", 金额: " + amount);
        return "https://wx.com";
    }

    public PayPlatform getPayPlatform() {
        return PayPlatform.WECHAT
    }
}

// 支付宝支付
class AlipayPay implements PayStrategy {
    public String pay(String orderId, long amount) {
        System.out.println("调用支付宝支付接口 -> 订单: " + orderId + ", 金额: " + amount);
        return "https://aplipay.com";
    }

    public PayPlatform getPayPlatform() {
        return PayPlatform.ALIPAY
    }
}

// 苹果内购(实际苹果内购并不需要调用苹果服务端接口,在最新的苹果内购中,苹果支持传一个自定义UUID,苹果在客户端验证订单或异步回调中返回该UUID,类似于我们传给微信、支付宝预下单时的系统订单号,后续开篇文章讲苹果内购)
class ApplePay implements PayStrategy {
    public String pay(String orderId, long amount) {
        System.out.println("调用苹果内购接口 -> 订单: " + orderId + ", 金额: " + amount);
        return UUID.random().toString();
    }

    public PayPlatform getPayPlatform() {
        return PayPlatform.APPLE
    }
}

// 如果不用spring的项目,可以增加支付策略工厂,下面的PayService类会有依赖spring的另一种工厂实现方式
class PayStrategyFactory {
    public static PayStrategy getPayStrategy(PayPlatform payPlatform) {
        switch (payPlatform) {
            case WECHATreturn new WechatPay();
            case ALIPAYreturn new AlipayPay();
            case APPLEreturn new ApplePay();
            defaultthrow new IllegalArgumentException("未知支付类型: " + payPlatform.name());
        }
    }
}

---------------------------------------------------------------------------------------------------------------

// 下面是支付场景,与支付策略类似

// 支付场景枚举
enum PayScene {
    // 金币
    COIN,
    // vip
    VIP;
}

// ===== 下单场景策略接口 =====
interface PaySceneHandler {
    // 返回支付金额,单位分
    Long handle(Long chargeProductId);

    PayScene getPayScene();

    // 这个方法看实际业务而定,如果生成订单后,需要继续实现业务逻辑,子类可以重写此方法(大部分项目应该都用jdk8了吧,如果低于jdk8,就老老实实写抽象方法)
    default void process(Order order) {

    }
}

// 充值金币
class ChargeCoin implements PaySceneHandler {

    public Long handle(Long chargeProductId) {
        System.out.println("充值金币");
        // 从 charge_product_coin 表查出支付金额
        long amount = 10L;
        return amount;
    }

    public PayScene getPayScene() {
        return PayScene.COIN
    }
}

// 充值vip
class ChargeVip implements PaySceneHandler {
    public void handle(Long chargeProductId) {
        System.out.println("充值VIP");
        // 从 charge_product_vip 表查出支付金额
        long amount = 20L;
        return amount;
    }

    public PayScene getPayScene() {
        return PayScene.VIP
    }
}

// ===== 服务类 =====
class PayService {

    // spring的依赖注入,将所有支付策略实现类注入
    @Resource
    private List<PayStrategy> payStrategies;

    // spring的依赖注入,将所有支付场景实现类注入
    @Resource
    private List<PaySceneHandler> paySceneHandlers;

    // 通过传过来的支付平台,与spring注入的策略对比
    public PayStrategy getPayStrategy(PayPlatform payPlatform) {
        for (PayStrategy payStrategy : payStrategies) {
            if (payStrategy.getPayPlatform() == payPlatform) {
                return payStrategy;
            }

            throw new IllegalArgumentException("未知支付类型: " + payPlatform.name());
        }
    }

    // 通过传过来的支付场景,与spring注入的场景对比
    public PaySceneHandler getPaySceneHandler(PayScene payScene) {
        for (PaySceneHandler paySceneHandler : paySceneHandlers) {
            if (paySceneHandler.getPayScene() == payScene) {
                return paySceneHandler;
            }

            throw new IllegalArgumentException("未知支付场景: " + payScene.name());
        }
    }

    // 下单
    public String prepareOrder(PayScene payScene, PayPlatform payPlatform, Long chargeProductId) {

        // 通过不同的支付场景,获取不同表的支付金额,也可以根据实际业务实现不同的逻辑
        PaySceneHandler sceneHandler = getPaySceneHandler(payScene);
        long amount = sceneHandler.handle(chargeProductId);

        // 创建订单,根据实际业务自行实现
        Order order = createOrder();
        String orderId = order.getId();

        // 没有spring的情况下通过工厂获取
        // PayStrategy strategy = PayStrategyFactory.getPayStrategy(payPlatform);
        PayStrategy strategy = getPayStrategy(payPlatform);

        // 此处调用不同支付厂家的下单接口
        String payResult = strategy.pay(orderId, amount);

        // 下单后如果还需要处理业务的话,继续调用
        sceneHandler.process(order);

        return payResult;
    }
}

// ===== controller =====
@RestController
@RequestMapping("/api/charge")
public class PayController {
    
    @Resource
    private PayService payService;

    @PostMapping("/prepare/order")
    public String prepareOrder(PayScene payScene, PayPlatform payPlatform, Long chargeProductId) {
        return payService.prepareOrder(payScene, payPlatform, chargeProductId);
    }
}

5. 异步回调

// ===== 回调策略接口 =====

interface CallbackSceneStrategy {
    void handle(String orderId);

    PayScene getPayScene();
}

// 充值金币回调处理
class ChargeCoinSceneCallback implements CallbackStrategy {
    public void handle(String orderId) {
        System.out.println("为订单 " + orderId + " 发放金币");
    }

    public PayScene getPayScene() {
        return PayScene.COIN
    }
}

// 购买VIP回调处理
class ChargeVipSceneCallback implements CallbackStrategy {
    public void handle(String orderId) {
        System.out.println("为订单 " + orderId + " 开通VIP");
    }

    public PayScene getPayScene() {
        return PayScene.VIP
    }
}

// 这里需要解释一下,每个支付平台,微信、支付宝、苹果内购等回调过来的参数不一样,需要根据payPlatform写策略类,
// 类似于支付策略,把不同的参数解析,校验签名等,然后返回订单号
interface CallbackParseStrategy {
    String parse(String requestBody);

    PayPlatform getPayPlatform();
}

// 微信支付回调解析
class WechatCallbackParse implements CallbackParseStrategy {
    public String parse(String requestBody) {
        System.out.println("解析微信回调参数,得到系统的orderId");
        String orderId = ""xxxxx;
        return orderId;
    }

    public PayPlatform getPayPlatform() {
        return PayPlatform.WECHAT
    }
}

// 支付宝支付回调解析
class AlipayCallbackParse implements CallbackParseStrategy {
    public String parse(String requestBody) {
        System.out.println("解析支付宝回调参数,得到系统的orderId");
        String orderId = ""xxxxx;
        return orderId;
    }

    public PayPlatform getPayPlatform() {
        return PayPlatform.ALIPAY
    }
}

// 苹果内购回调解析
class AppleCallbackParse implements CallbackParseStrategy {
    public String parse(String requestBody) {
         System.out.println("解析苹果回调参数,得到系统的orderId");
        String orderId = ""xxxxx;
        return orderId;
    }

    public PayPlatform getPayPlatform() {
        return PayPlatform.APPLE
    }
}

// 回调服务类
class CallbackService {

    // spring的依赖注入,将所有回调策略实现类注入
    @Resource
    private List<CallbackSceneStrategy> sceneStrategies;
    private List<CallbackParseStrategy> parseStrategies;

    // 通过传过来的支付场景,与spring注入的场景对比
    public CallbackSceneStrategy getCallbackSceneStrategy(PayScene payScene) {
        for (CallbackSceneStrategy sceneStrategy : sceneStrategies) {
            if (sceneStrategy.getPayScene() == payScene) {
                return sceneStrategy;
            }

            throw new IllegalArgumentException("回调未知支付场景: " + payScene.name());
        }
    }

    public CallbackParseStrategy getCallbackParseStrategy(PayPlatform payPlatform) {
        for (CallbackParseStrategy parseStrategy : parseStrategies) {
            if (parseStrategy.getPayPlatform() == payPlatform) {
                return parseStrategy;
            }

            throw new IllegalArgumentException("回调未知支付平台: " + payScene.name());
        }
    }

    
    public void callback(String requestBody, PayPlatform payPlatform) {
        
        // 解析回调参数得到订单号
        CallbackParseStrategy parseStrategy = getCallbackParseStrategy(payPlatform);
        String orderId = parseStrategy.parse(requestBody);

        // 自行实现getOrder获取订单信息
        Order order = getOrder(orderId);
        CallbackSceneStrategy sceneStrategy = getCallbackSceneStrategy(order.getPayScene());

        // 发放金币或充值vip等
        callbackStrategy.handle(orderId)
    }
}

// ===== 回调的controller =====
@RestController
@RequestMapping("/api/callback")
public class CallbackController {
    
    @Resource
    private CallbackService callbackService;

    @PostMapping("/wechat")
    public String wechatCallback(String requestBody) {
        return callbackService.callback(requestBody, PayPlatform.WECHAT);
    }

    @PostMapping("/alipay")
    public String alipayCallback(String requestBody) {
        return callbackService.callback(requestBody, PayPlatform.ALIPAY);
    }

    @PostMapping("/apple")
    public String appleCallback(String requestBody) {
        return callbackService.callback(requestBody, PayPlatform.APPLE);
    }
}

⚠️ 提示:在实际业务中,建议将参数、返回值使用对象的形式,后续参数有变,只需要改对象属性,不用更改方法的参数个数

6. 优化点

  • 两次策略模式应用

    • 下单策略(按支付厂家)
    • 回调策略(按业务类型)
  • 任何新增支付厂家或新增业务场景都不会影响原有流程

  • 回调逻辑和支付逻辑完全解耦,方便测试和维护


7. 总结

在真实项目中,支付相关业务很容易写成一团糟。 使用策略模式+工厂模式,将支付渠道业务回调分别解耦,可以大幅提升系统扩展性和可维护性。 在设计模式专栏中,每种设计模式都列出了优点与缺点,实际业务使用到设计模式时,自然也会把设计模式的优点与缺点一并带入。 本次使用到策略模式、工厂模式,会导致类数量膨胀,如果不用设计模式,会导致单个类代码膨胀,设计模式并不是银弹,不能完美解决我们的问题,需要自己在实际业务中做取舍,

⚠️ 提示:设计模式不是炫技,而是解决实际问题的手段。在支付业务中,优先保证流程稳定、安全,其次再考虑优雅的模式实现。

8. 附:支付平台与支付方式解释

国内的的支付平台,基本就是微信支付宝苹果内购,但还有一些第三方支付,他们内部其实也是接入了微信支付宝,需要区分支付平台与支付方式,我上面的案例实际上是APP原生支付,在我自己的项目中,我是把微信支付宝苹果内购当作支付方式让客户端传过来,至于实际接入哪家厂商的支付,由后台管理可配置切换。客户端传入微信支付宝苹果内购时,苹果内购比较特殊,是固定苹果内购,不需要读取后台管理配置的支付厂商(除非贵公司冒着被封号的风险在iOS端做切支付),如果传入微信支付宝,需要先读取后台管理配置的支付厂商,如果是原生支付,直接调用微信支付宝的下单接口,如果是某些聚合支付,调用聚合支付厂商的api接口,传入微信支付宝的支付方式进行下单。可以按下面层级区分:

支付厂商(平台)支付方式
微信官方APP原生支付、H5网页支付、扫码支付等
支付宝官方APP原生支付、H5网页支付、扫码支付等
聚合支付微信、支付宝(至于是微信官方、支付宝官方哪种支付方式,还得看具体的聚合支付厂商而定,我们只保留到这一层级就可以了)

扫码关注订阅号

Beyond Code 程序员公众号二维码.jpg