资讯动态

H5口红机源码实战:概率表驱动、权重配置与移动端活动页落地

发布时间:2026/10/14 4:06:56 来源:尧图企业网站定制
简介H5口红机源码是一套基于HTML5的移动端互动游戏开发项目面向具备一定前端或全栈基础的开发者用于快速搭建类似线下口红机的线上抽奖挑战玩法。压缩包约30.52MB内含docx安装配置说明、SQL数据库初始化脚本以及包含HTML/CSS/JavaScript等文件的完整源码目录方便开发者从环境部署到数据库导入一气呵成。项目支持自定义游戏规则、调整难度并扩展奖品逻辑是理解Canvas绘图、JS交互流程和数据库表设计的实用范例。目前已有369人学习下载适合希望通过真实项目练习H5开发、前后端联调与数据表规划的学习者二次开发或仿写。1. H5口红机源码是一套游戏更是一次概率营销我在门店做过一次活动真正留得住人的不是那台机器而是扫码后手机里那个 H5 口红机小游戏。H5口红机源码的本质就是把线下口红机“光柱循环、点击停止、落位开奖”的互动搬到浏览器里再套一层奖品概率接口。它解决的是商家最关心的三件事怎么让用户玩起来、中奖率怎么控、参与次数怎么限。适合网页前端新手拿来做活动页练手也适合门店运营的开发者把它改造成公众号或小程序里的引流工具。要理解这套源码先记住一个反直觉结论结果不是靠手气而是在点击前已经被概率表算好动画只是回放。2. 拆开源码包文件结构、核心循环与奖品权重先把文件结构搞清楚才知道哪些是 C 位哪些是陪跑。常见的口红机 H5 项目会把代码分成前端游戏、静态资源和极简后端三大块。h5-lipstick/ ├── index.html ├── css/ │ ├── base.css │ └── game.css ├── js/ │ ├── config.js │ ├── game.js │ ├── audio.js │ └── request.js ├── assets/ │ ├── images/ │ └── sounds/ └── server/ ├── prize.php └── log.php这个结构不是每个源码都一样但绝大多数能跑的 H5 口红机都跑不出这个骨架。js/config.js是全局参数js/game.js是游戏主逻辑server/prize.php决定你抽到什么。如果只是想在本地看效果把server/log.php删掉不影响运行但不要把config.js删了因为game.js开局就会读window.GAME_CFG读不到直接白屏。我一般会先开一个本地静态服务器把页面跑起来然后打开浏览器调试工具看 Network 面板里有没有红色请求。如果只有页面没有数据问题通常出在request.js里请求地址是相对路径还是绝对路径。很多源码默认请求路径是/server/prize.php本地预览根目录是项目目录时能通部署到子目录时就要改成./server/prize.php。这个改动不影响逻辑但影响能不能抽到奖。建议先备份一份原版再动路径后面写概率验证脚本还要用到。2.1 文件职责删之前先看引用关系把文件拆成三堆必须保留的包括index.html、css/、js/、assets/images/可以按需删的是audio.js、server/log.php不要轻易删的是server/prize.php。判断依据很简单index.html里script引了谁谁就是必需品。比如audio.js被删掉后游戏依然能跑但中奖时没声音体验会打折扣。server/prize.php被删掉后前端只能用本地随机函数任何人都能打开控制台反复调start()刷奖品。这块源码的第二个隐藏点藏在图片命名上。口红机面板每个格子对应一张图片文件名通常是p1.png到p8.png和config.js里的id一一对应。如果换成自己的图片最好保持相同命名和尺寸否则会出现口红图标被压缩变形。图片尺寸建议做成正方形透明底 PNG边上留 10% 安全距离这样动画移动时不会穿帮。许多二次开发翻车不是逻辑改错而是图片路径写错。2.2 游戏主流程先定奖品再演动画这是整个 H5 口红机最核心的一点。很多新手拿到源码后第一反应是找“碰撞检测”其实口红机 H5 根本没有碰撞检测它只有三段逻辑请求结果、移动光柱、展示结果。我见过一个版本把随机写在动画完成之后结果用户截屏时看到光柱已经压中奖品却弹了个“谢谢参与”这就是典型的时序错误。正确的做法是先向服务端要结果再让动画去“追”那个结果。核心代码简化后是这样// js/game.js 核心流程简化版 function start() { if (playing) return; // 防止连点导致定时器叠加 playing true; clearInterval(timer); request(/server/prize.php, { uid: currentUser }, function (res) { const prizeId res.prizeId; // 服务端已经算好抽到什么 const targetIndex prizeToIndex(prizeId); // 奖品 id 映射到面板下标 const extra 2 * prizeList.length targetIndex; // 至少先走两圈 let step 0; timer setInterval(() { step; moveLight(step % prizeList.length); if (step extra) { clearInterval(timer); showResult(prizeId); playing false; } }, cfg.speed); }); }参数含义prizeList.length是面板格子数量线下实体机一般是 8 格H5 版常见 5 到 8 格cfg.speed是每次移动的间隔毫秒数太短看着像抽筋太长用户会不耐烦240ms 左右比较接近实体机的手感extra里乘 2 是为了让用户至少看到两圈循环避免刚点开始就停在面前那种“秒停”会让人觉得有猫腻。整体思路是动画循环次数可以变但最终停的位置必须由targetIndex决定其余时间只是在转圈。有人会问如果服务端挂了怎么办正常做法是前端请求失败时直接提示“网络异常请重试”而不是继续本地随机。因为一旦放开本地随机用户就能通过断网、重放请求等手段反推奖品全部绕过服务端。所以哪怕只是 Demo也建议保留请求失败分支不要让游戏在没有服务端时“假装开奖”。2.3 权重表怎么配从固定概率到动态干预权重表通常写在config.js里然后由前端传给服务端做随机。也有的版本把权重写在 PHP 文件里。不管放在哪理解权重而不是百分比是第一步。// js/config.js window.GAME_CFG { speed: 240, prizes: [ { id: 1, name: 正装口红, weight: 5, stock: 10 }, { id: 2, name: 小样口红, weight: 15, stock: 50 }, { id: 3, name: 优惠券, weight: 30, stock: 200 }, { id: 4, name: 谢谢参与, weight: 50, stock: 0 } ] };这个配置中总权重是 100所以 id1 占 5%id4 占 50%。但不是所有源码总权重都是 100有的写 1、2、3此时实际中奖率是该项权重除以总权重奖品weight占比正装口红55 / 100 5%小样口红1515 / 100 15%优惠券3030 / 100 30%谢谢参与5050 / 100 50%如果运营说“把正装口红概率压到 1%”你要做的是把权重 5 改成 1同时保持其他权重相对不变。但更清晰的做法是统一取总权重为 10000每个权重是该奖品概率乘以 100。比如 5% 就是 5001% 就是 100。这种写法能避免运营按百分比填结果算错。动态干预的常见玩法是“连续中奖降权”。服务端在每次抽奖后把近 10 次结果存起来如果某奖品连续出现 3 次以上就临时把它的权重减半。这个逻辑会放在后端而不是前端否则用户可以改 JS。我在模拟项目里看到的实现是// server/prize.php 动态降权片段 $recent getRecentHits(10); // 近10次抽奖记录 if (count($recent) 3 array_sum($recent) 3 * $recent[0]) { // 前三次都是同一个奖品权重减半 $prize $prizes[$recent[0]] ?? null; if ($prize) $prize[weight] max(1, $prize[weight] / 2); }这里的getRecentHits可以从 Redis 或数据库读取真实的线上环境更推荐 Redis 的列表结构。动态干预不是必须的但能避免“头奖连出”导致商家亏本。设置权重时还有一条红线stock为 0 的奖品必须在下发前过滤否则会出现“抽中了正装口红但库存为 0”的投诉。过滤逻辑最简单就是后端unset掉无货项。如果要在前端看到库存可以展示“已售罄”状态但真实可中奖的库存永远放在服务端。3. 跑起来从本地 HTTP 到微信内调试H5口红机源码的好处是不需要安卓或 iOS 工程一个浏览器就能跑。但要分清“能看页面”和“能完整开奖”是两码事。想跑完整流程本地至少要能执行 PHP如果只跑前端动画Python 自带 HTTP 服务就够了。3.1 本地环境不装 Node 也能启动先进入源码根目录开一个静态服务器cd h5-lipstick # 方式一纯前端预览用 Python python3 -m http.server 8000 # 方式二带后端接口用 PHP 内建服务器 php -S 0.0.0.0:8000 -t .方式一适合你看动画、调样式但请求/server/prize.php会返回 404。方式二会把整个目录作为 Web 根目录同时处理 PHP 文件这样request.js里的接口才能通。用0.0.0.0监听是因为等会要让手机访问如果只监听localhost手机就进不来。如果你电脑上装了 PHP 但运行不到检查一下 PATH 环境变量Windows 下很多源码包自带的集成环境也能用不一定要把 PHP 配到全局。启动后在浏览器地址栏输入http://localhost:8000。如果页面显示一张口红机背景图但没有按钮多半是css/game.css没加载看看是不是项目名带空格导致路径解析错误。我在本地通常先开静态服务器再在调试工具里把手机模拟器打开把窗口宽度调到 375 像素这个尺寸接近主流手机的逻辑宽度。纯前端看完后再切到 PHP 方式跑一遍完整开奖确认接口日志里有prize.php请求。提示在 Windows 下如果php -S启动后页面能打开但接口 404先检查是不是在项目根目录执行而不是在server/子目录执行。3.2 移动端适配视口、触摸、全屏与防缩放H5 口红机是单手操作按钮通常就一个“开始/停止”所以触摸反馈比桌面端更重要。index.html 头部至少要这样写meta nameviewport contentwidthdevice-width, initial-scale1, maximum-scale1, user-scalableno meta nameformat-detection contenttelephonenomaximum-scale1和user-scalableno在很多新浏览器里被忽略了但微信内置 WebView 的老内核仍会遵守保留它们可以减少双指缩放。format-detection是为了防止手机号或数字被识别成拨号链接尤其是奖品文案里出现“888”这种数字时。CSS 上加一行* { -webkit-tap-highlight-color: transparent; }能让点击高亮消失否则按钮每次触摸都会闪一下灰框。移动端最容易忽略的是 300ms 点击延迟。现代浏览器一般没了但旧 WebView 还在解决办法是给按钮加touch-action: manipulationbutton { touch-action: manipulation; user-select: none; -webkit-user-select: none; }这里的原理是告诉浏览器这个元素禁止双击缩放系统可以提前触发点击不用再等待双击判断。如果你在真机测试时发现“点了没反应”先加这个样式再检查 JS 绑定的是click还是touchstart。很多源码两者都绑导致一次点击触发两次按钮会出现“弹一下又弹一下”的效果。建议统一改成pointerup事件兼容触摸和鼠标同时避免在 PC 浏览器里因为触摸事件残留导致触发两次。3.3 联调准备日志验证与接口模拟要保证 H5 在手机浏览器里能正常开奖不只是页面显示对还需要验证接口联通。常见做法是先在电脑上打开页面F12 控制台手动执行// 模拟调试在没有后端时快速验证前端流程 window.__MOCK__ true; function request(url, params, callback) { if (window.__MOCK__) { setTimeout(() callback({ prizeId: 4 }), 300); } }如果源码里request是全局函数这段代码可以在页面加载后覆盖。实际源码里request可能被定义为局部函数那就需要直接打开js/request.js改几行代码。这样做的意义是把“服务端问题”和“前端动画问题”隔离开如果模拟返回 id4 时前端能正常落到“谢谢参与”格说明动画没问题剩下的就是接口联调。真机联调时手机和电脑要在同一局域网电脑上跑php -S 0.0.0.0:8000 -t .然后手机浏览器访问http://电脑IP:8000。Windows 查看 IP 可以用ipconfigmacOS 用ifconfig | grep inet。在手机上打开后点“开始”如果接口跨域页面会提示失败这时看 Network 面板错误信息是 403 还是 404。404 是路径不对403 多半是跨域问题需要到 PHP 文件里加响应头。我建议在本地联调时把接口返回的 JSON 在控制台打出来确认prizeId是数字而不是字符串因为有些源码用字符串和数字做映射判断会在时全挂。4. 配置项与后端校验把“运气”握在运营手里把游戏跑通只是第一步。口红机不是玩具它是营销工具运营最关心的是“能不能调概率”“能不能看出谁中了”“能不能防止刷”。这一章讲配置和校验。4.1 奖品配置图片、名称、库存在config.js中奖品数组的每一项都决定面板上一个格子。常见的字段如下字段类型说明示例idnumber奖品唯一标识对应图片名1namestring前端展示名称正装口红weightnumber权重越大越容易中5stocknumber库存0 表示不可抽10imgstring图片路径assets/images/p1.png配置改写步骤先把assets/images/下图片换成自己品牌图命名p1.png到pN.png然后在config.js里同步 name 和 id。注意img字段不是所有源码都有有的版本直接从id拼路径这时不要加img否则读不到。如果你的源码没有stock字段建议在config.js里加一个stock但不要用它做真实扣库存只作为前端展示。真实库存要在服务端数据库里扣减否则同一份奖品会超发。4.2 后端校验别让用户直接调用 JS 函数新版浏览器开发者工具是开放的任何人都可以打开控制台执行源码里的函数。如果不做后端校验用户会反复调start()把口红机变成刷单工具。所以上线时必须让prize.php不再无条件返回奖品而是先做“身份校验、次数校验、库存校验”三层检查。代码骨架// server/prize.php ?php session_start(); // 1. 身份生产环境用 openidDemo 可以用 uid 参数模拟 $uid $_POST[uid] ?? guest; // 2. 次数校验这里用 session 演示线上用 Redis 做 $today date(Ymd); $key draw_count_{$uid}_{$today}; $count (int)($_SESSION[$key] ?? 0); if ($count 3) { http_response_code(403); exit(json_encode([error reach_limit])); } // 3. 库存过滤 $prizes loadPrizesFromDb(); // 从数据库读取 $pool array_filter($prizes, function ($p) { return $p[stock] 0; }); if (count($pool) 0) { http_response_code(403); exit(json_encode([error sold_out])); } // 4. 按权重抽奖 $total array_sum(array_column($pool, weight)); $rand mt_rand(1, $total * 100) / 100; $cursor 0; foreach ($pool as $prize) { $cursor $prize[weight]; if ($rand $cursor) { // 抽中后扣库存并记录日志 deductStock($prize[id], $uid); echo json_encode([prizeId $prize[id]]); break; } }这里把$_SESSION当存储只是演示。真实环境里多个用户共享内存Session 文件方案会在并发稍微高一点时出现锁竞争。建议用 Redis 的 INCR 命令加过期时间第 4.3 节会提到。loadPrizesFromDb和deductStock是占位函数生产环境换成数据库操作。注意mt_rand是 PHP 的随机数生成器比rand()更离散不要用array_rand因为array_rand是按数组下标均匀随机而不是按权重。4.3 参与限制次数、时间窗口除了抽奖逻辑还要把活动开关和参与限制做成配置项。有些源码把限制写死在game.js里比如“每天 3 次”但运营要改次数就得改代码发版很蠢。后端接口可以给一个独立的配置接口前端每次进入时拉取// 前端启动前拉活动配置 fetch(/server/config, { credentials: include }) .then(res res.json()) .then(data { if (!data.active) { showToast(活动已结束); return; } window.DAILY_LIMIT data.dailyLimit; // 每天可抽次数 initGame(); })// server/config.php 返回活动配置 header(Content-Type: application/json); echo json_encode([ active true, dailyLimit 3, startTime 2025-01-01 09:00:00, endTime 2025-01-31 22:00:00 ]);前端拿到dailyLimit之后再调用抽奖接口这样运营只改配置不用动前端。但注意次数判断永远在后端做前端显示剩余次数只是一个“预估”因为可能有多个页面标签同时打开。时间窗口同样要后端判断不能只靠前端隐藏按钮否则用户改时间还能抽。这里还能加一个“分享解锁”的玩法抽奖前要求把页面分享到好友或群聊服务端通过分享回调确认后再发放一次额外次数。分享解锁属于社交裂变实现时不要让前端自己上报“我分享了”否则会被脚本刷正确做法是用短链参数带上分享者标识服务端校验来源。5. 避坑清单真机白屏、概率失控与音效失效的四个典型坑这里写踩过最多的高频坑。每条按现象、原因、解决展开。5.1 真机白屏 / 点击没反应现象电脑上正常手机浏览器打开后只看到背景图点击开始按钮没有任何反应。原因十次里有七次是config.js没有被正确加载。game.js里第一行就读window.GAME_CFG一旦config.js因为路径错误或加载顺序不对没执行整段脚本抛异常页面后面的按钮绑定全部失效。另外按钮绑定了click但没处理 300ms 延迟用户点了但系统还没把点击事件派发出来加上没有视觉反馈看起来就是没反应。解决先在桌面浏览器控制台看有没有GAME_CFG is not defined之类的报错有就检查index.html中script srcjs/config.js是否在game.js之前。然后给按钮样式加touch-action: manipulation并把事件绑定改成pointerup。如果源码里用的是click不要直接改成touchstart因为touchstart会在手指按下瞬间触发和用户视觉上的“按下去”不同步。最稳妥是pointerup兼容鼠标和触摸。改完一定要到真机测一遍模拟器不能完全复现触摸时序。5.2 音效在 iOS 上静音、安卓爆音现象安卓手机第一次玩有声音iOS 手机完全没有音效或者点按钮后隔几秒才播放。原因iOS 浏览器对音频播放有严格限制页面在用户没有触摸之前不能创建音频上下文程序里一进页面就new AudioContext()会被静音。另一个原因是源码在每次调用play()时新建Audio对象iOS 只允许同一个用户手势触发的音频实例播放。安卓爆音通常是音频文件本身的问题比如采样率是 48kHz 但没有编码好或者声音文件是立体声且超长iOS 能解码但音量异常。解决在用户第一次点击“开始”时执行一次音频解锁函数function unlockAudio() { if (window.__unlocked) return; const ctx new AudioContext(); ctx.resume(); window.__unlocked true; } document.getElementById(startBtn).addEventListener(pointerup, unlockAudio);播放音效时不要每次都new Audio()而是在页面加载时预创建 2 到 3 个Audio实例循环使用。音效文件建议统一转成 44.1kHz、16bit、单声道的 MP3时长控制在 1 秒以内。如果你只是做活动页甚至可以把音乐素材全部删掉避免兼容性麻烦。5.3 概率失控——权重算错导致头奖爆率现象运营把weight从 5 改成 1结果抽了 100 次头奖出了 18 次跟预期 1% 完全偏离。原因源码里实际用的是百分比数组还是权重表没有先确认。如果代码用Math.floor(Math.random() * total)而total不是权重总和而是奖品数量概率完全失真。还有一种常见错误是权重区间写成和边界重叠导致头奖区间被多占一个点。另外如果stock过滤逻辑放在权重计算之后无货项被抽空后权重仍参与计算导致头奖实际占比被抬高。解决写一个独立模拟脚本把权重表跑一万次看分布是否等于“权重/总权重”。这个脚本不依赖游戏页面直接在 Node 或浏览器控制台跑。如果偏差超过正负 2%检查随机边界条件// 区间写法一定要左闭右开 let rand Math.random() * total; // 0 rand total for (const item of prizes) { if (rand item.weight) return item.id; rand - item.weight; }这里用的是而不是同时rand是浮点数不会出现整数化造成的区间重叠。如果源码里写Math.floor(rand)记得把总权重也放大避免概率集中到尾部。概率验证后还要确认stock为 0 的奖品在进入权重算法前就被排除否则它会占着百分比。5.4 安卓返回键直接退出页面 / 接口跨域 403现象用户在安卓微信里点完开始看到结果后想返回按了一次物理返回整个页面直接退出活动数据没上报。另一个常见现象是页面从主域名打开接口却在另一个域名请求被浏览器拦截返回 403。原因H5 页面默认只有一个历史记录入口安卓返回键行为等于浏览器后退页面关闭。跨域则是因为前后端没有设置 CORS 响应头有的源码只在部分接口写了Access-Control-Allow-Origin漏掉了预检请求。解决对于返回键在页面加载后主动压入一个历史栈并监听popstatehistory.pushState(null, null, location.href); window.addEventListener(popstate, function () { history.pushState(null, null, location.href); });这样用户按返回键时会先触发popstate并重新压回一个空记录页面不会关闭哪怕连续按两次也只会回到页面初始状态。这个技巧对活动页尤其是抽奖结算非常有用可以避免用户误关闭。对于跨域在server/prize.php顶部手动加上header(Access-Control-Allow-Origin: . ($_SERVER[HTTP_ORIGIN] ?? *)); header(Access-Control-Allow-Credentials: true); header(Access-Control-Allow-Headers: Content-Type); if ($_SERVER[REQUEST_METHOD] OPTIONS) { http_response_code(204); exit; }安全做法是不要把HTTP_ORIGIN直接回显而是做一个白名单判断如果来源不在允许列表里就不返回该头。但很多活动页不需要登录可以用*配合credentials: omit。如果接口必须带 session cookie就必须回显具体来源同时打开Allow-Credentials否则浏览器会拦截。6. 上线前最后一件事先做一次千次模拟验证这里的模拟验证不是把页面点一千遍而是用脚本直接打接口或权重算法把随机分布跑出来。很多源码在本地抽 10 次看不出问题一旦并发上来概率就歪了。先写一个纯粹的权重模拟脚本模拟 10000 次抽奖// simulate.js 在源码目录外层运行 const prizes [ { id: 1, weight: 5 }, { id: 2, weight: 15 }, { id: 3, weight: 30 }, { id: 4, weight: 50 } ]; const total prizes.reduce((sum, p) sum p.weight, 0); const result {}; for (let i 0; i 10000; i) { let rand Math.random() * total; let chosen prizes[prizes.length - 1].id; for (const p of prizes) { if (rand p.weight) { chosen p.id; break; } rand - p.weight; } result[chosen] (result[chosen] || 0) 1; } console.log(权重分布, result); // 预期 id4 出现在 5000 次附近误差超过 200 就要查代码跑完看比例id4 应该在 50% 附近id1 在 5% 附近。如果 id4 占比超过 52%说明权重算法里存在取整偏差或者total被错误改成奖品数量。这个脚本独立于源码不需要引入 config.js因为它模拟的只是抽奖算法不是整个页面。接下来验证服务端接口。先用 curl 连续打接口 100 次每次换一个 uid避免被次数限制挡住for i in $(seq 1 100); do curl -s -X POST http://localhost:8000/server/prize.php -d uidtest$i echo done result.log grep -o prizeId:[0-9]* result.log | sort | uniq -c注意uniq -c对 JSON 字符串去重能看到每个prizeId出现的次数。这个方法的缺点是没有把库存扣减和写日志逻辑算进去所以它只验证随机算法不验证并发扣库存。并发扣库存要再用压测工具或者用三条脚本同时请求同一个 uid观察扣减后是否出现负库存。最后给页面加一个“落单不上报”的补救机制。很多手机上用户抽完奖立刻切后台ajax请求可能没发出去。用navigator.sendBeacon在pagehide时补发结算数据window.addEventListener(pagehide, function () { navigator.sendBeacon(/server/log.php, JSON.stringify({ uid: currentUser, prizeId: lastPrizeId })); });sendBeacon比普通 fetch 更可靠它在页面进入后台时把数据交给系统发送不阻塞页面卸载。要注意pagehide里的数据量不要太大只传必要字段后端再根据uid和时间戳去重。我上一次上线前偷懒只测了 100 次结果权重参数被运营改成“1、15、30、50”后总权重从 100 变成了 96头奖实际概率成了 1.04%运营按 1% 配库存表面看没问题但因为有库存过滤和动态降权上线第一天头奖就被抽走两箱。从那以后我每次都会强制把千次模拟脚本和服务端分布核对跑一遍改一次配置跑一次上线前再跑一次希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