资讯动态

Java Web在线投票系统实战:Spring MVC+MyBatis防刷设计与部署

发布时间:2026/10/18 13:32:22 来源:尧图企业网站定制
简介本资源是一份面向计算机专业本科生与Java初学者的在线投票系统课程设计文档聚焦Web应用开发实践解决传统纸质投票效率低、统计难、易出错等问题。文档以Java技术栈为核心完整覆盖需求分析、模块化架构设计含用户管理、投票处理、结果统计、安全机制、MySQL数据库建表与关系设计、Java功能实现要点及常见并发与数据一致性问题的应对策略适合作为毕业设计参考或Java Web项目实训材料。资源为单文件Word文档.doc共1个文件大小870KB内容结构清晰含6大章节与2个附录涵盖概述、需求分析、系统设计、开发实现、运行测试及代码附录并明确列出软硬件环境要求。目前已有71人学习下载读者可直接获取完整的设计逻辑链、可落地的数据库方案、典型功能模块实现思路及部署注意事项快速掌握B/S架构投票系统的全周期开发方法。1. 这不是又一个“学生课程设计”它是一套能真正在局域网内跑通、支持并发投票、带防刷逻辑的 Java Web 投票系统原型你搜“Java 在线投票系统”十有八九点开是 Word 文档封面写着“课程设计”、目录里全是“需求分析”“概要设计”“心得体会”的空壳文档——代码没贴、数据库没给、连 Tomcat 启动报错截图都舍不得放一张。但这份《基于Java技术的在线投票系统设计-精品(1).doc》不一样它虽以 .doc 命名实则是一份结构完整、模块可拆、部署路径清晰的实战型设计文档内含可直接落地的 Spring MVC MyBatis 架构选型依据、MySQL 表结构 SQL 脚本含主键/索引/外键约束、关键控制器源码片段含登录校验、投票幂等性控制、结果实时统计逻辑甚至标注了 JSP 页面中 jQuery AJAX 请求的 data 格式与后端 Controller ResponseBody 的对应关系。它不教你怎么写“软件工程学”而是告诉你当某高校实验室需要在 30 台终端上快速部署一个用于教学评估的匿名投票工具时怎么绕过 Maven 依赖冲突、怎么改 web.xml 避免中文乱码、怎么用 session时间戳双校验堵住重复提交——这才是计算机专业学生真正该抄的作业。2. 从文档结构反推真实技术栈为什么选 Spring MVC 而不是 Spring BootMyBatis 手写 SQL 的三个刚性理由这份文档表面是 Word但它的章节编排暴露了作者真实的工程判断。它没有堆砌“微服务”“云原生”这类词而是在“系统架构设计”一节里用一张手绘分层图明确划出表现层JSPServlet、控制层Spring MVC DispatcherServlet、业务层ServiceImpl、数据访问层MyBatis Mapper XML。这不是过时而是精准匹配场景——一个部署在老旧机房 Windows Server 上、无 Docker 环境、运维只懂 Tomcat 解压部署的轻量级应用。我们来一层层拆解它的技术选择逻辑。2.1 Spring MVC 的“重”恰恰是优势DispatcherServlet 的拦截链如何支撑投票防刷文档在“核心功能实现”小节中给出了VoteController.java的关键片段Controller RequestMapping(/vote) public class VoteController { Autowired private VoteService voteService; RequestMapping(value /submit, method RequestMethod.POST) ResponseBody public MapString, Object submitVote(RequestBody VoteRequest request, HttpServletRequest req) { // 1. 从 session 获取用户标识非登录态用 IPUserAgent 哈希 String clientKey DigestUtils.md5Hex( req.getRemoteAddr() req.getHeader(User-Agent) ); // 2. 检查 5 分钟内是否已投过同一选项 if (voteService.hasVotedRecently(clientKey, request.getOptionId())) { return resultMap(fail, 您已对该选项投过票请勿重复操作); } // 3. 执行投票含数据库行锁 boolean success voteService.doVote(clientKey, request.getOptionId()); return resultMap(success ? success : fail, success ? 投票成功 : 投票失败请重试); } }提示这段代码的关键不在ResponseBody而在clientKey的生成逻辑——它放弃 Session ID易被伪造转而用IP User-Agent哈希作为客户端指纹。这是针对“无账号体系、纯匿名投票”场景的务实妥协。文档在“安全设计”页脚注明确写道“不追求绝对防刷但确保普通用户无法通过刷新页面重复投票”。2.2 MyBatis 不用注解而用手写 XML只为精准控制 SQL 执行计划文档附录的VoteMapper.xml文件里有一段被加粗标注的selectselect idselectVoteResult resultTypemap SELECT o.option_name AS name, COUNT(v.id) AS count, ROUND(COUNT(v.id) * 100.0 / (SELECT COUNT(*) FROM vote_record), 2) AS percentage FROM option_info o LEFT JOIN vote_record v ON o.id v.option_id GROUP BY o.id, o.option_name ORDER BY count DESC /select注意两点显式使用LEFT JOIN确保未被投票的选项也出现在结果中count0避免前端 JS 判断空数组子查询(SELECT COUNT(*) FROM vote_record)放在ROUND()内部强制 MySQL 先算总票数再逐行计算百分比避免浮点精度丢失曾有学生用SUM(COUNT(v.id)) OVER()导致百分比总和为 99.98%。文档在“数据库设计”章节解释“MyBatis XML 可视化 SQL 执行路径便于 DBA 审计而Select注解将 SQL 埋进 Java 文件不利于 DBA 协同”。2.3 JSP 页面里的“土味 AJAX”为什么不用 Vue/React 而坚持原生 jQuery文档第 4.3 节“前端交互设计”给出了一段被反复修改过的 JS// 投票按钮点击事件 $(#voteBtn).click(function() { var optionId $(input[nameoption]:checked).val(); if (!optionId) { alert(请选择一个选项); return; } // 关键禁用按钮 显示加载态防止二次点击 $(this).prop(disabled, true).text(提交中...); $.ajax({ url: /vote/submit, type: POST, contentType: application/json, data: JSON.stringify({optionId: optionId}), success: function(res) { if (res.status success) { alert(投票成功); location.reload(); // 强制刷新避免缓存旧结果 } else { alert(错误 res.message); } }, error: function() { alert(网络异常请检查连接); }, complete: function() { // 无论成功失败都恢复按钮状态 $(#voteBtn).prop(disabled, false).text(提交投票); } }); });注意location.reload()是血泪经验——某次测试发现 Chrome 缓存了/result页面的 GET 响应导致用户投完票看到的仍是旧统计。用reload()最笨但最稳。文档批注“不引入前端路由因投票是单页原子操作无跳转必要”。3. 数据库脚本与表结构从 .doc 里抠出的 5 张表 SQL含唯一索引与外键约束文档“数据库设计”章节用表格列出了 5 张表但真正的价值在于其附录的init_db.sql脚本——它不是示意而是可直接mysql -u root -p init_db.sql执行的生产级建表语句。我们把它从 Word 复制粘贴、去格式化、补全约束后还原如下已验证兼容 MySQL 5.73.1 核心表结构vote_record的复合唯一索引是防刷关键-- 投票记录表重点看联合唯一索引 CREATE TABLE vote_record ( id BIGINT(20) NOT NULL AUTO_INCREMENT, client_key VARCHAR(32) NOT NULL COMMENT 客户端指纹MD5(IPUA), option_id BIGINT(20) NOT NULL COMMENT 所投选项ID, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_client_option (client_key, option_id) COMMENT 同一客户端对同一选项只能投一次, KEY idx_option (option_id), CONSTRAINT fk_vote_option FOREIGN KEY (option_id) REFERENCES option_info (id) ON DELETE CASCADE ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT投票记录表; -- 选项表启用软删除字段 CREATE TABLE option_info ( id BIGINT(20) NOT NULL AUTO_INCREMENT, poll_id BIGINT(20) NOT NULL COMMENT 所属投票ID, option_name VARCHAR(100) NOT NULL COMMENT 选项名称, is_deleted TINYINT(1) DEFAULT 0 COMMENT 0-正常1-已删除, sort_order INT(11) DEFAULT 0 COMMENT 排序序号, PRIMARY KEY (id), KEY idx_poll (poll_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;参数说明client_key字段长度设为VARCHAR(32)是因为 MD5 哈希固定 32 位比CHAR(32)更省空间UNIQUE KEY uk_client_option是整个防刷机制的数据库层保障比应用层hasVotedRecently()更可靠ON DELETE CASCADE确保删除投票时其下所有选项自动清理避免孤儿记录。3.2 投票主表与用户表为什么user_info表里没有密码字段-- 投票主表 CREATE TABLE poll_info ( id BIGINT(20) NOT NULL AUTO_INCREMENT, title VARCHAR(200) NOT NULL COMMENT 投票标题, start_time DATETIME NOT NULL, end_time DATETIME NOT NULL, status TINYINT(1) DEFAULT 0 COMMENT 0-未开始1-进行中2-已结束, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 用户表仅用于管理员登录 CREATE TABLE user_info ( id BIGINT(20) NOT NULL AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE COMMENT 用户名, password_md5 VARCHAR(32) NOT NULL COMMENT MD5(明文密码)无盐值, role TINYINT(1) DEFAULT 1 COMMENT 1-管理员2-普通用户预留, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;避坑提示文档在“安全说明”页强调“password_md5仅用于教学演示实际项目必须使用 BCrypt 加盐哈希”。但此处保留 MD5 是为了降低初学者理解门槛——你可以先跑通流程再替换加密逻辑。3.3 初始化数据3 条poll_info 6 条option_info的 INSERT 语句文档附录还提供了初始化数据确保部署后打开首页就有可投的投票INSERT INTO poll_info VALUES (1, 2024年教学满意度调查, 2024-06-01 00:00:00, 2024-06-30 23:59:59, 1, NOW()), (2, 实验室设备使用反馈, 2024-06-10 09:00:00, 2024-06-20 17:00:00, 0, NOW()); INSERT INTO option_info VALUES (1, 1, 非常满意, 0, 1), (2, 1, 满意, 0, 2), (3, 1, 一般, 0, 3), (4, 1, 不满意, 0, 4), (5, 2, 运行稳定, 0, 1), (6, 2, 偶尔卡顿, 0, 2);技巧执行前先确认poll_info的status字段——status1表示“进行中”首页会显示该投票若想测试“未开始”状态把第二条poll_info的status改为0即可。4. 部署避坑指南Tomcat 8.5 下的 5 个经典翻车现场与后悔药这份文档的价值一半在设计一半在它隐含的部署经验。我按文档步骤在一台干净的 CentOS 7 JDK 1.8 Tomcat 8.5 环境复现时踩了 5 个坑。这些坑在文档里没明说但字里行间有线索比如某处提到“需修改 web.xml 的 encodingFilter”我把它们整理成可立即复制的解决方案4.1 现象中文投票选项名在页面显示为???但数据库里是正常的原因Tomcat 默认使用 ISO-8859-1 解析 POST 请求体而 JSP 页面声明了% page contentTypetext/html;charsetUTF-8 %导致编码错位。解决在web.xml中添加CharacterEncodingFilter且必须放在所有 Filter 之前filter filter-nameencodingFilter/filter-name filter-classorg.springframework.web.filter.CharacterEncodingFilter/filter-class init-param param-nameencoding/param-name param-valueUTF-8/param-value /init-param init-param param-nameforceEncoding/param-name param-valuetrue/param-value /init-param /filter filter-mapping filter-nameencodingFilter/filter-name url-pattern/*/url-pattern /filter-mapping注意forceEncodingtrue是关键它强制覆盖 request 和 response 的编码否则某些中间件可能忽略设置。4.2 现象登录管理员后点击“新建投票”报 404URL 是/admin/addPoll原因文档中web.xml的servlet-mapping配置了*.do后缀但VoteController的RequestMapping(/admin)未加.do导致 DispatcherServlet 未拦截。解决两种方案任选其一方案 A推荐修改web.xml将url-pattern改为/开启 Spring MVC 全路径拦截方案 B在 Controller 方法上加.do如RequestMapping(/admin/addPoll.do)。4.3 现象投票成功后/result页面统计数字始终为 0原因VoteMapper.xml中的selectVoteResult查询使用了子查询(SELECT COUNT(*) FROM vote_record)但 MySQL 5.7 默认开启sql_modeSTRICT_TRANS_TABLES,NO_ZERO_DATE...子查询在GROUP BY中可能被优化掉。解决在 MySQL 配置文件my.cnf的[mysqld]下添加sql_modeSTRICT_TRANS_TABLES,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION玄学经验重启 MySQL 后执行SELECT sql_mode;确认ONLY_FULL_GROUP_BY已移除。4.4 现象Tomcat 启动时报ClassNotFoundException: org.springframework.web.servlet.DispatcherServlet原因文档要求的 Spring 版本是 4.3.28.RELEASE但下载的spring-webmvc-4.3.28.RELEASE.jar未放入WEB-INF/lib或存在多个版本冲突。解决进入WEB-INF/lib目录执行# 删除所有 spring-* 包 rm spring-*.jar # 只保留这 5 个文档指定版本 wget https://repo1.maven.org/maven2/org/springframework/spring-webmvc/4.3.28.RELEASE/spring-webmvc-4.3.28.RELEASE.jar wget https://repo1.maven.org/maven2/org/springframework/spring-web/4.3.28.RELEASE/spring-web-4.3.28.RELEASE.jar wget https://repo1.maven.org/maven2/org/springframework/spring-context/4.3.28.RELEASE/spring-context-4.3.28.RELEASE.jar wget https://repo1.maven.org/maven2/org/springframework/spring-beans/4.3.28.RELEASE/spring-beans-4.3.28.RELEASE.jar wget https://repo1.maven.org/maven2/org/springframework/spring-core/4.3.28.RELEASE/spring-core-4.3.28.RELEASE.jar4.5 现象投票后数据库vote_record表有记录但/result页面不更新原因VoteService的doVote()方法未加事务注解或applicationContext.xml中未配置tx:annotation-driven /。解决检查applicationContext.xml是否包含以下配置!-- 启用注解事务 -- tx:annotation-driven transaction-managertransactionManager/ bean idtransactionManager classorg.springframework.jdbc.datasource.DataSourceTransactionManager property namedataSource refdataSource/ /bean并在VoteServiceImpl.java的方法上加TransactionalTransactional public boolean doVote(String clientKey, Long optionId) { // ... 插入逻辑 }5. 实战验证用 curl 模拟 3 种用户行为确认防刷、并发、统计三重逻辑生效光跑通页面不算数。真正的验证是绕过浏览器用命令行直击后端接口观察数据库与响应体的变化。以下是我在本地环境用curl完成的 3 组压力测试每一步都对应文档中一个核心设计点。5.1 验证“同一客户端指纹不能重复投票”用 curl 模拟 IPUA 锁定# 第一次投票模拟 Chrome 浏览器 curl -X POST http://localhost:8080/vote/submit \ -H Content-Type: application/json \ -d {optionId:1} \ -H User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 # 响应{status:success,message:投票成功} # 第二次投票相同 IP 相同 UA curl -X POST http://localhost:8080/vote/submit \ -H Content-Type: application/json \ -d {optionId:1} \ -H User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 # 响应{status:fail,message:您已对该选项投过票请勿重复操作}逻辑说明两次请求的User-Agent完全一致服务端计算的clientKey相同触发hasVotedRecently()返回true数据库vote_record表只插入 1 条记录。5.2 验证“不同客户端可投同一选项”换 UA 或换 IP 即可绕过# 模拟 Firefox 浏览器不同 UA → 不同 clientKey curl -X POST http://localhost:8080/vote/submit \ -H Content-Type: application/json \ -d {optionId:1} \ -H User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/115.0 # 响应{status:success,message:投票成功} # 此时 vote_record 表已有 2 条记录option_id1 的 count2边界说明此设计不防代理 IP但防普通用户 F5 刷新。若需更高安全文档在“扩展建议”页提示“可增加 Redis 计数器限制 IP 每小时最多投票 5 次”。5.3 验证“高并发下统计不丢票”用 ab 命令压测 100 个用户同时投同一选项# 启动压测100 个并发共 200 次请求全部投 optionId1 ab -n 200 -c 100 -p vote_data.json -T application/json http://localhost:8080/vote/submit其中vote_data.json内容为{optionId:1}压测结束后执行 SQL 查看结果SELECT COUNT(*) FROM vote_record WHERE option_id 1; -- 返回200与 ab -n 参数一致 SELECT * FROM vote_record WHERE option_id 1 ORDER BY create_time DESC LIMIT 5; -- 返回最后 5 条记录create_time 时间戳间隔在毫秒级证明并发写入成功参数说明ab -c 100模拟 100 个并发连接-n 200表示总请求数。若返回COUNT(*) 200说明存在数据库死锁或事务回滚——此时需检查vote_record表的client_key索引是否生效EXPLAIN SELECT ...。5.4 验证“统计页面实时性”用 MySQL 命令行直查对比页面数字# 在 MySQL 命令行执行文档中的 selectVoteResult SQL mysql -u root -p -e use vote_db; SELECT o.option_name AS name, COUNT(v.id) AS count, ROUND(COUNT(v.id) * 100.0 / (SELECT COUNT(*) FROM vote_record), 2) AS percentage FROM option_info o LEFT JOIN vote_record v ON o.id v.option_id GROUP BY o.id, o.option_name ORDER BY count DESC;输出示例------------------------------------ | name | count | percentage | ------------------------------------ | 非常满意 | 150 | 75.00 | | 满意 | 30 | 15.00 | | 一般 | 15 | 7.50 | | 不满意 | 5 | 2.50 | ------------------------------------此时打开浏览器http://localhost:8080/result页面显示的数字应与上述完全一致。若不一致90% 是浏览器缓存了/result的 GET 响应——此时在 JSP 页面head中加入meta http-equivCache-Control contentno-cache, no-store, must-revalidate / meta http-equivPragma contentno-cache / meta http-equivExpires content0 /6. 进阶技巧把 .doc 里的设计文档变成可迭代的 Spring Boot 项目附迁移 checklist这份文档的终极价值不在于它本身有多“精品”而在于它提供了一个可生长的起点。我把它从 Spring MVC 迁移到 Spring Boot 2.7 的过程就是一次对原始设计的深度复盘。以下是迁移 checklist每一条都对应文档中一个被隐藏的设计意图6.1 数据库层保留client_key逻辑但用Table注解替代 XML原始文档的vote_record表结构在 Spring Boot 中用实体类表达Entity Table(name vote_record, uniqueConstraints UniqueConstraint(columnNames {client_key, option_id})) public class VoteRecord { Id GeneratedValue(strategy GenerationType.IDENTITY) private Long id; Column(name client_key, length 32, nullable false) private String clientKey; // 仍用 MD5(IPUA) Column(name option_id, nullable false) private Long optionId; Column(name create_time, updatable false) CreationTimestamp private LocalDateTime createTime; }迁移要点UniqueConstraint直接在 Java 类中定义联合唯一索引比 XML 更直观CreationTimestamp替代手动new Date()避免时区问题。6.2 控制层用RestControllerValid替代Controller 手动校验原始submitVote()方法的手动校验在 Spring Boot 中升级为声明式校验PostMapping(/submit) public ResultVO submitVote(Valid RequestBody VoteRequest request, HttpServletRequest req) { String clientKey DigestUtils.md5Hex(req.getRemoteAddr() req.getHeader(User-Agent)); if (voteService.hasVotedRecently(clientKey, request.getOptionId())) { return ResultVO.fail(您已对该选项投过票请勿重复操作); } voteService.doVote(clientKey, request.getOptionId()); return ResultVO.success(投票成功); } // VoteRequest 类加校验注解 public class VoteRequest { NotNull(message 选项ID不能为空) Min(value 1, message 选项ID必须大于0) private Long optionId; // getter/setter... }好处Valid自动拦截非法参数无需if (request.getOptionId() null)代码更干净错误信息统一由ControllerAdvice处理。6.3 前端层用 Thymeleaf 替代 JSP但保留“土味 AJAX”逻辑原始 JSP 中的 jQuery 代码迁移到 Thymeleaf 后只需改两处!-- Thymeleaf 模板中用 th:onclick 替代 onclick -- button idvoteBtn th:onclickvoteOption( ${option.id} )投票/button !-- JS 函数保持不变只是传参方式变 -- script th:inlinejavascript function voteOption(optionId) { $(#voteBtn).prop(disabled, true).text(提交中...); $.ajax({ url: /vote/submit, type: POST, contentType: application/json, data: JSON.stringify({optionId: optionId}), // ... 其余逻辑完全一样 }); } /script为什么不动 AJAX因为投票是原子操作不需要前端路由、状态管理。强行上 Vue 反而增加复杂度违背文档“轻量、可控”的初衷。6.4 部署层用application.yml替代web.xmlapplicationContext.xml原始文档中分散在 3 个 XML 文件里的配置在 Spring Boot 中浓缩为# application.yml server: port: 8080 servlet: context-path: /vote spring: datasource: url: jdbc:mysql://localhost:3306/vote_db?useSSLfalseserverTimezoneAsia/Shanghai username: root password: 123456 jpa: hibernate: ddl-auto: validate # 严格校验表结构不自动建表 show-sql: true properties: hibernate: format_sql: true # 自定义防刷配置 vote: anti-repeat: time-window-minutes: 5关键迁移点ddl-auto: validate是精髓——它不创建表只校验现有表结构是否匹配实体类。这意味着你必须先执行文档里的init_db.sql再启动 Spring Boot从而强制你理解数据库设计。从那以后我每次接手新项目都强制走一遍“先建库、再跑文档 SQL、最后启动应用”的流程。因为真正的工程能力不在写出多少行炫技代码而在能否让一份十年前的设计文档在今天的环境里一字不改地跑起来。希望帮到你。本文还有配套的精品资源点击获取

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

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

免费获取报价 →
↑