运营同学又来找我做活动页了,这次的需求是“幸运大转盘抽奖”——用户在 H5 页面上转一次转盘,指针停在哪个奖品就发哪个。这种玩法在电商、教育、餐饮行业都特别常见,低门槛、高参与感,做一次活动能拉不少用户互动量。但真把需求拆开看,它并不只是画个转盘、转一下指针这么简单:概率怎么控、库存怎么扣、并发怎么防、前端动画和后端结果怎么保持一致,这些都是藏在“转盘”背后的核心问题。
这篇文章我就围绕一套可以直接拿去用的幸运大转盘抽奖系统源码来拆解,涵盖整体设计、技术选型、概率算法、防刷方案、前后端实现以及我实际踩过的坑。适合刚接触活动系统的开发者、想自建抽奖功能的站长,以及要跟开发沟通需求的运营同学,读完你就能知道一个转盘系统是怎么从零跑起来的,也能直接复现到自己的项目里。
标注一下这篇博文的落地技术栈:后端用 PHP,前端用原生 JavaScript + Canvas 绘制转盘,数据库用 MySQL。这个组合在轻量级活动页里非常常见,服务器要求低、部署简单、改起来也快。如果你更熟悉 Java 或者 Node.js,核心思路完全一样,照着迁移就行。
1. 整体设计与技术选型思路
1.1 核心需求拆解
一个完整的“幸运大转盘活动抽奖系统”,表面上是用户点一下按钮、转盘转起来、弹窗告诉用户中了什么。但背后需要拆分出这几块:
- 转盘展示:奖品名称、奖品图标、指针、按钮状态。这是用户唯一能看到的部分,决定了活动的“质感”。
- 抽奖逻辑:用户点击后,系统判断“这次是否中奖、中了哪个档位”,这是服务端需要保证的核心,不能被前端篡改。
- 概率控制:每个奖品的命中概率不是均等的,一般会设计成“感谢参与”“积分”“实物奖品”不同权重,而且可以配置。
- 库存管理:实物奖品有数量限制,发完之后再抽到就需要走“无奖”或“自动替换为下一个奖品”的逻辑。
- 防刷限制:用户是否登录、每人每天抽几次、是否限IP、是否需要消耗积分或抽奖次数。
- 活动配置后台:运营需要能配置奖池、奖品、概率、活动时间,不能每次改奖品都找开发。
把这六个需求理清楚之后,你会发现这基本就是一个小型的活动引擎,而不是一个单纯的“转盘动画”。而这正是很多初学项目最容易漏掉的部分——把动画做得很好,但后端结果是一句固定的“谢谢参与”,这种系统上线后运营会非常痛苦。
1.2 技术选型为什么这样定
先说几个被验证过很多次的成熟方案:
前端绘制转盘和指针,主流有三种做法:
- Canvas 绘制的旋转大转盘:用
canvas画出扇形、文字、图片,每次抽奖时通过requestAnimationFrame控制圆心角旋转。优点:高清、灵活、动态数据注入简单;缺点:需要自己处理绘制细节,不同屏幕适配要测。 - CSS3 transform + pre-made 图片:设计出一张转盘底图(360°圆形带奖品分区),然后用
transform: rotate(deg)旋转。优点:实现最快;缺点:中奖位置和旋转角度的计算要精确,如果分区不是整数很容易歪。 - SVG 扇形 + 动画库:用 SVG 路径绘制扇区,配合 GSAP 做动画。体验好,但代码量也上来了。
我最终选了 Canvas 实现,核心原因是奖品数量和每个扇区角度可以由后端返回的数据动态生成,运营后台改了奖池,前端的转盘分区就跟着变,不需要设计师重新出图。这在长期运营的场景下能省掉大量重复沟通。
后端选择 PHP 而不是 Node.js 或 Java,是考虑到很多中小站点、公众号营销平台和活动页部署在虚拟主机或轻量服务器上,PHP 的部署成本最低。而且这类抽奖接口本身不重,PHP-FPM 配合 Redis 做限流和库存扣减完全够用。
数据存储用 MySQL + Redis 组合:MySQL 存放奖品配置、中奖纪录、用户参与记录,持久化且方便运营后台统计;Redis 用于库存扣减、用户每日抽奖次数计数、互斥锁等高频操作。后面讲到并发防刷时你会看到 Redis 的价值。
这样组合下来,整个系统的优点是:
- 部署轻,任意一台跑得动 PHP 的服务器就行,不需要容器化,不需要复杂的中间件。
- 前后端职责清晰,前端只管转盘动画和接口调用,后端管概率、库存、风控。
- 数据结构清晰,后续如果要把“抽奖次数”换成“消耗积分”“消耗金币”,只需要在抽奖接口里加一个校验,不会影响其他模块。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心功能设计与细节解析
2.1 奖池配置与数据库设计
先设计数据库表,这是整个系统稳定性的基础。我把所有配置都拆到表里,避免硬编码。
奖品表 lp_prize 的结构大概是这样:
sql复制CREATE TABLE `lp_prize` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`prize_name` varchar(255) NOT NULL COMMENT '奖品名称',
`prize_type` tinyint(4) NOT NULL DEFAULT '1' COMMENT '奖品类型:1积分,2实物,3优惠券,4无奖',
`prize_img` varchar(500) DEFAULT NULL COMMENT '奖品图片路径',
`total_num` int(11) NOT NULL DEFAULT '0' COMMENT '库存总量',
`remain_num` int(11) NOT NULL DEFAULT '0' COMMENT '剩余库存',
`probability` decimal(10,4) NOT NULL DEFAULT '0.0000' COMMENT '概率权重,万分位',
`sort_order` int(11) NOT NULL DEFAULT '0' COMMENT '排序,角度越小越靠前',
`status` tinyint(4) NOT NULL DEFAULT '1' COMMENT '0下架 1上架',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='奖品表';
概率字段 probability 用万分之几来存,也就是说如果一个奖品的抽中概率是 15%,那存 1500。用整型/万分数可以避免浮点数比较带来的精度问题,后面做概率抽取时也能直接用整数运算。
还需要一张用户参与记录表 lp_lottery_record,记录每次抽奖的时间、奖品ID、结果状态,这张表主要给运营看数据、排查问题用。索引要建 (user_id, created_at),因为最常见的查询就是“某个用户在活动期间抽了几次”“某个人中了什么”。
库存字段为什么单独放 `rema
