必须用update ... where stock > 0 + affected_rows校验实现原子扣减,而非php层判断或单纯事务;同时结合唯一索引防重领、状态字段标识与数据库为唯一可信源。

优惠券发放时怎么避免超发?
TP6.0 本身不带分布式锁,直接用 Db::transaction() 在高并发下会失效——比如库存为 1,两个请求同时查到“还有余量”,都进事务并扣减,结果发出去 2 张。
必须加一层原子性校验。推荐在数据库层面用 UPDATE ... WHERE stock > 0 AND id = ?,然后检查 affectedRows 是否为 1:
$result = Db::name('coupon')->where([
'id' => $couponId,
'stock' => ['gt', 0]
])->dec('stock', 1)->execute();
if (!$result) {
throw new Exception('优惠券已抢光');
}
不要依赖 PHP 层的 if 判断再 update;也不要只靠事务隔离级别(即使 RR 级别,在非主键查询 + 并发更新时仍可能幻读)。
规则引擎怎么动态匹配用户和优惠券?
TP6.0 没有内置规则引擎,硬编码 if-else 易失控。建议把规则条件存成 JSON 字段,如:
"conditions": {
"min_order_amount": 100,
"user_level": ["vip", "svip"],
"category_ids": [101, 102],
"valid_days": 7
}
核销前用 PHP 做轻量级校验:
-
$order->amount >= $coupon->min_order_amount -
in_array($user->level, $coupon->user_level) -
array_intersect($order->cate_ids, $coupon->category_ids)不为空
别把规则编译成表达式或引入 Drools 这类重型方案——营销场景规则变动频繁,但复杂度其实有限,JSON + 简单 PHP 判断更可控、易调试。
核销时怎么防止重复使用或跨用户盗用?
核心是三重校验,缺一不可:
-
status == 'unused'(状态字段必须是数据库里查出来的,不能只信前端传的 ID) -
user_id == $currentUserId(必须关联到当前登录用户,不能只验证 coupon_id 存在) -
use_time IS NULL(防止有人篡改时间字段绕过过期逻辑)
更新语句务必带上全部约束:
Db::name('coupon')->where([
'id' => $couponId,
'user_id' => $userId,
'status' => 'unused',
'use_time' => null,
'expire_time' => ['gt', time()]
])->update(['status' => 'used', 'use_time' => time()]);
如果 affectedRows 不为 1,说明已被核销、过期或不属于该用户——直接报错,不给任何提示细节(避免信息泄露)。
Redis 缓存券码时要注意什么?
缓存券码(比如兑换码、短信验证码类券)常用 setex,但有两个坑:
- 过期时间必须和数据库里的
expire_time严格对齐,否则出现“缓存还没过期但 DB 已失效”的脏状态 - 不要用
INCR或GETSET做原子核销,TP6 的Cache::store('redis')默认不支持 Lua 脚本,decr返回值可能被并发覆盖
稳妥做法:缓存只存状态快照(如 coupon:1001:status → unused),真正核销仍走 DB 更新 + 检查影响行数;缓存仅用于快速拦截明显非法请求(比如查不到缓存就直接拒掉),不参与核心一致性逻辑。
真实并发场景下,DB 是唯一可信源,缓存只是加速层——这点容易被忽略,尤其当运营要求“秒级生效”时,反而更容易往缓存里堆逻辑。











