优惠券系统架构设计文档
版本:v1.0 适用场景:电商/O2O 类平台的优惠券发放、领取、叠加计算与核销
目录
- 背景与目标
- 需求概述
- 总体架构
- 核心领域模型
- 规则引擎设计
- 叠加规则设计详解
- 最优叠加方案计算(核心算法)
- 数据存储设计
- 服务拆分与 API 设计
- 高并发与一致性保障
- 可扩展性设计
- 监控与可观测性
- 总结与后续演进
1. 背景与目标
优惠券系统看似简单(发一张券、抵扣一点钱),但在实际业务中会迅速演变成规则最复杂、最容易出资损事故的模块之一,原因是:
- 券的类型多(满减、折扣、立减、免邮、赠品券……),每种的计算方式不同;
- 券与券之间存在复杂的叠加/互斥关系,且这些关系会随运营活动频繁调整;
- 用户手里可能同时持有多张可用券,系统需要在毫秒级内帮用户/自动挑出"最划算"的组合;
- 一旦规则出错(该互斥的没互斥、该叠加的没叠上),直接影响 GMV 和资损。
本设计的核心目标是:把"规则判定"与"组合寻优"两个能力做成独立、可配置、可插拔的引擎层,业务方(运营)只需要配置规则,不需要改代码就能上线新的叠加策略。
2. 需求概述
2.1 功能性需求
| 能力 | 说明 |
|---|---|
| 券模板管理 | 创建满减/折扣/立减/免邮/赠品等类型的券模板,设置门槛、适用范围、有效期、库存 |
| 发放与领取 | 支持主动领取、系统发放(下单赠送、会员权益)、定向发放 |
| 可用券查询 | 给定订单(商品、金额、店铺),查出用户当前可用的券列表 |
| 叠加规则判定 | 判断任意两张/多张券是否可以同时使用 |
| 最优叠加方案计算 | 在所有可用券中,自动计算出使订单实付最低(或折扣最大)的合法组合 |
| 核销 | 下单时锁定券、支付成功后核销、订单取消/退款后回滚 |
| 规则配置化 | 运营后台可视化配置叠加规则、互斥组,无需发版 |
2.2 非功能性需求
- 低延迟:查询可用券 + 计算最优组合需在结算页同步返回,目标 P99 < 100ms;
- 强一致性:券的锁定/核销/库存扣减不能超卖、不能被重复核销;
- 高并发:大促场景下领券、查询是脉冲式流量;
- 可扩展:新增券类型、新增叠加规则不应侵入核心链路代码;
- 可审计:每一次核销、每一次规则判定要可追溯(为什么这两张券不能一起用)。
3. 总体架构
整体采用分层架构,核心是把"规则引擎"和"叠加计算引擎"作为独立的领域服务下沉,供交易链路调用:
┌─────────────────────────┐
│ 接入层 │
│ API Gateway / BFF │
└────────────┬─────────────┘
│
┌───────────────┬────────────┼────────────────┬──────────────┐
▼ ▼ ▼ ▼ ▼
┌─────────────┐ ┌─────────────┐ ┌───────────────┐ ┌───────────┐ ┌───────────┐
│ 券模板/发放 │ │ 用户券包服务 │ │ 叠加计算服务 │ │ 核销服务 │ │ 运营后台API │
│ TemplateSvc │ │ WalletSvc │ │ StackingSvc │ │ RedeemSvc │ │ AdminSvc │
└──────┬──────┘ └──────┬──────┘ └───────┬───────┘ └─────┬─────┘ └─────┬─────┘
│ │ │ │ │
└───────────────┴───────┬────────┴───────────────┴─────────────┘
▼
┌───────────────────────────┐
│ 规则引擎 RuleEngine │
│ 领取规则 / 门槛规则 / 叠加规则 │
│ / 计价规则 │
└─────────────┬──────────────┘
▼
┌───────────────────────────┐
│ 数据与缓存层 │
│ MySQL(模板/实例/规则配置) │
│ Redis(库存原子扣减/热点缓存) │
│ 规则配置中心(可选:Nacos/Apollo)│
└───────────────────────────┘
关键设计原则:
- StackingSvc(叠加计算服务)不直接依赖交易系统,只依赖"订单快照 + 候选券列表 + 规则",可以独立测试、独立压测、甚至可以离线跑批对账。
- RuleEngine 是纯函数式的判定组件,输入上下文、输出通过/不通过,不产生副作用,便于单元测试和规则回归。
- 核销(资损敏感操作)与计算(读多写少)物理拆分,核销走强一致链路,计算可以适度做缓存和降级。
4. 核心领域模型
// ============ 券模板:运营配置的"发放方案" ============
type CouponTemplate struct {
ID string
Name string
Type CouponType // 满减/折扣/立减/免邮/赠品
DiscountValue decimal.Decimal // 折扣值:满减金额 / 折扣率(0.8) / 立减金额
Threshold decimal.Decimal // 使用门槛,如满 100 元可用
Scope ApplicableScope // 适用范围:全场/指定类目/指定商品/指定店铺
Category string // 券分类:platform(平台券)/merchant(商家券)/logistics(运费券)
ExclusionGroups []string // 所属互斥组 ID 列表
Priority int // 结算优先级,数值越小越先应用
ValidFrom time.Time
ValidTo time.Time
StockTotal int64
}
type CouponType string
const (
TypeFullReduction CouponType = "FULL_REDUCTION" // 满减,如满100减20
TypeDiscount CouponType = "DISCOUNT" // 折扣,如8折
TypeFixedOff CouponType = "FIXED_OFF" // 无门槛立减
TypeFreeShipping CouponType = "FREE_SHIPPING" // 免邮
TypeGift CouponType = "GIFT" // 赠品券
)
// ApplicableScope 适用范围,用于判断某张券对某个订单/商品是否生效
type ApplicableScope struct {
ScopeType string // ALL / CATEGORY / SKU / SHOP
TargetIDs []string
}
// ============ 券实例:用户实际持有的一张券 ============
type CouponInstance struct {
ID string
TemplateID string
Template *CouponTemplate // 运行时关联,便于计算
UserID string
Status CouponStatus // UNUSED / LOCKED / USED / EXPIRED / RETURNED
ReceivedAt time.Time
UsedAt *time.Time
OrderID *string
}
type CouponStatus string
const (
StatusUnused CouponStatus = "UNUSED"
StatusLocked CouponStatus = "LOCKED" // 下单时锁定,占用但未核销
StatusUsed CouponStatus = "USED"
StatusExpired CouponStatus = "EXPIRED"
StatusReturned CouponStatus = "RETURNED" // 订单取消/退款后回滚
)
// ============ 订单(结算上下文) ============
type Order struct {
ID string
UserID string
ShopID string
Items []OrderItem
OriginalAmount decimal.Decimal
ShippingFee decimal.Decimal
}
type OrderItem struct {
SKUID string
CategoryID string
Price decimal.Decimal
Quantity int
}
设计要点:CouponTemplate 承载"规则性"属性(门槛、适用范围、互斥组、优先级),CouponInstance 只承载"状态性"属性。叠加判定始终发生在模板层面,因为运营配置的互斥关系是针对"这一类券",而不是针对某一张具体的券实例——这个区分对后面第 7 节的性能优化很关键。
5. 规则引擎设计
5.1 规则分类
把规则按"作用阶段"分为四类,每一类对应结算链路的一个环节:
| 规则类型 | 作用阶段 | 典型规则 |
|---|---|---|
| 领取规则 Eligibility | 用户领券时 | 用户身份/等级、库存是否充足、是否已达领取上限、时间窗口 |
| 门槛规则 Threshold | 判断券对某订单是否可用 | 订单金额是否满足满减门槛、商品是否在适用范围内 |
| 叠加规则 Stacking | 多券组合判定 | 互斥组校验、每类型最大叠加张数、全局最大叠加张数、依赖关系 |
| 计价规则 Pricing | 结算金额计算 | 折扣计算公式、多券应用顺序、封顶金额 |
5.2 规则引擎接口设计
// RuleContext 规则执行上下文
type RuleContext struct {
Order *Order
User *UserProfile
Coupon *CouponInstance
Template *CouponTemplate
SelectedSet []*CouponInstance // 叠加校验时:当前已选定的券组合
Extra map[string]interface{}
}
// RuleResult 规则执行结果
type RuleResult struct {
Passed bool
Reason string // 不通过时的原因,便于前端提示 & 审计追溯
}
// Rule 所有规则类型的统一接口
type Rule interface {
ID() string
Type() RuleType
Evaluate(ctx *RuleContext) RuleResult
}
type RuleType string
const (
RuleEligibility RuleType = "ELIGIBILITY"
RuleThreshold RuleType = "THRESHOLD"
RuleStacking RuleType = "STACKING"
RulePricing RuleType = "PRICING"
)
// RuleEngine 规则引擎:按类型维护规则链,支持短路执行
type RuleEngine struct {
rulesByType map[RuleType][]Rule
}
func (e *RuleEngine) EvaluateChain(ruleType RuleType, ctx *RuleContext) RuleResult {
for _, r := range e.rulesByType[ruleType] {
if res := r.Evaluate(ctx); !res.Passed {
return res // 短路:任一规则不通过,整体不通过
}
}
return RuleResult{Passed: true}
}
5.3 规则的配置化(可插拔)
为了让运营"配置规则而不是等发版",规则引擎支持两种规则来源:
- 硬编码规则:实现
Rule接口的 Go 结构体,适合逻辑复杂、变动少的规则(如库存扣减校验); - 表达式规则:规则的判定逻辑用表达式(如
order.amount >= 100 && coupon.category == "merchant")存储在数据库/配置中心,运行时用轻量表达式引擎(如expr、cel-go)解析执行,适合运营高频调整的门槛类规则。
type ExpressionRule struct {
id string
ruleType RuleType
expression string // 存储在 DB/配置中心,后台可视化编辑
program *vm.Program // 预编译后的表达式,启动时/配置变更时编译
}
func (r *ExpressionRule) Evaluate(ctx *RuleContext) RuleResult {
result, err := expr.Run(r.program, ctx)
if err != nil || !result.(bool) {
return RuleResult{Passed: false, Reason: "未满足表达式规则:" + r.expression}
}
return RuleResult{Passed: true}
}
两类规则统一实现 Rule 接口,对上层 RuleEngine 透明——这是插件化的关键:新增规则类型不需要改 RuleEngine,只需要实现接口并注册。
6. 叠加规则设计详解
这是整个系统里业务方最关心、也最容易表达不清的部分。实践中"能否叠加"通常可以用三层约束表达:
6.1 互斥组(Exclusion Group)
同一个互斥组内的券模板,最多只能选一张。例如"新人专享券"和"大促满减券"属于同一互斥组,二选一。互斥组是最常用、表达能力最强的机制,大多数叠加规则都可以归约成互斥组划分。
6.2 类型级别/全局限制
除了互斥组,通常还有"数量上限"约束,例如:
- 平台券最多叠加 1 张;
- 商家券最多叠加 3 张;
- 一笔订单总共最多用 4 张券。
6.3 补充依赖规则
少数场景无法用"互斥组"这种对称关系表达,例如"运费券必须搭配满减券才能用""A 券不能和 C 券同时使用,但 A 和 B 可以"这种非对称/局部冲突关系,用补充的依赖规则表达:
// StackingPolicy 叠加策略:一份完整的叠加规则配置
type StackingPolicy struct {
MaxTotalCoupons int // 全局最多叠加张数
MaxPerCategory map[string]int // 每个券分类最多叠加张数,如 {"platform":1,"merchant":3}
ExclusionGroups [][]string // 互斥组:每个子数组内的模板ID最多选1个
DependencyRules []DependencyRule // 补充的依赖/局部冲突规则
}
// DependencyRule 非对称依赖/局部冲突,互斥组表达不了的场景用这个补充
type DependencyRule struct {
SourceTemplateID string
RequireTemplateIDs []string // 必须同时存在才可使用(为空表示无强制依赖)
ConflictTemplateIDs []string // 不能与之同时使用(局部冲突,不构成对称互斥组)
}
// MutualExclusionRule 实现 Rule 接口,校验互斥组约束
type MutualExclusionRule struct {
policy *StackingPolicy
}
func (r *MutualExclusionRule) Evaluate(ctx *RuleContext) RuleResult {
candidateGroups := groupsOf(ctx.Coupon.TemplateID, r.policy.ExclusionGroups)
for _, selected := range ctx.SelectedSet {
selectedGroups := groupsOf(selected.TemplateID, r.policy.ExclusionGroups)
if intersects(candidateGroups, selectedGroups) {
return RuleResult{Passed: false, Reason: "与已选券存在互斥关系"}
}
}
return RuleResult{Passed: true}
}
// StackLimitRule 实现 Rule 接口,校验数量上限约束
type StackLimitRule struct {
policy *StackingPolicy
}
func (r *StackLimitRule) Evaluate(ctx *RuleContext) RuleResult {
if len(ctx.SelectedSet)+1 > r.policy.MaxTotalCoupons {
return RuleResult{Passed: false, Reason: "已达全局最大叠加张数"}
}
category := ctx.Template.Category
limit, ok := r.policy.MaxPerCategory[category]
if ok {
used := countByCategory(ctx.SelectedSet, category)
if used+1 > limit {
return RuleResult{Passed: false, Reason: "已达该类型最大叠加张数:" + category}
}
}
return RuleResult{Passed: true}
}
MutualExclusionRule、StackLimitRule、DependencyRule 校验都注册进 RuleEngine 的 RuleStacking 规则链,依次短路执行——业务上"是否可叠加"这一个问题,被拆成了三条独立、互不感知的小规则,新增一种叠加约束只需要新增一个 Rule 实现并注册,不改动已有代码。
7. 最优叠加方案计算(核心算法)
7.1 问题建模
给定用户在某订单下的候选券集合 C = {c1, c2, ..., cn},以及第 6 节的叠加约束,求一个子集 S ⊆ C,满足:
S中任意两张券都不违反互斥组/依赖规则;S中每个分类的数量不超过上限,总数不超过全局上限;- 按结算优先级依次应用
S中的券后,订单实付金额最小(折扣总额最大)。
这本质上是一个带约束的组合优化问题:如果只有"互斥组 + 数量上限"两类约束,它接近分组背包问题(每组最多选一个 + 容量限制),多数场景下可以用动态规划精确求解;但一旦引入 DependencyRule 这种非对称的局部冲突,退化为通用图上的最大权独立集问题,理论上是 NP-Hard。
工程上的应对策略是:候选券数量在真实业务里通常很小(用户在一个订单下能同时满足门槛的券,过滤后一般不超过 15~20 张),因此用回溯 + 剪枝(branch and bound)做精确搜索完全可以在毫秒级完成;为极端情况兜底再加一个贪心降级方案 + 超时熔断。
7.2 计算流程
候选券过滤(领取规则+门槛规则)
│
▼
排序(按单张券折扣额降序,利于剪枝)
│
▼
回溯搜索(叠加规则做可行性剪枝,已选+理论上限做最优性剪枝)
│
├─ 在时间预算内求得精确解 ──▶ 返回最优组合
│
└─ 超时 ──▶ 贪心兜底(按折扣密度贪心选取) ──▶ 返回近似解(标记 Degraded)
7.3 Go 伪代码实现
// StackingResolver 负责计算给定候选券集合下的最优(折扣最大)组合
type StackingResolver struct {
engine *RuleEngine
priceCalc *PriceCalculator
timeBudget time.Duration // 单次计算的时间预算,如 30ms
}
const exactSearchLimit = 25 // 候选数超过该值直接走贪心,避免最坏情况指数爆炸
type ResolveResult struct {
SelectedCoupons []*CouponInstance
FinalPrice decimal.Decimal
TotalDiscount decimal.Decimal
Degraded bool // 是否因超时/候选过多而降级为启发式解
}
func (s *StackingResolver) Resolve(order *Order, candidates []*CouponInstance) *ResolveResult {
filtered := s.filterApplicable(order, candidates) // 过滤:过领取规则+门槛规则
if len(filtered) > exactSearchLimit {
return s.greedySearch(order, filtered)
}
deadline := time.Now().Add(s.timeBudget)
if best := s.exactSearch(order, filtered, deadline); best != nil {
return best
}
return s.greedySearch(order, filtered) // 精确搜索超时,兜底
}
// exactSearch 回溯 + 剪枝,寻找折扣总额最大的可行组合
func (s *StackingResolver) exactSearch(order *Order, candidates []*CouponInstance, deadline time.Time) *ResolveResult {
// 按单张券理论折扣从大到小排序,使剪枝尽早生效
sort.Slice(candidates, func(i, j int) bool {
return s.priceCalc.SingleDiscount(order, candidates[i]).
GreaterThan(s.priceCalc.SingleDiscount(order, candidates[j]))
})
var best *ResolveResult
var current []*CouponInstance
timeout := false
var backtrack func(idx int, upperBound decimal.Decimal)
backtrack = func(idx int, upperBound decimal.Decimal) {
if timeout || time.Now().After(deadline) {
timeout = true
return
}
currentDiscount := s.priceCalc.SimulateDiscount(order, current)
// 最优性剪枝:当前折扣 + 剩余候选券理论总折扣上限 都追不上 best,放弃该分支
if best != nil && currentDiscount.Add(upperBound).LessThanOrEqual(best.TotalDiscount) {
return
}
if idx == len(candidates) {
if best == nil || currentDiscount.GreaterThan(best.TotalDiscount) {
snapshot := append([]*CouponInstance{}, current...)
best = s.buildResult(order, snapshot, currentDiscount)
}
return
}
next := candidates[idx]
remainAfter := upperBound.Sub(s.priceCalc.SingleDiscount(order, next))
// 分支一:尝试选中 next(先过叠加规则可行性校验)
if s.isCompatible(order, current, next) {
current = append(current, next)
backtrack(idx+1, remainAfter)
current = current[:len(current)-1]
}
// 分支二:不选 next
backtrack(idx+1, remainAfter)
}
totalUpperBound := s.priceCalc.SumSingleDiscounts(order, candidates)
backtrack(0, totalUpperBound)
if timeout {
return nil
}
return best
}
// isCompatible 调用规则引擎的叠加规则链,判断 next 是否可与 current 组合
func (s *StackingResolver) isCompatible(order *Order, current []*CouponInstance, next *CouponInstance) bool {
ctx := &RuleContext{Order: order, Coupon: next, Template: next.Template, SelectedSet: current}
return s.engine.EvaluateChain(RuleStacking, ctx).Passed
}
// greedySearch 启发式降级:按折扣密度贪心选取,牺牲全局最优换取确定性延迟
func (s *StackingResolver) greedySearch(order *Order, candidates []*CouponInstance) *ResolveResult {
sort.Slice(candidates, func(i, j int) bool {
return s.priceCalc.SingleDiscount(order, candidates[i]).
GreaterThan(s.priceCalc.SingleDiscount(order, candidates[j]))
})
var selected []*CouponInstance
for _, c := range candidates {
if s.isCompatible(order, selected, c) {
selected = append(selected, c)
}
}
discount := s.priceCalc.SimulateDiscount(order, selected)
res := s.buildResult(order, selected, discount)
res.Degraded = true
return res
}
PriceCalculator 负责把"选定的一组券"转成"实际扣了多少钱"。因为满减/折扣的计算是顺序敏感的(先扣满减再打折,和先打折再扣满减,结果不同),所以结算顺序不是搜索出来的,而是由 CouponTemplate.Priority 固定配置的(通常约定:立减 → 满减 → 折扣 → 免邮),回溯只搜索"选哪些券",不搜索"以什么顺序应用":
type PriceCalculator struct{}
// SimulateDiscount 按固定优先级顺序依次应用券,返回折扣总额
func (p *PriceCalculator) SimulateDiscount(order *Order, coupons []*CouponInstance) decimal.Decimal {
ordered := sortByPriority(coupons) // 按 Template.Priority 升序
price := order.OriginalAmount
totalDiscount := decimal.Zero
for _, c := range ordered {
tpl := c.Template
var d decimal.Decimal
switch tpl.Type {
case TypeFullReduction:
if price.GreaterThanOrEqual(tpl.Threshold) {
d = tpl.DiscountValue
}
case TypeFixedOff:
d = decimal.Min(tpl.DiscountValue, price)
case TypeDiscount:
d = price.Mul(decimal.NewFromFloat(1).Sub(tpl.DiscountValue))
case TypeFreeShipping:
d = order.ShippingFee
}
price = price.Sub(d)
totalDiscount = totalDiscount.Add(d)
}
return totalDiscount
}
7.4 工程优化:模板级缓存
回溯搜索虽然快,但既然是同步链路,能不算就不算更好。关键观察是:叠加判定和折扣计算只依赖 CouponTemplate,不依赖 CouponInstance——也就是说,不同用户只要持有的模板组合相同、订单金额落在同一区间,最优解就是一样的。
因此可以在模板维度做缓存:
- 对"高频券模板组合 × 订单金额区间"做离线/懒加载预计算,把结果(最优模板子集)缓存进 Redis;
- 线上请求先查缓存命中的"最优模板子集",再映射回用户手里对应的具体券实例;
- 只有缓存未命中(长尾组合、新上线的券模板)才走实时回溯搜索。
这个优化通常能把大促高峰期的平均计算耗时从个位数毫秒进一步降到亚毫秒级,同时显著降低 CPU 消耗。
8. 数据存储设计
8.1 核心表结构(MySQL)
-- 券模板表
CREATE TABLE coupon_template (
id VARCHAR(32) PRIMARY KEY,
name VARCHAR(128) NOT NULL,
type VARCHAR(32) NOT NULL,
discount_value DECIMAL(12,2) NOT NULL,
threshold DECIMAL(12,2) DEFAULT 0,
scope_type VARCHAR(16),
scope_target_ids TEXT, -- JSON 数组
category VARCHAR(32),
priority INT DEFAULT 0,
valid_from DATETIME,
valid_to DATETIME,
stock_total BIGINT,
stock_remain BIGINT,
created_at DATETIME,
updated_at DATETIME
);
-- 券实例表(用户券包),按 user_id 分库分表
CREATE TABLE coupon_instance (
id VARCHAR(32) PRIMARY KEY,
template_id VARCHAR(32) NOT NULL,
user_id VARCHAR(32) NOT NULL,
status VARCHAR(16) NOT NULL,
received_at DATETIME,
used_at DATETIME,
order_id VARCHAR(32),
INDEX idx_user_status (user_id, status)
);
-- 叠加策略表(一份策略可关联多个互斥组/依赖规则)
CREATE TABLE stacking_policy (
id VARCHAR(32) PRIMARY KEY,
max_total_coupons INT,
max_per_category TEXT, -- JSON: {"platform":1,"merchant":3}
updated_at DATETIME
);
CREATE TABLE exclusion_group (
id VARCHAR(32) PRIMARY KEY,
policy_id VARCHAR(32) NOT NULL,
template_ids TEXT -- JSON 数组,组内模板互斥
);
CREATE TABLE dependency_rule (
id VARCHAR(32) PRIMARY KEY,
policy_id VARCHAR(32) NOT NULL,
source_template_id VARCHAR(32),
require_template_ids TEXT,
conflict_template_ids TEXT
);
-- 核销流水,用于对账和审计
CREATE TABLE redemption_log (
id VARCHAR(32) PRIMARY KEY,
coupon_id VARCHAR(32) NOT NULL,
order_id VARCHAR(32) NOT NULL,
action VARCHAR(16), -- LOCK / REDEEM / ROLLBACK
created_at DATETIME
);
8.2 缓存策略(Redis)
| 缓存内容 | 用途 | 策略 |
|---|---|---|
| 券模板库存 | 大促场景下的原子扣减 | Redis + Lua 脚本保证扣减原子性,异步回写 MySQL |
| 用户券包(可用券列表) | 减少结算页查询延迟 | 领取/核销时主动失效,TTL 兜底 |
| 模板级最优组合结果(见 7.4) | 减少重复计算 | 按"模板组合签名 + 金额区间"作 key,定期刷新 |
| 叠加策略配置 | 规则引擎加载 | 本地缓存 + 配置中心推送更新(Nacos/Apollo) |
9. 服务拆分与 API 设计
| 服务 | 职责 |
|---|---|
TemplateService |
券模板 CRUD、库存管理 |
WalletService |
用户领券、查询用户券包 |
StackingService |
可用券过滤 + 最优组合计算(第 7 节核心逻辑) |
RedeemService |
下单锁定、支付核销、取消/退款回滚 |
RuleAdminService |
运营后台:互斥组、依赖规则、表达式规则的可视化配置 |
关键接口(REST 风格示例):
GET /coupons/available?orderId=xxx
→ 返回该订单下用户所有"可用"的券(已过领取+门槛规则)
POST /coupons/optimal-combination
body: { orderId, candidateCouponIds[] }
→ 调用 StackingResolver.Resolve,返回最优组合 + 折扣金额
POST /coupons/lock
body: { orderId, couponIds[] }
→ 下单时锁定,状态 UNUSED → LOCKED
POST /coupons/redeem
body: { orderId }
→ 支付成功后核销,状态 LOCKED → USED
POST /coupons/rollback
body: { orderId }
→ 订单取消/退款,状态回滚为 UNUSED
10. 高并发与一致性保障
10.1 库存扣减防超卖
采用 Redis + Lua 脚本做原子扣减(检查剩余库存 + 扣减在一条 Lua 脚本内完成,避免竞态),扣减成功后异步写入 MySQL 流水,定期对账修正。
10.2 核销幂等与状态机
券的生命周期建模为显式状态机:
UNUSED ──锁定(下单)──▶ LOCKED ──核销(支付成功)──▶ USED
▲ │
└──────回滚(取消/退款)───┘
每次状态流转都要求携带订单号做幂等校验(同一订单重复调用核销接口不能重复核销),并记录 redemption_log 用于审计。
10.3 跨服务一致性
下单锁券、支付核销、库存扣减往往涉及多个服务,推荐用 本地消息表 + 事务消息(如 RocketMQ 事务消息) 或 Saga 模式 保证最终一致性:支付成功事件驱动"核销"和"库存正式扣减"两个动作,任一失败都有补偿流程(重试 + 人工介入告警)。
11. 可扩展性设计
- 规则可插拔:新增规则类型只需实现
Rule接口并注册进对应的RuleType链,不侵入RuleEngine和StackingResolver; - 规则可配置:高频调整的门槛/叠加参数用表达式规则或数据库配置驱动,运营后台可视化编辑,变更通过配置中心热推送,不需要发版;
- 券类型可扩展:新增券类型(如"阶梯满减")只需在
PriceCalculator.SimulateDiscount的 switch 分支中新增计算逻辑,并在CouponType枚举中注册; - 计算引擎可替换:
StackingResolver对外只暴露Resolve(order, candidates)接口,内部精确搜索/贪心/未来可能引入的整数规划求解器(如小规模场景接入开源 ILP 求解器)可以自由替换,不影响调用方。
12. 监控与可观测性
| 指标 | 说明 | 告警阈值建议 |
|---|---|---|
| 核销成功率 | 核销失败可能意味着资损或体验问题 | 失败率 > 0.1% 告警 |
| 库存扣减异常 | Redis/MySQL 库存不一致 | 出现即告警 |
| 最优组合计算耗时 P99 | 影响结算页体验 | P99 > 100ms 告警 |
| 降级(Degraded)比例 | 精确搜索超时走贪心的比例 | 比例持续 > 5% 需要排查候选券过多的原因 |
| 规则判定拒绝原因分布 | 定位哪条叠加规则最常触发拒绝,辅助运营优化规则 | 用于分析,非告警 |
建议在 RuleResult.Reason 和 ResolveResult.Degraded 上打点上报,便于事后分析"为什么用户没凑出更优惠的组合"。
13. 总结与后续演进
本设计的核心思路是把优惠券系统拆成两条相对独立的能力线:
- 规则引擎回答"能不能"的问题(领取资格、使用门槛、能否叠加);
- 叠加计算引擎回答"怎样最优"的问题,建模为带约束的组合优化,用回溯剪枝求精确解、贪心兜底求近似解,并通过模板级缓存把大部分请求前置到亚毫秒级。
后续可演进方向:
- 当叠加规则进一步复杂化(如涉及跨订单、跨店铺的联合满减)时,可以考虑引入更通用的 ILP(整数线性规划)求解器替换手写回溯;
- 结合用户历史行为,在多个"折扣相同"的最优解之间做个性化选择(如优先消耗即将过期的券);
- 引入 A/B 测试能力,在规则引擎层面支持"规则版本"灰度发布。