Hello World

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

0%

优惠券系统设计

当前业务规模(IoT 增值服务:云存储、流量包、套餐)采用 模板 + 用户券 + 使用记录 + 生命周期事件 四层模型。

整体模型关系

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
32
CREATE TABLE tb_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_type VARCHAR(64) NOT NULL DEFAULT 'cinmooreRights' COMMENT '商品类型:cinmooreRights=云存储',
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 '二期预留,一期仅支持单券。0=不可叠加(默认)、1=可叠加',
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_type VARCHAR(64) 商品类型 默认 cinmooreRights=云存储,预留多商品类型扩展(流量包、套餐等)
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 时系统自动暂停;运营也可手动暂停
  • 暂停 → 上线:运营调整预算/发行量后手动恢复上线
  • 上线/暂停 → 下线:运营手动下线,不再接受新的领取请求,已领取的券也无法消费
  • 下线 → 上线:运营可重新上线
  • 任意 → 删除:软删除,逻辑上不再展示和生效

UserCoupon 用户券

职责

表示用户实际拥有的一张券。例如:

  • 张三领取了”满100减20”

生成 tb_user_coupon 记录。

表结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
CREATE TABLE tb_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 关联 tb_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 已失效]
    LOCKED -->|EXPIRED 定时过期| EXPIRED
    USED -->|REFUNDED+RETURNED 退款退回| UNUSED
    USED -->|REFUNDED 退款不返还| END1((结束))
    EXPIRED --> END1

LOCKED → EXPIRED 说明: 当券被锁定后,如果券本身已过期(expire_time < NOW()),定时过期任务会直接将 LOCKED 状态的券过期为 EXPIRED,同时清除关联的 order_idlock_time。此时订单超时取消流程会尝试 unlockCoupon,但由于券已变为 EXPIRED,unlockCoupon 校验状态不是 LOCKED 会跳过解锁,这是预期行为。

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 tb_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 关联 tb_user_coupon.id
template_id BIGINT 模板ID 关联 tb_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 更新时间

示例

订单:

1
120元 = 12000分

优惠券:

