ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

SSM框架实战:网络投票管理系统从设计到实现全解析

SSM框架实战:网络投票管理系统从设计到实现全解析 开头先说实话这类ssm 网络投票管理系统的题目在毕业设计和课程设计里出现频率非常高我每年都会被问起好几次。它不是什么高深莫测的项目但胜在技术栈经典、业务逻辑完整、可扩展性强特别适合用来检验你对 SSM 框架的真正掌握程度。这篇文章我会从一个实际做过这套系统的开发者角度把从设计到实现到填坑的全过程都梳理一遍包括数据库怎么建、投票防刷怎么做、结果统计怎么写 SQL、遇到中文乱码和 Mapper 报错怎么排查把这些实战细节一次性讲透。1. 项目整体设计与技术选型思路1.1 为什么选SSM三件套很多同学一上来就纠结要不要直接用 Spring Boot我的观点很明确如果你是为了毕业设计或者课程项目SSMSpring SpringMVC MyBatis反而是更好的选择。原因有两个。第一SSM 是 Java Web 教学体系里的经典组合从 Servlet 到 SpringMVC 再到 Spring Boot 是一条完整的学习路径面试官和答辩老师天然认可这套技术栈的含金量。第二SSM 需要你手动配置各种 XML 和注解这个麻烦的过程恰恰能逼着你搞清楚请求是怎么进来的、对象是怎么注入的、SQL 是怎么执行的而不是像 Spring Boot 那样一键启动出了问题反而一头雾水。具体到三个组件的分工Spring 管对象IoC 容器和事务SpringMVC 管请求分发和参数绑定MyBatis 管数据库操作。这种各司其职的结构特别适合投票系统这种业务边界清晰的场景。1.2 三层架构怎么拆SSM 项目的标准分层是表现层Controller、业务层Service、持久层Mapper这三层在投票系统里对应得非常自然Controller层接收前端请求、校验参数、调用 Service、返回视图或 JSON 数据。比如VoteController负责处理投票查看结果等接口。Service层封装业务规则比如判断用户是否投过票更新投票结果校验投票时间是否在有效期内。这里的代码是整个系统的核心逻辑所在。Mapper层DAO层只负责和数据库打交道通过 MyBatis 的接口和 XML 映射文件执行 SQL。这种分层最大的好处是职责单一。比如你以后想把投票结果从 MySQL 迁到 Redis只需要改 Service 层的实现Controller 和页面前端完全不用动。我见过很多项目把 SQL 直接写在 Controller 里一开始确实省事后面改需求的时候那叫一个痛苦。1.3 功能模块怎么划分投票管理系统的功能模块通常分成前台和后台两大块前台用户端用户注册、登录、退出浏览所有投票主题列表查看投票详情、参与投票查看投票实时结果后台管理端管理员登录投票主题的增删改查投票选项的管理添加、删除选项控制投票的开始/结束状态查看统计数据和图表这样划分下来整个项目就非常清晰了前台是给投票用户用的后台是给管理员用的两者通过登录角色的权限来区分。这也对应了标题里网络投票管理系统的核心关键词——既能投又能管。2. 数据库设计与核心表结构2.1 四张核心表怎么画数据库设计是整个系统的地基地基没打好后面写代码全是坑。投票系统再怎么复杂核心表就四张用户表、投票主题表、投票选项表、投票记录表。用 MySQL 建表的实际代码大概长这样-- 用户表 CREATE TABLE t_user ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT 用户ID, username VARCHAR(50) NOT NULL UNIQUE COMMENT 用户名, password VARCHAR(100) NOT NULL COMMENT 密码(加密存储), nickname VARCHAR(50) COMMENT 昵称, role TINYINT DEFAULT 0 COMMENT 角色: 0普通用户 1管理员, create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 注册时间 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 投票主题表 CREATE TABLE t_subject ( 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 状态: 0未开始 1进行中 2已结束, create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 投票选项表 CREATE TABLE t_option ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT 选项ID, subject_id INT NOT NULL COMMENT 所属主题ID, content VARCHAR(255) NOT NULL COMMENT 选项内容, vote_count INT DEFAULT 0 COMMENT 得票数, sort_order INT DEFAULT 0 COMMENT 排序 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 投票记录表 CREATE TABLE t_record ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT 记录ID, subject_id INT NOT NULL COMMENT 投票主题ID, option_id INT NOT NULL COMMENT 选择的选项ID, user_id INT COMMENT 投票用户ID, ip VARCHAR(50) COMMENT 投票IP, create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 投票时间 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这套表结构是我实际在用且验证过没有大问题的方案。特别注意两点一是字符集用utf8mb4而不是utf8否则遇到生僻字或者特殊符号会乱码甚至报错二是所有表都用 InnoDB 引擎因为后面要做事务控制和行级锁MyISAM 做不到。2.2 防重复投票的库表设计网络投票系统最容易被人吐槽的就是刷票。虽然做互联网级别的防刷要上验证码、手机验证、设备指纹之类的方案但作为课程设计级别的项目至少在数据库层面就要从设计上杜绝最简单的重复投票。方案很直接在t_record表上给user_id和subject_id加一个联合唯一索引。这样同一个用户对同一个投票主题数据库层面就只允许插入一条记录直接从源头拦住重复投票。ALTER TABLE t_record ADD UNIQUE KEY uk_user_subject (user_id, subject_id);这个索引加上之后就算你的 Service 层忘记做判断数据库也会抛一个 DuplicateKeyException 来拦截相当于双保险。我测试过用这个方案可以挡住 99% 的误操作重复点击也让答辩的时候多一个可以说的技术亮点。2.3 状态字段与时间戳处理投票系统的核心业务规则是只能投进行中的投票。这个规则我建议用状态字段和时间判断双重控制status字段由管理员在后台手动控制创建投票时默认是未开始管理员点击开始投票后变成进行中点击结束投票后变成已结束。start_time和end_time作为自动兜底前端展示时用它们判断投票是否在有效期内。为什么要双重控制因为有些场景管理员需要提前结束投票比如发现有人刷票如果把状态完全交给时间管理员就没有干预手段了。反过来如果只靠管理员手动点万一忘了点开始用户就永远投不了票。两个机制配合既灵活又可靠。实际开发中我会在 Service 层写一个VoteStatusUtil的校验方法投票前统一判断当前时间是否在时间区间内且status 1。3. 核心功能实现细节与关键代码3.1 投票主流程与防重判断投票这个动作是前端的用户在页面选择一个选项点击投票按钮后端要完成的操作校验用户是否登录、校验投票是否正在进行、校验用户是否已经投过、更新选项票数、写入投票记录。其中校验用户是否已经投过这一步我用了两种方式配合第一种是查询法。在投票前先执行一条 select 语句查t_record表看当前用户对当前主题有没有记录。如果存在就直接返回您已经投过票。public boolean hasVoted(Integer userId, Integer subjectId) { RecordQuery query new RecordQuery(); query.setUserId(userId); query.setSubjectId(subjectId); return recordMapper.selectCount(query) 0; }第二种是数据库索引法。即使代码逻辑漏掉了第一步数据库的唯一索引也会抛异常由全局异常处理器统一捕获并转为提示信息。我之前重点强调的联合唯一索引在这里就能派上关键用场了。3.2 结果统计的 SQL 实现投票结果统计这块最核心的 SQL 是统计每个选项的得票数两种写法各有适用场景写法一直接查选项表的 vote_count 字段SELECT id, content, vote_count FROM t_option WHERE subject_id #{subjectId} ORDER BY sort_order;这种写法逻辑最简单——投票成功时在事务里对该选项的vote_count字段做1更新。适合投票并发量不大的场景查询也很快。写法二从投票记录表实时统计SELECT option_id, COUNT(*) AS cnt FROM t_record WHERE subject_id #{subjectId} GROUP BY option_id;这种写法更正宗因为数据源头是每一条真实的投票记录统计结果绝对准确。但数据量大时 GROUP BY 的查询压力会上升通常需要配合索引使用。我实际开发中采用了折中方案t_option.vote_count作为展示用的冗余字段t_record作为数据底账。投票时用事务同时更新两个地方事务保证了它们的强一致性。如果发现统计结果对不上账跑一条 SQL 用方案二重新覆盖方案一的字段值就行了数据恢复非常方便。3.3 图形化展示方案投票结果不能只会用数字列表展示图形化展示会让系统观感好得多也是一个加分项。最简单的方案是直接用纯 CSS 绘制百分比条形图不需要引入任何第三方库每个选项一行宽度根据票数占比动态计算。后端把每个选项的得票数和总票数传给前端模板模板里拼接宽度百分比。另一种方案是用 ECharts 画饼图或柱状图。ECharts 支持直接用 AJAX 加载 JSON 数据接口返回格式设计成[ {name: 选项A, value: 123}, {name: 选项B, value: 456}, {name: 选项C, value: 78} ]页面用$.ajax拿数据后直接填充到 ECharts 的series.data里。前端代码量很少。个人建议如果用 ECharts页面初始化的时候加载一次投票成功后再用 JS 局部刷新图表数据这样用户不用刷新页面就能看到实时结果。3.4 后台管理的权限控制后台管理功能做起来不难但很容易忽视谁有资格访问后台。如果任何人都能直接访问/admin/subject/add那这系统就毫无安全性可言了。我的方案是写一个简单的拦截器Interceptor来处理权限public class AdminInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session request.getSession(); User user (User) session.getAttribute(loginUser); if (user ! null user.getRole() 1) { return true; } response.sendRedirect(request.getContextPath() /loginPage); return false; } }在 SpringMVC 配置里注册拦截器并配置只拦截/admin/**路径。mvc:interceptors mvc:interceptor mvc:mapping path/admin/**/ bean classcom.xxx.interceptor.AdminInterceptor/ /mvc:interceptor /mvc:interceptors这样把管理后台和前台完全隔离答辩的时候你可以详细讲讲这个安全设计的逻辑——为什么不用 Filter 而用拦截器、拦截器里为什么用getRole() 1而不是写死用户名这些都是加分项。4. 核心配置与骨架代码实战4.1 配置文件的几个重点SSM 项目配置文件多很容易在配置上栽跟头。我把关键配置梳理一下照着写基本就不会出问题web.xml需要配置 ContextLoaderListener加载 Spring 容器、DispatcherServletSpringMVC 前端控制器、字符编码过滤器。字符编码过滤器一定要配在 DispatcherServlet 之前否则请求参数会乱码filter filter-nameencodingFilter/filter-name filter-classorg.springframework.web.filter.CharacterEncodingFilter/filter-class init-param param-nameencoding/param-name param-valueUTF-8/param-value /init-param init-param param-nameforceEncoding/param-name param-valuetrue/param-value /init-param /filter filter-mapping filter-nameencodingFilter/filter-name url-pattern/*/url-pattern /filter-mappingforceEncoding设置为 true 非常关键它表示不管请求还是响应全部强制使用 UTF-8 编码能避免很多诡异的中文乱码问题。spring-mvc.xml里需要开启注解驱动mvc:annotation-driven/、配置视图解析器InternalResourceViewResolver指定前缀/WEB-INF/views/和后缀.jsp、放行静态资源mvc:default-servlet-handler/或mvc:resources。静态资源这块特别容易漏漏了之后 CSS、JS 全部加载不出来页面样式全丢被问起来又找不到原因。applicationContext.xmlSpring 核心配置里配置数据源、事务管理器并扫描com.xxx.service包和 mapper 接口。这里有个细节Spring 容器扫描的包不要包含com.xxx.controllerController 层交给 SpringMVC 容器管而 SpringMVC 容器只扫描com.xxx.controller包。这样可以避免容器扫描范围混乱导致事务失效的问题。4.2 MyBatis Mapper 的两种实现方式MyBatis 有两种写法基于注解的和基于 XML 映射文件的。对于投票系统这种 SQL 比较简单的项目我推荐使用注解方式代码量少、直观。public interface SubjectMapper { Select(SELECT * FROM t_subject WHERE status 1) ListSubject selectActiveList(); Insert(INSERT INTO t_subject(title, description, start_time, end_time, status) VALUES(#{title}, #{description}, #{startTime}, #{endTime}, #{status})) Options(useGeneratedKeys true, keyProperty id) int insertSubject(Subject subject); }Options(useGeneratedKeys true, keyProperty id)是一个关键注解表示插入数据后把数据库自动生成的自增 ID 回填到实体对象的id字段。如果你要插入主题后再插入选项就必须拿到这个主题 ID 作为t_option.subject_id的外键值。如果 SQL 比较复杂比如多表联查再用 XML 方式单独处理两种方式可以共存。MyBatis 会自动扫描 Mapper 接口并加载同名 XML 文件只要名字一致、namespace 对应得上就行。4.3 Service 层的事务控制投票功能涉及更新投票数 插入投票记录两步数据库操作这两步必须在一个事务里要么都成功要么都失败否则就会出现票数和记录对不上的脏数据。Spring 声明式事务用Transactional注解一行搞定Transactional(rollbackFor Exception.class) public VoteResult vote(Integer userId, Integer subjectId, Integer optionId) { // 1. 校验投票是否有效 // 2. 更新 t_option 的 vote_count // 3. 插入 t_record 记录 }rollbackFor Exception.class表示任何异常都触发回滚。注意不要把rollbackFor省略掉否则默认只回滚 RuntimeException遇到受检异常事务不会回滚数据就出问题了。另外有一个很隐蔽的坑同一个类内部调用带Transactional的方法事务会失效因为 Spring 的事务是通过动态代理实现的内部调用不走代理。如果你要用事务就必须让 Controller 调用 Service 的公开方法而不要在 Service 里自己调自己。这个坑我当年踩过查了半天才发现。5. 常见问题与排查技巧实录5.1 中文乱码乱码问题在 SSM 项目里太经典了。表现形式多种多样可能是页面请求参数乱码、数据库存进去乱码、页面显示乱码。我提供一个最有效的排查顺序先看数据库连接串是否指定了编码jdbc:mysql://localhost:3306/vote_db?useUnicodetruecharacterEncodingutf8再看 web.xml 里的 CharacterEncodingFilter 是否配置且forceEncodingtrue并确认它过滤了/*所有请求。检查数据库表和字段的字符集上面建表 SQL 里已经写了utf8mb4如果你在用工具建表记得留意一下默认字符集。这三点排查完95% 的乱码都能解决。5.2 Mapper 绑定异常报错信息类似Invalid bound statement (not found): com.xxx.mapper.SubjectMapper.selectActiveList。这个问题通常是 Mapper 接口和 XML 映射文件没对上或者 XML 文件没被扫描到。排查思路确认 XML 文件中的namespace和接口全限定名一致。确认 XML 文件中的id和接口方法名一致。确认 Spring 配置中mapperLocations指向了正确的 XML 路径比如classpath:mapper/*.xml。如果用的是 Maven 项目还需要在 pom.xml 中配置资源路径让 Maven 把src/main/java下的 XML 也打包进 classpathbuild resources resource directorysrc/main/java/directory includes include**/*.xml/include /includes /resource /resources /build这个报错 90% 都是资源没打入 classpath 导致的把上面的配置加上一般就好了。5.3 数据库连接失败报错通常是Access denied for user rootlocalhost或者Communications link failure。前者是用户名密码错误或权限问题检查applicationContext.xml里数据源的账号密码配置后者是数据库服务没有启动或者连接串的地址端口不对用命令行mysql -u root -p测试能不能连。我用过一个工具叫 MySQL Workbench 或者 Navicat先在图形界面里连一遍能连上说明驱动和地址没问题再排查项目配置。5.4 静态资源 404 怎么办如果你用了/WEB-INF/views/作为视图前缀那/WEB-INF目录下的静态资源无法直接被浏览器访问必须在 spring-mvc.xml 里配置资源映射mvc:resources mapping/static/** location/static//然后在页面里用${pageContext.request.contextPath}/static/css/style.css这种方式引入。加${pageContext.request.contextPath}是为了带上项目部署路径否则项目不是部署在根路径时资源路径全部乱掉。6. 几个让系统更完整的扩展建议写完这个基础版投票系统后你完全可以再往上加功能提升项目的完整度和答辩的说服力图表统计扩展从简单的数字显示升级为 ECharts 饼图、柱状图甚至按时间段投票趋势的折线图。邮件通知功能投票结束后系统自动发邮件给管理员用 Spring 的 JavaMailSender 就能实现。验证码集成登录或投票时加入图片验证码防止简单的脚本刷票也让系统看上去更完善。拦截器优化把登录后跳转回原页面的逻辑做上提升用户操作体验。这些功能每个单独拿出来都可以写成一个小的技术点在答辩时作为系统的亮点展示尤其能体现你对投票防刷数据统计这些核心难点的思考。回到最初的话题ssm 网络投票管理系统这个题目为什么常青因为它把 Java Web 开发的核心知识点全串起来了框架整合、数据库设计、事务控制、拦截器、前端交互。认真做好这一个项目你收获的绝对不只是做完一个毕业设计这么简单而是对 Java Web 整个技术体系的一次完整实践。过程中踩过的每一个坑调过的每一个 bug都是后面工作或者下一个项目里实实在在的经验。
返回列表