从一张优惠券,看 SAP Commerce 20 年的促销引擎演进

0 阅读23分钟

从一张优惠券,看 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 互斥取一)
3iPhone 限时秒杀 9 折引流主推单品折扣 + 限时仅 0:00-0:30 内有效
488VIP 专享额外 9.5 折提升忠诚客群专享客户是 88VIP
5iPhone + 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

💡 CartRAOUserRAO 是 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 的解决方案
1iPhone 限时秒杀 9 折要先于 88VIP 9.5 折评估顺序不同,最终金额差几十块给每条规则配 Priority(优先级),数字大的先评估
2满 300 减 30 和满 500 减 80 是同档位的两个台阶,购物车过了 500 就只用第二档不能两条都减,避免同性质优惠叠加把这两条规则放进同一个 Rule Group(规则组)并标记互斥,组内只取最高优先级
3iPhone + 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 这个条件:

规则 Acart.total > 100 AND customer.isVip          → 减 20
规则 Bcart.total > 100 AND cart.hasCategory("服装") → 减 10
规则 Ccart.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.isVipcart.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
   THEN80

优惠券规则:
   IF 购物车金额 ≥ 500
   AND 客户输入了 SHOP100 这个码    ← 这就是一个特殊的 Condition
   THEN80

优惠券本身不提供折扣——它只是"在已有规则上加一个'码触发条件'"。 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(追求灵活)→ 反思与探索新方向(成熟克制)

故事还没有结束——业界对下一代方案的探索仍在进行中。但反思本身就已经能给所有正在做架构决策的人一面镜子:

  • 你正在追求的"灵活性",未来真的会被业务用到吗?
  • 你为这份"灵活性"付出的代价,业务知道吗?
  • 有没有更克制的方案,能同时满足业务和工程?

如果你正在做促销系统,希望这篇文章能让你在做架构决策时多一份冷静。