资讯动态

投票微信小程序完整源码+数据库:从建表到答辩避坑指南

发布时间:2026/10/3 4:26:18 来源:尧图企业网站定制
简介面向小程序开发学习者和毕业设计学生这份投票微信小程序完整源码与数据库提供了一套可直接运行的项目方案覆盖用户登录授权、创建投票、选手资料维护、在线投票与结果统计等核心环节适合期末大作业、课程设计或作为毕业设计基础框架。压缩包共八百八十个文件整体大小约十二点三一兆字节文件类型以图片、矢量图标、页面样式、服务端与前端脚本、数据库脚本和配置文件为主目录结构按功能模块划分便于阅读和二次开发。目前已有百余人浏览学习源码经过本地编译验证评审分达九十八分整体难度适中内容经助教审定能满足学习验证和答辩展示需求。除完整项目代码外包内还保留了部分备份文件与安装运行脚本方便快速部署结合数据库表结构、接口调用逻辑和前端交互代码能够清楚理解投票小程序的工程实现也为后续扩展功能或撰写设计文档提供参考。1. 投票微信小程序不是“写”不动而是“拼”不动“投票微信小程序完整源码数据库高分毕设项目”这套东西我在帮人调毕设时见过太多次。它最常见的用法是导入微信开发者工具连上数据库跑通投票、排行榜、个人中心然后截图写论文。但大多数翻车不是发生在写功能而是发生在“拼”的阶段前端要的字段后端没给数据库表里缺了活动状态字段真机上请求直接被拦最后数据乱得不敢给老师演示。这篇笔记就是把拼装过程的边界、参数和坑一次说透适合两类人拿现成源码直接改的以及拿源码当脚手架自己重新实现的。本质上都逃不开一件事把小程序前端、后端接口和数据库三端对齐。2. 先把系统边界画清楚小程序端、后端接口、数据库各扛什么活2.1 投票业务拆成四个核心流程一套投票微信小程序项目拆开看本质就是数据库的增删改查用户数据要查、活动数据要查、投票记录要插、排行榜要聚合全是增删改查的组合。把它做成能演示、能答辩、能扩展的项目至少要先拆出四个流程活动配置、用户身份、投票动作、结果统计。流程前端页面后端接口数据库角色活动配置活动列表、投票页头部查询活动、校验活动状态保存活动起止时间与状态用户身份登录页、个人中心微信登录换取 openid保存用户唯一标识投票动作投票页、候选人列表插入投票记录写入记录并累加票数结果统计排行榜、数据图表聚合查询按候选人分组排序活动配置决定“能不能投”和“投给谁”。投票小程序里常见的字段包括活动标题、开始时间、结束时间、状态以及候选人列表。这些字段如果一开始漏了状态后面会出现活动已经结束用户还能投票的尴尬情况。用户身份决定“谁投的”。微信小程序前端拿不到稳定的用户身份需要靠wx.login()拿到临时 code再交给后端向微信接口换 openid。openid 是每个微信用户在小程序里的唯一 ID这也是投票系统防止重复投票的基础。投票动作是核心中的核心。一个用户对同一个活动只能投一次这个约束要同时落在前端交互和后端数据库上。前端防连点后端靠唯一索引兜底缺一个都容易在演示时翻车。结果统计是答辩的亮点。投票完要看排行榜按候选人聚合票数倒序输出。这里涉及索引和聚合查询的写法写不好数据量一大页面就转圈。2.2 后端选型自建服务器 vs 微信云开发标题里明确有“数据库”三个字通常意味着这套源码的数据库是一个可导入本地的关系型库。我拿到这类项目后的第一件事是找.sql文件。有.sql文件走自建服务器最稳只有云开发目录那数据库是 JSON 结构本地跑不了。两种方案的核心区别在部署和维护方式上对比项自建后端 MySQL微信云开发数据库形态MySQL脚本可导入本地云数据库JSON 文档型域名要求需要 HTTPS 合法域名不需要小程序直连云开发本地调试可完全离线跑通依赖云端环境适合答辩SQL 和 ER 图能写进论文数据在云端不好截图展示如果论文要求画数据库 ER 图、贴 SQL 语句或者老师现场让你“把数据库打开看看”自建后端是更稳的选择。MySQL 是关系型数据库的主流网上资料多连mysql命令行的细节都能找到现成答案。如果你拿到的源码是云开发版本也不要灰心。可以把云开发数据库的表结构导出成 JSON再手工映射成 MySQL 建表语句。迁移工作量不大核心也就是用户表、活动表、候选人表、投票记录表这四张。2.3 源码工程目录的标准打开方式把源码包解压后先不要急着双击用微信开发者工具打开先在命令行里看目录结构。一套结构清晰的高分毕设项目目录通常长这样vote-miniapp/ ├── miniprogram/ # 小程序前端代码 ├── cloudfunctions/ # 云开发版本才有的云函数目录 ├── server/ # 自建后端源码 │ ├── app.js │ ├── routes/ # 路由目录 │ ├── db/ │ │ └── init.sql # 数据库初始化脚本 └── README.md # 启动说明拿到目录先做三件事。第一找init.sql或dump.sql没有数据库脚本的源码包直接减分一大半。第二看server目录下有没有package.json没有的话后端起不来说明对方只贴了代码没给依赖。第三确认小程序端的 AppID 是不是被作者占用导入后第一件事就是换成自己的 AppID。很多源码包是作者从自己毕设里直接压缩上传的机器上装的 Node 版本、MySQL 版本和你不一样所以node_modules一般不会打包。默认操作是进server目录执行npm install装完再启动别指望源码解压就能跑。3. 建库和写接口让“投票”先跑起来3.1 四张核心表先于代码落库数据库脚本是整套源码的心脏。投票小程序的核心表至少四张用户表、活动表、候选人表、投票记录表。很多毕设项目把候选人和活动合并成一张表能跑但后期要加“多活动投票”就得重构。我给出一份可直接落库的 MySQL 建表脚本CREATE TABLE user ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, openid VARCHAR(64) NOT NULL, nickname VARCHAR(64) DEFAULT , avatar_url VARCHAR(255) DEFAULT , created_at DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_openid (openid) ); CREATE TABLE vote_activity ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, title VARCHAR(100) NOT NULL, start_time DATETIME NOT NULL, end_time DATETIME NOT NULL, status TINYINT DEFAULT 1 COMMENT 1进行中 2已结束, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_status (status) ); CREATE TABLE candidate ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, activity_id INT UNSIGNED NOT NULL, name VARCHAR(50) NOT NULL, avatar_url VARCHAR(255) DEFAULT , vote_count INT UNSIGNED DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_activity (activity_id) ); CREATE TABLE vote_record ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, activity_id INT UNSIGNED NOT NULL, user_id INT UNSIGNED NOT NULL, candidate_id INT UNSIGNED NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_activity (activity_id), KEY idx_candidate (candidate_id), UNIQUE KEY uk_activity_user (activity_id, user_id) );vote_record表必须加联合唯一约束uk_activity_user (activity_id, user_id)。哪怕前端做了防重复提交网络重试或手速过快仍可能产生两条请求这条唯一约束是防重复投票的最后一道闸门。候选人表单独拆出来是为了以后扩展“多个活动共用一套候选人”或者“候选人需要换头像”时不用改表结构。vote_count字段是冗余设计的典型每次投票顺手加一排行榜直接查这个字段而不是临时COUNT投票记录。提示四张表之间我建议不加物理外键。毕设阶段数据量小外键约束会影响删除活动时的灵活度。逻辑关联靠activity_id和candidate_id保持论文里的 E-R 图照样画。3.2 投一票要动两张表事务是基本盘投票动作不是一条 INSERT 那么简单。正常情况下后端收到投票请求后要做两步往vote_record插入一条记录同时把candidate表的vote_count加一。两步之间只要一步成功一步失败票数就对不上。把两个写操作放进同一事务是最稳的写法。下面是一段 Node.js mysql2 连接池的示例代码const mysql require(mysql2/promise); const pool mysql.createPool({ host: localhost, user: root, password: 你的密码, database: vote_db, waitForConnections: true, // 队列等待空闲连接 connectionLimit: 10, // 连接池上限 }); async function vote(userId, activityId, candidateId) { const conn await pool.getConnection(); try { await conn.beginTransaction(); // 插入投票记录。唯一约束 uk_activity_user 会拦截重复投票 await conn.execute( INSERT INTO vote_record (activity_id, user_id, candidate_id) VALUES (?, ?, ?), [activityId, userId, candidateId] ); // 更新候选人票数使用自增表达式避免覆盖并发写入 await conn.execute( UPDATE candidate SET vote_count vote_count 1 WHERE id ?, [candidateId] ); await conn.commit(); return { success: true }; } catch (err) { await conn.rollback(); return { success: false, msg: err.message }; } finally { conn.release(); } }这段代码使用mysql2/promise连接池connectionLimit设置连接池上限常见设置为 10~20。太小会让多个用户同时投票时排队太大会浪费 MySQL 资源毕设阶段 10 足够。事务里的关键点在 rollback如果 INSERT 触发唯一约束错误catch 里执行 rollbackcandidate表的vote_count不会被错误加一。这也是把两张表操作放一个事务里的原因。3.3 排行榜查询索引、count 和分页的取舍排行榜页面的核心查询是按候选人聚合投票记录。常见写法SELECT c.id, c.name, c.avatar_url, COUNT(r.id) AS vote_count FROM candidate c LEFT JOIN vote_record r ON c.id r.candidate_id WHERE c.activity_id 1 GROUP BY c.id, c.name, c.avatar_url ORDER BY vote_count DESC LIMIT 20;这段 SQL 里有两个关键点。第一vote_record表上要有idx_candidate索引否则 GROUP BY 和 JOIN 在数据量上来后会变慢。第二ORDER BY vote_count DESC排序时可以用candidate.vote_count字段直接排而不是依赖COUNT的结果这样排序会更快。更优的做法是直接查询候选人表按vote_count排序SELECT id, name, avatar_url, vote_count FROM candidate WHERE activity_id 1 ORDER BY vote_count DESC LIMIT 20;投票记录表里存了完整流水候选人表里存了统计值两个配合能应付绝大多数查询场景。分页用 LIMIT 就好不要把全部记录拉到小程序端再排序小程序端的setData处理大量数组时会明显卡顿。注意不要在小程序端做count后再“补”一条请求更新页面很容易造成前后端数据不一致。排行数据交给后端一次返回前端只负责渲染。4. 小程序端搭起来登录、投票页、结果页怎么接4.1 从导入工程到跑通本地接口微信开发者工具导入源码前先确认电脑装了 Node.js然后打开工具选择“导入项目”目录指向miniprogram。导入后先看project.config.json里的 appid源码作者大概率用自己的 AppID你要换成自己的否则预览和真机调试都会报错。{ appid: wx你的AppID, compileType: miniprogram, libVersion: latest, setting: { urlCheck: false, es6: true, postcss: true, minified: true } }urlCheck: false是开发阶段跳过合法域名校验的开关设为 false 后小程序可以请求http://127.0.0.1:3000这样的本地接口。但这只是开发阶段的缓冲真机上这个开关不生效所以别指望靠它蒙混过关。如果要自定义顶部导航栏app.json里设置navigationStyle: custom此时自定义导航栏组件需要手动预留顶部状态栏高度。这个高度在不同机型上不一样常见做法是用wx.getSystemInfoSync()里的statusBarHeight字段动态设置而不是写死。4.2 授权登录openid 只存在于后端会话里小程序端没有 cookie登录态的维持方式通常是前端wx.login()拿临时 code后端拿 code 换 openid再生成 token 返回前端。后续请求头带 token后端解析出用户身份。async function login() { const { code } await wx.login(); const res await new Promise((resolve, reject) { wx.request({ url: http://127.0.0.1:3000/api/login, method: POST, data: { code }, success: r resolve(r.data), fail: () reject(new Error(login failed)), }); }); wx.setStorageSync(token, res.token); }拿到 code 后后端用app_id和app_secret向微信接口换 openid。整个流程里openid 不该返回到前端保存前端只需要持有 token。token 可以按自己的需要设置缓存时间。投票场景里建议 token 有效期设短一些比如 2 小时过期后重新wx.login()换新的这样后端可以比较方便地控制“登录状态”。别把它设成 30 天投票活动结束后用户的残留登录态容易在下一个活动里造成身份混淆。4.3 投票页单选框渲染、状态锁与页面跳转投票页最常见的翻车是用户连续点击投票按钮发出十几个请求。后端有唯一约束兜底不怕重复写入但用户看到按钮一直“转圈”会以为卡了。前端要用一个状态锁挡住重复提交。let voting false; async function handleVote() { if (voting) return; voting true; wx.showLoading({ title: 投票中 }); try { await voteApi({ candidateId: this.data.selectedId }); wx.hideLoading(); wx.showToast({ title: 投票成功, icon: success }); setTimeout(() { wx.redirectTo({ url: /pages/result/result?activityId${this.data.activityId} }); }, 1200); } catch (e) { wx.hideLoading(); wx.showToast({ title: e.message || 投票失败, icon: none }); } finally { voting false; } }候选人列表在 WXML 里用radio-group加wx:for渲染选中项通过bindchange事件写入selectedId。我给这个selectedId加一个校验用户没选任何候选人时直接点击投票要弹 toast 提示“请先选择候选人”不要默默调接口。投票成功后的跳转延迟 1~2 秒是故意的让用户看到成功提示再进下一页。因为redirectTo会关闭当前页面如果立即跳转Loading 和 Toast 容易被新页面覆盖用户会以为没投上。5. 投票小程序常见问题避坑按整套源码走还翻车5.1 MySQL 8 连不上认证插件先背一半锅现象后端已启动数据库账号密码也没错但一操作投票就报ER_NOT_SUPPORTED_AUTH_MODE用 Navicat 连接也失败。原因MySQL 8 默认使用caching_sha2_password认证插件而很多 Node 老版本驱动或可视化工具默认只认mysql_native_password两边握手失败。解决给当前用户单独指定认证插件不需要动 MySQL 全局配置ALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY 你的密码; FLUSH PRIVILEGES;如果担心直接改 root 有风险可以新建一个专用账号只授权vote_db库权限按需最小化。改完后把后端代码里的密码同步更新再重启服务。5.2 真机请求 403 或 404域名校验和路径问题现象开发者工具里一切正常真机预览时投票请求 403页面空白或请求发出去了但返回 404。原因403 通常是真机触发了合法域名校验开发时用了http://127.0.0.1或局域网 IP真机上微信不允许这类请求。404 则多是后端路由路径对不上比如后端挂在/api/vote小程序请求却写成了/vote。解决上线前在小程序管理后台配置 HTTPS 的 request 合法域名同时保证后端部署在带证书的服务器上。本地临时演示可以用“开发版”加“不校验合法域名”的组合但别把这个状态当生产配置。404 的排查先看后端路由挂载路径再看小程序请求 URL两边的斜杠和前缀必须完全一致。5.3 票数对不上并发请求把记录“覆盖”了现象几个人同时投票后后台统计票数和vote_record记录数对不上总票数少了。原因候选人的票数更新写成了先查再写的模式比如SELECT vote_count FROM candidate WHERE id1得到 10再加 1 变成 11然后UPDATE ... SET vote_count11。并发时两个请求都读到 10分别写成 11结果丢了一票。解决更新票数一律用自增表达式不要查出旧值再写回UPDATE candidate SET vote_count vote_count 1 WHERE id ?;这个写法让数据库自己完成“读旧值、加一、写新值”的原子操作。配合前面第 3 章里的唯一约束重复投票和并发丢票两个问题都能压住。5.4 排行榜不刷新缓存时间与下拉刷新现象投票成功后回到排行榜页面上的票数和刚才投完看到的不一致下拉刷新后甚至恢复了旧值。原因小程序页面的onLoad只在页面首次加载时执行从投票页redirectTo到结果页时如果结果页还在页面栈里onLoad不会重新触发。另外后端接口如果带了 HTTP 缓存头也会让小程序的请求拿到旧数据。解决结果页改用onShow()拉取排行榜数据每次页面从后台回到前台都会执行这个方法。后端接口响应头加上Cache-Control: no-cache。前端给排行榜数据设置缓存时间时建议不超过 60 秒太长了会在投票刚结束时出现明显延迟。5.5 样例错乱rpx 和 rem 的换算陷阱现象在 iPhone 上页面布局正常换到安卓机型后按钮错位、字体忽大忽小。原因源码里混用了 rpx 和 rem。rpx 是微信小程序的响应单位以 750 宽设计稿为基准rem 是网页单位依赖根字体大小。小程序端错误使用 rem 会在不同机型上算出完全不同的尺寸。解决小程序界面统一用 rpx。字体大小推荐 24rpx~32rpx 区间按钮高度用 80rpx 这类固定值间距用margin: 20rpx。个别地方需要跟随屏幕宽度变化的可以用wxss配合媒体查询做适配但不要再用 rem 做核心布局。6. 从“能跑”到“高分毕设”验证、答辩素材和最后检查6.1 用一次并发测试证明“接口能抗住”答辩最怕被问“并发怎么办”。我习惯用一段小脚本模拟多个用户同时投票顺便验证唯一约束const requests Array.from({ length: 20 }, (_, i) fetch(http://127.0.0.1:3000/api/vote, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ activityId: 1, candidateId: 1, userId: i 1 }), }) ); Promise.allSettled(requests).then(console.log);20 个并发请求打过去预期结果是只有 1 条成功其他因为uk_activity_user约束报重复。这个结果截图放进论文的测试章节比写十行“系统性能良好”有说服力得多。6.2 把结果页做成图表页排行榜表格之外加一个“各候选人得票占比”饼图或柱状图项目完整度会立刻不一样。小程序端可以用 ec-canvas 组件封装 echarts数据直接请求后端聚合接口。答辩时展示图表比展示数据库截图直观得多。6.3 答辩前三天只做检查不新增功能我的习惯是最后三天完全停止加功能只按“删库重导 → 启动后端 → 小程序预览”的顺序走三遍。任何一步需要翻笔记才能通过的都写进 README。真机调试时录一段 1 分钟完整流程视频防止现场环境报错没有后悔药。做过太多次这种项目后我最大的体会是高分毕设不是功能堆出来的是“别人拿过去能复现”换来的。把数据库脚本、启动步骤、依赖版本写清楚比多写一个投票弹窗动画更能让答辩老师认可。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