简介:这是一份采用B/S架构的网络投票系统完整源码包,面向Web开发初学者、毕业设计选题者以及需要快速部署在线投票平台的管理员。系统将业务逻辑与数据存储集中在服务器端,用户只需浏览器即可完成投票操作,设计上支持100以内的并发访问,兼顾了运行效率与维护简便性;管理员可通过后台创建投票主题、维护选项并调整系统配置,权限控制清晰,适合学习三层架构的经典实现。资源共56个文件,主要包含26个ASP动态页面(覆盖投票创建、选项编辑、系统配置、登录认证等后台功能)、14个GIF交互图标、4个TXT安装说明,以及CSS样式表、DLL组件和Access数据库文件,类型齐全,便于对照理解功能模块的划分。整个压缩包仅116KB,轻量易用,附有安装说明和功能简介,可直接部署用于小型在线投票场景,也可作为ASP+Access开发的参考范例。已有197人学习下载,对快速上手B/S系统开发具有不错的借鉴价值。
1. B/S方式网络投票系统:一个完整的Web投票全流程解决方案
在做网络投票系统之前,我一度以为投票应用不过是“一个表单加一个计数器”的小儿科。直到某个活动里需要支持多选项、限制刷票、按主题创建投票、后台实时看统计时,才发现从页面到服务端再到数据库,每一环都得单独设计。这个B/S方式的网络投票系统,正好覆盖了从用户注册登录、创建投票、参与投票、结果统计到后台管理的完整闭环,适用于课程设计、毕业设计,也适合直接改造成公司内部评优评先、社团换届选举的轻量工具。浏览器访问、服务端统一管理,不需要给每个使用者安装客户端,这正是B/S架构在投票这类多人访问场景下的核心优势。
2. 架构与目录拆解:三层模型、三张业务表和一份完整文件清单
2.1 技术选型:为什么是JSP加Servlet而不是Spring Boot
给这类项目定技术栈,我一般先考虑“谁能最快跑通”。JSP加Servlet是B/S架构里最经典、也是各大院校实验环境最常预装的一套组合,它不需要额外的Spring容器,请求从浏览器到Servlet再到JSP,链路短、直观。用Spring Boot当然更现代,但对一个以投票业务为主的小项目来说,Spring的依赖注入、自动配置反而成了需要先理解的黑匣子。
我见过不少同学一上来就选Spring Boot,结果卡在Maven依赖冲突上,连着两天起不来服务。换成JSP加Servlet之后,把WAR包丢进Tomcat就能跑。这个项目里我们用Servlet处理控制逻辑、JSP做页面展示、JavaBean封装业务数据,是经典的三层结构,分层清楚,后期想往Spring Boot迁移,业务代码里的DAO层和Service方法也能直接搬运。
2.2 三层架构:浏览器、Web服务器、数据库各自管什么
整个系统遵循标准B/S三层模型。浏览器层只负责渲染页面和收集用户操作,把HTTP请求发给Tomcat;Web服务层是Tomcat里跑的JSP和Servlet,JSP负责把Java数据渲染成HTML,Servlet负责接收请求、调用业务方法、控制页面跳转;数据层是MySQL,通过JDBC连接,由DAO类完成增删改查。
| 层次 | 技术 | 职责 |
|---|---|---|
| 表现层 | JSP、HTML、CSS、JavaScript | 页面渲染与用户交互 |
| 控制层 | Servlet | 接收请求、参数校验、业务调度 |
| 业务层 | JavaBean / Service | 投票规则、防重复校验、统计聚合 |
| 数据层 | MySQL + JDBC / DAO | 用户、投票、选项、记录持久化 |
以发起投票为例,用户在后台页面填写投票标题和选项,点提交后请求到AddVoteServlet,这个Servlet调用VoteService的addVote方法,方法里先创建投票主记录、再批量插入选项记录,全部成功才提交事务。三层各做各的事,任何一层出问题,日志里能直接定位到对应类名。
2.3 数据库设计:四张表如何撑起投票闭环
投票系统的核心数据模型是“主题—选项—记录”的三级结构。我设计了四张表:用户表tb_user存登录账号,投票主题表tb_vote存投票名称、创建人和有效时间,选项表tb_option存每个主题下有哪些选项,投票记录表tb_vote_record存谁在什么时候选了哪个选项。
CREATE TABLE tb_user ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT '用户ID', username VARCHAR(50) NOT NULL UNIQUE COMMENT '登录名', password VARCHAR(64) NOT NULL COMMENT 'MD5加密后的密码', real_name VARCHAR(50) COMMENT '真实姓名', role TINYINT DEFAULT 0 COMMENT '0-普通用户 1-管理员', create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '注册时间' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE tb_vote ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT '投票ID', title VARCHAR(200) NOT NULL COMMENT '投票标题', description TEXT COMMENT '投票说明', start_time DATETIME COMMENT '开始时间', end_time DATETIME COMMENT '结束时间', status TINYINT DEFAULT 1 COMMENT '1-进行中 0-已结束', create_by INT COMMENT '创建人ID', CONSTRAINT fk_vote_user FOREIGN KEY (create_by) REFERENCES tb_user(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE tb_option ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT '选项ID', vote_id INT NOT NULL COMMENT '所属投票ID', option_text VARCHAR(200) NOT NULL COMMENT '选项内容', vote_count INT DEFAULT 0 COMMENT '得票数', CONSTRAINT fk_option_vote FOREIGN KEY (vote_id) REFERENCES tb_vote(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE tb_vote_record ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT '记录ID', vote_id INT NOT NULL COMMENT '投票ID', option_id INT NOT NULL COMMENT '所选选项ID', user_id INT NOT NULL COMMENT '投票人ID', ip_address VARCHAR(50) COMMENT '投票IP', vote_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '投票时间', CONSTRAINT fk_record_option FOREIGN KEY (option_id) REFERENCES tb_option(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;这四张表的关联关系不复杂:tb_vote是主表,tb_option通过vote_id挂在主题下面,tb_vote_record同时记录vote_id和option_id,可以查到某次投票、某个选项的完整参与记录。外键约束保证了引用完整性,比如删除投票主题时,如果下面还有选项记录,数据库会报错而不允许直接删。
得票数没有实时从记录表统计,而是维护了vote_count这个冗余字段,每次投票时加一。这样结果页查询时不用count整张记录表,数据量大时响应更快,但代价是必须保证记录表和计数一致,所以写入代码里事务就很重要,后面第3章会细讲。
注意:如果业务要求完全匿名投票,可以把tb_vote_record的user_id允许置空,只靠ip_address做设备维度识别。是否需要userId做外键,取决于“要不要审计谁投了票”。
2.4 工程目录结构:每个文件夹该放什么
拿到源码先看目录结构,是理解一个工程最快的方式。这个项目是标准Java Web工程布局,源码包解压后主要目录如下:
VoteSystem/ ├── src/ │ ├── com.vote.entity/ # 实体类:User、Vote、VoteOption、VoteRecord │ ├── com.vote.dao/ # DAO层:对数据库的增删改查封装 │ ├── com.vote.service/ # 业务层:投票逻辑、防重复校验 │ ├── com.vote.servlet/ # Servlet:LoginServlet、VoteServlet等 │ └── com.vote.util/ # 工具类:DBUtil连接、MD5Util加密 ├── WebContent/ │ ├── index.jsp # 登录页 │ ├── register.jsp # 注册页 │ ├── vote_list.jsp # 投票列表页 │ ├── vote_detail.jsp # 投票详情与操作页 │ ├── result.jsp # 结果统计页 │ ├── admin/ # 后台管理页面 │ │ ├── admin_index.jsp # 后台首页 │ │ ├── vote_add.jsp # 新建投票页面 │ │ └── vote_manage.jsp # 管理投票页面 │ ├── css/ # 样式文件 │ ├── js/ # 前端脚本 │ └── WEB-INF/ │ ├── web.xml # Servlet映射与应用配置 │ └── lib/ # 依赖Jar包(MySQL驱动、JSTL等) └── sql/ └── init.sql # 建库建表与初始数据脚本src下的包结构按职责划分,实体类对应四张表,每个实体类的字段和表字段一一对应。DAO层每个实体一个类,比如VoteDao集中了insertVote、selectVoteById、updateVoteStatus等方法。Servlet层是控制入口,每个页面操作对应一个Servlet,web.xml里做URL映射。
lib目录是整个工程能跑起来的关键。mysql-connector-java驱动如果不放在WEB-INF/lib下,开发环境编译不报错,但Tomcat一运行就抛ClassNotFoundException。很多同学在Eclipse里明明能看到驱动包,部署后却报错,原因就是驱动只在构建路径里,没有跟着WAR包一起发布。
3. 核心模块拆解:一次投票从点击到落库的完整链路
3.1 登录模块:Session管理与密码加密是怎么配合的
登录是整个系统的入口。LoginServlet接收页面提交的用户名和密码,先用MD5Util把明文密码加密,再交给UserDao去数据库比对。比对成功之后,把User对象放进Session,同时判断role字段是不是1,是管理员就重定向到后台首页,否则进普通用户的投票列表页。
LoginServlet的doPost核心代码:
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding("UTF-8"); String username = request.getParameter("username"); String password = request.getParameter("password"); // 明文密码做MD5后再去数据库比较 String encryptedPwd = MD5Util.getMD5(password); UserDao userDao = new UserDao(); User user = userDao.findByUsernameAndPassword(username, encryptedPwd); if (user != null) { // 登录成功,用户信息写入Session HttpSession session = request.getSession(); session.setAttribute("loginUser", user); if (user.getRole() == 1) { response.sendRedirect(request.getContextPath() + "/admin/admin_index.jsp"); } else { response.sendRedirect(request.getContextPath() + "/vote_list.jsp"); } } else { request.setAttribute("errorMsg", "用户名或密码错误"); request.getRequestDispatcher("/index.jsp").forward(request, response); } }逻辑说明:这段代码有两个容易被忽略的点。第一,setCharacterEncoding("UTF-8")必须写在读取任何参数之前,不然POST请求里的中文用户名会乱码;第二,登录成功后用户信息是放Session而不是放request,因为Session跨多个请求存活,request在一次转发后就结束了。后续页面从Session里取loginUser,取不到就跳回登录页。
参数说明:username和password对应登录表单里两个输入框的name值。findByUsernameAndPassword返回User对象,为null说明账号或密码不匹配。getMD5是工具类方法,把用户输入转成32位十六进制串,和数据库里存的一致才能比对通过。
密码用MD5只是课设层面的常规做法,生产环境建议换BCrypt加盐。这个模块是我最早写完的,后来想补加密方案时发现改动会涉及注册、登录、改密三个位置,就暂时保持了MD5,改造时优先考虑替换。
3.2 投票主流程:页面展示、参数校验、事务写入三个步骤
投票是核心操作。用户在vote_detail.jsp看到投票标题、选项和截止时间,选中一个选项点提交,请求到VoteServlet。VoteServlet不直接操作数据库,而是调到VoteService,由Service层完成校验和写入,这样Servlet只做流程控制。
VoteServlet的doPost方法:
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding("UTF-8"); String voteIdStr = request.getParameter("voteId"); String optionIdStr = request.getParameter("optionId"); if (voteIdStr == null || optionIdStr == null) { response.sendRedirect(request.getContextPath() + "/vote_list.jsp"); return; } int voteId = Integer.parseInt(voteIdStr); int optionId = Integer.parseInt(optionIdStr); User loginUser = (User) request.getSession().getAttribute("loginUser"); if (loginUser == null) { response.sendRedirect(request.getContextPath() + "/index.jsp"); return; } VoteService voteService = new VoteService(); // 返回 1-成功 2-重复投票 3-投票已结束 int result = voteService.vote(voteId, optionId, loginUser.getId(), getIpAddr(request)); if (result == 1) { response.sendRedirect(request.getContextPath() + "/result.jsp?voteId=" + voteId); } else if (result == 2) { request.setAttribute("tipInfo", "您已经参与过该投票,不可重复提交"); request.getRequestDispatcher("/vote_detail.jsp?voteId=" + voteId).forward(request, response); } else { request.setAttribute("tipInfo", "该投票已结束"); request.getRequestDispatcher("/vote_detail.jsp?voteId=" + voteId).forward(request, response); } }逻辑说明:Servlet层只做三件事——接收参数、确认登录状态、根据业务返回值决定页面跳转。真正的防重复逻辑在VoteService里,因为投票成功的标准不单是写进一条记录,还包括在并发场景下不能写进两条,这需要事务和唯一约束配合。
getIpAddr是工具类里的方法,用request.getRemoteAddr()取客户端IP,直连Tomcat的情况下够用。如果以后接Nginx做反向代理,这个方法要改从X-Forwarded-For请求头里取,不然拿到的一律是Nginx的IP。
3.3 防重复投票:Service层的双重校验与事务控制
vote方法在Service层做了两个层面的校验。第一层查投票记录表里有没有相同vote_id和user_id的记录,有就返回重复投票;第二层查投票主题的status和end_time,不在有效期内直接拒绝。把状态校验放在业务代码里而不是SQL里,是为了能返回不同提示信息,让前端区分“重复投票”和“已结束”两种场景。
VoteService.vote方法:
public int vote(int voteId, int optionId, int userId, String ip) { Connection conn = null; try { conn = DBUtil.getConnection(); // 开启事务,防止投票记录和计数更新不同步 conn.setAutoCommit(false); VoteDao voteDao = new VoteDao(); VoteRecordDao recordDao = new VoteRecordDao(); // 第一步:查投票状态 Vote vote = voteDao.selectById(voteId); if (vote == null || vote.getStatus() == 0) { return 3; // 投票不存在或已结束 } // 判断是否在有效时间范围内 Date now = new Date(); if (vote.getEndTime() != null && now.after(vote.getEndTime())) { return 3; // 已过截止时间 } // 第二步:查当前用户是否已投过 VoteRecord existRecord = recordDao.selectByVoteIdAndUserId(voteId, userId); if (existRecord != null) { return 2; // 重复投票 } // 第三步:插入投票记录 VoteRecord record = new VoteRecord(); record.setVoteId(voteId); record.setOptionId(optionId); record.setUserId(userId); record.setIpAddress(ip); recordDao.insert(record); // 第四步:对应选项得票数加一 OptionDao optionDao = new OptionDao(); optionDao.increaseCount(optionId); conn.commit(); // 全部成功才提交 return 1; } catch (Exception e) { try { if (conn != null) conn.rollback(); // 异常回滚 } catch (SQLException ex) { ex.printStackTrace(); } e.printStackTrace(); return 0; // 系统内部错误 } finally { DBUtil.close(conn); } }逻辑说明:手动事务控制是这个方法的关键。插入投票记录和选项计数加一是两步写操作,不放在同一事务里,就可能出现记录写进去了、计数没更新,结果页的票数和实际记录对不上。先查再插的校验在单机部署下够用,一旦并发上来,两个请求同时查到“未投过”,然后又同时插入,就会出现重复记录。
提示:对生产环境,建议在tb_vote_record表上建(vote_id, user_id)联合唯一索引,让数据库在约束层面兜底。业务校验挡常规操作,唯一索引挡并发异常,两层都上才稳。
参数说明:voteId和optionId来自前端表单字段,userId从Session里loginUser.getId()取,ip是工具方法解析的客户端地址。调用方拿到返回值后按1、2、3分别处理,0通常不用单独页面,打印异常日志后提示用户稍后重试。
还有一个细节:increaseCount的SQL写的是“UPDATE tb_option SET vote_count = vote_count + 1 WHERE id = ?”,而不是先SELECT再UPDATE。原子自增语句在并发时不会丢更新,这个小习惯能少踩不少数据不一致的坑。
3.4 结果统计:一条聚合SQL加一个纯前端柱状图
结果页没有把统计逻辑做得太重,ResultServlet查出每个选项的得票数和总票数,JSP用循环渲染百分比。一个主题最多十几个选项,没必要引图表库,用CSS宽度模拟柱状图加载更快,代码也更好懂。
ResultServlet的查询逻辑:
public List<VoteOption> listOptionsWithCount(int voteId) { List<VoteOption> list = new ArrayList<VoteOption>(); Connection conn = null; PreparedStatement ps = null; ResultSet rs = null; try { conn = DBUtil.getConnection(); String sql = "SELECT o.id, o.option_text, o.vote_count, " + "(SELECT COUNT(*) FROM tb_vote_record r WHERE r.vote_id = ?) AS total " + "FROM tb_option o WHERE o.vote_id = ? ORDER BY o.vote_count DESC"; ps = conn.prepareStatement(sql); ps.setInt(1, voteId); ps.setInt(2, voteId); rs = ps.executeQuery(); while (rs.next()) { VoteOption option = new VoteOption(); option.setId(rs.getInt("id")); option.setOptionText(rs.getString("option_text")); option.setVoteCount(rs.getInt("vote_count")); option.setTotalVotes(rs.getInt("total")); list.add(option); } } catch (Exception e) { e.printStackTrace(); } finally { DBUtil.close(rs, ps, conn); } return list; }逻辑说明:子查询SELECT COUNT(*)算这个投票主题的总票数,同一个查询里把每个选项的得票数和总票数都取出来,JSP就能直接算百分比。按vote_count降序排列,得票最高的选项排最前。单条投票上万条记录时,子查询COUNT会有一定开销,到时可以改成在Service层先用独立查询算总数,再查选项列表。
JSP端渲染柱状图:
<c:forEach var="option" items="${optionList}"> <div class="option-row"> <span class="option-text">${option.optionText}</span> <div class="bar-wrapper"> <div class="bar-fill" style="width: ${option.voteCount / option.totalVotes * 100}%"> </div> </div> <span class="option-count">${option.voteCount}票</span> </div> </c:forEach>逻辑说明:c:forEach遍历选项列表,optionText、voteCount、totalVotes是Java代码里set进去的字段。bar-fill宽度用百分比,总票数为0时会出现除以零,我一般在Servlet里对totalVotes做最小值1的兜底。EL表达式的key必须和setAttribute传进来的key完全一致,大小写不同页面上就是空白,这个坑排查起来最费时间。
4. 部署与避坑:从JDK配置到Tomcat启动的全过程记录
4.1 环境准备:版本搭配建议与关键配置项
这个项目基于Java 8开发,配套Tomcat 8.5和MySQL 5.7是最省事的组合。JDK 8和Tomcat 8.5的兼容性非常成熟,不会遇到类文件版本错误。MySQL 8.0也能用,但要换8.x驱动,连接串加serverTimezone=Asia/Shanghai参数,否则报时区错误。
| 组件 | 推荐版本 | 注意事项 |
|---|---|---|
| JDK | 1.8 | 别用JDK 17以上,Java EE类库兼容性风险大 |
| Tomcat | 8.5.x | 解压版不用安装,设置CATALINA_HOME即可 |
| MySQL | 5.7 | 8.0需要换驱动并配置时区 |
| IDE | Eclipse Java EE版 | 自带Web部署工具,导入工程最省事 |
环境变量方面,JAVA_HOME指向JDK目录,CATALINA_HOME指向Tomcat解压目录,命令行执行java -version能正常输出就算过。如果本机装了多个JDK,检查PATH里最先命中哪个版本。我有一次部署时PATH指向了JDK 17,Tomcat 8.5直接启动失败,报的是UnsupportedClassVersionError,错误信息跟类名完全对不上,排查半天才发现是JDK版本问题。这是血泪经验,部署前先确认java -version再动手。
4.2 数据库初始化:导入SQL脚本与配置JDBC连接
拿到源码后,先找到sql/init.sql,在MySQL命令行或Navicat里执行。脚本包含建库和建表,如果重复执行会在建表那一步报Table already exists,我习惯在脚本开头加DROP TABLE IF EXISTS,多次执行不报错。源码包自带的脚本如果没有这句,自己补上就行。
接着在工程里找到数据库连接配置文件,一般是src目录下db.properties:
jdbc.driver=com.mysql.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/vote_db?useUnicode=true&characterEncoding=utf8 jdbc.username=root jdbc.password=123456参数说明:数据库装在远程服务器时,localhost改成服务器IP;password改成你本机MySQL的root密码。characterEncoding=utf8必须保留,否则页面写入的中文到数据库全变问号。JDBC连接通过DBUtil工具类封装,静态代码块里加载驱动、根据properties创建连接,工程里所有DAO都走这个类。如果改了密码不生效,先看编译输出目录里的properties是不是旧内容,有时候IDE没同步刷新,跑的还是上一份配置。
4.3 部署到Tomcat运行:一个完整的启动检查清单
在Eclipse里导入工程时选Import → Existing Projects into Workspace,确认WebContent被识别为Web根目录。右键工程选Run On Server,选择配置好的Tomcat 8.5,Eclipse会自动编译、部署到Tomcat的webapps目录并启动。
启动成功后浏览器访问http://localhost:8080/VoteSystem/,第一次进入是登录页。这里要提醒:URL里的VoteSystem大小写必须和WAR包名或工程名完全一致,Windows本地大小写不敏感,一旦部署到Linux服务器,大小写不一致就404,这是个非常隐蔽的问题。
启动后建议做两个验证。第一个是直接访问一个Servlet映射地址,比如http://localhost:8080/VoteSystem/LoginServlet,看请求能不能被web.xml正确路由;第二个是注册一个测试账号并发起投票,验证整个链路通没通。只打开首页不算跑通,投票能走完一轮才是真的没问题。
4.4 常见问题与避坑记录
这一节列出的问题,全是我在本地和服务器上实际踩过的,每条都按现象、原因、解决的顺序写,排错时直接对照。
问题一:启动时报ClassNotFoundException: com.mysql.jdbc.Driver
现象:Tomcat启动时或访问任意页面抛这个异常,找不到MySQL驱动类。 原因:mysql-connector-java.jar没放进WEB-INF/lib,只在IDE构建路径里。Eclipse编译能过,Tomcat运行时找不到。 解决:把Jar包复制到WebContent/WEB-INF/lib下,刷新工程,重新部署。
问题二:页面中文乱码,数据库里全是问号
现象:用户名、投票标题在浏览器正常,数据库里显示为????。 原因:连接串缺characterEncoding=utf8,或者数据库/表字符集不是utf8mb4。 解决:检查db.properties是否带编码参数,同时确认建库语句里有DEFAULT CHARSET=utf8mb4。两处有一处不对,中文都会变问号。
问题三:连接MySQL 8.0报CommunicationsException
现象:Java连不上数据库,报Communications link failure或时区错误。 原因:MySQL 8.0驱动类变成com.mysql.cj.jdbc.Driver,连接串必须指定serverTimezone。 解决:驱动换8.x,db.properties里jdbc.driver改成com.mysql.cj.jdbc.Driver,URL加上serverTimezone=Asia/Shanghai。
问题四:投票成功后票数没变
现象:vote_detail提交后提示成功,结果页数字还和之前一样。 原因:vote方法没提交事务,或increaseCount写成了先SELECT再UPDATE,并发时丢更新。 解决:业务方法里显式conn.commit(),UPDATE语句写成vote_count = vote_count + 1,提交后SELECT验证计数。
问题五:登录后跳转页面又被踢回登录页
现象:登录成功进了首页,点其他页面马上回到登录页,像session根本没生效。 原因:登录成功时用了request.setAttribute而不是session.setAttribute,或web.xml的session超时配得太短。 解决:用request.getSession().setAttribute保存登录态,检查session-config的timeout,默认30分钟不活动清掉是正常的。
5. 二次开发:四个改造方向让投票系统从课设走向生产
5.1 改造一:投票时间限制从硬编码变成配置化
现在的投票状态用status字段管理,需要管理员手动改。第一个改造是让列表页自动判断end_time,超过结束时间直接显示已结束。可以在列表查询SQL加一个动态条件,end_time小于当前时间的记录不展示,投票主题自动下架,不用人工干预。
5.2 改造二:加验证码抵御脚本刷票
防重复目前靠登录态加IP双重校验,脚本注册大量账号就能绕过去。改造成本最低的是在登录和投票两个入口加验证码。用java.awt包画一个简单数字验证码图片,CaptchaServlet生成后把答案存Session,提交时比对,脚本批量投的成本会高很多。
5.3 改造三:把Session从单机搬到Redis集群
单机部署Session没问题,如果用Nginx做负载均衡,请求分散到多个节点,Session就各自为政了。改造方向是引入Spring Session或自己写过滤器,把Session存进Redis,实现多节点共享。对课设来说这个改造偏重,但做线上投票活动时基本绕不开。
5.4 改造四:投票结果导出Excel
活动结束后,主办方通常要的是一份能存档的统计表而不是网页截图。后台加一个导出按钮,Servlet端用POI生成xls文件,把投票主题、选项得票数、投票记录明细写进单元格,设置Content-Type为application/vnd.ms-excel,浏览器自动下载。功能不复杂但使用频率很高。
我自己做第一次改造时,上来就加Redis和消息队列,结果部署环境根本撑不起来。后来和某导师聊过一次,他提醒我说投票系统这类业务,先分清核心功能和加分项,把防重复、时间控制、数据导出这三件套做扎实,就已经超过大多数课堂项目了。从那以后我拿到任何源码,都会先列一个改造优先级清单,强制自己只做与业务强相关的改动。希望这篇拆解对你跑通和改造这个B/S投票系统有帮助。
本文还有配套的精品资源,点击获取