news 2026/10/27 23:03:46

SpringBoot+MySQL真实投票系统:RBAC权限与事务防刷实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SpringBoot+MySQL真实投票系统:RBAC权限与事务防刷实战

简介:这是一套面向计算机专业本科生的高分毕业设计实战资源,基于SpringBoot构建的完整投票系统,适用于毕设开发、课程设计及Java全栈项目练手。资源包含可直接部署运行的源码工程、配套MySQL数据库脚本及详细说明文档,覆盖用户管理、活动创建、候选人配置、实时投票与结果统计等核心业务模块,技术栈融合Vue前端、SpringBoot后端、Redis缓存与邮件通知功能,具备良好的工程规范性与调试稳定性。压缩包共401个文件,含73个Java业务类、32个Vue组件、23个JS交互逻辑、144个XML配置及SQL/Properties/YML等配置文件,整体63.03MB,结构清晰、模块解耦,便于学习者理解MVC分层与前后端联调要点。目前已有99人下载学习,提供真实可运行的高分毕设方案,附带关键服务实现(如EmailSender、VoteRecordServiceImpl、RedisUtils)与典型实体类(User、Activity、Candidate),助读者快速掌握企业级投票系统开发全流程。

1. 这不是又一个“Hello World”投票Demo:它跑在真实MySQL上、带完整权限分级、能扛住毕业答辩现场20人并发刷票——Java毕设里少有的「能真用」SpringBoot投票系统

你见过多少个标着“高分项目”的Java毕设源码?点开一看,前端是Thymeleaf硬编码的5个按钮,后端Controller里写着if (voteOption == 1) { count1++; },数据库就一张vote_record表,连时间戳都用new Date()手写。答辩老师一问“怎么防刷票?”、“管理员和普通用户权限怎么隔离?”、“投票结束怎么自动锁表?”,当场哑火。而这份SpringBoot投票系统,从第一天编译起就踩在生产级逻辑上:它用JWT做会话管理,RBAC模型控制ADMIN/VOTER/CANDIDATE三类角色,MySQL事务包裹整个投票流程(查剩余票数→扣减配额→写日志→更新统计),连application-prod.yml里都预置了连接池最大活跃数、SQL执行超时阈值、慢查询日志开关。它不是教你怎么写@RestController,而是教你怎么让一个投票动作,在分布式感知缺失、无Redis缓存、纯单体架构下,依然不丢票、不错计、不越权。适合正在赶毕设 deadline 的大四学生,也适合想补全 SpringBoot + MyBatis-Plus + MySQL 实战链路的转岗 Java 工程师——毕竟,能把「一人一票」「禁止重复投票」「实时票数刷新」这三件事闭环落地的毕设代码,真不多。


2. 从解压到启动:5分钟跑通核心链路,看清它为什么比“教学Demo”多出3层防御

2.1 目录结构即设计意图:src/main/java 下藏着 RBAC 权限骨架与事务边界

解压后先看项目根目录:pom.xml、src/、sql/、docs/四大块清晰。重点盯src/main/java/com/example/votesystem/下的包结构:

├── config/ # JWT配置、MyBatis-Plus分页插件、Swagger2文档开关 ├── controller/ # RestController层,/api/v1/admin/ 和 /api/v1/voter/ 路径严格分离 ├── entity/ # @TableId(type = IdType.ASSIGN_ID) 主键策略明确;VoteRecord实体含@TableName("vote_record")注解 ├── mapper/ # MyBatis-Plus Mapper接口,无XML文件——全部注解驱动 ├── service/ # IAdminService / IVoterService / ICandidateService 三层服务接口 ├── service/impl/ # 投票核心逻辑在VoterServiceImpl.vote()方法里:查配额→校验时间→扣减→写日志→更新统计,全程@Transactional(rollbackFor = Exception.class) └── util/ # TokenUtils生成JWT、DateUtils处理投票周期、IdWorker雪花ID生成器(非UUID)

提示:别急着改application.yml!先确认sql/vote_system_init.sql已导入,否则启动报Table 'vote_system.candidate' doesn't exist是预期行为,不是你配错了。

