Hello World

吞风吻雨葬落日 欺山赶海踏雪径

0%

优惠券系统设计

优惠券系统采用 模板 + 用户券 + 使用记录 + 生命周期事件 四层模型。

整体模型关系

classDiagram
    CouponTemplate "1" --> "N" UserCoupon: 生成
    UserCoupon "1" --> "N" CouponEventLog: 记录
    UserCoupon "1" --> "0..1" CouponUseRecord: 核销

职责划分:

实体 职责
CouponTemplate 定义优惠券规则(满减、折扣、适用范围等)
UserCoupon 用户实际拥有的券,记录状态流转
CouponUseRecord 记录核销结果(实际优惠金额、实付金额)
CouponEventLog 记录整个生命周期事件,用于审计、排查、退款恢复

CouponTemplate 优惠券模板

职责

定义优惠券属性与规则。模板本身不能使用,用户领取后才生成用户券。

类似:

  • 满100减20
  • 8折券
  • 云存储专用50元券

表结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
CREATE TABLE coupon_template
(
id BIGINT PRIMARY KEY ,
template_code VARCHAR(64) NOT NULL,
template_name VARCHAR(128) NOT NULL,
status TINYINT NOT NULL,
coupon_type VARCHAR(32) NOT NULL,
discount_amount INT DEFAULT NULL COMMENT '固定减免金额(分),coupon_type=FIXED 时使用',
discount_rate INT DEFAULT NULL COMMENT '折扣率(千分之),coupon_type=DISCOUNT 时使用,如 800=8折',
threshold_amount INT DEFAULT NULL COMMENT '最低消费金额(分)',
max_discount_amount INT DEFAULT NULL COMMENT '最大优惠金额(分),折扣券封顶',
total_quantity INT DEFAULT NULL,
issued_quantity INT DEFAULT 0,
budget_amount INT DEFAULT NULL COMMENT '优惠预算上限(分)',
used_budget_amount INT DEFAULT 0 COMMENT '已使用预算(分)',
valid_type VARCHAR(32) NOT NULL,
valid_days INT DEFAULT NULL,
valid_start_time DATETIME DEFAULT NULL,
valid_end_time DATETIME DEFAULT NULL,
claim_start_time DATETIME DEFAULT NULL,
claim_end_time DATETIME DEFAULT NULL,
goods_ids JSON DEFAULT NULL COMMENT '适用商品ID数组,NULL表示不限商品,如 [1001, 1002]',
platforms JSON DEFAULT NULL COMMENT '一期不做,默认全平台',
channels JSON DEFAULT NULL COMMENT '适用支付渠道,枚举数组:WECHAT、ALIPAY,NULL表示全渠道',
stackable TINYINT DEFAULT 0 COMMENT '一期不做,默认不可堆叠',
stack_rule JSON DEFAULT NULL COMMENT '一期不做,默认不可堆叠',
rule_json JSON DEFAULT NULL,
remark VARCHAR(500) DEFAULT NULL,
gmt_create DATETIME COMMENT '创建时间',
gmt_modified DATETIME COMMENT '更新时间'
);

字段说明

字段 类型 名称 备注
id BIGINT 主键 雪花
template_code VARCHAR(64) 模板编码 唯一标识,如 CLOUD_50_OFF
template_name VARCHAR(128) 模板名称 如”云存储50元优惠券”
status TINYINT 状态 枚举:0=草稿、1=上线、2=暂停、3=下线、4=删除,详见下方状态说明
coupon_type VARCHAR(32) 优惠类型 枚举:FIXED=固定减免、DISCOUNT=折扣券
discount_amount INT 固定减免金额 coupon_type=FIXED 时使用,单位:分。如 2000 表示减20元。DISCOUNT 时为 NULL
discount_rate INT 折扣率 coupon_type=DISCOUNT 时使用,单位:千分之。如 800=8折、850=85折。FIXED 时为 NULL
threshold_amount INT 最低消费金额 满减门槛,单位:分。如满100减20则填 10000
max_discount_amount INT 最大优惠金额 折扣券封顶金额,单位:分。如8折最多减50元则填 5000
total_quantity INT 发行总量 NULL 表示不限量
issued_quantity INT 已发放数量 领取时累加
budget_amount INT 优惠预算上限 单券种最大优惠金额预算,单位:分。NULL表示不限预算
used_budget_amount INT 已使用预算 核销时累加优惠金额,单位:分。达到 budget_amount 时自动暂停发放
valid_type VARCHAR(32) 有效期类型 枚举:FIXED_DATE=固定时间范围、AFTER_RECEIVE=领取后N天
valid_days INT 有效天数 valid_type=AFTER_RECEIVE 时必填,如30表示领取后30天
valid_start_time DATETIME 有效期开始 valid_type=FIXED_DATE 时必填
valid_end_time DATETIME 有效期结束 valid_type=FIXED_DATE 时必填
claim_start_time DATETIME 领取开始时间 用户可领取的起始时间
claim_end_time DATETIME 领取结束时间 用户可领取的截止时间,过期后不可领取
goods_ids JSON 适用商品 商品ID数组,NULL表示不限商品,如 [1001, 1002]
platforms JSON 适用平台 一期不做,默认全平台。枚举数组:IOSANDROIDWEBMINI_PROGRAM,NULL表示全平台
channels JSON 适用支付渠道 枚举数组:WECHATALIPAY,NULL表示全支付渠道
stackable TINYINT 是否可叠加 一期不做,默认不可堆叠。0=不可叠加(默认)、1=可叠加
stack_rule JSON 叠加规则 一期不做,默认不可堆叠。可叠加时的规则详情,如 {"maxStackCount": 2, "stackableTypes": ["FIXED"]}
rule_json JSON 扩展规则 自定义规则,如 {"newUserOnly": true, "maxReceiveCount": 1}
remark VARCHAR(500) 备注 运营备注
gmt_create DATETIME 创建时间
gmt_modified DATETIME 更新时间

