简介这份资源面向微信平台互动营销开发入门者与中小商家提供一套可直接参考的砸金蛋小游戏实现方案用于公众号活动引流、品牌推广与用户互动场景。包内共6个文件以4张png图片素材、1个php后端脚本和1个html页面为主图片承担金蛋与界面视觉元素html负责游戏页面结构php处理数据交互压缩包约91KB体量轻便便于快速查阅与二次修改。已有218人学习下载说明该案例在微信小游戏开发学习中具有一定参考价值。读者可从中了解微信公众号与JS-SDK的接入思路、HTML5与CSS3构建游戏界面的方式、JavaScript实现砸蛋随机逻辑与奖品分配的方法以及后端数据存储和奖品库存管理的基本做法同时涉及防刷奖、SQL注入防护等安全考量适合作为课程设计或营销活动原型的技术参照。1. 微信开发砸金蛋一个活动页从入口到发奖的完整落地做微信活动页的开发者大概都遇到过这种需求运营要在公众号里挂一个砸金蛋抽奖用户点进去砸开蛋中奖了填手机号没中奖提示下次再来。听起来简单真动手就会发现坑不少——微信授权怎么拿 openid、奖品库存怎么防超发、用户重复砸怎么拦、页面在低端安卓机上卡不卡。这个「微信开发砸金蛋」项目就是围绕这套场景做的完整实现适合两类人一是手里有公众号、想快速上线一个抽奖活动的开发者二是想拿一个真实微信 H5 项目练手、搞清楚授权链路和并发扣减怎么写的同学。下面按「它是什么 → 怎么跑起来 → 核心代码怎么读 → 怎么改」的顺序拆能直接照着复现。2. 技术选型与目录结构为什么是这套组合2.1 前后端分离的轻量组合这类活动页的特点是生命周期短、并发集中在开奖瞬间、部署要快。所以技术选型上没必要上重框架常见做法是前端用原生 HTML CSS JavaScript 配一个轻量动画库后端用 Node.jsExpress或 PHP 起一个薄服务数据库用 MySQL 存奖品和记录Redis 做库存计数和防重。这个项目走的就是这条路线前端不引 React/Vue减少首屏体积微信内置浏览器对原生页面兼容性最好后端接口只做四件事——微信授权换 openid、查用户剩余次数、执行砸蛋扣库存、写中奖记录。为什么不用现成的抽奖 SaaS因为奖品规则、次数限制、发奖方式每个运营都不一样二次开发成本反而更高。自己写一套核心逻辑两三百行就能覆盖改起来心里有数。2.2 目录结构逐层看项目目录大致长这样读源码时按这个顺序进wechat-egg-smash/ ├── client/ # 前端 H5 │ ├── index.html # 活动主页面 │ ├── css/ │ │ └── main.css # 布局与蛋的动画样式 │ ├── js/ │ │ ├── app.js # 页面逻辑授权、砸蛋、弹窗 │ │ └── hammer.js # 锤子动画与点击判定 │ └── img/ # 蛋、锤子、背景图 ├── server/ # 后端服务 │ ├── app.js # Express 入口与路由注册 │ ├── routes/ │ │ ├── auth.js # 微信网页授权 │ │ └── lottery.js # 抽奖核心接口 │ ├── service/ │ │ ├── prize.js # 奖品池与概率计算 │ │ └── stock.js # Redis 库存扣减 │ ├── config/ │ │ └── index.js # appid、secret、数据库配置 │ └── sql/ │ └── init.sql # 建表语句 └── package.jsonclient是纯静态资源扔到任意静态服务器或 CDN 都能跑server是接口层负责和微信服务器、数据库打交道。这种拆分的好处是前端可以独立调试后端接口用 Postman 就能验证不用每次都真机走微信授权。2.3 数据表设计sql/init.sql里至少要有三张表这是整个活动的数据底座表名作用关键字段user存微信用户openid唯一、phone、created_atprize奖品配置id、name、total、remain、probabilityrecord中奖/参与记录openid、prize_id、status、created_atprize.probability存的是权重而不是百分比比如三个奖品权重分别是 1、5、94总和 100抽中第一个的概率就是 1%。用权重的好处是运营调整时只改数字不用保证加起来正好 100代码里做归一化就行。record.status用来区分「已中奖待发」「已发奖」「未中奖」发奖环节靠它做对账。3. 跑起来从配置到真机验证的完整步骤3.1 环境准备与配置填写先把仓库拉下来装依赖cd wechat-egg-smash/server npm install然后改server/config/index.js把公众号的参数填进去module.exports { appId: 你的公众号AppID, appSecret: 你的公众号AppSecret, // 授权回调地址必须是公众号后台配置过的域名 redirectUri: https://your-domain.com/auth/callback, redis: { host: 127.0.0.1, port: 6379 }, mysql: { host: 127.0.0.1, user: root, password: 你的密码, database: egg_smash } };这里有个血泪经验redirectUri的域名必须和公众号后台「网页授权域名」完全一致包括协议和端口。很多人本地用localhost调试结果微信直接报 redirect_uri 参数错误因为微信不接受 localhost。常见做法是本地起服务后用内网穿透工具映射一个临时域名或者干脆在测试服务器上调试。3.2 初始化数据库与启动服务建库建表mysql -u root -p -e CREATE DATABASE egg_smash DEFAULT CHARSET utf8mb4; mysql -u root -p egg_smash server/sql/init.sqlinit.sql里会插入几条示例奖品数据方便你启动后直接看到效果。接着启动 Redis 和后端redis-server node server/app.js看到server running on 3000就说明接口层起来了。前端client目录用任意静态服务器托管比如npx serve client然后浏览器打开对应地址。注意前端app.js里有个API_BASE常量要改成你后端服务的地址否则请求会打到错误的地方。3.3 微信授权链路验证真机验证前先在微信开发者工具里打开「公众号网页调试」填入你的页面地址。授权流程是这样的用户进入页面 → 前端跳转微信授权链接 → 微信回调到redirectUri→ 后端用 code 换 openid → 重定向回活动页并带上用户标识。后端routes/auth.js的核心逻辑const axios require(axios); const config require(../config); // 微信网页授权用 code 换 access_token 和 openid async function getOpenid(code) { const url https://api.weixin.qq.com/sns/oauth2/access_token; const res await axios.get(url, { params: { appid: config.appId, secret: config.appSecret, code, grant_type: authorization_code } }); // 微信返回里 errcode 非 0 表示失败常见是 code 已使用或过期 if (res.data.errcode) { throw new Error(微信授权失败: ${res.data.errmsg}); } return res.data.openid; }code只能用一次有效期五分钟所以后端拿到后要立刻换换完把 openid 写进 session 或签名 token 返回给前端。前端后续请求带上这个标识后端据此判断「这个用户今天还能砸几次」。调试时如果一直卡在授权页先看后端日志里微信返回的 errmsginvalid code多半是重复使用了 coderedirect_uri 参数错误就是域名没配对。4. 核心模块拆解砸蛋请求从进入到返回的链路4.1 前端点击判定与动画触发砸蛋的交互核心是「点中蛋才算数」。hammer.js里用点击坐标和蛋的位置做碰撞判断而不是简单给蛋绑 click因为锤子挥下去有个动画过程用户可能点在蛋边缘。简化后的判定逻辑// 判断点击点是否落在某个蛋的圆形范围内 function hitTest(clickX, clickY, egg) { const dx clickX - egg.centerX; const dy clickY - egg.centerY; // 半径留 10px 容差手指点击不会那么精准 return Math.sqrt(dx * dx dy * dy) egg.radius 10; } // 砸蛋入口先播动画动画结束再请求接口 function smash(egg) { if (egg.broken) return; // 已砸开的蛋不能重复砸 playHammerAnimation(egg, () { egg.broken true; requestLottery(egg.id); // 动画播完才发请求体验更顺 }); }这里的关键是「先动画后请求」。如果先请求再播动画网络慢的时候用户会看到蛋没反应以为没点上又猛戳结果触发多次请求。把请求放在动画回调里既保证视觉反馈即时又天然限制了单次点击只发一次请求。当然这只是前端拦截真正的防重必须靠后端。4.2 后端抽奖接口的完整处理routes/lottery.js是整个项目最需要看懂的代码它要在一个请求里完成校验用户 → 扣次数 → 算奖品 → 扣库存 → 写记录。顺序不能乱尤其是扣库存和写记录之间要考虑失败回滚。router.post(/smash, async (req, res) { const { openid } req.user; // 授权中间件解析出的用户 const today new Date().toISOString().slice(0, 10); // 1. 检查今日次数key 按天过期天然实现每日重置 const countKey smash:count:${openid}:${today}; const used await redis.incr(countKey); if (used 1) await redis.expire(countKey, 86400); if (used 3) { return res.json({ code: 4001, msg: 今日次数已用完 }); } // 2. 按权重抽奖 const prize await prizeService.draw(); // 3. 中奖则原子扣库存扣失败说明被抢光降级为未中奖 if (prize) { const ok await stockService.decr(prize.id); if (!ok) { await recordService.save(openid, null, 未中奖); return res.json({ code: 0, data: { win: false } }); } } // 4. 写记录 await recordService.save(openid, prize ? prize.id : null, prize ? 待发奖 : 未中奖); res.json({ code: 0, data: { win: !!prize, prize } }); });逐段说第一步用 Redis 的incr做计数incr是原子操作并发下不会出现两个请求都读到「已用 2 次」然后都放行的情况这是防超砸的关键。key 里带日期过期时间设 86400 秒第二天自动归零不用写定时任务。第二步prizeService.draw()按权重随机内部把所有权重加起来做区间映射。第三步扣库存用 Redis 的decr配合判断返回值如果扣完小于 0 就说明库存没了这时候要把这次抽中的结果作废、按未中奖处理避免超发。第四步落库status标记待发奖运营后台按这个状态导出名单发奖。4.3 库存扣减的原子性保障service/stock.js单独拎出来讲因为这是最容易翻车的地方const redis require(../redis); // 原子扣减先 decr 再判断小于 0 则回补 async function decr(prizeId) { const key stock:prize:${prizeId}; const remain await redis.decr(key); if (remain 0) { await redis.incr(key); // 回补保证库存不被扣成负数 return false; } return true; }为什么不用「先 get 再判断再 decr」因为那三步之间有空隙两个请求同时 get 到 1都判断通过然后都 decr库存就变 -1 了。decr本身是原子的扣完看结果负数就回补这个模式在秒杀场景里很常见。回补这一步不能省否则库存会被永久扣掉一个实际可发数量变少。数据库里的prize.remain字段作为持久化备份活动结束后以 Redis 为准做一次对账同步。5. 避坑与常见问题排查5.1 授权回调死循环现象是页面一直在「正在授权」和跳转之间来回。原因通常是回调地址处理不当后端换完 openid 重定向回活动页时活动页又检测到没有用户标识再次发起授权。解决方法是回调后把 openid 写进 URL 参数或 cookie活动页入口先判断有没有标识有就直接进没有才跳授权。另外检查redirectUri是否带了多余的 query 参数微信对回调地址的匹配是严格的。5.2 奖品超发现象是后台导出名单发现某奖品发了 120 份但配置只有 100 份。原因多半是库存扣减没做原子操作或者扣库存和写记录不在一个事务语义里写记录失败后库存没回补。解决方法是按 4.3 的decr模式扣库存写记录失败时手动incr回补并且活动结束后用record表按prize_id分组统计和prize.total对账差多少一目了然。5.3 低端安卓机动画卡顿现象是蛋的碎裂动画在部分机型上掉帧锤子挥下去半天没反应。原因是用了 CSSbox-shadow和大量 DOM 节点做粒子效果。解决方法是动画尽量用transform和opacity这两个属性走 GPU 合成粒子数量控制在 20 个以内图片提前preload避免砸开瞬间才加载碎裂图。真机测试别只用自己的手机找一台两三年前的安卓机跑一遍最稳。5.4 重复砸蛋请求现象是用户快速连点后端记录里同一个 openid 出现多条同一秒的记录。原因是前端动画没播完就允许再次点击或者网络慢时用户以为没反应。解决方法是前端在请求发出后给蛋加pending状态锁请求返回前忽略后续点击后端靠 Redis 计数兜底即使前端漏了次数限制也能拦住超额请求。双保险缺一不可。5.5 微信 JS-SDK 签名失败如果页面用到分享、拍照等能力需要引入 JS-SDK 并做签名。现象是config:invalid signature。原因通常是签名用的 URL 是前端location.href但实际参与签名的是去掉#后面部分的地址或者 access_token 缓存过期。解决方法是后端统一用「当前页面 URL 去掉 hash」做签名access_token 和 jsapi_ticket 都做缓存并设置提前刷新别每次请求都去微信拉。6. 二次开发把抽奖规则改成可配置的拿到这个项目后最常改的就是抽奖规则。原始代码把概率写死在prize表里运营想临时调整得改数据库不优雅。我的做法是加一张activity_config表把「每日次数」「奖品权重」「活动起止时间」都做成配置项后端启动时加载进内存配一个管理接口支持热更新。这样运营改规则不用重启服务也不用碰代码。具体改造分三步。第一步建表CREATE TABLE activity_config ( id INT PRIMARY KEY AUTO_INCREMENT, config_key VARCHAR(64) UNIQUE, config_value TEXT, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP );第二步在service/prize.js里把draw()改成从配置读取权重并加一层缓存let weightCache null; async function loadWeights() { const rows await db.query(SELECT id, probability FROM prize WHERE remain 0); weightCache rows; return rows; } async function draw() { const list weightCache || await loadWeights(); const total list.reduce((s, p) s p.probability, 0); let rand Math.random() * total; for (const p of list) { rand - p.probability; if (rand 0) return p; } return null; }第三步加一个POST /admin/reload接口调用loadWeights()刷新缓存管理后台改完配置点一下「生效」即可。验证方法是改完权重后连续抽 1000 次统计各奖品出现次数和配置的权重比例对比偏差在合理范围内就说明生效了。改造时有个细节要注意weightCache是进程内变量如果后端多实例部署每个实例的缓存刷新时机不一致会出现短暂的概率偏差。常见做法是把配置存 Redis各实例读同一个 key或者干脆接受这个短暂不一致毕竟活动页对概率的实时性要求没那么高。我一般会在管理接口里加个「刷新所有实例」的广播简单点就用 Redis 发布订阅。从那以后我每次做这类活动页都会先把「次数限制、库存扣减、记录落库」这三件事的原子性在纸上过一遍再动手写代码因为线上翻车基本都出在这三处。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?