简介:这是面向Java开发的微信大转盘互动抽奖项目,适合想在微信平台快速开展抽奖活动的开发者,也适合中级Java程序员通过实际项目学习前后端分离应用的构建。资源包共24个文件,类型涵盖Java源码、JSP页面、JavaScript和CSS前端代码、PNG/JPG图片素材,以及Eclipse项目配置和部署描述文件,解压后约289KB,轻量易读。该项目核心知识点包括HTML5 Canvas绘制动态转盘、CSS3动画控制旋转、jQuery事件处理、RESTful API设计,并涉及微信OAuth2.0登录授权、数据库存储、HTTPS安全通信与性能优化思路;开发时采用响应式设计,可适配Android/iOS微信内置浏览器。源码按WebRoot标准目录组织,入口页面和后端逻辑分布清晰,便于对照调试。已有228人学习下载,能帮助读者快速掌握大转盘抽奖的完整实现链路,并作为二次开发或毕业设计参考。 我之前负责过好几个微信大转盘类型的营销活动,说实话,第一次接到这个需求的时候,我内心是有点不屑的——一个转盘动画加一个抽奖接口,能有多复杂?结果从联调到上线,踩了一路的坑:微信授权回调反复失效、库存被高并发扣成负数、jsapi_ticket签名错误导致分享卡片出不来、凌晨被羊毛党刷走了一批实物奖品。后来我把这套东西搭成了一个相对标准化的方案,用Java(Spring Boot)做服务端,H5页面跑在微信内置浏览器里,再配上Redis处理库存与限流,才算是稳下来了。
这篇文章不打算只给一个demo代码,而是把整个项目的设计链路讲清楚:业务规则怎么定、技术选型怎么选、抽奖算法和库存控制的实现细节、微信OAuth授权与JS-SDK对接的完整流程,以及上线前必须搞定的防刷和自查项。适合正在做或准备做类似营销活动的后端开发,也适合想了解微信生态下Java服务端怎么和微信打交道的新手。
1. 微信大转盘的业务本质与技术选型
1.1 先想清楚活动规则,再写代码
大转盘本质是一个营销工具,核心不是转盘旋转的特效,而是围绕"奖品发放"的一套规则。最常见的活动目标有四种:公众号涨粉(强制关注后抽奖)、促活(签到送抽奖次数)、留资(抽奖前填手机号)、拉新(分享给好友得额外次数)。做之前要把奖品池设计好,我在实际项目中通常分三类:
- 虚拟奖品:优惠券、积分、会员天数,成本低、库存无限或可大量设置;
- 实物奖品:手机、耳机、周边,数量少、需要收货地址链路;
- 感谢参与:空奖也要有收益,可以给小额积分或优惠券,避免纯空气让用户流失。
关键的数值规则也要在开发前和运营确认死:每人每天抽几次?中奖率多少?奖品发放总量多少?活动周期多久?这些规则后期改起来非常痛苦,尤其是概率和库存,牵扯到抽奖算法和数据库设计,上线后频繁改配置很容易引发线上事故。
1.2 技术选型:H5活动页而不是小程序
大转盘项目常见的承载形态有两种:小程序和微信公众号H5。我这次选的是H5,理由很实际:营销活动讲究快速上线、随时投放,小程序需要审核、有类目限制,而且用户跳转链路长;H5只要部署一个HTTPS页面,用户在微信里点开链接就能玩。
前端用Vue或原生页面都行,转盘动画用CSS3 transform + transition实现,效果足够。后端是Java技术栈,Spring Boot负责接口,MySQL存业务数据,Redis扛并发和热点数据。这里有个硬性前提:H5页面必须绑定一个已认证的微信公众号,并且服务端要有能调用微信接口的AppID和AppSecret。域名必须是备案过的HTTPS域名,否则微信内打不开或授权失败。
1.3 整体架构和模块划分
系统拆成几块,各司其职:
- 用户模块:负责微信OAuth授权、openid与会话token的映射;
- 活动模块:配置活动时间段、每日抽奖次数、概率策略;
- 抽奖模块:核心抽奖算法,结合库存判断;
- 奖品模块:奖品列表、库存管理、中奖记录;
- 发放模块:虚拟奖品自动发放,实物奖品进入领奖流程。
请求链路大概是:用户微信打开H5,前端调后端获取token(后端去调微信OAuth),然后拉取活动配置与剩余抽奖次数;用户点击抽奖,后端完成抽奖算法加库存扣减加落库,返回中奖结果;前端根据结果做转盘动画和弹窗。整个链路里,最容易出问题的就是第2步和第4步,下面分别展开讲。
2. Java后端核心设计:抽奖、库存与并发控制
2.1 抽奖概率:权重算法与可配置的动态概率
很多人写抽奖就是Math.random()然后一串 if-else,活动上线后想调概率得改代码重新发布,非常尴尬。我用的是权重数组方案,把每个奖品的中奖权重配置在数据库或配置中心里。
public class LotteryUtil { public static int draw(List<Award> awards) { // 计算总权重 int totalWeight = awards.stream().mapToInt(Award::getWeight).sum(); int random = ThreadLocalRandom.current().nextInt(totalWeight); int cursor = 0; for (int i = 0; i < awards.size(); i++) { cursor += awards.get(i).getWeight(); if (random < cursor) { return i; } } return awards.size() - 1; } }这个算法的本质是把0到总权重之间的随机数映射到奖品区间上,权重越大,落点概率越高。好处是改一下权重数值就能调整中奖率,活动运营可以自己配置,不用发版。
这里提几个容易犯的错误:
- 权重取值要合理。比如空奖权重设100,实物奖品设1,实物中奖率不是百分之一,而是 1 / (100 + 1 + ...) 的总权重比例,设计时要把所有奖品的权重加起来算清楚;
- 实际中奖概率和用户感知概率是两回事,平台的"中奖率20%"通常指所有奖品合计中奖率,不是单个大奖的中奖率;
- 想控制高峰、低峰不同中奖率,可以做成时间段和权重映射,抽奖时按当前时间取一套权重,比如晚上8点到10点把实物奖品权重调高,带动活动热度。
2.2 库存扣减:从数据库行锁到Redis原子操作
大转盘最容易出事故的就是库存。实物奖品可能只有几十件,活动一开始流量涌进来,如果逻辑写成"先查库存、库存大于0就扣减",高并发下库存必然被减成负数,这就是经典的超卖问题。
最稳妥的数据库方案是原子更新:
UPDATE award_stock SET stock = stock - 1 WHERE award_id = ? AND stock > 0;这条SQL利用stock > 0条件保证不会扣成负数,如果返回影响行数为0,说明库存已经没了。数据库行锁天然保证并发安全,但缺点是每来一个请求都打一次库,抽奖这种高并发场景容易把数据库打满。
所以实践中我通常用Redis做预扣减。Redis的DECR是原子操作,先把库存放到Redis里,抽奖时先DECR,返回大于等于0说明扣减成功,小于0说明奖品已被抢完,再把key回滚到0。更严谨的写法是用Lua脚本保证"判断库存 + 扣减"两步原子执行,避免DECR之后再判断,中间被其他请求插队。
补充一点:Redis扣减成功不等于发奖成功。我一般把"扣库存"和"生成中奖记录"分开:Redis扣减成功后,往MQ里发一条消息,异步去MySQL落中奖记录、调用发券接口。如果异步流程失败,需要有个定时任务做对账补偿,把扣了库存但没生成记录的单子捞出来重试。
2.3 中奖记录与领奖流程的状态机
中奖之后不能只返回一个"恭喜你",数据模型要完整。t_lottery_record表的核心字段:id、openid、award_id、award_name、状态、领取信息、创建时间。状态机建议这样设计:
- 中奖(PENDING)
- 待领取(待用户填地址)
- 已领取 / 已发货(用户确认或后台发货)
- 已过期
实物奖品要引导用户填写收货地址,可以放在H5里提交,但地址接口一定要做好防刷,否则容易被批量提交脏数据。虚拟奖品走自动发券,发券接口必须幂等,因为MQ重试或前端重试都可能导致重复发放。我用的办法是:在中奖记录上加一个award_biz_id唯一索引,发券前先查有没有已发放记录,幂等键唯一,重复请求直接返回旧结果。
3. 微信生态对接:OAuth授权与JS-SDK签名
3.1 OAuth2网页授权:一切从openid开始
微信生态下没有传统网站的账号密码体系,用户唯一标识就是openid(同一用户在同一个公众号下的唯一ID)。获取openid需要走OAuth2网页授权,流程如下:
- 前端跳转到微信授权URL;
- 用户同意后,微信重定向到redirect_uri,并带上code参数;
- 后端拿到code,请求微信接口换取openid。
这里有个常见选择:如果只需要标识用户身份(抽奖就够),用snsapi_base静默授权,用户无感知;如果需要昵称头像,得用snsapi_userinfo,会弹授权框,转化率有损耗。大转盘我选静默授权,因为活动讲究低门槛,多一步授权就可能流失一批用户。
后端拿到openid后,要自己生成一个业务token返给前端,后续接口都带这个token,不要每次把openid暴露给前端。token可以放Redis并设置过期时间,过期后前端重新走授权,或者用refresh_token机制续期,具体看活动周期长短。
3.2 JS-SDK签名:wx.config 注入的坑
如果活动页要自定义分享卡片、隐藏右上角菜单,必须引入微信JS-SDK,核心步骤是后端生成签名,前端调用wx.config注入。签名参数包括:appId、timestamp、nonceStr、signature。
服务端生成signature的核心代码:
String string1 = "jsapi_ticket=" + ticket + "&noncestr=" + nonceStr + "×tamp=" + timestamp + "&url=" + url; String signature = DigestUtils.sha1Hex(string1);这里的坑非常多:
jsapi_ticket由access_token换取,官方要求缓存至少7200s,不能频繁刷新,否则会被限流;url必须是当前页面完整的URL(包括路径和query),但要去掉#及其后面的所有内容。前端跳转、动态路由都会导致签名校验失败;- 签名接口要实时接收前端传过来的实际页面URL,不能用后端配置的固定URL。
3.3 分享卡片自定义与真机调试
在JS-SDK config成功后,可以用wx.updateAppMessageShareData设置分享给好友的卡片,用wx.updateTimelineShareData设置分享到朋友圈的卡片。需要注意:分享链接的域名必须在公众号后台的"JS接口安全域名"里配置过,否则签名配了也没用。
调试JS-SDK的实用方法:在wx.error回调里把错误信息打出来,最常见的invalid signature基本就是URL不对或ticket缓存过期。另外,PC端预览和真机行为不完全一致,很多问题只有手机微信里才能复现,上线前一定要拿真机测一遍授权、分享、抽奖全流程。
分享卡片还有一个容易被忽略的体验点:分享出去的链接,新用户打开时最好直接进入活动页并自动授权,而不是再跳一次首页引导。我在分享链接上额外带一个from_user=xxx参数,用于记录分享关系,配合活动规则做"分享得次数"的奖励。
4. 防刷体系:用最低成本挡住羊毛党
4.1 用户维度:openid只是第一道防线
很多第一次做活动的人以为有openid就够了,实际远远不够。羊毛党手里可能有大量微信号,openid拦不住批量注册。我的经验是分层防护:
- 低价值奖品只校验openid和抽奖频率;
- 高价值奖品必须绑定手机号验证。手机号可以接短信验证码服务,成本可控,但能把绝大多数羊毛党挡在门外;
- 再高级一点,可以采集前端设备指纹(canvas指纹、webgl指纹、user-agent组合)在后端做风控记录,如果同一设备指纹在短时间内关联了多个openid,直接进风控名单。
不过设备指纹这套偶尔会误伤正常用户,比如一台家庭路由器下多人共用IP,或者公司WiFi大量用户访问,所以不能一刀切,可以做成"进名单后需要短信验证"而不是直接拒绝。
4.2 行为维度:频率限制与异常识别
控制抽奖频率是最基本的防刷手段。每个用户每天抽奖次数上限,比如5次;同一个IP的请求频率上限,比如每秒10次;同一openid的Token过期时间、同一手机号可绑定的抽奖账号数量,这些限制我在Redis里用滑动窗口或固定窗口计数器实现。简单说就是每次请求把用户维度key自增,超过阈值直接拒绝,并返回一个业务码让前端提示"今天次数用完"。
另一个容易被忽略的点是时段异常。正常用户不会在凌晨2点到5点集中抽奖,如果这个时段的抽奖量异常升高,大概率是脚本在跑。可以加一个简单的告警,比如该时段抽奖量环比超过3倍就通知开发,或者对该时段内的抽奖请求做更严格的验证,强制要求手机号。
再补充一个业务侧的心机玩法:把"谢谢参与"换成小额积分或优惠券,让羊毛党刷来的收益下降,普通用户流失率也下降,一举两得。
4.3 后端兜底:幂等与分布式锁
防刷不止在入口,核心接口必须有兜底。抽奖接口是典型的"点击一次、只允许成功一次"场景,前端按钮加loading只是体验层手段,后端必须防重。
我用的方案是Redis的SET key value NX EX实现分布式锁,key用lottery:user:{openid}:{yyyyMMdd},过期时间设置几秒,同一用户在锁未过期时重复请求直接返回"正在处理中"。
发券、改中奖记录这类写操作要有幂等键。之前说过,中奖记录里加唯一索引,发券前查重,这能在极端情况下保证不重复发奖。
另外,把所有抽奖请求都记录一份日志(openid、IP、时间、UA、中奖结果),活动结束后如果要复盘羊毛党行为,这些日志是最重要的证据。不要等出事了才后悔没打日志。
5. 实战踩坑清单与上线自查
5.1 我在联调中踩过最深的几个坑
第一个坑:签名用的URL和实际页面不一致。当时前端SPA用了history路由,页面地址动态变化,签名接口却传了固定URL,结果分享卡片一会儿能用一会儿不能用,排查了很久才发现是#和路由跳转导致的。后来我把签名接口改成必须由前端传当前URL,后端只负责用这个URL计算签名,问题才彻底解决。
第二个坑:多实例部署后,synchronized锁库存完全失效。我在单体架构里用JVM锁控制库存扣减没问题,后来上了两台实例,锁就管不住了。JVM锁只对单进程有效,多实例必须换成数据库行锁或Redis原子操作,这个教训很深刻。
第三个坑:把access_token和jsapi_ticket混为一谈。微信所有接口基本都要access_token,但JS-SDK签名用的是jsapi_ticket,不是access_token,两者作用完全不同,缓存逻辑也要分开写。另外access_token刷新时会失效旧的,多个实例并发刷新会出现互相顶掉的问题,官方建议用一个全局服务统一获取和刷新,拿到后放Redis共享给所有实例。
第四个坑:安卓微信X5内核下CSS3动画卡顿。转盘旋转动画在iOS上很流畅,安卓低端机上直接卡成PPT。解决方案是转盘旋转用transform: rotate配合transition,不要用JS不停计算位置;关掉不必要的阴影和滤镜;转盘背景图和奖品图标压缩后再用,体积尽量控制在200KB以内。
5.2 上线前必须自查的清单
| 检查项 | 说明 |
|---|---|
| 网页授权域名 | 公众号后台配置,和redirect_uri域名必须一致 |
| JS接口安全域名 | 用了分享功能必须配置,否则签名无效 |
| IP白名单 | 服务器出口IP要加入公众号IP白名单,否则拉不到access_token |
| HTTPS证书 | 微信内非HTTPS页面会被拦截 |
| 服务器时间 | 时间偏差过大会导致签名校验失败 |
| Redis持久化 | 库存和计数器不能断电全丢 |
| 数据库备份 | 上线前做一次全量备份 |
| 发券幂等 | 确保重复请求不会重复发奖 |
| 日志与监控 | 抽奖量、库存余量、发券失败率必须有监控 |
| 并发压测 | 至少模拟3倍预估峰值流量,确认库存不超发、接口不挂 |
再补充几个容易被忽略的小点:如果活动要投放多个渠道,建议在链接上带渠道参数,方便统计各渠道ROI;活动结束后不要马上关服务器,留一个"活动已结束"的静态页面,避免用户以为系统出Bug;一旦发现库存异常,第一时间置为售罄并对外公告,不要偷偷修数据,透明的处理反而更有公信力。
最后再分享一个我个人的习惯:这类活动项目上线之后,我一般会连续盯三天后台数据,重点关注中奖率与实际发放量的偏差、参与用户数曲线。如果发现某个时段参与量突然暴涨但转化率没跟上,多半是外部流量进来或者有人在作弊,及时调整规则和风控策略,比事后补救省心得多。希望这篇关于java微信大转盘项目的拆解能帮你少走点弯路。
本文还有配套的精品资源,点击获取