news 2026/10/29 5:03:34

JSP企业电子投票系统拆解:防重复投票、事务与部署

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
JSP企业电子投票系统拆解:防重复投票、事务与部署

简介:这份JSP企业电子投票系统源码包面向学习Java Web与JSP开发的学生及初级开发者,提供一套可直接部署运行的投票管理练习项目,帮助理解前后台权限划分、主题发布与结果公示等业务流程。压缩包共64个文件,约1003KB,以37个JSP页面为核心,配合CSS样式、XML配置、JPG界面截图,以及SQL脚本、MDF/LDF数据库文件和项目说明文档,覆盖从页面展示、后台管理到数据存储的完整链路。项目分为普通用户与管理员两个模块:普通用户可登录退出、参与已开放主题投票并查看已公布结果;管理员可撤销或恢复主题、永久删除、公布或隐藏结果、编辑与发布主题,并维护用户账号。现有391人学习,适合课程设计、毕业设计原型参考,也可作为JSP+Tomcat+JDK环境下练习请求处理、会话控制与数据库操作的实战素材。

1. 企业内网搞投票,这套 JSP 老架构为什么值得拆

去年年底,某公司的行政同事找我帮忙统计年度优秀团队评选结果,一开始用群接龙,随后发现同一个工号能投两次,还有人改了昵称重新投一次。最后三份名单对不上,只能人工核对到半夜。后来我把这套 JSP 企业电子投票系统部署到一台闲置的内网服务器上,账号绑定工号,一人一票,投票结束后结果直接出柱状图,两天就把事情办完了。

这套 JSP 企业电子投票系统属于很典型的 JavaWeb 单体项目:JSP 负责页面渲染,Servlet 接收请求并处理业务,JavaBean 承载数据,数据库保存选票,整个项目运行在 Tomcat 上,由 JDK 编译。它不追新,没有 Spring Boot,也没有前后端分离,但对“内网、几十到几百人、要留痕、要防止重复投票”这种企业投票场景来说,够用而且可控。

适合哪些人?有 Java 基础、想要一套自己能读懂能改的投票后端的人:学生做课程设计或毕设,企业内部做评优评先、方案表决,或者你手上有一堆类似的 JavaWeb 老代码要维护,先拿它练手。如果你想的是“下载下来就能直接上云做 SaaS”,那这套东西会让你失望;但如果你想搞清楚“投票系统到底怎么防刷、怎么统计、怎么部署”,它的结构反而比一堆脚手架清楚。

2. 技术栈与目录结构:JSP + Servlet + JavaBean 的三层分工

2.1 技术栈清单:每一样都在干具体活

很多人一看到 JSP 就觉得是“上古技术”,但拆开看,这套组合的分工其实非常清晰,每个组件职责明确,你要改哪一块,直接定位到对应目录就行。

  • JDK 1.8:编译期和运行期的基础。这类项目基本锁定 8,别想着用 17 或 21 去跑,反射和 JSP 编译容易出问题。
  • Tomcat 8.5 或 9.0:Servlet 容器,同时负责把.jsp编译成 Servlet。注意不要用 Tomcat 10,那是jakarta.*包名,老代码里的javax.servlet.*会全部找不到。
  • Servlet + JSP:Servlet 做控制器,接收表单并调用 Service;JSP 只做视图,把结果渲染成 HTML。这是 Model2 的思路,比纯 JSP 里写 SQL 的 Model1 干净得多。
  • JavaBean:实体类和 DAO、Service 层,承载User、Topic、Option这类对象,顺便实现序列化和参数封装。
  • JDBC 直连:项目里没有用 ORM,常见做法是写一个DBUtil统一获取连接,DAO 里手写 SQL。好处是 SQL 全在明面上,坏处是事务要自己管——这一点在第 4 章会重点讲。
  • JSTL + EL:JSP 里用${}和<c:forEach>取数据,避免满屏<% %>脚本片段。
  • 前端:Bootstrap 或类似的栅格样式,加 jQuery 做表单校验,图表一般是 ECharts 或 Highcharts,投票结果页直接渲染柱状图和饼图。

我用一句话总结这套选型:它能让你在一个 war 包里看到从请求到落库的完整链路,没有任何东西是“框架帮你做了但你看不见”的。对二次开发来说,这恰恰是优点。

2.2 目录结构:请求从哪进、数据往哪走

