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

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

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 或其子类 (SkuPromotionResult, ShopPromotionResult, PlatformPromotionResult)。这种设计:

  • 统一了返回格式: 无论何种促销,应用端 (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

相关推荐
律宏阔11 小时前
WSL Docker 端口明明空闲,但就是绑不上端口
linux·windows
律宏阔11 小时前
Windows 禁用联想笔记本所有 Lenovo 后台服务
windows
律宏阔11 小时前
WSL 突然断网,无法 ping 内网或外网
linux·windows
穷人小水滴11 小时前
用容器编译 VirtualBox 虚拟机软件 (ArchLinux, podman)
linux·容器·virtualbox
乱码三千12 小时前
如何优雅地直连无公网 IP 的远程 GPU 服务器
linux·人工智能·程序员
yunwei3712 小时前
使用 eBPF 跟踪 Nginx 请求
linux·后端·性能优化
yunwei3712 小时前
使用 eBPF 跟踪 MySQL 查询
linux·后端·性能优化
yunwei3712 小时前
eBPF 示例教程:使用 XDP 捕获 TCP 信息
linux·后端·性能优化
GeW12 小时前
制造业高端化,为什么必须重估Linux和数据库?
linux
GeW12 小时前
工业场景中的Linux与数据库选型:高端化转型的技术底座
linux