收银系统优惠功能架构:可扩展设计指南(含可扩展性思路与落地细节)

目标:把所有当前与未来可预见的促销形式抽象为统一、可组合、可编排、可灰度、可审计、可回放的"规则 + 策略 + 优化器"体系,并以"可扩展"为首要架构品质(extensibility first)。


TL;DR(关键决策一览)

  1. 分层模型 :把价格计算拆成 基准价层(Base Price) → 约束/过滤层(Eligibility) → 行为层(Effect) → 组合/优化层(Optimizer)

  2. 规则引擎不是全部 :使用规则引擎(或DSL)表达"何时生效" ;用可插拔的 Calculator/Optimizer 表达"如何计算最优组合"。

  3. Promotion DAG:将所有优惠规则节点化,形成有向无环图(DAG)执行编排,避免顺序硬编码,支持拓扑排序与分支并行。

  4. 冲突/叠加统一用策略对象建模 :用 StackingPolicy / ConflictPolicy 明确:互斥、择优、可叠加、叠加上限、分组内择一等。

  5. 版本化与仿真 :所有规则集(Ruleset)必须带版本;上线前 离线仿真(Simulation / Sandbox) + 灰度发布(Canary)

  6. 强观测性 :每笔订单输出"可审计价格证明(Price Proof)",包含命中的规则、丢弃的规则、优化器决策原因、时间与版本信息。

  7. 可插拔扩展点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;  // 关键!用于审计 & 回放
}

扩展要点

  • EffectConditionOptimizer 都用 接口 + SPI(Java ServiceLoader / Spring BeanFactory / Plugin Framework)暴露扩展点。

  • 规则集 RuleSet :按 租户/门店/渠道/时间 维度打包、预编译、缓存,支持本地 POS 离线运行。

  • PromotionContext 支持运行期自定义属性字典,避免 Context schema 频繁变更。


3. 执行模型:Promotion DAG + Pipeline

不要线性"if-else"堆叠 。构造一个 规则节点图(RuleNode Graph, DAG)

  1. 规则解析阶段 :将所有 PromotionRule 转换为 RuleNode(含条件、效果、优先级、冲突分组)。

  2. 编排阶段:根据

    • priority

    • group/stackingPolicy(决定是否同层 or 分支)

    • 依赖(如"必须在活动价之后计算券")

      生成 DAG。

  3. 执行阶段

    • 自顶向下 拓扑排序 执行;

    • 对每个节点先判定 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 + 会员价 + 平台券/店铺券选择最优"这类问题,顺序计算往往不是最优。建议:

  1. 候选规则集(Candidates):先把能命中的全部列出来。

  2. 组合搜索

    • 小集合:回溯/Branch & Bound

    • 中集合:动态规划(DP)(例如按叠加组分层 DP)

    • 大集合:启发式/贪心 + 局部回溯 ,或线性规划/整数规划(ILP)建模求解最大优惠(需要可落地的 ILP solver 时慎重)

  3. 可扩展 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. 未来演进(前瞻性)

  1. ILP/CP 优化器 :当促销组合极度复杂(跨品类、跨店、券包叠加)时,引入 整数线性规划约束规划(CP-SAT) 做精确求解(需在延迟可接受的离线/半实时场景使用)。

  2. 强化学习/多臂老虎机:对"如何定价/如何投放券"做策略优化(非结算路径,属于智能运营域)。

  3. 函数式结算内核:把优惠操作抽象为幺半群/单子(Monoid/Monad)叠加,天然满足组合律,便于形式化验证与并行化。

  4. 可视化 Rule Graph 编辑器:非技术人员编辑 DAG,并一键导出 DSL/AST。


14. 落地路线(增量改造建议)

  1. 第 0 步 :梳理现有优惠,沉淀为统一 PromotionRule 结构,并输出 Price Proof

  2. 第 1 步:把"何时生效(Condition)"迁到 DSL/规则引擎;效果(Effect)与优化器仍由代码控制。

  3. 第 2 步 :接入 Optimizer,完成冲突/叠加的策略化配置;引入 RuleSetVersion 机制。

  4. 第 3 步:引入仿真平台/灰度发布;打通营销中台。

  5. 第 4 步:将算法层抽象出 SPI,并为复杂组合场景接入 DP/ILP 优化器。


总结

"可扩展"的本质是 :把变化频繁的促销逻辑(条件、组合、优先)结构化、参数化、策略化 ,并以 DAG 编排 + 组合优化器 代替"写死执行顺序"的实现。配合版本化、仿真、灰度、审计,才能在促销方式日益增加的情况下依然保持可控、可演进、可验证。

相关推荐
双力臂40425 分钟前
Spring Boot 单元测试进阶:JUnit5 + Mock测试与切片测试实战及覆盖率报告生成
java·spring boot·后端·单元测试
EulerBlind37 分钟前
【运维】SGLang 安装指南
运维·人工智能·语言模型
Edingbrugh.南空44 分钟前
Aerospike与Redis深度对比:从架构到性能的全方位解析
java·开发语言·spring
风吹落叶花飘荡1 小时前
Ubuntu系统 系统盘和数据盘扩容具体操作
linux·运维·ubuntu
QQ_4376643141 小时前
C++11 右值引用 Lambda 表达式
java·开发语言·c++
永卿0011 小时前
设计模式-迭代器模式
java·设计模式·迭代器模式
誰能久伴不乏2 小时前
Linux如何执行系统调用及高效执行系统调用:深入浅出的解析
java·服务器·前端
mykyle2 小时前
Elasticsearch-ik分析器
大数据·elasticsearch·jenkins
慕y2742 小时前
Java学习第七十二部分——Zookeeper
java·学习·java-zookeeper
midsummer_woo2 小时前
基于spring boot的医院挂号就诊系统(源码+论文)
java·spring boot·后端