从一张优惠券,看 SAP Commerce 20 年的促销引擎演进
引子:你以为的"打折",远比你想的复杂
双十一零点。
你打开淘宝,购物车里有 3 件商品,总价 487 元。
页面上自动跳出来几行字:
"已享受满 300 减 30 优惠" "88VIP 专享额外 9.5 折" "店铺券 -20"
3 秒之内,3 条促销规则被同时算完:谁先谁后、哪两条冲突只能挑一条、最终减多少——全部算清楚。
你以为这只是"打个折"?
实际上,背后是一套大众已经用了几十年的常见做法——用配置代替代码,让业务规则像数据一样可以增删改查。不只电商促销,风控决策、合规审核、工单路由……所有"业务规则需要频繁变化"的系统都会遇到同样的工程挑战,也都收敛到类似的解法。
这篇文章用 SAP Commerce 作为参考样板:它在企业级电商领域跑了 20 年,6.0 版本引入了 Drools 规则引擎,10 年后又开始反思 Drools 的代价、探索新方向——这条 20 年的演进曲线本身就是好教材。
一、促销不是打折,是 5 大业务战略工具
聊架构之前,我们先建立共同语言——促销到底解决什么生意问题?
促销不是"亏钱讨好客户"——它是企业的战略撬动点。[[]]把它的业务价值拆开看,有 5 个维度:
接下来这篇文章里所有的技术决策,本质上都在服务这 5 个目标。
二、电商常见的 7 种促销玩法
业界电商的促销玩法千变万化,但拆开看其实就 7 大基础类型。
每种玩法可以产生 4 种效果。拿一件 100 元的 T 恤举例:
所以是"7 种玩法 × 4 种效果"的组合矩阵:
7 类 × 4 种效果 ≈ 23 种有效组合——这就是任何促销系统至少要覆盖的"基础武器库"。
听起来不多对吧?
那我接下来要给你看一个真实的「组合复杂度」——让你瞬间理解为什么这事不简单。
三、组合复杂度:一个真实的双 11 场景
假设你是营销经理,要在双十一当天上线一套面向所有客户的促销组合。我们以一位典型客户 Alice 的购物车为例——她是 88VIP,购物车里有:
你需要让以下 6 条规则同时生效或互斥:
| # | 规则 | 对应业务战略 | 对应玩法类型 | 触发条件 / 备注 |
|---|---|---|---|---|
| 1 | 全场满 300 减 30 | 提客单价 | 满减 | 购物车 ≥ 300 |
| 2 | 全场满 500 减 80 | 提客单价 | 满减(更高档) | 购物车 ≥ 500(与规则 1 互斥取一) |
| 3 | iPhone 限时秒杀 9 折 | 引流主推 | 单品折扣 + 限时 | 仅 0:00-0:30 内有效 |
| 4 | 88VIP 专享额外 9.5 折 | 提升忠诚 | 客群专享 | 客户是 88VIP |
| 5 | iPhone + AirPods 立减 200 | 搭配销售 | 搭配优惠 | 同时含 iPhone + AirPods |
| 6 | 店铺通码 SHOP100 | 精准触达 | 优惠券(券码触发) | 客户输入券码 |
| 凑单提示 | 总价超 5000 解锁免运费 + 赠手机壳 | 提客单价(凑单激励) | 凑单提示 | 仅提示,未达 5000 时显示 |
每一条单独看都简单。但放在一起,业务方提出了 6 个非常现实的问题:
每一个问题,都在拷问系统的不同能力。把它们抽象成架构师必须回答的 5 大核心难题:
这 5 个难题,是任何促销系统的「必答题」。
接下来,我们就来看:如果让你从零设计一套系统,你会怎么解决这 5 个难题?
四、规则引擎怎么解决这 5 大难题(以 Drools 为样板)
4.1 为什么需要规则引擎?
直觉上最朴素的想法:用 if-else 写代码。
if (cart.getTotal() > 300) { cart.applyDiscount(30); }
if (cart.getTotal() > 500) { cart.applyDiscount(80); }
if (customer.isVip()) { cart.applyPercentage(0.95); }
// ...
5 条规则——还能写。50 条规则——开始混乱。500 条规则——系统不可维护。更可怕的是:业务每改一条规则都要开发改代码、发版、上线;想撤销得再发一次版;想 debug 要从头读代码。业务逻辑写死在代码里,业务的"变化速度"和代码的"修改速度"完全错配。
能不能用 Groovy 这种脚本语言?让业务人员写脚本?也不行——三个原因:
| 问题 | 后果 |
|---|---|
| 业务人员写不了脚本 | 没有编程基础,写脚本门槛太高 |
| 脚本无法做规则优化 | 100 条独立脚本意味着每次买东西要执行 100 次完整脚本 |
| 脚本无法表达规则关系 | "这两条冲突"、"这条优先级更高"——脚本怎么表达? |
→ 所以你需要的不是"嵌入式脚本",而是专门为「规则评估」设计的引擎——这就是 规则引擎(Rule Engine) 存在的意义。业界用得最广、跑了 20 多年的 Drools 给出的核心抽象是条件 + 动作:
当 (Condition) 满足时
执行 (Action)
听起来跟 if-else 没区别?关键在它的组合方式和调度机制。下面分 4 节看它具体怎么解决这 5 大难题——从"怎么表达"到"怎么调度",到"怎么快",最后到"完整生命周期"。
4.2 用 Condition + Action + Group 表达任意业务
Commerce 在管理后台为营销人员准备了一套可拖拽配置的规则元素——营销人员通过组合这些元素来描述一条促销规则,配置结果存进数据库。真正能被 Drools 执行的规则语言(DRL)由 Commerce 底层在发布时自动生成,再交给 Drools。整个过程营销人员都不用碰代码。
这套规则元素一共就三类:
- Condition(条件):满足什么前提才给折扣。比如"购物车金额满 500"、"客户是 VIP"、"是首单客户"。
- Action(动作):满足条件后做什么。比如"减 80 块"、"整单打 9 折"、"送一个赠品"。
- Group(条件组):把多个条件组合起来。可以指定多个条件都满足才算(AND),也可以指定满足任意一个就算(OR)。条件组里还能再放条件组,层层套下去。
直接看一个复杂一点的例子。
假设营销人员想做这样一个活动:
VIP 老客户买够 500 块,或者新客户的第一单——都可以享受 9 折优惠。
营销人员在后台拖出来的配置长这样:
Group (OR)
├── Group (AND)
│ ├── Condition: customer.group = "VIP"
│ └── Condition: cart.total ≥ 500
└── Group (AND)
├── Condition: customer.isNew = true
└── Condition: customer.orderCount = 0
Action: apply percentage discount 10%
可以看到,整个配置就是一棵「条件的树」:外层用 OR 把"老客户场景"和"新客户场景"并起来,每个场景里面再用 AND 把它的两个细节条件凑起来。营销人员只要点点鼠标,不用写一行代码。
这棵树被系统自动编译后,进到 Drools 引擎里就是这样一段规则代码(DRL):
rule "VIP_or_NewUser_9zhe"
when
(
$cart : CartRAO(total >= 500)
and UserRAO(groups contains "VIP")
)
or
UserRAO(isNew == true, orderCount == 0)
then
apply percentage discount 10%
end
💡
CartRAO、UserRAO是 Commerce 在评估时塞给 Drools 的简化数据对象——分别装着购物车和客户的关键字段。Drools 不认识"购物车"、"客户"这些电商概念,所以 Commerce 中间做了一层翻译。
Commerce 预置了一批 Condition 和 Action 的"模板"(满减、VIP、单品折扣、套装价等),营销人员只需要在后台挑模板、填具体数值——比如选"购物车满减"模板,填上"满 500 减 80"——其他规则元数据(Priority、Rule Group、最大次数)也都在同一个表单里填。这样一条规则就配好了,覆盖电商里绝大多数促销玩法。
不管业务诉求多复杂——条件再多、AND / OR 再绕、再怎么层层嵌套——本质上都是用 Condition、Action、Group 这三样东西拼出来的一棵"条件树"。业务规则千变万化,配置方式始终就这一套。
Commerce 开箱即用提供了一批 Condition 和 Action 模板,再配上 Group 的自由组合——理论上可以覆盖电商里几乎所有"如果……就……"型的业务规则。看几个双十一典型促销对应的组合:
| 业务诉求 | Condition 组合 | Action 组合 |
|---|---|---|
| 全场满 300 减 30 | 购物车金额 ≥ 300 | 订单固定折扣 30 |
| 88VIP 9.5 折 | 目标客户 = 88VIP | 订单百分比 5% |
| iPhone 秒杀 9 折 | 合格商品包含 iPhone AND 时间在 0:00-0:30 | 商品百分比 10% |
| 输入券码减 20 | 购物车金额 ≥ 200 AND 券码 = SHOP100 | 订单固定折扣 20 |
| 满 5000 送手机壳 | 购物车金额 ≥ 5000 | 送赠品 = 手机壳 |
→ 都是同一套元素的不同搭配——业务人员负责搭配,开发者负责元素本身的稳定性。极少数全新效果(如触发后给客户加积分、调外部 CRM 等)才需要扩展新的 Condition / Action,Commerce 也留好了标准的扩展点。
这解决了难题 1:业务可配置。
4.3 用 Priority + Rule Group + maxRuleExecutions 处理多规则共存
回到双十一那 6 条规则——光会表达"满足什么条件做什么动作"还不够,业务还希望它们之间有这样的关系:
| # | 业务诉求(Case) | 痛点 | Drools 的解决方案 |
|---|---|---|---|
| 1 | iPhone 限时秒杀 9 折要先于 88VIP 9.5 折评估 | 顺序不同,最终金额差几十块 | 给每条规则配 Priority(优先级),数字大的先评估 |
| 2 | 满 300 减 30 和满 500 减 80 是同档位的两个台阶,购物车过了 500 就只用第二档 | 不能两条都减,避免同性质优惠叠加 | 把这两条规则放进同一个 Rule Group(规则组)并标记互斥,组内只取最高优先级 |
| 3 | iPhone + AirPods 立减 200,客户买两套也只能减一次 | 不限次会被薅羊毛 | 给这条规则设 maxRuleExecutions = 1,一次评估里最多触发一次 |
营销人员后台配好之后,Commerce 会把这些配置编译成 Drools 可以执行的规则。为了说明机制,下面用一段简化伪 DRL 表示(真实生成的 DRL 会有更多元数据、查询和执行跟踪代码):
rule "iPhone_miaosha_9zhe"
salience 500 ← ① 全场最高优先级 → 第一个被评估
when
$cart : CartRAO()
$iphone : ProductRAO(code == "iphone15") from $cart.entries
DateRAO(now between "2026-11-11 00:00" and "2026-11-11 00:30")
then
apply percentage discount 10% on $iphone
end
rule "VIP_9.5zhe"
salience 400 ← ① 比 iPhone 秒杀低 → 在它之后才评估,
所以先扣 9 折再扣 VIP 9.5 折
when
$cart : CartRAO()
$user : UserRAO(groups contains "88VIP")
then
apply percentage discount 5% on $cart
end
rule "manjian500"
salience 350
@ruleGroup("manjian", exclusive = true) ← ② 进入"manjian"组、组内互斥;
350 > 300 且购物车过 500 → 命中并占住该组
when
$cart : CartRAO(total >= 500)
then
apply fixed discount 80 on $cart
end
rule "manjian300"
salience 300
@ruleGroup("manjian", exclusive = true) ← ② 同组,但 manjian500 已占住该组
→ 引擎跳过它,不再触发
when
$cart : CartRAO(total >= 300)
then
apply fixed discount 30 on $cart
end
rule "iPhone_AirPods_bundle"
salience 200
@maxRuleExecutions(1) ← ③ 即使买了 2 套 iPhone + AirPods
也只会触发 1 次,只减 200,不再减第二次
when
$cart : CartRAO()
$iphone : ProductRAO(code == "iphone15") from $cart.entries
$airpods : ProductRAO(code == "airpods") from $cart.entries
then
apply fixed discount 200 on $cart
end
这段伪代码想表达的是三个关键控制点:salience / Rule Group / maxRuleExecutions。回到双十一那 6 条规则,Commerce 会把营销人员配置的优先级、互斥组、最大执行次数翻译成规则元数据和运行时控制逻辑,最终按"优先级排队 → 互斥仲裁 → 限次执行"跑下来,命中结果如下:
不过这里有个容易让人误解的点值得展开讲一下:salience 是 Drools 原生的规则属性;但 maxRuleExecutions、Rule Group 这类促销语义,是 Commerce 通过规则元数据、执行跟踪和 AgendaFilter 等机制接到 Drools 上的。换句话说,Drools 提供规则匹配和触发机制,Commerce 在它之上补上了电商促销需要的业务策略。
Rule Group 互斥逻辑是怎么"嫁接"到 Drools 上的
@ruleGroup 在 Drools 看来只是一张"标签"
Drools 的 DRL 语法允许在规则上挂任意自定义元数据,格式就是 @key(value):
rule "manjian500"
@ruleGroup("manjian", exclusive = true) ← 一张自定义标签
salience 350
when ...
then ...
end
Drools 引擎对这种标签的行为是:
- ✅ 会读出这张标签的内容(提供 API 让外部程序查到)
- ❌ 但完全不知道它代表什么含义
- ❌ 不会根据它做任何"互斥"动作
对 Drools 来说,@ruleGroup("manjian", exclusive = true) 和 @xyz("foo", bar = 42) 没有任何区别——都只是规则上贴着的一张元数据标签而已。
真正实现互斥的是 Commerce 写的 AgendaFilter
要理解 AgendaFilter 是怎么工作的,先看 Drools 给外部代码暴露的几个核心 API:
| Drools API | 干什么 |
|---|---|
kieSession.insert(fact) | 把数据(Fact)扔进 Drools 的工作内存 |
kieSession.fireAllRules() | 触发引擎,按 salience 排队评估所有命中规则 |
kieSession.fireAllRules(agendaFilter) | 带过滤器的触发——每条规则被触发前,先问一下 filter 是否放行 |
AgendaFilter.accept(match) | 引擎调用这个回调,由外部代码决定"这条规则要不要真的执行" |
Commerce 实现一个自己的 AgendaFilter,在 accept(match) 里做电商业务判断:"这条规则属于哪个 Rule Group?组内已经有规则触发过了吗?"
下面这张时序图把整个 Commerce ↔ Drools 的对接流程串起来——从营销人员发布规则、到客户结算时的 5 个 API 调用、到 Drools 反向回调 AgendaFilter:

