ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Java Web投票系统毕业设计:四表结构、防重复投票与部署避坑指南

Java Web投票系统毕业设计:四表结构、防重复投票与部署避坑指南 简介这是一份面向计算机专业毕业生与Java Web初学者的投票系统毕业设计源码基于Servlet、JSP、MVC模式并结合MySQL等关系型数据库实现完整投票流程可帮助理解用户认证、防重复投票、SQL注入防护等实际项目要点。压缩包共136个文件包含19个java源文件、19个class编译文件、10个jar依赖库、4个jsp页面、3个xml配置及54个gif演示截图等整体约5.7MB结构清晰便于对照学习。已有293人学习浏览。通过研读源码可掌握从投票主题管理、选项展示到投票统计与结果输出的完整开发思路同时了解JDBC数据库操作、Session会话跟踪及安全编码等最佳实践适合作为课程设计参考或毕业设计二次开发基础。1. 计算机毕业设计选投票系统Java Web源码到底在解决什么问题把“投票系统 Java Web项目源码”当作毕业设计题目看多数人第一反应是“这不就是个能在页面上点一下的问卷吗”。真动手就会明白投票系统的核心不是那个按钮而是“同一个人不能投两次”“票数不能算错”“投票过期之后必须锁死”这三件小事。Java Web的方向正好把这些点全部暴露在Servlet、JSP和SQL里一个能跑通的源码包等于给你搭好了从需求分析到答辩演示的完整骨架。这个方向适合正在选毕设题目的本科生也适合想用Java Web做课程设计的人。我会按数据库设计、请求链路、环境部署、避坑、答辩提亮的顺序把这个方向讲透。你拿到的虽然是一个.zip源码但真正值钱的是里面可复现的技术链。2. 把投票系统拆成四张表数据库设计决定毕设能讲多深2.1 功能边界先划清楚别一上来就画“系统管理”很多同学一打开源码看到User表里有role字段就开始往“后台权限管理”方向扩张加菜单、加角色、加操作日志最后论文一半篇幅都在写后台管理系统投票本身反而说不清。我见过的投票类毕设最靠谱的功能边界是普通用户注册登录、查看投票列表、参与投票、查看投票结果管理员创建投票、设置起止时间、维护选项。不需要单独建一张角色表User表里一个role字段就够了0是普通用户1是管理员。功能边界直接决定四张表能不能撑起来。如果你把边界划成“用户、投票、选项、投票记录”四块那么论文里的需求分析、ER图、流程图都能从这四张表展开。反而加进“投票分类表”会让页面多两级跳转对“防重复投票”和“结果统计”这两个核心诉求没有贡献。我一般会建议把“分类”做成vote表上的一个冗余字段而不是一张关联表毕设阶段够用。2.2 表结构落地用户、投票、选项、投票记录投票系统最稳的底部结构是四张核心表缺一不可。直接给出创建表的SQL在MySQL 8.0和5.7下都能跑CREATE DATABASE IF NOT EXISTS vote_db DEFAULT CHARACTER SET utf8mb4; USE vote_db; CREATE TABLE user ( id int(11) NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL, password varchar(64) NOT NULL, nickname varchar(50) DEFAULT NULL, role tinyint(4) NOT NULL DEFAULT 0, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE vote ( id int(11) NOT NULL AUTO_INCREMENT, title varchar(255) NOT NULL, description text, start_time datetime NOT NULL, end_time datetime NOT NULL, status tinyint(4) NOT NULL DEFAULT 1, create_by int(11) DEFAULT NULL, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE vote_option ( id int(11) NOT NULL AUTO_INCREMENT, vote_id int(11) NOT NULL, option_text varchar(255) NOT NULL, sort_order int(11) DEFAULT 0, PRIMARY KEY (id), KEY idx_vote_id (vote_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE vote_record ( id int(11) NOT NULL AUTO_INCREMENT, vote_id int(11) NOT NULL, option_id int(11) NOT NULL, user_id int(11) NOT NULL, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_vote_user (vote_id,user_id), KEY idx_option_id (option_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这里最容易被忽略的是vote_record表里的唯一约束uk_vote_user它是防重复投票的数据库级兜底。即使代码里忘了判断第二个人插入同一条(vote_id, user_id)时数据库也会直接抛异常这比任何前端按钮都可靠。user表用username做唯一键是为了避免两个用户注册同一个账号。vote表和vote_option表之间没有外键我刻意不加物理外键因为后期改选项、删投票时物理外键会带来一堆级联麻烦毕业设计里用逻辑外键也就是通过SQL的join去关联就够了论文里也能解释清楚“为什么不用外键约束”这一层考虑。2.3 关键字段的取舍逻辑为什么不用冗余票数很多人做投票系统第一反应是在vote_option表上加一个votes字段每次用户投票就UPDATE vote_option SET votes votes 1这样查询结果快。但这个字段会让并发问题暴露在答辩现场如果两个请求同时读到votes10各自加1后写回票数就变成11而不是12。虽然可以用行锁SELECT ... FOR UPDATE解决但毕设里没必要承担这个复杂度。我们直接用COUNT()聚合每次展示结果时实时统计把“算错票数”的锅甩给数据库引擎自己反而更安全。看一下这个聚合查询它会出现在“查看结果”页面SELECT o.id, o.option_text, COUNT(r.option_id) AS votes FROM vote_option o LEFT JOIN vote_record r ON o.id r.option_id WHERE o.vote_id ? GROUP BY o.id ORDER BY o.sort_order;LEFT JOIN是关键某个选项如果一票都没有COUNT(r.option_id)会返回0而不是空行。如果用INNER JOIN零票的选项直接消失页面会让投票人误以为该选项不存在。ORDER BY o.sort_order保证了选项顺序不是按票数排的而是按创建时定的顺序这样在答辩演示时不会因为票数变化导致页面跳动。如果你想在“数据统计”这章再讲深一点可以给vote_record增加两个扩展字段扩展字段类型用途注意点ipvarchar(45)记录投票来源IP答辩能讲“地域分析”IPv6地址最长45字符user_agentvarchar(255)记录浏览器信息判断设备和重复刷票注意中文编码问题这两个字段不是必需但我建议加上因为很多监听老师喜欢追问“如何发现恶意刷票”。你就可以回答同一IP在短时间内的记录数量异常就可以在SQL里加条件告警。四张表加两个扩展字段整个系统的数据沉淀能力已经超过多数毕设要求。3. 核心链路在Servlet里一次投票请求从进入Tomcat到写库3.1 分层约定JSP只负责展示Servlet只做路由拿到一个Java Web源码先别急着跑先看目录里是不是有src/main/java、src/main/webapp这种结构。传统的JSPServlet项目里JSP应该只负责用EL表达式和JSTL把数据渲染成HTMLServlet负责接收请求、调Service、决定跳转哪个页面。最常见的翻车写法是把Connection和Statement直接写在JSP的%代码块里答辩时老师问“这页面的数据从哪来”你只能指着屏幕说“这有个SQL”。所以我拿到源码的第一件事是确认WEB-INF/lib下有没有.jar依赖以及Servlet是不是用WebServlet注解写的。请求链路可以梳理成浏览器提交表单 - Tomcat启动Filter设置编码 - Servlet读取参数 - Service处理业务 - DAO操作数据库 - Servlet转发到JSP。先看一个最基础的编码Filter它必须在任何写业务逻辑之前注册import javax.servlet.*; import javax.servlet.annotation.WebFilter; import java.io.IOException; WebFilter(/*) public class EncodingFilter implements Filter { Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { request.setCharacterEncoding(UTF-8); response.setCharacterEncoding(UTF-8); chain.doFilter(request, response); } }这个Filter用WebFilter(/*)拦截所有请求setCharacterEncoding放在chain.doFilter之前才会对Controller里的request.getParameter()生效。如果你看到源码里每个Servlet都写一遍编码设置说明设计有重复正确做法是统一放到Filter里。注意如果源码是Servlet 3.0以前的版本没有注解支持就得在web.xml中配置Filter不要硬套注解。3.2 投票动作的代码实现先查过再插入才是“防重复”投票的入口通常是一个VoteServlet负责接收voteId和optionId两个参数。我给出一个典型的doPost实现WebServlet(/vote/submit) public class VoteServlet extends HttpServlet { private final VoteService voteService new VoteService(); Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException { User user (User) req.getSession().getAttribute(loginUser); if (user null) { resp.sendRedirect(login.jsp); return; } int voteId Integer.parseInt(req.getParameter(voteId)); int optionId Integer.parseInt(req.getParameter(optionId)); try { boolean ok voteService.vote(voteId, optionId, user.getId()); if (ok) { resp.sendRedirect(result.jsp?voteId voteId); } else { resp.sendRedirect(detail.jsp?voteId voteId errorduplicate); } } catch (IllegalArgumentException e) { resp.sendRedirect(detail.jsp?voteId voteId errorexpired); } catch (Exception e) { resp.sendRedirect(detail.jsp?voteId voteId errorsystem); } } }在Servlet里只做三件事判断Session获取参数根据Service返回值跳转。error参数用duplicate和expired区分不同失败原因页面再根据参数显示友好提示。这里不能直接把error参数写在URL里给用户看英文单词需要在JSP里映射成中文。我一般会在JSP顶部做一层${param.error duplicate ? 您已经投过票了 : }判断。真正的投票校验逻辑在VoteService.vote()方法里用最朴素的JDBC也能写清楚public boolean vote(int voteId, int optionId, int userId) { Connection conn null; PreparedStatement checkVote null; PreparedStatement insert null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 1. 检查投票是否存在且在有效期内 String checkVoteSql select status from vote where id? and now() between start_time and end_time; checkVote conn.prepareStatement(checkVoteSql); checkVote.setInt(1, voteId); ResultSet rs checkVote.executeQuery(); if (!rs.next()) { return false; } // 2. 检查是否已投过票利用唯一约束兜底 String existSql select id from vote_record where vote_id? and user_id?; PreparedStatement exist conn.prepareStatement(existSql); exist.setInt(1, voteId); exist.setInt(2, userId); if (exist.executeQuery().next()) { conn.rollback(); return false; } // 3. 插入投票记录 String insertSql insert into vote_record(vote_id, option_id, user_id) values(?,?,?); insert conn.prepareStatement(insertSql); insert.setInt(1, voteId); insert.setInt(2, optionId); insert.setInt(3, userId); insert.executeUpdate(); conn.commit(); return true; } catch (SQLException e) { try { if (conn ! null) conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } return false; } finally { DBUtil.close(checkVote, insert, conn); } }这段代码里conn.setAutoCommit(false)是必须的检查是否存在和插入记录必须在同一个事务里。如果并发情况下两个请求同时通过第一步检查唯一约束会让第二个插入抛SQLIntegrityConstraintViolationException事务回滚系统就防住了重复投票。你不需要在这里加SELECT ... FOR UPDATE因为唯一约束已经做了最终裁决加行锁反而增加死锁风险。参数方面now()直接用MySQL时间保证判断用的是数据库服务器时间而不是应用服务器时间避免两台机器时钟不一致导致投票时间错乱。3.3 结果页的聚合SQL用一条左连接把票数算出来投票结果页的数据来源就是前面那段LEFT JOIN聚合SQL。你还需要把它封装到VoteDao里返回一个ListVoteResult。我建议在循环外一次性把SQL准备好不要逐条计数否则页面会发起N次查询答辩时会显得性能意识不足。还有一个常见的坑老师问“投票过期后还能不能看结果”。我一般会在结果页的Servlet里再加一次时间校验SELECT title, description, CASE WHEN NOW() BETWEEN start_time AND end_time THEN 1 ELSE 0 END AS active FROM vote WHERE id ?;如果active0页面就只展示结果不展示投票按钮。这种“时间边界”逻辑放在SQL里比在Java代码里比较三个时间对象要简单也方便你在论文中写清楚数据库时间戳是唯一的时间标准应用层的System.currentTimeMillis()不作数。4. 把源码跑起来JDK、Tomcat、MySQL与导入项目顺序4.1 环境版本对照JDK 8还是11、Tomcat 8还是9很多源码跑不起来不是代码错是环境版本错。重点看Tomcat和JDK的搭配。我建议直接用JDK 8 Tomcat 9 MySQL 8这套组合理由很现实大部分毕业设计源码还停留在javax.servlet命名空间Tomcat 9完全兼容Tomcat 10以后换成了jakarta.servlet老项目直接编译不过。如果你运气好拿到的是源码很新、用了jakarta版本那就需要Tomcat 10以上。所以第一步是先打开源码看import javax.servlet还是import jakarta.servlet。组件推荐版本备选版本注意点JDK1.811老项目不要追求17Tomcat9.0.x8.5.x避开Tomcat 10的命名空间问题MySQL8.05.7驱动类名不同Maven3.6.3不装也行判断源码是否Maven工程如果源码根目录有pom.xml就是Maven工程依赖由Maven统一管理如果没有就找WEB-INF/lib下的jar。这两种工程的导入方式不一样别一上来就往IDEA里拖。4.2 解压zip后怎么让IDEA认成Web项目拿到计算机毕业设计-投票系统-Java Web项目源码.zip先解压到纯英文路径防止中文路径导致Tomcat读取配置失败。打开IDEA后按下面顺序操作点击File-New-Project from Existing Sources选择解压后的根目录。如果根目录有pom.xml选择Maven否则选择Eclipse或Web类型让IDEA自动识别。在Project Structure里把Project SDK设为1.8Language level设为8。打开Modules展开项目名如果没有Web这一行就点加号添加Web在Web Resource Directory里指向src/main/webapp或老式项目的WebContent目录。打开Artifacts点加号添加Web Application: ExplodedIDEA会生成一个可以部署到Tomcat的中间目录。打开Run Configurations添加Tomcat Server在Deployment页签里把刚才的Artifact加进去设置好Application context为/vote或空。这里最容易让人慌的是第4步IDEA识别不出Web目录导致代码里JSP路径全部爆红。加Web Facet的本质是告诉IDEA“这个目录下放着web.xml和静态资源”并不是把文件夹改名。4.3 数据库连接串、端口、字符集的三个参数组数据库初始化这一步先建库再导入SQL文件。MySQL命令行里执行mysql -u root -p CREATE DATABASE IF NOT EXISTS vote_db DEFAULT CHARACTER SET utf8mb4; exit; mysql -u root -p vote_db D:/projects/vote/sql/vote_db.sql如果源码包没有提供SQL文件就用我第2章那段建表SQL手动执行到库里。接下来修改数据库连接配置通常在src/db.properties或DBUtil.java里jdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/vote_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse jdbc.usernameroot jdbc.password123456注意MySQL 8.0的驱动类名是com.mysql.cj.jdbc.Driver老的是com.mysql.jdbc.Driver。serverTimezoneAsia/Shanghai在MySQL 8.x版本里必须加否则连接会报时区错误。useSSLfalse是为了避免本地开发时证书校验失败。如果你把项目部署到服务器上再换成服务器时区。Tomcat端口冲突也很常见。打开conf/server.xml找到Connector port8080 ...改成8081或9090。改完端口后访问地址的URL也要跟着变不要对着8080去访问跳转不到的页面。5. 避坑毕业设计跑不起来的五个典型原因5.1 现象Tomcat启动报ClassNotFoundException: com.mysql.jdbc.Driver原因非常直接MySQL驱动jar不在运行时的classpath里。非Maven项目需要在WEB-INF/lib下放一份mysql-connector-java-x.x.x.jarMaven项目则需要在pom.xml里声明依赖。另一个隐蔽原因你用的MySQL 8.0数据库但驱动还是5.1版本驱动里的类名还是com.mysql.jdbc.Driver两者能对上但连接协议不兼容运行时会出现更奇怪的认证错误。解决方式先确认MySQL版本。mysql --version看到8.x就去下载mysql-connector-java-8.0.x.jar并把代码里的Class.forName(com.mysql.jdbc.Driver)改成com.mysql.cj.jdbc.Driver。如果不想改代码可以在数据库里把root账户的认证插件改回mysql_native_password但这是头痛医头我一般选择改代码因为驱动版本新总没有坏处。5.2 现象页面中文全部乱码乱码这个坑百分之八十是“多处编码设置不一致”造成的。JSP页面顶部缺了pageEncoding声明就会出现整个页面???Servlet响应里没设ContentType就会出现向页面写到一半的中文字符变乱码数据库连接串里没有characterEncodingutf8就会出现存进去是中文、查出来是问号。解决方式是统一成UTF-8缺一不可。第一JSP头部加% page contentTypetext/html;charsetUTF-8 languagejava %第二Filter里同时设置request.setCharacterEncoding(UTF-8)和response.setCharacterEncoding(UTF-8)第三数据库URL带上useUnicodetruecharacterEncodingutf8第四建表语句显式用DEFAULT CHARSETutf8mb4。我遇到乱码问题时会先用浏览器开发者工具看响应头里的Content-Type确认是响应编码没写再排查数据库。这样能把问题定位时间缩短一半。5.3 现象连不上MySQL报Communications link failure这个报错的常见场景是JDBC URL没问题驱动也加载了但MySQL服务没启动或者URL里的端口、时区参数不对。Tomcat项目连不上数据库时先别查代码先用命令行确认mysql -u root -p -e select 1能通吗然后看show variables like port确认实际端口是不是3306。如果端口对再看MySQL版本如果是8.0必须在URL里加serverTimezoneAsia/Shanghai。另一个原因是防火墙。本地开发时一般没事但如果连接的是远程数据库Ubuntu服务器默认可能不开3306端口。解决方式是临时uFW allow 3306同时确认MySQL的bind-address不是只绑定了127.0.0.1。毕业设计的源码多半是本地连库所以我建议你直接把数据库和Tomcat装在同一台机器上什么问题都少一半。5.4 现象刷新一下票数暴涨重复投票成功了这是最严重的一个坑也最能成为答辩亮点。原因通常有三个前端用了disabled按钮但没有后端校验后端校验和插入不在同一个事务里vote_record表没有唯一约束。现象是用户不停地点“提交”数据库里出现了多条同人同投票主题的记录。解决方式分三层。第一层数据库建表时就加UNIQUE KEY uk_vote_user(vote_id, user_id)保证物理上不可能重复。第二层在Service层用事务包裹“查询存在”和“插入记录”如果插入时抛出重复键异常就回滚并返回“已投票”提示。第三层在Servlet层用Session或Redis缓存“用户对某投票”的标记减少数据库压力。前两层是必做的第三层属于优化答辩时讲出来会让老师觉得你想过并发问题。你可以在答辩演示时打开数据库当场执行一条SELECT COUNT(*) FROM vote_record WHERE vote_id? AND user_id?证明记录只有一条这比任何口头解释都有说服力。5.5 现象源码导入IDEA后运行按钮是灰的这纯粹是IDE配置问题不是代码问题。源码导入后IDEA不知道它是一个Web项目没有生成Web Facet和Artifact所以Tomcat配置区的Run按钮不可点击。解决方式是回到第4.2节的六个步骤缺哪一步补哪一步。最常被漏掉的是Artifacts只加Facet不加ArtifactIDEA认为你根本没有可部署的内容运行按钮照样是灰的。添加Web Application: Exploded后还要在Run Configuration里把Artifact挂到Deployment。注意老一代版本的IDEA里按钮叫“Deployment”新版本叫“Deploy”位置不一样但配置逻辑相同。如果配置完仍然是灰的就重启IDEA让索引和插件重新加载。6. 把项目改到能答辩三个验证技巧和一个诚实的建议6.1 用Session重复提交把防重逻辑变成答辩演示素材答辩最怕老师问“怎么证明你的系统能防重复投票”。我给你一个零成本的演示方案先登录一个用户打开开发者工具Application面板找到当前Session的JSESSIONID投一次票。然后在同一个Session下再次提交页面应该显示“您已经投过票了”。这时不要停下来直接在Navicat里执行SELECT COUNT(*) FROM vote_record WHERE user_id1 AND vote_id1给老师看这条记录只有一行。这一套动作组合在一起既验证了前端拦截又验证了数据库约束比嘴上讲“用唯一约束”直观得多。6.2 做一个按分钟刷新的投票结果JSON接口如果想让页面有点技术含量加一个ResultJsonServlet把聚合结果直接输出成JSON前端用定时器刷一刷新数据。这样答辩时你就能现场演示“我投一票另一个浏览器打开的页面自动票数1”。这个Servlet的核心只有几行代码resp.setContentType(application/json;charsetutf-8); MapString, Object data new HashMap(); data.put(voteId, voteId); data.put(options, resultList); resp.getWriter().write(new Gson().toJson(data));前端用fetch(resultJson?voteId1)每30秒拉一次替换页面里的数字。这个功能不会影响原有JSP但对答辩的视觉冲击力很大因为老师看到的是一个“实时系统”而不是静态页面。6.3 迁移到Spring Boot要保持不变的三个核心如果导师要求“用Spring Boot重新实现”你不需要推翻重来。Servlet变成RestControllerJSP变成Thymeleaf或Vue静态页但三个核心必须原样保留第一vote_record表的唯一约束不能丢第二投票校验的“先查后插”事务逻辑不能丢第三用COUNT()聚合而不是冗余票数字段。把这三点讲清楚你就从“会写JSP”升级成“理解业务约束在Web框架里的落地方式”。我自己的教训是当年第一步就把votes字段冗余进vote_option答辩时老师问“两个请求同时到达怎么办”我说加锁结果被追问锁粒度场面很不好看。后来改成唯一约束事务聚合代码短了一大截问题也解释得清楚。这个方向本身不难难的是你愿意把每个字段为什么存在、每个事务为什么需要都当成答辩的弹药。希望帮到你。本文还有配套的精品资源点击获取
返回列表