1
满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
16
CREATE TABLE tb_coupon_event_log
(
id BIGINT PRIMARY KEY,
user_coupon_id BIGINT NOT NULL,
user_id BIGINT DEFAULT NULL COMMENT '用户ID,冗余自 tb_user_coupon.user_id',
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 关联 tb_user_coupon.id
user_id BIGINT 用户ID 冗余自 tb_user_coupon.user_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 更新时间

示例

1
2
3
ISSUED
2026-01-01
用户领取
1
2
3
LOCKED
2026-01-10
提交订单
1
2
3
USED
2026-01-10
支付成功
1
2
3
REFUNDED
2026-01-15
订单退款
1
2
3
RETURNED
2026-01-15
优惠券退回

operator_type 默认值规则

事件类型 operator_type operator_id 说明
ISSUED SYSTEM - 系统自动发放
LOCKED USER userId 用户主动下单触发
UNLOCKED SYSTEM - 系统自动解锁(订单取消/超时)
USED USER userId 用户支付成功触发
EXPIRED SYSTEM - 定时任务批量过期
REFUNDED SYSTEM - 退款触发
RETURNED SYSTEM - 退回触发

注:LOCKED 和 USED 事件由用户主动触发,记录 operatorType=USERoperatorId=userId;其余事件由系统自动触发,记录 operatorType=SYSTEM,不记录 operatorId。

运行流程

领取优惠券

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

筛选可用券(listAvailableCoupons)

此流程负责筛选符合使用条件的券,返回全部可用券列表,并内部标记最优券。
最优券选择也可由 calculateOptimalCoupon 独立完成(见下方)。

入参userIdgoodsIdpaySystemNamecopies(购买份数,可选,默认1)

copies 参与抵扣金额计算:beforeCouponPrice = priceDel(商品差异化定价) × copies,份数越多,beforeCouponPrice 越大,可能影响门槛校验和折扣券的实际抵扣金额。

graph TD
    A[查询用户优惠券列表
status=UNUSED 且未过期] --> B{逐一校验} B --> C{status=UNUSED?} C -->|否| X[排除该券] C -->|是| D{在有效期内?} D -->|否| X D -->|是| E{匹配商品范围
goods_ids?} E -->|否| X E -->|是| F{匹配支付渠道
channels?} F -->|否| X F -->|是| G[计算抵扣金额
基于 priceDel × copies] G --> H[加入可用券列表] H --> I{列表非空?} I -->|否| J[返回空列表] I -->|是| K[标记最优券
抵扣最大优先
金额相同选快过期的] K --> L[返回可用券列表
isOptimal 已标记]

注:

  • platforms 字段一期未启用,默认全平台通过,故流程图中省略。
  • 返回列表中每张券的 discountAmount 已基于 copies 计算完成,最优券的 isOptimal=true,前端可直接使用,无需再调 calculateOptimalCoupon

选择最优券(calculateOptimalCoupon)

graph TD
    A[获取可用券列表] --> B{列表为空?}
    B -->|是| C[返回无可用券]
    B -->|否| D[逐张计算抵扣金额
基于 beforeCouponPrice] D --> E[选择抵扣金额最大的券
金额相同时优先选快过期的券] E --> F[返回最优券及抵扣金额]

一期规则couponIds.size() > 1 时直接抛出 COUPON_ONLY_ONE_ALLOWED 异常,限制单券使用。
二期扩展:当 stackable=1 时,按 stack_rule 组合多券叠加,每张券独立基于原价计算抵扣。

预算管控逻辑

预算扣减和回退均通过 MQ 异步处理,保证最终一致性。

graph TD
    A[优惠券核销成功
MQ消费端处理] --> 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[正常展示]

退回预算回退:优惠券退回时,MQ 消费端(handleReturn)原子递减 used_budget_amount,解析优先级:CouponUseRecord.discountAmountOrderPay.discountRefs(couponId)OrderPay.discountValue(一期单券 fallback)。

下单锁券

下单时优惠券处理分为预校验锁定两步,预校验在创建订单前执行,锁定在创建订单时执行。

graph TD
    A[用户提交订单
携带 couponIds] --> B{couponIds 非空?} B -->|否| Z[无券,走普通下单流程] B -->|是| V{一期校验
couponIds.size <= 1?} V -->|否| W[抛出异常
COUPON_ONLY_ONE_ALLOWED] V -->|是| P[第一步:预校验
validateCouponsForPlaceOrder] P --> P1[批量查询用户优惠券和模板] P1 --> P2[逐张校验
归属→状态UNUSED→未过期→模板可用→商品范围→支付渠道] P2 --> P3{校验通过?} P3 -->|否| W2[抛出异常
终止下单] P3 -->|是| Q[第二步:锁定
lockCoupon] Q --> C[UserCoupon
UNUSED → LOCKED
乐观锁 version+1
一条SQL完成状态+order_id+lock_time+版本递增] C --> D[写入 EventLog
event_type=LOCKED
before_status=UNUSED
after_status=LOCKED] D --> F{订单取消?} F -->|30分钟超时自动取消| G[UserCoupon
LOCKED → UNUSED] F -->|用户主动取消| G G --> H[写入 EventLog
event_type=UNLOCKED
before_status=LOCKED
after_status=UNUSED] H --> I[优惠券回到可用状态
用户可再次使用]

预校验 vs 锁定

  • 预校验validateCouponsForPlaceOrder):不锁定券,仅校验可用性,用于在创建订单前提前拦截无效优惠券,避免产生无效订单。校验项:存在性、归属、状态 UNUSED、未过期、模板可用(上线/暂停)、商品范围、支付渠道。
  • 锁定lockCoupon):原子更新 UNUSED → LOCKED,一条 SQL 完成状态变更 + 设置 order_id/lock_time + 版本递增,避免两步更新导致状态/版本回滚。

支付成功

graph TD
    A[支付成功回调] --> B[UserCoupon
LOCKED → USED
乐观锁 version+1] B --> C[写入 EventLog
event_type=USED
before_status=LOCKED
after_status=USED] C --> D[事务提交后发送 MQ
tag=COUPON_REDEMPTION] D --> E[MQ消费端异步处理] E --> F[创建 CouponUseRecord
从 OrderPay 解析金额
幂等:已存在则跳过] F --> G[原子递增
used_budget_amount += discount_amount]

异步化说明CouponUseRecord 创建和预算扣减通过 MQ 异步处理,保证主流程(状态变更+事件日志)的响应速度。

  • MQ 消息在事务提交后发送(TransactionSynchronizationManager),确保只有状态变更成功才发消息。
  • handleCouponRedemption 消费端采用幂等策略:若 CouponUseRecord 已存在则跳过,仅执行预算扣减。
  • CouponUseRecord 的金额信息从 OrderPay.discountRefs 解析,优先取 actualAmount(实际抵扣),fallback 取 discountValue(一期单券场景)。
  • MQ 发送失败不影响主流程,预算变更由 MQ 重试或补偿任务保证最终一致性。

