投票网站源码

去年帮一个社区组织做线上选举,投票预算零。网站找了一圈开源投票系统,源码要么太重,投票要么后台丑得没法看。网站索性自己写了一套,源码前后端加起来不到两千行代码。投票这篇文章就把这套源码拆开讲讲,网站给同样有需求的源码人一个参考。

先说技术选型。投票后端用 Python 的网站 Flask,数据库 SQLite。源码为什么不用 MySQL?投票因为投票场景通常并发不高,几百人同时在线已经算多了,网站SQLite 完全扛得住,源码还省去了配置数据库的麻烦。前端就是原生 HTML + CSS + 少量 JavaScript,没上框架。有人可能觉得这样太原始,但投票页面讲究的是加载快、逻辑清晰,React 那一套在这里属于杀鸡用牛刀。

核心数据表只有三张

第一张 voters,存选民信息。字段很简单:id、name、voter_key(唯一投票凭证)、has_voted(布尔值)。这里有个细节——我一开始想用邮箱做唯一标识,后来发现很多社区成员根本没有邮箱,改成发一串随机生成的 8 位字母数字组合,打印在纸条上发给每个人。投票时输入这串 key,系统校验是否存在、是否已投过票。

第二张 candidates,存候选人:id、name、description、vote_count。vote_count 这个字段其实可以不要,每次从投票记录里 COUNT 也行,但那样查询慢,不如冗余一个计数器,投票时直接 +1。

第三张 votes,存投票记录:id、voter_id、candidate_id、timestamp。这张表主要用来审计和导出结果,日常计票靠 candidates.vote_count 就够了。

防刷票的几个土办法

没有短信验证,没有实名认证,怎么防止一个人投多次?我的方案是“一次性 key + 服务端状态锁”。每个 key 在数据库里对应 has_voted 字段,投票接口收到请求后,先用 UPDATE voters SET has_voted=1 WHERE voter_key=? AND has_voted=0 这种原子操作去抢锁。如果返回的影响行数是 0,说明这个 key 已经投过了,直接拒绝。这比先查询再更新靠谱,避免了并发下的竞态条件。

另一个细节是投票页面的表单提交。很多人会写 <form action="/vote" method="post">,然后里面放一个隐藏的 voter_key。这样有个问题:用户刷新页面会重复提交。我的做法是投票成功后返回一个 303 重定向到结果页,这样刷新就不会重复投票了。

前端页面的取舍

投票页面的设计原则就一条:让用户用最短的时间完成操作。所以页面上没有导航栏,没有页脚,没有 logo。打开就是一行提示“请输入你的投票密钥”,下面一个输入框,一个按钮。输入 key 之后跳转到候选人列表,每个候选人旁边一个单选按钮,底部一个提交按钮。整个流程不超过三次点击。

结果页倒是花了点心思。用纯 CSS 做了横向柱状图,每个候选人一条,宽度按得票比例来。没有用 Chart.js,因为那玩意儿加载要 200 多 KB,而我的 CSS 方案只多了 30 行代码。刷新页面就能看到实时结果,对于社区选举这种场景,实时性比美观重要得多。

部署比写代码还简单

整个项目就一个 app.py 加一个 templates 文件夹。部署到一台 1 核 1G 的云服务器上,用 gunicorn 跑四个 worker,前面挂 nginx 做反向代理。数据库文件放在项目目录下,每天凌晨用 cron 备份一次到对象存储。整套下来,服务器成本每月不到 10 块钱。

源码我放在了 GitHub 上,MIT 协议,随便改随便用。有人问为什么不加个后台管理界面,我说你直接用 SQLite 命令行工具改数据库就行了,加个后台反而增加了攻击面。投票系统这种东西,功能越少越安全。