设计模式专栏(五):设计模式在实际项目中的应用 —— 支付系统扩展与回调处理案例(关注“Beyond Code 程序员”订阅号查看更多文章)
在真实业务开发中,设计模式并不是教科书上的概念,而是解决实际问题的一种思维方式。下面我结合我在项目中的真实经验,分享支付场景下如何运用设计模式来提升代码的扩展性和可维护性。
⚠️ 提示:设计模式是一种编程思想,其实现方式并非唯一。本文示例代码仅作参考,并不意味着设计模式必须按照示例中的方式实现。
1. 背景故事
刚开始项目只接入了微信支付和支付宝支付,业务也很单一。 后来业务扩展:
-
接入了苹果内购、聚合支付(A聚合、B聚合等,它们内部还是调用微信或支付宝)。
-
支付场景分为多种:
- 充值金币(虚拟币)
- 购买 VIP 套餐(直接人民币购买)
- 后续更多场景
关键点:无论下单时的业务是什么,真正的业务处理都在支付回调时完成。 因此,需要同时解决:
- 多支付方式接入的可扩展性
- 支付回调业务逻辑的解耦
为了演示代码,下面简单创建了订单表、充值产品表-金币,充值产品表-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(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '系统订单号',
`third_order_sn` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '第三方订单支付流水号',
`amount` bigint unsigned NOT NULL DEFAULT '0' COMMENT '订单金额,单位分',
`pay_platform` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '支付厂家平台,alipay 支付宝,wechat 微信,apple 苹果',
`pay_way` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '支付方式,app 客户端原生支付,h5 网页端支付,apple 苹果内购',
`pay_scene` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '充值场景,icon 金币,vip 购买vip',
`state` varchar(10) CHARACTER 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(64) CHARACTER 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(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '状态,Y 上架,N 下架',
`apple_product_id` varchar(32) CHARACTER 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(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT 'app平台,Android 安卓,iOS 苹果',
`type` varchar(64) CHARACTER 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(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '状态,Y 上架,N 下架',
`apple_product_id` varchar(32) CHARACTER 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 WECHAT: return new WechatPay();
case ALIPAY: return new AlipayPay();
case APPLE: return new ApplePay();
default: throw 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网页支付、扫码支付等 |
| 聚合支付 | 微信、支付宝(至于是微信官方、支付宝官方哪种支付方式,还得看具体的聚合支付厂商而定,我们只保留到这一层级就可以了) |
扫码关注订阅号