回到目录

临近年会,谈谈抽奖

2017-01-08

又到互联网公司年会密集月了,年会里必然会抽奖(什么还有不抽奖的年会??什么还有不开年会的公司??)。当然抽奖形式也各异,最简单粗暴的方式就是抽奖箱里抓阄。但也有很多时候为了现场效果需要在大屏幕里抽奖。曾经参与过部门年会抽奖程序的编写,后面这套代码也多次用在其他场合。今年部门抽奖还是我来负责,虽说可以直接用之前的代码,但个人原则就是不做重复劳动,所以这次也尝试了新的技术,觉得有必要做个系统总结,方便传承给后面的人,也算是沉淀个最佳实践。

一、程序之外

① 避嫌

开发抽奖程序本身是个「吃力不讨好」的工作,因为很多时候为了避嫌,不得不去掉你自己的名字。我们部门的做法就是开发完成后在年会当天在几个技术 Leader 面前演示,并且可以让他们进行 code review,完了之后封存代码,这样你也可以参与抽奖。

同时还可以在公司内部开源代码平台中上传你的源代码,让其他程序员们一同分析和找 bug。当然也有人说抽奖时用的代码跟你现在上传的源代码一定是同一份吗?那就录制编译视频并公布 md5,抽奖的时候当直播校验 md5 码并运行程序。方式还有很多,但大家不要钻牛角尖,想想也不至于嘛。

② 特定规则

也就是一些所谓的「潜规则」,这是个存在即有理的东西,初衷也是确保有限范围内的公平。可以罗列并简单介绍(并非我所使用):

  • 不同概率:异地员工也可以参与抽奖,但不能抽大奖;新人或入职半年内的员工中奖概率大或小一些等;
  • 名单范围:领导们一般都会从抽奖名单中剔除(或只能抽小奖);只有现场人员可以抽;若有分组比赛环节,则抽奖只能在某些特定组员里抽;新员工和老员工分开抽;中过一次奖的人会从抽奖名单中剔除等;

看似没有绝对公平,其实也算合乎情理,抽奖是门学问,不仅要考虑公平公正,还得兼顾大部分人的感情。从编码角度来说,这些规则会增加抽奖程序的复杂性,否则直接一个随机或数组打乱就行了。

二、核心代码

抽奖代码逻辑还是相对纯粹的,核心的有两点:随机的算法、概率的计算。

① 随机的算法

不管什么语言,核心的函数即 random() 或 shuffle(),取得一个随机数或打乱某个数组。但是经过几次年会来看,会发现某些人总是可以中,也许是运气,也许是随机数的规律,所以想要得到一个让人信服的「真正随机」,也许可以向大自然索取——基于大气噪音的随机数。

这次我用的是 www.random.org 提供的 HTTP API,值得注意的是获取到的是完全的随机,所以可能会出现重复。若希望得到 20 个唯一的随机数,可以请求 100 个随机数,去重后裁剪数组得到。

② 概率的计算

概率的计算涉及两方面,其一是某些人群中奖的概率,其二是某些奖品得奖的概率。但实际上两者是同一个意思,程序上可以理解为某些人群的中奖概率(因为奖品就在那里,中不中看你)。

举个例子,老员工、新员工这类人群,在抽某些奖的时候比重不一样;比如在最后的大奖时,只有老员工可以抽,那只需要简单的在数据库标注下不同的标识,然后只抽这些人即可;若最后的大奖老员工中奖率 70%,新员工中奖率 30%,那可以随机抽出 7 个老员工 + 3 个新员工,再从 10 个人里随机得到一个。

③ 技术选型

第一年用的是 HTML/CSS/JS + PHP + MySQL 的常规模式,后面优化成 HTML/CSS/JS + Vue + WebSQL 的模式,减少了不必要的服务器。

下图为 WebSQL 的界面:

④ 抽奖模式

怎么理解呢?抽奖模式无非两种,第一种把抽奖权限交给一个人,由他进行每一轮的抽奖;第二种则是每个人可以通过网页或其他途径进行「分布式」抽奖。它们的差异是:是否需要考虑并发、奖品库存和概率的问题。由于目前我所经历的都是模式一,这里暂无太多经验,且不细说。

三、容灾

这里的「灾」可以是天灾,也可以是人祸。不管哪种,都需要考虑到。

天灾之一:中途断电

这里的断电既可以是场地断电,也可以是电脑电源断电(哪位大哥把插头踢了?!)。最好的办法自然就是用笔记本来作为抽奖程序的载体,场地断电或笔记本电源断电都没有影响,数据和抽奖流程得以保留。

天灾之二:电脑故障

有一个备用的电脑是必不可少的,但是备用电脑并没有保留之前的抽奖数据(如中过奖的人不能再中奖),解决的办法是使用云数据库,又或者做好源数据导出的功能,能及时导出到另外一台电脑。

这个问题的严重性还取决于你的抽奖是流程性的,还是相对独立的。相对独立即每次抽奖都是把同一个界面调出来,不同的只有抽奖的人数,这样流程与抽奖逻辑是相对解耦的。而流程性的话,每次的抽奖都与上一次环环相扣(如有三轮抽奖,每轮抽十次,在第五次的时候死机了),重启后如何恢复到这个状态?

办法当然有:

  • 界面流程可跳转,即可以从首页跳到任意一轮的某次抽奖界面
  • UI 与逻辑解耦,如抽奖逻辑不依靠某个类名或节点,而是通过 data-* 的方式绑定抽奖个数/次数/类型/回合等信息
  • 数据的状态备份,即通过数据库来承载数据内容的变化信息,而非存放在缓存里

天灾之三:网络堵塞

大部分年会现场 WiFi 是瘫痪的,移动网络也难以幸免。如果抽奖程序用的是云数据库,或者随机数来自于网络,这个时候还是可以测试下移动网络热点的速度如何,我们部门的年会抽奖就是用我自己的手机热点撑起来的。
当然,一定要做备选方案,当网络请求故障或超时时,使用本地程序生成随机数,不用云数据库。

人祸之一:中途刷新

这个简单,前端可以拦截刷新:

<script type="text/javascript">
    window.onbeforeunload = function() {
        return "Dude, are you sure you want to leave? Think of the kittens!";
    }
</script>

人祸之二:连点多次

我的做法是函数节流 + 变量标识,用 underscore.js 的 throttle 可以屏蔽掉短时间内的连续点击;变量标示即点击按钮后立个 flag,抽奖结果出现后取消 flag(因为抽奖出结果一般是有动画的),可以避免出现抽奖结果的过程中误点。

四、更多...

当然,如果要做得更好,还有很多值得做的东西。如:

  • 为了方便回溯或检查,可以把抽奖日志打印下来,包含了时间 + 用户操作 + SQL 语句等。
  • 做好代码的容错逻辑,try catch 常用,在 SQL 语句出错时要有必要的事务回退等
  • 做好后台,方便重置中奖人员、罗列每一轮中奖名单、必要的数据统计等

相关链接: