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

目标:把所有当前与未来可预见的促销形式抽象为统一、可组合、可编排、可灰度、可审计、可回放的"规则 + 策略 + 优化器"体系,并以"可扩展"为首要架构品质(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 编排 + 组合优化器 代替"写死执行顺序"的实现。配合版本化、仿真、灰度、审计,才能在促销方式日益增加的情况下依然保持可控、可演进、可验证。

相关推荐
NE_STOP1 天前
Vide Coding--AI编程工具的选择
java
大树881 天前
金刚石散热越强,管路越先见顶
大数据·运维·服务器·人工智能·ai
摇滚侠1 天前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
码云数智-园园1 天前
C++20 Modules 模块详解
java·开发语言·spring
程序员黑豆1 天前
JDK 下载安装与配置详细教程
java·前端·ai编程
大志哥1231 天前
ES和Logstash日志链路系统上线后遭遇切片爆炸(解决)
大数据·elasticsearch
霸道流氓气质1 天前
领域驱动设计(DDD)在 Spring Boot 微服务中的实践指南
运维·spring boot·微服务
小宇宙Zz1 天前
Maven依赖冲突
java·服务器·maven
swordbob1 天前
NIO的channel中什么是 fd(File Descriptor,文件描述符)
java·开发语言·nio
咖啡八杯1 天前
GoF设计模式——享元模式
java·spring·设计模式·享元模式