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

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 {@Overridepublic PromotionTypeEnum promotionType() {return PromotionTypeEnum.HALF_PRICE; // 我负责处理第二件半价}@Overridepublic 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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/news/922221.shtml
繁体地址,请注明出处:http://hk.pswp.cn/news/922221.shtml
英文地址,请注明出处:http://en.pswp.cn/news/922221.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【HTTP 请求格式】从请求行 到 请求体

引言 在前后端开发中&#xff0c;前端和后端之间的交互主要依赖于 HTTP&#xff08;HyperText Transfer Protocol&#xff0c;超文本传输协议&#xff09;。HTTP 是互联网通信的基础&#xff0c;它定义了客户端&#xff08;通常是浏览器或App&#xff09;和服务器之间如何交换数…

【自记】SQL 中 GROUPING 和 GROUPING SETS 语句的案例说明

我们用一个生活中的例子来理解&#xff0c;比如你开了家小超市&#xff0c;想统计「销售额」&#xff0c;但需要从多个角度看&#xff08;比如按 “日期 商品”、“仅日期”、“仅商品”、“整体总销售额”&#xff09;。假设你的销售数据长这样&#xff08;简化版&#xff09…

C语言第五课:if、else 、if else if else 控制语句

C语言第五课&#xff1a;if、else 、if else if else 控制语句if else 、if else if else 联合使用编程快速学习平台if else 、if else if else 联合使用 代码示列 #include <stdio.h> int main(){//设置中文编码输出到控制台system("chcp 65001");//今天星…

七彩喜智慧养老:用科技温暖晚年,让关爱永不掉线

“当银发潮遇见科技力&#xff0c;养老方式正在发生一场静悄悄的变革。”你有没有想过&#xff1a;当父母年迈独居时&#xff0c;如何确保他们的安全&#xff1f;当老人突然摔倒&#xff0c;如何第一时间获得救助&#xff1f;当慢性病需要长期管理&#xff0c;如何避免频繁奔波…

window显示驱动开发—为头装载和专用监视器生成自定义合成器应用(二)

显示相关的 API 的比较 API用途和目标受众DisplayInformation用于检索 CoreWindow 的呈现和布局属性。HdmiDisplayInformation用于枚举和设置受限模式集的仅限 Xbox 的 API。 高度专用于 Xbox 媒体应用方案。DisplayMonitor用于查询物理监视器设备的属性。 不公开有关操作系统…

Linux 高性能 I/O 事件通知机制的核心系统调用—— `epoll_ctl`

epoll 是 Linux 上处理大量文件描述符 I/O 事件的高效模型&#xff0c;而 epoll_ctl 则是你用来指挥 epoll 实例&#xff08;epoll instance&#xff09;的“遥控器”&#xff0c;负责向它添加、修改或删除需要监视的文件描述符&#xff08;FD&#xff09;及其感兴趣的事件。1.…

mysql 必须在逗号分隔字符串和JSON字段之间二选一,怎么选

如果必须在逗号分隔字符串和JSON字段之间二选一&#xff0c;那么 JSON字段是明显更好的选择。以下是详细的对比分析&#xff1a;对比结论&#xff08;直接看这里&#xff09;方面JSON字段逗号分隔字符串胜出方查询能力✅ 丰富的JSON函数支持❌ 只能使用LIKE模糊查询JSON数据验证…

DPI和DIP的区别

DPI 和 DIP 是两个在计算机图形和移动开发领域常见的术语&#xff0c;它们都与屏幕显示和尺寸有关&#xff0c;但含义和用途不同。 DPI (Dots Per Inch) 定义&#xff1a;DPI 的全称是 Dots Per Inch&#xff0c;即每英寸点数。它是一个衡量物理密度的单位&#xff0c;表示在…

数据帮助我们理解未知世界

主持人 尼古拉安根&#xff1a; 大家好&#xff0c;我是挪威南方财富基金首席执行官尼古拉安根。今天非常荣幸能与大卫斯皮格尔哈尔特爵士对话。坦率地说&#xff0c;他不仅是世界上最优秀的统计学家之一&#xff0c;也是我见过的最佳风险沟通者。他撰写了大量优秀著作&#xf…

在使用git的很多操作是保持工作区干净

这是一条铁律下面是错误操作&#xff1a;自己明明写完了代码&#xff0c;想要提交。此时你的工作区长这样你的提交顺序是&#xff1a;git pull -> git commit -> git push但是现实往往不这样&#xff0c;万一拉下来的代码和你当前工作区的代码有冲突&#xff0c;你必须要…

通过语法推导树快速求短语,简单短语和句柄

第一步&#xff1a;写出规范推导&#xff08;最右&#xff09;序列 规范推导就是最右推导。我们的目标是从起始符号 E 出发&#xff0c;通过每步替换最右边的非终结符&#xff0c;最终得到句型 R(Pi)。 文法 G[E]: E :: RP | PP :: (E) | iR :: RP | RP* | P | P* 推导过程&…

智能学习辅助系统-部门管理开发

文章目录准备工作工程搭建增删改查查询部门删除部门新增部门修改部门查询回显修改数据日志技术准备工作 需求&#xff1a;部门管理的查询、新增、修改、删除 使用REST风格的URL&#xff1a; GET &#xff1a; 查询POST &#xff1a;新增PUT &#xff1a; 修改DELETE &#x…

【图解】idea中快速查找maven冲突

现象 今天启动项目时&#xff0c;总是以下报错&#xff0c;并退出SLF4J: Class path contains multiple SLF4J bindings. SLF4J: Found binding in [jar:file:/F:/.m2/repository/org/apache/logging/log4j/log4j-slf4j-impl/2.13.3/log4j-slf4j-impl-2.13.3.jar!/org/slf4j/im…

LightGBM、XGBoost和CatBoost自定义损失函数和评估指标

LightGBM、XGBoost和CatBoost自定义损失函数和评估指标函数&#xff08;缩放误差&#xff09;数学原理损失函数定义梯度计算评估指标LightGBM实现自定义损失函数自定义评估指标使用方式XGBoost实现自定义损失函数自定义评估指标使用方式CatBoost实现自定义损失函数自定义评估指…

2025-09-08升级问题记录: 升级SDK从Android11到Android12

将 Android 工程的 targetSdkVersion 从 30 &#xff08;Android 11&#xff09;升级到 31&#xff08;Android 12&#xff09;需要关注一些重要的行为变更和适配点。 主要适配要点&#xff1a; 适配类别关键变更点适配紧迫性简要说明组件导出属性声明了 Intent Filter 的组件…

利用OpenCV实现模板与多个对象匹配

代码实现&#xff1a;import cv2 import numpy as npimg_rgb cv2.imread(mobanpipei.jpg) img_gray cv2.cvtColor(img_rgb, cv2.COLOR_BGR2GRAY) template cv2.imread(jianto.jpg, flags0) h, w template.shape[:2]# 读取图像# # 顺时针旋转 90 度&#xff08;k1&#xff0…

OS28.【Linux】自制简单的Shell的修bug记录

目录 1.问题代码 2.排查 前期检查 查找是谁修改了environ[0] 使用gdb下断点 查看后续的影响 分析出问题的split_commandline函数 3.反思 4.正确代码 5.结论 6.除此之外...... ★提示: 此bug非常隐蔽,不仔细分析很难查出问题,非常锻炼调试能力! 1.问题代码 #includ…

Debian 系统上安装与配置 MediaMTX

&#x1f3af; 在 Debian 系统上安装与配置 MediaMTX&#xff08;原 rtsp-simple-server&#xff09;&#xff1a;打造轻量级流媒体服务器 作者&#xff1a;远在太平洋 环境&#xff1a;Debian 10/11/12 | Ubuntu 可参考 关键词&#xff1a;MediaMTX、rtsp-simple-server、RTSP…

分布式专题——10.4 ShardingSphere-Proxy服务端分库分表

1 为什么要有服务端分库分表&#xff1f; ShardingSphere-Proxy 是 ShardingSphere 提供的服务端分库分表工具&#xff0c;定位是“透明化的数据库代理”。 它模拟 MySQL 或 PostgreSQL 的数据库服务&#xff0c;应用程序&#xff08;Application&#xff09;只需像访问单个数据…

Mysql相关的面试题1

什么是聚集索引&#xff08;聚簇索引&#xff09;&#xff1f;什么是二级索引&#xff08;非聚簇索引&#xff09;&#xff1f; 聚集索引就是叶子节点关联行数据的索引&#xff0c;二级索引就是叶子节点关联主键的索引&#xff0c;聚集索引必须有且仅有一个&#xff0c;二级索引…