模板状态说明

枚举值 状态名称 说明 可领取 可下发 已领取可消费
0 草稿 编辑状态,模板尚未发布,仍在配置中。不能领取,已领取的不能消费
1 上线 正式状态,模板已发布。用户可正常领取、系统可下发,已领取的券可正常消费
2 暂停 因预算或发放量达到限制而自动/手动暂停。不能领取、不能下发,但已领取的券仍可正常消费
3 下线 相对于上线状态,模板已下线。不能领取,已领取的券也不能消费
4 删除 模板已作废删除。不能领取,已领取的券也不能消费

模板状态流转

graph LR
    DRAFT[0 草稿] -->|上线| ONLINE[1 上线]
    ONLINE -->|预算/发放量达限| PAUSED[2 暂停]
    PAUSED -->|运营手动恢复| ONLINE
    ONLINE -->|下线| OFFLINE[3 下线]
    DRAFT -->|删除| DELETED[4 删除]
    ONLINE -->|删除| DELETED
    PAUSED -->|删除| DELETED
    OFFLINE -->|删除| DELETED
    OFFLINE -->|重新上线| ONLINE

状态行为差异

  • 草稿 → 上线:运营确认模板配置无误后手动上线,上线后用户才可见、可领取
  • 上线 → 暂停:当 used_budget_amount >= budget_amountissued_quantity >= total_quantity 时系统自动暂停;运营也可手动暂停
  • 暂停 → 上线:运营调整预算/发行量后手动恢复上线
  • 上线/暂停 → 下线:运营手动下线,不再接受新的领取请求,已领取的券也无法消费
  • 下线 → 上线:运营可重新上线
  • 任意 → 删除:软删除,逻辑上不再展示和生效

其他核心枚举

coupon_type 优惠类型

含义 使用的字段
FIXED 固定减免 discount_amount
DISCOUNT 折扣券 discount_rate + max_discount_amount

valid_type 有效期类型

含义 使用的字段
FIXED_DATE 固定时间范围 valid_start_time + valid_end_time
AFTER_RECEIVE 领取后N天 valid_days

UserCoupon 用户券

职责

表示用户实际拥有的一张券。例如张三领取了”满100减20”,则生成一条 user_coupon 记录。

表结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
CREATE TABLE user_coupon
(
id BIGINT PRIMARY KEY ,
coupon_no VARCHAR(64) NOT NULL,
template_id BIGINT NOT NULL,
user_id BIGINT NOT NULL,
status VARCHAR(32) NOT NULL,
acquire_time DATETIME NOT NULL,
expire_time DATETIME NOT NULL,
lock_time DATETIME,
used_time DATETIME,
order_id BIGINT,
version INT DEFAULT 0,
gmt_create DATETIME COMMENT '创建时间',
gmt_modified DATETIME COMMENT '更新时间'
);

字段说明

字段 类型 名称 备注
id BIGINT 主键 雪花
coupon_no VARCHAR(64) 券码 唯一标识,用户可见的券编号
template_id BIGINT 模板ID 关联 coupon_template.id
user_id BIGINT 用户ID 领券用户
status VARCHAR(32) 状态 枚举:UNUSED=未使用、LOCKED=已锁定、USED=已使用、EXPIRED=已过期
acquire_time DATETIME 领取时间
expire_time DATETIME 过期时间 到期后状态变为 EXPIRED
lock_time DATETIME 锁定时间 提交订单时锁定,支付成功后变为 USED,支付失败回退 UNUSED
used_time DATETIME 使用时间 核销时间
order_id BIGINT 订单ID 使用时关联的订单
version INT 版本号 乐观锁,默认0
gmt_create DATETIME 创建时间
gmt_modified DATETIME 更新时间