把压缩包解开,导入 IDE 之后,主流目录结构大致是这样,我按实际职责标注了每一层干什么:

evote/ ├── src/ │ └── com/evote/ │ ├── bean/ # 实体:User、Topic、Option、VoteRecord │ ├── dao/ # 数据访问:UserDao、TopicDao、VoteDao │ ├── service/ # 业务:VoteService、StatService,事务在这里开 │ ├── servlet/ # 控制器:LoginServlet、VoteServlet、ResultServlet │ ├── filter/ # 过滤器:编码过滤、登录拦截 │ └── util/ # 工具:DBUtil、MD5Util、JsonUtil ├── WebContent/ (或 web/) │ ├── WEB-INF/ │ │ ├── web.xml # Servlet 映射、过滤器注册、欢迎页 │ │ └── lib/ # mysql-connector、jstl、servlet-api 等 jar │ ├── static/ # css、js、echarts.min.js │ ├── login.jsp │ ├── vote/ │ │ ├── list.jsp # 投票主题列表 │ │ ├── detail.jsp # 选项勾选页 │ │ └── result.jsp # 结果图表页 │ └── admin/ # 后台:主题管理、用户管理、数据导出 └── db/ └── evote.sql # 建表和初始数据

这张图里最值得注意的两点是:WEB-INF/lib才是依赖真正生效的地方,你在 IDE 里 Build Path 加了 jar 但没往这里放,部署上去照样ClassNotFoundException;另外src下的包是按“职责”分的,不是按“页面”分的,所以你改一个投票逻辑,通常要在servlet → service → dao三处连着看,别只改一处就以为完事。

web.xml里一般能看到三段配置:字符编码过滤器、登录拦截过滤器、以及各个 Servlet 的<url-pattern>。如果是 Servlet 3.0 之后,也可以直接用@WebServlet("/vote/submit")注解,两种方式混用在新老项目里很常见,遇到 404 先看这里。

2.3 数据库模型:五张表撑起整套投票

投票系统的核心难点从来不是“把票存进去”,而是“怎么保证一个人只投一次”和“怎么快速出统计”。这套项目的表设计基本围绕这两点展开,下面是按常见实现整理出来的建表语句:

