微信小程序投票评选系统源码实战:毕设部署与避坑指南

发布时间:2026/9/26 22:27:11
微信小程序投票评选系统源码实战:毕设部署与避坑指南 简介这套微信小程序投票评选系统源码是面向毕业设计场景的完整Java项目包含小程序端与后台管理端适合计算机相关专业学生参考学习也可作为课程设计或毕业设计的基础框架。项目实现投票活动配置、选手管理、用户投票、结果排行等功能覆盖前后端交互、数据库设计与接口调用等关键环节。压缩包共884个文件大小约19.64MB主要包含Java源码、小程序WXML/WXSS/JS页面、XML与JSON配置文件、CSS样式以及数据库脚本和项目配置文件目录结构清晰便于按模块定位代码。资源已在本地编译验证可运行下载后配置JDK、数据库等环境即可启动代码逻辑经过指导老师确认功能满足常规毕业设计答辩要求。当前已有475人学习下载适合需要快速搭建投票评选类小程序项目的开发者参考。1. 微信小程序投票评选系统源码先把它当“可跑的毕设样板”而不是代码仓库拿到这个 zip 的瞬间需要你先做一次身份切换别把它当“别人写好的代码收藏”而是当“一件需要你复现、修改、能答辩讲清每一步的毕业设计作品”。这套微信小程序投票评选系统源码由小程序前端、Java 后端、MySQL 数据库三部分组成本地解压后配好环境就能跑起来。它能解决的最核心的问题是活动创建、候选人/选项维护、用户扫码或登录后投票、榜单实时展示以及后台对票数结果的统计。适合正在做 Java 方向或小程序方向毕业设计的人也适合想快速搭一套微信端投票应用的开发者。后面的内容会完全围绕“能跑通、能改、能答辩”来展开。2. 源码结构先拆清楚小程序端、Java 后端与数据库三块怎么分工2.1 小程序端核心代码页面、组件与接口请求封装打开 zip 后先按三个目录去识别结构小程序端一般是miniprogram/或pages/这样的目录Java 后端是标准的 Maven 工程pom.xml在根目录数据库脚本是一个独立的.sql文件。我习惯的顺序是先看数据库脚本再看后端接口最后才碰小程序页面。小程序端只是消费者它拿到的每一个数据都由后端接口决定页面报错多数是接口问题。典型的小程序端目录结构是这样的miniprogram/ pages/ index/ # 首页活动列表入口 detail/ # 活动详情与投票按钮 my/ # 个人中心登录状态、我参与的活动 utils/ request.js # wx.request 封装统一携带 token app.js app.json project.config.json # 微信开发者工具导入时识别这个文件这套投票评选系统最关键的封装在于utils/request.js。所有页面请求都走这一个文件而不是每个页面单独写wx.request。这样的好处很直观后端返回 401 时统一跳转登录页token 过期自动处理接口地址改动只改一处。const BASE_URL http://localhost:8080/api; function request(path, method GET, data {}, token ) { return new Promise((resolve, reject) { wx.request({ url: BASE_URL path, method: method, data: data, header: { content-type: application/json, Authorization: token ? Bearer token : }, success(res) { if (res.statusCode 401) { wx.navigateTo({ url: /pages/login/login }); reject(res); } else if (res.statusCode 200 res.statusCode 300) { resolve(res.data); } else { reject(res); } }, fail(err) { reject(err); } }); }); } module.exports { request, BASE_URL };这段代码需要解释几个点。BASE_URL在本地调试时是localhost:8080真机调试时要换成开发机的局域网 IP否则手机永远请求不到。Authorization头即使为空也会带上后端可以从请求头里读取 token为空则当作未登录处理由后端拦截器返回 401。wx.request的默认超时是 60 秒投票、榜单查询这类短接口一般不用改如果出现上传图片长时间无响应多半不是这个文件的问题而是后端接口处理太慢。再看具体页面的使用方式。首页index.js拉取活动列表// pages/index/index.js const { request } require(../../utils/request); Page({ data: { activityList: [], loading: false }, onLoad() { this.fetchList(); }, fetchList() { this.setData({ loading: true }); request(/activity/list, GET) .then(data this.setData({ activityList: data.rows })) .catch(() wx.showToast({ title: 加载失败, icon: none })) .finally(() this.setData({ loading: false })); }, goDetail(e) { const id e.currentTarget.dataset.id; wx.navigateTo({ url: /pages/detail/detail?id${id} }); } });loading状态用来控制页面加载动画避免用户重复点击data.id从wxml里通过>// pages/detail/detail.js const { request } require(../../utils/request); Page({ data: { activity: null, options: [], voted: false }, onLoad(query) { this.activityId query.id; this.fetchDetail(); }, fetchDetail() { request(/activity/detail?id${this.activityId}, GET) .then(data { this.setData({ activity: data.activity, options: data.options }); }); }, vote(e) { const optionId e.currentTarget.dataset.id; request(/vote/submit, POST, { activityId: Number(this.activityId), optionId: optionId }) .then(() { this.setData({ voted: true }); wx.showToast({ title: 投票成功, icon: success }); this.fetchDetail(); }) .catch(() wx.showToast({ title: 投票失败, icon: none })); } });这里有两个容易被忽略的细节。投票成功后立即重新拉取详情得票数才能实时刷新而不是本地做假递增前端通过voted状态禁用投票按钮只是一种体验优化真正防重复投票必须靠后端和数据库唯一索引后面避坑章节会展开。2.2 Java 后端接口与数据库表设计从登录鉴权到投票计数后端是 Spring Boot 风格的 Java 工程Controller、Service、Mapper 三层结构。Controller 层负责接收小程序请求、做参数校验Service 层放业务逻辑Mapper 层直接操作数据库。投票的核心接口是POST /api/vote/submit。RestController RequestMapping(/api) public class VoteController { PostMapping(/vote/submit) public Result submit(RequestBody VoteReq req, HttpServletRequest request) { Integer userId (Integer) request.getAttribute(userId); voteService.submitVote(userId, req.getActivityId(), req.getOptionId()); return Result.success(); } GetMapping(/activity/detail) public Result detail(RequestParam Integer id) { return Result.success(voteService.getActivityDetail(id)); } }userId从request.getAttribute取说明登录用户的鉴权在拦截器里已经完成Controller 不需要再解析 token。Service 层的提交投票方法要更细Transactional public void submitVote(Integer userId, Integer activityId, Integer optionId) { // 1. 活动存在且处于进行中 Activity activity activityMapper.selectById(activityId); if (activity null || activity.getStatus() ! 1) { throw new BizException(活动不存在或已结束); } // 2. 同一用户对同一活动只能投一次 Integer count voteRecordMapper.countByUserAndActivity(userId, activityId); if (count 0) { throw new BizException(你已经投过票了); } // 3. 插入投票流水 VoteRecord record new VoteRecord(); record.setUserId(userId); record.setActivityId(activityId); record.setOptionId(optionId); voteRecordMapper.insert(record); // 4. 更新候选人的冗余票数 optionMapper.incrCount(optionId); }Transactional保证插入流水和更新人数同一个事务要么都成功、要么都失败。第 2 步的count判断属于“先查再插”极端并发下可能漏判所以数据库层还必须加唯一索引兜底这在很多毕设源码里没有体现你拿到手后建议自己补上。数据库表设计是投票系统的地基。正常的投票评选系统会有四张核心表它们的字段和关系是这样表名关键字段说明userid, openid, nickname, avatar小程序用户openid 唯一activityid, title, start_time, end_time, status投票活动status 0 未开始、1 进行中、2 已结束optionid, activity_id, name, vote_count候选人/选项vote_count 为冗余计数vote_recordid, user_id, activity_id, option_id, create_time投票流水每人每活动一条记录冗余计数字段option.vote_count是为了列表页展示时不用写COUNT(*)聚合查询速度更快。但冗余就意味着有对不上的风险这也为第 5 章的核对方法埋了伏笔。vote_record的create_time很重要判断用户“当天是否已投”“活动是否在有效期内”都靠它做防刷票的频控也要用到。3. 把投票评选系统跑起来环境配置、导入步骤与关键参数3.1 开发环境选型JDK、MySQL、微信开发者工具的版本对应关系跑这套代码之前先把环境定下来。推荐组合在这套源码上踩坑最少JDK 8对应 Java 后端基础工程MySQL 5.7数据库脚本按这个版本写的函数和字符集兼容性最好微信开发者工具稳定版用于打开小程序前端。如果本机是 JDK 11 或 MySQL 8也能跑但配置上要多留意驱动版本和字符集。组件推荐版本说明JDK1.8前端后端联调环境最稳答辩演示不容易出幺蛾子MySQL5.7导入 SQL 后中文不乱码、排序正常微信开发者工具稳定版导入project.config.json即可MavenIDEA 内置首次加载依赖较慢需要联网小程序基础库2.14 以上用到了wx.request、wx.chooseMedia不需要最新版注意某些 Java 后端工程里用了LocalDate或更新语法JDK 8 也支持只有极老的工具链才会出问题。建议先在 IntelliJ IDEA 里用 Maven 面板点击刷新看依赖能否完整下载。这一步经常因为网络原因卡住解决办法是换阿里云镜像仓库具体做法是在settings.xml里加一段mirror配置指向maven.aliyun.com。不要跳过这个动作依赖不完整时后端启动直接报类找不到。3.2 导入数据库与修改配置文件连接串、端口与字符集数据库是整套系统的基础必须先恢复。打开 MySQL 后执行导入命令行方式如下mysql -u root -p vote_system.sql也可以用 Navicat 直接右键运行 SQL 文件效果一样。导入成功后确认数据库名假设叫vote_system。接下来打开后端工程的application.ymlspring: datasource: url: jdbc:mysql://localhost:3306/vote_system?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 server: port: 8080jdbc:mysql://localhost:3306/vote_system这一段里的vote_system必须和 SQL 导入后的库名一致否则启动报数据库不存在。characterEncodingutf8能避免中文投票标题写入后变乱码。serverTimezoneAsia/Shanghai是很多源码漏写的参数MySQL 5.7 配上后时间字段才和本机一致否则 Java 后端插入create_time会少 8 小时。修改完配置先做一件事在 IDEA 里启动后端主类看控制台日志。正常的日志末尾会出现类似Tomcat started on port(s): 8080的字样。这一步能跑到说明数据库连接、Mapper 映射、表结构都是对的。启动时最常见的报错是数据库拒绝连接优先查三项MySQL 服务是否启动、用户名密码是否正确、数据库名是否匹配。3.3 小程序端导入与真机调试BASE_URL 和开发工具设置后端跑通之后再用微信开发者工具导入小程序端。导入时选择 zip 解压后包含project.config.json的那个目录不要选到外层文件夹。导入成功后用小程序的测试号即可不需要注册 AppID能正常使用 wx.request 本地调试模式就打开了。小程序端需要改的配置只有一个变量# 位置miniprogram/utils/request.js # 本地开发者工具调试保持 localhost 即可 const BASE_URL http://localhost:8080/api; # 真机预览时改成开发机的局域网 IP例如 const BASE_URL http://192.168.1.101:8080/api;改 IP 的依据是在命令行执行ipconfig查看 IPv4 地址。这里有一处必踩的坑微信开发者工具默认对请求域名做合法校验本地调试时要打开“详情—本地设置—不校验合法域名”真机预览时如果后端跑在本地这条同样要勾。如果后端已经部署到服务器则必须在微信公众平台配置 request 合法域名且该域名必须备案。本地开发阶段最省事的办法是让手机和电脑连接同一 WiFi把BASE_URL改成电脑的局域网 IP后端保持监听0.0.0.0。4. 投票评选系统避坑手册接口 401、票数不准、真机连不上、图片上传失败4.1 接口一直 401token 过期与拦截器放行配置现象小程序端任何接口都返回 401页面数据全部加载不出来。重新登录也不行或者只能用几分钟又失效。原因这套源码在登录后由后端签发 JWT token小程序端把 token 缓存到wx.setStorageSync。401 通常是两个原因叠加一是后端 token 有效期设置太短有的毕设工程默认只有 600 秒二是后端拦截器没有放行/api/login这类登录接口导致登录请求本身也被拦截。解决先在后端定位拦截器配置把不校验 token 的路径明确放行。然后在 token 校验逻辑里把过期时间延长到 7 天Jwts.builder() .setSubject(String.valueOf(userId)) .setExpiration(new Date(System.currentTimeMillis() 7 * 24 * 3600 * 1000L)) .signWith(SignatureAlgorithm.HS256, secretKey) .compact();前端同步做一件事登录成功后把新 token 写入缓存请求封装里每次从缓存读取。不要在小程序端写死 token否则每次后端重启或密钥变化都会导致 401。4.2 票数不准并发下的 update 计数重复累加现象活动结束后前端展示的某个选项票数和vote_record流水表对不上多出几票。原因后端只做了optionMapper.incrCount(optionId)这种UPDATE option SET vote_count vote_count 1的操作。用户网络卡顿时重发请求后端在并发下先查后插的防重判断失效流水可能只插了一条但计数被加了两次。还有一种情况是用户退出页面又重新进入前端${voted}状态被重置按钮恢复可点导致二次请求。解决投票提交逻辑内部先插流水插入成功后再更新冗余计数并且依赖数据库唯一索引兜底。SQL 如下ALTER TABLE vote_record ADD UNIQUE KEY uk_user_activity (user_id, activity_id);加上这个唯一索引后即便请求重发第二次插入会因为唯一键冲突而失败incrCount不再执行。后端捕获到重复插入异常后直接返回“你已经投过票了”而不是报 500。这是投票系统防止重复计票最实在的兜底方案拿到的源码如果没加建议自己补上。4.3 真机请求不到本地服务不是域名的锅现象开发者工具模拟器里一切正常一转到真机预览所有请求直接失败页面空白。原因真机上跑的是手机环境它访问的localhost是手机自己不是开发电脑。微信开发者工具模拟器能通是因为模拟器与开发机共享网络栈真机就没有这个便利。另外后端如果只监听了localhost局域网内其他设备也访问不到。解决把BASE_URL里的localhost改成开发机局域网 IP获取 IP 的方法是命令行执行ipconfig找IPv4 地址那一行。同时确认后端以0.0.0.0地址启动Spring Boot 可以在启动命令里加java -jar vote-system.jar --server.address0.0.0.0手机和电脑连同一个 WiFi手机浏览器直接访问http://192.168.1.101:8080/api/activity/list能拿到 JSON 就说明网络通了再回小程序端调接口。这一步排错要逐步确认后端是否启动、电脑 IP 是否正确、手机是否能访问 8080 端口、防火墙是否放行。四个环节缺一不可。4.4 图片上传失败临时路径被清与 multipart 提交方式现象活动创建时上传候选图片偶尔成功、经常失败后端日志提示文件找不到或临时目录不可写。原因小程序端先用wx.chooseMedia选了图片拿到的是一个临时文件路径这个路径只在小程序本次会话内有效。如果把临时路径直接传给后端后端再去读这个路径文件可能已经被清理。有些毕设写法是把临时文件复制到后端的某个目录之后再读但复制逻辑写得不对就会出现偶发失败。解决正确做法是用wx.uploadFile直接把文件传给后端而不是把本地路径塞进 JSON。核心代码如下wx.uploadFile({ url: BASE_URL /upload/image, filePath: tempFilePath, name: file, header: { Authorization: Bearer wx.getStorageSync(token) }, success(res) { const data JSON.parse(res.data); if (data.code 0) { wx.showToast({ title: 上传成功, icon: success }); } } });注意name字段必须和后端MultipartFile的参数名一致PostMapping(/upload/image) public Result upload(RequestParam(file) MultipartFile file) { ... }如果后端存储图片的方式是 Base64另一种思路是不走wx.uploadFile而是用wx.getFileSystemManager().readFile把临时文件读成 Base64 字符串再包进 JSON 提交。这种方案后端不需要配 Multipart 配置适合表结构里直接存图片字段的情况。核心是先判断这个源码走的是哪种存储再选对应的提交方式不要混用。5. 把票数计算结果再查一遍验证规则与两点进阶技巧5.1 用 SQL 核对活动票数与排名榜投票系统交付前最值得做的一件事是用流水表反推真实票数验证前端展示的榜单是不是对的。拿到的源码里option.vote_count是冗余字段冗余字段意味着存在不一致风险。结题前执行下面这条 SQLSELECT r.option_id, COUNT(*) AS real_count FROM vote_record r INNER JOIN activity a ON r.activity_id a.id WHERE a.status 2 AND a.id 1 GROUP BY r.option_id ORDER BY real_count DESC;把结果和前端榜单的排序、数字逐项对比。如果两边一致说明冗余计数一直维护正确如果不一致说明曾经发生过并发重复请求或计数丢失这时需要手工修复冗余字段UPDATE option o SET o.vote_count ( SELECT COUNT(*) FROM vote_record r WHERE r.option_id o.id );这条 SQL 用真实流水重算每个候选人的票数修复之后重新看前端票数就和流水完全一致了。5.2 定时任务修正票数并建立防刷票的提交习惯投票系统跑久了冗余计数难免出偏差。进阶做法是加一个 Spring 定时任务每天凌晨把vote_count重刷一遍。核心逻辑在 Service 里加一个方法Scheduled(cron 0 0 3 * * ?) public void refreshVoteCount() { ListOption options optionMapper.selectAll(); for (Option option : options) { Integer realCount voteRecordMapper.countByOption(option.getId()); option.setVoteCount(realCount); optionMapper.updateById(option); } }定时修正的意义在于日常查询走冗余字段速度更快凌晨低峰期再花几秒钟把偏差找平性能和一致性都保住。防刷票则是一个习惯问题投票接口里至少要做两层校验登录态校验确保 userId 真实和投票幂等校验unique key 兜底。网络重放、重复点击这些非常规请求都会被唯一索引挡在最外层。从那以后我每次交付投票类毕设都会强制走一遍“count 流水、对比排名、刷新冗余”这个三连步骤确认票数闭环再交出去。希望帮到你。本文还有配套的精品资源点击获取