状态流转

graph LR
    START((开始)) -->|ISSUED 发放| UNUSED[UNUSED 未使用]
    UNUSED -->|LOCKED 下单锁定| LOCKED[LOCKED 已锁定]
    LOCKED -->|UNLOCKED 支付失败| UNUSED
    LOCKED -->|USED 支付成功| USED[USED 已使用]
    UNUSED -->|EXPIRED 过期| EXPIRED[EXPIRED 已失效]
    USED -->|REFUNDED+RETURNED 退款退回| UNUSED
    USED -->|REFUNDED 退款不返还| END1((结束))
    EXPIRED --> END1

状态说明:

状态 含义 触发场景
UNUSED 未使用 用户领取后初始状态
LOCKED 已锁定 提交订单时锁定,防止并发使用
USED 已使用 支付成功后核销
EXPIRED 已失效 超过 expire_time 自动过期

乐观锁(version)

解决并发问题。同一张券不能被多次使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 1. 读取券,拿到当前 version
UserCoupon coupon = userCouponMapper.selectById(couponId);
int currentVersion = coupon.getVersion(); // 比如 0

// 2. 尝试更新,带上 version 条件
int rows = userCouponMapper.updateStatus(
couponId, "LOCKED", currentVersion
);

// 3. 判断结果
if (rows == 0) {
// 并发冲突,券已被其他请求修改
throw new ConcurrentUpdateException("优惠券状态已变更,请重试");
}
// 成功,继续后续业务逻辑

CouponUseRecord 用券记录

职责

记录优惠券在某个订单上实际产生了多少优惠。因为订单金额会变化,不能只看 UserCoupon。

表结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
CREATE TABLE coupon_use_record
(
id BIGINT PRIMARY KEY ,
user_coupon_id BIGINT NOT NULL,
template_id BIGINT NOT NULL,
user_id BIGINT NOT NULL,
order_id BIGINT NOT NULL,
order_amount INT NOT NULL COMMENT '原始订单金额(分)',
discount_amount INT NOT NULL COMMENT '券实际减免金额(分)',
pay_amount INT NOT NULL COMMENT '实付金额(分),order_amount - discount_amount',
use_time DATETIME NOT NULL,
gmt_create DATETIME COMMENT '创建时间',
gmt_modified DATETIME COMMENT '更新时间'
);

字段说明

字段 类型 名称 备注
id BIGINT 主键 雪花
user_coupon_id BIGINT 用户券ID 关联 user_coupon.id
template_id BIGINT 模板ID 关联 coupon_template.id
user_id BIGINT 用户ID
order_id BIGINT 订单ID
order_amount INT 订单金额 原始订单金额(分),与 tb_order_pay.original_price 对齐
discount_amount INT 优惠金额 券实际减免金额(分)
pay_amount INT 实付金额 order_amount - discount_amount(分)
use_time DATETIME 使用时间
gmt_create DATETIME 创建时间
gmt_modified DATETIME 更新时间

示例

订单 120 元 = 12000 分,优惠券满100减20:

1
order_amount=12000 discount_amount=2000 pay_amount=10000

CouponEventLog 生命周期事件

职责

统一记录优惠券的生命周期事件,用于审计、问题排查、运营分析、退款恢复。

表结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
CREATE TABLE coupon_event_log
(
id BIGINT PRIMARY KEY ,
user_coupon_id BIGINT NOT NULL,
event_type VARCHAR(32) NOT NULL,
before_status VARCHAR(32),
after_status VARCHAR(32),
operator_type VARCHAR(32),
operator_id BIGINT,
biz_id VARCHAR(64),
ext_info JSON,
event_time DATETIME NOT NULL,
gmt_create DATETIME COMMENT '创建时间',
gmt_modified DATETIME COMMENT '更新时间'
);

字段说明

字段 类型 名称 备注
id BIGINT 主键 雪花
user_coupon_id BIGINT 用户券ID 关联 user_coupon.id
event_type VARCHAR(32) 事件类型 枚举:ISSUED=发放、LOCKED=锁定、UNLOCKED=解锁、USED=使用、EXPIRED=过期、REFUNDED=退款、RETURNED=退回
before_status VARCHAR(32) 变更前状态
after_status VARCHAR(32) 变更后状态
operator_type VARCHAR(32) 操作者类型 枚举:USER=用户、SYSTEM=系统、ADMIN=管理员
operator_id BIGINT 操作者ID
biz_id VARCHAR(64) 业务ID 如订单号、退款单号等
ext_info JSON 扩展信息 附加业务数据
event_time DATETIME 事件时间
gmt_create DATETIME 创建时间
gmt_modified DATETIME 更新时间

