大促折扣系统设计:规则引擎与历史低价追踪
作者:da吃一鲸8862026.11.06 19:53浏览量:0简介:面向数字商店的大型限时促销场景,本文拆解折扣规则引擎、价格历史追踪、活动生命周期与高并发保障四大核心模块的设计要点,配套数据模型与计算代码示例,帮助开发者搭建一套价格准确、切换准时、可审计的促销定价系统。
一、从一个常见场景说起
数字内容商店(游戏、应用、数字出版物等)经常举办大型限时促销:活动覆盖成百上千个商品,折扣力度从九折到0.5折不等,页面上还会出现“首次打折”“历史新低”“持平史低”等标签,并带有明确的截止时间。对用户而言,这是一份“捡漏清单”;对研发团队而言,这背后是一整套促销定价系统的综合考验。
支撑这类活动,至少要解决四个问题:一是如何用一套通用模型描述千差万别的折扣形式;二是如何准确记录并判定“历史新低”;三是如何保证活动准时开始、准时结束、价格可回滚;四是如何在大流量下保证价格计算不出错。下面逐个展开。
二、领域模型:商品、SKU与价格体系
- 商品与SKU分离
促销清单里常见“同一作品的标准版、豪华版、合集包同时参加活动,且不同版本折扣不同”。这说明建模时必须把“作品”与“可售卖单元”分开:
- 作品:逻辑层面的内容主体,用于展示、检索与聚合;
- SKU:最小可售卖单元,独立定价。一个作品可派生出标准版、豪华版、年度版等多个SKU,合集包本身也是独立SKU,内部通过关联表维护与子SKU的关系。
- 价格的三层结构
一个SKU在任意时刻的价格可拆解为:原价(长期基准价,变动极少)、划线价(页面展示的“被划掉”的价格)、成交价(用户实际支付的价格,由规则引擎计算得出)。对应的价格历史表可以这样设计:
CREATE TABLE price_history (
id BIGINT PRIMARY KEY,
sku_id BIGINT NOT NULL,
currency CHAR(3) NOT NULL, — 支持多币种
list_price DECIMAL(10,2) NOT NULL, — 原价/划线价
sale_price DECIMAL(10,2) NOT NULL, — 实际成交价
activity_id BIGINT, — 关联活动,日常价可空
start_time DATETIME NOT NULL,
end_time DATETIME NOT NULL,
KEY idx_sku_time (sku_id, start_time)
);
注意两点:价格必须连同币种一起存储,多区域商店的同一SKU可能有不同计价;每条价格记录都要绑定生效区间与活动来源,这是后续审计与回滚的基础。
三、折扣规则引擎:把“花式折扣”抽象成规则
- 规则类型建模
促销中出现的各种折扣形式,本质都可归为几类原子规则:比例折扣(按百分比下调)、定额减免(减固定金额)、套装价(合集包整体定价)、升级补差(老版本用户付费升级)、条件规则(满减、会员专享等)。
def apply_rule(price, rule):
if rule.type == “PERCENT_OFF”:
price = price * (100 - rule.value) / 100
elif rule.type == “AMOUNT_OFF”:
price = price - rule.value
return round(price, 2) # 精度处理统一收敛在引擎内
- 优先级、互斥与叠加
当一个SKU同时命中多条规则时,引擎必须明确三件事:优先级(先应用哪条,通常活动专属价优先于通用满减);是否可叠加(互斥规则只取最优一条,可叠加规则按顺序依次作用);兜底约束(最终成交价不得高于原价,也不得低于配置的最低价保护线,防止配置错误导致“一折清仓”事故)。
def calc_final_price(base_price, rules):
price = base_price
for rule in sorted(rules, key=lambda r: r.priority):
if rule.exclusive and price != base_price:
continue # 互斥规则:已有更优价格则跳过
price = min(price, apply_rule(price, rule))
floor = get_price_floor() # 最低价保护
return max(min(price, base_price), floor)
- 金额精度必须集中处理
金额计算必须使用十进制类型而非浮点数,舍入规则要统一收敛在引擎内,避免结算、退款、对账三方各自实现导致账目不平。
四、“历史新低”如何判定:价格历史的存储与计算
促销页面上“新史低”“平史低”的标签,技术上是一个聚合查询问题:判断当前活动价是否低于(或等于)该SKU历史最低成交价。
- 查询优化
若每次展示都执行MIN聚合查询,大促流量下历史表会成为瓶颈。常见做法:以SKU为维度建立聚合表,维护历史最低价、最低价时间等字段;活动价写入历史表后通过消息队列异步刷新聚合结果,不阻塞主链路;采用本地缓存加分布式缓存的双层结构,并在活动开始前主动预热。
def judge_price_level(sku_id, current_price):
hist_min = agg_cache.get(sku_id) # 优先读聚合缓存
if hist_min is None:
hist_min = load_min_from_db(sku_id) # 未命中回源
if current_price < hist_min:
return “NEW_LOW” # 历史新低
if current_price == hist_min:
return “EQUAL_LOW” # 持平史低
return “NORMAL”
- 判定口径要先定义清楚
“历史最低”统计哪个范围?是否包含不同版本?合集包是否折算单价?这些问题没有唯一答案,但必须在产品层面统一口径:判定一律以SKU为粒度,不跨版本混合计算;合集包按包价参与判定;判定结果落库留痕,便于事后核对与争议处理。
五、活动生命周期:准时生效与可靠回滚
定时任务可靠性。活动起止依赖定时调度:时间基准统一使用服务器侧UTC存储,展示时按区域时区转换;多实例部署时用分布式锁防止重复执行,切换动作必须幂等;活动开始前几分钟完成页面缓存、聚合数据预热,避免开抢瞬间缓存击穿。
价格快照与回滚。活动结束意味着价格恢复,绝不能依赖“再配一次原价”。正确做法是活动开始前对受影响SKU做价格快照,结束时按快照恢复;每次价格变更都写入审计日志,记录操作人、来源活动、变更前后价格,即使出现配置事故也能分钟级回滚。
边界时刻一致性。截止时刻前后可能出现“页面显示已结束、结算仍按活动价成交”的窗口。应以结算侧价格快照为准而非页面展示;结算时做二次校验,价格引擎重新计算并比对用户提交的价格,不一致则拒绝订单并提示刷新。
六、大促流量下的稳定性保障
促销开场往往是流量峰值,典型保障手段包括:商品详情、价格标签等读接口全量走缓存,静态资源经CDN分发;价格标签属于增强信息,缓存未命中时可先返回基础活动价,异步补齐标签;价格变更、降价订阅等写操作通过消息队列异步化削峰;对“成交价高于原价”“折扣率超出配置范围”“活动价与结算价不一致”等异常设置实时告警,这类错误的资损与口碑伤害远大于短暂不可用。此外,“降价提醒”是高频需求:价格写入后发出变更事件,订阅服务比对用户阈值后投递推送,注意做好去重与频控,避免活动期间对用户形成轰炸。
七、小结
一场看似简单的大促,背后是领域建模、规则引擎、历史数据、定时调度与高并发治理的综合工程。回到开头的清单:覆盖大量SKU的折扣矩阵,对应可扩展的规则引擎;“新史低/平史低”标签,对应价格历史聚合与统一判定口径;明确的截止时间,对应可靠的活动生命周期与回滚机制。把这四块能力做扎实,无论促销形式如何变化,系统都能做到价格准确、切换准时、事故可回溯。这套方法论同样适用于电商、内容分发等任意涉及动态定价的业务场景。

登录后可评论,请前往 登录 或 注册