在电商系统中,促销计算是业务逻辑最复杂、变更最频繁的模块之一。它不仅需要处理多种促销类型(满减、折扣、优惠券等),还要管理它们之间的优先级和互斥关系。
Shoptnt 设计了一套基于 策略模式 (Strategy Pattern) 和 责任链模式 (Chain of Responsibility) 的促销计算引擎,实现了极高的灵活性和可扩展性。本文将深入剖析其核心实现,揭示其如何优雅地管理复杂的促销规则。
项目地址: https://gitee.com/bbc-se/shoptnt
一、 核心架构:PromotionHandler 策略接口
一切的核心是 PromotionHandler
接口。它定义了一个促销计算单元的契约,是一种典型的策略模式应用。
java
public interface PromotionHandler {
// 策略方法:执行促销计算
List<PromotionResult> execute(Promotion promotion, List<SkuDeal> skuDealList);
// 标识策略类型:处理哪种促销(如 HALF_PRICE)
PromotionTypeEnum promotionType();
// 标识策略层级:处理哪个层级(SKU, SHOP, PLATFORM)
PromotionLevel promotionLevel();
}
设计要点:
-
单一职责: 每个
PromotionHandler
只负责一种特定类型促销的计算逻辑,如HalfPriceHandler
只处理第二件半价。 -
开闭原则: 新增促销类型时,只需实现一个新的
PromotionHandler
,无需修改现有代码。 -
明确标识:
promotionType()
和promotionLevel()
方法使得调度器可以精准地找到并调用对应的处理器。
二、 调度中心:PromotionCalculateClientImpl
PromotionCalculateClientImpl
是促销计算的调度中心 或上下文 (Context) 。它的核心作用是收集所有 PromotionHandler
策略,并按需调用它们。
1. 自动收集所有策略:
通过 Spring 的依赖注入,所有实现了 PromotionHandler
的 Bean 都会被自动注入到 promotionHandlerList
中。
java
@Autowired
private List<PromotionHandler> promotionHandlerList; // 所有促销策略的集合
2. 分层过滤与执行:
在 calculateShopPromotion
方法中,调度器首先过滤出非平台级别的处理器(!handler.promotionLevel().equals(PromotionLevel.platform)
),然后遍历这些处理器进行计算。
java
// 1. 过滤出需要的策略(店铺级和SKU级)
List<PromotionHandler> shopHandlerList = promotionHandlerList.stream()
.filter(handler -> !handler.promotionLevel().equals(PromotionLevel.platform))
.collect(Collectors.toList());
// 2. 遍历策略列表,让每个策略都尝试计算
for (PromotionHandler promotionHandler : shopHandlerList) {
List<PromotionResult> resultList = calculateShopPromotion(promotionList, promotionHandler, skuList);
promotionResultList.addAll(resultList);
}
3. 策略匹配:
在 calculateShopPromotion
(私有方法) 中,调度器会遍历所有促销活动,将活动类型与处理器的类型进行匹配。只有匹配的处理器才会被执行。
java
private List<PromotionResult> calculateShopPromotion(List<Promotion> promotionList,
PromotionHandler promotionHandler,
List<SkuDeal> skuList) {
for (Promotion promotion : promotionList) {
// 关键:促销活动类型 必须 匹配 处理器类型
if (promotion.getType().equals(promotionHandler.promotionType())) {
// 匹配成功,执行该策略的计算逻辑
List<PromotionResult> promotionResults = promotionHandler.execute(promotion, skuList);
promotionResultList.addAll(promotionResults);
}
}
}
这个过程形成了一个隐式的责任链:调度器将促销活动和商品信息传递给一系列处理器,每个处理器只处理自己关心的那部分。
三、 策略实现:以 HalfPriceHandler 为例
让我们以 HalfPriceHandler
为例,看一个具体的策略是如何实现的。
1. 标识身份:
java
@Service
@Order(PromotionOrder.HalfPrice) // 定义计算优先级
public class HalfPriceHandler implements PromotionHandler {
@Override
public PromotionTypeEnum promotionType() {
return PromotionTypeEnum.HALF_PRICE; // 我负责处理第二件半价
}
@Override
public PromotionLevel promotionLevel() {
return PromotionLevel.sku; // 我是SKU级别的活动
}
}
@Order(PromotionOrder.HalfPrice)
注解至关重要,它定义了该处理器在 promotionHandlerList
中的执行顺序,确保了"单品优惠"先于"组合优惠"计算。
2. 核心计算逻辑 (execute
方法):
-
遍历商品: 处理器会遍历传入的所有商品 (
SkuDeal
)。 -
检查资格: 检查商品是否参与了当前的第二件半价活动 (
promotion.getSkuIdList().contains(...)
)。 -
计算优惠: 如果满足条件,则计算优惠金额。逻辑是:
优惠金额 = (购买数量 / 2) * (单价 / 2)
。 -
构建结果: 将计算结果封装成一个
SkuPromotionResult
对象并返回。注意:它修改了SkuDeal
的subtotal
(小计金额),这个修改后的值会传递给后续的处理器,从而实现促销的叠加计算。
java
// 在 handle 方法中
double subtotal = skuDeal.getSubtotal(); // 获取当前小计(可能已被之前的处理器优惠过)
subtotal = CurrencyUtil.sub(subtotal, discount); // 减去本次优惠金额
skuDeal.setSubtotal(subtotal); // 设置新的小计,影响后续计算
四、 计算顺序的控制:@Order 注解
PromotionOrder
类定义了不同促销类型的执行顺序,这是保证复杂促销规则能正确叠加的关键。
java
public class PromotionOrder {
public static final int Minus = 10; // 单品立减
public static final int Seckill = 15; // 秒杀
public static final int HalfPrice = 15; // 第二件半价
public static final int FullMinus = 25; // 满减
public static final int ShopCoupon = 30; // 店铺券
public static final int PlatformCoupon = 35; // 平台券
}
执行顺序规则:
-
价格直降型优先: 如
Minus
(立减)、Seckill
(秒杀)、HalfPrice
(第二件半价)等直接修改商品单价的活动最先计算。 -
满减活动次之:
FullMinus
(满减)等基于总价条件的活动随后计算。 -
优惠券最后:
ShopCoupon
和PlatformCoupon
最后计算,因为它们通常是基于所有优惠后的最终金额进行减免。
这种顺序符合商业直觉:先享受单品折扣,再享受满减优惠,最后用券抵扣。
五、 结果的统一抽象:PromotionResult 体系
所有促销计算的结果都统一返回为 PromotionResult
或其子类 (SkuPromotionResult
, ShopPromotionResult
, PlatformPromotionResult
)。这种设计:
-
统一了返回格式: 无论何种促销,应用端 (
cart
模块) 都使用同一套接口来处理结果。 -
包含了丰富信息: 不仅包含优惠金额 (
cashBack
),还包含赠品信息 (giftList
)、运费减免 (isFreeFreight
)、提示信息 (promotionTips
) 等。 -
支持多态: 使用
@JsonTypeInfo
注解,方便在序列化和反序列化时自动处理不同的子类。
总结:如何新增一个促销规则?
假设我们要增加一个"买三送一"的活动。
-
定义促销类型: 在
PromotionTypeEnum
中新增BUY_THREE_GET_ONE
。 -
实现策略处理器: 创建一个新的
BuyThreeGetOneHandler
类,实现PromotionHandler
接口。-
在
promotionType()
中返回BUY_THREE_GET_ONE
。 -
在
promotionLevel()
中返回PromotionLevel.sku
。 -
在
execute()
方法中实现"买三送一"的逻辑:计算应赠送的数量,并可能修改SkuDeal
的数量或设置赠品信息到PromotionResult
中。
-
-
定义执行顺序: 在
PromotionOrder
中为其定义一个顺序值(例如18
),位于单品折扣和满减之间。 -
完成! 由于调度器是自动收集所有
PromotionHandler
的,你的新处理器会自动被纳入计算流程,无需修改任何调度逻辑。
架构优势
-
极致解耦: 计算逻辑 (
promotion
模块) 与应用逻辑 (cart
模块) 完全分离,通过PromotionResult
DTO 进行通信。 -
高可扩展性: 新增促销类型如同插拔组件,符合开闭原则。
-
灵活的计算顺序: 通过
@Order
轻松管理复杂的优先级和叠加规则。 -
易于测试: 每个
PromotionHandler
都可以被单独测试。
Shoptnt 的促销计算引擎是一个经典且优秀的设计范例,完美展示了如何用设计模式解决复杂的业务问题。欢迎访问项目源码深入学习: