简介:一套可直接部署的H5口红机互动游戏源码,面向具备一定编程基础的开发者,适合用于商场抽奖、线上营销或移动端娱乐场景,可快速搭建在线口红机小游戏。压缩包约30.52MB,内含安装配置文档、SQL数据库脚本与第五版完整源码工程,覆盖前端页面绘制、游戏逻辑控制、数据库初始化等模块;其中docx文档对服务器环境、部署步骤和前后端集成做了说明,sql脚本则预置了等级、用户、奖品等基础数据。已有369人学习浏览。借助源码,开发者可自定义中奖概率、奖品列表与界面视觉,也可系统练习H5 Canvas绘图、JavaScript交互、CSS3动效及MySQL配置等技能。整体结构清晰,既能作为二次开发的基座,也能作为Web游戏开发教学案例。 口红机这类H5互动玩法,在零售和美妆行业里其实并不新鲜了,但这两年因为可以嫁接在微信公众号、企业微信甚至直接投放到直播间,重新火了起来。前阵子帮一家线下美妆集合店做了一套“H5口红机源码”定制方案,正好把整个从零到上线的过程完整跑了一遍。这篇就围绕这套源码方案,说说我当时的设计思路、核心参数处理和几个实际踩过的坑,给正要动手做类似H5互动的朋友一个参考。
1. 整体方案设计:先想清楚口红机的本质是什么
虽然叫“口红机”,但回归到产品本质,它就是一个抽奖类H5,只是把传统大转盘、刮刮卡的形式换成了“扭蛋/夹娃娃”的视觉外壳。用户点击启动,转盘开始减速旋转,最终停在一个格子上,对应不同的奖品档位。对运营方来说,这类玩法转化链路短、用户参与门槛低,适合做拉新、复购引导和私域引流。
1.1 为什么选择H5形态而不是小程序或App
口红机这个场景天然适合H5,原因有三点:
- 跨端分发方便:一套代码可以塞进微信公众号菜单、企业微信会话窗口、朋友圈广告落地页,甚至用Uniapp包装成App。实体店线下扫码也无缝衔接。
- 免安装、轻交互:用户点开即玩,路径极短,符合化妆品这类冲动消费、碎片化场景的用户心态。
- 版本迭代灵活:商家换个活动主题、改个奖品比例,运营可以直接在后台配置,前端H5发布即可,不需要走应用商店审核流程。
我做这套源码的核心技术栈是:前端基于原生H5 + Canvas实现转盘动画,后端采用Node.js + Redis做奖品发放和防刷控制。之所以不直接用容易找到的纯静态开源版本,是因为商家既要转盘好看,又要求能后台配置奖品,并且要防止被脚本撸羊毛。
1.2 功能需求拆解
开工之前,我把需求拆成了下面几个核心模块:
- 用户端H5:设备适配、转盘动画、抽奖交互、中奖结果弹窗、奖品填写与兑换记录。
- 运营后台:奖品池配置(名称、库存、概率)、抽奖次数限制、中奖记录列表、兑换核销。
- 服务端接口:抽奖动作、概率计算、库存扣减、防并发与防刷、数据埋点。
- 安全机制:签名校验、用户唯一标识(openid/sessionuid)、令牌防重放。
这里特别提醒一句:如果你只是想要一个展示用的Demo,那张口就来的网上“H5口红机源码”确实够用;但一旦涉及真实奖品、真实库存和真实投放,后端逻辑必不可少,否则被刷到你怀疑人生。
2. 核心细节解析:转盘绘制、概率算法与动画实现
2.1 Canvas绘制转盘,而不是CSS方案
转盘部分我选用了原生Canvas绘制,而非CSS3 + transform拼图。原因是:Canvas能灵活支持任意数量的扇形分区,颜色、文案、奖品图片每格独立控制,而且在动画旋转时性能表现更稳定,不至于在低端安卓机上出现闪烁。
核心实现大概如下:
function drawWheel(canvas, prizes) { const ctx = canvas.getContext('2d'); const len = prizes.length; const angle = Math.PI * 2 / len; const radius = canvas.width / 2; const centerX = radius; const centerY = radius; for (let i = 0; i < len; i++) { const startAngle = i * angle; const endAngle = (i + 1) * angle; // 绘制扇形底色 ctx.beginPath(); ctx.moveTo(centerX, centerY); ctx.arc(centerX, centerY, radius, startAngle, endAngle); ctx.closePath(); ctx.fillStyle = prizes[i].bgColor; ctx.fill(); // 描边 ctx.strokeStyle = '#fff'; ctx.lineWidth = 2; ctx.stroke(); // 绘制文字(奖品名) ctx.save(); ctx.translate(centerX, centerY); ctx.rotate(startAngle + angle / 2); ctx.textAlign = 'right'; ctx.fillStyle = '#fff'; ctx.font = 'bold 14px sans-serif'; ctx.fillText(prizes[i].name, radius - 14, 5); ctx.restore(); } }绘制时注意两个细节:文字旋转用的是ctx.rotate(),旋转的基准是圆心,所以要先把画布原点平移到圆心再旋转;每个扇形先填充再描边,避免边框被覆盖。字体大小需要根据奖品名称长度动态适配,防止文案超出扇形边界。
2.2 抽奖算法:如何保证预设中奖率又可干预
抽奖算法是整个源码的灵魂,也是商家最容易理解错的地方。转盘停在哪一格,不是“随机的”,而是先决定中奖结果,再反推转盘停靠角度。
我这里的概率配置采用权重方式,运营后台设置每个奖品权重,权重越大命中率越高:
function getPrizeIndex(prizes) { const totalWeight = prizes.reduce((sum, p) => sum + p.weight, 0); let random = Math.random() * totalWeight; for (let i = 0; i < prizes.length; i++) { random -= prizes[i].weight; if (random <= 0) { return i; } } return prizes.length - 1; }算法本身不复杂,但有个很关键的点:计算结果和最终转盘动画必须一一对应。前端拿到接口返回的奖品索引后,再根据该扇区的角度计算应该旋转到的目标角度,不能先转盘再出结果,否则就会出现“指针明明指着一等奖,弹窗却显示谢谢参与”的尴尬。
2.3 缓动动画:旋转到指定格子的关键
转盘动画我用的是二阶缓动函数,先加速后减速,模拟物理惯性。计算核心在下面这段:
function rotateWheel(targetAngle, duration, callback) { const startAngle = currentAngle; const totalRotation = 360 * 5 - startAngle % 360 + targetAngle; const startTime = performance.now(); function frame(now) { const elapsed = now - startTime; const progress = Math.min(elapsed / duration, 1); const eased = easeOutQuart(progress); const angle = startAngle + totalRotation * eased; canvas.style.transform = `rotate(${angle}deg)`; if (progress < 1) { requestAnimationFrame(frame); } else { currentAngle = angle; callback && callback(); } } requestAnimationFrame(frame); }totalRotation这里我特意加了360 * 5,也就是至少转5整圈再停,这样视觉上有足够的“转起来”效果,不会让人感觉只挪动了一格,参与感和仪式感会强很多。
2.4 多端适配与真机调试
H5运行环境复杂,特别是下半屏在部分安卓浏览器和低版本微信中被输入法/底部栏遮挡,适配必须用视口单位+安全区域处理。转盘区域的整体宽度、操作按钮位置我都采用响应式布局,同时也注意了刘海屏的safe-area-inset-bottom。建议真机自测时候着重看PC Chrome、iOS微信、安卓微信、iOS Safari几类环境,性能和交互表现差异比较大。
3. 工程化落地:从源码骨架到完整前端项目
3.1 项目目录结构与开发环境
这一版前端我采用了轻量化的vue3 + vite + pinia组合,也方便后续运营快速二开。项目结构如下:
h5-lipstick-machine/ ├─ src/ │ ├─ views/ # 页面:首页/中奖记录/奖品填写 │ ├─ components/ # 组件:转盘/弹窗/抽奖按钮等 │ ├─ store/ # pinia状态:用户信息、抽奖状态 │ ├─ api/ # 接口请求封装 │ ├─ utils/ # 工具库:签名、坐标转换、格式化 │ └─ App.vue ├─ index.html └─ vite.config.js3.2 设备/环境判断与免登方案
很多线下口红机的投放渠道是微信,因此我用了基于url的code换openid方案,也就是“H5免登录授权”。用户在微信内点击链接进入活动页,静默获取openid,绑定用户ID后即视为已登录,避免额外注册步骤带来的用户流失率。
export async function ensureLogin() { const query = getUrlQuery(); if (!query.code && isWeChat()) { const appId = config.appId; const redirectUrl = encodeURIComponent(window.location.href); const authUrl = `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${appId}&redirect_uri=${redirectUrl}&response_type=code&scope=snsapi_base&state=ok#wechat_redirect`; window.location.replace(authUrl); return; } return fetchUserInfo(query.code); }通过微信静默授权拿到openid,整个流程非常快。注意:这一步依赖已认证的服务号;如果主体类型是订阅号或未认证账号,code换openid的接口权限是没有的。如果后续还要发券,会涉及更复杂的微信卡券接口,同一个openid体系可以共用,不用重复开发用户系统。
3.3 封装防连点与并发保护
抽奖按钮必须做“防连点”处理。用户手速再快,也不该发出多个抽奖请求。另外,接口层同一openid必须做并发控制,否则可能出现库存超卖、点亮多份等问题。服务端我用Redis的SET NX EX做锁:
const lockKey = `lottery:lock:${openid}`; const isLocked = await client.set(lockKey, '1', 'NX', 'EX', 5); if (!isLocked) { return { code: 429, msg: '操作太频繁,请稍后再试' }; } // ... 业务抽奖、扣库存 await client.del(lockKey);在设置Redis锁时要注意锁过期时间不能太短,否则一个抽奖请求还没处理完,第二个请求已经能继续打进来了;但也不能太长,否则用户正常刷新或偶发重试时会被锁住一段时间。我这里取的是5秒,实战中根据业务响应速度微调即可。
3.4 环境配置:HBuilder X与uniapp场景的注意点
由于商家后来提了句“最好也帮我做一版小程序或者App”,所以我源码里也预留了uniapp包装口。如果你打算用HBuilder X跑这套H5,有几个地方需要提前处理:
- 转盘image资源路径要使用绝对路径
/static/...,避免在App打包后资源404。 - Canvas在uniapp的app端与H5端的API不完全一致,如果只是H5目标可以忽略;如果要跨端,建议转盘直接绘制成一张图片,再用CSS控制旋转,这样可以保持各端表现一致。
- 下图层的底部安全区需要给
page设置padding-bottom: env(safe-area-inset-bottom),否则在iPhone X及以上机型会顶到Home Indicator。
这套方案在HBuilder X里新建项目时选择“Vue3项目”即可,组件和页面基本不用改,只是把部分<div>换成<view>,转盘重新适配一下尺寸。
4. 老生常谈但必须聊透的常见问题与排查技巧
4.1 为什么转盘停下来后中奖结果对不上
这是客户反馈最高频的问题,十有八九出在“后端发奖结果”与“前端动画目标角度”不同步。排查思路很简单:先抓包看接口返回的prizeIndex,再在浏览器里直接打断点看rotateWheel传入的targetAngle,两者不一致的话,检查是否存在接口重复请求,或者前端缓存了旧结果。线上环境如果还有问题,大概率是抽奖成功但发放奖品动作在异步队列中失败,建议在结果弹窗里增加一个“背包/记录”页,让用户能自助查看兑换码。
4.2 安卓低端机转盘卡顿,或者直接白屏
转盘卡顿的一个元凶是高分辨率Canvas。解决办法是在window.devicePixelRatio大于1时,对Canvas画布做物理像素缩放。举个例子:逻辑尺寸是375 × 375,高分辨率屏需要画成750 × 750的物理实际尺寸,再通过CSS把显示宽度锁回375px,否则字体和边缘是模糊的。
const dpr = Math.max(window.devicePixelRatio, 1); canvas.width = logicalWidth * dpr; canvas.height = logicalHeight * dpr; canvas.style.width = logicalWidth + 'px'; canvas.style.height = logicalHeight + 'px'; ctx.scale(dpr, dpr);白屏多半发生在较老的内置浏览器不支持Canvas API或ES6语法。没精力做兼容的话,至少保证核心转盘页面使用requestAnimationFrame降级方案,关掉H5的GPU加速设置。
4.3 H5点击按钮后没有反应,控制台无报错
大概率是事件绑定区域被透明度为0的遮罩层盖住了。检查CSS中定位元素的z-index,以及点击目标的最上层DOM是不是pointer-events: none。这在弹窗、loading动画和转盘重叠时特别容易出问题。我习惯在调试时给所有层级加一个临时高亮边框,用肉眼快速看是哪个元素挡住了点击热区。
4.4 iOS低电量模式转盘动画异常
这个坑比较特殊,但也值得一说:iOS在“低电量模式”下会对requestAnimationFrame做降频处理,导致转盘动画看起来一顿一顿的。处理办法是动画帧里做时间差校准,用performance.now()算出来的实际时间差去设定rotation角度,不要简单按帧累加。
5. 运营视角的补充:奖品设置与防刷的一点点经验
从商家运营的角度来看,口红机这类活动的奖池设计一般采用“低门槛出奖+高价值抽奖”的组合。实际操作上,我会在后台配置三档奖品,效果相对稳定:
| 奖品档位 | 奖品示例 | 权重范围 | 发放节奏 |
|---|---|---|---|
| 高价值 | 正装口红 | 1-2 | 固定库存,每天控制数量,核销后释放 |
| 中价值 | 小样/中样/礼包 | 5-10 | 关联门店核销 |
| 低价值 | 优惠券/积分/谢谢参与 | 80-120 | 可以低频或高频均可,主要看预算 |
日常冷启动阶段,建议中奖率不要低于60%,否则用户根本没有继续玩下去的欲望;但活动高峰(比如节假日大促)可以把高价值奖品的权重压得非常低,把原子逻辑放在后端配置里调整,前端完全不用发版。
至于防刷,除了前面说的Redis锁之外,再加一层限制——同一openid每天抽奖次数上限(如3次),以及同设备deviceId维度的限制。至于更进阶的IP维度和风控模型,一般体量的活动用不到,不要为了防刷牺牲过多体验。
6. 部署上线与性能优化
部署上,我倾向将H5静态资源(js/css/img)放CDN,接口服务独立部署(Node或Nginx反代),用复制的HTTPS域名接入H5。性能优化方面,做了下面几件事,实测首屏渲染速度提速一倍多:
- 转盘背景和奖品图标合并成雪碧图,减少图片请求数。
- 抽奖接口在进入页面的空闲期预加载,比如用户滑到按钮附近时提前请求配置和用户状态。
- 骨架屏和loading动画:转盘Canvas在图片未加载完成时先渲染底色占位,避免白屏焦虑。
- 按需加载:中奖记录页和奖品填写页都用懒加载,只在用户触发跳转时加载JS分包。
另外提醒句:如果被微信内置浏览器缓存坑过,静态资源文件名可以增加hash,并在nginx服务器配置长的Cache-Control;活动配置和奖品文案不要直接写死在前端包里,建议走接口实时下发,避免用户命中旧版本。
这套源码整体的工程化程度不高,但结构和注释都比较清晰,二次开发成本低。最近很多做线下门店的老板也在聊“口红机H5”的玩法,核心还在脚本之外:中奖概率、活动节奏、用户回流机制才是活动效果的胜负手。代码是骨架,运营才是灵魂。如果上手过程中遇到问题,欢迎按照上面讲的核心思路排查,祝各位一次跑通。
本文还有配套的精品资源,点击获取