事件类型

事件 含义 典型 before→after
ISSUED 发放 → UNUSED
LOCKED 锁定 UNUSED → LOCKED
UNLOCKED 解锁 LOCKED → UNUSED
USED 使用 LOCKED → USED
EXPIRED 过期 UNUSED → EXPIRED
REFUNDED 退款 USED → *
RETURNED 退回 * → UNUSED

示例

一张券的完整生命周期:

时间 事件 说明
2026-01-01 ISSUED 用户领取
2026-01-10 LOCKED 提交订单
2026-01-10 USED 支付成功
2026-01-15 REFUNDED 订单退款
2026-01-15 RETURNED 优惠券退回

运行流程

领取优惠券

graph TD
    A[用户领取优惠券] --> B[创建 UserCoupon
status=UNUSED] B --> C[计算过期时间
expire_time=acquire_time+valid_days] C --> D[写入 EventLog
event_type=ISSUED] D --> E[返回领取成功]

查询可用券

graph TD
    A[查询用户优惠券列表] --> B{status=UNUSED?}
    B -->|否| X[排除该券]
    B -->|是| C{在有效期内?}
    C -->|否| X
    C -->|是| D{匹配商品范围
goods_ids?} D -->|否| X D -->|是| E{匹配平台范围
platforms?} E -->|否| X E -->|是| F{匹配支付渠道
channels?} F -->|否| X F -->|是| G{stackable=?} G -->|stackable = 0| H[同一订单仅可用1张券
自动匹配抵扣金额最高的券] G -->|stackable = 1| I[按 stack_rule 匹配
可叠加券组合] H --> J[返回可用券列表] I --> J

预算管控逻辑

graph TD
    A[优惠券核销成功] --> B[used_budget_amount += discount_amount]
    B --> C{budget_amount 不为NULL?}
    C -->|否| D[继续正常发放]
    C -->|是| E{used_budget_amount
>= budget_amount?} E -->|否| D E -->|是| F[自动暂停该券种发放
拦截后续领取请求] D --> G[运营后台展示预算使用率] F --> G G --> H{使用率 > 80%?} H -->|是| I[预警提示] H -->|否| J[正常展示]

下单锁券

graph TD
    A[用户提交订单] --> B[校验优惠券可用性]
    B --> C[UserCoupon
UNUSED → LOCKED] C --> D[写入 EventLog
event_type=LOCKED
before_status=UNUSED
after_status=LOCKED] D --> E{订单取消?} E -->|30分钟超时自动取消| F[UserCoupon
LOCKED → UNUSED] E -->|用户主动取消| F F --> G[写入 EventLog
event_type=UNLOCKED
before_status=LOCKED
after_status=UNUSED] G --> H[优惠券回到可领取状态
用户可再次使用]

支付成功

graph TD
    A[支付成功回调] --> B[UserCoupon
LOCKED → USED] B --> C[创建 CouponUseRecord
记录 order_amount / discount_amount / pay_amount] C --> D[累加 coupon_template.used_budget_amount] D --> E[写入 EventLog
event_type=USED
before_status=LOCKED
after_status=USED]

支付失败

graph TD
    A[支付失败回调] --> B[UserCoupon
LOCKED → UNUSED] B --> C[写入 EventLog
event_type=UNLOCKED
before_status=LOCKED
after_status=UNUSED]

退款

退款有两种方案,根据模板配置决定:

方案一:券不返还

graph TD
    A[订单退款] --> B[UserCoupon
USED 保持不变] B --> C[写入 EventLog
event_type=REFUNDED]

方案二:券返还

graph TD
    A[订单退款] --> B[UserCoupon
USED → UNUSED] B --> C[写入 EventLog
event_type=REFUNDED
before_status=USED
after_status=UNUSED] C --> D[写入 EventLog
event_type=RETURNED] D --> E{优惠券是否过期?} E -->|未过期| F[用户可再次使用] E -->|已过期| G[状态变为 EXPIRED
不可再次使用]

模型总结

4 张核心表:

表名 职责
coupon_template 优惠券模板定义
user_coupon 用户实际拥有的券
coupon_use_record 核销记录
coupon_event_log 生命周期事件

这样既不会出现十几张营销表导致过度设计,也能满足未来扩展:

  • 云存储优惠券
  • 流量包优惠券
  • 套餐优惠券
  • 兑换码
  • 新人券
  • 活动券
  • 邀请码奖励券
  • 订阅升级优惠券

后续即使接入 Stripe 订阅升级、套餐促销、自动发券等需求,也无需推翻当前模型。