
简介这是一套基于PHP与MySQL开发的大转盘抽奖系统源码面向需要快速搭建营销抽奖活动的网站开发者或企业站长解决活动运营中奖品管理、卡密发放与中奖概率控制等核心问题。资源包共15个文件包含13个PHP功能脚本、1个Markdown说明文档及1张封面图整体压缩包大小约20.52MB目录结构涵盖前台抽奖页、后台管理、安装向导与配置示例等模块。系统提供一键安装向导后台需账号密码登录支持虚拟卡密和实物奖品独立库存设置前台必须输入有效卡密才能抽奖后台可批量生成卡密并绑定特定奖品实现必中还支持收货地址填写、发货标记及卡密CSV导出满足常见抽奖活动全流程管理需求。目前已有38人学习下载适合有一定PHP基础、希望二次开发或直接部署抽奖活动的开发者参考使用。1. 幸运大转盘上线前夜这套PHP源码先解决概率和发货的信任问题活动运营丢过来一句话周五上线一个抽奖大转盘奖品有手机、优惠券和谢谢参与。这种需求表面是转盘动画真正难的是抽奖逻辑、并发安全、防重复、库存扣减还有后台核对名单。我拆这套PHP幸运大转盘源码的原因就在这里——它把活动页、抽奖接口、管理后台、数据库表全做齐了部署完就能当一个小活动系统用。适合想省事的运营、PHP初中级开发做二开也适合拿来当毕业设计和课程设计的完整工程参考。下面我先把它的设计与实现拆给你看再讲落地配置和坑。2. 抽奖核心设计权重区间、库存扣减与用户状态的三角关系2.1 权重区间抽奖为什么不能直接用rand()%100很多初版写法是$rand rand(1,100)小于等于10就一等奖小于等于30就二等奖。问题在于奖品一旦删掉一个、或者权重调整成小数边界维护就全乱套。这套源码用权重区间方式每个奖品配一个rate权重全部奖品rate求和然后mt_rand(1,sum)落在哪个区间就中哪个。未中奖也作为一个档位参与rate设成剩余概率界面显示谢谢参与。?php /** * 按权重区间抽奖 * $prizeList: 一维数组每个元素含 pid/name/rate/stock * rate 用整数权重支持千分比/万分比 */ function drawByWeight($prizeList) { $totalRate 0; foreach ($prizeList as $item) { $totalRate (int)$item[rate]; } $randPoint mt_rand(1, $totalRate); $cursor 0; foreach ($prizeList as $item) { $cursor (int)$item[rate]; if ($randPoint $cursor) { return $item; } } return null; }逻辑说明先把所有权重求和生成随机点再从左往右累加区间第一个覆盖随机点的奖品就是结果。参数说明rate取整数如果按百分比配置五个奖品概率加起来要等于100如果按万分比把谢谢参与档的rate设成10000减去其他奖品权重。这样调整某个奖品的rate只改一份数据不用改代码。进一步说把未中奖当成一个档位处理比单独判断是否中奖更统一——你只需要维护一张奖品表所有中奖结果都走同一个查找函数。2.2 库存不足的奖品如何消失抽奖前的可用列表过滤只算权重还不够一等奖库存为0时你不可能继续按原概率抽到它。常规做法是每次抽奖前从数据库把stock大于0的奖品查出来剔除无库存奖品后再按剩余权重计算。这让每个奖品实时中奖率随库存变化更符合运营直觉也避免显示有一等奖但永远抽不到的投诉。?php // 取可用奖品列表 $sql SELECT pid, name, rate, stock FROM tp_prize WHERE activity_id ? AND stock 0 AND status 1 ORDER BY sort_order ASC; $stmt $pdo-prepare($sql); $stmt-execute([$activityId]); $available $stmt-fetchAll(PDO::FETCH_ASSOC); // 若列表为空说明活动没有可发奖品直接返回未中奖 if (empty($available)) { return [pid 0, name 未中奖]; } $result drawByWeight($available);这里status字段控制上下架sort_order控制转盘上奖品显示顺序。注意过滤后权重总和会缩小未中奖档的权重要不要也同步缩小运营一般希望库存少时中奖率变低所以正确做法是谢谢参与档一直保留在列表里不参与库存过滤但它的rate是动态算出来的10000减去其他奖品权重之和。我一般会在取列表时把未中奖档固定追加进去而不是写死在数据库中这样改活动奖品时不容易漏算概率。2.3 用户状态与防重复token、次数限制、会话存储转盘抽奖和普通接口不同用户刷新一次就可能把一次机会用掉八成。这套系统我见到的方式是三步抽奖前生成token存在session提交抽奖时带token并立刻销毁每次抽奖记录写入lottery_log表按user_id和日期统计次数单IP再做一层上限防止有人换账号刷。?php session_start(); // 生成一次性 token $drawToken md5(uniqid(mt_rand(), true)); $_SESSION[draw_token] $drawToken; // 提交抽奖时校验 if ($_POST[token] ! $_SESSION[draw_token]) { die(json_encode([code 0, msg 非法请求请刷新页面])); } unset($_SESSION[draw_token]); // 当日次数限制 $dayStart date(Y-m-d) . 00:00:00; $countSql SELECT COUNT(*) FROM tp_lottery_log WHERE user_id ? AND create_time ?; $stmt $pdo-prepare($countSql); $stmt-execute([$userId, $dayStart]); if ($stmt-fetchColumn() DAILY_LIMIT_PER_USER) { die(json_encode([code 0, msg 今日抽奖次数已用完])); }token和服务端session绑定销毁时机在收到请求参数后第一时间防止连续提交。次数限制以lottery_log表为准页面刷新不会绕过。单IP限制建议放在更外层的入口比如Nginx的limit_req模块或者PHP里按IP写个计数但别直接用REMOTE_ADDR做严格限制同一公司NAT出口可能把正常用户全挡了我一般把单IP上限放宽到单人次数上限的5倍左右它只用来拦脚本不拦真人。3. 部署与后台配置把源码包跑成一期可运营的活动3.1 环境准备与目录规划这种PHP抽奖源码通常是PHP 5.6~7.x时代的产物配MySQL使用。部署前先确认环境推荐组合Apache 2.4/Nginx 1.18 PHP 7.4 MySQL 5.7。PHP 8.x能不能跑需要先做语法兼容测试老代码常用的花括号数组偏移在PHP 8里会直接报错。源码包解压后目录结构大体如下路径作用关键文件admin/后台管理login.php, prize_list.php, log_list.phpapi/抽奖与用户接口draw.php, prize_list_api.phpincludes/公共函数、配置、数据库连接config.php, functions.php, db.phpassets/前端样式、转盘资源css/, js/, images/install/安装向导install.sql, index.phplottery.sql数据库初始化脚本数据表结构与初始数据部署步骤一句话版本先传到站点目录再导入SQL再改config.php数据库连接最后访问install/初始化管理员账号。注意把install目录在上线前删掉或加访问限制否则别人可以直接访问安装脚本重置你的管理密码。3.2 数据库导入与config配置数据库初始化用命令行和phpMyAdmin都行我习惯命令行因为导入大SQL文件时phpMyAdmin经常超时。mysql -uroot -p -e CREATE DATABASE IF NOT EXISTS lottery_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; mysql -uroot -p lottery_db lottery.sql字符集用utf8mb4比utf8更稳抽奖活动里用户填手机号、昵称带emoji时不会入库报错。lottery.sql导入后再单独建一个业务账号别让站点直连root。接下来改includes/config.php?php define(DB_HOST, 127.0.0.1); define(DB_PORT, 3306); define(DB_NAME, lottery_db); define(DB_USER, lottery_app); define(DB_PASS, 换成你自己的密码); define(DB_CHARSET, utf8mb4); define(ACTIVITY_START, 2025-01-01 00:00:00); define(ACTIVITY_END, 2025-01-31 23:59:59); define(DAILY_LIMIT_PER_USER, 3); define(SINGLE_IP_LIMIT, 20);活动起止时间由服务端判断别依赖前端时间DAILY_LIMIT_PER_USER和SINGLE_IP_LIMIT是运营经常调的两个数调完不用改代码。改完配置后要做一个连接检查用php cli跑最方便php -r require includes/db.php; echo $pdo ? db ok . PHP_EOL : db fail . PHP_EOL;如果这一步报错先看错误信息里是连接超时还是认证失败连接超时多半是DB_HOST写成了localhost而MySQL只监听127.0.0.1认证失败就检查账号权限。3.3 在后台建活动、配奖品、看中奖记录后台登录后第一件事是创建活动活动绑定时间、每人次数、IP限制。第二件事是维护奖品列表每个奖品填名称、图片、权重、库存、发放方式自动发放/线下领取。建议奖品权重用百分比的100倍来填也就是一等奖1%填100谢谢参与50%填5000避免小数导致权重总和算不准。导出中奖名单是高频需求很多后台代码带导出按钮没有的话自己加一个导出接口按活动筛选直接输出CSV?php // 导出中奖记录 CSV header(Content-Type: text/csv; charsetutf-8); header(Content-Disposition: attachment; filenamelottery_ . date(Ymd) . .csv); $out fopen(php://output, w); // 带 BOM防止 Excel 打开乱码 fwrite($out, \xEF\xBB\xBF); fputcsv($out, [ID, 用户名, 手机号, 奖品, 中奖时间, 领奖状态]); // 按活动、中奖筛选取数 $sql SELECT id, user_name, mobile, prize_name, create_time, status FROM tp_lottery_log WHERE prize_id 0 AND activity_id ? ORDER BY id DESC; $stmt $pdo-prepare($sql); $stmt-execute([$activityId]); while ($row $stmt-fetch(PDO::FETCH_ASSOC)) { fputcsv($out, $row); } fclose($out);CSV的BOM是给Excel用的UTF-8编码下不加BOM对方打开就是乱码。status字段在后台要支持改成已发放/未发放发奖后回到log_list.php标记即可。另外导出名单时手机号、用户名尽量做脱敏展示别整表导出发给无关人员活动结束后这份数据也要按公司安全规范定期清理。4. 上线前绕开这五道坎抽奖系统避坑与常见问题排查这几个坑是我在真实活动里踩过的前两个属于没有一个能跑活动的级别后面三个则在做二开时最容易碰到。4.1 概率与展示异常中奖率恒为0、转盘落点错乱现象1转盘正常转但每次都停在谢谢参与后台配置的一等奖10%仿佛不存在。原因1最常见两种。一是库存设置为0抽奖前过滤把一等奖从可用列表里剔除了这时一等奖中奖率实际为0二是权重字段被填成了0.1这种小数PHP用(int)强转后变成0整个权重区间坍塌。解决1后台操作完奖品后用调试脚本打印当前可用列表和各奖品权重确认库存大于0再测。权重字段全部用整数1%填100谢谢参与用10000减去其他权重之和这样能直接比对数。现象2指针指到三等奖用户却收到二等奖用户投诉被黑了。原因2多数是前端先播动画再由动画停止角度去服务器要结果或者服务器先返回结果前端动画落点根据索引重新计算时索引错了。前端拿到的索引和后端配置的奖品顺序不是一个列表。解决2抽奖接口只返回是否中奖和奖品索引前端把索引映射成转盘停靠角度后端把结果存session动画播完取一次。前后端各管一半永远不允许前端自行决定中奖结果。4.2 重复与超发刷新可再抽、并发双中现象1用户抽完奖F5刷新机会数没扣或者转盘又能转。原因1次数扣减只在前端JS里做刷新页面就重置或者后端虽然写了计数但只在抽奖完成时才写token允许同一页面重复提交。解决1服务端是唯一准绳抽奖次数以lottery_log当日记录数为准页面加载时先查剩余次数抽奖接口再加一次性token。token在提交后立即销毁别等业务逻辑跑完再销毁。现象2一等奖库存设置1件开奖瞬间给了两个人。后台看日志两条记录前后相差不到一秒。原因2两个请求同时进来都先SELECT把库存读成1都判断大于0然后都UPDATE扣减。这是典型的竞态条件常规先查后扣写法必现。解决2把查库存和扣库存合并成一条原子UPDATE用受影响行数判断是否抢到?php $pdo-beginTransaction(); $sql UPDATE tp_prize SET stock stock - 1 WHERE pid ? AND stock 0; $stmt $pdo-prepare($sql); $stmt-execute([$pid]); $affected $stmt-rowCount(); if ($affected ! 1) { $pdo-rollBack(); // 没抢到库存按未中奖处理 return false; } // 写入中奖记录 $insertSql INSERT INTO tp_lottery_log (user_id, prize_id, prize_name, create_time) VALUES (?, ?, ?, NOW()); $stmt $pdo-prepare($insertSql); $stmt-execute([$userId, $pid, $prizeName]); $pdo-commit();这里没有用SELECT FOR UPDATE因为条件式中stock 0本身就把判断和修改合并了MySQL行锁足够挡住并发。注意事务里不要执行多余查询避免锁时间拉长扣库存和中奖记录写入必须在同一个事务里否则会出现库存扣了但没记录的情况。4.3 环境兼容PHP 8下老代码直接报错现象部署在PHP 8.3上页面直接Fatal error报错信息类似Array and string offset access syntax with curly braces is no longer supported看着像玄学其实就是语法兼容问题。原因老版本PHP源码里大量使用$arr{0}、$str{1}这种大括号数组偏移语法PHP 7.4废弃、PHP 8删除直接致命错误。mysql_*系列函数在PHP 7就移除了遇到也会一起爆。解决项目不依赖新特性就本地跑PHP 7.4最省事必须上PHP 8就用编辑器全局将大括号偏移替换成方括号偏移正则模式大概是(\$[A-Za-z_][A-Za-z0-9_]*)\{([^}])\}替换为$1[$2]。替换完用php -l对每个文件做语法检查确认没有遗漏再部署。5. 让大转盘更好运营概率模拟验证与动态调整5.1 用CLI脚本验证配置和概率后台配置完最怕上线后才发现概率不对。我习惯写一个命令行模拟脚本用同一套drawByWeight跑10万次统计输出每个奖品实际命中率?php // simulate_draw.php 用法php simulate_draw.php [活动ID] $activityId isset($argv[1]) ? $argv[1] : 1; require includes/db.php; require includes/functions.php; $prizes getAvailablePrizes($activityId); // 复用实际查询 $stats []; $total 100000; for ($i 0; $i $total; $i) { $r drawByWeight($prizes); $key $r[name] ?? 未中奖; $stats[$key] isset($stats[$key]) ? $stats[$key] 1 : 1; } foreach ($stats as $name $count) { printf(%-12s 出现 %-6d 次实际概率 %.2f%%\n, $name, $count, $count / $total * 100); }按这个输出和配置权重比对单奖品误差超过0.5%就要回头查权重配置和库存过滤逻辑。这套校验每次上线前都跑一遍比人工抽查一百次订单放心得多。5.2 分时段动态概率实现运营希望晚上黄金时间一等奖概率拉高改动方案是在奖品的配置表加一个time_switch字段权重拆分早/晚两份。抽奖前按当前时间选择正确的一组权重核心逻辑只是把drawByWeight的入参换成对应时间段的奖品数组其他不动。5.3 上线前检查清单正式上线前我按这个顺序强制走一遍活动时间配置是否覆盖完整周期库存逐项核对管理员账号权限是否收住只允许查看和导出不允许直接改抽奖接口返回install目录已删除日志表每日归档是否加了计划任务模拟脚本跑一遍确认概率和配置一致。早年代过一个大转盘上线第二天就发现同一个头像连中三次预算直接被打穿。后来养成的习惯就是每次改完概率都要强制跑一遍10万次模拟脚本再拿模拟结果和配置比对全部通过才上线。这套PHP幸运大转盘源码本身不难难的是不做校验就敢上线希望帮到你。本文还有配套的精品资源点击获取