简介:面向Java Web初学者、高校课程设计与中小企业内部简易投票场景的JSP电子投票系统完整源码包,基于JSP、Tomcat和JDK构建,帮助读者理解登录鉴权、主题投票、结果公布与后台维护等典型业务。压缩包共64个文件、约1003KB,以37个JSP页面为核心,配合CSS样式、XML配置、SQL建库脚本及MDF/LDF数据库文件,并附带项目截图与论文文档,便于对照页面结构、数据表设计、部署环境和前后台页面跳转关系。已有391人学习下载,适合作为Java Web入门练习、课程作业或毕业设计参考。系统清晰划分普通用户与管理员:普通用户可登录退出、对已开放主题投票并查看已公布结果;管理员可撤销或恢复主题、永久删除主题、公布或隐藏结果、编辑和发布主题,以及新增删除用户。读者可据此梳理会话状态、权限控制、表单校验、数据库增删改查与后台管理菜单组织,快速搭建可运行的投票系统原型,并在此基础上完成功能扩展与二次开发。
1. 内网里的评优投票:一份 JSP 企业电子投票系统能顶什么用
某公司行政组织年度评优,起初用在线问卷工具收集,结果卡在两件事上:数据必须落在自己机房的数据库里,投票要按部门隔离、一人一票、能追溯。最后回到内网自建,用的就是这套 JSP 企业电子投票系统。它把投票主题发布、候选人维护、投票校验、结果统计和账号权限都收在一套后台里;技术栈没有花哨设计——JDK + Tomcat + JSP/Servlet + JavaBean + JDBC,MySQL 存数据,标准的传统 Java Web 分层。这套东西适合三类人:需要交课程设计或毕业设计的同学、想在局域网里搭个小投票工具的开发者,以及想找一份能完整跑通、每个请求都能读懂去向的 JSP 工程练手的人。跟着把 Servlet 生命周期、Session 会话和 JDBC 事务走一遍,比看十篇原理文章都实在。
2. 技术栈与目录结构:先弄清楚这套 JSP 工程是怎么分层的
2.1 为什么 JSP + Servlet + JavaBean 这套老组合还在内网跑
先说实话,这套组合在互联网公司基本绝迹了,但在企业内网、学校机房、行政系统里活得很好,原因很朴素。第一是部署成本低:整个项目打成 war 包扔进 Tomcat 的webapps目录,重启就能用,不需要 Node 环境、不需要打包构建、不需要配反向代理。第二是资源占用小,一台 2 核 4G 的旧服务器挂几十个并发投票毫无压力。第三是改起来直观,改一个 JSP 页面保存后刷新浏览器就能看到效果,行政那边临时要加一句话、换个表头,五分钟的事。
代价也很清楚:JSP 里容易混进 Java 代码,业务逻辑和展示逻辑黏在一起;没有依赖注入,DAO 得自己 new;单元测试几乎没法做。所以我在看这类项目时,第一眼不是看功能多不多,而是看它有没有把bean / dao / servlet / filter这几层分干净。分干净了,后面加题型、加报表都顺手;分不干净,改一处崩三处,那种血泪经验相信做过老项目的人都懂。
这份系统的分层思路是典型的 Model2:JSP 只负责渲染,Servlet 接参数做流程控制,DAO 拿着Connection敲 SQL,JavaBean 在层与层之间当载体。没有 Service 层是个小遗憾,业务判断基本写在 Servlet 里,改造时如果你想把逻辑抽出来,Servlet 就是切口。
2.2 解压后先看目录:一份典型的 Web 工程长什么样
解压出来的目录结构大同小异,Eclipse 动态 Web 项目习惯用WebRoot,IDEA 习惯用web,看到哪个都别慌,里面的东西是一样的。核心是先找到三样:src里的 Java 源码、WEB-INF/web.xml里的 Servlet 映射、以及根目录下那个.sql建库脚本。
JSPVoteSystem/ ├── src/ │ └── com/company/vote/ │ ├── bean/ # 实体类:User、Vote、VoteItem、VoteRecord │ ├── dao/ # 数据访问:UserDao、VoteDao、RecordDao │ ├── servlet/ # 控制器:LoginServlet、VoteSubmitServlet 等 │ ├── filter/ # 登录校验、编码过滤 │ └── util/DBUtil.java # JDBC 连接与关闭工具 ├── WebRoot/ │ ├── WEB-INF/ │ │ ├── web.xml # Servlet / Filter 注册与欢迎页 │ │ └── lib/ # 放 mysql-connector-java 等 jar │ ├── css/ js/ images/ │ ├── login.jsp │ ├── vote_list.jsp # 投票列表(前台) │ ├── vote_detail.jsp # 投票详情与提交表单 │ ├── result.jsp # 结果统计 │ └── admin/ # 后台:主题管理、候选人管理、用户管理 └── vote.sql # 建库建表 + 初始管理员账号看这个结构要注意两点。一是WEB-INF/lib里必须有数据库驱动 jar,少了它启动不报错、一访问就抛ClassNotFoundException,很多人卡在这一步。二是admin/目录下的 JSP 要么统一被 Filter 拦截,要么干脆挪进WEB-INF/里——放在WEB-INF下的 JSP 浏览器直接敲地址是访问不到的,只能由 Servlet 转发进去,等于免费加了一道门。
2.3 数据库表设计:投票系统绕不开的六张表
不管界面长什么样,投票类系统的数据模型基本就这六张表。理解它们的关联,比读十页代码都快:一个投票主题(t_vote)下挂多个选项(t_vote_item),用户投一次票就在记录表(t_vote_record)里落一行,统计就是把记录表按选项分组数数。
-- 投票主题表 CREATE TABLE t_vote ( id INT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(120) NOT NULL, description VARCHAR(500), start_time DATETIME NOT NULL, end_time DATETIME NOT NULL, max_choice TINYINT DEFAULT 1, -- 1 单选,>1 多选 status TINYINT DEFAULT 1, -- 1 进行中,0 已关闭 create_by INT, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 选项 / 候选人表 CREATE TABLE t_vote_item ( id INT PRIMARY KEY AUTO_INCREMENT, vote_id INT NOT NULL, item_name VARCHAR(80) NOT NULL, item_desc VARCHAR(300), sort_no INT DEFAULT 0, KEY idx_vote (vote_id) -- 按主题查选项,这行索引不能省 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 投票记录表:一人一票的约束落在这里 CREATE TABLE t_vote_record ( id INT PRIMARY KEY AUTO_INCREMENT, vote_id INT NOT NULL, item_id INT NOT NULL, user_id INT NOT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_vote_user_item (vote_id, user_id, item_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;第三张表的唯一索引是全篇最关键的一行。常见做法是在 Servlet 里先select count(*)判断有没有投过,再insert;两个请求同时到,两边都查不到记录,结果投了两次。把「不能重复」交给数据库的唯一索引,Java 层只负责捕获异常,这才是可靠的做法。参数说明:max_choice控制单选多选,status配合start_time / end_time决定当前能不能投,sort_no控制选项在页面上的顺序——行政那边经常要求按姓氏笔画或者部门顺序排,加个字段比改代码便宜。
另外两张是用户表t_user(含role字段区分普通用户和管理员)和部门表t_dept,用于按部门统计参与率。字段不多,但role一定要在登录时就塞进 Session,否则每个 Servlet 里都要重复查库判断身份。
3. 环境搭建与跑通:从 JDK 到 Tomcat 的完整启动链路
3.1 JDK 与 Tomcat 版本对齐,这一步错了后面全白搭
JSP 项目最常见的翻车点不是代码,是版本。javax.servlet和jakarta.servlet这两个包名是分水岭:Tomcat 9 及以下用javax.servlet.*,Tomcat 10 开始全部改成jakarta.servlet.*。老项目里的import javax.servlet.http.HttpServlet;在 Tomcat 10 上编译都过不去,更别说运行。所以拿到这份 rar 的第一件事,是确认它的导入包名,然后选对应的大版本。
# 1. 确认 JDK,老项目一般 1.8 就够,别上 17 折腾 java -version # 2. 看项目里的 import,决定 Tomcat 大版本 grep -r "import javax.servlet" src/ | head -3 # 输出 javax.servlet.* → 用 Tomcat 8.5 / 9 # 输出 jakarta.servlet.* → 用 Tomcat 10 及以上 # 3. 配置环境变量后启动(Linux / macOS 示例) export JAVA_HOME=/usr/local/jdk1.8.0_301 export CATALINA_HOME=/usr/local/apache-tomcat-9.0.65 $CATALINA_HOME/bin/startup.sh # 4. 看日志确认起来了,没有异常再往下走 tail -f $CATALINA_HOME/logs/catalina.out命令背后的含义值得说清楚:JAVA_HOME必须指向 JDK 而不是 JRE,Tomcat 编译 JSP 时需要tools.jar(JDK 8)或者完整 JDK 环境,只装 JRE 会在第一次访问 JSP 时抛Unable to compile class for JSP。startup.sh之后不要急着开浏览器,先看catalina.out,端口占用、内存参数、应用加载失败都会在这里打出来,比在浏览器里看到一片 404 强得多。
Windows 下对应的是startup.bat,环境变量在系统设置里配,CATALINA_HOME路径不要带中文和空格——这是老项目的玄学之一,路径带空格时setclasspath.bat拼出来的 classpath 会被截断,报错信息还特别误导人。
3.2 数据库初始化与连接配置
建库脚本一般在根目录,导入之前先看一眼里面的库名和字符集,别直接source完事。
# 建库并导入结构和初始数据 mysql -uroot -p -e "CREATE DATABASE vote_db DEFAULT CHARSET utf8mb4;" mysql -uroot -p vote_db < vote.sql # 确认表都进去了,顺便看看初始管理员账号 mysql -uroot -p vote_db -e "SHOW TABLES; SELECT id,username,role FROM t_user LIMIT 5;"连接参数通常写死在DBUtil.java里,也有的项目放db.properties,看src下有没有这个文件。改造时我一般会把它挪到 properties,避免改一次库就重新编译。
// DBUtil.java —— 手写连接池太重,用 DriverManager 加静态块足够这个体量 public class DBUtil { private static final String URL = "jdbc:mysql://127.0.0.1:3306/vote_db?useUnicode=true&characterEncoding=utf8" + "&useSSL=false&serverTimezone=Asia/Shanghai"; private static final String USER = "root"; private static final String PWD = "123456"; static { try { Class.forName("com.mysql.cj.jdbc.Driver"); // 5.x 驱动换成 com.mysql.jdbc.Driver } catch (ClassNotFoundException e) { throw new ExceptionInInitializerError("数据库驱动没放进 WEB-INF/lib:" + e); } } public static Connection getConn() throws SQLException { return DriverManager.getConnection(URL, USER, PWD); } // 关闭顺序不能反:ResultSet → Statement → Connection public static void close(Connection c, Statement s, ResultSet r) { try { if (r != null) r.close(); } catch (SQLException ignored) {} try { if (s != null) s.close(); } catch (SQLException ignored) {} try { if (c != null) c.close(); } catch (SQLException ignored) {} } }三个参数值得单独提醒。characterEncoding=utf8是防止中文姓名变问号的第一道关;serverTimezone=Asia/Shanghai在 MySQL 8 上不加会抛时区异常,而这个异常只在真正建立连接时才出现,启动阶段看不出来;useSSL=false是内网环境省掉握手告警。驱动类名要注意版本:MySQL 5.x 用com.mysql.jdbc.Driver,8.x 用com.mysql.cj.jdbc.Driver,写错了报错是ClassNotFoundException,跟 jar 没放进去的表现一模一样,很容易排查错方向。
3.3 部署到 Tomcat 并验证第一个页面
部署有两条路:把整个工程目录拷到webapps/下,或者打成 war。开发阶段用第一条,改完 JSP 直接生效。
# 方式一:整个目录拷过去,访问 http://localhost:8080/JSPVoteSystem/ cp -r JSPVoteSystem/ $CATALINA_HOME/webapps/ # 方式二:打包成 war,放到 webapps 下 Tomcat 会自动解压 cd JSPVoteSystem && jar -cvf ../JSPVoteSystem.war . cp ../JSPVoteSystem.war $CATALINA_HOME/webapps/ # 验证:先用 curl 看返回码,别急着开浏览器 curl -I http://localhost:8080/JSPVoteSystem/login.jspcurl -I返回 200 说明静态资源链路通了;返回 404 说明 context path 不对,检查 war 包名和web.xml里的<welcome-file-list>;返回 500 一般就是驱动缺失或者数据库连不上,这时候去catalina.out里找Caused by,能直接定位到根因。等登录页能打开,用初始管理员账号登进去,先建一个投票主题、挂两个选项、投一票、看结果页数字有没有动——这四步走完,整个项目的核心链路就验证通过了,后面再谈改造才有意义。
4. 核心链路拆解:一次投票从点击到落库经历了什么
4.1 登录与权限过滤:Filter 挡住未登录请求
登录逻辑本身很普通:查用户、比对密码、把User对象塞进 Session。真正决定这个项目能不能用的,是后面的拦截。没有 Filter 的话,任何人手敲/admin/user_list.jsp就能进后台。
// LoginFilter.java —— 注册在 web.xml 里,拦 /admin/* 和 /vote/* public class LoginFilter implements Filter { @Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) resp; String uri = request.getRequestURI(); // 放行登录页和静态资源,否则连登录页都进不去,直接死循环 if (uri.endsWith("login.jsp") || uri.endsWith("login.do") || uri.contains("/css/") || uri.contains("/js/")) { chain.doFilter(req, resp); return; } HttpSession session = request.getSession(false); // false:没有就返回 null,不新建 User user = (User) (session == null ? null : session.getAttribute("loginUser")); if (user == null) { response.sendRedirect(request.getContextPath() + "/login.jsp"); return; } // 后台路径再判一次角色,普通用户不能进 admin if (uri.contains("/admin/") && user.getRole() != 1) { response.sendError(403, "无权访问"); return; } chain.doFilter(req, resp); } }参数和写法上有几个点。getSession(false)是关键,用getSession()会给每个路过 Filter 的请求都建一个 Session,内存白白涨;sendRedirect要带上getContextPath(),不然部署名改了之后跳转会跑到根路径;角色判断放在 Filter 里比在每个 Servlet 里写一遍干净得多。web.xml里对应的注册块:
<filter> <filter-name>LoginFilter</filter-name> <filter-class>com.company.vote.filter.LoginFilter</filter-class> </filter> <filter-mapping> <filter-name>LoginFilter</filter-name> <url-pattern>/admin/*</url-pattern> <url-pattern>/vote/*</url-pattern> </filter-mapping>如果项目用的是 Servlet 3.0 注解,直接在类上加@WebFilter(urlPatterns={"/admin/*","/vote/*"})也行,但要注意注解和web.xml里的metadata-complete="true"会打架——web.xml里一旦声明了完整元数据,所有注解全部失效,这是排查「Filter 明明写了却不生效」时最容易漏掉的一条。
4.2 投票提交的 Servlet 处理与防重复投票
投票提交是整个系统唯一有并发风险的地方,也是最容易写错的地方。Servlet 里要做四件事:取参数、校验时间窗、落库、处理重复。
// VoteSubmitServlet.java public class VoteSubmitServlet extends HttpServlet { private final VoteDao voteDao = new VoteDao(); @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding("UTF-8"); // 必须在取参数之前调 resp.setContentType("text/html;charset=UTF-8"); User user = (User) req.getSession().getAttribute("loginUser"); int voteId = Integer.parseInt(req.getParameter("voteId")); String[] itemIds = req.getParameterValues("itemId"); // 单选传一个,多选传多个 if (itemIds == null || itemIds.length == 0) { req.setAttribute("msg", "请至少选择一项"); req.getRequestDispatcher("/vote_detail.jsp?voteId=" + voteId).forward(req, resp); return; } // 服务端再校验一次选项数量,不能只靠前端 disable Vote vote = voteDao.findById(voteId); if (itemIds.length > vote.getMaxChoice()) { req.setAttribute("msg", "最多只能选 " + vote.getMaxChoice() + " 项"); req.getRequestDispatcher("/vote_detail.jsp?voteId=" + voteId).forward(req, resp); return; } try { voteDao.saveRecords(voteId, user.getId(), itemIds); // 事务内批量插入 resp.sendRedirect(req.getContextPath() + "/vote/result?voteId=" + voteId); } catch (DuplicateVoteException e) { req.setAttribute("msg", "你已经投过票了"); req.getRequestDispatcher("/vote_detail.jsp?voteId=" + voteId).forward(req, resp); } catch (SQLException e) { throw new ServletException("投票失败", e); } } }DAO 层把「先查再插」换成「直接插 + 捕获唯一键冲突」,配合 2.3 里那个唯一索引,就避免了前面说的并发问题:
// VoteDao.saveRecords —— 多条选项要么全成功,要么全回滚 public void saveRecords(int voteId, int userId, String[] itemIds) throws SQLException, DuplicateVoteException { Connection conn = null; try { conn = DBUtil.getConn(); conn.setAutoCommit(false); // 关掉自动提交,开事务 String sql = "INSERT INTO t_vote_record(vote_id,item_id,user_id) VALUES(?,?,?)"; try (PreparedStatement ps = conn.prepareStatement(sql)) { for (String itemId : itemIds) { ps.setInt(1, voteId); ps.setInt(2, Integer.parseInt(itemId)); ps.setInt(3, userId); ps.addBatch(); } ps.executeBatch(); } conn.commit(); } catch (SQLIntegrityConstraintViolationException e) { if (conn != null) conn.rollback(); throw new DuplicateVoteException(); // 唯一索引挡住了重复投票 } catch (SQLException e) { if (conn != null) conn.rollback(); throw e; } finally { DBUtil.close(conn, null, null); } }逻辑说明:setAutoCommit(false)之后,批处理的几条记录是一个整体,多选场景下不会出现「投了三票只进去两票」的脏数据。addBatch/executeBatch相比循环单条插入,二十个选项能省掉十几次网络往返。SQLIntegrityConstraintViolationException是 MySQL 驱动对唯一键冲突的专门异常类型,捕获它比捕获宽泛的SQLException精确,不会把别的错误误判成重复投票。参数上,voteId来自表单隐藏域,itemId来自复选框的 value,userId一定要从 Session 取而不是从请求参数取——从参数取等于让用户自己填投票人,那就没得玩了。
req.setCharacterEncoding("UTF-8")必须写在第一次getParameter之前,写在后面完全无效,浏览器提交的中文选项名会变成乱码存进库。这一点我在老项目里见过太多次。
4.3 统计页面的 SQL 聚合与 JSP 渲染
结果页是新需求改得最多的地方:要显示票数、要显示百分比、要按票数倒序、有时候还要看「本部门已投 x/y 人」。核心就一条聚合 SQL,把它写对了,页面怎么变都好办。
-- 按选项聚合票数,并算出占比;LEFT JOIN 保证零票的选项也能显示出来 SELECT i.id, i.item_name, COUNT(r.id) AS cnt, ROUND(COUNT(r.id) * 100.0 / NULLIF((SELECT COUNT(*) FROM t_vote_record WHERE vote_id = ?), 0), 2) AS pct FROM t_vote_item i LEFT JOIN t_vote_record r ON r.item_id = i.id WHERE i.vote_id = ? GROUP BY i.id, i.item_name ORDER BY cnt DESC, i.sort_no ASC;两个细节。LEFT JOIN而不是INNER JOIN,是因为新建的选项还没人投时,INNER JOIN会把它直接从结果里抹掉,页面上看起来像丢了选项。NULLIF(..., 0)是防除零,一票都没有的时候总数为 0,不处理会算出NULL或者报错,页面上显示成空白比显示 0 更让人困惑。ORDER BY cnt DESC, sort_no ASC是票数相同时用配置顺序兜底,避免每次刷新顺序乱跳——这种小地方最影响使用者对系统的信任。
JSP 侧把结果集遍历出来即可,注意百分比条的宽度也要跟着算:
<!-- result.jsp 片段 --> <c:forEach var="row" items="${resultList}"> <div class="item"> <span class="name">${row.itemName}</span> <span class="cnt">${row.cnt} 票(${row.pct}%)</span> <div class="bar" style="width:${row.pct}%; background:#4a90d9;"></div> </div> </c:forEach>${row.pct}为null时在这里会被渲染成空字符串,宽度算出来是 0,页面不会崩。但如果项目里还用着老式<%= %>脚本片段,null直接拼进style就会出现width:null%,浏览器直接忽略这条样式,看起来像是「零票选项没有进度条」,其实只是没渲染出来。改造时我一般优先把这些脚本片段换成 JSTL,省掉一堆空指针判断。
5. 避坑与排查:JSP 老项目最容易翻车的五个地方
5.1 启动阶段:版本错配、驱动缺失和 404
现象一:浏览器一片 404,控制台没有任何异常。
原因通常是 context path 对不上。war 包叫JSPVoteSystem.war,访问路径就是/JSPVoteSystem/,大小写敏感,敲成/jspvotesystem/一样是 404。还有一种情况是web.xml里的<url-pattern>和表单action不一致,比如表单提交到/vote/submit.do,而 Servlet 映射的是/vote/submit。
解决:先访问http://localhost:8080/看 Tomcat 首页,能看到就说明容器正常;再看webapps目录下有没有解压出来的文件夹;最后把表单 action 和web.xml里的 pattern 逐字对一遍。
现象二:启动正常,一访问登录页就抛java.lang.ClassNotFoundException: com.mysql.cj.jdbc.Driver。
原因是驱动 jar 没进WEB-INF/lib,或者只加进了 IDE 的 build path。IDE 里能跑是因为 IDE 把 jar 加进了自己的 classpath,脱离 IDE 部署到 Tomcat 就找不到。
解决:把mysql-connector-java-x.x.x.jar复制到WebRoot/WEB-INF/lib/下,重新部署。这个目录是 Web 应用自己的类加载器扫描的,跟 Tomcat 的lib目录不是一回事。
现象三:升级到 Tomcat 10 之后,编译报package javax.servlet does not exist。
原因就是 3.1 说的包名迁移。Tomcat 10 起 Servlet API 全部迁到jakarta.servlet,老代码一行没改就编译不过。
解决:要么把 Tomcat 换回 9.x,要么全项目批量替换javax.servlet→jakarta.servlet并同步换掉web.xml的 schema 版本。前者五分钟,后者半小时,看你的长期打算。
5.2 运行阶段:中文乱码、重复投票和统计对不上
现象四:投票选项里的中文存进数据库变成???。
原因有三个层次,得逐个排:JSP 页面头部的<%@ page contentType="text/html;charset=UTF-8" %>、Servlet 里的req.setCharacterEncoding("UTF-8")、数据库表和连接串的字符集。三层里任何一层漏了,中文都会坏。另外 GET 请求的参数编码由 Tomcat 的Connector决定,setCharacterEncoding管不到。
解决:JSP 加 page 指令,Servlet 在取参数前设编码,server.xml的 Connector 加URIEncoding="UTF-8",建表用utf8mb4。四件事做齐,基本不会再乱码。实在排查不出来,就打印一下req.getParameter拿到的字符串长度,长度不对说明在传输层就已经坏了。
现象五:统计页面总票数比实际投票人数多。
原因分两种。一是唯一索引没建,同一用户重复提交进了多条记录;二是多选场景下唯一索引建成了(vote_id, user_id),导致用户投第二项时被挡住,反而不对。索引必须是(vote_id, user_id, item_id)三列组合。
解决:先用SELECT vote_id,user_id,COUNT(*) c FROM t_vote_record GROUP BY vote_id,user_id HAVING c > 1把重复数据揪出来,清掉;再补上正确的唯一索引。统计参与率的时候要COUNT(DISTINCT user_id),而不是COUNT(*),这一个字的差别能让参与率从 120% 变回 100%。
提示:所有「应该唯一」的约束,能落到数据库索引就别只写在 Java 的 if 里。并发这件事,代码挡不住,索引挡得住。
6. 二次开发:把投票系统改成你实际需要的形态
改这类项目,我建议从最小的切口进。最常见的第一需求是「单选改多选」,其实max_choice字段已经预留好了,你只需要把vote_detail.jsp里的type="radio"换成type="checkbox",name 保持itemId不变,Servlet 那边getParameterValues本来就能接数组,一行 Java 都不用改。第二个需求通常是加权重评分(比如 1 到 5 分),这时候t_vote_record要加一个score字段,聚合 SQL 从COUNT换成AVG,前端把单选框换成一个下拉或者星级组件,改动面仍然可控。
导出 Excel 是行政最爱提的需求。项目里如果已经有 POI 的 jar,直接照着现有 DAO 的查询结果写一个ExportServlet,用HSSFWorkbook或者XSSFWorkbook铺表头;没有就先把依赖放进WEB-INF/lib。导出接口同样要过LoginFilter,我见过有人为了「方便分享给领导」把导出地址做成免登录,结果整个投票明细裸奔在内网里。
验证改造有没有改坏,我的习惯是跑一遍固定的四条链路:登录 → 建主题 → 投票 → 看统计。再补一条边界:投票时间窗之外提交,看服务端有没有拦住。前端的disabled属性只是体验,服务端不校验等于没校验。
| 改造点 | 涉及文件 | 容易忽略的地方 |
|---|---|---|
| 单选改多选 | vote_detail.jsp | 前端数量限制必须服务端再校验一次 |
| 加权重评分 | t_vote_record、统计 SQL | 聚合用AVG而不是COUNT |
| 导出 Excel | 新增ExportServlet | 导出接口也要过权限 Filter |
| 按部门统计 | t_user、t_dept联表 | 用COUNT(DISTINCT user_id) |
要说一个我最深的教训:这类老项目最危险的操作不是加功能,是「顺手重构」。曾经为了把 Servlet 里的业务逻辑抽到 Service 层,改到一半发现多处 JSP 直接new DAO()绕过 Servlet 查数据,抽完之后那些页面全挂。从那以后我每次动这类项目,都强制先把所有 JSP 里的<%脚本片段 grep 一遍,把「谁在绕过控制层直接连库」摸清楚,再决定从哪儿下手。希望这些能帮到你,少走两趟弯路。
本文还有配套的精品资源,点击获取