☰
投票系统Java Web源码拆解:数据库设计、MVC分层与部署避坑
2026/10/8 1:14:38 网站建设 项目流程

简介:一份面向计算机专业毕业设计的Java Web投票系统源码,适合需要完成课程设计、准备毕业答辩或系统学习Java Web开发的读者。项目基于Servlet、JSP与MVC设计模式,覆盖投票主题管理、选项展示、用户投票、结果统计和按频道浏览等核心功能;同时演示了JDBC连接MySQL等关系型数据库、使用Session防止重复投票、采用参数化查询防范SQL注入等关键开发手段。压缩包共136个文件,包含19个Java源文件、19个class编译产物、4个JSP页面和4个HTML页面,另配54个GIF动态截图与17个JPG界面图,便于对照前后台效果;10个jar包提供第三方依赖,3个XML文件承载部署配置,整体仅5.7MB,结构清晰、部署轻量。已有293人学习下载。通过分析VoteDAOImpl、DoVoteAction、VoteResultAction等核心类,可深入理解数据访问层、控制层与表现层的协作方式,是一份兼顾毕业设计参考与Java Web综合实训价值的完整源码包,覆盖了从数据库表设计到Servlet控制器的标准项目结构。

1. 投票系统 Java Web 源码:毕业设计选它,到底在选什么

很多计算机专业的毕业设计清单里,投票系统 Java Web 项目源码是出现频率最高的那一类。它看着不起眼,却刚好卡在课程设计到企业项目的分界线上:有 JSP/Servlet 的传统 Web 请求链路,有 MySQL 的表关系设计,有 Session 和 Cookie 的会话管理,还有前端页面的数据渲染。这些恰恰是 Java Web 方向面试最常被问到的内容,也是从一个会写语法的大学生,过渡到能独立交付一个完整功能的开发者,最短的练手路径。

这篇笔记不讲虚的,直接把投票系统拆开:数据库表怎么设计才能不加表就支持多种投票类型,JSP 里如何用 MVC 思想把请求处理逻辑和页面显示拆开,多选投票的选项结果在 MySQL 中用什么结构存储最省事,以及那些让很多人翻车的编码、Session 失效和 SQL 注入边界问题。适合正在做课程设计的学生,也适合想快速回忆 Java Web 全链路的老手。

2. 从零拆投票系统:先搞清楚它由哪几个模块组成,再动手写代码

2.1 投票系统的功能边界:不是所有投票都一样

常见的投票业务可以分成三类:单选投票、多选投票和匿名投票。单选投票每个用户对某个主题只投一票,多选投票允许在多个候选项中勾选若干个,匿名投票则要求不记录投票人的身份,只记录结果。这三个业务差异直接影响数据库表的设计和 Service 层的接口划分。

先看一个典型的投票系统用例图。参与者角色有一个:普通用户;管理员角色有一个:发起投票的人。用户能做的操作是查看投票列表、进入投票详情页、提交投票、查看实时结果;管理员除以上功能外,还能新增投票主题、维护候选项、开启或关闭投票,并查看统计汇总。这个边界定义对了,后续的代码结构才不会失控。

很多初次做的人一上来就写 DAO 层,把数据库访问代码直接堆进 JSP,页面里全是<%脚本片段。这个做法在几百行的玩具项目里能跑,但一旦加上管理员端和后端统计,就会变成黑匣子——改一个字段要翻遍全部 JSP。结构上应该至少分成三层:Controller 层接收请求,Service 层处理业务规则,DAO 层访问数据库。JSP 只负责渲染,不写业务逻辑。这里的治理原则很简单:投票业务规则集中在 Service 里,数据访问集中在 DAO 里,页面上只出现 JavaBean 和 JSTL 标签。

这个系统的核心难点集中在三处。第一,多选投票的结果如何和候选项表做关联统计;第二,如何防止同一用户重复投票,除了前端按钮置灰,后端必须有真正的幂等校验;第三,投票数据的实时统计,在 MySQL 端用什么 SQL 能一次查询出所有选项的票数,而不是在 Java 内存里循环计算。

下面把这些问题拆成模块,逐一落地。

2.2 数据库设计:三张表解决所有投票类型

一个能支持单选和多选的投票系统,最少只需要三张表:投票主题表 vote_topic、候选项表 vote_option、投票记录表 vote_record。主题表保存投票的标题、类型、开始时间和结束时间、是否启用;候选项表保存每个主题下的选项名称和排序;记录表保存每一次投票行为。

