简介:这是一份基于JSP+Servlet的企业电子投票系统完整项目,采用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. 功能拆解和技术选型:为什么是JSP+Servlet而不是Spring Boot
2.1 四个核心功能模块,一个都不能少
这套系统从使用角色上可以分成管理员端和员工端两条线。管理员端负责投票的创建和监控,员工端负责参与投票和查看结果。我把它拆成四个模块来设计:
- 投票管理模块:管理员创建投票、设置截止时间、添加或删除选项、关闭或开启投票。这个模块是系统的中枢,所有业务动作都从这里发起。
- 在线投票模块:员工浏览当前正在进行中的投票,选择选项后提交。这里需要处理会话状态,确认当前用户身份,并做投票资格校验。
- 实时统计模块:投票数据落库后,系统按选项维度进行分组统计,以柱状图或百分比的形式在前端展示。不夸张地说,这个模块决定了投票结果的公信力。
- 用户与权限模块:虽然叫"匿名投票",但系统必须知道是谁在投,只是投票结果本身不关联用户身份。管理员和普通员工的权限要做区分,防止普通员工误入后台管理页面。
我自己的经验是,很多初学者做投票系统只关心"投票"这个动作本身,把用户管理和权限控制放到最后做甚至不做,结果一上线就出问题——谁都能打开管理页面删投票,或者刷票刷得飞起。在企业场景里,权限和防刷是和投票功能同等重要的模块,不能砍。
2.2 技术栈的合理性分析
现在新项目基本都上Spring Boot了,但JSP+Servlet这套老技术栈在特定场景下仍然有它的位置。这套系统的技术选型,我当初是这样考虑的:
- 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;charset=UTF-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-8,JSP页面头部加pageEncoding="UTF-8",数据库连接URL加characterEncoding=utf-8 |
| 部署后页面报404 | URL路径和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用Ctrl+Shift+R强制刷新,或者在JSP页面<head>里加入<meta http-equiv="Cache-Control" content="no-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.Entry<String, 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;filename=vote_result.xlsx"); workbook.write(response.getOutputStream()); workbook.close();这块要注意的是Content-Disposition头里的文件名,如果含中文必须做URL编码,否则浏览器下载会乱码。另外POI的版本要和你JDK匹配,POI 4.x以上需要JDK 8+,老版本Tomcat上跑会有奇怪的NoClassDefFoundError。
5.4 视频展示这种小需求,加得非常顺手
有次需求方临时说要在投票说明页放一段候选人的自我介绍视频,我本来以为要专门写一套视频播放模块,后来用JSP自带的HTML5<video>标签搞定了:
<video width="640" height="360" controls> <source src="${pageContext.request.contextPath}/upload/candidate_demo.mp4" type="video/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的登录状态管理""统计结果实时可视化"这些具体的技术点写进去,面试官一眼就能看出你不是背了个课设,而是真真正正跑过、压过、排查过的。这个小技巧,可能就是你和别人拉开差距的那一步。
本文还有配套的精品资源,点击获取