第 9 章:投票互动系统
“人类的本质是复读机,但人类的乐趣在于投票选出那个最好的复读机。”
投票是提升活动活跃度的神器。无论是“十佳歌手”评选,还是“最美摄影”大赛,一个实时、公正的投票系统都是必不可少的。
9.1 投票配置设计
投票的配置 (voteConfig) 相对独立,存储在 activities 表中。
数据结构
{
"displayMode": "grid", // 展示模式:列表/网格
"maxVotes": 3, // 每人最多投几票
"allowRevote": false, // 是否允许改票
"showVoteResult": true, // 投票后是否显示结果
"options": [
{
"id": "opt_1",
"name": "1号选手 张三",
"imageUrl": "cloud://...",
"description": "来自计算机系的灵魂歌手"
},
{
"id": "opt_2",
"name": "2号选手 李四"
// ...
}
]
}
9.2 投票逻辑实现
投票逻辑在 server/vote/index.js 中。
核心校验
在 submitVote 云函数中,我们必须做严格的校验:
- 活动状态: 活动是否已结束?是否是投票类型的活动?
- 选项有效性: 用户投的
optionId是否在配置的options列表里? - 票数限制:
voteOptionIds.length是否超过maxVotes? - 重复投票: 查询
votes表,检查该用户是否已经对该活动投过票。- 如果已投且
allowRevote=false: 报错“您已投票”。 - 如果已投且
allowRevote=true: 执行update更新操作。 - 如果未投: 执行
create插入操作。
- 如果已投且
9.3 防刷机制
只要有投票,就有刷票。虽然我们无法做到银行级的风控,但可以增加刷票成本。
- 身份限制: 必须登录才能投票(OpenID 唯一)。
- 频率限制: (进阶) 云函数通过 Redis 限制同一 IP 或同一用户单位时间内的请求频率。
- 黑名单: 对于异常用户,管理员可以在后台将其禁用 (
role='banned')。
9.4 实时统计与排名
用户投完票,最想看的就是当前排名。
统计逻辑 (getVoteStatistics)
为了保持云函数的轻量和可移植性,我们没有使用复杂的 SQL 聚合查询,而是采用了更直观的 “应用层聚合” 策略。
- 拉取数据:
models.votes.list({ where: { activityId } })获取该活动的所有投票记录。 - 内存统计: 遍历记录,利用 JS 对象进行计数。
// server/vote/index.js
const optionVoteCount = {}
votes.records.forEach((vote) => {
// 解析 voteOptionIds 字符串
let voteOptionIds = []
try {
voteOptionIds = JSON.parse(vote.voteOptionIds)
} catch (e) {
/*...*/
}
// 累加票数
voteOptionIds.forEach((optionId) => {
optionVoteCount[optionId] = (optionVoteCount[optionId] || 0) + 1
})
})
- 生成结果: 将统计出的
count映射回options数组,并按票数降序排列。
性能优化方案
如果未来遇到全校级的大规模投票(如 10万+ 数据),上述内存统计可能会变慢。届时可以考虑:
- 预计算字段: 在
activities表中维护一个vote_counts字段。每次用户投票时,直接更新该字段,读取时直接返回,无需实时计算。 - 缓存: 将统计结果缓存到 Redis 或 CDN 中,设置 1 分钟的过期时间。
本章小结: 我们完成了一个功能完备的投票系统,涵盖了配置、校验、防刷和统计。至此,核心业务功能(活动、报名、投票)已全部实现。
接下来,我们将进入本书最精彩、最硬核的部分——可视化编辑器。如果你想知道那些拖拖拽拽就能生成页面的功能是怎么写出来的,千万不要错过下一章!