几个关键认知:
- 发布期是一次性的——规则编译成 DRL 后常驻 Rete 网络,不需要每次评估都重新灌
- 评估期数据先喂后触发——
insert× N 把数据扔进工作内存,fireAllRules才是按"开始"按钮 accept(match)是反向回调——Drools 评估每条规则前,反过来调 Commerce 提供的 filter,由 Commerce 决定放不放行- 结果通过
getObjects拿——Drools 在评估中产生的 Discount 对象躺在工作内存里,Commerce 主动取
→ Drools 只提供机制(拦截钩子),Commerce 提供策略(互斥逻辑)。这是一个非常典型的"机制与策略分离"设计——Drools 不绑死具体业务,Commerce 拿这个钩子实现自己想要的任何调度规则。也正因为如此,Rule Group 的互斥能力跟 Drools 本身解耦:以后想换引擎,只要新引擎也提供类似的拦截钩子,这套逻辑就能搬过去。
这解决了难题 2、3、4:多规则共存 + 时效性 + 一致性。
4.4 用 Rete 算法保证性能
讲完了"怎么表达",接下来是"怎么快"。
Drools 真正的"性能关键技巧"是 Rete 算法——这是 Drools 和"配置脚本"最大的差别。
朴素做法
假设你有 3 条规则都包含 cart.total > 100 这个条件:
规则 A:cart.total > 100 AND customer.isVip → 减 20
规则 B:cart.total > 100 AND cart.hasCategory("服装") → 减 10
规则 C:cart.total > 200 → 减 30
朴素做法是一条条遍历——同一个 cart.total > 100 被独立判断 3 次。3 条规则不算啥,200 条规则呢?同一个数据被反复访问几百次。
Rete 的做法:把规则编译成"共享判断树"
Rete 网络本质上是一个 DAG(有向无环图,Directed Acyclic Graph):
这种结构在计算机科学里随处可见:
| 场景 | 同样的 DAG 思想 |
|---|---|
| 编译器 | 表达式求值(公共子表达式只算一次) |
| 前端框架 | React 虚拟 DOM diff(共享相同的子树) |
| 数据库 | 查询计划优化(公共子查询合并) |
| 构建工具 | Make / Bazel 增量构建(变化的文件才重新编译) |
| 大数据引擎 | Spark / Flink 的 DAG 调度(任务依赖图) |
它们的共同思想都是:
「先把工作建成图,再让相同的工作只做一次,让没变的部分不重做。」
Rete 是 1979 年由 Charles Forgy 提出的算法——比 React 早 30 多年,但本质上是同一类思想的早期典范。
下面这张图就是 Rete 网络的可视化:
Rete 把规则拆成条件原子,构建一张"判断网络",相同条件的节点共享:
cart.total > 100 只判断了一次,所有依赖它的规则共享结果。
Rete 还做了一件更聪明的事:增量更新
当客户又加了一件商品,cart.total 从 150 变成 250:
- 朴素算法:重新评估所有规则——所有条件全部重新跑一遍
- Rete 算法:只重新评估"依赖 cart.total 这个节点"的那部分网络——其他节点(比如
customer.isVip、cart.hasCategory)的判断结果保持不变,直接复用
Rete = 把规则编译成共享判断的网络,运行时只算变化的部分。
这是一种典型的用空间换时间 算法:网络结构常驻内存(所以 Drools 内存消耗大),换取运行时的极速判断(所以 Drools 中等规模下性能极好)。
一个直观的类比
如果你做过前端:Rete 和 React 的虚拟 DOM diff 算法是同一类思想——把声明式描述编译成可增量更新的结构,让"变化"成为优化的核心。
这也解释了为什么 Groovy 这种通用脚本做不到——Groovy 不知道你的"100 条脚本之间有什么共享逻辑",只能一条条执行。Drools 知道你的"100 条规则有哪些共享条件",所以能优化。
这解决了难题 5 中的「性能扩展性」——Drools 让规则数量增加时,性能不是线性下降。
4.5 一条规则的完整生命周期:从创建到退货
到这里,4.1-4.4 分别讲了"怎么表达 / 怎么调度 / 怎么快 / 怎么对接"。最后一张图把一条规则的完整生命周期串起来——以"满 500 减 80"为例,从创建到退货:
这 5 个阶段,覆盖了业务可配置 + 多规则共存 + 时效性 + 一致性 + 可追溯——所有 5 大难题都得到了系统的工程支撑。
五、优惠券不是独立系统——它是 Condition 的特化
讲完促销,很多人会问:那 Coupon(优惠券)怎么办?是不是另一套子系统?
很多团队第一反应会把优惠券做成独立系统—— 一张优惠券 = 折扣金额 + 有效期 + 商品限制 + 用户限制 + ... 全打包在券对象里。 结果是:促销有一套引擎,优惠券又有一套引擎,退货撤销、报表统计、扩展开发都要写两遍。
Commerce 的做法相反——码只是「触发器」,规则才是「折扣逻辑」:
普通规则:
IF 购物车金额 ≥ 500
THEN 减 80
优惠券规则:
IF 购物车金额 ≥ 500
AND 客户输入了 SHOP100 这个码 ← 这就是一个特殊的 Condition
THEN 减 80
优惠券本身不提供折扣——它只是"在已有规则上加一个'码触发条件'"。 Coupon 在 Commerce 里就是 Condition 的一种特化形态。
这个设计带来三个实际好处——复用引擎(没有"独立的券引擎",退货撤销、报表统计、扩展开发都只写一遍)、灵活组合(同一个码可以触发任意类型的促销,满减、赠品、免运费都挂同一个"券码 Condition")、易于扩展(码的形态可以千变万化,但规则评估层一视同仁)。下面就看这三种码具体怎么做。
营销人员在实际工作里,针对不同的发券场景,需要的码形态完全不一样:
| 业务场景 | 需要的码形态 | 业务诉求 |
|---|---|---|
| 大促广撒网(双十一、618 全平台优惠) | 一码通用——所有人共用同一个码,比如「SHOP100」 | 简单、好记、好传播,朋友圈截图就能用 |
| 精准营销(短信 / 邮件给老客户发个性化券) | 一人一码——每个客户收到一个独有的码,每个码只能用一次 | 防止外泄被滥用、能追溯到每张券的使用情况 |
| 会员专享 / 客服补偿(生日券、补偿券、企业大客户专属) | 绑定到具体客户——码本身和客户绑死,别人拿到也用不了 | 强身份校验、不可转让 |
这三种发券场景业务诉求完全不同——共用码追求传播力,一人一码追求安全可追溯,客户绑定追求身份强校验。Commerce 的处理很干净——这三种券在底层共用同一个「券码 Condition」:
IF 客户输入的码 ∈ 某个券池
THEN 触发某条促销规则
差异只在"券池"是怎么组织的:
| 券类型 | 券池的形态 | 校验逻辑 |
|---|---|---|
| 共用码(Single-Code) | 一个码,所有人都能用 | 输入的码 == 这个固定码 |
| 一人一码(Multi-Code) | 一批预生成的码,每个码用一次就作废 | 码是否在池子里 + 是否已被用过 |
| 客户专属(Customer Coupon) | 码 ↔ 客户身份的映射表 | 当前登录用户是否持有这个码 |
→ 业务的"千变万化"全在数据层做出来;规则评估层完全一样。
这也给系统演进留下了空间:如果后续出现新的券形态,核心思路仍然可以尽量沿用"券码/券池作为触发条件,折扣逻辑留在促销规则里"这条边界,把变化更多放在券数据和校验层,而不是重新发明一套促销计算逻辑。
六、Campaign:把"规则"组织成"战役"
到这一步,单条规则、单张券我们都能处理了。
但真实的业务场景通常是:双 11 一次性上线 50 条促销规则 + 10 万张券。
如果让业务一条一条管,会发生什么?
痛点:
- 50 条规则散落,没人能看全局
- 时间设置容易漏一两条
- 没法统一报告"双 11 整体效果"
Commerce 给出的答案是 Campaign(营销活动):
打个比方:Campaign 不是新的促销,它是装促销的「伞」——一个营销活动是这把伞,下面的所有规则、券、页面都跟着伞统一启停。
Campaign 的两种调度模式
Campaign 还能联动营销页面——这是 Commerce 一个很妙的设计:
Campaign 激活
→ 关联的 Banner、活动页自动出现在前台
Campaign 结束
→ 这些页面自动消失(不需要人工撤)
营销人员配一次时间,所有页面自动跟着 Campaign 走——避免了"忘了撤 Banner"的常见事故。
这种"同一套底层、分层暴露给不同角色"的设计也是 Commerce 在 Drools 之上做出来的一个重要工程价值——规则层面向开发和实施配规则,Campaign 层面向运营管整体战役,页面联动层面向营销设计管投放——每个角色都在自己熟悉的层上独立工作,互不干扰。
七、Drools 也有它的局限——10 年下来的反思
整套架构看起来很优雅。但 10 年下来,业界对 Drools 类规则引擎的局限性也有了更清晰的认识:
"当年用 Drools 替代原生促销实现的选择,今天再看真的对吗?"
下面是 Drools 在大规模生产环境暴露出的 4 个躲不开的问题。
7.1 性能与多租户:Rete 的根本代价
大规模促销下,问题通常不是"某一条规则慢",而是几类成本叠在一起:
发布成本:规则越多,编译、发布、热切换越重
内存成本:Rete 网络需要常驻内存,规则网络越复杂,内存压力越大
隔离成本:多商户/多租户场景下,不同商家的规则如何隔离、限流、扩容会变复杂
运维成本:规则未命中、性能突降、偶发超时,都比普通业务代码更难排查
这个问题在单租户独立部署场景下通常还能通过资源扩容、规则拆分、发布治理来缓解;但在多租户 SaaS 模式(多个商家共享一套服务)下,发布、内存、隔离和运维成本会一起放大。
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
回想 5.3 节讲过的——Rete 算法是"用空间换时间",网络常驻内存换取运行时速度。这个权衡在规则量可控时很划算;但当规则规模、租户数量、发布频率一起上来时,系统要付出的资源和治理成本会明显上升。
→ 所以这里的重点不是简单地说"Drools 不行",而是要看清楚:通用规则引擎把灵活性做大了,同时也把发布、内存、隔离和运维问题带进了架构账本。
7.2 灵活性悖论:业务人员"用不会"
最反直觉的一条:
Drools 当初的卖点是"业务人员零代码,自由组合规则"——但真实项目里:
- 大多数项目里,业务人员只会用技术团队预先验证过的几种"标准模式"
- 拖拽多种 Condition × 多种 Action 的"自由组合"——实际上从来没人真的随便组合
- 因为一个未经测试的组合可能会和订单计算、库存、积分系统出诡异 bug
辛苦做出来的「灵活性」,99% 时间在落灰。
这其实是软件工程里有专门名字的反模式——YAGNI(You Aren't Gonna Need It,你不会需要它)。
Kent Beck 在 Extreme Programming Explained 里反复强调:不要为「也许有一天会需要」的功能买单。
Drools 给出"任意组合"的能力,但真实业务永远只会用一小部分组合——剩下的"灵活性"成了技术债的礼物。
7.3 复杂度:调试难度超出预期
理论上"零代码"的系统,实际上学习曲线超过自研:
- 开发新的 Condition/Action 需要懂 DRL(Drools Rule Language)
- 调试 Drools 规则像调试一个黑箱——内部 Rete 网络的中间状态不容易看到
- 遇到疑难问题(死循环、性能突降、规则未命中等),定位根因往往比写一个新规则还耗时
业界用 Drools 的项目里,偶尔会遇到大数据量下的死循环 / OOM 等疑难问题——排查 Rete 网络的内部状态非常困难,常见的工程做法是在外面包一层超时检测兜底,先保住可用性,再慢慢排查。
→ 当你的"业务可配置工具"复杂到这种程度,它已经背离了"让业务自由"的初衷。
7.4 扩展性的虚假承诺
业务承诺:"灵活配置就能搞定一切"——
现实:"积木不够用时必须写 Java(开发新的 Condition/Action)"。
→ 这跟原生方案"加新类型要开发"有本质区别吗?没有。
但你为这"虚假的灵活性"付出了性能、复杂度和多租户治理成本的代价。
7.5 业界正在探索的几种新方向
面对这些挑战,业界对"促销系统应该怎么演进"出现了几个不同的探索方向:
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
→ 没有万能解答案——每条路线都是一种"代价交换"。
但一个共识正在浮现:
对于多数业务场景,「有限模板 + 受控扩展」 的设计,可能比 「通用引擎 + 任意组合」 更接近工程最优解。
这不是说 Drools 错了——它在它适用的场景(单租户、大企业、规则量可控)依然是优秀方案。
但如果你今天从零开始设计一套面向 SaaS、多租户的促销系统—— 也许应该重新审视那条「看起来更先进」的路线。
说到底,这是一笔需要算清楚的账:灵活性本身没有价值,被用上的灵活性才有。系统提供 100 分的灵活性,业务实际只用 10 分——剩下那 90 分要用性能、复杂度、甚至多租户的可能性来买单。"看起来很先进"的方案,往往就是这笔账没算清的产物。
八、AI 时代:促销系统会被颠覆吗?
最后我们看一眼未来。
AI 来了,促销系统会被颠覆吗?
我的判断是:不太会被直接颠覆,更可能先被增强。
这里不是在说某个具体产品路线,而是我作为作者的一点架构思考:AI 更适合先站在规则系统之上,做推荐、选择和辅助决策,而不是直接替代促销引擎本身:
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
关键洞察:
AI 不直接重写促销计算
AI 不绕过商家配置的规则边界
AI 更像是在"现有的 Condition + Action + 优惠券框架"内做推荐和选择
→ 这反过来说明,促销系统最关键的抽象仍然有价值:
Condition + Action + 优惠券这套抽象足够通用——即使未来引入 AI,AI 更可能是在它之上做一层「智能决策辅助」,而不是绕开这套边界重新计算折扣。
这才是好的抽象的标志:经得起时代变化的考验——系统先把"什么可变(业务规则、优惠券、Campaign 配置)、什么不变(Condition + Action + Group 的表达机制、规则评估机制)"这条分界线画清楚,未来无论接入 AI、推荐算法还是自动化运营,都更容易被放在一个可控的位置上。
而 AI 落地到促销系统时,核心难题可能并不只是"AI 算得多准",而是 「如何让 AI 在商家可控的边界内决策」。
这就回到了我们前面反复提到的设计思路:有限的、可控的、可预测的灵活性,比无限的灵活性更有价值。
→ AI 时代的诉求,反过来为"克制式架构"提供了新的论据。
结语:促销系统的本质
写完整篇文章,我想用一句话总结:
促销系统的设计,本质上是"用规则语言表达业务意图"的工程。 它的复杂度,不在于"如何打折"——而在于"如何让业务的'千变万化'在工程上稳定、可控、可演进"。
Commerce 用 20 年走过了一条清晰的演进曲线:
原生实现(朴素)→ Drools(追求灵活)→ 反思与探索新方向(成熟克制)
故事还没有结束——业界对下一代方案的探索仍在进行中。但反思本身就已经能给所有正在做架构决策的人一面镜子:
- 你正在追求的"灵活性",未来真的会被业务用到吗?
- 你为这份"灵活性"付出的代价,业务知道吗?
- 有没有更克制的方案,能同时满足业务和工程?
如果你正在做促销系统,希望这篇文章能让你在做架构决策时多一份冷静。