2.2 数据库初始化:4张表撑起完整业务,vote_record的联合唯一索引是防刷票的物理防线

sql/vote_system_init.sql不是简单建表语句,而是按生产环境标准写的 DDL:

-- 候选人表:含状态字段(ENABLED/DISABLED),支持动态上下架 CREATE TABLE `candidate` ( `id` bigint NOT NULL COMMENT '主键', `name` varchar(50) NOT NULL COMMENT '姓名', `position` varchar(100) NOT NULL COMMENT '竞选职位', `status` tinyint NOT NULL DEFAULT '1' COMMENT '状态:1-启用,0-禁用', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; -- 投票记录表:关键在联合唯一索引,强制(voter_id, candidate_id)不重复 CREATE TABLE `vote_record` ( `id` bigint NOT NULL COMMENT '主键', `voter_id` bigint NOT NULL COMMENT '投票人ID', `candidate_id` bigint NOT NULL COMMENT '候选人ID', `vote_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '投票时间', `ip_address` varchar(45) DEFAULT NULL COMMENT '客户端IP(用于审计)', PRIMARY KEY (`id`), UNIQUE KEY `uk_voter_candidate` (`voter_id`,`candidate_id`) COMMENT '一人一票物理保障' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

为什么这个UNIQUE KEY uk_voter_candidate比代码里if (hasVoted())更可靠?
因为它是数据库层面的原子约束。即使两个请求同时到达应用层(竞态条件),MySQL 的行锁会保证只有一个插入成功,另一个直接抛DuplicateKeyException,被@Transactional捕获后返回{"code":400,"msg":"您已投过此候选人"}。这是教科书里不会写、但线上系统必须有的“最后防线”。

2.3 启动前必改的3个配置项:端口、数据库、JWT密钥——改错一个就卡在登录页

打开src/main/resources/application.yml,以下三项必须手动核对:

server: port: 8081 # 默认8080可能被IDEA或Tomcat占用,建议改成8081避免冲突 spring: datasource: url: jdbc:mysql://localhost:3306/vote_system?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai username: root # 你的MySQL用户名 password: 123456 # 你的MySQL密码 driver-class-name: com.mysql.cj.jdbc.Driver jwt: secret: voteSystem2024SecretKey # 必须修改!默认密钥明文写死,上线前务必换成32位以上随机字符串 expiration: 86400 # JWT有效期:1天(秒),别调太长,也别设成0

参数说明:

  • server.port: 若提示Address already in use: bind,说明端口被占,改数字即可;
  • spring.datasource.url:serverTimezone=Asia/Shanghai是关键,缺它会导致java.sql.SQLException: The server time zone value 'UTC' is unrecognized;
  • jwt.secret: 这个字符串会参与JWT签名,一旦泄露,攻击者可伪造任意用户Token。我一般用openssl rand -base64 32生成。

启动命令(确保Maven已配置好):

mvn clean package -Dmaven.test.skip=true java -jar target/vote-system-1.0.0.jar

看到Started VoteSystemApplication in X.XXX seconds即成功。访问http://localhost:8081/swagger-ui.html可调用所有API——别急着点登录,先看下一节。


3. 真实场景验证:用Postman模拟管理员创建候选人、用户投票、后台查看统计——每一步都在触发事务与权限检查

3.1 管理员登录与候选人管理:POST/api/v1/admin/login返回的Token决定你能操作什么

用 Postman 发送 POST 请求到http://localhost:8081/api/v1/admin/login,Body 选raw → JSON:

{ "username": "admin", "password": "123456" }

响应体关键字段:

{ "code": 200, "msg": "登录成功", "data": { "token": "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhZG1pbiIsImF1dGgiOiJBRE1JTiIsImlhdCI6MTcxNzQyMDYwMCwiZXhwIjoxNzE3NTA3MDAwfQ.XXXX", // 复制这个token "user": { "id": 1, "username": "admin", "role": "ADMIN" } } }

注意:role: "ADMIN"是后续权限拦截的依据。把此 token 粘贴到 Postman 的Authorization → Bearer Token中,再调用/api/v1/admin/candidate/add才会成功。否则返回401 Unauthorized。

添加候选人示例(Headers 已带 Token):

POST /api/v1/admin/candidate/add { "name": "张三", "position": "班长", "status": 1 }

成功返回{"code":200,"msg":"添加成功","data":1},data:1是插入的 candidate.id。

3.2 普通用户投票全流程:从获取候选人列表到提交投票,事务如何保证数据一致性

普通用户登录(注意路径是/voter/login):

POST /api/v1/voter/login { "username": "voter001", "password": "123456" }

返回 token 后,设置 Headers → Bearer Token,再调用:

GET /api/v1/voter/candidates?status=1

返回当前启用的候选人列表(含id,name,position,voteCount)。此时voteCount是实时查vote_record表GROUP BY candidate_id统计出来的,不是缓存。

关键一步:提交投票

POST /api/v1/voter/vote { "candidateId": 1 // 对应张三的id }

后端VoterServiceImpl.vote()方法执行:

  1. 查询voter表确认用户存在且状态为ENABLED;
  2. 查询candidate表确认候选人status=1且未过期;
  3. 事务内执行:voteRecordMapper.insert(new VoteRecord().setVoterId(voterId).setCandidateId(candidateId));
  4. 若因uk_voter_candidate冲突抛异常,则@Transactional回滚,返回 400;
  5. 若成功,触发candidateMapper.updateVoteCountById(candidateId)更新票数。

验证效果:
立刻再 GET/api/v1/voter/candidates?status=1,张三的voteCount+1;
用 MySQL 客户端查SELECT COUNT(*) FROM vote_record WHERE candidate_id = 1;,结果一致。

3.3 后台统计与导出:/api/v1/admin/statistics返回JSON,/api/v1/admin/export生成Excel

管理员 Token 下调用:

GET /api/v1/admin/statistics?startDate=2024-01-01&endDate=2024-12-31

返回结构化统计:

{ "totalVotes": 142, "candidateStats": [ { "candidateName": "张三", "position": "班长", "voteCount": 78, "percentage": 54.93 }, { "candidateName": "李四", "position": "学习委员", "voteCount": 64, "percentage": 45.07 } ], "voterStats": { "totalVoters": 200, "votedVoters": 142, "abstentionRate": 29.0 } }

导出 Excel(需额外依赖 poi):

GET /api/v1/admin/export?format=xlsx

响应头Content-Disposition: attachment; filename="vote_statistics_20240605.xlsx",浏览器自动下载。源码里ExportService.exportToExcel()用XSSFWorkbook构建,字段名、样式、冻结首行都已预设——毕设答辩时,老师让你“导出一份看看”,你点一下就弹出 Excel,比口头解释强十倍。


4. 避坑指南:这6个血泪问题,90%的人在启动后30分钟内就会遇到

4.1 现象:启动报错java.lang.ClassNotFoundException: com.mysql.cj.jdbc.Driver

原因:pom.xml里 MySQL 驱动版本与本地 MySQL 服务版本不匹配。该源码用的是mysql-connector-java:8.0.28,若你装的是 MySQL 5.7,驱动 8.x 默认要求serverTimezone参数,但若漏写或写错,也会报此类错。
解决:检查application.yml中url是否含serverTimezone=Asia/Shanghai;若仍报错,降级驱动至5.1.47(需同步改driver-class-name: com.mysql.jdbc.Driver)。

4.2 现象:Swagger 页面打不开,提示Failed to load API definition

原因:Spring Boot 2.6+ 默认禁用 Springfox Swagger,而该源码用的是springfox-swagger2:2.9.2,与新版本 Spring Boot 存在兼容性问题。
解决:在pom.xml中添加以下依赖排除(非替换):

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <exclusions> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-tomcat</artifactId> </exclusion> </exclusions> </dependency>

并确保config/SwaggerConfig.java中@EnableSwagger2注解存在,且Docketbean 的select().apis(RequestHandlerSelectors.basePackage("com.example.votesystem.controller"))路径正确。

4.3 现象:登录返回{"code":401,"msg":"无效Token"},但Token明显是刚生成的

原因:JWT 密钥jwt.secret在application.yml和JwtTokenUtil.java中不一致。源码里JwtTokenUtil.SECRET是硬编码字符串,若你只改了 yml 里的jwt.secret,没同步改 Java 文件里的常量,就会验签失败。
解决:打开util/JwtTokenUtil.java,找到private static final String SECRET = "voteSystem2024SecretKey";,将其改为与application.yml中完全一致的字符串。这是最隐蔽的坑,调试半小时找不到原因。

4.4 现象:投票成功但vote_record表里没数据,candidate.vote_count也没变

原因:VoterServiceImpl.vote()方法上@Transactional注解失效。常见于:① 该方法被本类其他方法直接调用(绕过代理);②@Transactional写在 private 方法上(Spring AOP 不生效);③application.yml中spring.aop.proxy-target-class=true未开启 CGLIB 代理。
解决:确认vote()是 public 方法;检查VoteSystemApplication.java是否加了@EnableTransactionManagement(该源码已加);在application.yml中显式声明:

spring: aop: proxy-target-class: true

4.5 现象:候选人列表返回空,但数据库里明明有数据

原因:CandidateController.listCandidates()方法中@RequestParam Integer status接收参数时,若 URL 传?status=(空值)或?status=abc(非数字),MyBatis-Plus 的QueryWrapper.eq("status", status)会生成WHERE status = null,导致查不到。
解决:在 Controller 方法里加判空:

@GetMapping("/candidates") public Result listCandidates(@RequestParam(required = false) Integer status) { QueryWrapper<Candidate> wrapper = new QueryWrapper<>(); if (status != null) { wrapper.eq("status", status); } return Result.success(candidateService.list(wrapper)); }

源码中已实现此逻辑,但若你二次开发删了判空,就会翻车。


5. 进阶技巧:把「一人一票」升级成「IP+设备指纹」双因子防刷,30行代码搞定

毕设答辩时,老师常问:“如果有人用脚本刷票怎么办?” 光靠uk_voter_candidate只能防同一账号重复投,挡不住换号群刷。真正的加分项,是加一层轻量级设备识别——不用引入 Redis 或浏览器指纹库,就用现有技术栈增强。

5.1 原理:用HttpServletRequest.getRemoteAddr()+User-AgentMD5 生成设备指纹

VoterServiceImpl.vote()方法里,在事务开始前插入设备指纹校验:

// 新增:获取客户端IP和User-Agent String ip = request.getRemoteAddr(); String userAgent = request.getHeader("User-Agent"); String deviceFingerprint = DigestUtils.md5Hex(ip + "|" + userAgent).substring(0, 16); // 取前16位缩短 // 查询该设备今日是否已投过票(加索引提升性能) QueryWrapper<VoteRecord> fpWrapper = new QueryWrapper<>(); fpWrapper.eq("device_fingerprint", deviceFingerprint); fpWrapper.ge("vote_time", LocalDate.now().atStartOfDay()); long todayVoteCount = voteRecordMapper.selectCount(fpWrapper); if (todayVoteCount >= 3) { // 限制单设备每日最多3票 throw new BusinessException("设备今日投票已达上限"); }

配套 SQL(执行一次):

ALTER TABLE vote_record ADD COLUMN device_fingerprint VARCHAR(16) DEFAULT NULL COMMENT '设备指纹'; CREATE INDEX idx_device_time ON vote_record(device_fingerprint, vote_time);

5.2 效果对比:原始方案 vs 设备指纹增强后的防刷能力

场景原始方案设备指纹增强后
同一账号,不同IP刷票✅ 拦截(uk_voter_candidate)✅ 拦截
不同账号,同一IP(如机房)❌ 放行✅ 拦截(IP相同,UA相同 → 指纹相同)
同一账号,不同设备(手机/电脑)✅ 放行(合理)✅ 放行(指纹不同)
同一设备,不同账号(如家庭共用WiFi)❌ 放行(可能误伤)⚠️ 限制3票/日,平衡体验与安全

提示:device_fingerprint字段加了索引,查询速度毫秒级;MD5 截取16位足够区分设备,又节省存储空间。别用UUID.randomUUID(),那会失去设备绑定意义。

5.3 验证方法:用curl模拟不同设备,观察日志与数据库变化

开两个终端,分别模拟手机和PC:

# 模拟手机(UA含Mobile) curl -X POST http://localhost:8081/api/v1/voter/vote \ -H "Authorization: Bearer eyJhbGciOiJIUzI1NiJ9..." \ -H "User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1" \ -d '{"candidateId":1}' # 模拟PC(UA不含Mobile) curl -X POST http://localhost:8081/api/v1/voter/vote \ -H "Authorization: Bearer eyJhbGciOiJIUzI1NiJ9..." \ -H "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" \ -d '{"candidateId":1}'

查vote_record表:

SELECT voter_id, candidate_id, ip_address, device_fingerprint, vote_time FROM vote_record ORDER BY vote_time DESC LIMIT 5;

你会看到两行记录的device_fingerprint完全不同,证明设备识别生效。

从那以后我每次做投票类功能,不管毕设还是内部工具,都强制走一遍「IP+UA生成指纹→建索引→加日限制」三步。它不增加复杂度,却让系统从“能跑”变成“敢用”。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/27 23:03:04

AI表情重构技术:Photoshop智能肖像修图全攻略

1. 项目概述&#xff1a;AI表情重构技术如何拯救人像摄影最近在小红书等社交平台上&#xff0c;一个名为"AI表情重构流"的修图方法突然火了起来。作为一名有十年修图经验的摄影师&#xff0c;我发现这个方法确实解决了很多摄影师的痛点——那些因为表情僵硬而被判&qu…

作者头像 李华
网站建设 2026/10/27 23:02:59

XSKILL技术:AI经验积累框架解析与应用

1. 项目概述&#xff1a;XSKILL如何让AI实现经验积累 在AI技术快速发展的今天&#xff0c;我们经常遇到一个核心痛点&#xff1a;大多数AI系统都是"从零开始"学习每个新任务&#xff0c;无法有效积累和复用过往经验。XSKILL技术的出现&#xff0c;正是为了解决这一关…

作者头像 李华
网站建设 2026/10/27 23:01:59

opencode 深度解析:终端 Agent 生态位、模型接入与 Skill 实战

如果你用过 Claude Code 再回头看 opencode&#xff0c;会有一种“这工具怎么越做越像个活物”的感觉。上篇讲完了基础安装和日常用法&#xff0c;这篇我打算把大家问得最多的几个层面一次性聊透&#xff1a;opencode 在整套 AI 编码工具链里到底处于什么生态位、它的模型服务面…

作者头像 李华
网站建设 2026/10/27 23:01:32

职场高效沟通:闲聊的信息架构与价值转化

1. 闲聊的价值与边界茶水间、电梯里、午休时的那些看似随意的对话&#xff0c;往往藏着职场生存的智慧密码。上周三下午在17楼咖啡机前&#xff0c;我和市场部老张五分钟的闲聊&#xff0c;意外解开了困扰我两周的跨部门协作困局——这种神奇时刻在我十年职场生涯中反复验证&am…

作者头像 李华
网站建设 2026/10/27 23:01:06

Agent-Reach:多智能体协作触达层架构设计与实践

多智能体项目做久了&#xff0c;你会发现一个特别扎心的问题&#xff1a;Agent 本身的能力再强&#xff0c;如果彼此之间"找不到、够不着、传不通"&#xff0c;整个系统就是一团散沙。我今年一直在搞一套内部的多 Agent 协作平台&#xff0c;前期最大的痛点不是模型选…

作者头像 李华
网站建设 2026/10/27 23:00:41

基于若依框架的无人机智慧巡检后端源码设计与实现

简介&#xff1a;这份源码是面向无人机智慧巡检场景的后端系统完整实现&#xff0c;基于轻量级、易扩展的若依框架与Java语言构建&#xff0c;适合具备一定Java与Spring开发基础、希望研究行业级后端架构的开发者参考。系统围绕实时视频流检测报警、报表数据查看与大屏可视化展…

作者头像 李华