基于WebH5+开源方案的局域网年会大屏签到抽奖系统实战
发布时间:2026/10/1 17:34:21来源:尧图网络
去年年底一个做零售的朋友找到我说公司年会要搞签到抽奖两百多人现场大屏要实时人数、入场欢迎、多轮抽奖最好主持人拿自己手机就能控制节奏。他们之前问过几家现成的SaaS报价按人头算还要求把员工手机号传到第三方服务器IT负责人一看就否了。我最后用一套纯Web、开源的思路在局域网里搭了套轻量方案一台笔记本当服务器前后端加起来不到两千行代码两天开发加测试年会当晚全程没翻车。这篇文章不写广告就记录我这次完整落地过程——需求怎么拆、技术怎么选、核心模块怎么写、现场具体踩了哪些坑。年会只是这套方案的一个场景。会销、招商会、新品发布会、经销商大会、培训结业典礼凡是“现场大屏签到抽奖实时互动”的需求底下都是同一套逻辑。把这篇文章看完你自己也能在三天内搭出一套可用的系统。1. 先想明白年会大屏系统本质是一套“现场活动操作系统”很多人一听“大屏签到抽奖”第一反应就是“做个页面放个抽奖按钮”结果做到一半发现越做越复杂。原因很简单你真的在做的是一个小型的实时活动操作系统而不是一个静态网页。1.1 年会全流程里的三个核心场景我把年会当晚的节奏拆成三段每一个场景对系统的要求都不一样。第一段是入场签到通常开场前60分钟。员工陆陆续续到酒店扫桌上的二维码手机浏览器打开签到页填姓名和手机号大屏立刻滚动一条“欢迎张伟入场”。这个阶段拼的是速度一百多人排队谁也不想堵在签到台上填三分钟表单。所以签到页必须极简两个输入框加一个按钮自动聚焦、自动跳转移动端键盘弹出来也不能把按钮顶没。第二段是暖场互动大概是开场前和开场后的过渡时间。大屏不再只是欢迎语改成滚动播放签到完成的员工头像墙、实时签到人数、签到率有条件的话还能让员工发祝福弹幕。这一段拼的是气氛系统需要持续、平滑地推送新数据不能让画面一卡一卡的。第三段是晚宴抽奖通常是后半场的重头戏。主持人说“开始抽奖”大屏上名单快速滚动停下的瞬间名单定格中奖人的姓名和座位信息上屏后台同时记录奖项防止重复中奖。这一段拼的是准确性和仪式感错误绝对不能有——哪怕一个人重复中奖底下的吐槽声都能传到台上。这三个场景看起来是分开的但数据是连续贯通的。签到名单必须直接成为抽奖资格的唯一来源抽奖结束的数据要能留存大屏展示的内容不能和控制后台混在一起。如果上来就写页面很容易写成“签到是一套数据抽奖再导一份Excel”现场一乱就全乱。1.2 容易被忽略的隐藏需求我这次和主办方过需求问了一圈挖出了几个没写在需求文档里、但现场一定会碰到的问题。第一指定人员中奖。年会嘛总有几个奖项主办方是提前商量好归属的。如果系统不支持白名单你只能现场在抽奖池里手动删人非常容易被主持人念错。我在设计时提前留了“指定中奖”的开关白名单人员参与单独一轮抽奖既照顾了安排又不会在正常轮次里穿帮。第二临时补签。总会有人忘带手机、手机没电、或者到场晚了但抽奖前又想把名字加进去。后台必须提供一个“一键补签”入口可以直接在名单里搜索一个人并加入签到池操作要在10秒内完成。第三中奖了但人不在现场。主持人喊名字没人应答这时候要能“重抽”。设计上就是抽奖轮次可以回退——把刚才那轮中奖记录标记为“作废”从抽奖池里重新抽一个补上。第四主持人遥控。抽奖不能总喊一个人站在控制台后面点鼠标主持人和嘉宾在台上更需要用手机控制。所以管理端要做一个移动版主持人扫码登录后手机上就能点“开始抽奖”“停止”。第五已中奖的人不参与后续抽奖。除非是“阳光普照奖”否则每轮抽奖的资格池要动态过滤掉前面已中奖的人。这是现场最容易漏、也是最容易出事故的需求。1.3 为什么“Web开源”是这个场景的最优解想过几个方案最后落到Web和开源上不是偶然。参会者的手机五花八门iOS、Android、老版本系统都有。WebH5只需要一个浏览器扫个二维码就打开不用安装、不用注册、不用登录。小程序和原生App虽然体验更好但需要企业主体、应用审核、公网域名、备案年会日期是死的等不起这些流程。数据可控也很关键。我用局域网部署员工的手机号、姓名全部留在本机数据库里不经过任何第三方服务器。这对很多公司的IT部门来说是一个硬性要求。开发调试也方便。代码开源意味着我可以直接改逻辑——改品牌颜色、改抽奖动画、改签到字段这些都是商用SaaS很难提供给你的定制能力。商业产品为了适配所有客户往往把逻辑封装得很死你要“领导指定中奖”这种功能客服大概率回你“我们支持白名单但得加钱换套餐”。所以结论很清楚做一个轻量的、开源的、可改的Web系统完全为了具体一场活动的流程服务比套用任何“年会系统SaaS”都靠谱。2. 技术选型的决策过程为什么是WebH5加本地服务而不是小程序需求摸清楚之后技术选型就顺理成章了。但这里有几个选择题我还是想单独说一下免得你因为“看别人都在用某个框架”就直接抄。2.1 移动端入口H5、小程序、原生App怎么选我在移动端入口上做了个对比表格可以直接抄方案参会者体验开发/上线成本离线/局域网能力结论原生App最好但需要安装高双端开发好不推荐没人愿意装App微信小程序好扫码即用需企业主体、审核、公网域名差必须联网不推荐年会等不起审核WebH5好浏览器扫码即开低一套代码全平台好局域网IP即可推荐小程序不是不能用而是它的部署链路太长。小程序要求后端接口必须是HTTPS域名而且服务器必须暴露在公网。年会的现场网络经常是临时搭的根本没有公网域名给你用。H5在这种情况下就非常灵活——局域网里给一个IP地址路由器上把端口一开手机扫码就进。2.2 服务端和数据库怎么选服务端我选了Node.js Express Socket.IO理由很实际Node.js的事件模型天生适合WebSocket长连接和广播场景几百个手机保持连接时也不会像同步阻塞模型那么吃力另一方面前后端都写JavaScript一个人一天能写完所有页面和接口调试成本低。数据库用SQLite起步。你可能觉得SQLite太“玩具”但实际算一笔账两百人的年会签到峰值也就是几十个人同时提交抽奖就更少了一分钟一次左右。SQLite单文件、零配置备份直接拷文件完全够用。我用的是better-sqlite3同步接口写起来比async的数据库驱动简单得多。如果是上千人的大会或者同时开多个分会场再考虑换MySQL或PostgreSQL。我的建议是把数据库操作封装成一个接口层前期用SQLite后期换数据库时只改一个模块。2.3 大屏动画技术不要一上来就WebGL大屏端很多人一上来就想上WebGL做那种3D粒子效果。我的建议是先把需求摆出来滚动名字墙、头像墙、人数跳动、抽奖名单滚动、中奖动画。这些用Canvas 2D和CSS3动画就完全够用了。WebGL的问题在于兼容性。年会现场有不少老投影仪、老电视、老电脑显卡驱动不新WebGL上下文可能直接创建失败。而且为了一个名字滚动特效引入一个渲染引擎代码量翻倍调试成本也翻倍。所以我的方案是优先CSS3 transform和opacity动画只有头像墙用了Canvas 2D发现这张卡了再降级成列表滚动。核心原则所有动画在30帧以上就行不要追求60帧炫技稳定优先。2.4 整体数据流一台电脑同时服务手机、大屏和后台整套系统跑在一台电脑上数据流是这样的手机H5页面 - 发起HTTP签到请求 - 服务端写入SQLite - 同时通过Socket.IO广播事件 - 大屏和后台实时收到通知并做对应渲染。管理后台操作抽奖 - 服务端执行抽奖算法 - 写入中奖记录 - 广播中奖名单 - 大屏显示开奖结果。这个架构的关键约束是所有状态都以服务端为准前端只做渲染和交互。哪怕大屏死机了刷新之后重新从服务端拉一次全量状态现场就能立刻恢复。千万不要让大屏页面自己维护一份“签到人数”之类的状态一旦断线重连就对不上了。3. 核心模块逐个拆解签到、大屏、抽奖引擎的代码思路这一节我挑三个最重要的模块把代码思路讲清楚。完整代码太长这里给核心片段你把它们拼起来就是一套可运行的骨架。3.1 签到模块幂等、唯一、可补签签到接口是全场请求量最高的接口也是最容易被并发打崩的地方。先看核心逻辑我用的是better-sqlite3同步接口写起来很直白const db require(better-sqlite3)(data/event.db); const express require(express); const app express(); app.use(express.json()); app.post(/api/signin, (req, res) { const { name, mobile } req.body; if (!/^1\d{10}$/.test(mobile)) { return res.status(400).json({ ok: false, msg: 手机号格式不对 }); } // 利用数据库唯一索引做幂等防止并发重复插入 const existed db .prepare(SELECT id, name FROM participants WHERE mobile ?) .get(mobile); if (existed) { return res.json({ ok: true, already: true, name: existed.name }); } db.prepare( INSERT INTO participants (name, mobile, created_at) VALUES (?, ?, ?) ).run(name, mobile, Date.now()); // 内存计数器 1不回表统计避免 count 查询拖慢接口 state.totalSigned 1; io.emit(signin:new, { name, total: state.totalSigned }); res.json({ ok: true }); });几个值得说的点。第一手机号唯一索引是防重的最可靠手段。只靠后端代码先查再插在并发高时仍可能出现两个请求同时通过“没查到”的判断然后都去插入。所以建表时要把mobile字段设为UNIQUE双保险。第二不要在每个签到请求里执行SELECT COUNT(*)。一开始我图省事签到后立即查一次总数返回给前端做展示结果并发一上来这个查询和写锁叠在一起接口就变慢了。后面改成内存里维护一个计数器只有服务重启时才从数据库恢复一次。第三签到事件用Socket.IO广播大屏收到后即刻渲染替代前端轮询。轮询在两百人同时在线时会产生大量无意义的HTTP请求没必要。补签逻辑就是后台调用同一个接口但走管理员身份校验效果上没有任何区别——补签成功的人一样出现在签到池里一样有资格抽奖。3.2 大屏页面模式分离、断线重连、全量状态恢复大屏端我设计了三个页面/mobile、/screen、/admin互不干扰。大屏页面有个专门的URL参数modescreen进入全屏并且隐藏管理元素。断线重连是现场最容易出问题的点。会议室Wi-Fi不稳定大屏连接断了很正常关键是断线后不能让人去按F5刷新。我在页面里做了自动重连socket.on(disconnect, () { statusBar.textContent 连接已断开正在重连...; }); socket.on(connect, async () { statusBar.textContent ; // 重连成功后拉一次全量状态补偿断线期间可能漏掉的事件 const state await fetch(/api/state).then(r r.json()); renderTotal(state.totalSigned); renderLatestSignins(state.recentSignins); });这里最重要的一行是fetch(/api/state)。WebSocket重连成功只能说明通道恢复了但断线期间现场新签到的人大屏根本不知道。所以每次连接成功后都必须重新拉一次全量状态把漏掉的补回来。大屏的渲染我尽量用简单的方式签到滚动区用CSS3动画定时把新元素插到顶部超过一定数量就把底部元素删掉人数数字用requestAnimationFrame做一个从旧值到新值的渐变避免生硬跳动。这些说起来简单但你实际在86寸电视上看一次就会发现动画的平滑程度直接影响现场氛围。3.3 抽奖引擎公平性、并发控制、奖品池扣减抽奖是整个系统的脸面算法错了其他做得再好都白搭。我的做法是Fisher-Yates洗牌后取前N个而不是每次Math.random()抽一个然后去重。原因很简单洗牌可以保证每个人被抽中的概率严格相等而且不会出现“抽到第十个人时才撞上重复”的尴尬。const { randomInt } require(crypto); function shuffle(arr) { const result [...arr]; for (let i result.length - 1; i 0; i--) { const j randomInt(0, i 1); [result[i], result[j]] [result[j], result[i]]; } return result; } function drawWinners(pool, count) { if (pool.length count) { throw new Error(抽奖池人数不足); } return shuffle(pool).slice(0, count); }为什么索引用crypto.randomInt而不是Math.random()严格来说Math.random()在非加密场景里也够用但crypto.randomInt的随机性更好代码也不复杂。而且它会基于系统熵源生成对与会者来说“更可信”——虽然他们看不到代码但你的内心会更安稳。抽奖请求还要加一把“互斥锁”。现场主持人和管理员手里都可能有控制端两个人同时点“开始抽奖”服务端必须保证同一时刻只有一个抽奖流程在跑。最简单的做法是一个布尔标志let drawing false; app.post(/api/draw, (req, res) { if (drawing) { return res.status(400).json({ ok: false, msg: 正在抽奖中请稍候 }); } drawing true; try { // 1. 取出所有符合本轮资格的人已签到、未中奖 // 2. 洗牌取 N 个 // 3. 事务里写入中奖记录扣减奖池 // 4. 广播结果 } finally { drawing false; } });单机部署下这个布尔标志是够用的如果将来扩展成多实例后端才需要换成Redis锁。年会场景完全没必要把架构搞到那一步。“已中奖的人不参与后续抽奖”这句需求落到SQL上就是资格池的过滤条件WHERE signed_at IS NOT NULL AND awarded_at IS NULL。注意这个条件要放在每轮抽奖开始时动态执行而不是抽奖前一次性生成名单——否则后面补签的人就永远进不了奖池。3.4 “指定中奖”怎么实现这个需求很多开发都遇到过我的方案是用一个manual_win标志位。抽奖请求里带一个type参数如果是manual就从白名单里洗牌整轮中奖如果是normal再从整个资格池里洗牌。中奖记录里写清楚“本轮为指定中奖”后续流程完全一致不影响其他轮次。更稳的做法是把指定中奖的人从总的资格池里排除掉单独抽一轮。这样他们不会在正常轮次里重复出现抽奖结果也不会暴露“指定”痕迹。这也是现场演示时最不会穿帮的方案。4. 现场最容易翻车的几个点我都替你踩了一遍代码写完之后真正的大头是现场部署。这一章写的每一条都是我自己踩过或者亲眼见过的坑。4.1 网络几百人同时扫码Wi-Fi才是最脆弱的环节年会现场的无线网络永远是最不可控的东西。家用路由器连三五十个设备就开始丢包两百人同时在线时基本必挂。所以我的建议很直接要么用公司办公网络请网管开独立SSID和AP要么现场自带一台企业级AP。部署前一定要做一次压力测试。方法很土但有效找十来个同事同屏打开签到页同时狂点签到按钮观察服务端的CPU和接口响应时间。如果十个人都能把接口拖慢那两百人进场时必出事故。另一个容易忽略的问题是2.4G和5G频段。有些手机自动漫游到信号弱的频段页面加载就变得极慢。如果路由器支持最好把2.4G和5G设成同一个SSID并开启频段引导如果不支持就关闭一个频段别让手机自己乱跳。最后一定要准备一个兜底方案。现场实在没网或者路由器挂了我用一台4G路由器顶上去手机和服务端都连它照样能跑。前提是现场必须有运营商信号这个提前测试。4.2 大屏兼容性字体、分辨率、浏览器版本大屏页面是按1920x1080设计的但现场可能是4K电视、老投影仪、甚至一块竖屏广告机。我建议页面布局全部用vw/vh和flex自适应不要写死像素宽度。文字大小用clamp()控制最小和最大尺寸避免在4K屏上大得离谱。字体是大屏最容易翻车的地方。如果你用了在线字体现场没有外网页面会回退成丑陋的系统字。我后来把所有字体文件打包进了项目或者直接用系统自带的中文字体比如苹方、微软雅黑保证离线环境下的观感。浏览器方面现场电脑可能装的是很老的Chrome甚至IE。我的底线是只用ES6语法不用太新的CSS特性能用flex就不上grid关键动画做降级。投屏前把大屏页在目标电脑上完整过一遍别到了现场才打开。4.3 防作弊和现场临时情况处理防作弊不能完全不设防也不能搞得太复杂。我在签到页做了手机号唯一性校验后端还能记录签到时间、IP、User-Agent后台可以看到“同一手机号短时间多次签到”的异常记录。对年会来说做到这一步已经够了。现场比防作弊更常见的是“临时要加人”。办会的人经常会临时塞进几个嘉宾、客户、领导而且要求他们必须能参与抽奖。后台那个“一键补签”按钮就是为这个准备的。操作流程三步打开后台搜索手机号或姓名点补签。整个过程大约5秒不影响主持人节奏。如果中奖者不在现场主持人要求重抽我设计了一个“作废重抽”按钮。点击后会把这轮中奖记录标记为作废但保留历史记录再从当前资格池里洗牌重抽。历史记录一定要留否则最后对账时说不清。4.4 一次真实并发故障的完整排查链路年会开始前20分钟我们遇到了全场唯一一次事故签到接口突然变慢从正常的20毫秒涨到2秒多。第一反应是查服务端日志。日志显示大量的请求都阻塞在数据库操作上但数据库是SQLite只有几十兆理论上不可能这么慢。第二步看进程状态。我用top看了一眼CPUNode进程占用不高IO也不高排除死循环的可能。然后我怀疑是网络——但因为所有手机连的是同一个局域网AP内网流量不该成为瓶颈。第三步看数据库状态。我在服务端临时写了一个监控接口打印数据库连接池和锁等待情况结果发现问题SQLite的写锁被长时间持有。虽然我已经开了WAL模式但WAL改善的是读写并发写写之间依然串行。具体原因是我在签到流程里同步执行了一个查询“本场已签到人数”的统计而它和insert在同一事务里。当几十个手机同时提交签到多个insert开始排队每个insert又等前面的统计查询释放锁整个写链路就串行了。修复方案很粗暴但有效把人数统计从接口链路里拆出去。签到成功后立即返回统计交给内存中的计数器维护只在需要快照时写一次库。改完之后接口恢复20毫秒全场再无问题。这次排查给我的教训是做现场系统必须把“高并发下锁竞争”挂在心上尤其是SQLite这类单写者数据库。日志、监控、快速回滚手段一样都不能少。5. 可复制的落地部署手册从空目录到年会现场前面讲了设计思路这一章直接给操作步骤。跟着做不用一天就能在本地跑起来。5.1 开发环境准备需要的东西很基础一台装了Node.js的电脑版本建议LTS我用的v20一个编辑器一台能跑浏览器的手机。mkdir event-lottery cd event-lottery npm init -y npm install express socket.io better-sqlite3 corsbetter-sqlite3是一个带原生编译模块的库如果安装时卡在编译可以试试先装build-essential或者换Node版本。这一步是我唯一见过别人卡壳的地方。npm源如果太慢可以临时设置一个可用的镜像源但注意这只影响开发环境下载依赖现场运行时完全不需要外网。5.2 项目结构与数据库建表目录结构建议这样组织event-lottery/ ├── server.js # Express Socket.IO 入口路由分发 ├── db.js # SQLite 初始化与公共查询封装 ├── routes/ │ ├── signin.js # 签到接口、补签接口 │ └── draw.js # 抽奖接口、重抽接口、白名单接口 ├── public/ │ ├── mobile/ # 手机签到页 │ ├── screen/ # 大屏页 │ └── admin/ # 管理后台含移动控制端 └── data/ └── event.db # SQLite 数据文件现场备份时直接拷贝三张核心表CREATE TABLE participants ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, mobile TEXT NOT NULL UNIQUE, signed_at INTEGER, awarded_at INTEGER, award_level TEXT, manual_win INTEGER DEFAULT 0 ); CREATE TABLE awards ( id INTEGER PRIMARY KEY AUTOINCREMENT, level TEXT NOT NULL, name TEXT NOT NULL, total_count INTEGER NOT NULL, remain_count INTEGER NOT NULL ); CREATE TABLE draw_records ( id INTEGER PRIMARY KEY AUTOINCREMENT, award_id INTEGER, participant_id INTEGER, drawn_at INTEGER, status TEXT DEFAULT normal -- normal / voided );建表时就写好UNIQUE约束和索引比在代码里做各种判断省心得多。5.3 本地联调电脑、手机、大屏三端对齐启动服务node server.js然后用ifconfigMac/Linux或ipconfigWindows查到电脑的局域网IP例如192.168.1.100。手机连同一个Wi-Fi浏览器访问手机签到页http://192.168.1.100:3000/mobile大屏页http://192.168.1.100:3000/screen管理后台http://192.168.1.100:3000/admin联调时最关键的测试是“手机签到是否能立刻出现在大屏上”。正常情况应该1秒内看到欢迎语和人数变化。如果大屏没反应优先检查WebSocket是否连上再看浏览器控制台有没有报错。双屏投屏建议提前设置好后台控制页放在笔记本主屏大屏页用扩展屏投到电视或投影。这里有个很坑的细节Windows扩展屏的方向如果不设置鼠标可能拖不到第二屏Mac上还要注意镜像和扩展的区别别把控制台也投出去了。5.4 现场检查清单我把每次活动前要检查的东西列成清单打印出来直接打钩检查项操作为什么局域网连通手机能打开签到页判断Wi-Fi和AP是否正常双屏方向鼠标能拖到第二屏避免控制台投不上大屏分辨率用现场电视/投影试跑适配1920x1080字体离线可用断网刷新大屏页避免字体回退数据库备份拷贝data/event.db到U盘现场故障可恢复备用电脑第二台电脑预装同样代码主电脑挂了直接顶上系统休眠设置永不睡眠演出中意外锁屏必翻车电源笔记本插电带排插电量撑不住全晚抽奖前重启清空内存状态重新载入保证状态干净无线频段统一SSID或关闭一个频段避免手机漫游乱跳5.5 容灾方案主备切换容灾不是可选项。我一般会准备两台电脑装一模一样的项目。活动前把数据库初始化好现场如果主电脑出问题直接拔U盘把event.db拷到备用机改一下局域网IP重启服务所有参会者刷新一下签到页就能继续。因为状态都在数据库里备用机恢复的是最新快照不会丢已签到数据。如果连这个时间都没有还有一个更快的方案只换IP不换数据库。把主电脑的网络断掉备用机设成同样的IP服务直接起参会者什么都不用改。前提是备用机上的数据库刚好是同一份。所以我每次活动前都会生成一个统一的数据库文件两台电脑各放一份定时拷贝。6. 沉淀下来的经验这套系统的边界和扩展方向最后聊聊我做完整套东西之后的体会不光是技术层面的。6.1 开源的意义和边界开源的价值不是给你一份现成的代码直接跑而是给你一个“组装思路”。年会系统的代码本身并不复杂真正值钱的是需求理解、部署经验和现场排错能力。所以我很建议你从GitHub找几个签到大屏项目看看把它们拆开再按自己需求组装而不是找到一个项目就拿来部署。许可证方面也要留意如果后续要商用尽量选MIT/Apache这类宽松许可避免GPL传染。6.2 可以扩展的方向做完年会之后我陆续把这套系统扩展到了其他活动上。会销现场的大屏签到、产品发布会的互动问答、培训课程的学员签到、经销商大会的多轮抽奖用的都是同一套骨架。只要把活动ID作为一级参数数据库加一个activity_id字段就能一套代码服务多场活动。再往后可以做会议投票、节目评分、抽奖结果导出、签到率统计报表。这些模块和签到抽奖一样核心永远是一个可靠的服务端状态机加上一个能撑住现场实时性的WebSocket通道。6.3 我的几点体会如果你也要做类似的东西我最想叮嘱三句话。第一稳定压倒一切。大屏动画可以朴素一点但绝对不能关键时刻掉链子。宁可少一个炫酷特效也不能多一个崩溃风险。第二需求一定提前问透。“指定中奖”“临时补签”“重抽”这种东西现场再开发就晚了。多问一句比多做十行代码值钱。第三Plan B必须有。备用电脑、备用AP、备用4G路由器都不贵但现场可能因为你多带了一个排插就救了整场。做现场系统的人本质上是在做风险管理技术只是其中一环。这套东西我后来在公司内部分享过好几次每次讲开场前20分钟那次排查下面都有人点头。希望这篇记录也能帮你少踩几个坑。
网站建设高端定制企业官网