JSP企业电子投票系统实战:Servlet+MySQL架构与并发防重设计解析 📅 发布时间:2026/9/8 1:55:41 👁 浏览次数: 简介这是一份基于JSPServlet的企业电子投票系统完整项目采用Tomcat与JDK环境运行面向Java Web学习者、课程设计者及需要快速搭建投票功能的开发人员。系统分为普通用户与管理两个模块用户可登录、对开放主题投票、查看已公布结果管理员可发布/编辑主题、公布或隐藏结果、撤销或恢复主题并支持用户管理。资源压缩包共64个文件主体为37个JSP页面同时包含CSS样式、XML配置、项目配置文件、运行截图以及SQL/数据库文件整体大小约1003KB目录结构清晰便于定位。已有389人学习下载。附带企业电子投票系统论文及数据库脚本从功能设计到环境部署均有参考价值适合毕业设计、期末实训或作为企业投票场景的基础框架二次开发。1. 接手JSP企业电子投票系统先搞清楚它到底解决了什么问题我最近在整理旧项目时又翻出了这套JSP企业电子投票系统当年可是在公司年会上扛过真实流量的。很多人一听JSP投票系统就觉得是课设级别的小玩意儿但把它放在企业场景里需求复杂度完全不一样——员工年终评优、工会选举、技术方案投票动辄几百上千人同时在线还要保证匿名性、防重复投票、结果可追溯这可不是写个request.getParameter就能糊弄过去的。这套系统说白了就是一套基于Java Web经典技术栈的完整投票解决方案前端页面用JSP渲染后端用Servlet处理业务逻辑数据落到MySQL里。它最核心的价值不是能投票这三个字而是把企业投票场景里的几个硬性要求全部兜住了一是匿名性投票人身份和投票内容必须彻底分离二是唯一性一个人对同一个议题只能投一次三是实时性前台投完票后台马上能出统计结果。任何一条没做好这套系统上线就是在给自己挖坑。我拆解这套系统的思路时习惯先画一张业务流转图管理员创建投票主题和选项设定投票起止时间然后把投票链接发给全员员工登录后进入投票页面勾选选项提交后台Servlet接收请求后先做校验——是否在投票有效期内、当前用户是否已投过票——校验通过才写入数据库同时把统计结果同步更新。整个链路看起来不复杂但每一步拆开都有细节可挖下面我把每个环节的设计逻辑和踩坑经验都摊开讲。2. 功能拆解和技术选型为什么是JSPServlet而不是Spring Boot2.1 四个核心功能模块一个都不能少这套系统从使用角色上可以分成管理员端和员工端两条线。管理员端负责投票的创建和监控员工端负责参与投票和查看结果。我把它拆成四个模块来设计投票管理模块管理员创建投票、设置截止时间、添加或删除选项、关闭或开启投票。这个模块是系统的中枢所有业务动作都从这里发起。在线投票模块员工浏览当前正在进行中的投票选择选项后提交。这里需要处理会话状态确认当前用户身份并做投票资格校验。实时统计模块投票数据落库后系统按选项维度进行分组统计以柱状图或百分比的形式在前端展示。不夸张地说这个模块决定了投票结果的公信力。用户与权限模块虽然叫匿名投票但系统必须知道是谁在投只是投票结果本身不关联用户身份。管理员和普通员工的权限要做区分防止普通员工误入后台管理页面。我自己的经验是很多初学者做投票系统只关心投票这个动作本身把用户管理和权限控制放到最后做甚至不做结果一上线就出问题——谁都能打开管理页面删投票或者刷票刷得飞起。在企业场景里权限和防刷是和投票功能同等重要的模块不能砍。2.2 技术栈的合理性分析现在新项目基本都上Spring Boot了但JSPServlet这套老技术栈在特定场景下仍然有它的位置。这套系统的技术选型我当初是这样考虑的JSP负责视图渲染利用c:forEach等JSTL标签遍历投票列表不需要单独写一堆JSON接口开发效率高。Servlet负责请求转发和业务调度用doGet和doPost区分页面跳转和数据提交逻辑清晰直白。数据库用MySQL三张核心表搞定不需要引入MyBatis或Hibernate这些重量级框架。会话管理用HttpSession登录成功后在session里存入用户Id和角色标识投票时直接从session取身份信息。统计图表用前端开源库ECharts后端只负责输出JSON格式的统计数据前端负责渲染。说实话如果你的项目是部署在老旧的企业内网服务器上环境里只有Tomcat和JDK那这套技术栈反而是最稳妥的选择——零外部依赖一个WAR包打天下服务器上拖进去就能跑。我当年部署的时候那台老爷级服务器连Maven仓库都连不上全靠直接把class文件塞进WEB-INF/classes目录不也跑得好好的。技术选型这事合适才是第一位的。注意如果你接手的是别人写的JSP项目第一件事不是看代码而是确认JDK版本和Tomcat版本。JSP项目对容器版本非常敏感Tomcat 8和Tomcat 9对Servlet API的支持有差异直接用高版本Tomcat跑老项目最容易报ClassNotFoundException之类的诡异错误。3. 数据库设计与核心逻辑实现三张表撑起整个系统3.1 表结构设计要点这套系统的数据库表设计遵循了“小而美”的原则总体上三张核心表就能覆盖全部业务需求管理员和员工统一放用户表、投票主题和选项分别放两张表另外用一张关联表记录投票明细和防重复校验。这是核心的建表语句几个关键字段我加了一行注释说明设计意图-- 用户表管理员和普通员工统一存储role字段区分身份 CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) UNIQUE NOT NULL, password VARCHAR(100) NOT NULL, role TINYINT DEFAULT 0 COMMENT 0:员工, 1:管理员 ); -- 投票主题表 CREATE TABLE vote_topic ( id INT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(200) NOT NULL COMMENT 投票标题例如年度优秀员工评选, description TEXT COMMENT 投票说明展示在投票页面上, start_time DATETIME NOT NULL, end_time DATETIME NOT NULL, status TINYINT DEFAULT 1 COMMENT 1:进行中, 0:已关闭 ); -- 投票选项表 CREATE TABLE vote_option ( id INT PRIMARY KEY AUTO_INCREMENT, topic_id INT NOT NULL COMMENT 关联vote_topic表的id, option_text VARCHAR(200) NOT NULL COMMENT 选项文字, vote_count INT DEFAULT 0 COMMENT 该选项获得的票数, FOREIGN KEY (topic_id) REFERENCES vote_topic(id) ); -- 投票记录表核心防重表 CREATE TABLE vote_record ( id INT PRIMARY KEY AUTO_INCREMENT, topic_id INT NOT NULL, user_id INT NOT NULL, option_id INT NOT NULL, vote_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_topic_user (topic_id, user_id) );这里最值得讲的是vote_record表里的唯一索引uk_topic_user。这是防重复投票的数据库层面的杀手锏——即使应用层有并发漏洞两个人同时点提交数据库的唯一索引也会拦住第二条记录抛一个DuplicateKeyException出来。我做压力测试的时候用JMeter模拟200个并发请求同时投同一个议题最终落库的记录数准确无误就是这个唯一索引的功劳。很多初学者只在前端用display:disabled禁用按钮防重复提交那是纯糊弄自己绕过前端直接发POST请求分分钟把票投烂。3.2 投票核心链路的Servlet实现投票提交是整个系统最核心的Servlet我把它拿来做示例讲讲。这里不只写代码重点是讲清楚每步校验背后的逻辑WebServlet(/vote/submit) public class VoteServlet extends HttpServlet { protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 设置编码解决中文字符乱码问题这个放第一行 request.setCharacterEncoding(UTF-8); response.setContentType(text/html;charsetUTF-8); // 1. 从session获取当前登录用户 HttpSession session request.getSession(); User user (User) session.getAttribute(loginUser); if (user null) { response.getWriter().write(请先登录); return; } // 2. 校验是否是管理员自己投自己的管理操作防止越权 // 这里仅限普通员工提交投票 if (user.getRole() 1) { response.getWriter().write(管理员不能参与投票); return; } // 3. 获取参数 int topicId Integer.parseInt(request.getParameter(topicId)); int optionId Integer.parseInt(request.getParameter(optionId)); // 4. 查询投票主题判断时间窗口 VoteTopic topic voteTopicDao.findById(topicId); Date now new Date(); if (now.before(topic.getStartTime()) || now.after(topic.getEndTime())) { response.getWriter().write(投票不在有效时间内); return; } // 5. 尝试插入投票记录唯一索引兜底防重 try { voteRecordDao.insert(new VoteRecord(topicId, user.getId(), optionId)); } catch (DuplicateKeyException e) { response.getWriter().write(您已参与过该投票); return; } // 6. 更新对应选项的票数 voteOptionDao.increaseCount(optionId); response.sendRedirect(request.getContextPath() /vote/result?topicId topicId); } }看到没整个投票流程是先校验再插入最后更新计数。这个顺序是我调过一堆并发问题后总结出来的最佳实践先插记录、后更新票数这样万一更新失败用户至少不会重复投票先更新票数再插记录一旦插入失败票数就虚高了对不上账。3.3 统计结果的数据转换投票结束后前端需要用柱状图展示各选项的得票数和占比。后端统计的写法比较直接一条SQL加一个循环// 按选项分组统计关联查询选项文字和得票数 String sql SELECT o.option_text, o.vote_count FROM vote_option o WHERE o.topic_id ? ORDER BY o.id; // 遍历选项列表同时从topic表读取总投票人数计算百分比 for (VoteOption vo : optionList) { double percent totalCount 0 ? 0 : (vo.getVoteCount() * 100.0 / totalCount); percentMap.put(vo.getOptionText(), Math.round(percent * 10) / 10.0); }这里有一个容易踩坑的地方计算百分比时如果总票数是0直接用vote_count / totalCount会报ArithmeticException——除零异常。更稳妥的做法是先把totalCount算出来判断是否为0再做除法或者用DecimalFormat限制小数位数避免出现33.333333333%这种前端展示很难看的数字。统计接口的数据用JSON格式输出前端拿到之后用ECharts渲染。用JSON而不是直接在JSP里拼HTML的好处是未来如果要换成小程序端或者移动端这个接口可以无缝复用不需要再改后端。4. 实操部署与疑难杂症排查4.1 从零部署一套可运行的环境我在反复部署这套系统的过程中整理出了一套标准的部署流程照着走基本不会出大问题环境准备安装JDK 1.8配置JAVA_HOME环境变量安装Tomcat 8.5安装MySQL 5.7。初始化数据库执行项目中提供的init.sql脚本创建数据库、三张核心表和初始管理员账号。修改数据库连接配置在WEB-INF/classes/db.properties中修改jdbc.url、jdbc.username、jdbc.password注意数据库名和你创建库时保持一致。打WAR包如果你是从IDEA或Eclipse里导出的项目直接右键选择Build War即可如果是纯手工部署跳过这步。部署到Tomcat把WAR包复制到webapps目录下启动Tomcat后会自动解压也可以直接拖解压后的文件夹进去。验证启动访问http://localhost:8080/vote/init.jsp如果看到初始化成功的提示说明环境已经通了。开放端口如果部署在服务器上让别人访问记得在防火墙里放行8080端口。4.2 高频问题排查记录这套系统我在不同环境里部署了不下十次把遇到过的高频问题整理成了一张速查表每一类问题都附上了排查方向和解决办法现象可能原因排查与解决页面中文全是???或乱码JSP页面编码和数据库编码不一致统一设置为UTF-8JSP页面头部加pageEncodingUTF-8数据库连接URL加characterEncodingutf-8部署后页面报404URL路径和WebServlet注解路径不匹配检查注解的映射路径与页面访问路径是否一致注意contextPath前缀投票提交后没有反应Servlet中request.getParameter拿到null检查JSP表单的name属性和Servlet读取的参数名是否完全一致登录成功后跳转不过去session超时或session写入失败确认登录时执行了session.setAttribute并设置了合适的session超时时间数据库连接超时或拒绝连接数据库地址、用户名或密码配置错误先在本机用MySQL客户端连一遍排除数据库本身问题再检查配置文件管理员能看到所有投票操作按钮权限校验不严JSP页面里用c:if test${loginUser.role 1}包裹管理按钮投票结果显示NaN或负数初始化数据时票数字段为空或负值给vote_count字段加默认值0并加上非负约束检查这里我特别想讲一下 JSP 文件修改后不生效的问题。这是JSP项目里最经典的坑我要单独列出来详细说说。4.3 JSP改了不生效90%是这三类情况很多JSP初学者改完页面刷新发现页面还是老样子第一反应是代码错了其实大概率不是代码问题。我把这类改了不生效的情况归类了一下经验全部来自实战踩坑第一类浏览器缓存了旧的JSP页面。JSP最终会被编译成Servlet再渲染成HTML但有些浏览器会对动态页面做缓存尤其是通过“后退”按钮返回页面时展示的还是旧版本。解决办法是强制清缓存刷新Chrome用CtrlShiftR强制刷新或者在JSP页面head里加入meta http-equivCache-Control contentno-store。第二类Tomcat没有重新编译JSP。JSP文件在Tomcat里会被编译成.class文件存在work/Catalina/localhost/项目名/org/apache/jsp目录下。如果时间戳没更新Tomcat会认为JSP没变直接复用老的class文件。解决办法是把work目录清空再重启Tomcat。我习惯在部署新版本前直接删掉整个work/Catalina目录一劳永逸。第三类改的是备份文件而不是真正生效的文件。这种情况在团队协作时尤其常见——大家用Git管理代码改完忘了提交或者提交了忘了pull本地改了但服务器上还是旧代码。排查办法是到服务器上查看JSP文件的最后修改时间用stat命令确认。提示别在生产环境直接改JSP文件来修Bug哪怕只是改一行文字。JSP被修改后会触发重新编译在编译期间访问页面可能出现错误。正确姿势是在本地改完测试通过再打包替换整个WAR包或同步整个项目文件夹。5. 从踩坑到稳定运行的优化实录5.1 并发投票与数据一致性的实战验证这套系统最让我心虚的一个场景是公司全员评优的时候几百号人同时涌进来投票。刚部署完那会儿心里没底就先用压力测试工具打了一轮。我用的JMeter模拟了200个并发用户的投票请求默认配置跑完发现投票结果和实际提交数差了十几票当场冷汗就下来了。后来定位到问题是投票动作不是原子操作——先查再插再更新三个步骤中间一旦有并发交错就会出现重复计数或者漏计数。最后的解决方案是双保险应用层先查一遍是否已投数据库层再用唯一索引兜底这两个措施叠加后我再压500个并发结果纹丝不动。这个经验后来我写进了项目笔记里不管什么系统防重复的手段至少要设计两层一层在应用里挡常规操作一层在数据库里挡极端并发。5.2 匿名投票的安全边界设计企业投票有个特殊的矛盾点既要保证投票过程匿名又要防止有人恶意刷票。我的处理思路是把投票人身份和投票内容在数据层面彻底分离——vote_record表里只存用户ID和选项ID但业务上严格保证一个用户在同一主题下只能有一条记录。前端页面压根不展示谁投了谁查询SQL也不允许跨表关联用户姓名。实际操作中我还加了一条管理端的审计功能管理员可以导出所有投票记录但记录里只有选项ID和投票时间看不到员工姓名。这样就既保证了可审计性又守住了匿名性这条红线。5.3 结果导出Excel的小扩展投票结束后经常需要把结果导成Excel发给领导或存档。我在这套系统里扩展了一个导出功能用的是Apache POI库。核心逻辑就是查询统计结果然后一行行写入Excel单元格// 创建Excel工作簿 Workbook workbook new XSSFWorkbook(); Sheet sheet workbook.createSheet(投票统计); // 创建表头 String[] headers {选项, 得票数, 占比}; Row headerRow sheet.createRow(0); for (int i 0; i headers.length; i) { Cell cell headerRow.createCell(i); cell.setCellValue(headers[i]); } // 填充数据 int rowNum 1; for (Map.EntryString, Double entry : resultMap.entrySet()) { Row row sheet.createRow(rowNum); row.createCell(0).setCellValue(entry.getKey()); row.createCell(1).setCellValue(entry.getValue().intValue()); row.createCell(2).setCellValue(entry.getValue() %); } // 输出到响应流 response.setContentType(application/vnd.openxmlformats-officedocument.spreadsheetml.sheet); response.setHeader(Content-Disposition, attachment;filenamevote_result.xlsx); workbook.write(response.getOutputStream()); workbook.close();这块要注意的是Content-Disposition头里的文件名如果含中文必须做URL编码否则浏览器下载会乱码。另外POI的版本要和你JDK匹配POI 4.x以上需要JDK 8老版本Tomcat上跑会有奇怪的NoClassDefFoundError。5.4 视频展示这种小需求加得非常顺手有次需求方临时说要在投票说明页放一段候选人的自我介绍视频我本来以为要专门写一套视频播放模块后来用JSP自带的HTML5video标签搞定了video width640 height360 controls source src${pageContext.request.contextPath}/upload/candidate_demo.mp4 typevideo/mp4 您的浏览器不支持视频播放 /video只要把MP4文件丢到项目webroot下或者上传到服务器上用相对路径指向。需要注意的两个点是Tomcat默认上传大小限制是2MB项目里需要修改web.xml里的max-upload-size视频文件建议放在webapps外面用Tomcat的虚拟目录映射到项目里免得打包部署时把大文件打进WAR包拖慢发布速度。6. 一个温和但务实的建议把这套系统当作架构启蒙课来拆如果你是想学Java Web开发的小白我很推荐拿这套JSP投票系统当入门项目。它麻雀虽小五脏俱全——JSP到Servlet到DAO到数据库经典分层结构清清楚楚没有任何多余的框架包装你能看到最原始的数据流转方式。先把这套东西跑通再去玩Spring Boot你会突然发现很多东西都能对号入座Controller就是Servlet的增强版MyBatis就是JDBC的封装SpringMVC的DispatcherServlet就是所有Servlet的大管家。我在实际使用中最大的体会是这套系统虽然老但它把一些永恒的问题——并发控制、权限管理、数据一致性、编码处理——都清晰地暴露在最底层。比如你在Spring Boot里很少需要手动处理request.setCharacterEncoding(UTF-8)但在JSP里漏了这行字乱码就会教你做人而这种被坑过的记忆比任何教程都深刻。最后再分享一个小技巧如果你准备在简历上用这类项目别只写实现了投票功能要把通过唯一索引解决并发重复投票基于Session的登录状态管理统计结果实时可视化这些具体的技术点写进去面试官一眼就能看出你不是背了个课设而是真真正正跑过、压过、排查过的。这个小技巧可能就是你和别人拉开差距的那一步。本文还有配套的精品资源点击获取