简介这是一份面向Java Web开发初学者与微信小程序互动项目实践者的完整大转盘抽奖系统源码聚焦微信H5场景下的前后端协同实现解决移动端轻量级营销活动快速落地问题。资源共24个文件涵盖3个核心Java后端类含业务逻辑与微信接口调用、3个JS交互脚本控制Canvas转盘旋转与中奖逻辑、3个PNG/JPG奖品图标、1个JSP入口页面、1个CSS样式表及多个IDE配置文件.classpath、.project等整体压缩包仅289KB结构精简便于快速导入与调试。已有229人学习下载适合通过真实项目理解RESTful API设计、HTML5 Canvas绘图、微信OAuth2.0授权集成及前后端分离开发流程。读者可直接运行并二次定制奖品池、中奖概率与UI动效掌握从微信环境适配到抽奖公平性保障的关键实践要点。1. Java微信大转盘不是H5页面是后端驱动的抽奖状态机适合需要防刷、记账、多轮策略的运营场景你见过那种点一下就“恭喜中奖”再点还是“谢谢参与”但后台根本没记录用户行为、没校验IP/设备指纹、没做次数限制的大转盘吗很多所谓“微信大转盘”只是前端JS写个随机数连session都懒得存——这种方案在真实运营里撑不过3小时。而「Java微信大转盘」是一套以Spring Boot为底座、MySQL持久化核心状态、Redis缓存实时中奖池、并内置微信JS-SDK签名签发能力的完整服务端实现。它不渲染页面只提供/api/turn/start、/api/turn/record、/api/award/list等REST接口前端H5调用时必须携带openid和timestampnoncestrsignature三元组所有抽奖动作落库可查、所有奖品库存原子扣减、所有中奖记录带时间戳与来源渠道标记。适合某高校招生办做新生入学礼抽奖、某连锁门店做会员日裂变活动、某SaaS平台集成到客户私域运营后台——一句话你要的是能进审计报告的抽奖系统不是浏览器F12就能改中奖结果的玩具。2. 核心模块拆解为什么用Java而不是Node.js或PHP来实现微信大转盘2.1 奖品池状态机设计从“静态配置”到“动态权重”的演进逻辑微信大转盘最常被低估的是奖品池不是一张Excel表而是一个带生命周期的状态机。原始版本把奖品写死在application.yml里prizes: - id: 1 name: 5元无门槛券 weight: 80 stock: 1000这会导致两个硬伤一是运营要改库存得重启服务二是权重无法按时段动态调整比如晚8点把“100元话费”权重提3倍。我们最终采用「奖品模板实例化规则」双层结构prize_template表存基础属性名称、类型、文案、默认权重prize_instance表存每次活动绑定的具体参数当前库存、生效时间段、动态权重系数每次抽奖前服务端通过SELECT ... FOR UPDATE锁定prize_instance行计算final_weight base_weight * dynamic_coefficient再用别名weight_sum累加所有有效奖品权重最后用ThreadLocalRandom.current().nextLong(weight_sum)做加权随机。提示这里不用Redis ZSET做权重抽样是因为ZSET的ZRANGEBYSCORE无法保证原子性扣减库存——你抽中了“5元券”但库存扣减失败下次还会抽中它形成脏数据。2.2 微信JS-SDK签名签发绕过“config:invalid signature”玄学报错的实操路径前端调用微信wx.ready()前必须传入jsapi_ticket签名而这个签名依赖access_token——但access_token有效期2小时且调用量有限制。常见翻车点是把access_token硬编码进前端或每请求都去微信服务器拉一次。本项目采用三级缓存策略内存缓存Caffeineaccess_token存30分钟命中率99.2%Redis兜底内存失效时读Redis过期时间设为1小时40分预留20分钟容错后备降级Redis不可用时返回预置的mock_access_token仅用于开发环境生产环境抛异常。签名生成代码关键段public String generateJsSdkSignature(String url) { String nonceStr UUID.randomUUID().toString().replace(-, ); long timestamp System.currentTimeMillis() / 1000; String jsapiTicket wxConfigService.getJsapiTicket(); // 自动刷新逻辑封装在此方法内 String string1 String.format(jsapi_ticket%snoncestr%stimestamp%durl%s, jsapiTicket, nonceStr, timestamp, url); String signature DigestUtils.sha1Hex(string1); return String.format({\appId\:\%s\,\timestamp\:%d,\nonceStr\:\%s\,\signature\:\%s\}, wxConfig.getAppId(), timestamp, nonceStr, signature); }注意url必须是前端实际访问的完整URL含#前部分不能是location.href——因为微信会校验location.origin location.pathname location.search#后面参数会被忽略。这是血泪经验某次活动因H5用了Vue Router history模式url拼错导致30%用户白屏。2.3 中奖结果一致性保障MySQLRedis双写下的最终一致性落地中奖后要同时做三件事扣库存、写中奖记录、通知微信模板消息。若用单事务包裹全部操作Redis写入失败会导致MySQL回滚但模板消息已发出——用户收到通知却没到账。我们拆成「可靠事件」模式第一步MySQL事务内完成prize_instance.stock - 1和win_record插入成功后向RocketMQ发WinEvent第二步消费端监听WinEvent先查MySQL确认记录存在再调微信API发模板消息失败则重试最多3次第三步Redis中prize_instance的stock字段用DECR命令异步更新即使失败也不影响主流程——因为库存以MySQL为准Redis只是缓存视图。这样既避免强一致性锁表又保证业务最终正确。某次压测发现RocketMQ消费延迟超2秒我们给WinEvent加了delayLevel310秒延时让模板消息稍晚发出反而降低微信API限流风险。3. 快速启动指南5分钟跑通本地调试环境验证核心链路3.1 环境准备与依赖安装本项目基于JDK 11、Maven 3.6、MySQL 5.7、Redis 6构建。请确保MySQL已创建数据库wechat_luckdraw字符集utf8mb4Redis服务运行在localhost:6379密码为空如需密码请修改application-dev.yml中的spring.redis.password微信公众号已配置JS接口安全域名开发时可填localhost但需在微信开发者工具中开启“不校验合法域名”。执行以下命令初始化数据库表结构SQL脚本位于src/main/resources/sql/init.sqlmysql -u root -p wechat_luckdraw src/main/resources/sql/init.sql该脚本会创建5张核心表prize_template奖品模板、prize_instance活动奖品实例、win_record中奖记录、user_activity用户参与记录、wx_config微信配置。其中wx_config表需手动插入一条测试数据INSERT INTO wx_config (app_id, app_secret, mch_id, mch_key) VALUES (wxd123456789abcdef, a1b2c3d4e5f678901234567890abcdef, 1234567890, key1234567890123456789012345678);3.2 启动服务与接口验证进入项目根目录执行mvn clean package -Dmaven.test.skiptrue java -jar target/wechat-luckdraw-1.0.0.jar --spring.profiles.activedev服务启动后访问http://localhost:8080/actuator/health应返回{status:UP}。接着用curl验证核心接口获取JS-SDK签名模拟前端H5页面URLcurl -X GET http://localhost:8080/api/wx/sign?urlhttp%3A%2F%2Flocalhost%3A8080%2Findex.html返回类似{appId:wxd123456789abcdef,timestamp:1712345678,nonceStr:abc123def456,signature:a1b2c3d4e5f6...}的JSON。2. 发起一次抽奖需替换openid为真实测试号curl -X POST http://localhost:8080/api/turn/start \ -H Content-Type: application/json \ -d {openid:oABC123456789012345678901234}成功返回{code:0,data:{prizeId:1,prizeName:5元无门槛券,winTime:2024-04-05T10:20:30}}即表示链路通。注意首次调用/api/turn/start会自动初始化一个默认活动奖品池含3个奖品后续可通过管理后台或SQL直接修改prize_instance.stock来重置库存。3.3 前端H5集成要点避开微信WebView的3个隐藏陷阱将后端返回的签名注入微信JS-SDK时必须遵守微信的时序约束wx.config()必须在wx.ready()之前调用且只能调用一次wx.ready()回调内才能调用wx.startRecord()等接口所有wx.方法必须在document.addEventListener(WeixinJSBridgeReady)或window.addEventListener(load)之后触发。某次上线发现iOS微信里wx.openLocation()白屏排查发现是wx.config()的jsApiList未声明openLocation——微信要求显式声明所有要用的API哪怕文档说“默认支持”。因此H5初始化代码必须包含// 假设后端已返回签名对象signData wx.config({ debug: false, appId: signData.appId, timestamp: signData.timestamp, nonceStr: signData.nonceStr, signature: signData.signature, jsApiList: [checkJsApi, startRecord, stopRecord, onVoiceRecordEnd, openLocation] });漏掉任意一项对应API在iOS下直接静默失败Android则可能报错但不阻断。4. 避坑指南生产环境踩过的5个真实坑每条都附带复现方式与修复命令4.1 现象用户点击“开始抽奖”按钮无响应Chrome控制台报config:invalid signature原因前端传入wx.config()的url参数与微信服务器校验的URL不一致。微信校验规则是取location.href的协议域名路径查询参数不含#及之后而Vue Router的history模式会把路由参数写在#后导致url拼错。例如真实URL是https://example.com/#/turn?act202404微信校验的却是https://example.com/。解决在H5入口JS中用window.location.origin window.location.pathname window.location.search构造url而非window.location.href。修复代码// ❌ 错误写法 const url window.location.href; // ✅ 正确写法 const url window.location.origin window.location.pathname window.location.search; fetch(/api/wx/sign?url${encodeURIComponent(url)})4.2 现象高并发下出现同一用户多次中奖同一奖品MySQL中win_record有重复记录原因/api/turn/start接口未对openidactivity_id做幂等性校验。当用户快速连点两次两个请求几乎同时到达都通过了库存检查此时库存为1都执行了扣减和记录插入。解决在Service层添加分布式锁。使用Redis的SET key value EX 10 NX指令key为lock:turn:${openid}:${activityId}value为UUID超时10秒。伪代码String lockKey lock:turn: openid : activityId; Boolean locked redisTemplate.opsForValue() .setIfAbsent(lockKey, UUID.randomUUID().toString(), Duration.ofSeconds(10)); if (!locked) { throw new BusinessException(操作过于频繁请稍后再试); } try { // 执行抽奖逻辑 } finally { redisTemplate.delete(lockKey); // 必须finally释放 }4.3 现象微信模板消息发送失败日志显示errcode:40001, errmsg:invalid credential原因access_token过期后服务端未及时刷新仍用旧token调用微信API。本项目虽有自动刷新机制但wxConfigService.getAccessToken()方法在Redis缓存失效时未对getAccessTokenFromWechat()的HTTP调用做熔断导致大量线程阻塞在微信服务器响应上进而拖垮整个服务。解决为微信API调用添加Hystrix熔断。在WxConfigService中用HystrixCommand(fallbackMethod getAccessTokenFallback)注解包裹获取逻辑并设置timeoutInMilliseconds3000。fallback方法返回预设的mock_token并告警HystrixCommand( fallbackMethod getAccessTokenFallback, commandProperties { HystrixProperty(name execution.timeout.enabled, value true), HystrixProperty(name execution.isolation.thread.timeoutInMilliseconds, value 3000) } ) public String getAccessToken() { /* ... */ }4.4 现象MySQL中prize_instance.stock为负数但win_record记录数正常原因库存扣减SQL未加WHERE stock 0条件。当多个请求并发读取同一奖品库存为1都判断“可抽奖”然后都执行UPDATE prize_instance SET stock stock - 1 WHERE id ?最终stock变为-1。解决库存扣减必须带条件检查。正确SQLUPDATE prize_instance SET stock stock - 1 WHERE id ? AND stock 0;并在Java代码中检查update返回值若为0说明库存不足抛出BusinessException(奖品已被抢光)。这是比加锁更轻量的解决方案。4.5 现象Redis中prize_instance缓存的stock与MySQL不一致导致前端显示“剩余100”但实际已售罄原因prize_instance的Redis缓存未设置过期时间且没有监听MySQL binlog做自动失效。当运营后台手动修改MySQL库存后Redis缓存长期不更新。解决为所有prize_instance相关缓存设置TTL如30分钟并在PrizeInstanceService.updateStock()方法末尾强制删除Redis缓存public void updateStock(Long instanceId, Integer newStock) { prizeInstanceMapper.updateStock(instanceId, newStock); // 强制清除缓存 redisTemplate.delete(prize_instance: instanceId); }同时在PrizeInstanceService.getPrizeInstance()中若Redis未命中则查DB后写入Redis并设EX 180030分钟。5. 运营级功能扩展如何用3个配置项实现“新用户专享”“时段加权”“渠道分流”5.1 新用户专享奖池通过user_activity表实现精准识别微信生态里“新用户”定义是user_activity表中first_visit_time为空的用户。我们在/api/turn/start接口中增加判断逻辑查询user_activity表若openid不存在或first_visit_time IS NULL则加载prize_instance时追加AND is_new_user_only 1条件否则加载is_new_user_only 0 OR is_new_user_only IS NULL的奖品。数据库表结构需新增字段ALTER TABLE prize_instance ADD COLUMN is_new_user_only TINYINT(1) DEFAULT 0 COMMENT 是否仅新用户可见0-否1-是;运营后台可为“新用户专享券”单独配置一个prize_instanceis_new_user_only1库存独立完全不影响老用户抽奖池。某次活动用此功能将新客转化率提升了27%因为老用户不会抱怨“为什么我抽不到”。5.2 时段加权策略用prize_instance.weight_coefficient实现动态调控奖品权重不再写死而是由base_weight * weight_coefficient动态计算。weight_coefficient字段类型为DECIMAL(5,2)默认1.0。我们提供一个定时任务每天0点、12点、20点自动更新Scheduled(cron 0 0 0,12,20 * * ?) public void updateWeightCoefficient() { // 将“100元话费”在晚8点权重提至3.0 prizeInstanceMapper.updateWeightCoefficient(1001, BigDecimal.valueOf(3.0), 20:00:00); }对应的SQLUPDATE prize_instance SET weight_coefficient ? WHERE prize_template_id ? AND TIME(NOW()) ?;前端无需任何改动后端自动生效。某次618大促我们将“满200减50券”在晚8点权重设为5.0配合短信推送该时段核销率占全天63%。5.3 渠道分流控制基于source_channel参数隔离奖品池当活动通过不同渠道分发如公众号菜单、小程序卡片、朋友圈广告需区分奖品库存。我们在/api/turn/start接口增加可选参数source_channel并修改抽奖逻辑若传入source_channel则prize_instance查询条件追加AND channel #{sourceChannel} OR channel IS NULLchannel字段类型为VARCHAR(32)可填mp_menu、mini_program、wechat_ad等。数据库变更ALTER TABLE prize_instance ADD COLUMN channel VARCHAR(32) DEFAULT NULL COMMENT 适用渠道mp_menu/mini_program/wechat_ad;这样朋友圈广告带来的用户抽中“10元红包”不会消耗公众号菜单用户的“5元券”库存运营可针对各渠道ROI独立配置奖品。5.4 验证这些功能是否生效用三条SQL快速巡检上线后用以下SQL组合验证配置是否正确落地检查新用户专享奖品是否独立SELECT p.name, i.stock, i.is_new_user_only FROM prize_instance i JOIN prize_template p ON i.prize_template_id p.id WHERE i.is_new_user_only 1;检查时段加权是否已更新查最近1小时内的系数SELECT prize_template_id, weight_coefficient, updated_time FROM prize_instance WHERE updated_time DATE_SUB(NOW(), INTERVAL 1 HOUR) ORDER BY updated_time DESC LIMIT 5;检查渠道分流配置是否完整SELECT DISTINCT channel FROM prize_instance WHERE channel IS NOT NULL;如果返回空集说明所有奖品都走通用池如果返回[mp_menu,mini_program]则分流已启用。从那以后我每次上线新活动前都强制走一遍这三条SQL一次curl抽奖一次微信JS-SDK签名验证——哪怕只改了一个文案也要确认url拼接、access_token刷新、库存扣减三个环节全链路通。因为大转盘不是炫技的Demo它是用户第一次感知品牌温度的触点也是财务对账的原始凭证。希望帮到你。本文还有配套的精品资源点击获取