优惠券库存超卖因Beego默认不启事务致SELECT...FOR UPDATE失效,须显式事务包裹「查-判-减」;指定商品与满减校验需统一CouponRule结构体;防重复领券应建联合唯一索引并捕获ErrDBDuplicateEntry;性能优化需复合索引、避免函数操作及N+1查询。

优惠券库存扣减为什么总是超卖
Beego 默认的数据库操作不自动开启事务,SELECT ... FOR UPDATE 在非事务中无效,导致并发请求读到旧库存后重复扣减。必须显式开启事务并确保整个「查-判-减」流程包裹其中。
实操建议:
- 用
o.Begin()启动事务,成功后调用o.Commit(),失败时o.Rollback() - 查询库存必须用
o.QueryTable("coupon").Filter("id", cid).ForUpdate().One(&coupon),缺ForUpdate()会失效 - Beego 的
ForUpdate()仅对 MySQL 有效,PostgreSQL 需改用QueryRow手写SELECT ... FOR UPDATE - 避免在事务中做 HTTP 调用、日志写入等耗时操作,否则锁持有时间过长
如何让优惠券支持「指定商品可用」和「满减门槛」组合校验
不能只靠数据库字段硬编码规则,得把条件判断逻辑收口到统一方法里,否则后续加「品类白名单」「会员等级限制」时会散落在各处。
实操建议:
- 定义结构体
CouponRule,字段包含MinAmount、ScopeType(0=全站, 1=指定SKU, 2=指定类目)、ScopeIDs(JSON 字符串存 ID 列表) - 校验时先查订单商品列表,再根据
ScopeType做匹配:全站直接过,指定 SKU 就查ScopeIDs是否包含任一item.SkuID - 满减门槛用
order.TotalAmount - order.DiscountAmount >= rule.MinAmount,注意要扣除已使用的其他优惠,否则可能误判 - 别在 Controller 层做规则解析,抽成 service 方法,输入
*models.Order和*models.Coupon,返回bool, error
用户领券并发高,怎么防止重复领取同一张券
靠应用层锁(如 sync.Mutex)没用,部署多实例时锁不共享;靠数据库唯一索引又太重——用户可能有多个领券入口(活动页、分享链接、后台发放),需要更灵活的控制粒度。
实操建议:
- 建联合唯一索引:
CREATE UNIQUE INDEX idx_user_coupon ON user_coupon (user_id, coupon_id, source_type),source_type区分「活动领取」、「邀请奖励」等场景 - 插入前不预查,直接
o.Insert(&userCoupon),捕获 Beego 的ErrDBDuplicateEntry错误(MySQL 返回 1062) - 错误处理别直接返回「已领取」,要查一下
user_coupon表确认是否真存在,避免因网络重试导致插入失败但实际已成功 - Redis 可作为二级防护:领券前
SETNX user:123:coupon:456 1 EX 60,但不能替代数据库唯一约束,只是降载
Beego ORM 查询优惠券列表时性能突然变差
常见于加了多表 JOIN 或模糊搜索后,比如查「用户可用的、未过期的、名称含“双11”的优惠券」,ORM 生成的 SQL 缺乏索引支持,单次查询从 20ms 涨到 2s。
实操建议:
- 给高频查询字段建复合索引,例如
CREATE INDEX idx_coupon_status_expired_on ON coupon (status, expired_at),WHERE 条件顺序必须和索引字段顺序一致 - 避免在 WHERE 中对字段做函数操作:
WHERE DATE(expired_at) = '2024-06-01'会让索引失效,改用WHERE expired_at >= '2024-06-01 00:00:00' AND expired_at < '2024-06-02 00:00:00' - Beego 的
RelatedSel()容易触发 N+1 查询,查用户券列表时,别用o.LoadRelated(&user, "Coupons"),改用原生 SQL 或预加载QueryTable("user_coupon").Filter("user_id", uid).RelatedSel("Coupon") - 上线前用
EXPLAIN看执行计划,特别留意type是否为ALL(全表扫描)
优惠券系统最难的不是功能实现,而是各种边界条件下的一致性保障——比如退款时要不要返还优惠券、用户注销后券记录怎么归档、跨天结算时过期时间按服务端还是客户端时间。这些点 Beego 不提供现成方案,得在业务代码里一处处对齐。


















