简介面向微信平台互动抽奖场景的Java大转盘项目适合具备Java Web与HTML5基础、希望了解微信内嵌H5活动页实现方式的初中级开发者。项目以Java处理后端业务与微信接口交互前端借助HTML5、CSS3和JavaScript完成转盘绘制、旋转动画与页面交互并涉及RESTful接口、用户授权、抽奖记录存储及基础安全与响应式适配等知识点可用于学习前后端分离的活动页搭建流程。压缩包共24个文件、约289KB包含java、jsp、class等后端与页面代码js、css等前端脚本样式png、jpg图片素材以及xml、prefs、project等工程配置和元数据文件目录保留较完整的项目结构便于对照阅读。已有231人学习下载适合作为微信抽奖类小项目的结构参考与实现思路样本帮助梳理转盘动画、接口调用和工程组织方式。1. 抽奖不是转一下那么简单一个 Java 大转盘项目能解决什么很多人第一次接到“做个大转盘”的需求脑子里浮现的是前端 CSS 旋转动画。真到写代码才发现动画是最不值钱的部分。真正难的是奖品库存怎么扣、用户一天能抽几次、中奖概率按权重怎么算、并发下会不会超发、抽完了怎么补偿。我之前在一个活动系统里就翻过车——两个人同时抽中最后一张券数据库里库存变成 -1被运营追着问了一下午。这个 Java 微信大转盘项目核心解决的就是这套后端抽奖链路奖品配置、权重概率、库存扣减、抽奖次数限制、记录落库配合微信侧的入口做活动页。适合正在做营销活动、积分商城、小程序互动页的后端开发也适合想搞清楚“抽奖系统到底怎么写才不出事”的同学。前端转盘只是它的壳里面那套概率与库存的控制才是主体。2. 技术选型与目录结构先看清这套代码的骨架2.1 为什么是 Spring Boot MyBatis 这套组合抽奖业务的特点是读多写少、瞬时并发高、逻辑集中在少数几个接口上。这种场景用 Spring Boot 起步最省事内嵌容器、starter 依赖、配置外置一个 jar 扔到服务器上就能跑。持久层选 MyBatis 而不是 JPA主要是奖品配置表、抽奖记录表这类结构比较固定但查询条件多变的表手写 SQL 更好控尤其是扣库存那句update ... where stock 0用 ORM 生成反而绕。缓存层大概率会引入 Redis。奖品池、用户当日剩余抽奖次数这类数据放数据库里扛不住瞬时流量常见做法是活动开始前把奖品的 id、权重、剩余库存预热到 Redis抽奖时先走缓存判断再异步或同步落库。这不是项目正文写明的而是这个业务场景下合格开发基本都会做的事我在几个类似活动里也都是这么搭的。事务边界也值得说一句。抽奖涉及“扣次数 扣库存 写记录”三步这三步必须在同一个事务里否则会出现次数扣了奖品没发或者奖品发了次数没扣。用Transactional包住服务层方法是最低要求注意别把远程调用比如发券接口放事务里会拖长事务持有时间。2.2 目录结构与各层职责一个典型的 Java 大转盘后端目录大致长这样src/main/java/com/example/lottery ├── controller 接口层暴露抽奖、奖品列表、记录查询 ├── service 业务层抽奖核心逻辑、库存扣减、次数校验 │ └── impl ├── mapper MyBatis 映射接口 ├── entity 数据库实体Prize、LotteryRecord、UserTimes ├── dto 请求/响应对象 ├── config 数据源、Redis、微信相关配置 ├── common 统一返回、异常、常量、工具类 └── task 定时任务库存回补、记录清理 resources ├── mapper XML SQL 文件 ├── application.yml 主配置 └── sql 建表脚本分层的意义在于改的时候知道往哪下手。想调中奖概率去 service想加一个奖品字段entity、mapper XML、dto 三处同步改想换缓存方案只动 config 和 service 里对缓存的调用。我见过把抽奖逻辑全塞 controller 里的写法后来加个“保底必中”需求改动面直接失控。2.3 建表脚本与核心字段抽奖系统至少三张表理解字段比理解代码更重要-- 奖品配置表 CREATE TABLE t_prize ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(64) NOT NULL COMMENT 奖品名称, type TINYINT NOT NULL COMMENT 1实物 2优惠券 3积分 4谢谢参与, weight INT NOT NULL COMMENT 权重概率weight/总权重, stock INT NOT NULL COMMENT 剩余库存, total_stock INT NOT NULL COMMENT 总库存, status TINYINT DEFAULT 1 COMMENT 1上架 0下架, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 用户抽奖记录表 CREATE TABLE t_lottery_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id VARCHAR(64) NOT NULL COMMENT 微信 openid, prize_id BIGINT NOT NULL, prize_name VARCHAR(64) NOT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_user (user_id, create_time) ); -- 用户抽奖次数表 CREATE TABLE t_user_times ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id VARCHAR(64) NOT NULL, day DATE NOT NULL, used INT DEFAULT 0 COMMENT 已用次数, total INT DEFAULT 3 COMMENT 每日总次数, UNIQUE KEY uk_user_day (user_id, day) );字段设计上有两个细节容易忽略。weight存权重而不是存百分比是因为奖品数量变动时不用重算所有概率加总即可stock和total_stock分开存方便活动结束后统计发放率。t_user_times用(user_id, day)做唯一键而不是每次 count 记录表是为了避免高频 count 查询把记录表压垮。3. 抽奖核心链路从一次请求到奖品落库3.1 权重算法的实现与坑抽奖的本质是“按权重从候选集合里取一个”。最直白的写法是累加权重法public Prize drawByWeight(ListPrize prizes) { // 只统计库存充足且在架上的奖品 ListPrize candidates prizes.stream() .filter(p - p.getStatus() 1 p.getStock() 0) .collect(Collectors.toList()); int totalWeight candidates.stream().mapToInt(Prize::getWeight).sum(); if (totalWeight 0) { return null; // 全部无货返回兜底 } // 随机落点落在哪个权重区间就中哪个奖 int hit ThreadLocalRandom.current().nextInt(totalWeight); int cursor 0; for (Prize p : candidates) { cursor p.getWeight(); if (hit cursor) { return p; } } return candidates.get(candidates.size() - 1); }逻辑说明先把库存为 0 或下架的奖品剔除避免抽到发不出去的奖把有效奖品的权重加总在其中随机取一个整数落点再遍历累加落点落在哪个区间就返回哪个奖品。这里用ThreadLocalRandom而不是Random是因为前者在多线程下没有竞争抽奖接口并发时性能差别明显。参数上要留意weight的取值。如果某个奖品权重设成 0它永远不会被抽中但仍然会出现在候选列表里浪费一次遍历更隐蔽的问题是如果“谢谢参与”的权重没配所有有效奖品权重加总后会偏小整体中奖率被放大。我的习惯是活动配置时强制要求所有奖品的权重总和为固定值比如 10000这样运营调一个奖品权重其余按比例自动变化不会出现“概率加起来不是 100%”的玄学问题。3.2 库存扣减用条件更新对冲并发概率算对了奖品接下来最难的是扣库存。先讲错误示范// 反例先查再改并发下必然超发 Prize prize mapper.selectById(id); if (prize.getStock() 0) { mapper.updateStock(id, prize.getStock() - 1); }两个线程同时查到stock 1都判断通过都执行更新库存变成 -1。正确做法是把判断塞进 SQL 的 where 条件里靠数据库的行锁保证原子性!-- PrizeMapper.xml -- update idreduceStock UPDATE t_prize SET stock stock - 1 WHERE id #{id} AND stock 0 /updateint affected prizeMapper.reduceStock(prize.getId()); if (affected 0) { // 扣减失败说明库存刚被抢完回滚这一轮重新抽 throw new BizException(奖品已抢完请重试); }affected返回受影响行数等于 1 才算扣成功等于 0 表示条件不满足库存已空。这个方式不依赖任何锁靠数据库自身的行级锁兜底在单库场景下足够稳。要注意的是这段逻辑必须和写抽奖记录、扣用户次数放在同一个Transactional方法里否则扣了库存记录没写库存就白扣了。如果是多库分片或者库存必须放 Redis 的场景常见做法是 Redis 里用DECR扣减扣到负数再回补配合 Lua 脚本保证“判断 扣减”原子执行。项目如果没用到 Redis用上面这套条件更新就够了别为了炫技引入不必要的复杂度。3.3 抽奖次数限制与幂等次数限制的两个核心问题一天能抽几次以及同一次请求重复提交算不算。Transactional(rollbackFor Exception.class) public LotteryResult draw(String openid) { // 1. 抢占当日次数条件更新等价于加锁 int affected userTimesMapper.consumeTimes(openid, LocalDate.now()); if (affected 0) { throw new BizException(今日抽奖次数已用完); } // 2. 按权重选出奖品 ListPrize prizes prizeMapper.listOnline(); Prize prize drawByWeight(prizes); // 3. 扣库存失败则抛异常回滚次数 if (prize ! null) { int stockAffected prizeMapper.reduceStock(prize.getId()); if (stockAffected 0) { throw new BizException(奖品已抢完请重试); } // 4. 落记录 recordMapper.insert(buildRecord(openid, prize)); } return buildResult(prize); }对应的次数扣减 SQLupdate idconsumeTimes UPDATE t_user_times SET used used 1 WHERE user_id #{openid} AND day #{day} AND used lt; total /update这里used total是关键和扣库存同理把“还有没有次数”的判断交给数据库。整个方法包在事务里任何一步抛异常都会把次数恢复回去用户不会白白损失一次机会。幂等这块光靠次数限制不够。用户在弱网下点两下可能发出两个请求第一个还没返回第二个就到了次数被扣两次。常见做法是前端在按钮上做防抖锁后端再加一层请求 token进页面时发一个一次性 token 存 Redis抽奖时带上服务端DEL成功才放行。项目里如果前端没有这套机制至少应该在接口层按 openid 做短时间的重复请求拦截最简单的实现是 RedisSETNX openid:draw 1 EX 3三秒内同一用户重复请求直接拒绝。3.4 兜底奖与保底策略抽奖系统有个必须提前想清楚的问题候选奖品全部库存为 0 时返回什么。drawByWeight里我返回了 null上层如果不处理用户看到的就是“抽奖失败”体验很差。常规做法是配一个type 4的“谢谢参与”权重设得足够大且库存设成一个不会耗尽的大数比如 999999保证任何时候都有兜底结果。保底策略是另一层需求连抽 N 次不中就给个安慰奖。实现方式一般是在抽奖记录表上做 count超过阈值时强制返回指定奖品。这个逻辑别写在权重算法里抽完结果后再判一次改写最终奖品因为保底是业务规则不是概率规则混在一起以后调概率会互相干扰。4. 跑起来与联调本地起服务到微信侧对接4.1 环境准备与启动步骤这套项目本地跑起来不算复杂基本路径是装 JDK 8 或以上、Maven 3.6、MySQL 5.7/8.0可选 Redis。步骤按顺序走# 1. 建库建表 mysql -u root -p -e CREATE DATABASE lottery DEFAULT CHARSET utf8mb4; mysql -u root -p lottery src/main/resources/sql/init.sql # 2. 改配置把数据源指到本地 # application.yml 里改 spring.datasource.url / username / password # 3. 编译打包 mvn clean package -DskipTests # 4. 启动 java -jar target/lottery-0.0.1-SNAPSHOT.jar # 或者开发期直接跑 mvn spring-boot:run启动后访问http://localhost:8080/prize/list能看到奖品列表 JSON说明链路通了。如果这一步就报错先看控制台日志里的 SQL 异常八成是建表脚本没执行或者字段名对不上。Redis 是可选项配置里如果写了 Redis 但本地没起服务启动时连接失败会导致整个应用起不来这种情况要么起个 Redis要么把相关配置注释掉别硬扛。4.2 微信侧对接要注意的几件事项目带微信入口意味着要考虑授权和 openid 获取。常规链路是用户从公众号菜单或小程序进入活动页 → 页面跳转微信授权 → 拿 code 换 openid → 带着 openid 调抽奖接口。后端一般有两个接口配合一个用 code 换 openid一个把 openid 和 session 绑起来返回给前端。本地联调时这块最容易卡住因为微信授权回调域名必须是备案过的线上域名localhost 不行。常规做法是也用一个内网穿透工具把本地端口映射出去配到公众号的网页授权域名里或者干脆在开发环境做个开关跳过微信授权直接用固定的测试 openid 调抽奖接口把业务逻辑先跑通微信联调放到测试环境再做。这个开关一定要做得干净别把绕过授权的代码带到生产环境去。access_token 的缓存也值得提一句。微信接口的 access_token 有调用频率限制且全局唯一必须缓存起来复用。放在 Redis 里设一个比官方过期时间短一点的 TTL比如官方 7200 秒缓存设 7000 秒避免临界点失效。4.3 接口自测清单联调前建议把这几条自己先走一遍能省掉大量和前端来回扯皮的时间场景预期结果检查点正常抽奖返回奖品名和记录 id库存减 1记录表新增一行次数用尽返回“次数已用完”库存不变无新记录奖品库存为 0抽到其他奖或谢谢参与不出现库存为负并发 50 次请求成功次数不超过库存stock最小为 0重复提交只产生一条记录次数只扣一次并发那条可以用ab或 JMeter 打重点看最终库存和记录数是否对得上。我一般会专门跑一轮库存 10、并发 50 的测试跑完查select stock from t_prize where id ?只要出现负数就说明扣减逻辑有问题。5. 避坑排查这几处翻车点我基本每次都能遇到5.1 库存超发记录和库存对不上现象活动结束后盘点奖品发放数量超过总库存。原因通常是扣库存用了“先查再改”的写法或者扣库存和写记录不在同一个事务里。解决扣减一律用update ... set stock stock - 1 where stock 0判断affected行数整条抽奖链路放进同一个事务且事务方法上标rollbackFor Exception.class否则受检异常不会回滚。5.2 概率偏差某个奖品几乎不中现象运营反馈某个权重不低的奖品实际中奖率极低。原因一般是候选列表里混进了库存为 0 的奖品或者权重字段在数据库里是字符串类型比较时按字典序走了。解决抽奖前先过滤status 1 and stock 0并在配置页面加校验所有有效奖品权重总和必须是约定值改完权重打印一次实际概率分布核对。5.3 次数被重复扣用户投诉少了一次机会现象用户说点了一次按钮次数却少了两次。原因在前面提过弱网下重复请求或者前端按钮没锁。解决后端接口做幂等用 RedisSETNX拦截短时间内的重复请求前端按钮点击后置灰拿到响应再恢复次数扣减用条件更新避免used total的脏数据。5.4 事务里调用远程接口库锁等待超时现象活动高峰期出现大量Lock wait timeout exceeded。原因是抽奖方法里调了发券、发消息这类远程接口事务迟迟不提交行锁一直持有。解决把远程调用移出事务边界先在事务内完成扣库存和落记录事务提交后再发券如果发券失败用补偿任务或消息队列重试不要为了强一致把事务拉长。5.5 时区导致“今天”算错现象凌晨零点到八点之间抽奖提示次数已用完或者次数重置了。原因是服务器时区是 UTCLocalDate.now()取到的是前一天或后一天。解决统一在配置文件里指定spring.jackson.time-zone和 JVM 的-Duser.timezoneAsia/Shanghai数据库连接串上也加serverTimezoneAsia/Shanghai三处对齐。6. 二次开发把固定活动改成可配置的抽奖平台拿到这套代码最直接的改造方向是把“写死的活动”变成“多活动可配置”。原项目的奖品和次数大概率是全局一份实际业务里往往要支持同时跑多个活动每个活动独立奖池、独立次数规则。我的改法是在t_prize和t_user_times上都加一个activity_id抽奖接口入参带上活动标识缓存 key 也按活动维度隔开这样一套服务能支撑多个入口。第二个方向是概率策略外置。现在权重写在数据库字段里改一次要动数据。更好的做法是抽奖时从配置中心或缓存里读一份概率表权重值支持热更新运营调概率不需要重启也不需要改库。实现上把drawByWeight的入参从ListPrize换成ListPrizeWeight权重来源抽成独立方法方便替换。第三个方向是抽奖记录和风控打通。真实活动里刷奖是不小的损失常见做法是按 openid 和设备指纹统计抽奖频次短时间内高频请求直接进黑名单抽奖接口前置一层风控校验。这块不要塞进抽奖主逻辑做成拦截器或独立 service命中风控时返回统一文案避免暴露判定规则。验证改造是否成功我习惯先跑那条“库存 10、并发 50”的压力测试再走一遍次数边界把用户次数设成 1连点三次确认只产生一条记录。改完这两条都过了基本就稳了。说个我自己的习惯。每次接抽奖类需求我会先在本子上把“奖品、库存、次数、概率、兜底”五个词写下来然后逼着自己回答并发下这五个词分别在哪一步可能出错。这五个问题答不上来代码写得再顺我也不敢上线。从那以后我每次做这类活动都强制走一遍这个自检省下的返工时间远比写检查的时间多。希望帮到你。本文还有配套的精品资源点击获取
企业数字化 ERP 产品动态
相关推荐
FreeCAD打开STEP/DXF/DWG并测量零件尺寸实战 图纸发过来一堆 stp、dwg、dxf,手上却只有一台没装商业软件的机器——这种场面我碰上过不止一次。用 FreeCAD 打开 stp、dwg、dxf 这类模型文件,再把零件尺寸量出来,是我这几年最常走的应急路径之一。它免费、跨平台、不用注册账号࿰… · 2026/11/5 4:31:40
ZrLog高可用架构的Prometheus+Grafana监控部署指南 1. 项目概述在当今互联网服务架构中,监控系统的重要性不言而喻。作为一位有着多年运维经验的工程师,我深知一个完善的监控体系对于保障业务稳定运行的关键作用。本文将详细介绍如何为ZrLog高可用架构部署基于PrometheusGrafana的监控系统,这套… · 2026/11/5 4:30:40
Word中pt单位的本质:从铅字到屏幕的物理尺寸标尺 1. 为什么“pt”这个单位在Word里总让人摸不着头脑?你是不是也遇到过这种情况:辛辛苦苦调好一段标题的字号,打印出来却发现和屏幕上看到的完全不一样;或者把文档发给同事,对方说“这行距太挤了”,你打开一看… · 2026/11/5 4:30:40
学术论文写作工具全解析:从LaTeX到Word公式编辑 1. 论文写作工具全景概览学术写作从来不是单打独斗的过程。记得我博士期间为了赶一篇JCR一区论文,连续72小时对着Word文档调整公式格式的经历,让我深刻意识到专业工具的重要性。如今市面上涌现的论文辅助工具,早已从单纯的文字处理进化到涵盖… · 2026/11/5 8:38:17
美股量化分析:历史数据接入与处理实战 1. 美股数据分析的基础:历史数据接入实战做美股量化分析的朋友都知道,历史行情数据是策略开发的基石。无论是技术指标回测、波动率统计,还是机器学习特征工程,数据质量直接决定了分析结果的可靠性。去年我在搭建美股量化分析系统时… · 2026/11/5 8:38:17
自动驾驶控制系统:PID与MPC的工程实践 1. 自动驾驶控制系统的核心架构设计在自动驾驶车辆的运动控制中,横向和纵向控制是两个最基础也最关键的子系统。我参与的这套控制系统采用了经典的分层设计思路:纵向控制负责车辆的前后运动(加速/减速),横向控制则掌管… · 2026/11/5 8:38:17
ag-grid-community安装配置避坑:模块注册、样式与容器高度 简介:面向前端开发者与数据密集型后台项目维护者的 ag-Grid Community 开源数据网格资源包,适合需要表格排序、过滤、分组、分页、单元格编辑与自定义渲染的 JavaScript、React、AngularJS 及 Web Components 项目参考。压缩包共 2000 个文件,… · 2026/11/5 8:38:17
自动驾驶横纵向混合控制架构与PID+MPC实现 1. 自动驾驶控制系统的核心架构在自动驾驶领域,横纵向控制是车辆运动控制的基础模块。纵向控制负责车辆的前后运动(加速/减速),横向控制则决定车辆的左右运动(转向)。这套系统相当于人类驾驶员手脚的配合—… · 2026/11/5 8:38:17
中望3D草图歪斜全解析:视图与几何偏移的区分与修复 1. 草图视图歪了到底歪的是什么很多人第一次遇到这个问题时的反应是“视图转一下不就行了”,结果鼠标中键按住转了三百六十度,草图还是斜的,标注尺寸也跟着歪,最后画出来的线全是斜的。这里得先把一个概念掰清楚:中望3… · 2026/11/5 8:37:17
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 3:16:49
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 6:59:29
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 7:02:38
多智能体协作系统架构设计与实践优化 1. 多智能体协作系统的时代背景2017年Transformer架构的诞生彻底改变了人工智能的发展轨迹。当行业还沉浸在单一大模型带来的性能突破时,Anthropic的研究团队已经将目光投向了更前沿的领域——如何让多个智能体通过协作产生超越单体能力的系统智能。这就像交响乐团中… · 2026/11/5 0:00:39
大模型系统性入门实战指南:从原理到部署微调的全链路路线 提到大模型的系统性入门,我先说个结论:这事真不难,难的是资料太散、路线太多,今天这个框架明天那个工具,新人很容易在“收藏夹吃灰”和“教程看到一半放弃”之间反复横跳。市面上关于大模型的内容少说有几十万篇&#… · 2026/11/5 0:00:39
鸿蒙文件系统沙箱机制解析与开发实践 1. 鸿蒙文件系统沙箱机制深度解析作为一名从Android转向鸿蒙开发的工程师,我深刻体会到文件系统访问是迁移过程中最先遇到的"拦路虎"。在Android上运行良好的文件读写代码,在鸿蒙真机环境却频频出现文件写入后神秘消失或直接抛出权限拒绝错误的… · 2026/11/5 0:00:39
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 3:16:49
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 6:59:29
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 7:02:38