简介:这是一份面向毕业设计场景的微信小程序投票评选系统完整源码包,采用Java作为后端支撑,前端为微信小程序原生实现,涵盖页面交互、业务接口与数据库脚本,适合计算机相关专业学生毕设选题、课程设计或小程序开发实战学习。源码已在本地环境编译通过,下载后按说明配置JDK、数据库及小程序工具等环境即可运行;整体功能完整,经过指导老师验收认可,可放心作为课题设计参考或二次开发的基础框架。压缩包总计884个文件,以js、wxml、wxss等小程序核心文件为主,同时包含java/class后端源码、xml/json配置文件、png/jpg等图片素材以及sql数据库脚本,压缩后约19.64MB,内部目录按前后端与资源类型划分,便于按需查阅。目前已有475人浏览学习,适合需要快速获取可运行毕设项目,或想系统理解微信小程序与Java后端联调方法的开发者。
1. 微信小程序投票评选系统:为什么源码和数据库必须一起交付
一个能跑起来的投票评选系统,难点从来不在页面长什么样,而在“这一票到底算没算进去”。我接过不少类似的项目包,标题写着微信小程序开发的投票评选系统源码数据库.zip,解压后大多是三样东西:小程序前端、后端接口服务、一份初始化 SQL。缺了数据库脚本,前端写得再漂亮也只是一层空壳。这个标题最值得关注的地方,就是它把源码和数据库放在一起交付,意味着拿到手的目标是“本地能跑起来、改一改能上线”,而不是看 demo。适合的人群很明确:做课设毕设、接外包、或者公司内部要做一场员工投票评选的开发者。下文我会把登录链路、计票接口、建表方案和验收方法完整拆开讲,也会写明哪些地方最容易在验收时翻车。
2. 投票评选系统的核心链路:从微信登录态到一票落库
投票系统的第一道门槛是身份识别。微信小程序之所以在投票评选场景里这么常见,是因为它自带微信登录体系,用户不需要注册账号,打开就能投。但“能识别身份”和“能防刷票”是两件事,很多新手在这里踩坑。系统链路一般是这样走的:用户打开小程序触发wx.login,拿到临时凭证code;后端拿code去微信接口换openid;后端签发自己的token返回给小程序;之后每次请求投票接口都带这个token。整条链路里,code是一次性的,有效期只有几分钟,而后端换到的openid才是用户的稳定身份标识。
2.1 为什么选微信小程序承载投票评选
企业内部评优、校园十佳评选、门店人气投票,这类场景的共同特点是:用户群体已经集中在微信里,不希望为了投票专门下载 App 或注册账号。微信小程序打开即用,分享到群聊里就能投,这是它最大的优势。另一个优势是身份维度可控:每个微信用户有唯一的openid,后端可以用它来做“每人限投一票”的约束。
但这里有一个很常见的误判:以为有了openid就能防止刷票。事实上同一用户可以注册多个微信号,每个号的openid不一样,系统会把他们当成不同的人。如果评选规则对“同一人只能投一票”有强约束,单靠openid是不够的,需要再绑定手机号或使用unionid。选型时要先问清楚活动方:是“每微信号限投一票”还是“每人限投一票”,这两者的实现成本差别很明显。
后端选型上,小项目用微信云开发最省事,数据库、云函数、存储都齐了,不需要自己买服务器;但如果你拿到的是传统源码包,后端通常是 Node.js 或 Java 写的,配合 MySQL 使用。我一般建议小团队直接自建后端,原因有三个:本地调试方便、数据在自己手里、后续要加导出功能不受云开发限制。投票评选这种低频活动,一台低配服务器就够了。
2.2 三层模块拆解与登录态最小实现
一套完整的投票评选系统源码,按功能可以拆成三层:用户端小程序、评选管理端、后端服务。用户端负责展示评选活动、候选人列表、投票按钮和实时票数;管理端负责创建评选、上传候选人、查看票数排名和导出结果;后端服务负责登录鉴权、投票计票、数据统计。
常见的源码包目录结构大致是这样的:
| 路径 | 内容 |
|---|---|
miniprogram/ | 微信小程序前端代码,包含页面、组件、工具函数 |
service/或server/ | 后端接口服务源码 |
db/或sql/ | 数据库初始化脚本,含建库建表语句 |
README.md | 部署说明、环境要求、启动步骤 |
拿到包之后,最先要看的就是README里声明的环境版本,前端开发者工具版本、后端 Node.js 或 JDK 版本、MySQL 版本,这三个版本不匹配是后面大量报错的根源。
登录态的实现是整个系统的地基。小程序端拿到openid不能直接信任,必须由后端通过微信接口换取。最小实现如下:
// 小程序端:触发登录并缓存 token const doLogin = () => { wx.login({ success: async (res) => { if (!res.code) return; const resp = await wx.request({ url: 'https://your-server.com/api/auth/login', method: 'POST', data: { code: res.code } }); const { token, openid } = resp.data.data; wx.setStorageSync('token', token); wx.setStorageSync('openid', openid); // 登录完成后跳转到投票页 wx.switchTab({ url: '/pages/vote/index' }); } }); };这里的wx.login拿到的code是一次性的,后端拿到后调用微信的code2Session接口,换取openid和session_key。session_key一般不用暴露给前端,后端拿到后可以直接丢弃或用于解密。注意code2Session接口需要appid和secret,这两个值应当只配置在后端环境变量里,绝不能写进小程序源码;一旦泄露,别人可以冒用你的身份做接口调用。换到openid后,后端签发自己的token,推荐用 JWT,有效期可以设 7 天,用户在评选周期内不需要重复登录。
3. 跑通投票接口:小程序请求封装、防重复提交与计票服务
登录链路通了之后,接下来就是核心业务:投票。投票接口看起来只是一个简单的“票数加一”,但实际开发中要处理三件事:请求统一带身份凭证、防止同一用户重复投票、并发情况下保证票数不丢。这一章我会给出小程序端的请求封装代码和后端计票接口实现,这两段代码是这套系统的骨架。
3.1 小程序端请求封装:统一携带 token 与超时策略
小程序里如果每个页面都直接调wx.request,会写出大量重复代码,而且容易漏带token。习惯上会先封装一个request函数,所有接口都走它。这样后续要加日志、加错误提示、统一处理登录失效,只需要改一个文件。
// utils/request.js 封装微信请求 const request = (options) => { const token = wx.getStorageSync('token'); return new Promise((resolve, reject) => { wx.request({ url: options.url, method: options.method || 'GET', data: options.data || {}, timeout: 10000, header: { 'Content-Type': 'application/json', 'Authorization': token ? `Bearer ${token}` : '' }, success: (res) => { if (res.statusCode === 401) { // token 过期,清理缓存并重新登录 wx.removeStorageSync('token'); wx.navigateTo({ url: '/pages/login/index' }); return; } if (res.statusCode >= 200 && res.statusCode < 300) { resolve(res.data); } else { reject(res); } }, fail: (err) => reject(err) }); }); }; module.exports = request;这段代码的逻辑重点有三个。第一,Authorization头统一从本地缓存读 token,所有业务接口都不需要关心登录态怎么传。第二,timeout: 10000设了 10 秒超时,投票场景下网络慢时用户会着急点第二次,超时时间不宜太长,但也不能太短,弱网环境下 5 秒可能不够。第三,401时统一清 token 并跳登录页,避免每个页面重复处理登录失效。
有一个细节新手容易忽略:wx.request的success回调不代表业务成功,HTTP 200 只是网络层通了,业务层的code字段才是真正的状态码。所以封装里我习惯再加一道判断,如果res.data.code不为 0,用wx.showToast给出业务错误提示,例如“您已经投过票了”。
3.2 后端计票接口:原子更新与幂等判断
后端计票接口的设计决定了票数准确性。最典型的错误写法是:先SELECT votes FROM candidate WHERE id = ?,在代码里加一,再UPDATE回去。这种“读改写”模式在并发请求下一定会丢票,两个请求同时读到 100,各自加一后写回 101,实际应该变成 102。
正确的做法是用数据库的原子自增。MySQL 里直接执行UPDATE candidates SET votes = votes + 1 WHERE id = ?,由数据库保证并发安全。但“票数没丢”还不够,还要保证同一用户不能重复投票,这就要在接口层做幂等控制。
// service/vote.js 投票接口核心逻辑 app.post('/api/vote', async (req, res) => { const { candidateId } = req.body; const userId = req.user.id; // 由鉴权中间件注入 // 方案一:数据库唯一索引兜底,能防重复 // 方案二:Redis SETNX 做短窗口幂等,减轻数据库压力 const key = `vote:${userId}:${candidateId}`; const acquired = await redis.set(key, '1', 'EX', 86400, 'NX'); if (!acquired) { return res.json({ code: 4001, message: '您已经投过票了' }); } // 通过幂等检查后执行原子更新 await db.execute( 'UPDATE candidates SET votes = votes + 1 WHERE id = ?', [candidateId] ); // 写一条明细记录,用于审计和对账 await db.execute( 'INSERT INTO vote_record (election_id, user_id, candidate_id) VALUES (?, ?, ?)', [req.body.electionId, userId, candidateId] ); const newVotes = await db.query( 'SELECT votes FROM candidates WHERE id = ?', [candidateId] ); res.json({ code: 0, data: { newVotes: newVotes[0].votes } }); });这里的参数选择值得细说。EX 86400表示幂等键有效期一天,覆盖大多数评选活动的时间窗口;如果评选可以跨天,这个值要改成活动结束时间。NX表示只有当 key 不存在时才写入,多个请求同时进来只有一个能成功。项目里如果没有 Redis,可以把这段去掉,完全依赖数据库的unique key (user_id, candidate_id)兜底,效果一样,只是数据库压力会稍高。对于日活几千的小型投票系统,推荐直接用唯一索引方案,少一个中间件少一分运维负担,也符合“能用简单方案就不用复杂架构”的原则。
4. 数据库设计:投票记录、候选人与多轮评选的建表方案
数据库是整个投票系统的核心资产。标题里特意提到“数据库”,说明这份源码不是只给前端页面,而是带完整的建表脚本。拿到 zip 解压后,db/目录下通常有一份.sql文件,用 MySQL 或 Navicat 导入即可。但导入只是第一步,你要能看懂表结构、知道哪些字段是防重复的关键,才敢改业务。
4.1 三张核心表:user、candidate、vote_record
一套投票评选系统最少需要三张表:用户表、候选人表、投票记录表。如果系统支持多个评选活动同时进行,还要加一张election活动表。下面这份建表 SQL 是典型设计,字段做了精简但保持了核心约束:
CREATE DATABASE IF NOT EXISTS vote DEFAULT CHARACTER SET utf8mb4; USE vote; -- 活动表:一场评选活动 CREATE TABLE election ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, title VARCHAR(100) NOT NULL, start_time DATETIME NOT NULL, end_time DATETIME NOT NULL, status TINYINT NOT NULL DEFAULT 1 COMMENT '1进行中 0已结束' ) ENGINE=InnoDB; -- 候选人表:归属于某场活动 CREATE TABLE candidate ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, election_id INT UNSIGNED NOT NULL, name VARCHAR(50) NOT NULL, cover_url VARCHAR(255) DEFAULT '', votes INT UNSIGNED NOT NULL DEFAULT 0, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, KEY idx_election (election_id) ) ENGINE=InnoDB; -- 投票记录表:一票一条明细 CREATE TABLE vote_record ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, election_id INT UNSIGNED NOT NULL, user_id INT UNSIGNED NOT NULL, candidate_id INT UNSIGNED NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_user_candidate (user_id, candidate_id), KEY idx_candidate (candidate_id) ) ENGINE=InnoDB;字段设计上有几个关键决策。candidate.votes是一个冗余计数器,用来快速展示票数,省去每次查询都COUNT一张明细表的开销;三张表都使用 InnoDB,保证事务和行级锁能力。vote_record表最核心的约束是UNIQUE KEY uk_user_candidate (user_id, candidate_id),这个唯一索引从数据库层面保证同一个用户对同一个候选人只能产生一条记录,是防重复投票的最后一道防线。
utf8mb4是必须的字符集,不是utf8。原因在于微信昵称可能包含 Emoji,比如“王小😀”,utf8只支持三个字节,存这种字符会直接报错或变成乱码。建库时如果漏了这一步,后期改字符集要重建表,代价很大。
4.2 票数统计与分页查询:计数器为什么比临时 COUNT 快
业务上最常见的两个查询是:活动候选人排行榜、候选人详情页的票数。排行榜的 SQL 很简单:
SELECT id, name, votes FROM candidate WHERE election_id = 1 ORDER BY votes DESC, id ASC LIMIT 20 OFFSET 0;注意ORDER BY votes DESC, id ASC这里把id作为次级排序。因为投票数相同的候选人可能很多,如果只按votes排序,MySQL 返回结果的顺序不确定,翻页时可能会看到同一行数据跨页重复或遗漏。加上id ASC后排序稳定,结果可预测。
这个查询直接读candidate.votes计数器,即使有几千个候选人,走idx_election索引后也是毫秒级。但如果全部明细只存在vote_record里,每次都要做聚合COUNT,数据量大时索引扫描的代价明显更高。所以设计上计数器用于展示,明细表用于审计。
另一个会踩坑的是深分页。LIMIT 200000, 20这种写法,MySQL 会先把前 200020 行全部查出来再丢弃前 20 万行,越往后越慢。投票榜这种一次性活动数据量通常不大,但如果候选人很多,我一般会改用游标分页:
SELECT id, name, votes FROM candidate WHERE election_id = 1 AND votes < ? ORDER BY votes DESC LIMIT 20;调用方把上一页最后一条记录的votes和id传进来作为查询条件,而不是用OFFSET。这样每页扫描的条数等于页大小,不会随页码增加而变慢。
5. 避坑:投票系统验收阶段最容易翻车的 5 个常见问题
投票系统的验收往往比开发更痛苦。功能表面上都能跑,但一进入真实验收场景就开始出各种怪问题。这里把我在投票类项目中反复遇到的高频问题列出来,每一条都按现象、原因、解决来写。
5.1 换微信小号反复投票,规则形同虚设
现象:活动方反馈同一个真人换了微信号之后还能再投,投票数明显虚高。
原因:系统只校验了openid,而同一个微信用户注册多个小号后会得到不同的openid。如果活动规则是“每人限一票”,仅靠openid无法识别出这是同一个人。
解决:接入微信开放平台的unionid,同一微信身份主体下的所有账号共享同一个unionid,用它来判重。如果没有开放平台资质,可以改为绑定手机号,投票前要求输入手机号并做短信验证。规则一定要在活动开始前确认清楚,是“每微信号一票”还是“每认证用户一票”,两种规则对应完全不同的技术方案。
5.2 并发投票后票数对不上,总量比明细少
现象:压测时同时发起 50 个投票请求,最后candidate.votes的合计值比vote_record表的明细记录数少了几条。
原因:后端使用了“先查再改”的逻辑,两个请求同时读到旧值,各自加一后写回,最后只加了一次。这是典型的竞态条件。
解决:把计票语句改成原子更新UPDATE candidates SET votes = votes + 1 WHERE id = ?,并配合vote_record的唯一索引。上线后跑对账脚本,用COUNT(vote_record)和candidate.votes做一致性校验,不一致立刻报警。
5.3 zip 包里的数据库导入报错,表不存在或中文乱码
现象:按 README 导入数据库,MySQL 提示Table 'vote.candidate' doesn't exist,或者导入后中文全部变成问号。还有一种情况是压缩包解压时提示需要密码,但文档里没给密码。
原因:表不存在的常见原因是 MySQL 的lower_case_table_names参数在 Linux 下默认是 0,表名区分大小写,而 SQL 文件里建表语句和后续插入语句的大小写不一致;乱码则是客户端连接字符集与表字符集不一致。解压提示密码,可能是压缩包被标记了伪加密,这是一个在 zip 文件中常见的标记位问题,实际上数据并没有加密,普通解压工具会误判。
解决:导入前先检查 MySQL 大小写敏感配置,统一把表名写成小写;导入时指定--default-character-set=utf8mb4,并在 SQL 文件头部确认没有混入latin1的旧表结构。伪加密压缩包用 7-Zip 打开,它通常能忽略伪加密标记直接解压。这类坑属于“拿到资源包第一晚就劝退”的类型,排查顺序是:先看字符集,再看表名大小写,最后检查压缩包本身。
5.4 投票成功提示后刷新,票数还是旧值
现象:用户投票后收到“投票成功”的提示,回到列表页看到的票数却没有变化,杀掉小程序重新进入才正常。
原因:列表页和详情页的接口响应被缓存了。小程序的wx.request默认不会缓存 GET 请求,但如果用了第三方请求库,或者后端在响应头里设置了Cache-Control,就会命中缓存。还有一种情况是投票成功后没有通知列表页刷新,页面还停留在旧的渲染数据上。
解决:投票成功后手动调用列表数据刷新,而不是依赖页面的onShow自动刷新;后端对票数接口设置Cache-Control: no-cache。如果用了 CDN,注意排查 CDN 层是否缓存了动态接口。最省事的做法是请求时加一个时间戳参数,例如?ts=Date.now(),从根上避开缓存。
5.5 双击投票按钮发出两张票
现象:手快连点两次投票按钮,后台vote_record里出现了两条同用户同候选人的记录,票数加了两次。
原因:前端没有做按钮防重复点击,后端也有判断但没有唯一索引兜底,导致并发请求都通过了业务校验。
解决:三层防御缺一不可。前端在点击后立刻把按钮置灰,直到请求结束;后端用前面说的Redis SETNX或数据库唯一索引兜底;最后在vote_record表上建立unique key。我见过只做前端置灰、没做后端防御的项目,用户用抓包工具照样能连发请求。记住一句话:前端置灰是体验问题,后端幂等才是数据安全问题。
6. 上线前验证:数据对账、并发压测与真机手测
投票系统上线前一天,我固定会做三件事:跑对账、压接口、真机手点。这三步能过滤掉大多数验收事故,比反复看代码更有效。
第一步是对账,直接查冗余计数器和明细表是否一致:
SELECT c.id, c.votes AS counter_votes, (SELECT COUNT(*) FROM vote_record v WHERE v.candidate_id = c.id) AS real_votes FROM candidate c HAVING counter_votes <> real_votes;正常结果应为空集。这一步验证了计数器和明细表没有分叉,也顺带检查了唯一索引是否生效。
第二步是接口压测。小系统不需要上 LoadRunner,用本地 Node 脚本并发打 20 个请求即可。测试完再跑一次上面的对账 SQL,如果票数一致,说明原子更新和幂等控制都生效了。
const tasks = Array.from({ length: 20 }, () => fetch('http://localhost:3000/api/vote', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ candidateId: 1, electionId: 1 }) }).then(res => res.json()) ); Promise.all(tasks).then(results => { const successCount = results.filter(r => r.code === 0).length; console.log(`成功 ${successCount} 票,拒绝 ${20 - successCount} 次`); });第三步是真机手测,重点场景只有三个:连点投票按钮、断网恢复后重试、同一微信号在两台手机上同时投票。前两个场景覆盖了用户最常见的误操作,第三个场景验证幂等约束是否真的在数据库层生效。
这几轮做完,我心里才有底。做过的投票项目里,有两次是压测后对账发现票数少了,排查到最后都是读改写竞态;还有一次是上线当天发现表名大小写问题导致部分接口 500。从那以后,对账脚本和真机测试就成了我固定保留的习惯。项目可以小,但票数不能错,这是投票系统唯一的底线。希望帮到你。
本文还有配套的精品资源,点击获取