先看主题表的建表语句:

CREATE TABLE vote_topic ( id INT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(100) NOT NULL, type TINYINT NOT NULL DEFAULT 0 COMMENT '0-单选 1-多选', max_choices INT NOT NULL DEFAULT 1 COMMENT '多选时最大可选数', start_time DATETIME NOT NULL, end_time DATETIME NOT NULL, status TINYINT NOT NULL DEFAULT 1 COMMENT '1-启用 0-关闭', create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

type 字段区分单选和多选;max_choices 字段是给多选用的,提交时后端要校验用户勾选的数量不能超过这个值。单选时 max_choices 固定为 1。把类型放到主题表而非单独建表,核心考虑是一个投票主题对应一组候选项,类型的差异只体现在提交校验和统计口径上,不需要为每个类型建一张独立表。这是新人在设计中最容易犯的错:把单选和多选当成两种数据对象,分表存储,结果自己把统计逻辑写复杂了。

再看候选项表和投票记录表:

CREATE TABLE vote_option ( id INT PRIMARY KEY AUTO_INCREMENT, topic_id INT NOT NULL, option_name VARCHAR(200) NOT NULL, option_order INT NOT NULL DEFAULT 0, FOREIGN KEY (topic_id) REFERENCES vote_topic(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE vote_record ( id INT PRIMARY KEY AUTO_INCREMENT, topic_id INT NOT NULL, user_key VARCHAR(64) NOT NULL COMMENT '用户标识:登录用户为user_id,匿名投票为客户端指纹', option_id INT NOT NULL, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, INDEX idx_topic_user (topic_id, user_key), INDEX idx_option (option_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

vote_record 表中的 user_key 字段是这篇笔记最想强调的设计点。很多初版设计里,投票记录表直接放一个 user_id 外键指向用户表,但这会有一个问题:匿名投票场景下没有登录用户,user_id 为 NULL,重复投票校验完全失效。用 user_key 统一承载用户标识,登录用户填 user_id 的字符串形式,匿名用户填客户端指纹(IP 加 User-Agent 做哈希),这样去重逻辑就统一落在一条字段上,不必根据投票类型写两套查重 SQL。

2.3 避免重复投票的两种校验手段,缺一不可

重复投票校验是投票系统的业务核心。前端把投票按钮在提交后置灰,只是用户体验优化,不是安全措施——因为任何人都可以绕过页面直接构造 HTTP 请求。真正的校验必须在后端做两层。

第一层是业务层去重。在 VoteService.submitVote 方法内,先查 vote_record 表里是否存在 topic_id 和 user_key 都匹配且 create_time 在投票时间范围内的记录。这里的时间范围不能省,原因是同一个用户被删除投票记录后,理论上可以再次投票,去重维度要限定在“当前这场投票活动内”。第二层是数据库层兜底。业务层的两步操作存在并发窗口:两个请求同时查询,都发现没有记录,然后同时插入,于是出现重复投票。解决方式是对 vote_record 表加唯一索引,索引列就是 (topic_id, user_key)。这样即使业务层逻辑被并发绕过,数据库也会拒绝第二条插入语句。

ALTER TABLE vote_record ADD UNIQUE KEY uk_topic_user (topic_id, user_key(64));

注意 user_key 字段长度是 64,唯一索引允许对 VARCHAR 指定前缀长度,这里直接用整列建索引即可。加了唯一索引后,业务层查重的代码可以简化:不先查询,直接执行 INSERT,捕获 DuplicateKeyException 异常,捕获到就说明已经投过票,返回“请勿重复提交”。这个做法比先查再插少了一次数据库往返,同时彻底规避并发问题,是投票系统里最值得抄的代码。

2.4 统计结果的 SQL 写法:一条 GROUP BY 搞定,不要在 Java 里循环

投票结果页需要展示每个选项的得票数和占比。如果只统计一次汇总,用 GROUP BY 是最优解,没有必要先把所有投票记录查出来再在 Java 中计数——那既浪费内存又浪费传输带宽。

SELECT vo.id AS option_id, vo.option_name, COUNT(vr.id) AS vote_count, ROUND(COUNT(vr.id) / total.total_count * 100, 2) AS percent FROM vote_option vo LEFT JOIN vote_record vr ON vo.id = vr.option_id CROSS JOIN ( SELECT COUNT(*) AS total_count FROM vote_record WHERE topic_id = ? ) AS total WHERE vo.topic_id = ? GROUP BY vo.id, vo.option_name, total.total_count ORDER BY vo.option_order;

这里用了 CROSS JOIN 把总票数一次性带进子查询,避免在 Java 里先查总数再逐项算百分比。LEFT JOIN 保证没有得票的选项也能显示 0 票,而不是直接在结果里消失。订单排序按 option_order 而非票数,是为了保证页面上的选项顺序和创建时一致——按票数排序会让人误以为选项在投票过程中被移动过。

3. 用 MVC 骨架搭 Java Web 工程:Servlet 层这样拆,代码才不会烂

3.1 Servlet 路由的设计:一个投票模块一个 Controller

很多课程设计的通病是每个功能建一个 Servlet:AddVoteServlet、DeleteVoteServlet、StatisticVoteServlet……页面一多,web.xml 里全是 Servlet 映射,改一个路径要翻半天的配置文件。这种做法的根源是照搬了 Action 模式却不知道合并路由。Java Web 项目里,一个投票域的全部操作完全可以收敛到一个 VoteServlet,用 action 参数分发到不同方法。

@WebServlet("/vote") public class VoteServlet extends HttpServlet { private VoteService voteService = new VoteService(); @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String action = req.getParameter("action"); if ("list".equals(action)) { listTopics(req, resp); } else if ("detail".equals(action)) { showDetail(req, resp); } else if ("result".equals(action)) { showResult(req, resp); } else { resp.sendError(HttpServletResponse.SC_NOT_FOUND); } } @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String action = req.getParameter("action"); if ("submit".equals(action)) { submitVote(req, resp); } else { resp.sendError(HttpServletResponse.SC_NOT_FOUND); } } }

统一入口之后,每个方法内部只做三件事:解析参数、调用 Service、指定转发路径。参数校验放在 Service 层还是 Servlet 层,我是这样分的:和 HTTP 相关的格式校验(非空、长度、数值范围)放在 Servlet 层,和业务相关的规则校验(投票是否在有效期内、选项是否属于该主题、是否重复提交)放在 Service 层。这样分层的好处是,如果将来投票系统要接 WebService 接口,已经写好的校验逻辑可以整体复用,只需要在接口层做格式适配。

web.xml 中注意一个细节:如果不是 Servlet 3.0 以上版本,@WebServlet 注解不生效,必须在部署描述符中显式声明映射。现在的 Tomcat 8.5 和 9 都支持注解扫描,但很多学校的实验环境还在用 Tomcat 7,写代码前先确认 Servlet 版本,否则项目部署后访问 /vote 会直接 404,而这种 404 还很难排查。

3.2 Service 层的事务控制:手动提交还是交给框架

投票系统的 Service 层有一个典型的跨表写操作:提交投票时至少要插入一条 vote_record,同时更新 vote_topic 表的参与人数。如果只做第一件事,页面上展示的“已参与人数”就是一个统计查询,不需要事务;但如果要在主题表里维护一个冗余计数器,就必须保证插入记录和更新计数在同一事务内。

为什么会有冗余计数器?因为它可以让列表页免去每个主题一个 COUNT 子查询,性能好上不少。代价是事务复杂度上升。在没有 Spring 的纯 Java Web 项目里,事务通常用 JDBC 的 Connection 手动管理:

public void submitVote(VoteRecord record) throws SQLException { Connection conn = null; boolean oldAutoCommit = true; try { conn = DBUtil.getConnection(); oldAutoCommit = conn.getAutoCommit(); conn.setAutoCommit(false); VoteRecordDAO recordDAO = new VoteRecordDAO(conn); recordDAO.insert(record); recordDAO.incrementParticipantCount(record.getTopicId()); conn.commit(); } catch (SQLException e) { if (conn != null) { conn.rollback(); } throw e; } finally { if (conn != null) { conn.setAutoCommit(oldAutoCommit); conn.close(); } } }

这段代码有几个容易踩的坑。第一,conn.setAutoCommit(false) 必须放在事务开始处,且完成后要把旧值恢复,因为连接可能来自连接池,恢复状态是为了下一次复用不被污染。第二,DAO 的构造函数要接收外部传入的 Connection,而不是自己在 DAO 内部重新获取,否则事务会断开——两个 DAO 各拿各的连接,commit 和 rollback 互不相干,这是经典翻车现场。第三,rollback 后要重新抛出异常,让 Servlet 层返回统一的错误页,而不是在 catch 里吞掉异常假装成功。

如果在项目里引入了 Spring,事务就简单得多,@Transactional 注解标注在 Service 方法上即可。但课程设计的场景通常不建议直接上 Spring,原因是评委问“你怎么控制事务的”时,能讲清楚 JDBC 手动提交比背一个注解的原理要加分得多。这也是一个 Java Web 源码项目区别于纯 SSH 框架项目的价值点:它逼着你理解事务的本质。

3.3 JSP 页面不能写 Java 代码:用 JSTL 和 EL 表达式渲染投票列表

投票列表页是典型的动态数据渲染场景:从数据库查出所有启用中的投票主题,循环展示标题、类型、起止时间和当前状态。纯 JSP 的写法是在<%脚本里用 for 循环拼 HTML 字符串,这种做法维护性极差,而且 JSP 中的脚本片段会在编译期直接嵌进 Servlet 的 service 方法,页面改版就要动 Java 代码,命令 Java 程序员和前端工程师互相折磨。

正确做法是用 JSTL 标签库加 EL 表达式。先在 WebContent/WEB-INF/lib 下放 jstl.jar 和 standard.jar,然后在 JSP 顶部声明:

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>

渲染代码可以这样写:

<c:forEach items="${topicList}" var="topic" varStatus="status"> <div class="vote-item"> <h3><a href="vote?action=detail&topicId=${topic.id}">${topic.title}</a></h3> <p> 类型:${topic.type == 0 ? '单选' : '多选'} <c:if test="${topic.type == 1}">(最多选 ${topic.maxChoices} 项)</c:if> </p> <p>起止时间:${topic.startTime} ~ ${topic.endTime}</p> </div> </c:forEach>

这里用 EL 表达式 ${topic.title} 直接访问 VoteTopic JavaBean 的 getTitle 方法,用三元表达式 ${topic.type == 0 ? '单选' : '多选'} 做类型展示,用 c:if 标签做多选时候选个数的补充说明。整个页面的可读性比脚本片段强得多,且 JSP 中不再出现任何 import、Connection、ResultSet 这些与展示无关的代码。

页面渲染的另一边要注意字符编码。JSP 文件本身的保存编码、pageEncoding 属性、request 和 response 的 setCharacterEncoding,这三处不一致是中文乱码的总根源。通常要在 Servlet 的 doGet 和 doPost 最前面加 request.setCharacterEncoding("UTF-8"),并保证过滤器里把所有请求都统一指定了编码,而不只是在某个 Servlet 里设置——否则从 JSP 表单提交的中文数据进入数据库后就变成问号,这种问题查起来极其耗时。

4. 把项目跑起来的完整路径:从导入 Eclipse 到 Tomcat 部署,新手最容易卡在前三步

4.1 项目导入到 IDE 的两种方式:Maven 还是传统 Web 工程

这份投票系统源码如果不到 200MB,大概率是传统 Eclipse Web 工程,包含 .classpath、.project 和 WebContent 目录;如果带 pom.xml 和 src/main/java 目录结构,则是 Maven 工程。两种工程的导入方式完全不同,先搞清楚这一点能省半小时的折腾时间。传统工程用 Eclipse 的 File > Import > General > Existing Projects into Workspace,选中根目录后直接导入即可;Maven 工程要用 File > Import > Maven > Existing Maven Projects,引入后由 IDE 解析依赖。

我一般会先看项目根目录里有没有 pom.xml,有就按 Maven 处理,没有就按传统工程处理。值得提醒的是,用 IDEA 打开传统 Eclipse Web 工程时,需要手动配置 Artifacts 里的 Web facet,否则 Tomcat 启动后找不到 Web 根目录,请求直接 404。这里引出一个通用的判断逻辑:任何 Java Web 源码项目导入后,第一步不是急着点运行,而是先确认 project structure 里的 Modules 是否标记为 Web,Libraries 里是否有 Tomcat 的运行时依赖。这两项不对,后面所有页面都会出问题,而且报错信息往往很隐晦。

4.2 本地 MySQL 初始化:字符集和时区决定首轮调试是否顺滑

投票系统的建表脚本通常在项目的 sql 目录下,一般叫 vote_system.sql 或 init.sql。执行脚本前要确认两个设置:数据库字符集和连接时区。字符集建议用 utf8mb4,因为它能存储表情符号,且与微信等外部系统的文本兼容性最好;时区设置影响的是 MySQL 8.0 以上版本的默认连接,不加 serverTimezone 参数会报 CST 时区错误,这是 2020 年后所有 Java Web 项目本地部署最普遍的错误提示。

mysql -u root -p -e "CREATE DATABASE vote_system DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;" mysql -u root -p vote_system < sql/vote_system.sql

执行完建库脚本后,打开项目里的 jdbc.properties 或 DBUtil.java,确认 jdbc.url 里是否带 characterEncoding=UTF-8 和 useSSL=false,不带这两个参数,Java 连接 MySQL 时中文读写会乱码,本地调试时所有中文标题都会变成问号。这里补一条网络检索里的高频教训:很多人把字符集问题归咎于数据库表结构,反复修改字段的 CHARSET,但最终排查发现是 JDBC URL 没有指定编码,驱动默认用了系统平台编码去和数据库交互。

4.3 Tomcat 部署步骤和启动顺序:从 manager 页面到直接访问根路径

Tomcat 8.5 和 9 是 Java Web 项目最常见的运行容器。部署方式有两种:把项目打成 WAR 包放进 Tomcat 的 webapps 目录,或者在 IDE 里配置 Tomcat Server 并用 Artifact 方式热部署。后者是课程设计答辩前的首选,因为它支持修改代码后自动重载,不需要手动重启。

部署后的访问路径有一个很容易误解的地方。假设项目的 Context Path 是 /vote,启动 Tomcat 后访问地址应该是 http://localhost:8080/vote/,而不是直接访问根路径 http://localhost:8080/。根路径默认显示 Tomcat 的欢迎页,只有把 ROOT 目录替换成项目才能免去前缀访问。很多新手发现自己部署后“项目打不开”,其实是没搞清楚 URL 路径映射。

启动成功后先做冒烟测试:打开投票列表页确认能查到数据、进入详情页确认单选和多选的选项都能渲染、提交一次投票后确认结果页的百分比总和为 100%。这三个动作覆盖了后端数据库连接、前端 JSP 渲染、提交链路三块核心逻辑,跑通这三步,这个项目至少能答辩通过。

5. 避坑:Java Web 投票系统最常见的 5 个翻车点与排查方法

5.1 现象:页面提交后报 500,日志显示 “Cannot create PoolableConnectionFactory”

原因:数据库连接池初始化失败,最常见是 MySQL 用户名密码错误,或者数据库没建成功,但报错信息被连接池包装后变成 Connection refused 一类。另一个高频原因是 MySQL 8.0 的驱动类名是 com.mysql.cj.jdbc.Driver,而项目里用的是旧驱动 com.mysql.jdbc.Driver,两者在 MySQL 8.0 下直接不能连。

解决:先单独用命令行 mysql -u 用户名 -p 连接数据库,确认账号密码无误。再检查项目的 jdbc.properties 里的 driverClassName,MySQL 5.x 用 com.mysql.jdbc.Driver,8.x 用 com.mysql.cj.jdbc.Driver。最后确认 lib 下的 mysql-connector-java.jar 版本和数据库版本匹配,小于 5.1.47 的驱动连 8.0 也会抛异常。

5.2 现象:页面中文全部显示为问号,数据库里也是问号

原因:这个现象背后有三处都可能出错,而日志中通常没有明确线索。最常见是 JSP 页面编码和数据库编码不一致、JDBC URL 没带 characterEncoding=UTF-8、以及 Tomcat 的 URIEncoding 未设置 UTF-8 三者的组合。

解决:按顺序检查三处。JSP 的 pageEncoding 改成 UTF-8 并确认文件本身以 UTF-8 保存;JDBC URL 末尾加上 ?characterEncoding=UTF-8&useSSL=false;Tomcat 的 server.xml 中的 Connector 节点增加 URIEncoding="UTF-8"。设置完成后重启 Tomcat,重新提交一条投票数据,看中文是否恢复正常。如果仍乱码,用命令查看数据库中这条记录的十六进制内容,确认数据进入库时是否就已经损坏。

5.3 现象:刷新页面时浏览器弹出“确认重新提交表单”的提示

原因:提交投票的 POST 请求返回后,直接把结果页作为本次 POST 的响应渲染,浏览器刷新时重发 POST,重复提交被判定拦截,用户端表现就是弹窗。这个问题的根源不是投票去重逻辑,而是经典的 PRG(Post/Redirect/Get)模式缺失。

解决:VoteServlet 的 submitVote 方法处理完投票写入后,不要直接转发到 JSP,而是用 resp.sendRedirect("vote?action=result&topicId=" + topicId) 重定向到结果页。这样刷新时发起的是 GET 请求,不会触发重复提交,同时配合数据库唯一索引,安全性就完整了。

5.4 现象:投票结果显示所有选项的百分比相加不足 100%

原因:统计 SQL 中用了 LEFT JOIN,但百分比计算分母用的是右边表的 COUNT 结果,如果总票数为 0,ROUND 函数中会出现除零错误,部分数据库返回 NULL,前端显示为空。另一种情况是投票记录中存在 option_id 为空的历史脏数据,被 COUNT 计入总票数,但选项表 JOIN 不到名称,页面无法展示。

解决:在子查询中对总票数做兜底处理,IFNULL(COUNT(vr.id), 0)防止除零;统计前对 vote_record 做一次清理,删除 option_id 不在 vote_option 表中的脏记录。前端显示百分比之前加一个 c:if 判断,如果 totalCount 为 0,直接展示“暂无投票”,避免百分比的语义歧义。

5.5 现象:Tomcat 部署后打开列表页 404,但静态 HTML 能访问

原因:项目根目录没有识别为 Web 应用。传统 Eclipse 工程中,WebContent 目录才是 Web 根目录,如果 IDE 的 Artifacts 配置把 Output directory 指向了 classes 目录,而不是 war exploded 目录,Tomcat 加载后找不到 WEB-INF/web.xml,就无法识别这是一个 Web 应用。

解决:在 IDE 的 Project Structure 中确认 Artifacts 类型为 Web Application Exploded,Output Layout 里要有 WebContent 对应的输出目录。如果项目是 Maven 工程,检查 pom.xml 中 packaging 是否为 war,以及 maven-war-plugin 的 webResources 配置是否覆盖了 webapp 目录。这类 404 的根因在配置而非代码,排查方向是先看 Tomcat 启动日志有没有部署 context 的打印,而不是去改 Servlet 的映射路径。

6. 进阶:从投票系统源码里挖出两个以后会用到的能力——验证码与分页查询

投票系统除了核心投票功能,通常还会包含用户登录和后台管理。这里把两个在答辩加分和求职面试中都很常见的技术点单独拆出来讲:登录验证码和投票历史的分页查询。前者在面试中几乎是必考题,后者是一个真实的性能优化场景。

验证码的核心不是图片生成本身,而是会话中验证码的存储与校验时机。常见做法是用 javax.imageio.ImageIO 画一张带有随机数字的 BufferedImage,把答案存进 Session,用户提交时对比用户输入和 Session 中的值。这里最值得注意的细节是:验证码必须一次性使用,无论校验成功还是失败,都要在比对后立即从 Session 中移除,否则一个验证码可以被反复使用,暴力破解登录接口就失去了防御意义。代码示意如下:

String expected = (String) req.getSession().getAttribute("captcha"); String actual = req.getParameter("captcha"); req.getSession().removeAttribute("captcha"); if (expected == null || !expected.equalsIgnoreCase(actual)) { // 返回验证码错误提示 }

分页查询的典型坑是只改了 SQL 的 LIMIT,没注意总条数查询和当前页数据的原子性。用一个 PageBean 封装当前页码、每页条数、总条数和数据列表,Service 层在一次方法调用里先查询总数,再查询当前页数据,两个查询之间如果投票被删除,页面上会出现总数与实际数据条数不一致的轻微偏差。对投票系统而言这个偏差可接受,但如果在统计报表场景,就需要在事务中再加锁。分页 SQL 没有太多玄学,注意排序字段要稳定,否则翻页时数据顺序会跳动,这是很多人在做“上一页/下一页”时遇到的怪问题。

最后一个建议,关于系统上线后的观察:给投票接口补充简单的埋点日志,记录提交时间、user_key 特征、topic_id 和响应耗时。答辩时展示这份日志,能说明你对系统运行状态有主动监控的意识,这比任何华丽的页面都更能证明工程能力。我做过一次匿名投票系统的优化,当时线上出现同一 IP 短时间大量投票的情况,靠的就是 user_key 的 IP 哈希特征做了一次事后分析,才定位到是爬虫脚本。希望在你这边的课程设计里,这些积累能让你少走一点弯路。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询