微信投票小程序开发实战:从数据模型到防重复投票
简介这是一份面向高校计算机专业毕业设计的微信投票小程序源码适用于需要快速搭建投票类小程序或完成同类课题的学生。项目基于微信小程序前端与PHP后端实现核心功能包括用户投票与图片上传、投票选项比例统计、非匿名用户投票详情展示、到点自动结束、发起人提前结束、管理员删除与审核等业务链完整。资源共90个文件以wxss样式、js逻辑、json配置、wxml页面为主辅以PHP服务器端接口和SQL数据库脚本压缩包约155KB结构清晰便于直接导入开发者工具调试。开发者日志中还记录了手机端调用接口失败、服务器SSL证书配置等真实排错过程适合参考其问题定位思路。目前已有202人学习下载搭配README使用说明可帮助少走弯路。1. 微信投票小程序毕设选题容易规则设计见功力“毕业设计微信投票小程序”这个标题每年都会出现但真正拉开分数差距的从来不是“能不能投出一票”而是活动配置、投票规则、防重复投票和结果统计这一整条链路是否严谨。微信小程序自带登录、分享和传播场景投票又是高频交互前后端边界清晰适合作为独立完成的毕业设计。技术栈建议选 uniapp 加微信云开发或者 uniapp 加自有后端前者省去服务器运维后者更贴近企业真实分工。本文会从数据建模、登录资格、投票核心逻辑、结果聚合到抓包验收把整个闭环拆开讲照着做能直接跑通一个可演示的微信投票小程序。2. 投票数据模型先于页面poll / option / vote_record 三张表的结构边界投票小程序的页面不多活动列表、活动详情、投票结果、我的投票单看任何一页都不复杂。但一旦进入“多人同时投票”“活动截止后不能再投”“每个人只能投一次”这些场景数据结构没设计好后面所有接口都会写得很难受。常见的做法是先按关系型思维设计三张核心表再落到云开发数据库或后端数据库中。2.1 用 SQL 思维定义活动与选项状态、时间窗口、投票上限先看活动表的字段设计。无论底层用 MySQL 还是云开发的 collection这张表的信息都应该覆盖活动本身的所有规则。以下是参考 DDLCREATE TABLE poll ( id BIGINT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(100) NOT NULL, description TEXT, status TINYINT NOT NULL DEFAULT 0, -- 0草稿 1进行中 2已结束 start_time DATETIME NOT NULL, end_time DATETIME NOT NULL, vote_limit INT NOT NULL DEFAULT 1, -- 每人可投几票 is_multi TINYINT NOT NULL DEFAULT 0, -- 0单选 1多选 multi_max INT DEFAULT 1, -- 多选时最多选几项 total_votes INT NOT NULL DEFAULT 0, -- 冗余统计 create_time DATETIME DEFAULT CURRENT_TIMESTAMP );option 表相对简单核心是关联 poll_id、选项名称、排序号和一个冗余计数器。把 total_votes 放在 option 表里而不是每次实时 count是因为投票结果页会被反复打开频繁聚合对数据库压力很大。CREATE TABLE option ( id BIGINT PRIMARY KEY AUTO_INCREMENT, poll_id BIGINT NOT NULL, option_name VARCHAR(200) NOT NULL, sort_order INT DEFAULT 0, vote_count INT NOT NULL DEFAULT 0 );这两张表设计好之后活动创建页的后端接口就变成了单纯的新增记录。要特别注意的是end_time和status的关系status 不应完全依赖定时任务去更新查询投票接口时应该同时判断start_time now end_time即使 status 还没来得及被任务改成“已结束”用户也无法继续投票。这个双保险在演示时很有用避免出现“活动明明过了时间但还能投”的尴尬。2.2 投票记录表把 openid 和规则快照一起存第三张表是 vote_record它记录每一次投票行为也是防重复投票的依据。最简单的形式是 poll_id 加 openid 加 option_id。但需要注意如果活动是“每人最多投 3 票”仅靠一条记录判断不够需要先查该用户在该活动下的记录数是否小于 vote_limit。CREATE TABLE vote_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, poll_id BIGINT NOT NULL, option_id BIGINT NOT NULL, openid VARCHAR(64) NOT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_poll_openid_option (poll_id, openid, option_id) );唯一索引建议加在“poll_id openid option_id”上它表达的业务含义是“同一用户在同一活动里不能重复投同一个选项”。如果活动是单选题这个约束就够了如果是多选配合 vote_limit 还能防止用户反复切换选项刷票。需要说明的是投票记录并不需要记录投票时刻活动的完整规则快照但如果你要做“修改投票规则后的统计审计”可以在 vote_record 里冗余存一份vote_limit或is_multi。2.3 从数据模型反推页面列表、详情、结果页的查询路径数据模型确定后小程序的页面接口基本就定下来了活动列表页查 poll 表按 start_time 倒序只返回 status 为“进行中”或“已结束”的活动。活动详情页查 poll 表加 option 表附带当前用户是否已投票的状态。这个“是否已投票”是通过查 vote_record 得到的不能只依赖前端存储。投票结果页直接读 option 表的 vote_count不需要实时去 count vote_record。很多初次做毕设的同学会在详情页实时 count vote_record 来显示票数活动访问量小的时候没区别一旦演示时多人同时打开页面数据库压力会明显上升。用冗余计数字段投票时在事务里同时更新 option.vote_count就能用一次查询拿到全部结果。云开发数据库没有事务时可以用 db.command.inc 做原子自增效果一样。3. 微信投票小程序的登录链路与投票资格校验openid 是底线微信小程序有一套自己的登录机制核心是通过 wx.login 获取临时 code再在后端换取 openid。openid 是用户在某个小程序下的唯一标识投票防刷的底线就是它。登录链路设计得好后续所有“谁投了票、能不能再投”的判断都有据可依。3.1 微信登录流程code 换 openid 的完整调用前端的登录触发点一般放在 App.vue 的 onLaunch 里也可以用 uni.login 兼容 H5 和 App 端。代码示例如下uni.login({ provider: weixin, success: async (loginRes) { const code loginRes.code; const res await uni.request({ url: https://your-domain.com/api/login, method: POST, data: { code } }); if (res.data.openid) { uni.setStorageSync(openid, res.data.openid); uni.setStorageSync(session_token, res.data.token); } } });后端收到 code 后调用微信接口jscode2session把 appid、secret 和 code 换成 openid。这一步必须放在服务端执行不能在小程序端直接请求微信接口否则 secret 会暴露在客户端代码里。云开发环境会更简单云函数内通过cloud.getWXContext().OPENID直接拿到 openid连 code 换取的步骤都省了。两种方案各有适用场景自有后端更适合展示“完整后端能力”云开发更适合快速交付。毕设答辩时如果被问到“为什么不用云开发”可以回答为了练习独立部署和接口设计如果被问到“为什么用云开发”可以回答为了聚焦业务逻辑避免运维成本。两种说法都站得住脚。3.2 投票资格校验的三个层次登录态、活动状态、行为限制投票接口的入参不能只传 option_id前端还要带上 openid 或 token后端再根据 openid 判断资格。完整校验流程分三步判断 token 或 openid 是否有效无效直接拒绝。这防止了有人绕过小程序直接调接口。判断活动状态当前时间是否在 start_time 和 end_time 之内活动 status 是否为“进行中”。判断行为限制根据 vote_record 查该用户已投数量超过 vote_limit 则拒绝。// 云函数 vote 的核心校验逻辑 const wxContext cloud.getWXContext(); const openid wxContext.OPENID; const { pollId, optionIds } event; const pollRes await db.collection(poll).doc(pollId).get(); const poll pollRes.data; const now Date.now(); if (now poll.startTime || now poll.endTime) { return { code: 403, msg: 不在投票时间内 }; } const votedCount await db.collection(vote_record) .where({ pollId, openid }) .count(); if (votedCount.total poll.voteLimit) { return { code: 403, msg: 已达投票上限 }; }这段逻辑里最常见的错误是只在前端判断“是否已投过”因为前端页面缓存可以被绕过另一个常见错误是只判断 vvote_record 里有没有记录但没处理多选时“同一个选项重复提交”的情况。把唯一索引或查询条件建好这两个坑都能提前避开。3.3 白名单模式与演示模式评委体验优先毕业设计答辩时评委通常不会现场注册新微信也不一定有耐心看完整注册流程。常见的做法是在活动表加一个voter_type字段0 为公开投票1 为白名单投票。白名单模式下只有指定的微信用户才能参与。实现方式是在 user 表里维护一个 openid 列表投票前先查该 openid 是否在名单内。如果你用云开发白名单可以做成一个独立的 collection字段只有 openid 和备注。演示前把评委或自己的微信号加进去其他用户看到的是“该活动仅限受邀用户参与”。这比临时改代码优雅得多而且能作为“权限控制”的功能点写进设计文档。4. 用 uniapp 与云开发实现微信投票小程序投票、结果与页面联动uniapp 是微信小程序开发里最常见的跨端方案一套代码可以编译到微信小程序、H5 和 App。前端页面负责展示云函数负责业务逻辑云数据库负责存储。这一章从投票动作、结果聚合到前端页面的联动给出可运行的最小实现。4.1 投票页布局与交互单选、多选、提交状态投票详情页的交互要区分单选和多选。单选模式下点击选项直接选中并提交或先提交按钮多选模式则先记录选中集合点“提交投票”时一起上传。考虑到用户体验单选建议做成点击即提交多选必须配提交按钮否则容易误触。页面核心代码view classoption-list view v-foritem in options :keyitem._id classoption-item taphandleSelect(item) text{{ item.optionName }}/text text v-ifselectedIds.includes(item._id)✓/text /view /view button v-ifpoll.isMulti tapsubmitVote提交投票/buttonmethods: { handleSelect(option) { if (this.poll.isMulti) { // 多选逻辑最多选 multiMax 个 if (this.selectedIds.includes(option._id)) { this.selectedIds this.selectedIds.filter(id id ! option._id); } else if (this.selectedIds.length this.poll.multiMax) { this.selectedIds.push(option._id); } } else { // 单选逻辑立即提交 this.submitVote([option._id]); } }, async submitVote(optionIds) { const res await uniCloud.callFunction({ name: vote, data: { pollId: this.poll._id, optionIds } }); if (res.result.code 0) { uni.showToast({ title: 投票成功 }); this.loadResult(); } } }这段代码的关键在于selectedIds的维护多选的边界是multiMax超过后不能继续选中而不是弹出提示后仍然允许选中。另一个容易出问题的点是投票成功后的状态刷新正确做法是重新拉取活动详情和选项数据不能只改本地一个布尔值。4.2 云函数投票原子更新与并发安全投票云函数不仅要写 vote_record还要原子更新 option 表的 vote_count。不用事务的写法是两次独立更新这在并发投票时可能丢失计数。云开发数据库里可以用db.command.inc实现原子自增。const db cloud.database(); const _ db.command; async function vote(event) { const wxContext cloud.getWXContext(); const openid wxContext.OPENID; const { pollId, optionIds } event; // 1. 校验活动状态 const pollRes await db.collection(poll).doc(pollId).get(); const poll pollRes.data; const now Date.now(); if (now poll.startTime || now poll.endTime) { return { code: 403, msg: 不在投票时间范围内 }; } // 2. 校验用户已投次数 const votedCountRes await db.collection(vote_record) .where({ pollId, openid }) .count(); if (votedCountRes.total optionIds.length poll.voteLimit) { return { code: 403, msg: 投票数量超过上限 }; } // 3. 检查是否重复投过相同选项 for (const optionId of optionIds) { const dupRes await db.collection(vote_record) .where({ pollId, optionId, openid }) .count(); if (dupRes.total 0) { return { code: 403, msg: 不能重复投同一选项 }; } } // 4. 写入投票记录并原子更新票数 const addRes await db.collection(vote_record).add({ data: { pollId, optionIds, openid, createTime: Date.now() } }); for (const optionId of optionIds) { await db.collection(option).doc(optionId).update({ data: { voteCount: _.inc(1) } }); } return { code: 0, msg: 投票成功, id: addRes._id }; }这里值得注意的细节是“投票记录”的存储结构optionIds 存的是数组方便一条记录承载一次多选。校验时先查已投数量再加本次数量超过 voteLimit 则直接拒绝。如果把多选拆成多条 vote_record代码会更复杂而且“每人最多 3 票”的语义会变得难以控制。4.3 结果统计与服务端聚合排行、占比与缓存策略投票结果页要展示每个选项的票数和占比。数据来源直接用 option 表的 voteCount 字段前端计算百分比即可不需要额外的统计接口。const total options.reduce((sum, item) sum item.voteCount, 0); options.forEach(item { item.percent total 0 ? Math.round((item.voteCount / total) * 100) : 0; });如果需求不只是“票数”还想看“每日投票趋势”或者“按时间维度的增长曲线”那就需要定时任务或聚合查询。云开发的聚合能力可以按天分组const $ db.command.aggregate; const res await db.collection(vote_record).aggregate() .match({ pollId }) .group({ _id: $.dayOfMonth($createTime), count: $.sum(1) }) .sort({ _id: 1 }) .end();这个聚合在演示时很加分因为它能画出投票人数随时间变化的折线图而大部分人只做静态票数展示。性能方面数据量不大时实时聚合没有问题数据量大时应该用定时任务把统计结果写到单独的统计表里再让前端读统计表。答辩时把这个“离线统计”的思路讲出来会明显比“每次实时 count”更成熟。5. 微信投票小程序的真机验证与抓包排错从上线到验收的最后一公里开发调试阶段用开发者工具的模拟器没问题但投票涉及微信登录、网络请求和分享参数必须走真机预览才能暴露问题。这一章聚焦两类工作微信公众平台的基础配置以及用抓包工具核对真实请求链路。5.1 小程序后台配置与真机预览的前提条件在微信公众平台注册小程序后需要在“开发管理 - 开发设置”里记录 AppID并在“服务器域名”里配置 request 合法域名。如果用云开发需要开通云环境并在 uni-app 的 manifest.json 里填入云环境 ID。真机预览前必须勾选“不校验合法域名”否则开发阶段请求会被拦截但最终演示时建议打开校验体现配置的规范性。常见的预览卡点有三个AppID 用了测试号导致登录接口鉴权失败云开发环境 ID 填错导致数据库读写报错request 域名没有备案或没有配置 https。前两个属于配置遗漏第三个是真实环境要求开发者工具里可以临时关闭校验真机上必须配置正确。5.2 用抓包工具核对投票请求与响应微信小程序抓包是定位前端与后端联调问题的有效手段。开发者工具自带 Network 面板能看到每个请求的 URL、参数和响应但真机上无法直接看到请求细节这时可以用 Charles 做 HTTPS 抓包。抓包步骤概括如下电脑和手机连接同一局域网Charles 开启 SSL Proxying添加目标域名。手机 WiFi 代理指向电脑 IP 和 Charles 端口。手机安装 Charles 的 CA 证书并在系统设置中信任该证书。打开小程序进行投票操作在 Charles 中过滤目标域名查看请求参数和返回。需要注意微信小程序的请求是基于微信自带网络的部分请求走了微信的通道抓包看到的是小程序业务服务器的请求。如果接口返回了 403 或自定义错误码优先检查投票时间、投票上限和重复投递三个维度。5.3 验收检查单一份可以直接用于自测的清单在提交前按下面的清单过一遍大部分常见扣分点都能提前发现检查项操作方式预期结果登录链路清除缓存后打开小程序展示登录中随后进入活动列表投票时间控制把活动结束时间改成过去详情页按钮置灰接口返回 403重复投票同一账号投票两次第二次提交提示“已达投票上限”多选上限设置 multiMax 为 2选 3 个选项第三个无法选中结果实时性两个账号分别投票刷新结果页票数与占比同步更新分享传播转发给另一个微信对方打开后可投票分享卡片正常显示标题与封面最后一个检查项是“清缓存后再次登录”这一步能验证 openid 是否被正确持久化。如果每次重新打开小程序都会生成新的 openid说明登录逻辑没走完整链路投票记录会变成“不同用户”防重复就失效了。顺带一提如果导师要求使用独立后端而不用云开发核心接口保持不变只需要把云函数替换成 PHP 或 Java 的 REST API前端把 uniCloud.callFunction 改成 uni.request。投票规则、数据模型和校验顺序的思路完全可以复用。本文还有配套的精品资源点击获取