支付失败

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

退款

退回优惠券的通用入口为 returnCoupon(couponId),按当前券状态分支处理:

  • USED → 走退款完整链路(见方案二)
  • LOCKED → 校验订单已取消 + 券未过期后回退(见 LOCKED 状态退回)
  • 其他状态 → 直接返回 false,不允许退回

方案一:券不返还

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

方案二:券返还(USED 状态退回)

graph TD
    A[订单退款] --> B[UserCoupon
USED → UNUSED
乐观锁 version+1] B --> C[写入 EventLog
event_type=RETURNED
before_status=USED
after_status=UNUSED] C --> D{优惠券是否过期?} D -->|未过期| E[用户可再次使用] D -->|已过期| F[UNUSED → EXPIRED
写入 EventLog
event_type=EXPIRED] F --> G[事务提交后发送 MQ
预算回退] E --> G

退回后预算回退通过 MQ 异步处理(handleReturn),原子递减 used_budget_amount,保证最终一致性。

LOCKED 状态退回

场景:券已锁定(LOCKED),但对应订单已取消,用户主动调用退回接口将券释放回可用状态。

graph TD
    A[用户调用退回接口
returnCoupon] --> B{券状态?} B -->|USED| C[走方案二
退款退回链路] B -->|LOCKED| D{对应订单已取消?} D -->|否| E[退回失败
订单未取消] D -->|是| F{券是否过期?} F -->|已过期| G[退回失败
券已过期] F -->|未过期| H[LOCKED → UNUSED
乐观锁 version+1] H --> I[写入 EventLog
event_type=UNLOCKED
before_status=LOCKED
after_status=UNUSED] I --> J[券回到可用状态
用户可再次使用] B -->|其他| K[退回失败
状态不允许退回]

前置条件:LOCKED 状态退回必须同时满足”订单已取消”和”券未过期”两个条件,否则退回失败。

与 tb_order_pay 的关联

优惠券使用时,抵扣信息会写入 tb_order_pay,新增以下折扣相关字段:

字段 类型 说明
original_price INT 优惠前价格(分),所有折扣抵扣前的订单金额
discount_value INT 实际抵扣总额(分),恒等式:price_cent = original_price - discount_value
discount_refs JSON 折扣引用数组,每个元素含 type/id/amount/actualAmount
price_detail JSON 价格计算明细,含原价/差异化定价/份数/4G升级/优惠券等完整计算链路

discount_refs JSON 数组格式如下:

1
2
3
4
5
6
7
8
[
{
"type": "COUPON",
"id": 10001,
"amount": 5000,
"actualAmount": 3000
}
]
字段 类型 说明
type String 优惠类型,当前固定为 COUPON
id Long 用户券 ID(tb_user_coupon.id)
amount Integer 券面值(分):固定减免金额或折扣券封顶金额
actualAmount Integer 实际抵扣金额(分):基于 beforeCouponPrice 计算后的真实抵扣

一期:数组最多 1 个元素(单券)。
二期:多券叠加时,每张券对应一个独立元素。

关联字段:tb_order_pay.coupon_price = Σ actualAmount(所有 COUPON 元素之和)。

异常码

异常枚举 错误码 说明
COUPON_NOT_FOUND 5000200 优惠券不存在
COUPON_STATUS_INVALID 5000201 优惠券状态无效(非期望状态)
COUPON_EXPIRED 5000202 优惠券已过期
COUPON_CONCURRENT_CONFLICT 5000203 优惠券状态已变更,请重试(乐观锁冲突)
COUPON_NOT_APPLICABLE 5000204 优惠券不适用于当前商品
COUPON_CHANNEL_NOT_SUPPORTED 5000205 当前支付方式不支持使用优惠券
COUPON_THRESHOLD_NOT_MET 5000206 未达到优惠券使用门槛
COUPON_BUDGET_EXCEEDED 5000207 优惠券预算已用完
COUPON_NOT_OWNED_BY_USER 5000208 非当前用户的优惠券
COUPON_ONLY_ONE_ALLOWED 5000209 仅支持使用一张优惠券

最终推荐模型

对于当前项目,建议只保留 4 张核心表:

表名 职责
tb_coupon_template 优惠券模板定义
tb_user_coupon 用户实际拥有的券
tb_coupon_use_record 核销记录
tb_coupon_event_log 生命周期事件

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

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

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