必须用 orm.runtransaction() 手动控制事务,先校验模板状态与时间、用户领取上限,再在事务中更新 used_quantity 并插入 user_coupons 记录,避免超发。

Beego 中怎么实现优惠券发放的原子性校验
直接在 Controller 层做库存扣减 + 用户领券插入,不加事务或锁,大概率出现超发。Beego 本身不内置数据库事务管理,得靠 orm.RunTransaction() 或原生 sql.Tx 手动控制。
推荐用 beego 的 ORM 封装事务,关键点有三个:必须用同一个 orm.Ormer 实例、所有操作在闭包内完成、失败时 panic 触发回滚。
- 先查
coupon_templates表确认status = 1、start_time ≤ now()、end_time ≥ now()、total_quantity > used_quantity - 再查
user_coupons表统计该用户已领数量:WHERE template_id = ? AND user_id = ? AND status != 2,对比usage_limit_per_user - 最后在
orm.RunTransaction()里执行:更新模板表的used_quantity(+1),同时插入新记录到user_coupons
注意:Beego ORM 的 Update() 默认不带 WHERE 条件,务必显式传入 cols 和条件 map,否则可能误更新整张表。
Beego 路由怎么区分满减券和折扣券的发放入口
别用一个接口加 type 参数硬判断,容易让路由语义模糊、权限难收敛。应该按业务动作拆成独立路由,比如:
-
POST /v1/coupons/full-reduction/receive→ 满减券领取 -
POST /v1/coupons/discount/receive→ 折扣券领取
这样做的好处是:中间件可针对不同路径配置不同限流策略(比如折扣券更稀缺,QPS 限制更严);日志打点能天然区分类型;后续加灰度开关也只需改路由级配置,不用侵入业务逻辑。
在 router.go 中注册时,建议用 beego.Router() 而非 beego.AutoRouter(),避免因 Controller 方法名变化导致路由错位。例如:
beego.Router("/v1/coupons/full-reduction/receive", &CouponController{}, "post:ReceiveFullReduction")
为什么 Beego 的缓存不能直接缓存 user_coupons 查询结果
因为领券是强一致性场景,缓存过期窗口内可能出现“显示已领,实际还能再领一张”的问题。Redis 缓存适合放模板配置(coupon_templates)、活动状态(status 字段)、适用商品列表这类读多写少且允许短时 stale 的数据。
对 user_coupons 这类用户维度高频写、需精确计数的表,要么不缓存,要么用带版本号的缓存(如把 user_id:template_id 作为 key,value 包含 count 和 version),每次更新后主动 del 对应 key。
- 错误做法:
Cache.Get("user_coupons_" + userId)缓存整个用户所有券列表 —— 数据膨胀快、更新成本高 - 正确做法:
Cache.Incr("user_coupon_count_" + userId + "_" + templateId)计数器 + DB 主键查询兜底
Beego 自带的 cache 模块默认使用 memory 驱动,上线前必须显式切换为 redis 驱动,否则多实例部署时缓存不同步。
Beego 日志里怎么记录优惠券发放失败的真实原因
很多开发者只记 "receive failed",但排查时根本不知道是库存不足、用户已达上限,还是数据库连接超时。应该把关键判定变量打全,至少包含:template_id、user_id、stock_left、user_received_count、limit_per_user。
示例写法(用 beego logs 模块):
logs.Error("coupon receive fail: template_id=%d, user_id=%d, stock_left=%d, user_count=%d, limit=%d",
templateId, userId, stockLeft, userCount, limitPerUser)
特别注意:不要在日志里拼接敏感字段如手机号、open_id,要用脱敏格式(如 138****1234)。Beego 的 logs.Async(true) 开启异步写日志,避免 IO 阻塞请求。
发放失败响应体里,前端只返回通用错误码(如 ERR_COUPON_EXHAUSTED),具体原因不透出,防止被恶意探测库存。











