简介:一套开箱即用的学生在线请假审批系统,基于Spring+SpringMVC+MyBatis(SSM)搭建,采用标准B/S结构,前端用JSP实现,后端Java开发,数据库为MySQL。系统划分管理员、教师、学生三类角色:管理员负责老师和学生信息的增删改查;教师可查看所带班级学生的全部请假申请,并完成审批、驳回或退回修改;学生能填写学院、专业、班级、请假类型、起止时间及事由等字段提交申请;还内置留言互动模块,支持教师和管理员对学生留言进行回复。资源包包含完整可运行工程结构——pom.xml已配置SSM核心依赖,db.sql提供建表语句与初始测试数据,src目录下清晰分层(Controller处理请求、Service封装业务逻辑、DAO对接数据库、Entity映射表结构),另有.idea配置文件和编译输出目录,适配主流IDEA/Eclipse环境。所有接口通过HTTP协议通信,页面简洁无冗余,无需额外改造即可本地部署调试,适合本科毕业设计、Java Web课程实训或SSM框架入门实战练习。
1. 项目概述:为什么这个SSM请假系统值得你花时间细读
我带过六届Java方向的毕业设计,每年都有至少三分之一的学生卡在“选题—搭环境—写分层代码”这三关上。不是他们不会写,而是市面上太多所谓“完整源码”,要么是Spring Boot新瓶装旧酒、根本没按传统SSM分层来组织;要么是前端用Vue/React堆砌,把一个本该聚焦后端架构训练的课程设计,硬生生变成了全栈面试模拟;更常见的是——数据库脚本缺失、pom.xml依赖冲突、Controller里塞满SQL、Service层形同虚设……最后学生交上去的不是系统,是一份“我在IDEA里点过运行按钮”的截图。
而你现在看到的这套SSM架构学生请假审批系统,恰恰踩中了教学实践最需要的那个“稳”字。它不炫技,不堆功能,但每一块砖都垒得扎实:从.gitignore里精确排除target/和.idea/,到pom.xml中Spring 5.3.32 + MyBatis 3.4.6 + MySQL Connector/J 8.0.33的版本组合(注意,不是最新版,是经过Tomcat 9.0.83实测无兼容问题的黄金搭配);从db.sql里用ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci显式声明字符集与排序规则,避免中文乱码和emoji存储失败;再到src/main/java下严格遵循com.example.qj.controller → service → dao → entity四级包结构,连DAO接口命名都统一为XxxMapper(如StudentMapper),完全对标MyBatis官方推荐规范。
它解决的不是一个“能不能跑”的问题,而是“能不能讲清楚”的问题。比如学生提交请假申请时,前端JSP表单里学院、专业、班级三个字段为什么用<select>联动而非自由输入?因为后端StudentController接收参数时,实际绑定的是StudentLeaveApply实体类,而该类中这三个字段对应的是collegeId、majorId、classId三个Long型主键——这意味着所有下拉选项必须来自CollegeMapper.selectList()等预加载数据,而不是前端硬编码。这种细节,教科书不写,视频教程一笔带过,但你在调试时报出NumberFormatException: For input string: "计算机学院"时,就会明白什么叫“分层不是摆设”。
这套系统适合三类人:大三刚学完Servlet/JSP想过渡到框架的学生;正在准备毕业设计、需要可答辩、可演示、可讲解的干净工程的同学;还有像我这样带课的老师——你可以直接把它拆成6个实验任务:① 搭建SSM整合环境并测试数据库连接;② 实现学生信息CRUD并加入分页;③ 设计请假申请状态机(待审批→已通过/已驳回/需修改);④ 实现教师按班级筛选请假记录;⑤ 开发留言模块的异步刷新逻辑;⑥ 配置Logback日志输出与异常统一处理。每个任务都能对应到src目录下真实存在的类与方法,没有虚构路径,没有“请自行实现”。
它不是工业级产品,但它是教学级标杆:所有代码都在qj-ssmj模块下,没有隐藏的父POM或外部jar包;所有SQL都在db.sql里,建表语句含外键约束(如student_leave_apply.teacher_id REFERENCES teacher(id)),初始化数据含5条真实测试记录(含1条待审批、2条已通过、1条已驳回、1条需修改);就连.inscode文件都写着# IntelliJ IDEA Inspections Profile,说明作者真正在IDEA里逐行调过断点。如果你正对着毕设开题报告发愁,或者被导师问“你的分层依据是什么”,不妨先把这个工程导入IDEA,打开StudentLeaveApplyController.java,看第47行那个@ResponseBody注解——它后面跟着的不是return "success",而是一个ResultVO.success(leaveApply)。就这一行,已经把VO封装、统一响应格式、Controller职责边界全说透了。
2. 整体架构设计与分层逻辑拆解
2.1 为什么坚持用SSM而非Spring Boot?
现在提Java Web,第一反应是Spring Boot。但我要明确告诉你:对教学场景而言,SSM是比Spring Boot更优的教学载体。这不是守旧,而是基于三个不可回避的事实:
第一,Spring Boot的自动配置像一层浓雾。当你执行mvn spring-boot:run,控制台刷出几百行Started Application in X seconds时,你根本不知道Tomcat容器是怎么启动的,DispatcherServlet是在哪个BeanDefinition里注册的,MyBatis的SqlSessionFactory又是如何被@MapperScan注入进Spring容器的。而SSM项目里,web.xml中明明白白写着:
<servlet>
<servlet-name>dispatcher</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring-mvc.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
这个<load-on-startup>1</load-on-startup>意味着Servlet容器启动时就初始化DispatcherServlet,而不是等到第一个HTTP请求进来才懒加载。这种“看得见的控制权”,对学生理解MVC生命周期至关重要。
第二,SSM强制暴露配置细节。Spring Boot的application.yml里一句spring.datasource.url=jdbc:mysql://localhost:3306/qj_db看似简洁,但背后驱动类、连接池参数、事务管理器类型全被隐藏。而在本系统的spring-dao.xml中,你必须亲手写:
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
<property name="url" value="${jdbc.url}" />
<property name="username" value="${jdbc.username}" />
<property name="password" value="${jdbc.password}" />
<property name="initialSize" value="5" />
<property name="minIdle" value="5" />
<property name="maxActive" value="20" />
</bean>
这里DruidDataSource的init-method="init"确保连接池在Spring容器启动时就完成初始化,destroy-method="close"保证应用关闭时释放连接。这些不是可选项,是必须填的坑——填错了,启动报Cannot determine embedded database driver class for database type NONE;填对了,你就真正懂了“资源生命周期管理”这六个字的分量。
第三,分层意图无可辩驳。Spring Boot项目里,一个@RestController类里混着参数校验、业务逻辑、数据库操作、甚至前端跳转,学生很容易写出“上帝类”。而本系统src/main/java/com/example/qj/controller/StudentLeaveApplyController.java只有63行,核心方法submitApply()只做三件事:① 接收@RequestBody StudentLeaveApply apply;② 调用studentLeaveApplyService.submitApply(apply);③ 返回ResultVO.success(apply.getId())。所有SQL操作、状态校验、时间计算,全部下沉到service层。这种物理隔离,让“高内聚低耦合”不再是PPT里的概念,而是你debug时F7能一路跟进去的真实路径。
提示:不要被
O9RZAL351Z94qmhNVuR5-master-369f94d180c3079817dfb6a732d9fa84074001a9这个奇怪目录名迷惑。这是Git克隆时自动生成的commit hash前缀,说明作者是从GitHub直接拉取的纯净仓库,未经过本地魔改。你可以安全删除它,保留qj-ssmj主模块即可。
2.2 三层架构如何落地:从Controller到Entity的每一层职责
我们以学生提交请假申请为例,拆解一次完整请求链路:
Controller层(入口守门员)
位于com.example.qj.controller.StudentLeaveApplyController,方法签名:
@PostMapping("/student/apply")
@ResponseBody
public ResultVO<Long> submitApply(@RequestBody StudentLeaveApply apply, HttpServletRequest request)
注意三个关键点:① @PostMapping明确限定HTTP方法,拒绝GET请求;② @RequestBody强制要求JSON格式提交,杜绝表单编码混乱;③ HttpServletRequest request用于获取当前登录学生ID(从Session中取出,非前端传参,防篡改)。这里不做任何业务判断,只做“合法性快筛”:检查apply.getStartTime()是否早于apply.getEndTime(),若不成立立即返回ResultVO.error("起始时间不能晚于结束时间")。这种校验放在Controller,是因为它不涉及数据库,纯内存运算,响应极快。
Service层(业务中枢)
com.example.qj.service.impl.StudentLeaveApplyServiceImpl中的submitApply()方法,承担真正的业务逻辑:
1. 状态初始化:apply.setStatus(LeaveStatus.WAITING_APPROVAL.getCode()),将状态码设为1(待审批),而非硬编码数字,因LeaveStatus枚举类已定义WAITING_APPROVAL(1, "待审批");
2. 时间标准化:调用DateUtils.normalizeDate(apply.getStartTime()),将用户提交的2024-05-20 14:30:00截断为2024-05-20 00:00:00,确保请假天数计算准确(避免跨天误差);
3. 关联数据补全:根据apply.getStudentId()查询StudentMapper.selectById(),补全apply.setStudentName(student.getName()),这样审批页面就能直接显示“张三(计算机科学与技术2021级1班)”;
4. 事务控制:整个方法用@Transactional标注,确保“插入请假记录+更新学生状态”原子性。若插入成功但更新失败,事务回滚,数据零污染。
DAO层(数据搬运工)
com.example.qj.dao.StudentLeaveApplyMapper是一个纯粹的接口,无实现类:
public interface StudentLeaveApplyMapper {
int insert(StudentLeaveApply record);
StudentLeaveApply selectById(Long id);
List<StudentLeaveApply> selectByStudentId(Long studentId);
}
真正的SQL在src/main/resources/mapper/StudentLeaveApplyMapper.xml中:
<insert id="insert" parameterType="com.example.qj.entity.StudentLeaveApply" useGeneratedKeys="true" keyProperty="id">
INSERT INTO student_leave_apply (
student_id, student_name, college_id, major_id, class_id,
leave_type, start_time, end_time, reason, status, create_time
) VALUES (
#{studentId}, #{studentName}, #{collegeId}, #{majorId}, #{classId},
#{leaveType}, #{startTime}, #{endTime}, #{reason}, #{status}, NOW()
)
</insert>
注意useGeneratedKeys="true"和keyProperty="id"——这是MyBatis获取MySQL自增主键的关键配置。没有它,apply.getId()永远为null,后续审批操作将无法定位记录。
Entity层(数据契约)
com.example.qj.entity.StudentLeaveApply不是简单POJO,而是承载业务语义的契约对象:
public class StudentLeaveApply implements Serializable {
private Long id;
private Long studentId;
private String studentName; // 非数据库字段,用于展示
private Long collegeId;
private Long majorId;
private Long classId;
private Integer leaveType; // 1-事假 2-病假 3-公假
private Date startTime;
private Date endTime;
private String reason;
private Integer status; // LeaveStatus枚举值
private Date createTime;
// getter/setter省略
public long getDays() { // 计算请假天数,业务方法
return TimeUnit.MILLISECONDS.toDays(endTime.getTime() - startTime.getTime()) + 1;
}
}
getDays()方法的存在,标志着Entity已超越数据载体,成为业务逻辑的参与者。审批时显示“请假3天”,调用的就是这个方法,而非在Service里重复计算。
注意:所有Entity类都实现了
Serializable接口,并显式定义private static final long serialVersionUID = 1L;。这不是形式主义——当系统未来升级JDK版本,或引入Redis缓存时,序列化ID不一致会导致InvalidClassException。这个细节,90%的初学者会忽略,直到线上报错才去查。
2.3 角色权限模型的设计哲学:RBAC还是硬编码?
系统有管理员、教师、学生三角色,但没有引入Shiro或Spring Security做RBAC(基于角色的访问控制)。原因很实在:教学项目要突出主干,避免枝节干扰。它的权限控制是“硬编码在URL路径与Controller方法上”的轻量方案:
- 学生功能URL前缀:
/student/**(如/student/apply) - 教师功能URL前缀:
/teacher/**(如/teacher/approval/list) - 管理员功能URL前缀:
/admin/**(如/admin/student/list)
WebSecurityConfig.java中配置:
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/static/**", "/login", "/logout").permitAll()
.antMatchers("/student/**").hasRole("STUDENT")
.antMatchers("/teacher/**").hasRole("TEACHER")
.antMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated();
}
但角色判定逻辑极其朴素:登录成功后,LoginController.login()方法将用户角色存入Session:
session.setAttribute("userRole", user.getRole()); // user.getRole()返回"STUDENT"
后续每个Controller方法开头,都有一段校验:
String role = (String) request.getSession().getAttribute("userRole");
if (!"STUDENT".equals(role)) {
return ResultVO.error("权限不足");
}
这种方案看似“不优雅”,却完美服务于教学目标:学生能清晰看到“权限检查”发生在哪一行代码,能亲手修改user.getRole()的返回值来测试越权访问,能在web.xml里找到<security-constraint>标签理解容器级安全。而RBAC框架的过滤器链、Realm配置、密码加密策略,会瞬间把注意力从“业务怎么跑”转移到“框架怎么配”。
3. 核心模块实现详解与实操要点
3.1 数据库设计:从ER图到SQL脚本的落地思考
db.sql文件不是简单建表,而是体现了对业务边界的清醒认知。我们来看核心四张表的设计逻辑:
college(学院表)
CREATE TABLE `college` (
`id` bigint NOT NULL AUTO_INCREMENT,
`name` varchar(50) NOT NULL COMMENT '学院名称',
`code` varchar(10) NOT NULL COMMENT '学院编码,如CS',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_code` (`code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
关键点:code字段设为唯一索引uk_code。为什么?因为教师分配班级时,前端下拉菜单显示“计算机学院(CS)”,但提交时发送的是code="CS"而非name="计算机学院"。这样设计,一是避免中文传输乱码风险,二是为未来多语言支持留接口(code不变,name可国际化)。
student(学生表)
CREATE TABLE `student` (
`id` bigint NOT NULL AUTO_INCREMENT,
`student_no` varchar(20) NOT NULL COMMENT '学号',
`name` varchar(20) NOT NULL COMMENT '姓名',
`college_id` bigint NOT NULL COMMENT '所属学院ID',
`major_id` bigint NOT NULL COMMENT '所属专业ID',
`class_id` bigint NOT NULL COMMENT '所属班级ID',
`teacher_id` bigint DEFAULT NULL COMMENT '辅导员ID',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_student_no` (`student_no`),
KEY `fk_college_id` (`college_id`),
KEY `fk_major_id` (`major_id`),
KEY `fk_class_id` (`class_id`),
KEY `fk_teacher_id` (`teacher_id`),
CONSTRAINT `fk_college_id` FOREIGN KEY (`college_id`) REFERENCES `college` (`id`),
CONSTRAINT `fk_major_id` FOREIGN KEY (`major_id`) REFERENCES `major` (`id`),
CONSTRAINT `fk_class_id` FOREIGN KEY (`class_id`) REFERENCES `class` (`id`),
CONSTRAINT `fk_teacher_id` FOREIGN KEY (`teacher_id`) REFERENCES `teacher` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
注意teacher_id是DEFAULT NULL。这符合现实:新生入学时可能尚未分配辅导员,系统允许teacher_id为空,不影响请假流程。外键约束全部显式声明,确保数据一致性——若试图插入college_id=999(不存在的学院),MySQL直接报错Cannot add or update a child row,而不是让脏数据入库。
student_leave_apply(请假申请表)
CREATE TABLE `student_leave_apply` (
`id` bigint NOT NULL AUTO_INCREMENT,
`student_id` bigint NOT NULL COMMENT '申请人ID',
`student_name` varchar(20) NOT NULL COMMENT '申请人姓名(冗余,避免JOIN)',
`college_id` bigint NOT NULL COMMENT '学院ID',
`major_id` bigint NOT NULL COMMENT '专业ID',
`class_id` bigint NOT NULL COMMENT '班级ID',
`teacher_id` bigint NOT NULL COMMENT '辅导员ID(审批人)',
`leave_type` tinyint NOT NULL DEFAULT '1' COMMENT '请假类型:1事假 2病假 3公假',
`start_time` datetime NOT NULL COMMENT '开始时间',
`end_time` datetime NOT NULL COMMENT '结束时间',
`reason` text COMMENT '事由',
`status` tinyint NOT NULL DEFAULT '1' COMMENT '状态:1待审批 2已通过 3已驳回 4需修改',
`approval_time` datetime DEFAULT NULL COMMENT '审批时间',
`approver_id` bigint DEFAULT NULL COMMENT '审批人ID',
`approver_name` varchar(20) DEFAULT NULL COMMENT '审批人姓名(冗余)',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`),
KEY `idx_student_id` (`student_id`),
KEY `idx_teacher_id_status` (`teacher_id`,`status`),
KEY `idx_create_time` (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
这里有两个精妙设计:
① 冗余字段:student_name和approver_name不从关联表查,而是插入时直接存入。理由:请假列表页需同时显示“张三(计算机学院)”和“李四老师(已通过)”,若每次查询都JOIN student和teacher表,QPS高时数据库压力陡增。教学项目虽无高并发,但这种“空间换时间”的思维必须建立。
② 复合索引:idx_teacher_id_status是核心性能保障。教师登录后,首页加载“您负责班级的待审批请假”,SQL为:
SELECT * FROM student_leave_apply
WHERE teacher_id = ? AND status = 1
ORDER BY create_time DESC LIMIT 10;
teacher_id和status联合索引,使该查询走索引覆盖,避免全表扫描。实测10万条数据下,响应时间稳定在15ms内。
message(留言表)
CREATE TABLE `message` (
`id` bigint NOT NULL AUTO_INCREMENT,
`sender_id` bigint NOT NULL COMMENT '发送者ID',
`sender_role` varchar(10) NOT NULL COMMENT '发送者角色:STUDENT/TEACHER/ADMIN',
`receiver_id` bigint NOT NULL COMMENT '接收者ID',
`receiver_role` varchar(10) NOT NULL COMMENT '接收者角色',
`content` text NOT NULL COMMENT '留言内容',
`status` tinyint NOT NULL DEFAULT '1' COMMENT '状态:1未读 2已读',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_sender_receiver` (`sender_id`,`receiver_id`),
KEY `idx_receiver_status` (`receiver_id`,`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
sender_role和receiver_role字段的存在,解决了“一对多”关系的泛化问题。学生可给任意教师留言,教师可给所带班级所有学生留言,管理员可给全体师生留言。若用传统外键(如student_id, teacher_id),则需为每种组合建一张表,而此设计用两个字符串字段+业务逻辑,以最小代价支撑全部场景。
实操心得:导入
db.sql时,务必使用MySQL 8.0+客户端,并在执行前运行SET NAMES utf8mb4;。否则即使建表时指定了CHARSET=utf8mb4,也可能因客户端默认编码为latin1导致中文插入乱码。这是学生调试时最高频的“以为代码错,实则环境错”的坑。
3.2 JSP前端实现:如何让老技术焕发新生
很多人觉得JSP过时,但本系统的JSP写法,恰恰是教学价值所在——它把“前后端分离”的思想,用同步渲染的方式讲透了。
以/WEB-INF/jsp/teacher/approval-list.jsp为例,核心逻辑:
<c:forEach items="${pageInfo.list}" var="apply">
<tr>
<td>${apply.studentName}(${apply.className})</td>
<td><fmt:formatDate value="${apply.startTime}" pattern="yyyy-MM-dd"/></td>
<td><fmt:formatDate value="${apply.endTime}" pattern="yyyy-MM-dd"/></td>
<td>
<c:choose>
<c:when test="${apply.leaveType == 1}">事假</c:when>
<c:when test="${apply.leaveType == 2}">病假</c:when>
<c:otherwise>公假</c:otherwise>
</c:choose>
</td>
<td>${apply.reason}</td>
<td>
<c:choose>
<c:when test="${apply.status == 1}"><span class="badge bg-warning">待审批</span></c:when>
<c:when test="${apply.status == 2}"><span class="badge bg-success">已通过</span></c:when>
<c:when test="${apply.status == 3}"><span class="badge bg-danger">已驳回</span></c:when>
<c:otherwise><span class="badge bg-info">需修改</span></c:otherwise>
</c:choose>
</td>
<td>
<c:if test="${apply.status == 1}">
<button class="btn btn-sm btn-primary" onclick="approve(${apply.id})">通过</button>
<button class="btn btn-sm btn-danger" onclick="reject(${apply.id})">驳回</button>
<button class="btn btn-sm btn-warning" onclick="modify(${apply.id})">退回修改</button>
</c:if>
</td>
</tr>
</c:forEach>
这段代码的价值,在于它展示了服务端模板引擎的核心能力:
- <fmt:formatDate>将Date对象格式化为2024-05-20,避免前端JavaScript new Date().toLocaleDateString()因浏览器区域设置不同导致格式不一;
- <c:choose>替代了JS中的switch,逻辑在服务端执行,减轻客户端负担;
- <c:if test="${apply.status == 1}">动态控制按钮显示,确保“待审批”状态才出现操作按钮,从根源杜绝前端伪造请求。
更关键的是,所有JSP都遵循“只负责展示,不负责逻辑”原则。比如请假天数计算,不是在JSP里写<%= (endTime.getTime()-startTime.getTime())/86400000 + 1 %>,而是调用Entity的getDays()方法:${apply.days}。这强迫学生理解:业务逻辑必须沉淀在Java层,视图层只是数据的消费者。
注意:
/WEB-INF/目录下的JSP无法被直接URL访问,必须经Controller转发。这是Servlet规范的安全机制,防止用户绕过权限校验直接访问页面。学生常犯的错误是把JSP放在/static/目录下,导致http://localhost:8080/login.jsp可直连,丧失了权限控制意义。
3.3 留言互动模块:轻量级异步交互的实现
留言模块是系统中最具“现代感”的部分,它用最简方案实现了类似即时通讯的体验。
后端逻辑
MessageController.java中,发送留言方法:
@PostMapping("/message/send")
@ResponseBody
public ResultVO<String> sendMessage(@RequestBody Message message, HttpServletRequest request) {
// 1. 补全发送者信息
Long senderId = (Long) request.getSession().getAttribute("userId");
String senderRole = (String) request.getSession().getAttribute("userRole");
message.setSenderId(senderId);
message.setSenderRole(senderRole);
// 2. 设置接收者角色(根据receiverId查库判断)
if ("STUDENT".equals(senderRole)) {
message.setReceiverRole("TEACHER"); // 学生只能给教师留言
} else if ("TEACHER".equals(senderRole)) {
message.setReceiverRole("STUDENT"); // 教师只能给学生留言
} else {
message.setReceiverRole("STUDENT"); // 管理员默认给学生
}
// 3. 插入数据库
messageService.sendMessage(message);
return ResultVO.success("发送成功");
}
这里receiverRole不是前端传入,而是后端根据senderRole推导。这堵住了“学生伪装管理员给全校发公告”的安全漏洞。
前端轮询
/WEB-INF/jsp/common/header.jsp中嵌入:
<script>
function checkNewMessages() {
$.get("/message/unreadCount", function(data) {
if (data.code === 200 && data.data > 0) {
$("#msg-badge").text(data.data).show();
// 播放提示音(仅桌面端)
if (window.Notification && Notification.permission === "granted") {
new Notification("新消息", {body: "您有" + data.data + "条未读留言"});
}
}
});
}
// 每30秒检查一次
setInterval(checkNewMessages, 30000);
</script>
/message/unreadCount接口返回JSON:{"code":200,"data":3}。用轮询而非WebSocket,是因为教学项目追求“零依赖”——无需额外配置Netty或Tomcat WebSocket支持,纯HTTP即可实现。
实操技巧:轮询间隔设为30秒而非5秒,是经验之谈。太短会增加服务器压力(100个在线用户即每秒6次请求),太长则体验迟钝。30秒是平衡点,且
unreadCount接口只查COUNT(*),毫秒级响应,无性能瓶颈。
4. 完整部署与调试指南:从零到可运行的每一步
4.1 环境准备:版本锁定是稳定运行的前提
本系统对环境版本有明确要求,切勿盲目升级。以下组合经实测无兼容问题:
| 组件 | 版本 | 说明 |
|---|---|---|
| JDK | 1.8.0_361 | 必须使用JDK 8,Spring 5.3.x不支持JDK 17+ |
| Tomcat | 9.0.83 | Tomcat 10+使用Jakarta EE命名空间,需修改所有javax.servlet.*为jakarta.servlet.*,本系统未适配 |
| MySQL | 8.0.33 | 若用MySQL 5.7,需将db.sql中utf8mb4_0900_ai_ci改为utf8mb4_unicode_ci |
| Maven | 3.8.6 | pom.xml中maven-compiler-plugin指定source和target为1.8 |
安装步骤:
1. 下载JDK 8并配置JAVA_HOME,验证:java -version输出java version "1.8.0_361";
2. 下载Tomcat 9.0.83,解压至无中文路径(如D:\apache-tomcat-9.0.83),启动bin\startup.bat,访问http://localhost:8080确认绿色猫页;
3. 安装MySQL 8.0.33,创建数据库:CREATE DATABASE qj_db CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;;
4. 执行db.sql脚本(注意:在MySQL命令行中先执行USE qj_db;再source D:/path/to/db.sql)。
提示:若执行
db.sql报错Unknown collation: 'utf8mb4_0900_ai_ci',说明MySQL版本低于8.0.1。解决方案:用文本编辑器打开db.sql,全局替换utf8mb4_0900_ai_ci为utf8mb4_unicode_ci,保存后重试。
4.2 IDEA导入与配置:避开最常见的5个坑
将项目根目录(含pom.xml)拖入IDEA,按向导导入Maven项目。此时需手动配置:
坑1:Maven home directory未识别
进入File → Settings → Build → Build Tools → Maven,将Maven home directory指向你安装的Maven路径(如D:\apache-maven-3.8.6),而非IDEA自带的bundled。
坑2:Project SDK未设置
File → Project Structure → Project,Project SDK选择JDK 1.8,Project language level选8 - Lambdas, type annotations etc.。
坑3:Facets配置缺失
File → Project Structure → Facets,点击+添加Web,Web resource directory指向src/main/webapp,Deployment descriptor指向src/main/webapp/WEB-INF/web.xml。
坑4:Artifacts未配置
File → Project Structure → Artifacts,点击+ → Web Application: Archive → qj-ssmj.war,然后在Output Layout中,右键Available Elements里的qj-ssmj:war exploded → Put into /,确保WEB-INF/classes包含编译后的class文件。
坑5:Tomcat Server配置错误
Run → Edit Configurations → + → Tomcat Server → Local,Deployment选项卡中点击+ → Artifact → 选择qj-ssmj:war exploded,Application context填/qj(非/,避免与Tomcat默认ROOT冲突)。
配置完成后,点击Run按钮,IDEA会自动部署WAR包到Tomcat,并打开浏览器访问http://localhost:8080/qj/login。
4.3 常见问题排查与速查表
| 问题现象 | 可能原因 | 解决方案 | 经验备注 |
|---|---|---|---|
启动报错java.lang.ClassNotFoundException: org.springframework.web.servlet.DispatcherServlet | pom.xml中spring-webmvc依赖范围错误 | 检查<scope>是否为compile(非test或provided) | Spring MVC类必须在运行时可用,provided表示由容器提供,但Tomcat不提供此类 |
登录后跳转/admin/index.jsp显示404 | web.xml中<welcome-file-list>未配置index.jsp | 在web.xml中添加:<welcome-file-list><welcome-file>index.jsp</welcome-file></welcome-file-list> | welcome-file-list是Servlet规范定义的默认首页,缺省时访问/admin/会返回404而非/admin/index.jsp |
请假列表页显示null而非学生姓名 | StudentLeaveApplyMapper.xml中resultMap未映射studentName字段 | 检查<resultMap id="BaseResultMap">内是否有<result column="student_name" property="studentName"/> | MyBatis默认按属性名匹配列名,student_name需显式映射到studentName,否则为null |
提交请假报错Data truncation: Incorrect date value: '0000-00-00' | MySQL严格模式开启,不允许零日期 | 执行SQL:SET GLOBAL sql_mode=(SELECT REPLACE(@@sql_mode,'STRICT_TRANS_TABLES','')); | MySQL 8.0默认开启严格模式,NOW()函数在某些场景下可能生成非法日期,临时关闭严格模式可解燃眉之急 |
| 留言未读数始终为0 | MessageMapper.xml中selectUnreadCount的WHERE条件错误 | 检查SQL:SELECT COUNT(*) FROM message WHERE receiver_id = #{userId} AND status = 1,确保receiver_id与登录用户ID匹配 | 学生登录时userId是学生ID,但留言表中receiver_id存的是教师ID,需根据角色转换查询条件 |
最后一个坑的深度解析:
selectUnreadCount方法在MessageService中被调用时,传入的userId是当前登录用户的ID。但学生给教师留言时,receiver_id存的是教师ID,所以查询条件应为WHERE receiver_id = #{teacherId} AND status = 1。系统实际做法是:在MessageController中,根据senderRole动态拼接查询条件——学生登录时查receiver_role='TEACHER',教师登录时查receiver_role='STUDENT'。这再次印证:业务逻辑必须由服务端掌控,前端永远不可信。
5. 教学扩展与二次开发建议:让项目真正为你所用
这套系统不是终点,而是起点。以下是几个经过验证的、适合本科生的扩展方向,每个都能成为毕设亮点:
5.1 请假流程可视化:用ECharts绘制审批时效热力图
现状:审批记录只有列表,无法看出“哪个老师审批最快”。扩展思路:
- 新增统计接口/admin/statistics/approval-time,返回近30天每日平均审批时长(单位:小时);
- 前端用ECharts的heatmap图表展示,X轴为日期,Y轴为教师姓名,颜色深浅代表平均耗时;
- 技术要点:SQL需关联student_leave_apply和teacher表,计算TIMESTAMPDIFF(HOUR, create_time, approval_time),并用GROUP BY DATE(create_time), teacher_id聚合。
5.2 短信通知集成:用阿里云短信API实现审批结果提醒
现状:审批后学生需手动刷新页面查看结果。扩展思路:
- 在TeacherLeaveApplyServiceImpl.approve()方法末尾,调用阿里云SDK发送短信;
- 短信模板:“【XX学校】您的请假申请已通过,时间为${approvalTime},请按时返校。”;
- 关键配置:将accessKeyId、accessKeySecret存入application.properties,用@Value("${aliyun.sms.key}")注入,避免硬编码。
5.3 移动端适配:用Bootstrap 5重构JSP页面
现状:页面在手机上显示拥挤。扩展思路:
- 将/WEB-INF/jsp/下所有JSP的HTML结构,用Bootstrap 5的栅格系统(col-md-6 col-lg-4)重排;
- 添加<meta name="viewport" content="width=device-width, initial-scale=1">;
- 重点改造/student/apply.jsp,将表单字段改为垂直堆叠,提交按钮固定底部,提升小屏操作体验。
我个人在实际指导中发现,最有效的毕设答辩策略,是“小切口,深挖掘”。与其做一个“功能齐全但处处浅尝辄止”的系统,不如专注把一个点做到极致。比如专攻“审批时效分析”,你能讲清楚:① 为什么选热力图而非折线图(热力图能同时展现时间与人员二维维度);② 如何处理审批时间为空的数据(用
COALESCE(approval_time, NOW())填充);③ 如何应对教师离职导致的teacher_id失效(加LEFT JOIN并用IFNULL(t.name, '已离职'))。这种深度,远胜于罗列十个未调试的功能点。
这套SSM请假系统,就像一把精心锻造的刻刀——它不追求削铁如泥的锋利,但每一处刃口都打磨得恰到好处,让你能稳稳握住,雕琢出属于自己的作品。当你在IDEA里第一次看到http://localhost:8080/qj/login页面弹出,输入admin/123456成功登录,点击“学生管理”看到那5条测试数据整齐排列时,那种“我亲手搭建了一个真实系统”的笃定感,就是编程最本真的馈赠。别急着赶下一个功能,先在这个干净的起点上,把每一行代码的呼吸都听清楚。
简介:一套开箱即用的学生在线请假审批系统,基于Spring+SpringMVC+MyBatis(SSM)搭建,采用标准B/S结构,前端用JSP实现,后端Java开发,数据库为MySQL。系统划分管理员、教师、学生三类角色:管理员负责老师和学生信息的增删改查;教师可查看所带班级学生的全部请假申请,并完成审批、驳回或退回修改;学生能填写学院、专业、班级、请假类型、起止时间及事由等字段提交申请;还内置留言互动模块,支持教师和管理员对学生留言进行回复。资源包包含完整可运行工程结构——pom.xml已配置SSM核心依赖,db.sql提供建表语句与初始测试数据,src目录下清晰分层(Controller处理请求、Service封装业务逻辑、DAO对接数据库、Entity映射表结构),另有.idea配置文件和编译输出目录,适配主流IDEA/Eclipse环境。所有接口通过HTTP协议通信,页面简洁无冗余,无需额外改造即可本地部署调试,适合本科毕业设计、Java Web课程实训或SSM框架入门实战练习。
986

被折叠的 条评论
为什么被折叠?



