Shoptnt 促销计算引擎详解:策略模式与责任链的完美融合

发布于:2025-09-12 ⋅ 阅读:(22) ⋅ 点赞:(0)

在电商系统中,促销计算是业务逻辑最复杂、变更最频繁的模块之一。它不仅需要处理多种促销类型(满减、折扣、优惠券等),还要管理它们之间的优先级和互斥关系。

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; // 平台券
}

执行顺序规则:

  1. 价格直降型优先: 如 Minus(立减)、Seckill(秒杀)、HalfPrice(第二件半价)等直接修改商品单价的活动最先计算。

  2. 满减活动次之: FullMinus(满减)等基于总价条件的活动随后计算。

  3. 优惠券最后: ShopCoupon 和 PlatformCoupon 最后计算,因为它们通常是基于所有优惠后的最终金额进行减免。

这种顺序符合商业直觉:先享受单品折扣,再享受满减优惠,最后用券抵扣。

五、 结果的统一抽象:PromotionResult 体系

所有促销计算的结果都统一返回为 PromotionResult 或其子类 (SkuPromotionResultShopPromotionResultPlatformPromotionResult)。这种设计:

  • 统一了返回格式: 无论何种促销,应用端 (cart 模块) 都使用同一套接口来处理结果。

  • 包含了丰富信息: 不仅包含优惠金额 (cashBack),还包含赠品信息 (giftList)、运费减免 (isFreeFreight)、提示信息 (promotionTips) 等。

  • 支持多态: 使用 @JsonTypeInfo 注解,方便在序列化和反序列化时自动处理不同的子类。

总结:如何新增一个促销规则?

假设我们要增加一个“买三送一”的活动。

  1. 定义促销类型: 在 PromotionTypeEnum 中新增 BUY_THREE_GET_ONE

  2. 实现策略处理器: 创建一个新的 BuyThreeGetOneHandler 类,实现 PromotionHandler 接口。

    • 在 promotionType() 中返回 BUY_THREE_GET_ONE

    • 在 promotionLevel() 中返回 PromotionLevel.sku

    • 在 execute() 方法中实现“买三送一”的逻辑:计算应赠送的数量,并可能修改 SkuDeal 的数量或设置赠品信息到 PromotionResult 中。

  3. 定义执行顺序: 在 PromotionOrder 中为其定义一个顺序值(例如 18),位于单品折扣和满减之间。

  4. 完成! 由于调度器是自动收集所有 PromotionHandler 的,你的新处理器会自动被纳入计算流程,无需修改任何调度逻辑。

架构优势

  • 极致解耦: 计算逻辑 (promotion 模块) 与应用逻辑 (cart 模块) 完全分离,通过 PromotionResult DTO 进行通信。

  • 高可扩展性: 新增促销类型如同插拔组件,符合开闭原则。

  • 灵活的计算顺序: 通过 @Order 轻松管理复杂的优先级和叠加规则。

  • 易于测试: 每个 PromotionHandler 都可以被单独测试。

Shoptnt 的促销计算引擎是一个经典且优秀的设计范例,完美展示了如何用设计模式解决复杂的业务问题。欢迎访问项目源码深入学习:

https://gitee.com/bbc-se/shoptnt


网站公告

今日签到

点亮在社区的每一天
去签到