这年头还搞年会抽奖简直良心企业既然请了我少不得给客户弄个年会抽奖程序我今年陆续接了几个小项目其中一个特别有意思——帮客户做年会抽奖程序。说实话接之前我心里还在嘀咕现在好多公司年会都是线上办奖品直接快递到家连热闹都省了。这客户居然还坚持线下搞年会、搞现场抽奖确实算得上良心企业了。不过吐槽归吐槽活儿得干得漂亮。年会抽奖这事看着简单——不就是从名单里随机挑几个幸运儿吗但真上手做门道还挺多直播大屏上的视觉效果要够炫抽奖节奏要能调动全场情绪中奖名单要经得起所有人质疑关键时刻程序还不能掉链子。这篇文章我把整个从零到一的过程拆开写一遍包括技术选型、核心代码、现场运维还有那些文档里不会写的坑。给准备接类似活儿的朋友留个参考也给企业行政或负责年会的同学一个底稿——你可以拿去跟技术沟通需求心里有数。1. 内容整体设计与思路拆解1.1 抽奖程序的核心需求是什么先别急着写代码。拿到这个需求我第一件事是跟客户确认到底要什么。一通聊下来整理出下面几个核心诉求支持多人同时在线抽奖抽奖过程要公开透明防止员工质疑“内定”。现场有大屏投放能实时看到抽奖者头像/名单滚动视觉上要热闹。支持多轮抽奖每轮奖品不同参与人范围可能需要按规则调整比如只限本部门、只限未中奖者。要有氛围感抽中时最好有音效、动画甚至能带动全场一起喊。数据要可追溯中奖记录要留档之后要发奖。这五条看着简单但每条背后都对应一个技术决策。比如“公开透明”意味着随机算法不能是简单的Math.random()拍脑袋需要有过程可验证性——哪怕只是伪随机也要让现场的人觉得公平。我做了一个双随机设计大屏端不断滚动名字主持人喊“停”的同时前端请求后端接口接口内部通过时间戳加名单总数计算落点并且在滚动画面的中间位置弹出“锁定中奖者”的过程动画让全场都看到系统是在一堆名单里捞人而不是预先指定。再说“参与范围调整”。这个老板说“第一轮阳光普照抽个十个人第二轮抽管理层第三轮抽没中过奖的人。”这涉及名单分组和状态过滤。技术上如果不提前设计好抽奖池模型现场临时改需求会非常被动。所以数据层必须预先做好人员分组部门、是否管理层、是否已中奖等维度每轮抽奖动态生成候选人列表而不是全公司一锅端。最容易被忽视的需求是“抽中后的名单不可重复中奖”。很多初版抽奖程序就是随机从不重复集合里挑没考虑如果一个人重复中奖怎么办。根据之前参加年会的经验绝大多数员工对“重复中奖”极其敏感哪怕是阳光普照抽十个人也会有人嚷嚷“这不公平”。所以我在第二轮开始就做了已中奖人员自动排除的逻辑除非客户明确要求重复参与。1.2 技术方案选型为什么不用现成抽奖软件既然市面上有那种付费的抽奖软件或者H5工具为什么还要自己写一个程序我当时也犹豫过后来跟客户算了一笔账结论是自己写成本可控、可定制、不用受制于人。付费抽奖软件或网页端的抽奖工具优势是即开即用但痛点也很明显界面模板固定换公司Logo、换背景、换字体都需要额外花钱定制。随机逻辑是黑盒员工如果较真你没办法解释“这个软件凭什么选他”。数据导入有限制很多工具只支持固定Excel模板部门分组、管理标签、历史中奖记录这些字段没法灵活处理。一旦年会当天现场出问题技术支持基本找不到人或者回复极慢。自己写一个至少能做到三件事完全掌控视觉与交互、随机过程足够透明、任何问题随时现场改代码。虽然开发成本高一些但后期维护和现场应变能力是现成工具比不了的。做技术的应该都有体会我们多花的其实是前期的两三个晚上但求个踏实。技术栈我选了最简单可靠的一套后端用 Node.js 或者 Python Flask 都可以我最后用的是 Python Flask写随机接口和名单管理都很顺手。前端用原生 HTML CSS JavaScript配合 CDN 引入的动画库Animate.css和音效库Howler.js不用重框架。原因很简单年会只有一个晚上我不想在部署现场因为 React 或者 Vue 的构建流程而手忙脚乱原生三件套随时能改、随时能跑。数据存储全部放在内存和本地 JSON 文件不引入数据库。名单顶多几千人内存完全够用JSON 文件读写也方便年会结束直接把文件打包存档就行。注意选型不一定要用多高级的技术最关键的是稳定、可控、快。年会现场没人关心你用了什么框架他们只关心大屏能不能正常滚动、抽奖结果能不能服众。1.3 双屏设计中控端与大屏端的联动逻辑年会现场最有意思的体验是主持人、中控人员和观众看到的内容不是完全一样的。我设计了两套界面中控端主持人/操盘手页面用于控制抽奖池、查看已中奖名单、启动抽奖、设置每轮奖品和数量。中控端只有一台笔记本能访问不对外投影。大屏端观众页面投放到年会大屏幕只展示滚动名单、抽奖过程动画、中奖结果保留基本的连接状态。大屏端不能直接操作抽奖防止误触。这套中控和大屏的分离好处是安全。操盘手在中控端点“开始抽奖”大屏端做出视觉反馈等到抽奖结果出来中控端可以提前看到名单再决定何时向全场公开。比如老板可能在台下示意“这一轮多抽一个”这时候中控端直接加数量再启动大屏端完全无缝衔接。两个页面之间通信一开始考虑用 WebSocket但年会环境网络复杂不一定允许后端开长连接。后来换了更稳的方案轮询接口。中控端每次发起状态变更后端更新一个“当前轮次状态”的文件或内存对象大屏端每隔500毫秒拉一次这个状态。延迟完全可接受而且断了重连也不会有复杂的同步问题。2. 核心细节解析与实操要点2.1 名单数据结构的预处理抽奖程序能不能撑住现场很大程度取决于名单数据有没有提前整理干净。我接手客户发来的 Excel 时里面各种问题重名、人名前后带空格、部门字段对不上、有人离职忘删、还有写在备注栏里的一大段话。如果直接读进去抽奖抽中了才发现这个人早走了那场面就尴尬了。我的做法是开发一个清洗脚本用 Python 的 pandas 库读 Excel然后做几件事统一字段至少要包含工号或唯一ID、姓名、部门、是否管理层、是否特邀嘉宾。去重。重名的用工号区分没有工号的用“部门姓名”组合判断。过滤无效数据。删除空白行、离职标记、测试人员。自动打标签。比如“五周年员工”、“十年员工”、“本命年”等等为特殊抽奖轮次做准备。清洗后生成一个 JSON 文件结构类似[ { id: E001, name: 张三, department: 市场部, role: 员工, tags: [五年老员工, 未中奖] }, { id: E002, name: 李四, department: 技术部, role: 管理层, tags: [未中奖] } ]这个环节建议至少提前一周跟客户确认。如果客户给的是纸质版名单让行政帮你敲一份电子版反复核对人数。万一现场抽奖时发现漏人说实话挺影响信誉的。我在这个项目中检查了三遍把“在职且到场”的名单单独摘了出来没到场的员工也拉了一个名单备用以防现场有人说“某某出差了应该算进去”。2.2 随机算法的公平性与随机过程的可视化说到随机很多初级做法是random.randint(0, len(list)-1)够用但现场做演示会很干巴。我做了两层改进第一层名单洗牌。每轮抽奖开始前后端先把候选名单做一次 Fisher-Yates 洗牌打乱顺序。这样即使是同一批名单每轮展示时的滚动顺序都不相同看起来更随机。第二层落点选择。洗牌后的名单依然是一个数组停的时候选哪个位置如果是完全随机直接取random_index random.randint(0, current_count-1)然后从洗牌后的数组中取出arr[random_index]。为了防止“每次都是列表中间那几个位置”的观感我还加入了一个偏移逻辑random_index的取值加入正态分布的扰动少数时候会偏向数组开头或者结尾这样看起来更像真实抽奖。但真正让现场觉得“公平”的不是算法多复杂而是随机过程的呈现方式。大屏端在滚动名单的时候每200毫秒切换一个高亮位置大家的眼睛都跟着滚。主持人喊“停”之后大屏不会立刻停而是有一个“减速—停止—放大高亮—弹出中奖卡片”的过程这中间至少有2秒钟的悬念时间全场气氛会瞬间拉满。这个体验远比直接闪一下结果要强。2.3 奖项轮次与概率的配置化设计年会抽奖基本不是单轮通常有四到五轮阳光普照、三等奖、二等奖、一等奖、特等奖、老板加码奖。每一轮的人数、奖品名、参与人群都可能不一样。我把轮次配置做成了 JSON 配置项放在后端启动时加载{ rounds: [ { name: 三等奖, prize: 蓝牙耳机, count: 10, participants: all_active, exclude_winners: true }, { name: 二等奖, prize: 机械键盘, count: 5, participants: dept:市场部,技术部, exclude_winners: true }, { name: 特等奖, prize: 大红包, count: 1, participants: all_active, exclude_winners: true, allow_additional: true } ] }有个细节allow_additional这个字段是我特意加的。因为老板现场喜欢“再抽一个”、“要不加个奖”如果没有这个配置现场就得改代码。有了它每一轮抽奖结束后中控端可以直接“追加抽一个名额”后端会从剩余未中奖名单里再选一个人流畅且不容易出错。概率设计上阳光普照这种大范围抽奖就不要单独设权重了人人平等抽起来才开心。如果是“互动小游戏”转盘类的环节概率配置另说那种要保持可控——比如要给大奖设一个千分之几的触发率。但年会这种纯抽奖我强烈建议不要调任何“暗箱概率”现场两千双眼睛盯着真出问题你解释不了。2.4 中奖记录的持久化与数据回溯抽奖不是抽完就结束了发奖环节要核对名单年会后财务要审批备案。所以从第一轮开始所有中奖数据都要落盘。我设计了一份 JSON 记录文件每抽中一个人后端立刻写入{ round: 三等奖, prize: 蓝牙耳机, time: 2025-01-15 20:33:10, winners: [ {id: E001, name: 张三, department: 市场部}, {id: E004, name: 王五, department: 技术部} ] }这个文件一方面方便年会结束后导出 Excel 发给行政另一方面万一现场有人质疑“怎么又是他”你可以拿出中奖记录逐轮核对。我做的中控端还加了一个“实时中奖列表”页面按轮次展示所有已经抽出的名单字要大、信息要全方便领导当场确认。建议条件允许的话中控端加一个“全流程录屏”按钮录下从开始抽奖到全部结束的屏幕过程。出现问题直接回放比任何解释都有用。3. 实操过程与核心环节实现3.1 环境准备本地开发与现场部署开发阶段我用的是最简单的本地环境Python 3.10安装 Flask、pandas。前端放在 Flask 的 static 目录直接跑python app.py访问本地 5000 端口。现场部署时直接用这台笔记本当作服务器接入年会现场的网络交换机中控端和大屏端都通过局域网访问同一台机器的 IP。现场网络环境一定要提前踩点。我之前遇到过一次现场 WiFi 连接人数太多大屏端刷新变得很慢。后来学聪明了年会前一周专门去现场看了一圈网络状况如果现场有有线网络接口优先拉网线没有的就自己带一个千兆路由器把后台机器、中控笔记本、大屏投放设备组成一个独立的小局域网不走现场 WiFi。这样既稳定又避免被别人蹭网。部署到现场前的检查清单笔记本电源和备用电源。网线至少两根、转接头一个。大屏的 HDMI 线或者无线投屏设备。提前十分钟启动服务检查大屏画面比例16:9 还是 4:3很多现场屏幕比例特殊。预备一台备用笔记本把整个项目文件拷进去万一主力机出问题五分钟内顶上。3.2 前端大屏滚动抽奖的实现思路大屏端最核心的是滚动名单动画。我用原生 JS 写了一个非常轻量但效果不错的方案把所有候选人姓名渲染为一个列表每 200 毫秒通过修改scrollTop实现纵向滚动同时在中间固定的一个窗口高亮当前“选中”的人名并用 CSS 渐变制造一种“飞速闪过”的模糊感。滚动速度和节奏很讲究。我调了几个版本最后定下来滚动阶段每秒切换 6 个人名太快眼睛跟不上太慢没有紧张感。点击“停止抽奖”后滚动不是瞬间停而是用 2 秒时间逐步减速先从 100% 速度降到 60%再降到 30%最后定格。这个减速过程用requestAnimationFrame控制流畅不卡顿。定格之后我加了一个 500ms 的停顿再弹出中奖卡片。卡片上有名字、部门、头像占位图配上撒花动画和一段“恭喜恭喜”的音效。这段反应时间看上去是表演实际上非常重要——它在给后端留出写记录的时间也避免主持人和全场观众因为“太快看到结果”而觉得有猫腻。前端音效我推荐用 Howler.js提前把几段 audio 文件放在本地包括背景音乐循环轻快但不抢戏。滚动时的紧张鼓点。中奖时的喜庆音效。如果现场互动抽奖还可以加连接提示音。年会前一天我专门确认了现场的音响输出接口确保笔记本的音频能接到调音台。如果接入不上大屏端设置成静音模式让现场音响师傅配合放背景音千万别指望笔记本自带喇叭——现场吵起来根本听不见。3.3 中控端的功能布局与操作流程设计中控端是操盘手的核心阵地布局一定要清爽避免现场手忙脚乱。我的页面分成三大区域左侧当前轮次信息显示第几轮、奖品名、抽奖人数、参抽人群。中间候选名单预览默认只显示数量勾选“预览名单”后才展示姓名防止误操作。右侧已中奖名单列表按轮次分组中奖信息实时刷新。操作流程我设计了四个按钮开始滚动、停止抽奖、确认重抽、进入下一轮。这里有个细节停止抽奖时前端不立刻锁死中奖者而是弹窗提示“中奖者为某某是否确认”只有操盘手和主持人对视确认没问题后点击“确认保存”后端才真正把中奖记录写进 JSON。万一抽到已经离职的人或者老板觉得这个奖不应该给他可以直接“重抽”大屏端再回到滚动阶段重新抽一次完全不留痕迹。“重抽”这个功能很多人觉得没必要但做过现场就知道太有必要了。去年某次活动就遇到抽到的人不在现场电话也打不通要是不能重抽整个环节就卡住了。提前想好预案现场就不会抓瞎。3.4 离线兜底预案没有网络也能正常抽系统做得再好也架不住现场意外。我说说最极端的情况年会现场断网、局域网失灵、笔记本无线网卡出问题。所以这个项目里我做了一个完全离线的兜底方案前端和后端都跑在同一台笔记本上浏览器访问http://localhost:5000不依赖任何外网请求。所有 CDN 资源和音效文件在开发完成时下载到本地Font Awesome、Animate.css、Howler.js 全部本地化从一开始就不做线上依赖。大屏端虽然和中控端分离但如果是同一台笔记本同时投屏中控端和大屏端都通过 localhost 访问根本不需要走网络。如果必须两台电脑那就提前用网线把两台电脑直连配好静态 IP192.168.1.10和192.168.1.11不经过路由器也能跑。虽然这个方案并不复杂但能把网络风险几乎降到零。提示我以前见过很靠谱的团队年会当天直连大屏的笔记本突然蓝屏因为所有数据都存在机器本地换个电脑就全废了。所以我的项目文件除了本机另外用 U 盘存一份U 盘就插在现场导播台的电脑上备用。真出事插上去五分钟内就能把服务起起来。3.5 更多玩法摇一摇互动抽奖和其他彩蛋有些客户会提更热闹的需求比如大屏摇一摇抽奖。这种玩法其实是在大屏上展示一个二维码员工用手机扫码进入一个 H5 页面然后同时摇动手机后台通过加速度传感器接口DeviceMotion收集摇动次数在固定时间内按摇动次数排名前几名获奖。实现上并不复杂比普通抽奖多两块移动端 H5 页面读取设备的加速度计算摇动阈值和冷却时间防止有人拿着手机乱抖刷数据。后端排行系统每收到一条摇动数据就更新排名倒计时结束时锁定榜单。年会现场这个玩法反响很不错因为它让每个人都有参与感。不过要注意高峰期几十上百人同时摇手机后端请求量会突然涨起来。我的处理是前端先本地聚合每 2 秒把累计的摇动次数批量上报一次而不是每摇一下发一个请求。否则后端很容易被瞬时流量打崩。如果客户不想搞太复杂也可以把摇一摇做成伪参与——大屏上随机抽取手机号码后四位现场比对谁的电话响虽然老套但氛围也不差。不过我个人还是更推荐真正扫码摇一摇毕竟大家手里都拿着手机参与门槛极低气氛很快就能带起来。4. 常见问题与排查技巧实录4.1 中奖名单重复问题怎么根治抽奖最容易出的问题就是重复中奖。虽然我在每一轮的候选名单里默认排除了已中奖人员但总有几个场景会漏多轮抽奖并行比如老板在三等奖快结束时说“再加两个”而这轮还没结束下一轮已经配置好了如果不刷新候选名单就可能抽到已经中奖的人。管理员手动导入名单时中奖状态没有同步。我最终的处理思路是后端维护一个全局的global_winners_set任何一轮确认中奖后立刻把这个人的 ID 加进去。生成候选名单时统一从全员名单里剔除global_winners_set里的 ID再加轮次特定的过滤规则。也就是说候选名单的筛选逻辑是“全局已中奖排除 本轮参与条件”两条缺一不可。4.2 大屏画面比例失调怎么办年会现场的大屏很可能是特殊分辨率比如超宽屏、竖屏或者拼接屏。前端写的是 1920×1080 的 16:9 自适应布局但现场屏幕可能不是这个比例。我准备了两种方案如果现场屏幕可以用播放器或者导播台强制缩放那我在前端就不做特殊处理交给导播台调整。如果必须让网页自适应我就在 CSS 里用transform: scale()动态计算缩放比例确保页面按现场屏幕宽度铺满内容不变形。实际操作中大部分年会现场是用导播台切画面这种情况下网页只需要把 16:9 的输出投上去导播台那边会等比缩放加黑边这个完全没问题。最怕的是那种直连 LED 屏、没有任何中间处理设备的情况那就得提前测试现场调试很费时间。4.3 现场网络抖动导致大屏端状态不同步虽然我尽量用局域网直连但偶尔还是会遇到网络抖动。大屏端是轮询后端状态如果两次轮询之间网络卡顿可能会短暂停在上一帧。好在我把“大屏端的滚动动画”放在本地执行只有“开始”“停止”“确认结果”这些关键信号依赖后端同步所以一般的网络波动不会造成明显卡顿。万一真遇到长时间断连我的兜底方案是在大屏端页面加一个手动刷新按钮同时在中控端增加一个“重置大屏”操作。中控端点击后前端会直接重新拉取当前轮次状态恢复到大屏的最新进度。但这个操作要谨慎如果正在抽奖途中点了会让场面出现两秒空白。所以按钮加了二次确认提示。4.4 音效卡顿与音频输出优先级的问题音效卡顿的根源通常是浏览器自动播放策略。Chrome 等浏览器默认阻止未交互页面自动播放带声音的视频和音频除非用户点了页面。现场大屏端默认是没有交互的所以音频经常放不出来。解决方案在主持人说开场白时中控端就向大屏端发送一个“启用音频”信号大屏端页面主动创建一个AudioContext并 resume这样后续的抽奖音效可以随时播放。更简单的做法是在页面底部放一个隐藏按钮让工作人员提前点一下解锁音频。另一个小坑是笔记本插上 HDMI 后默认音频输出可能还会走笔记本扬声器而不是现场音响。我有一次没注意结果抽奖播到三等奖才发现现场音响没响。后来养成了习惯每次到现场先开系统设置看音频输出设备声音测试起码放三遍才放心。4.5 常见问题速查表问题现象可能原因快速解决方案大屏名单没有滚动前端 JS 报错或名单为空刷新页面确认后端服务正常抽中的人员已中过奖全局去重集合未生效重启抽奖服务重新生成候选名单音效不播放浏览器自动播放策略拦截工作人员提前点击页面解锁音频中控端与大屏端状态不同步网络抖动或服务重启中控端点击“重置大屏”现场屏幕显示变形分辨率和比例不匹配用导播台缩放或前端动态 scale 适配名单里有离职人员导入数据未清洗提前用清洗脚本过滤抽奖过程太卡大屏端渲染性能不足减少同时渲染的人名数量仅渲染可见区域笔记本电脑突然关机电量不足或过热全程外接电源备用机五分钟接管5. 从项目交付到现场执行的心得这个活儿做完最大的感受是写一个抽奖程序本身不复杂真正考验人的是对现场的理解。你做的东西是给两千人看的不是给自己在 IDE 里自嗨的所以在设计每个功能前我都先问一个问题“如果我是台下观众我会觉得这个环节好玩吗会觉得公平吗”有一个细节我印象特别深。为了让抽奖更有仪式感我在大屏端加了一个“倒数三二一”的动画效果。主持人喊“准备——”大屏上开始倒数字全场跟着喊“三、二、一”然后滚屏启动。这个效果不是技术难点但它把整个抽奖环节的情绪完全调动起来了。年会结束后客户跟我说好几个员工专门发朋友圈拍了这一段效果超出预期。还有一点就是一定要留足提前量。名单确认要提前程序预演要提前现场彩排也一定要跑一遍。彩排时最好用真实名单哪怕只抽一轮也能发现不少细节问题。比如我彩排时发现名单滚动过程中偶尔会出现两个名字重叠的渲染 bug就是因为 CSS 的line-height在不同字号下没对齐。这种问题如果不彩排现场大屏上闪一下虽然影响不大但看起来很廉价。最后分享一个小技巧给客户交付的时候我除了源代码还附带了一份“现场操作手册”用两页纸写清楚每个按钮是干嘛的、什么顺序点、万一出问题先按哪个按钮。这份手册客户那边的行政和技术小哥人手一份。因为年会当天我虽然到场但不能保证每时每刻都盯在中控旁边。让客户自己也能操作才是真正的交付完成。以后再有人找我做年会抽奖我大概率还会接。毕竟能在线下把几百上千人聚在一起用一段几分钟的抽奖环节把气氛推到最高点这种现场感和成就感是纯线上活动完全没法比的。只要项目启动前把名单理清楚、把轮次规则定明白、把现场网络和音视频调试到位剩下的就交给现场的人去热闹就好。