简介:PHP九宫格抽奖系统源码面向网站开发人员或运营人员,用于快速搭建营销抽奖、粉丝互动等场景。系统由PHP后端与前端页面组成,后台可自定义奖品名称、抽奖概率与库存数量,数量为0的奖品不会出现在中奖结果;并提供抽奖码验证机制,用户须凭有效抽奖码才能参与,抽奖码可随机生成或绑定指定奖品,有效控制活动成本。资源共710个文件,压缩包大小约17.66MB,主要包含PHP业务逻辑、HTML/CSS/JS前端脚本、GIF/PNG图片素材、SQL数据库文件及少量音频文件,结构清晰,后台功能模块完整,便于二次开发时定位与修改。后台还支持自定义抽奖背景、背景音乐和虚假购买人数,增强活动氛围。目前已有73人学习/下载,适合需要快速上线抽奖活动或希望二次开发完善批量抽奖码功能的开发者。
1. 九宫格抽奖不是转盘,PHP 实现前先想清楚这三件事
市面上凡是带“PHP九宫格抽奖系统源码”关键词的交付物,绝大多数是给你一套可以用的抽奖页面加后台管理,核心就三块:九宫格界面渲染、抽奖概率控制、中奖记录与库存扣减。很多人拿到源码第一件事是改奖品名和图片,结果上线第二天就出问题——要么概率对不上,要么同一个人刷爆奖品,要么并发一高就超发。问题不在 PHP 本身,而在抽奖系统的设计逻辑。
做九宫格抽奖,首先要区分“前端动画”和“后端抽奖”。九宫格的转动动画只是展示层,真正决定用户中什么奖的,是服务端生成的一个中奖结果。前端转了 3 圈停在某个格子,那是根据后端返回的奖品 ID 反推出来的动画路径,绝不是随机停在哪个格子就算哪个奖品。理解这一点,后面所有代码才能写对。
再就是概率模型。九宫格一般有 8 个奖品位加 1 个“谢谢参与”,但概率不是平均分配的。有的奖品要控制中奖率在 5% 以内,有的要 30%,还要保证所有奖品的中奖率加起来不超过 100%。这需要一个可靠的权重算法,而不是用array_rand或mt_rand(1, 8)直接完事。第三个要提前定的是库存维度:每个奖品有独立库存,抽完就没了。你需要在一次抽奖请求内完成“判断剩余次数 → 计算中奖结果 → 扣减库存 → 写入记录”这四步,任何一步出问题都会造成超发或数据不一致。
这套系统的目标用户很明确:做营销活动的运营人员、接外包的 PHP 开发、以及想在自己业务里快速嵌入抽奖模块的产品团队。适合做的场景是微信内 H5 活动、电商节庆营销、会员积分消耗类玩法。不适合做的场景是现金红包、高价值实物抽奖且没有第三方公证或风控的情况——那对防刷和审计的要求远超一套普通源码的承载范围。下面按实际落地顺序逐步拆解。
2. 核心抽奖逻辑与概率控制:先用权重算法撑住中奖率
2.1 九宫格抽奖的两种概率模型,别混着用
抽奖系统的概率模型有两大流派:固定概率和剩余库存概率。PHP 九宫格抽奖源码里最常见的是固定概率,也就是每个奖品在配置表里写死一个权重值,比如 5、10、20,表示相对中奖可能性。这个模型实现简单,适合预算可控、奖品数量充足的场景。
但实际做营销活动时,固定概率会有个尴尬局面:某个奖品库存只剩最后 5 份,但权重还是 20,于是这 5 份可能在前 1 小时内被抽完,后 23 小时这个奖品的位置一直是空的但概率还占着。用户转到一个空奖品格,体验很差。所以很多商业源码会在固定概率基础上叠加一个“剩余库存检查”,一旦库存为 0,就把该奖品从抽奖池里摘除,把它的权重让渡给“谢谢参与”或其它奖品。
我的建议是:如果你是把这套源码用在真实活动上,直接用“库存感知 + 动态权重”模型。它只比固定概率多十来行代码,但能避免大量售后问题。下面给出的实现也是基于这个模型。
2.2 PHP 实现动态权重抽奖:代码与参数说明
先定义一个标准的奖品数组结构,包含奖品 ID、名称、权重、库存。这是一个最小可跑的配置:
$prizes = [ ['id' => 1, 'name' => 'iPhone 15', 'weight' => 5, 'stock' => 3], ['id' => 2, 'name' => '蓝牙耳机', 'weight' => 10, 'stock' => 20], ['id' => 3, 'name' => '优惠券 10元', 'weight' => 30, 'stock' => 500], ['id' => 4, 'name' => '再来一次', 'weight' => 15, 'stock' => 100], ['id' => 5, 'name' => '积分 50', 'weight' => 20, 'stock' => 1000], ['id' => 6, 'name' => '帆布袋', 'weight' => 10, 'stock' => 50], ['id' => 7, 'name' => '台历', 'weight' => 8, 'stock' => 80], ['id' => 8, 'name' => '鼠标垫', 'weight' => 12, 'stock' => 120], ['id' => 0, 'name' => '谢谢参与', 'weight' => 30, 'stock' => PHP_INT_MAX], ];这里注意 ID 为 0 的“谢谢参与”库存设为 PHP_INT_MAX,表示它永远可抽。接下来写抽奖核心函数:
function drawPrize(array $prizes): array { // 第一步:过滤已无库存的奖品,并从抽奖池中移除 $pool = array_filter($prizes, function ($item) { return $item['stock'] > 0; }); // 第二步:计算总权重 $totalWeight = 0; foreach ($pool as $item) { $totalWeight += $item['weight']; } // 第三步:生成随机数并分段判断 $rand = mt_rand(1, $totalWeight); $cursor = 0; foreach ($pool as $item) { $cursor += $item['weight']; if ($rand <= $cursor) { return $item; } } // 理论上不会走到这里,但为防御返回谢谢参与 return $prizes[0]; }这段代码分三步走。第一步用array_filter把无库存的奖品剔除,这样权重就不会被死奖品占用。第二步累加所有剩余奖品权重,得到总权重区间。第三步生成一个 1 到总权重之间的随机整数,然后逐个累加权重区间判断落点。这个算法的核心是把每个奖品的权重映射到一段连续区间上,随机数落在哪个区间就中哪个奖。
关于随机数函数的选择:老代码里常见rand(),但 PHP 7 以后mt_rand()的随机性更好,PHP 8 里rand()与mt_rand()已经是同一种实现了。如果你的 PHP 版本默认启用了random_int,对安全要求高的场景建议用random_int(1, $totalWeight)替代mt_rand,它基于 CSPRNG,不可预测性更强,也更能防止用户通过多次抽奖统计推算随机规律。
2.3 概率配比经验:让总概率不超 100% 的调参方法
动态权重算法的特点是概率等于权重除以当前总权重。所以配置权重时,你要先定好目标概率再反推权重值。比如想让 iPhone 中奖率接近 5%,总权重设计在 200 左右,那 iPhone 权重就设为 10,其它所有奖品之和为 190。实际计算时还要注意:库存越少的奖品,在它耗尽之前被抽中的概率随时间推移反而变高——因为其它奖品可能先耗尽被移除,而它自己还没被抽中。这是一个容易被忽视的特性。
刚才说“活动预算可控”指的是你要在配置权重时预判最坏情况。举例:总权重 200,iPhone 占比 5%,活动预计 1 万人次参与,那么理论上会有约 500 次命中 iPhone 的概率事件。如果 iPhone 库存只有 3 台,那么它很可能会在活动开始后不久就被抽完。一旦库存归零,剩余 497 次“本该命中 iPhone”的随机机会会被其它奖品吸收,表现为其它奖品的中奖率上升。
实际操作中,我一般会建一个权重配置表,字段包括prize_id, weight, stock, daily_limit,然后写一个drawPrize的单元测试脚本,循环调用 10 万次,统计每个奖品的实际中奖次数与理论概率的偏差。偏差在 1% 以内就可以接受。不要把权重调完直接上线,这种玄学在抽奖系统上最容易翻车。
3. 抽奖接口与九宫格动画联动:从后端结果到前端转盘
3.1 前端旋转结果必须由后端驱动,否则用户能摸出规律
很多初版九宫格源码的做法是前端先转,转完再把最终格子 ID 发给后端记录。这个顺序完全错误。如果前端能决定停在哪一格,懂技术的人可以直接改 JS 变量或拦截接口响应,把结果改成任意奖品。正确顺序是:
- 用户点击抽奖按钮
- 前端把用户标识 + 本次抽奖的令牌发给后端
- 后端校验资格、执行抽奖、扣库存、写记录
- 后端把中奖奖品 ID 返回给前端
- 前端根据奖品 ID 计算目标格子的角度和圈数,执行转盘动画
这样无论用户怎么改前端代码,最终结果都由后端兜底。哪怕他伪造接口返回,数据库里的记录还是后端生成的那个结果,不会产生真实奖品损失——除非他伪造的是“再调一次接口”。
3.2 PHP 抽奖接口:一次请求内完成资格校验、抽奖、扣库存、落库
接口文件按常见 MVC 结构放在controller层,实际逻辑写入服务类。这里给出一个精简可用的接口主体,在一个事务里完成关键步骤:
public function lottery(Request $request) { $userId = $request->post('user_id'); $actId = $request->post('act_id'); // 1. 校验活动状态和用户剩余抽奖次数 $act = ActModel::find($actId); if (!$act || $act->status != 1) { return json(['code' => 1, 'msg' => '活动未开启']); } $remainTimes = $this->checkRemainTimes($userId, $actId); if ($remainTimes <= 0) { return json(['code' => 2, 'msg' => '抽奖次数已用完']); } // 2. 加锁防止并发重复请求,这里用 Redis 原子自增 $lockKey = "lottery:act:{$actId}:user:{$userId}"; $lock = Redis::set($lockKey, 1, 'EX', 10, 'NX'); if (!$lock) { return json(['code' => 3, 'msg' => '操作太频繁']); } try { // 3. 事务内抽奖:读取库存、计算奖品、扣库存、写记录 $pdo->beginTransaction(); $prizes = PrizeModel::where('act_id', $actId)->get()->toArray(); $winner = drawPrize($prizes); if ($winner['id'] != 0) { $affected = PrizeModel::where('id', $winner['id']) ->where('stock', '>', 0) ->decrement('stock'); if (!$affected) { throw new Exception('库存扣减失败'); } } $pdo->commit(); // 4. 写抽奖记录,异步或同步均可 $this->writeLotteryRecord($userId, $actId, $winner['id']); // 5. 延迟释放锁,防止请求结束后立即重放 Redis::del($lockKey); return json(['code' => 0, 'data' => [ 'prize_id' => $winner['id'], 'prize_name' => $winner['name'] ]]); } catch (Exception $e) { $pdo->rollBack(); Redis::del($lockKey); return json(['code' => 4, 'msg' => '系统繁忙']); } }这段代码里有两个关键点值得说明。第一是 Redis 锁只用来防同一用户短时间内的重复提交,不是分布式锁的全量方案——真正的超卖防护靠的是decrement语句本身带where('stock', '>', 0)条件。这一步是原子操作,即便并发请求同时进来,数据库行锁也会保证只有库存大于 0 的那个请求能扣减成功。第二是先扣库存再写记录。库存是硬资源,记录只是流水,如果反过来写,一旦扣库存失败就会出现一条记录对应不到库存消耗的脏数据。
需要注意的细节是:drawPrize在事务里读取的是当前库存状态,但 PHP-FPM 模式下多个进程同时读同一个奖品数组,各自执行完抽奖后统一到decrement这一步靠数据库兜底。这意味着可能出现 A 进程和 B 进程都算出中了同一个奖品,但只有一个能成功扣库存。A 成功返回 iPhone,B 扣减失败抛出异常,用户看到的是“系统繁忙”。从数据一致性角度这没毛病,但用户体验差。优化办法是在读取库存前先用SELECT ... FOR UPDATE锁住奖品表的相关行,保证一次只有一个请求能读到当前库存。小活动可以不锁,但高并发场景建议加锁。
3.3 九宫格动画角度计算:把奖品 ID 映射到格子位置
前端拿到prize_id后,需要把它转换成第几个格子。九宫格布局顺序一般是从左上角开始顺时针编号,共 8 个格子加中间 1 个按钮位。中间按钮不参与中奖,所以奖品只能落在 8 个外围格子中的某一个。
// 九宫格顺时针索引的格子角度映射 const gridAngles = { 1: 0, // 上方正中 2: 45, // 右上角 3: 90, // 右侧正中 4: 135, // 右下角 5: 180, // 下方正中 6: 225, // 左下角 7: 270, // 左侧正中 8: 315 // 左上角 }; function rotatePointer(targetGridIndex, targetAngle) { const pointer = document.getElementById('pointer'); const currentRotation = getCurrentRotation(pointer); // 转盘至少转 5 圈,再到达目标角度 const fullSpins = 360 * 5; const targetFinal = fullSpins + targetAngle; const nextRotation = currentRotation + 360 - (currentRotation % 360) + targetFinal; pointer.style.transition = 'transform 4s cubic-bezier(0.2, 0.8, 0.2, 1)'; pointer.style.transform = `rotate(${nextRotation}deg)`; }这段 JS 的逻辑核心是计算“当前旋转角度 + 至少 5 圈 + 目标角度”的最终值。getCurrentRotation从元素的transform矩阵里解析出当前角度,避免下次抽奖时从零开始转,造成指针倒退的违和感。cubic-bezier(0.2, 0.8, 0.2, 1)是减速曲线,让转盘在最后 1 秒缓缓停在目标格子上,观感更接近物理惯性。这里的格子索引约定要和后端奖品表里的grid_index字段保持一致,否则会出现数据中 iPhone、动画指到优惠券的尴尬局面。
常见做法是把奖品表和格子索引绑定:后端返回prize_id时同时返回grid_index,前端不再自己维护映射关系。这样后端调整格子顺序时,前端不用改代码。别问我为什么强调这一点——很多外包项目就是前端把格子写死,后端一改奖品顺序,整个转盘指向全乱。
4. 数据库设计与高并发扣减:别让库存超发成为事故现场
4.1 奖项配置表、抽奖记录表、用户次数表的结构怎么定
一个能支撑真实活动的抽奖系统,至少要三张表:活动表、奖品表、抽奖记录表。抽奖次数可以放到用户表或单独的次数流水表。这是最常见的结构,大部分 PHP 九宫格抽奖系统源码也沿用这个设计,差别在于字段细节和索引。
CREATE TABLE `act_prize` ( `id` int unsigned NOT NULL AUTO_INCREMENT, `act_id` int unsigned NOT NULL DEFAULT 0 COMMENT '活动ID', `prize_name` varchar(64) NOT NULL DEFAULT '' COMMENT '奖品名称', `weight` int unsigned NOT NULL DEFAULT 0 COMMENT '权重', `stock` int unsigned NOT NULL DEFAULT 0 COMMENT '总库存', `remain_stock` int unsigned NOT NULL DEFAULT 0 COMMENT '剩余库存', `grid_index` tinyint unsigned NOT NULL DEFAULT 0 COMMENT '九宫格位置 1-8', `start_time` datetime DEFAULT NULL COMMENT '可抽开始时间', `end_time` datetime DEFAULT NULL COMMENT '可抽结束时间', PRIMARY KEY (`id`), KEY `idx_act_id` (`act_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `act_lottery_record` ( `id` bigint unsigned NOT NULL AUTO_INCREMENT, `act_id` int unsigned NOT NULL DEFAULT 0, `user_id` int unsigned NOT NULL DEFAULT 0, `prize_id` int unsigned NOT NULL DEFAULT 0, `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_user_act` (`user_id`, `act_id`), KEY `idx_act_time` (`act_id`, `create_time`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;注意奖品表里我设计了stock和remain_stock两个字段。stock是初始库存,用于后台展示“总共几个”;remain_stock是实际扣减字段。如果你只有一个字段,后台想改初始库存或复盘时就没有基准数可对照。扣减时使用UPDATE act_prize SET remain_stock = remain_stock - 1 WHERE id = ? AND remain_stock > 0,这样可以在 SQL 层面杜绝超卖。
抽奖记录表必须建立(user_id, act_id)联合索引,这是查询用户剩余次数、判断是否已中奖的最常用路径。(act_id, create_time)索引用于后台按活动维度导出中奖名单。没有这两个索引,数据量到十万级后查询会明显变慢——这在营销活动结束后导出报表时特别明显。
4.2 并发扣库存的三种方案,PHP 场景该怎么选
方案一是数据库原子更新,就是上面写的UPDATE ... WHERE remain_stock > 0。这是最稳妥的兜底方案,缺点是行锁在高并发下会串行化,单表支撑每秒几百次扣减没问题,几千次就开始吃力。方案二是 Redis 预扣库存,所有扣减走 Redis 的DECR命令,异步同步到数据库。这个方案吞吐量高,但会出现 Redis 和数据库库存不一致的风险,比如 Redis 扣了 10、数据库只扣了 5,因为同步脚本挂了。方案三是 Redis 原子操作 + 数据库最终一致,适合大型活动,普通业务用不上。
PHP 项目天然是请求结束后进程销毁的模型,所以方案二是最容易翻车的。我见过一个项目用 Redis 扣库存,结果活动结束后对账发现少发了几十件奖品——原因是某个用户抽中后主动取消订单或退款,但 Redis 里的扣减没有回滚。所以小规模活动直接用数据库原子扣减最靠谱,配合事务能保证绝不超发。如果确实并发很高,可以再加一层 Redis 令牌桶限流,把瞬时击穿的压力挡在业务层之外,而不是用 Redis 直接替代数据库库存。
// Redis 预扣库存 + 数据库最终扣减的折中方案 $redis = Redis::connection(); $remain = $redis->decr("act:stock:{$prizeId}"); if ($remain < 0) { $redis->incr("act:stock:{$prizeId}"); // 回补 throw new Exception('该奖品已抽完'); } // 异步写入数据库扣减记录,或用消息队列处理 Queue::push(new DeductStockJob($prizeId));这个折中方案适合有一定开发能力的团队使用:Redis 负责流量削峰,异步任务负责最终扣减数据库库存。但注意,如果异步任务失败且没有补偿机制,库存还是会漂移。所以无论选哪种方案,都必须有定时对账脚本,定期把 Redis 扣减总数与数据库扣减总数做比对,发现不一致就告警。
4.3 抽奖次数的正确扣法:先减次数还是先抽奖?
这是整个系统里最容易逻辑颠倒的地方。必须先扣次数再抽奖,还是先抽奖再扣次数?答案是:在同一事务里,先锁定用户次数记录,再抽奖,再一起提交。顺序上“先扣次数”更安全,因为如果抽奖成功但次数扣减失败,用户会无限抽;反过来,次数扣了但抽奖异常,用户会投诉。
$pdo->beginTransaction(); try { // 锁定用户次数记录 $userTimes = UserTimesModel::where('user_id', $userId) ->where('act_id', $actId) ->lockForUpdate() ->first(); if (!$userTimes || $userTimes->remain_times <= 0) { $pdo->rollBack(); return json(['code' => 2, 'msg' => '抽奖次数已用完']); } // 扣减次数 $userTimes->remain_times -= 1; $userTimes->save(); // 抽奖 $winner = drawPrize($prizeList); // 扣库存、写记录... $pdo->commit(); } catch (Exception $e) { $pdo->rollBack(); }lockForUpdate()是这里的关键——它会对该用户的行加排他锁,保证同一时刻只有一个请求能读到最新的剩余次数。不加锁的话,两个并发请求同时读到剩余次数为 1,都执行扣减,结果用户实际抽了 2 次,但数据库只扣了 1 次。这个问题在高并发下几乎必现,而且很难从日志里排查,因为单看每次请求都“正常”。用lockForUpdate后,并发请求会排队执行,性能损耗可接受,但数据一致性有保障。
5. 避坑与排查:PHP 九宫格抽奖系统常见的 5 个翻车点
5.1 中奖结果和格子对不上
现象:后台配置的奖品在九宫格的第 3 格,但用户转到的位置是第 5 格,中奖记录却记录为第 3 格的奖品。原因:格子编号约定不统一。前端按从左到右从上到下编号,后端按顺时针编号,或者两者都用了顺时针但起始位置不同(从左上角开始 vs 从正上方开始)。解决:把grid_index定义在奖品表里,以后端数据为准,前端动态读取这个字段计算角度。后端修改格子顺序时,前端代码不用动。
5.2 库存显示为 0 但还能抽中
现象:后台奖品库存显示剩余 0,但用户仍然抽到了这个奖品。原因:抽奖算法里的array_filter过滤了库存为 0 的奖品,但库存字段读取的是缓存值,不是数据库实时值。常见于用了 Redis 缓存奖品列表且没有在扣减后同步更新缓存。解决:扣减库存后立即刷新缓存,或直接不缓存库存字段——库存走数据库实时读取,奖品基础配置走缓存。拆开缓存粒度,这个坑自然消失。
5.3 同一用户并发请求抽了多次
现象:用户快速连点抽奖按钮,后端收到多个请求,用户剩余次数被扣除多次。原因:前端只做了按钮 disabled 防点,没做后端幂等控制;后端也缺少用户级别的锁。解决:在接口层加用户 + 活动的 Redis 锁,锁存在期间拒绝重复请求。锁的过期时间设为 10 秒,正常情况下一次抽奖请求 500ms 内结束,不会误伤正常用户。前端也要在收到响应后才解除按钮禁用,不能等动画结束就解锁——动画 4 秒,接口 0.5 秒,用户可能在动画期间再次点击。
5.4 超卖导致发不出奖品
现象:活动结束后统计,中奖记录比实际库存多出若干条。原因:抽奖算法先读库存再扣库存,两个步骤之间存在时间窗口,并发请求下多个进程同时读到库存为 1,各自判断“可以中奖”,然后都执行了扣减。解决:把扣减条件带上remain_stock > 0,并且用UPDATE返回的受影响行数判断是否真正扣成功。受影响行数为 0 说明库存已经被其它请求扣没了,此时重新抽奖或直接返回失败。注意,商品表要用 InnoDB,不能用 MyISAM,后者不支持行锁。
5.5 概率设置总和超过 100% 但系统不报错
现象:运营配置权重后,感觉一等奖总是不出,而“谢谢参与”占比过高。原因:运营把“权重”理解成“百分比”,100 分填了 30 又填 20,最后各项总和超过 100 也没有校验。权重模型的正确理解是相对比例,不是百分数。解决:后台配置页加一个实时计算条——总权重是多少,每个奖品当前折算概率是多少,超过 100% 时自动按比例归一化。如果后台没有这个功能,就自己在配置表上写个校验脚本,所有奖品权重加起来除以总权重,超过 100% 就报警。这是我个人在交付抽奖源码时必须补的一个功能,能省掉一半以上的运营沟通成本。
6. 验证抽奖系统可靠性的三个手段:从概率自测到生产模拟
抽奖系统上线前最重要的一步是验证两个指标:概率是否符合预期、高并发下是否不超卖。第一个用批量模拟,第二个用并发压测。先说概率验证。
// 模拟 20 万次抽奖统计中奖分布 $prizes = include 'prize_config.php'; $stats = []; for ($i = 0; $i < 200000; $i++) { $result = drawPrize($prizes); $stats[$result['name']] = ($stats[$result['name']] ?? 0) + 1; } foreach ($stats as $name => $count) { printf("%s: %.4f%%\n", $name, $count / 200000 * 100); }这段脚本跑完后,把每个奖品的实际概率和期望概率对比,偏差超过 1% 就需要检查权重计算逻辑。注意一个细节:模拟时要用与生产环境相同的 PHP 版本和随机数函数,不同 PHP 版本的mt_rand实现细节有差异,虽然在统计学上影响极小,但既然做验证就做到位。
并发压测用ab或wrk就行,不必上复杂工具。压测目标是:200 并发持续 30 秒,总请求 6000 次,活动配置 3 个奖品各有库存 100,验证最终数据库剩余库存不为负数,中奖记录条数与扣减总数一致。
ab -n 6000 -c 200 -p post_data.txt -T application/x-www-form-urlencoded http://your-domain.com/api/lottery压测结束后执行这条对账 SQL,检查是否有中奖记录数超过扣减数的异常:
SELECT p.id, p.remain_stock, COUNT(r.id) AS record_count, (p.stock - p.remain_stock) AS deducted_count FROM act_prize p LEFT JOIN act_lottery_record r ON r.prize_id = p.id GROUP BY p.id HAVING deducted_count != record_count;任何一行结果里deducted_count不等于record_count,都说明扣减逻辑有问题,绝对不能上线。常见的情况是deducted_count大于record_count——库存扣了但记录没写;另一种是record_count大于deducted_count——记录写了但库存没扣。前者发生在事务提交前进程退出,后者发生在写记录的代码放在事务之外。我的处理习惯是把库存扣减和记录写入放在同一个事务里,事务结束后再返回结果,这样就不会出现两侧不一致。
最后一个技巧是给抽奖接口加一个测试模式开关。在配置表里加一个test_mode字段,开启后 drawPrize 函数不读权重、直接按奖品 ID 顺序命中,方便前端联调转盘动画。真正的活动上线时关闭这个开关。这个功能虽然简单,但在多轮联调和测试中能节省大量时间,比反复清数据库调权重高效得多。希望这些方法能帮你把九宫格抽奖做得少踩坑。
本文还有配套的精品资源,点击获取