简介:这是一套面向计算机相关专业毕业生的SpringBoot投票系统完整项目,适合正在准备毕设、课程设计或期末大作业的学生,也可作为Java Web实战练习的参考案例。项目经过严格调试,可直接运行,省去从零搭建的繁琐过程。压缩包共401个文件,约63.03MB,涵盖73个java源文件、144个xml配置、32个vue前端组件、23个js脚本及23张png界面截图,另附sql数据库脚本、properties与yml配置文件、md说明文档等,前后端结构完整,便于理解投票业务从数据层到展示层的实现链路。资源围绕用户管理、活动创建、候选人维护与投票记录统计等核心模块展开,已有99人学习下载。对于需要快速完成毕设或积累项目经验的学习者,可借助源码与说明文档梳理系统架构、调试运行环境,并在此基础上进行功能扩展与二次开发。
1. 从一份能跑通的投票系统源码说起:SpringBoot 项目到底该怎么落地
很多同学做 Java 毕业设计时,第一反应是去搜「Java 毕业设计基于 SpringBoot 的投票系统源码+数据库」,下载一堆压缩包,解压之后发现要么跑不起来,要么数据库对不上,要么代码里全是看不懂的注解。我自己第一次带人做这类项目时也翻过车:拿到的源码里 SpringBoot 版本是 2.1,本机装的是 3.x,启动直接报 jakarta 包找不到,折腾了一整天才定位到是版本不兼容。所以这篇文章不打算泛泛讲「投票系统怎么做」,而是围绕一份典型的 SpringBoot 投票系统源码,把环境搭建、数据库设计、核心投票逻辑、防刷票、以及部署前必须检查的坑,一条一条拆开讲清楚。适合正在做课程设计、毕业设计,或者想拿一个完整 CRUD 项目练手 SpringBoot 的 Java 学习者。读完你至少能判断:这份源码值不值得改、怎么改成自己的、哪些地方最容易出问题。
2. 投票系统的数据库设计与 SpringBoot 工程结构:先定表再写代码
2.1 投票系统最少需要哪几张表
一份能用的投票系统源码,数据库层面通常不会太复杂,但必须把「用户—投票主题—选项—投票记录」这条链路理清楚。我一般会先画四张核心表,再根据是否需要匿名、是否需要多选、是否需要审核去扩展。下面这张表是我在多个课程设计项目里反复用过的结构,字段名可以直接抄,也可以按自己习惯改,但关系不能乱。
| 表名 | 作用 | 关键字段 | 说明 |
|---|---|---|---|
| user | 用户信息 | id, username, password, role | role 区分普通用户和管理员 |
| vote | 投票主题 | id, title, description, start_time, end_time, status | status 控制是否开放 |
| vote_option | 投票选项 | id, vote_id, option_text, count | count 做冗余统计,避免每次 count(*) |
| vote_record | 投票记录 | id, user_id, vote_id, option_id, create_time | 用来判断是否已投、防重复 |
这里有一个设计上的取舍: vote_option 里的 count 字段是冗余的,理论上可以通过 vote_record 聚合出来。但投票系统读多写少,每次展示结果都去 group by 统计,数据量一大就慢。我一般会在投票成功的事务里同时更新 vote_record 和 vote_option.count ,用一次事务保证一致性。如果对一致性要求极高,也可以不加这个字段,改成 Redis 计数器,但毕业设计层面没必要上那么重。
2.2 SpringBoot 工程分层与依赖版本选择
拿到源码后先别急着改业务,先看 pom.xml 。SpringBoot 投票系统最常见的依赖组合是: spring-boot-starter-web 、 spring-boot-starter-thymeleaf (或前后端分离用 spring-boot-starter-web + Vue)、 mybatis-plus-boot-starter 或 spring-boot-starter-data-jpa 、 mysql-connector-java 、 lombok 。下面是一个我常用的最小依赖片段,版本号根据你本机 JDK 调整,JDK 8 配 SpringBoot 2.7.x,JDK 17 配 SpringBoot 3.x。
<!-- pom.xml 关键依赖,JDK 8 环境 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.18</version>
<relativePath/>
</parent>
<dependencies>
<!-- Web 层,提供 REST 接口和页面渲染 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Thymeleaf 模板,如果前后端分离可去掉 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!-- MyBatis-Plus,简化 CRUD -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.1</version>
</dependency>
<!-- MySQL 驱动,注意 8.x 用 com.mysql.cj.jdbc.Driver -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
这段配置里最容易翻车的是 SpringBoot 版本和 JDK 的匹配。如果你本机是 JDK 17,却用了 2.7.x,启动时可能报 Unsupported class file major version ;反过来 JDK 8 用 3.x 会直接找不到 javax 包。另一个坑是 MySQL 驱动,8.0 以上必须用 com.mysql.cj.jdbc.Driver ,并且连接串要加 serverTimezone=Asia/Shanghai ,否则插入时间会差 8 小时。这些细节在源码里往往被作者本机环境掩盖了,换台机器就暴露。
2.3 配置文件与启动类的最小可运行改造
拿到源码后,第一步不是看业务代码,而是把 application.yml 改成自己本机的数据库。下面是一个可以直接套用的配置模板,重点看注释里标出的三个必改项。
server:
port: 8080
spring:
datasource:
# 必改项1:数据库名换成你本地创建的库
url: jdbc:mysql://localhost:3306/vote_system?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false
# 必改项2:用户名
username: root
# 必改项3:密码
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
mybatis-plus:
configuration:
# 控制台打印 SQL,调试投票逻辑时非常有用
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
map-underscore-to-camel-case: true
global-config:
db-config:
id-type: auto
改完配置后,先执行数据库脚本。大多数源码包里会带一个 .sql 文件,用 Navicat 或命令行导入即可。导入后检查两张表: user 里有没有管理员账号, vote 里有没有测试投票主题。如果 vote 表是空的,前端页面会一片空白,这不是代码问题,是数据没初始化。我一般会手动插一条测试数据,确认列表页能渲染出来,再往下调投票接口。
3. 投票核心逻辑:从投一票到防重复投票的完整实现
3.1 投票接口的 Controller 与 Service 分层写法
投票系统的核心就一个动作:用户对某个选项投一票。但这个动作背后要处理四件事——校验投票是否在有效期内、校验用户是否已投过、写入投票记录、更新选项计数。下面是一个典型的 Service 实现,用 @Transactional 保证四步要么全成功要么全回滚。
@Service
public class VoteService {
@Autowired
private VoteMapper voteMapper;
@Autowired
private VoteOptionMapper voteOptionMapper;
@Autowired
private VoteRecordMapper voteRecordMapper;
/**
* 执行投票
* @param userId 当前登录用户 id
* @param voteId 投票主题 id
* @param optionId 选项 id
*/
@Transactional(rollbackFor = Exception.class)
public void doVote(Long userId, Long voteId, Long optionId) {
// 1. 校验投票主题是否存在且在进行中
Vote vote = voteMapper.selectById(voteId);
if (vote == null || vote.getStatus() != 1) {
throw new RuntimeException("投票不存在或已结束");
}
LocalDateTime now = LocalDateTime.now();
if (now.isBefore(vote.getStartTime()) || now.isAfter(vote.getEndTime())) {
throw new RuntimeException("不在投票时间范围内");
}
// 2. 校验该用户是否已经投过这个主题
Long count = voteRecordMapper.selectCount(
new QueryWrapper<VoteRecord>()
.eq("user_id", userId)
.eq("vote_id", voteId)
);
if (count > 0) {
throw new RuntimeException("您已经投过票了");
}
// 3. 写入投票记录
VoteRecord record = new VoteRecord();
record.setUserId(userId);
record.setVoteId(voteId);
record.setOptionId(optionId);
record.setCreateTime(now);
voteRecordMapper.insert(record);
// 4. 更新选项计数,用 SQL 自增避免并发覆盖
voteOptionMapper.update(null,
new UpdateWrapper<VoteOption>()
.setSql("count = count + 1")
.eq("id", optionId)
);
}
}
这段代码里有两个关键点。第一, selectCount 判断重复投票在并发下并不可靠,两个请求同时查到 0 就会都插入。毕业设计层面可以在 vote_record 表上加 unique(user_id, vote_id) 唯一索引,让数据库兜底,插入失败就抛异常。第二,更新计数用 setSql("count = count + 1") 而不是先查再 set,避免并发写丢失。这两处是投票系统最容易被面试官追问的地方,也是实际部署时最容易出问题的点。
3.2 防刷票的三个层次:登录拦截、唯一索引、频率限制
很多源码只做了「登录后才能投票」,这远远不够。我一般会分三层来防:第一层是 SpringBoot 拦截器,未登录直接跳登录页;第二层是数据库唯一索引,防止同一用户重复插入;第三层是简单的频率限制,防止有人用脚本短时间大量请求。下面是一个基于拦截器的登录校验配置。
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private LoginInterceptor loginInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor)
// 拦截所有投票相关接口
.addPathPatterns("/vote/**", "/api/vote/**")
// 放行登录、注册、静态资源
.excludePathPatterns("/login", "/register", "/css/**", "/js/**");
}
}
拦截器里从 session 或 token 中取用户信息,取不到就返回 401 或重定向。这一层能挡住大部分普通用户,但挡不住直接调接口的脚本。所以第二层唯一索引必须加,SQL 如下:
-- 在 vote_record 表上添加唯一索引,防止同一用户对同一主题重复投票
ALTER TABLE vote_record ADD UNIQUE INDEX uk_user_vote (user_id, vote_id);
加了唯一索引后,Service 里那个 selectCount 其实可以去掉,直接 insert,捕获 DuplicateKeyException 返回友好提示。这样并发下也不会出现重复记录。第三层频率限制如果不想引入 Redis,可以用 Guava 的 RateLimiter 做单机限流,或者用 SpringBoot 的 @Async 加计数器,但毕业设计层面做到前两层已经能应付绝大多数场景。
3.3 投票结果展示与实时刷新
投票结果页通常要展示每个选项的票数和百分比。如果每次刷新都去 count(*) ,数据量大时会慢。我一般直接用 vote_option.count 字段,配合一条查询把选项和票数一起取出来。下面是一个 MyBatis-Plus 的查询示例。
public List<VoteResultVO> getResult(Long voteId) {
// 查出该主题下所有选项,按票数倒序
List<VoteOption> options = voteOptionMapper.selectList(
new QueryWrapper<VoteOption>()
.eq("vote_id", voteId)
.orderByDesc("count")
);
// 计算总票数,用于算百分比
int total = options.stream().mapToInt(VoteOption::getCount).sum();
return options.stream().map(opt -> {
VoteResultVO vo = new VoteResultVO();
vo.setOptionText(opt.getOptionText());
vo.setCount(opt.getCount());
// 防止除以零
vo.setPercent(total == 0 ? 0 : opt.getCount() * 100 / total);
return vo;
}).collect(Collectors.toList());
}
这段逻辑本身不复杂,但要注意 total == 0 的边界,否则会抛 ArithmeticException 。另外如果前端要实时刷新,可以用 setInterval 每 5 秒调一次接口,但别设太短,否则数据库压力大。毕业设计演示时 5 到 10 秒足够。
4. 源码改造与二次开发:怎么把别人的投票系统变成自己的
4.1 先跑通再改:导入源码后的检查清单
拿到一份投票系统源码,不要一上来就改包名、改界面。我一般按下面这个顺序检查,任何一步不通过就先解决,别往下走。
| 检查项 | 怎么看 | 常见问题 |
|---|---|---|
| JDK 版本 | java -version | 源码用 JDK 8,本机是 17 |
| Maven 依赖 | mvn clean install | 私服依赖下载失败 |
| 数据库脚本 | 导入 .sql 后看表和数据 | 缺少初始化数据 |
| 配置文件 | application.yml 的库名密码 | 没改成本机 |
| 启动类 | 是否有 @MapperScan | 漏扫导致 Mapper 注入失败 |
| 端口占用 | `netstat -ano | findstr 8080` |
这份清单里最容易被忽略的是 @MapperScan 。有些源码用 @Mapper 注解在每个接口上,有些用启动类统一扫描。如果启动报 Consider defining a bean of type 'xxxMapper' ,八成是扫描路径不对。改的时候在启动类上加 @MapperScan("com.xxx.mapper") 即可。
4.2 改包名、改主题、改界面的安全操作顺序
确认能跑通后,再开始改成自己的。顺序很重要:先改包名,再改数据库表前缀,最后改界面。改包名用 IDE 的 Refactor 功能,不要手动一个个改,否则容易漏掉 mapper.xml 里的 namespace 。改完包名后, application.yml 里的 mybatis-plus.mapper-locations 也要同步改。数据库表如果不想动,可以保留原表名,只改 @TableName 注解。界面改动用 Thymeleaf 的话,直接改 templates 下的 html,注意表单提交的 action 路径要和 Controller 的 @RequestMapping 对上。
4.3 增加一个「多选投票」功能的完整改动点
很多毕业设计只做单选,答辩时容易被问「能不能多选」。加多选其实不难,改动点有四个:第一, vote 表加一个 type 字段,0 单选 1 多选;第二,前端把 radio 改成 checkbox;第三,Controller 接收 optionIds 数组而不是单个 optionId ;第四,Service 里循环插入记录并更新计数。下面是对应的 Service 改动片段。
@Transactional(rollbackFor = Exception.class)
public void doMultiVote(Long userId, Long voteId, List<Long> optionIds) {
Vote vote = voteMapper.selectById(voteId);
if (vote == null || vote.getType() != 1) {
throw new RuntimeException("该投票不支持多选");
}
// 检查是否已投过
Long count = voteRecordMapper.selectCount(
new QueryWrapper<VoteRecord>().eq("user_id", userId).eq("vote_id", voteId)
);
if (count > 0) {
throw new RuntimeException("您已经投过票了");
}
// 循环写入并更新计数
for (Long optionId : optionIds) {
VoteRecord record = new VoteRecord();
record.setUserId(userId);
record.setVoteId(voteId);
record.setOptionId(optionId);
record.setCreateTime(LocalDateTime.now());
voteRecordMapper.insert(record);
voteOptionMapper.update(null,
new UpdateWrapper<VoteOption>()
.setSql("count = count + 1")
.eq("id", optionId)
);
}
}
注意多选时唯一索引 uk_user_vote 仍然有效,因为一个用户对一个主题只允许投一次,只是这一次可以选多个选项。如果业务允许改票,那就不能加这个唯一索引,改成先删后插,但毕业设计一般不允许改票,保持简单。
5. 避坑与排查:投票系统源码跑不起来时先看这几条
5.1 启动报数据库连接失败
现象:启动时抛 Communications link failure 或 Access denied for user 。原因通常是 application.yml 里的库名、用户名、密码没改,或者 MySQL 服务没启动。解决:先用命令行 mysql -uroot -p 能登进去,再检查连接串里的 serverTimezone 和 useSSL 参数。如果是 MySQL 8,驱动类必须是 com.mysql.cj.jdbc.Driver 。
5.2 页面 404 或模板找不到
现象:访问 localhost:8080 报 404,或者 Thymeleaf 报 Template not found 。原因一般是 Controller 的 @RequestMapping 和模板文件名对不上,或者模板放在了 src/main/resources/templates 之外。解决:检查 @Controller 返回的字符串是否和 templates 下的 html 文件名一致,注意大小写。如果用的是前后端分离,前端端口和后端端口不同,还要配跨域。
5.3 投票后票数不变
现象:点投票按钮提示成功,但结果页票数还是 0。原因通常是事务没提交,或者 vote_option 的 count 字段更新条件写错了。解决:看控制台打印的 SQL,确认 update vote_option set count = count + 1 where id = ? 有没有执行。如果执行了但没变,检查 optionId 传的是不是选项表的主键,而不是 vote_id 。
5.4 重复投票没被拦住
现象:同一个账号可以反复投。原因是没有加唯一索引,或者 Service 里的 selectCount 判断被绕过了。解决:先加 unique(user_id, vote_id) 索引,再把 Service 里的判断改成捕获 DuplicateKeyException 。如果用的是 session 判断,检查 session 是否过期或没存用户 id。
5.5 部署到服务器后时间差 8 小时
现象:本地投票时间正常,部署后 create_time 差 8 小时。原因是连接串没加 serverTimezone=Asia/Shanghai ,或者服务器时区是 UTC。解决:连接串加上时区参数,同时检查服务器 date 命令输出的时区,必要时用 timedatectl set-timezone Asia/Shanghai 调整。
6. 把投票系统改成能写进简历的项目:三个进阶技巧
第一个技巧是加一个「投票结果导出 Excel」功能。用 Java POI 或者 EasyExcel,把 vote_option 的统计结果写成表格,答辩时演示导出,比单纯展示页面更有说服力。EasyExcel 的写法比 POI 简单很多,定义一个 VoteResultExcel 类,用 @ExcelProperty 标注列名,然后 EasyExcel.write(response.getOutputStream(), VoteResultExcel.class).sheet("结果").doWrite(list) 一行搞定。注意导出接口要设置 response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") ,否则浏览器可能直接打开乱码。
第二个技巧是加一个简单的「投票趋势图」。不用上 ECharts 那么重,用 Thymeleaf 把数据渲染成 JSON 塞到页面里,前端用 Chart.js 画个柱状图就行。数据来源就是 vote_option 的 count 字段,按选项分组。这个功能在答辩时很加分,因为看起来像「数据分析」,实际上只是把已有的数字画出来。
第三个技巧是给投票接口加一个「操作日志」。用 SpringBoot 的 AOP,在 doVote 方法上加一个自定义注解 @LogRecord ,切面里把用户 id、投票 id、时间写进 vote_record 或者单独的日志表。这样答辩时被问「怎么追溯投票行为」,你可以直接说「有日志表,每次投票都有记录」。AOP 的切点表达式写 @annotation(com.xxx.annotation.LogRecord) ,切面里用 @AfterReturning 拿参数即可。
我自己带过几届毕业设计,最深的教训是:不要一开始就追求功能多,先把「登录—投票—看结果」这条主链路跑通,再往上加东西。很多同学源码下载了十几个 G,最后能跑起来的没几个,时间全花在配环境上。我一般会建议先拿一份最简单的 SpringBoot 投票系统,把数据库导入、配置改好、启动成功,然后每加一个功能就提交一次 Git,出问题能回滚。这个习惯比任何源码都值钱。希望帮到你。
本文围绕可运行的SpringBoot投票系统源码,系统讲解环境搭建、数据库设计(用户/主题/选项/记录四表结构)、分层开发(Controller-Service事务控制)、防刷票三重机制(登录拦截+唯一索引+频率限制)、多选功能扩展及典型部署问题排查(如时区偏差、404、重复投票)。聚焦Java Web工程实践,覆盖MyBatis-Plus、Spring Security拦截器、并发一致性保障等关键技术点。
189

被折叠的 条评论
为什么被折叠?



