带SSM框架做开发,很多人一开始是拒绝的——Spring Boot都出了这么多年了,怎么还在用SSM?但真到了课设、毕设这个场景,我反而觉得SSM是个特别合适的选择。它不是最潮的,但它足够经典、足够稳,网上资料多到看不完,出了问题也几乎都能搜到答案。这篇文章我会以"疫情背景下学生日报系统"为完整案例,把从需求分析、技术选型、数据库设计到功能实现、部署调试、论文写作的全过程都拆开讲一遍。篇幅会比较长,但每一段都值得看,尤其是后面"问题排查"那部分,基本都是我实际调试过程中踩过的坑。
1. 需求拆解与整体设计思路
1.1 学生日报系统到底在解决什么问题
特殊管理时期,高校辅导员最头疼的事情之一就是"信息收集"。每天要统计学生的健康状况、所在地、是否去过重点区域,光靠微信群接龙、Excel汇总,一天至少消耗两三个小时。某个几百人的学院,一上午可能就耗在催报和整理数据上了。
日报系统的核心价值,就是把这个重复性极高、容错率极低的工作,变成一套自动化的线上流程。学生登录后填报一次,后台自动汇总、自动提醒、自动统计,辅导员打开后台就能看到全院的上报情况。这不是什么高大上的技术,但它解决的问题非常实在。
从开发者的角度拆解这个系统,核心需求其实只有三类:
- 学生端:登录后填写每日健康状况、所在地、体温、行程轨迹,历史记录可查可改。
- 管理端:查看全院/全系学生上报情况,按日期、班级、状态筛选,支持数据导出。
- 系统端:角色权限隔离、数据统计可视化、未上报提醒、异常数据标记。
需求一定要先拆到这个颗粒度,再去做设计。很多同学上来就建表、写代码,做到一半发现"这个功能没考虑"、"那个表要改",返工成本比想象中高得多。
1.2 为什么选SSM:不是落后,是合适
Spring Boot目前确实是主流,但课程设计和毕业设计这个场景里,SSM反而有几个实际优势是我踩完坑之后才真正确认的:
第一是学习深度。SSM去掉了Spring Boot的自动配置,把Spring的IOC容器、AOP切面、事务管理、MyBatis的Mapper映射全部暴露在明面上。导师问"这个Bean是怎么注入的"、"事务是怎么控制的",你至少能答得上来,而不是说"框架帮我弄好了"。答辩的时候,SSM项目反而比Spring Boot项目更容易展示技术深度。
第二是资料丰富度。SSM流行了十几年,从入门博客到开源项目,再到各种报错解决方案,数量是Spring Boot的几倍。对新手来说,遇到一个问题搜不到答案是非常打击信心的,而SSM的"历史积累"恰好能规避这个问题。
第三是部署可控。SSM项目打包成War包扔进Tomcat就能跑,环境要求明确,不涉及Docker、K8s这些额外内容。对于以"完成设计并演示"为目标的项目来说,这种简单可靠反而更重要。
1.3 系统架构与功能模块规划
整个系统采用经典的三层架构,严格分层,代码结构清晰:
- 表现层:JSP + Bootstrap + jQuery,负责页面渲染和用户交互。
- 业务层:Spring Service接口 + 实现类,负责业务逻辑处理和事务管理。
- 持久层:MyBatis Mapper接口 + XML映射文件,负责数据库操作。
功能模块上,我按角色分成三大块:
| 角色 | 核心功能 | 关键页面 |
|---|---|---|
| 学生 | 每日日报填报、历史记录、个人信息维护 | 日报填写页、我的记录页 |
| 辅导员/教师 | 查看所带班级学生日报、未上报名单、数据导出 | 班级报表页、异常标记页 |
| 系统管理员 | 用户管理、班级管理、全局统计、系统参数配置 | 用户管理页、数据大屏页 |
这样划分之后,每个角色的SQL、Controller、页面都是独立的一部分,写起来思路不会乱,论文里的"模块设计"章节也有东西可写。
2. 数据库设计:日报系统的地基工程
2.1 核心表结构设计思路
日报系统这种业务,数据库设计比代码本身更重要。表之间的关系理清了,代码写起来是水到渠成的事。我这里拆解4张核心表。
第一张是用户表。存储学生和管理员信息,字段包括用户ID、用户名、密码(用MD5加密)、姓名、学号/工号、角色ID、班级ID、联系电话、状态。这里有一个关键设计——不要把密码明文存库,即使只是一个课设项目,也应该有这个意识。用MD5或加盐MD5都行,Spring自带的DigestUtils用起来就够。
第二张是角色表。表结构非常简单,就角色ID、角色编码、角色名称三个字段。数据预先插入三条:admin(管理员)、teacher(辅导员)、student(学生)。角色和用户分开建表,是为了后续扩展权限用的,别把角色字段直接塞进用户表。
第三张是班级表。字段包括班级ID、班级名称、所属学院、年级。管理员的用户管理页面需要按班级筛选学生,报表页面需要按班级汇总,这张表是各种统计查询的基础。
第四张是日报表。这是整个系统最核心的一张表,字段包括日报ID、学生ID、填表日期、体温、健康状况(正常/异常)、所在地、是否去过风险区域、行程轨迹、备注、创建时间、更新时间。这里建议加一个report_date字段,而不是直接用create_time来当"填报日期",因为学生可能会补报,create_time是实际插入时间,而report_date才代表"报的是哪一天"。
2.2 一个关键索引设计
日报表的查询场景非常固定,基本都是"某一天全院谁没报"、"某个人某一周报了几次"、"某个班级的异常记录有多少"。所以我在(report_date, student_id)上建了联合索引,查询效率有保障。
另外一点,日报表的数据量会随着时间线性增长,一个几百人的学院跑一个学期就是十几万条。对于课设项目来说不需要分表,但可以在论文的性能测试部分,专门对数据量增长后的查询优化做一点分析,这会是论文的一个加分项。
2.3 为什么冗余字段反而更合理
在设计日报表的时候,我一开始是纯关系型思路——日报表只存student_id,学生的姓名、班级信息全部靠联表查询。结果发现报表页面反复JOIN三张表,SQL写得又臭又长,还好几次因为JOIN条件写错导致数据显示异常。
后来我把学生的姓名、班级名称直接冗余到日报表里,当作冗余字段。这样查询日报列表时单表就能搞定,联表次数大大减少。虽然严格来说违反了数据库第三范式,但在这种数据量不大、报表查询频繁的业务场景里,用空间换时间是实际工程中的常见做法。论文的数据库设计章节里,把这一点作为"反范式化设计"来写,反而能体现你的思考。
3. 核心功能实现:从登录认证到统计报表
3.1 登录认证与拦截器设计
登录模块看起来简单,但里面有一个很重要的设计:权限拦截。
SSM项目里最常用的方式是Interceptor+Session。用户登录成功后,把用户ID、角色ID、姓名存进Session;然后写一个LoginInterceptor,在preHandle方法里判断Session是否为空,为空就跳转登录页,同时放行登录接口和静态资源。
public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session = request.getSession(); Object userId = session.getAttribute("userId"); if (userId == null) { response.sendRedirect(request.getContextPath() + "/login.html"); return false; } return true; } }权限隔离比登录拦截要多做一步。管理员可以访问所有接口,辅导员只能看到自己班级的数据,学生只能操作自己的日报,这个在Controller层通过@RequestMapping加路径前缀来做最直接:/student/**、/teacher/**、/admin/**的接口分别对应三种角色,配置拦截器时逐个匹配。
这个设计我在论文里是这样写的:通过"基于角色的访问控制模型"(RBAC),将系统权限划分为学生、辅导员、管理员三个角色,每个角色拥有独立的接口路径空间,既避免了越权访问,也便于后续权限扩展。
3.2 日报填报与状态机流转
学生日报的填写是整个系统的最高频操作,一天一次。这个页面的逻辑不复杂,但有几个细节值得注意。
一是防重复提交。同一个学生同一天只能提交一条日报。我在数据库层面加了一个唯一约束UNIQUE KEY (student_id, report_date),同时Service层先查后插。双重保障,万无一失。
二是状态流转设计。日报状态我用1表示正常,2表示异常,0表示待审核。学生提交时默认是"正常",但如果体温超过37.3度或选择了"异常",状态自动置为异常,辅导员在后台看到之后会进行确认或者标记为重点关注对象。
public int addReport(SysUser user, ReportDTO dto) { // 1. 校验是否已填报 Report existing = reportMapper.selectByStudentAndDate(user.getId(), dto.getReportDate()); if (existing != null) { throw new ServiceException("今日已填报,请勿重复提交"); } // 2. 根据体温和选项自动判断状态 int status = 1; if (dto.getTemperature() > 37.3 || dto.getHealthStatus() == 2) { status = 2; } // 3. 插入并冗余基本用户信息 return reportMapper.insert(Report.builder() .studentId(user.getId()) .studentName(user.getRealName()) .className(user.getClassName()) .temperature(dto.getTemperature()) .healthStatus(dto.getHealthStatus()) .location(dto.getLocation()) .status(status) .reportDate(dto.getReportDate()) .build()); }三是回退修改。学生不小心填错了,当天可以修改,隔天不能再动。这个逻辑用一个update接口加上report_date = 今天的条件即可,非常简单但非常实用。
3.3 未上报提醒与批量催促
日报系统如果只是"填了就行",那辅导员还是得天天催。我专门实现了一个"未上报统计"的功能:
在teacherServiceImpl里面写一个多条件查询,按班级维度统计当日已填报人数和总人数,两者一减就是未上报名单。这个功能不需要额外建表,一条SQL加一个循环就搞定,但对辅导员来说价值极大。接口返回的是一棵"班级 -> 未上报学生列表"的结构,前端渲染成一个折叠面板,点开一个班级就能看到所有没交的人。
用SQL的话,核心是这样的:
SELECT c.id AS class_id, c.name AS class_name, u.id AS student_id, u.real_name FROM t_class c LEFT JOIN t_user u ON u.class_id = c.id AND u.role_id = 3 WHERE u.id NOT IN ( SELECT student_id FROM t_report WHERE report_date = #{today} )这里的小技巧是NOT IN子查询可能会慢,换成LEFT JOIN加IS NULL的写法会更稳定,数据量大了以后性能差距能明显感受到。
3.4 统计报表与可视化
报表页面的核心是统计"全院上报率"和"各班级上报情况"。我用ECharts做了两个图:一个柱状图展示各班级上报人数对比,一个饼图展示异常占比。
后端返回JSON格式的统计结果,前端用Ajax拉数据后交给ECharts渲染:
$.ajax({ url: '/admin/statistics/overview', type: 'GET', dataType: 'json', success: function (res) { var chart = echarts.init(document.getElementById('reportChart')); chart.setOption({ xAxis: { type: 'category', data: res.classNames }, yAxis: { type: 'value' }, series: [{ type: 'bar', data: res.reportCounts, itemStyle: { color: '#4A90D9' } }] }); } });做报表模块的时候有一个比较深的体会:统计逻辑尽量在后端算完,前端只负责展示。有同学图省事,把原始数据全部返回给前端,用JavaScript去算总数。这样既慢,后期改口径也麻烦。我在后端封装了一个StatisticsVO,把班级名称、上报人数、总人数、上报率、异常数全部算好,前端拿来即用。
4. 数据库连接与事务管理:最容易翻车的地方
4.1 事务配置的几个坑
SSM的事务管理是答辩高频问题,也是实际开发中特别容易被忽略的地方。
首先要注意,@Transactional默认只对RuntimeException回滚,对CheckedException不回滚。日报填报时如果抛出一个ServiceException(自定义运行时异常),是可以正确回滚的。但如果你用Exception直接捕获会导致事务失效——这是很经典的坑。
其次,事务要加在Service实现类上,不是Controller,也不是Mapper。我见过有同学把@Transactional写在Controller方法上,数据库操作根本没有被事务管理,数据却能插入成功,其实就是因为Controller层根本不参与Spring的事务代理。
最后,MyBatis的openSession默认autoCommit=true,因此一旦事务配置出问题,你执行两条SQL可能第一条成功第二条失败,数据就脏了。调试时最直接的判断方法就是:在Service里故意抛一个异常,看看数据到底有没有回滚。
4.2 数据库连接池与编码配置的细节
连接池我用的阿里Druid,不只是因为它性能好,更因为它的监控页面在调试时非常有用。配置里有一个关键项:
<property name="filters" value="stat"/>加上stat过滤器之后,Druid会统计每个SQL的执行时间,访问/druid/index.html就能看到慢SQL列表。课设阶段数据量小,看不出什么问题,但这个功能在论文里写"系统性能优化手段"是非常合适的素材。
另一个特别容易忽略的配置是URL的编码参数。MySQL 5.7默认编码是latin1,如果连接串里不写characterEncoding=utf-8,数据库里存的汉字就会变成问号。正确写法:
jdbc:mysql://localhost:3306/daily_report?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/ShanghaiserverTimezone是MySQL 8.x必须写的,不写会报时区错误;MySQL 5.7不需要。这个版本差异问题,我在下面"部署排查"部分还会再提。
5. 部署调试:从开发环境到正常运行
5.1 环境版本搭配参考
SSM项目的环境搭配是很多新手的第一道坎,版本不对寸步难行。我这里直接用自己验证过的组合:
| 组件 | 版本 | 备注 |
|---|---|---|
| JDK | 1.8 | 长期稳定,Spring 4/5都兼容 |
| Maven | 3.6.x | 依赖管理必备 |
| Tomcat | 8.5 | 与JDK 1.8完全兼容 |
| MySQL | 5.7 或 8.0 | 5.7更省心,8.0要处理时区 |
| 前端框架 | Bootstrap 3.4 / jQuery 3.x | CDN引入或本地资源均可 |
一个比较实际的经验是:不要用最新版本的Spring。SSM项目最常用的组合是Spring 4.3.x + MyBatis 3.4.x + MyBatis-Spring 1.3.x,这套组合的兼容性问题早就被前人踩平了,随便搜都能找到解决办法。Spring 5搭配JDK 8没问题,但有些报错资料没有Spring 4丰富,没必要在这个环节给自己添堵。
5.2 从零到跑通项目的完整步骤
这里写一份可以直接照做的部署流程:
- 安装JDK 1.8,配置
JAVA_HOME和PATH环境变量,命令行执行java -version验证。 - 安装Maven 3.6.x,配置
settings.xml里的本地仓库路径和镜像地址(国内用阿里云镜像,下载快)。 - 安装MySQL 5.7,创建数据库和账号,执行项目附带的
daily_report.sql脚本初始化表结构和基础数据。 - 打开IDEA,导入项目(以Maven项目方式),等待依赖下载完成。
- 修改
jdbc.properties中的数据库账号、密码。 - 配置Tomcat 8.5,把项目部署到Tomcat并启动。
- 浏览器访问
http://localhost:8080/,能看到登录页即部署成功。
这里有一个很多人会忽略的步骤:启动之前先检查web.xml里的欢迎页配置。有的项目把首页配置成了index.html,但你明明没有这个文件,结果就是访问首页404。
5.3 部署过程中最典型的三类问题
第一类是端口被占用。Tomcat默认8080,你之前启动过其他服务,或者IDE里的Tomcat没有正常关闭,就会报端口冲突。解决办法:
netstat -ano | findstr 8080 taskkill /PID 进程号 /F第二类是数据库连接失败。最常见原因是MySQL服务没启动,或者jdbc.properties里的账号密码与本地不一致。验证手段很简单,先用Navicat或命令行直接连接数据库,能连上说明配置没问题,连不上就检查账号权限和密码。
第三类是内存溢出。启动项目时如果IDEA默认给了很小的堆内存,Tomcat跑起来容易崩。在启动配置的VM options里加上:
-Xms128m -Xmx512m课设项目这个大小就够用了,不用盲目调大。
6. 论文文档的写作框架与答辩准备
6.1 一万字论文的结构如何规划
这个项目附带论文文档的要求是一万字以上。很多同学听到一万字就头大,但其实日报系统这个题目,素材非常充足,写到一万到一万两千字是很自然的。
我给的论文目录结构建议是:
- 绪论:项目背景、国内外研究现状、研究意义,2000字左右。
- 相关技术介绍:SSM框架、MySQL、前端技术,1200字左右。
- 系统分析:可行性分析、需求分析、用例图、功能需求和非功能需求,2000字左右。
- 系统设计:总体架构设计、功能模块设计、数据库设计(ER图、表结构)、接口设计,3000字左右。
- 系统实现:每个核心模块的实现思路和关键代码说明,2500字左右。
- 系统测试:测试环境、测试用例、测试结果、性能与安全分析,1500字左右。
- 总结:写"开发过程中解决了什么问题、收获了什么",800字左右。
这个结构下,每章的字数要求都很轻松。数据库设计一章里放几张表结构说明表,每个字段都写清楚含义和约束,3000字就出来了。
6.2 流程图和ER图的绘制建议
论文需要图,但不需要花哨。PowerDesigner画ER图,Visio或draw.io画流程图,这两个工具就够了。
画ER图的重点是实体、属性和关系要一一对应到表结构,不要出现图里画了比如"角色"实体,但数据库里没有角色表;图上画的1对多关系,数据库里没有对应的外键或关联字段。这是导师和答辩老师最喜欢挑的问题,一眼就能看出论文和实际系统是不是"两张皮"。
绘制用户登录时序图时,注意展示"客户端-Controller-Service-Mapper-Database"五层之间完整的信息交互过程。答辩老师问"你系统的工作流程是什么样的",你对着时序图讲,比口头说清楚十倍。
6.3 答辩中被高频询问的问题清单
按我的经验,SSM项目的答辩问题基本集中在下面几个方向:
- 框架原理类:Spring IOC是什么?AOP在你项目里哪里用到了?MyBatis的#{}和${}有什么区别?事务传播行为有哪些?
- 功能实现类:日报防重复提交怎么实现?权限拦截怎么做的?数据统计的SQL怎么写?
- 数据库设计类:为什么用户表和角色表分开?日报表为什么冗余姓名和班级?哪张表的数据量增长最快?如何优化?
这些问题大部分都藏在本文的各个章节里。#{}和${}的区别,相信大家都知道前者是预编译占位符、后者是字符串拼接,但我建议大家一定要能说清楚:${}存在SQL注入风险,因此条件查询里传表名、排序字段时一定要做白名单校验。
7. 开发过程中踩过的坑与改进方向
7.1 几个印象深刻的翻车现场
第一个坑是IDEA编译版本问题。项目导入后,pom.xml里没有显式声明JDK版本,IDEA默认用的Project Structure里的版本是1.8,但Maven编译用的却是JDK 17(因为本机装了别的版本),结果编译报错"Unsupported class file major version",折腾了一个多小时。解决办法是在pom.xml里显式声明编译版本:
<properties> <java.version>1.8</java.version> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> </properties>第二个坑是JSP页面乱码。页面里中文全是问号,排查了半天发现是pageEncoding没设置:
<%@ page contentType="text/html;charset=UTF-8" language="java" pageEncoding="UTF-8" %>同时还要保证web.xml里配置了CharacterEncodingFilter,这个过滤器要放在所有Filter的最前面,否则后面配置的Filter依然可能乱码。
第三个坑是前端资源加载失败。Bootstrap和jQuery用了CDN链接,但演示现场没网,结果整个页面样式全丢了,操作逻辑虽然还能跑,但观感极差。从这个教训之后,我所有课设项目的静态资源都坚持放本地,不依赖外网。
7.2 系统可以继续扩展的方向
日报系统做成这样,距离一个"能实战"的系统还有差距,但作为课程设计它的完整度已经足够。如果学有余力,有几个方向可以继续深挖:
- 引入Redis缓存登录态和热点统计数据,减少数据库压力。
- 增加消息推送,用WebSocket实时推送未上报提醒。
- 导出功能升级,支持生成PDF格式的日报审核单。
- 前端升级为Vue或React,与后端使用RESTful API对接。
这些方向其实都可以直接写进论文的"展望"章节,但要注意别写得太空。每一句展望都要跟上"为什么做"以及"大概怎么做",这样答辩老师才会觉得你真的思考过。
7.3 关于这个项目我最后想说的
学生日报这类管理系统,从技术难度上说并不高,但它覆盖了SSM框架开发的完整链路:三层架构、事务管理、拦截器、动态SQL、联表查询、前端渲染、部署调试、项目文档。走完一遍,你已经把"会用框架"变成了"能独立开发一个完整项目",这是课设项目真正重要的产出。
如果你正在做类似的项目,我的建议只有一条:大部分时间应该花在数据库设计和调试排错上,而不是死磕代码量。因为一个系统的复杂度不在于代码行数,而在于数据关系是否合理、异常处理是否完整、权限控制是否严密。
从我经验来看,任何时候都得验证版本兼容性——特别是MySQL 5.7和8.0,JDK 8和17,Tomcat 8和9,两两之间的组合差异足以让人崩溃一整个晚上。把这个观念内化,以后做任何项目都能少走很多弯路。