-- 用户表:username 用工号/学号,天然一人一号 CREATE TABLE t_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(32) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL, -- 老项目多为 MD5,二次开发建议换 BCrypt real_name VARCHAR(32), role TINYINT DEFAULT 0, -- 0 普通员工 1 管理员 status TINYINT DEFAULT 1 -- 1 启用 0 停用 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 投票主题:max_select 控制单选/多选 CREATE TABLE t_topic ( id INT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(128) NOT NULL, max_select TINYINT DEFAULT 1, -- 1=单选,>1 表示最多可选几项 start_time DATETIME, end_time DATETIME, status TINYINT DEFAULT 0 -- 0 草稿 1 进行中 2 已结束 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 选项:vote_count 是冗余计数字段,列表页不必每次 count CREATE TABLE t_option ( id INT PRIMARY KEY AUTO_INCREMENT, topic_id INT NOT NULL, option_text VARCHAR(128) NOT NULL, vote_count INT DEFAULT 0, KEY idx_topic (topic_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 投票主记录:唯一索引是防重复投票的最后一道闸 CREATE TABLE t_vote_record ( id INT PRIMARY KEY AUTO_INCREMENT, topic_id INT NOT NULL, user_id INT NOT NULL, vote_time DATETIME DEFAULT CURRENT_TIMESTAMP, ip VARCHAR(64), UNIQUE KEY uk_topic_user (topic_id, user_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 投票明细:多选时一个 record 对应多条 detail CREATE TABLE t_vote_detail ( id INT PRIMARY KEY AUTO_INCREMENT, record_id INT NOT NULL, option_id INT NOT NULL, KEY idx_record (record_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

参数说明几个关键点:uk_topic_user这个唯一索引是整个系统的关键,只要它在,哪怕应用层判断因为并发没有生效,数据库这一层也会抛SQLIntegrityConstraintViolationException,插入不可能成功两次。t_option.vote_count是典型的冗余设计,用空间换统计速度,代价是每次投票要同步更新,所以必须包在同一个事务里。t_vote_detail存在的意义是支持多选和事后追溯,如果只存option_id的计数,你永远说不清“某个人到底投了哪几项”。

3. 环境搭建与启动:JDK、Tomcat、MySQL 一条链跑通

3.1 版本对齐:先确认 JDK 和 Tomcat,再谈别的

这类项目翻车最多的不是代码,是版本。我一般按下面这个顺序检查,一步都不能跳:

# 1. 确认 JDK,老项目基本锁定 1.8 java -version # 期望:java version "1.8.0_xxx" # 2. 确认 JAVA_HOME 指向的是同一个 JDK,不是系统自带的另一个 echo $JAVA_HOME # Linux/macOS echo %JAVA_HOME% # Windows # 3. 确认 Tomcat 版本,8.5 / 9.0 都能跑,10 及以上包名变了不要用 $CATALINA_HOME/bin/version.sh # 4. 启动并把日志盯住,第一次启动的报错全在这里 $CATALINA_HOME/bin/startup.sh tail -f $CATALINA_HOME/logs/catalina.out

命令行含义:version.sh会同时打印 Tomcat 版本和它使用的 JVM 版本,这一行信息能直接排掉一半“启动就报 UnsupportedClassVersionError”的问题。startup.sh在 Windows 下对应startup.bat;catalina.out是标准输出汇总,中文乱码、数据库连不上、Servlet 初始化失败都会打在这里。

如果java -version是 1.8 但 Tomcat 启动时报Unsupported major.minor version 52.0之类的错,说明 Tomcat 用的是另一个更高版本的 JVM,改setenv.sh(Windows 是setenv.bat)里的JAVA_HOME即可,别去改系统环境变量折腾半天。

3.2 数据库导入与连接参数

先把db/evote.sql导进去,建库、建表、插管理员账号一步到位:

# 建库并导入,字符集显式指定,避免后面中文变问号 mysql -uroot -p -e "CREATE DATABASE evote DEFAULT CHARSET utf8mb4;" mysql -uroot -p evote < db/evote.sql # 验证:能查到初始管理员就说明导入成功 mysql -uroot -p -e "SELECT id, username, role FROM evote.t_user;"

接着改连接配置。项目里一般是一个db.properties放在src根目录,打包后会进WEB-INF/classes:

# src/db.properties jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://127.0.0.1:3306/evote?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai jdbc.username=root jdbc.password=换成你的密码

参数里三个容易忽略的点:characterEncoding=utf8不加,中文候选人名字进去就是乱码;serverTimezone=Asia/Shanghai不加,MySQL 8 的驱动会直接抛时区异常;useSSL=false是内网环境省掉证书握手,公网部署时不要关。

对应的工具类通常长这样,注意驱动加载放在静态块里,只跑一次:

// src/com/evote/util/DBUtil.java public class DBUtil { private static String url, user, pwd; static { try (InputStream in = DBUtil.class.getClassLoader() .getResourceAsStream("db.properties")) { Properties p = new Properties(); p.load(in); Class.forName(p.getProperty("jdbc.driver")); // 显式加载驱动,老项目常见写法 url = p.getProperty("jdbc.url"); user = p.getProperty("jdbc.username"); pwd = p.getProperty("jdbc.password"); } catch (Exception e) { // 静态块抛异常会变成 ExceptionInInitializerError,日志里要能看出是配置问题 throw new ExceptionInInitializerError("数据源初始化失败: " + e.getMessage()); } } public static Connection getConn() throws SQLException { return DriverManager.getConnection(url, user, pwd); } // close(ResultSet, Statement, Connection) 逐个判空关闭,此处略 }

逻辑说明:getResourceAsStream从 classpath 根读取配置,所以db.properties必须放在源码根目录,跟着编译进classes。Class.forName在 JDBC 4 之后其实可以省略,但老代码里保留着,删掉也不会错,只是别在删的时候顺手把 import 弄乱。真正要留意的是连接没有归还机制——这套写法每次getConn()都是新连接,并发一上来就会打满max_connections,第 6 章会给替换方案。

3.3 打包部署与首次登录自检

部署这一步,Eclipse 里是 Add and Remove 到 Tomcat,IDEA 是配 Artifact 打 war 包,命令行则更直白:

# 打 war 并丢进 webapps,Tomcat 会自动解压部署 mvn clean package -DskipTests # 若有 pom;无 pom 则用 IDE 导出 war cp target/evote.war $CATALINA_HOME/webapps/ $CATALINA_HOME/bin/shutdown.sh && $CATALINA_HOME/bin/startup.sh

部署完别急着点页面,先按这个清单过一遍:访问http://localhost:8080/evote/,能跳到login.jsp说明部署成功;用初始管理员账号登录,能进后台说明数据库通了;新建一个投票主题并加两个选项,能保存说明写库正常;换一个普通账号投一票,能在结果页看到数字变化,说明整条链路通了。任何一步卡住,回到catalina.out里找第一个Caused by,那才是根因,后面的报错都是连带产生的。

提示:Tomcat 默认端口 8080,如果本机已被占用,改conf/server.xml里的Connector port即可。改完记得同步改项目里写死的跳转地址,老项目里硬编码localhost:8080的地方不少。

4. 核心链路拆解:一次投票从提交到落库的完整流程

4.1 投票提交:VoteServlet 里到底做了几件事

投票是整个系统里唯一有写操作且必须保证一致性的入口,所以这段代码值得逐行看。下面按常见实现整理,注释标了每一步在防什么:

// src/com/evote/servlet/VoteServlet.java @WebServlet("/vote/submit") public class VoteServlet extends HttpServlet { private final VoteService service = new VoteService(); @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // 1. 会话校验:没登录直接打回登录页 User login = (User) req.getSession().getAttribute("loginUser"); if (login == null) { resp.sendRedirect(req.getContextPath() + "/login.jsp"); return; } // 2. 参数解析:topicId 必填,optionId 可能多值 int topicId = Integer.parseInt(req.getParameter("topicId")); String[] optionIds = req.getParameterValues("optionId"); // 3. 选项数量校验,上限来自主题的 max_select if (optionIds == null || optionIds.length == 0 || optionIds.length > service.maxSelect(topicId)) { req.setAttribute("msg", "请选择 1 到 " + service.maxSelect(topicId) + " 项"); req.getRequestDispatcher("/vote/detail.jsp").forward(req, resp); return; } // 4. 交给 Service,Servlet 不碰 SQL Result r = service.castVote(topicId, login.getId(), optionIds); resp.setContentType("application/json;charset=UTF-8"); resp.getWriter().write("{\"code\":" + r.getCode() + ",\"msg\":\"" + r.getMsg() + "\"}"); } }

参数说明:req.getParameterValues("optionId")拿的是数组,因为多选主题下前端是 checkbox;如果前端是 radio 单选,用getParameter也行,但代码就得为两种模式各写一套,所以统一按数组处理更省事。service.maxSelect(topicId)每次查一次库,看起来有点浪费,但这正好挡住了“前端把单选主题改成多选提交”的越权操作——前端校验只是体验,服务端校验才是边界,这句话在这种老项目里体现得特别明显。

事务和写库逻辑在 Service 层,这是全篇最该抄走的一段:

// src/com/evote/service/VoteService.java public Result castVote(int topicId, int userId, String[] optionIds) { Connection conn = null; try { conn = DBUtil.getConn(); conn.setAutoCommit(false); // 关自动提交,主记录和明细必须同生共死 VoteDao dao = new VoteDao(conn); if (dao.exists(topicId, userId)) { // 应用层查重 conn.rollback(); return Result.fail(4001, "你已经投过票了"); } int recordId = dao.insertRecord(topicId, userId); for (String oid : optionIds) { int optionId = Integer.parseInt(oid); dao.insertDetail(recordId, optionId); // 明细表 dao.incrCount(optionId); // 冗余计数+1 } conn.commit(); return Result.ok("投票成功"); } catch (SQLIntegrityConstraintViolationException dup) { rollbackQuietly(conn); // 并发下唯一索引兜底 return Result.fail(4001, "你已经投过票了"); } catch (Exception e) { rollbackQuietly(conn); log.error("投票失败 topic={} user={}", topicId, userId, e); return Result.fail(5000, "投票失败,请重试"); } finally { closeQuietly(conn); } }

逻辑说明:setAutoCommit(false)之后,insertRecord和循环里的insertDetail、incrCount要么全成功,要么全回滚。如果不加事务,就很容易出现“主记录插进去了、明细插了一半、计数没加上”,结果页人数和票据总数永远对不上,而且这种脏数据事后极难排查。SQLIntegrityConstraintViolationException这个 catch 不能省,它处理的是两个请求同时通过了exists判断、最后靠唯一索引拦下一个的情况,没有它,并发场景下用户会看到一个 500 错误页。

4.2 防重复投票:三道拦截各管一段

“一人一票”这四个字,在实现上有三层,每层解决的问题不一样,缺一层就会漏:

第一层是会话,session.getAttribute("loginUser")为空就跳登录。它挡的是未登录游客,但不能挡“同一个人登录两次”。第二层是应用层查重,dao.exists(topicId, userId)在插入前查一次。它能给出友好提示,但在高并发下存在检查与插入之间的时间窗,两个请求可能都查到“没投过”。第三层是数据库唯一索引,uk_topic_user是最后的保障,前面两层都漏了它也兜得住。

还有一层常见的辅助是 IP 或 Cookie 记录,项目里t_vote_record.ip字段就是干这个的。要提醒一句:IP 只能作为审计线索,不能当防刷依据,公司内网几十号人共用一个出口 IP,拿它做限制会误伤一大片;Cookie 更是随手就能清。真正的防重复,靠的是“人”这个维度,也就是登录账号加唯一索引。

注意:如果这套系统要支持“匿名投票”,就得把user_id换成一个一次性的投票凭证(比如管理员发的随机码),唯一索引改成uk_topic_token。直接删掉唯一索引改成只记 IP,基本等于放弃防刷。

4.3 结果统计:一条 group by 撑起整页图表

结果页的查询比投票简单得多,核心就是按选项分组计数:

-- 某主题下每个选项的得票数,LEFT JOIN 保证零票选项也能显示 SELECT o.id, o.option_text, COUNT(d.id) AS cnt FROM t_option o LEFT JOIN t_vote_detail d ON d.option_id = o.id WHERE o.topic_id = ? GROUP BY o.id, o.option_text ORDER BY cnt DESC;

参数说明:这里用LEFT JOIN而不是INNER JOIN,原因是新加的选项可能一票没有,用内连接会直接从图表里消失,看着像 bug。GROUP BY里把option_text也带上,是为了兼容某些数据库的ONLY_FULL_GROUP_BY模式,不加可能报错。如果你更信冗余字段,也可以直接SELECT option_text, vote_count FROM t_option WHERE topic_id = ?,速度快但要求计数绝对准确——一旦有人手工改过库,两边就对不上了,所以我在统计页一律用明细表现算,冗余字段只用于列表页展示。

数据到了 JSP,图表部分通常是这样渲染的:

<%-- vote/result.jsp:jsonResult 由 ResultServlet 序列化后放入 request --%> <div id="chart" style="width:680px;height:360px;"></div> <script src="${pageContext.request.contextPath}/static/echarts.min.js"></script> <script> var rows = ${jsonResult}; // 形如 [{"name":"方案A","value":12}, ...] echarts.init(document.getElementById('chart')).setOption({ tooltip: { trigger: 'axis' }, xAxis: { type: 'category', data: rows.map(function (r) { return r.name; }) }, yAxis: { type: 'value', minInterval: 1 }, series: [{ type: 'bar', data: rows.map(function (r) { return r.value; }), barWidth: 36 }] }); </script>

${jsonResult}是 EL 直接输出字符串,所以 Servlet 那边必须用JSON库序列化好再setAttribute,不能塞一个List进去,否则页面上会出现[object Object]。minInterval: 1是为了让票数为整数时刻度不出现 0.5 这种尴尬值,小细节,但演示时很加分。

5. 避坑与排查:这类 JSP 老项目最容易翻车的五处

5.1 中文全变问号,从表单到数据库一路排查

现象:新建投票主题时标题是“年度优秀团队评选”,保存后列表页显示成一串问号,数据库里查出来也是问号。

原因:三处编码没对齐——JSP 页面pageEncoding、请求体编码、数据库连接串编码。老项目里web.xml的CharacterEncodingFilter经常被注释掉或者forceEncoding没开。

解决:JSP 头部统一<%@ page contentType="text/html;charset=UTF-8" pageEncoding="UTF-8" %>;web.xml里注册编码过滤器并设forceEncoding=true;JDBC URL 加useUnicode=true&characterEncoding=utf8;建库时显式DEFAULT CHARSET utf8mb4。四处都改完再重启,只改一处等于没改。

5.2 启动报 ClassNotFoundException: com.mysql.cj.jdbc.Driver

现象:IDE 里跑单元测试一切正常,部署到 Tomcat 后一访问登录页就 500,日志第一行是驱动类找不到。

原因:依赖只加到了 IDE 的 Build Path,没有复制进WEB-INF/lib。Tomcat 的类加载器只认WEB-INF/lib和WEB-INF/classes,IDE 的配置它看不见。

解决:把mysql-connector-java的 jar 复制到WEB-INF/lib下,重新部署。顺带检查servlet-api.jar和jsp-api.jar不要放进去,这两个由容器提供,放进去反而会引发类冲突。这是我从 IDE 切到命令行部署时踩过的坑,属于典型的“本地能跑、线上就崩”。

5.3 重复投票拦不住,同一个人投了三次

现象:测试时用同一个账号连续点提交,结果页的票数涨了三次。

原因:多半是只在 JSP 层做了“已投票就隐藏按钮”,后端完全没校验;或者t_vote_record上的唯一索引建表时漏了。

解决:先SHOW INDEX FROM t_vote_record;确认uk_topic_user在不在,不在就补上;再把 Service 层的exists判断加上,并 catch 唯一键冲突。补索引前记得先清理已有的重复数据,否则加索引会直接失败,清理 SQL 按topic_id, user_id分组保留最小id即可。

5.4 改了 JSP 刷新没变化,重启才好

现象:调样式时改了detail.jsp,浏览器强制刷新还是旧页面。

原因:Tomcat 会把 JSP 编译成 Servlet 放在work/Catalina/localhost/evote/下,某些配置下不会自动重新编译;另外浏览器也会缓存 CSS 和 JS。

解决:确认conf/web.xml里development=true且modificationTestInterval不要设太大;调试期直接在浏览器 DevTools 里勾掉缓存,或者 URL 后面加时间戳。真要清理就删掉work目录下对应应用的文件再重启,比反复重启整个 Tomcat 快。

5.5 统计人数和投票总数对不上

现象:记录表里有 120 条投票记录,结果页各选项票数加起来是 137。

原因:一是多选主题,一个人投三项,明细表自然比记录表多;二是事务没包住,出现主记录成功、明细部分失败;三是有人直接改过t_option.vote_count,冗余字段和明细表脱节。

解决:先确认主题是不是多选,多选下“总票数”和“投票人数”本来就是两个口径,页面上要分开显示。然后跑一条对账 SQL:SELECT o.id, o.vote_count, COUNT(d.id) FROM t_option o LEFT JOIN t_vote_detail d ON d.option_id=o.id GROUP BY o.id HAVING o.vote_count <> COUNT(d.id);,有差异的记录说明历史数据脏了,用明细表回写一次冗余字段即可。

6. 二次开发:把这套老投票改成能交付的模块

拿到这套代码之后,我一般不会大改,而是按“风险从低到高”分批动。先把下面这张改造清单过一遍,心里有数再动手:

改造点落手位置难度风险
密码 MD5 换 BCryptMD5Util+ 登录校验低老账号需重置或兼容双算法
JDBC 直连换连接池DBUtil低配置写错会全站不可用
JSP 脚本片段换 JSTL各*.jsp中漏改变量名会静默显示空
增加投票结果导出 Excel新增ExportServlet中大数据量要分批查
增加“匿名投票”模式表结构 + Service高唯一索引口径要重设计
拆成前后端分离新增 REST 层高相当于重写视图层

第一批我建议只动前两项。密码这块,把MD5Util换成 BCrypt 之后,登录逻辑里加一个兼容分支:先用 BCrypt 校验,失败再按 MD5 算一次,命中就顺手把库里的值升级成 BCrypt。这样老账号不用集体重置,用户无感。

连接池的替换更简单,DBUtil.getConn()的签名保持不动,内部换成从HikariDataSource取连接,close()改成归还连接。改完之后用SHOW PROCESSLIST;观察一下,并发压测时连接数应该稳定在池大小附近而不是一路涨。

// 换池后:对外签名不变,DAO 层一行都不用改 private static HikariDataSource ds; static { HikariConfig cfg = new HikariConfig(); cfg.setJdbcUrl(url); cfg.setUsername(user); cfg.setPassword(pwd); cfg.setMaximumPoolSize(10); // 内网几十人的规模,10 条足够 cfg.setConnectionTimeout(3000); // 3 秒拿不到连接就快速失败,别让请求堆着 ds = new HikariDataSource(cfg); } public static Connection getConn() throws SQLException { return ds.getConnection(); // close 即归还,不再是物理关闭 }

第二批再考虑功能扩展。导出 Excel 我通常不引 POI 的重依赖,投票结果就几列几十行,直接输出 CSV 加 BOM 头,用 Excel 打开不乱码,代码量不到三十行。真要动“匿名投票”,那得先想清楚业务口径:匿名之后还能不能查“谁没投”?能查就说明还留着身份关联,那就不算真匿名。这个问题想不明白,改完上线一定被业务方追着问。

验证方法我也固定下来了:每改一处,就跑一遍那四条手工用例——未登录提交、重复提交、多选超限提交、投票后看统计。再补一条对账 SQL 保证明细和计数一致。改表结构时先用mysqldump备份一份,别嫌麻烦。

最后说个血泪教训。我头一回改这类项目,图快直接在t_vote_record上加了唯一索引,结果表里有历史重复数据,DDL 执行到一半失败,锁了表,测试环境停了半小时。从那以后我每次动老项目的表结构,都强制先跑一遍重复数据检测、先备份、再在空库上预演一次 DDL。老代码可以慢慢改,数据丢了真没有后悔药。希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/29 5:03:10

参考文献标注规范与定稿核对:正文对应、字段完整、工具清洗

上个月帮一位同学看他准备投出去的稿子&#xff0c;正文里明明写着[12]&#xff0c;翻到文末&#xff0c;参考文献列表最长只排到[9]&#xff1b;再往下核对&#xff0c;一条期刊论文的条目把卷号当成期号填了&#xff0c;页码只写了个起始页。这不算什么高深的学术问题&#x…

作者头像 李华
网站建设 2026/10/29 5:01:18

Laravel Actions asListener监听器静默失效排查

接手事件驱动的项目&#xff0c;最尴尬的瞬间不是代码写不出来&#xff0c;而是本地一切正常——事件一抛&#xff0c;Action 里的处理逻辑立刻开跑&#xff0c;日志里一行行副作用记得明明白白&#xff1b;代码推到自托管部署平台之后&#xff0c;日志干干净净&#xff0c;业务…

作者头像 李华
网站建设 2026/10/29 5:00:21

MCP Server:统一AI模型调用的标准协议与实践

1. 为什么我们需要MCP Server这样的标准协议&#xff1f;在AI技术爆发的今天&#xff0c;开发者面临着一个尴尬的局面&#xff1a;每个AI服务提供商都有自己的API规范、数据格式和调用方式。这就像是在一个没有统一语言的国家旅行&#xff0c;每到一个新地方都要重新学习当地方…

作者头像 李华
网站建设 2026/10/29 5:00:01

Keil与Proteus联调实战:单片机仿真调试配置与排错指南

简介&#xff1a;Keil Proteus.zip 是一份面向单片机初学者与电子工程实践者的软硬件协同开发示例包&#xff0c;围绕 Keil MDK 与 Proteus 的联合使用&#xff0c;给出一个温度测量小项目的工程雏形&#xff0c;可用于理解从 C 源码编写、编译生成 HEX 到电路仿真的基本流程。…

作者头像 李华
网站建设 2026/10/29 4:59:52

P4环境配置避坑:五组件版本绑定与编译顺序实践

简介&#xff1a;面向P4可编程数据平面学习与开发的环境配置安装包&#xff0c;适合需要搭建bmv2软件交换机与p4c编译工具链的高校学生、网络实验课程学习者及SDN方向研发人员。包内汇集behavioral-model、gmock-1.7.0、p4c、protobuf-3.2.0、thrift-0.9.2等常用组件&#xff0…

作者头像 李华
网站建设 2026/10/29 4:59:25

安卓自定义属性实战:从GradientButton设计到商用组件封装

1. 项目概述&#xff1a;为什么“自定义属性”是安卓开发绕不开的硬功夫你有没有遇到过这样的场景&#xff1a;写了一个漂亮的圆形进度条控件&#xff0c;同事想在布局里直接用app:progress"75"设置初始值&#xff0c;结果报错说这个属性不存在&#xff1f;或者你封装…

作者头像 李华