目标:把所有当前与未来可预见的促销形式抽象为统一、可组合、可编排、可灰度、可审计、可回放的"规则 + 策略 + 优化器"体系,并以"可扩展"为首要架构品质(extensibility first)。
TL;DR(关键决策一览)
-
分层模型 :把价格计算拆成 基准价层(Base Price) → 约束/过滤层(Eligibility) → 行为层(Effect) → 组合/优化层(Optimizer)。
-
规则引擎不是全部 :使用规则引擎(或DSL)表达"何时生效" ;用可插拔的 Calculator/Optimizer 表达"如何计算最优组合"。
-
Promotion DAG:将所有优惠规则节点化,形成有向无环图(DAG)执行编排,避免顺序硬编码,支持拓扑排序与分支并行。
-
冲突/叠加统一用策略对象建模 :用
StackingPolicy / ConflictPolicy
明确:互斥、择优、可叠加、叠加上限、分组内择一等。 -
版本化与仿真 :所有规则集(Ruleset)必须带版本;上线前 离线仿真(Simulation / Sandbox) + 灰度发布(Canary)。
-
强观测性 :每笔订单输出"可审计价格证明(Price Proof)",包含命中的规则、丢弃的规则、优化器决策原因、时间与版本信息。
-
可插拔扩展点 :
ConditionResolver / EffectCalculator / Optimizer / SplitStrategy / ConflictResolver / RoundingPolicy
全部 SPI 化,支持运行时热插拔。
1. 典型优惠场景拆分与抽象
-
价格改写类:渠道价、会员价、活动价(直接修改基准价/列表价)。
-
件数/金额门槛类:满N件折扣、满金额减、第二件半价、第N件X折。
-
券类:店铺券、平台券、类目券、品牌券、商品券(并带有使用门槛、限定范围、有效期、可叠加策略)。
-
排除/约束类:黑白名单、打折排除品类/品牌/渠道、与其他特定优惠互斥。
-
分摊/分组类:组合商品、套装价、跨品类满减、跨店满减、跨渠道不可组合。
结论 :你需要一个统一的抽象:Promotion = Condition × Effect × Policy ,并用 组合(Composition)而非继承 来适配不断增长的促销方式。
2. 核心领域模型(可扩展)
// 规则声明(源头的配置/DSL 解析结果)
class PromotionRule {
String ruleId;
String version; // 规则版本
RuleType type; // PRICE_ADJUST, DISCOUNT, COUPON, BUNDLE ...
ConditionTree condition; // 何时生效(Eligibility)
Effect effect; // 做什么(如:改价、减免、返券、赠品)
Scope scope; // 适用商品、店铺、渠道、会员等级等
StackingPolicy stackingPolicy; // 叠加/互斥/择优
Priority priority; // 编排优先级 or DAG 拓扑权重
LocalDateTime startAt;
LocalDateTime endAt;
}
// 扣减/改价行为抽象
interface Effect {
Money apply(LineItem item, PromotionContext ctx);
}
// 条件树(AND/OR/NOT + 叶子条件)
class ConditionTree {
LogicalOp op;
List<ConditionTree> children;
Condition leaf; // 叶子时有效
}
interface Condition {
boolean match(Cart cart, LineItem item, PromotionContext ctx);
}
// 叠加/互斥策略
class StackingPolicy {
boolean stackable; // 是否可叠加
String group; // 同组互斥 or 同组取最优
int stackLimit; // 最多叠加几个
ConflictPolicy conflictPolicy; // 遇到冲突如何裁决
}
// 优化器:从所有"可能命中"的规则组合里求最优(如最大优惠)
interface Optimizer {
CalculationResult optimize(Cart cart, List<ApplicablePromotion> candidates, PromotionContext ctx);
}
class PromotionContext {
String tenantId;
String storeId;
String channel;
String userId;
MemberLevel memberLevel;
Map<String, Object> attributes;
RuleSetVersion ruleSetVersion; // 关键!用于审计 & 回放
}
扩展要点:
-
Effect
、Condition
、Optimizer
都用 接口 + SPI(Java ServiceLoader / Spring BeanFactory / Plugin Framework)暴露扩展点。 -
规则集 RuleSet :按 租户/门店/渠道/时间 维度打包、预编译、缓存,支持本地 POS 离线运行。
-
PromotionContext 支持运行期自定义属性字典,避免 Context schema 频繁变更。
3. 执行模型:Promotion DAG + Pipeline
不要线性"if-else"堆叠 。构造一个 规则节点图(RuleNode Graph, DAG):
-
规则解析阶段 :将所有
PromotionRule
转换为 RuleNode(含条件、效果、优先级、冲突分组)。 -
编排阶段:根据
-
priority
-
group/stackingPolicy
(决定是否同层 or 分支) -
依赖(如"必须在活动价之后计算券")
生成 DAG。
-
-
执行阶段:
-
自顶向下 拓扑排序 执行;
-
对每个节点先判定
ConditionTree
(只算一次); -
通过
EffectCalculator
计算可能的优惠; -
把候选优惠投递给
Optimizer
做全局最优; -
输出 Price Proof。
graph TD
A[Base Price Layer] --> B[Price Adjust Layer: 渠道价/活动价/会员价]
B --> C[Conditional Discount Layer: 满减/件数折/第N件]
C --> D[Coupon Layer: 平台券/店铺券]
D --> E[Optimizer: 冲突裁决/最大优惠组合]
E --> F[Final Settlement] -
Pipeline(伪代码)
PriceContext ctx = preLoadContext(request);
RuleSet ruleSet = ruleRepository.load(ctx.tenantId, ctx.storeId, ctx.channel, now);
List<ApplicablePromotion> candidates = new ArrayList<>();
for (RuleNode node : ruleSet.getDag().topologicalOrder()) {
if (node.conditionTree().match(cart, ctx)) {
ApplicablePromotion ap = node.effect().preview(cart, ctx); // 不立即生效
if (ap != null) candidates.add(ap);
}
}
CalculationResult result = optimizer.optimize(cart, candidates, ctx);
return result.withPriceProof();
4. 冲突与叠加的"策略化"表达
不要在业务逻辑里写死"会员价不能和满减叠加" 。把所有叠加、互斥、择优规则上收为策略对象,并配置化:
stackingPolicies:
- group: base-price
members: [member_price, channel_price, activity_price]
conflictPolicy: PICK_HIGHEST_PRIORITY
stackable: false
- group: coupon
members: [platform_coupon, shop_coupon]
conflictPolicy: MAX_SAVING
stackable: true
stackLimit: 2
ConflictPolicy 枚举(举例):
-
PICK_HIGHEST_PRIORITY
:优先级最高者胜 -
MAX_SAVING
:选择让用户节省最多的钱 -
MIN_PRICE
:选择单品最低最终价 -
CUSTOM
:外部扩展(给你留后门)
实现方式:
-
Optimizer
实现具体策略(支持动态组合) -
RuleSet
将策略注入Optimizer
,形成 RuleSet-Bound Optimizer,不同门店/渠道可走不同策略
5. DSL / 规则管理:声明式、可编译、可测试
5.1 DSL 目标
-
业务可读、支持复杂条件组合、可版本化、可静态校验
-
可编译为 bytecode / expression tree / pre-compiled MVEL/SpEL 提高执行性能
5.2 一个最小可用 DSL 片段(EBNF)
RULE := 'rule' RULE_ID '{' META CONDITION ACTION POLICY '}'
META := 'name:' STRING ',' 'priority:' INT ',' 'time:' TIMERANGE
CONDITION := 'when' BOOL_EXPR
ACTION := 'then' EFFECT_EXPR
POLICY := 'policy' '{' 'group:' STRING ',' 'stackable:' BOOL ',' 'conflict:' POLICY_KIND '}'
5.3 示例 DSL
rule R_MEMBER_PRICE {
name: "会员价改写",
priority: 10,
time: [2025-01-01, 2026-01-01]
when user.memberLevel >= GOLD and item.tags contains "VIP"
then price := item.memberPrice
policy {
group: "base-price"
stackable: false
conflict: PICK_HIGHEST_PRIORITY
}
}
rule R_SECOND_HALF {
name: "第二件半价",
priority: 50,
time: [2025-01-01, 2025-12-31]
when item.count >= 2 and item.category == "CLOTHES"
then discount := 0.5 on nth(2)
policy {
group: "conditional-discount"
stackable: true
conflict: MAX_SAVING
}
}
扩展点 :
nth(2)
、bundle(items)
,allocateBy(Proportion|Average|MaxPrice)
等函数全部通过 函数注册表 可扩展。
6. 算法层:从"顺序计算"升级为"组合优化"
对于"第二件半价 + 满300减50 + 会员价 + 平台券/店铺券选择最优"这类问题,顺序计算往往不是最优。建议:
-
候选规则集(Candidates):先把能命中的全部列出来。
-
组合搜索:
-
小集合:回溯/Branch & Bound
-
中集合:动态规划(DP)(例如按叠加组分层 DP)
-
大集合:启发式/贪心 + 局部回溯 ,或线性规划/整数规划(ILP)建模求解最大优惠(需要可落地的 ILP solver 时慎重)
-
-
可扩展 Optimizer 接口:支持替换/堆叠不同策略(如"优先券策略" vs "最大优惠策略")
伪代码(按叠加组分层 DP):
// promotionsByGroup: Map<String, List<ApplicablePromotion>>
DP[0] = {state: empty, saving: 0}
for each group in topoOrder(groups):
DP' = {}
for (s in DP):
// 1) 该组不使用任何优惠
DP'.add(s)
// 2) 该组内根据 stackingPolicy 选择若干
for (subset in enumerateFeasibleSubsets(promotionsByGroup[group])) {
newState = applySubset(s, subset)
if (isFeasible(newState)) DP'.maximize(newState)
}
DP = DP'
return argmax_saving(DP)
7. 典型促销模式的算法模板
7.1 会员价/渠道价/活动价(价格改写类)
-
策略:放在 Base Price 层,互斥,取优先级最高或最低价。
-
扩展性 :新价格来源只需实现
PriceAdjustEffect
。
7.2 第二件半价、第N件X折
-
策略 :提供
NthItemSelector
接口(可扩展 nth(2), nth(3..), everyNth(n))。 -
算法:对同一 SKU LineItem,按数量排序并标记命中的 index,计算折扣分摊。
7.3 N件打折 / 捆绑包(Bundle)
-
策略 :
GroupMatcher
(定义成组策略:同SKU、同类目、任意商品...)。 -
算法:背包/组合选择(选出满足条件的组合数),对每组应用折扣,剩余商品继续进入下一层计算。
7.4 满减/满折(金额门槛)
-
策略:按"可叠加组"分层;可设叠加上限、不能与券叠加。
-
算法:金额求和后判断门槛,折扣分摊基于金额比例(可插拔策略)。
7.5 券(平台/店铺/类目/商品)
-
策略 :
CouponEligibility
(门槛、白名单、黑名单)、CouponConflictPolicy
(优先券 or 最大优惠)。 -
算法:枚举可用券组合(上限控制),交由 Optimizer 做组合搜索。
8. 数据模型(关系型 + 文档型混合)
关系型(结构化、查询友好)
-
promotion_rule
(规则主表)- id, type, priority, start_at, end_at, scope_type, scope_id, stacking_group, stack_limit, conflict_policy, rule_version, rule_status
-
promotion_condition_group
/promotion_condition_item
-
promotion_effect
(JSON 存 effect 参数,如折扣率、改价来源、nth 规则...) -
promotion_conflict_matrix
(可选,维护历史互斥配置) -
promotion_ruleset_version
(规则集版本表,用于门店/渠道维度)
文档型(存 DSL、编译产物、Price Proof)
-
promotion_dsl_snapshot
(原始 DSL) -
promotion_compiled_snapshot
(编译后表达式树/字节码) -
price_proof
(每笔订单的规则命中与决策路径)
9. API 契约(简版)
POST /pricing/settle
Content-Type: application/json
{
"tenantId": "t1",
"storeId": "s1",
"channel": "POS",
"user": {"id": "u1", "memberLevel": "GOLD"},
"cart": {
"items": [
{"sku": "A", "qty": 3, "price": 100},
{"sku": "B", "qty": 1, "price": 200}
]
},
"couponCodes": ["C123", "S456"],
"ruleSetVersion": "2025-07-26_01"
}
响应(含 Price Proof)
{
"finalAmount": 250.00,
"discountTotal": 150.00,
"lineItems": [ ... ],
"appliedPromotions": [
{
"ruleId": "R_SECOND_HALF",
"version": "1.2.3",
"saving": 50,
"scope": {"sku": "A"}
}
],
"rejectedPromotions": [
{
"ruleId": "R_MEMBER_PRICE",
"reason": "conflict-with:R_SECOND_HALF",
"policy": "PICK_HIGHEST_PRIORITY"
}
],
"ruleSetVersion": "2025-07-26_01",
"traceId": "...",
"latencyMs": 23
}
10. 性能、缓存与部署
-
规则预编译:DSL → AST/Bytecode/MVEL Expression,避免运行时解析。
-
多级缓存:
-
L1:线程本地/本机内存(按 RuleSetVersion 缓存)
-
L2:Redis(规则集、配置、冲突矩阵)
-
L3:对象存储(DSL/编译快照)
-
-
POS 离线容错:
-
本地持久化
RuleSetVersion
快照(含有效期) -
过期回退策略(FallbackRuleSet)
-
-
并发优化:
-
按门店/渠道分桶,减少无关规则装载
-
热规则集"固定地址 + CAS 替换"方式实现无锁热更新
-
11. 测试策略 & 审计
-
Golden Master:对关键促销活动生成黄金样本价单(输入 → 期望输出),任何规则修改都跑回归。
-
Property-Based Testing:对数量/金额随机生成,验证单调性(更多件数或更高金额不应导致更高价等)
-
Snapshot / Replay:生产价单可回放(Price Proof + RuleSetVersion)定位问题。
-
Rule Coverage:统计每个规则的命中率、被冲突剔除率,辅助运营清理僵尸规则。
12. 运营侧能力(营销中台)
-
规则模板化:常见促销以模板形式装配,运营填参数即可。
-
仿真平台(PromoLab):上传历史订单批量回放,对比新旧规则集收益/损失。
-
灰度 & AB:按门店/渠道/用户分群投放不同 RuleSetVersion。
-
可观察性大屏:展示关键指标(规则命中率、平均折扣、GMV 影响、延迟 p95/p99)。
13. 未来演进(前瞻性)
-
ILP/CP 优化器 :当促销组合极度复杂(跨品类、跨店、券包叠加)时,引入 整数线性规划 或 约束规划(CP-SAT) 做精确求解(需在延迟可接受的离线/半实时场景使用)。
-
强化学习/多臂老虎机:对"如何定价/如何投放券"做策略优化(非结算路径,属于智能运营域)。
-
函数式结算内核:把优惠操作抽象为幺半群/单子(Monoid/Monad)叠加,天然满足组合律,便于形式化验证与并行化。
-
可视化 Rule Graph 编辑器:非技术人员编辑 DAG,并一键导出 DSL/AST。
14. 落地路线(增量改造建议)
-
第 0 步 :梳理现有优惠,沉淀为统一
PromotionRule
结构,并输出Price Proof
。 -
第 1 步:把"何时生效(Condition)"迁到 DSL/规则引擎;效果(Effect)与优化器仍由代码控制。
-
第 2 步 :接入
Optimizer
,完成冲突/叠加的策略化配置;引入RuleSetVersion
机制。 -
第 3 步:引入仿真平台/灰度发布;打通营销中台。
-
第 4 步:将算法层抽象出 SPI,并为复杂组合场景接入 DP/ILP 优化器。
总结
"可扩展"的本质是 :把变化频繁的促销逻辑(条件、组合、优先)结构化、参数化、策略化 ,并以 DAG 编排 + 组合优化器 代替"写死执行顺序"的实现。配合版本化、仿真、灰度、审计,才能在促销方式日益增加的情况下依然保持可控、可演进、可验证。