FEATURED · 精选文章

JSP+SSM学生请假管理系统源码解析:从数据表到审批状态机

发布时间 / 2026/9/15 23:22:13
来源 / 创域科博编辑部
栏目 / 资讯中心
JSP+SSM学生请假管理系统源码解析:从数据表到审批状态机 简介一款面向计算机相关专业毕业设计场景的JSP基于SSM学生请假管理系统源码包完整涵盖学生端、辅导员端、院系领导端三大角色模块覆盖请假申请、审批、销假、班级汇总等核心流程并基于Bootstrap实现响应式前台适合初阶至中阶学习SSM整合与Web项目开发。包内共299个文件以71个java源文件、51个jsp页面、51个jar依赖库、22个css样式及sql数据库脚本为主辅以xml配置、js交互脚本与说明文档整体压缩后约30.02MB结构清晰便于直接导入Eclipse/Idea配合MySQL 5.6运行。已有102人学习下载。资源除项目源码与数据库备份外还附带运行配置说明和文档代码经过测试可成功运行答辩均分达96分下载后如遇环境问题可私聊远程指导可作为课程设计、毕业设计或SSM入门进阶的完整参考。1. 一个JSPSSM学生请假管理系统为什么还值得拆开看JSP基于SSM的学生请假管理系统听起来像是几年前的毕业设计标配但它的代码组织恰恰能看清Java Web最本质的链路SpringMVC如何把请求映射到方法Mybatis如何把SQL归还给开发者Spring又如何在一个审批动作里把两条SQL包进同一个事务。整个系统分学生端、辅导员端、院系领导端三个角色覆盖请假申请、审批、跨级上报、销假和班级汇总这些业务放到现在的微服务项目里会被拆成若干服务但核心状态流转逻辑并没有变。这份源码适合正在准备毕业设计或数据库课程设计的人也适合刚接触SSM想找一份能跑通全流程的源代码做参考的人。这篇文章会从数据表设计讲到审批状态机再到拦截器权限、JSP渲染和Tomcat部署排错最后落到一个很实用的验证技巧上。2. 学生请假系统的数据表设计与SSM接线方式2.1 请假单表的状态字段划分核心表 t_leave 的设计直接决定整个审批流好不好写。常见的单表单状态设计如下CREATE TABLE t_leave ( id INT PRIMARY KEY AUTO_INCREMENT, student_id INT NOT NULL, class_id INT NOT NULL, leave_type VARCHAR(20) DEFAULT 事假, begin_time DATETIME NOT NULL, end_time DATETIME NOT NULL, leave_days INT NOT NULL, reason VARCHAR(500), status TINYINT NOT NULL DEFAULT 0, teacher_opinion VARCHAR(200), dean_opinion VARCHAR(200), create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, KEY idx_student (student_id), KEY idx_status (status) ) ENGINEInnoDB DEFAULT CHARSETutf8;请假单和审批意见放在同一行状态用一个整数表示0代表草稿、1代表待辅导员审批、2代表辅导员已同意但超过三天需要院系领导再批、3代表最终通过、4代表驳回、5代表已销假。这种设计的优势是查询列表时不用JOIN审批记录表劣势是审批历史被覆盖只能看到最终意见。如果要做审计可以把审批记录拆成 t_approve_record 表但课设场景下目前这张表够用。状态迁移关系可以总结成下表后面所有业务方法都围绕这张表实现动作前置状态后置状态执行角色提交请假申请无1学生辅导员同意且请假天数≤313辅导员辅导员同意且请假天数312辅导员辅导员驳回14辅导员院系领导同意23院系领导院系领导驳回24院系领导学生销假35学生注意“超过三天”这个边界条件请假4天及以上才会进入状态2第3天以内的请假辅导员直接批通过。如果把这个判断写成 3那正好请假的第3天就会被误报到院系领导那里。2.1.1 为什么用状态整型而不是多个布尔字段用 is_teacher_approve、is_dean_approve、is_pass、is_complete 四个布尔字段看起来直观但会出现非法组合例如既没通过又销了假。整型状态配合统一的状态常量类才能保证只走合法路径public class LeaveStatus { public static final int DRAFT 0; public static final int WAIT_TEACHER 1; public static final int WAIT_DEAN 2; public static final int APPROVED 3; public static final int REJECTED 4; public static final int COMPLETED 5; }Service 层只引用这些常量不写魔法数字。后面如果要扩展成“辅导员退回修改”之类的状态加一个常量并补一条合法迁移路径即可不需要改表结构。2.2 SpringMVC 与 Mybatis 的配置接线SSM 没有 Spring Boot 的自动配置所有组件都要在 XML 里显式声明。看一个典型工程的 web.xmlservlet servlet-namespringmvc/servlet-name servlet-classorg.springframework.web.servlet.DispatcherServlet/servlet-class init-param param-namecontextConfigLocation/param-name param-valueclasspath:spring/springmvc.xml/param-value /init-param load-on-startup1/load-on-startup /servlet servlet-mapping servlet-namespringmvc/servlet-name url-pattern//url-pattern /servlet-mappingurl-pattern 配成/表示所有请求都进 SpringMVC静态资源需要在 springmvc.xml 里用 mvc:resources 单独放行否则 bootstrap.css、ace.min.css 这些前端样式会被拦截返回404。load-on-startup 填 1 会启动时就初始化 DispatcherServlet部署后系统能不能用第一时间就能知道不用等第一个请求。Mybatis 侧的配置更容易踩坑。spring-context.xml 里要同时指定 SQL 映射文件路径和 Mapper 接口扫描包bean idsqlSessionFactory classorg.mybatis.spring.SqlSessionFactoryBean property namedataSource refdataSource/ property namemapperLocations valueclasspath:mapper/*.xml/ property nametypeAliasesPackage valuecom.school.leave.entity/ /bean bean classorg.mybatis.spring.mapper.MapperScannerConfigurer property namebasePackage valuecom.school.leave.dao/ /beanmapperLocations 指向 SQL 映射文件目录接口文件放在 dao 包下启动时每个 Mapper 接口会自动绑定同名的 XML。typeAliasesPackage 让实体类在 XML 里可以用类名小写作为别名例如直接写resultTypeleaveForm不用写全限定名。2.2.1 父容器和子容器为什么要拆两个 XMLSpring 容器扫描 Service、DaoSpringMVC 容器只扫描 Controller。如果 Controller 扫描范围写成了 com.school.leave 整个包Controller 里的 Service 会被 SpringMVC 子容器重新实例化从而绕过 Spring 父容器的事务代理Transactional 直接失效。常见做法是 spring-context.xml 里 exclude 掉 Controllerspringmvc.xml 里 include 只扫 Controller。这也是刚搭 SSM 时最容易排查半天的坑。2.3 审批操作的事务边界放在 Service 层辅导员审批这个动作至少涉及“更新请假单状态”和“写班级汇总记录”两步把 Transactional 写在 Service 实现类上才能保证这两条 SQL 要么都成功、要么都回滚。Controller 不要写事务因为 Controller 还要做视图转发等操作事务结束得太晚反而占用数据库连接。Transactional(rollbackFor Exception.class) public void approve(Integer leaveId, Integer teacherId, boolean pass, String opinion) { LeaveForm form leaveMapper.selectById(leaveId); if (form null || form.getStatus() ! LeaveStatus.WAIT_TEACHER) { throw new BusinessException(申请状态已变化请刷新后重试); } if (pass) { int newStatus form.getLeaveDays() 3 ? LeaveStatus.WAIT_DEAN : LeaveStatus.APPROVED; leaveMapper.updateStatus(leaveId, LeaveStatus.WAIT_TEACHER, newStatus, opinion); } else { leaveMapper.updateStatus(leaveId, LeaveStatus.WAIT_TEACHER, LeaveStatus.REJECTED, opinion); } }rollbackFor 必须写 Exception.class因为 Spring 默认只对 RuntimeException 回滚自定义 BusinessException 如果继承自 Exception 就不会触发回滚。updateStatus 的 SQL 里带and status #{oldStatus}作为乐观锁条件两个辅导员同时处理同一条申请时只有第一个人的更新影响行数为1第二个人影响行数为0就能识别出并发冲突。3. 请假审批流与销假闭环三个角色之间的状态机实现3.1 拦截器实现角色权限控制三个角色对应的功能模块可以按下表划分角色URL 前缀功能模块学生/student/**请假申请、修改、进度查询、历史记录、销假辅导员/teacher/**待处理申请、请假管理、销假管理、班级汇总、领导审批管理院系领导/dean/**请假审批管理菜单在 JSP 里可以用c:if控制显示但真正安全边界在 URL 层。常见做法是写一个 LoginInterceptor在 preHandle 里判断 session 中的登录用户和请求路径前缀Override public boolean preHandle(HttpServletRequest req, HttpServletResponse resp, Object handler) throws Exception { Object user req.getSession().getAttribute(loginUser); if (user null) { resp.sendRedirect(req.getContextPath() /login); return false; } String uri req.getRequestURI(); if (uri.contains(/student/) !student.equals(((User) user).getRole())) { resp.sendRedirect(req.getContextPath() /login); return false; } if (uri.contains(/teacher/) !teacher.equals(((User) user).getRole())) { resp.sendRedirect(req.getContextPath() /login); return false; } return true; }注册拦截器时要在 excludePathPatterns 里放行 /login、/logout 和静态资源路径。拦截器只能挡住入口不能防横向越权所以 Controller 里还需要再校验一次数据归属查询请假单时把当前登录学生的 id 也传进去而不是只靠 URL 里的 id 参数。3.2 学生端请假申请的提交与校验学生提交请假申请时Controller 先做基础校验结束时间不能早于开始时间请假天数计算规则统一按自然日处理long days (end.getTime() - begin.getTime()) / (1000 * 60 * 60 * 24) 1;这个公式计算的是包含开始当天的自然日天数请半天假也记作1天。如果学校要求区分上午下午就得改成按小时段计算表结构也要相应增加 half_day 之类的字段。提交成功插入一条 status1 的请假单辅导员端立即可见。学生端的“修改请假信息”功能只能作用于待审批状态SQL 里要加and status 1。一旦辅导员已经处理修改接口必须拒绝否则会出现学生把已通过的请假原因改掉、辅导员还不知道的情况。提交建议用 AJAX 请求、接口返回{code: 200, msg: 提交成功}失败时前端可以保留表单内容不用整页刷新。3.3 辅导员端处理待办、跨级别上报与班级汇总辅导员待处理列表是状态 1 的数据对应的 Mapper 查询select idselectPendingByClass resultTypeLeaveForm SELECT * FROM t_leave WHERE status 1 AND class_id #{classId} ORDER BY create_time DESC /selectclassId 不是前端传的而是辅导员登录后从用户表关联的班级信息里查出如果辅导员带多个班改成IN (SELECT class_id FROM t_class_teacher WHERE teacher_id #{teacherId})。同意时执行的更新语句update idupdateStatus UPDATE t_leave SET status #{newStatus}, teacher_opinion #{opinion}, update_time NOW() WHERE id #{id} AND status #{oldStatus} /updateupdateStatus 的返回值如果是0Service 层抛出“该申请已被处理请勿重复提交”。超过三天的申请 newStatus 传 2进入院系领导列表不超过三天直接置为 3学生端就能看到“已通过”。辅导员端“已处理申请”页面要展示 3、4、2 三种状态而不是只查 3否则被上报的申请在辅导员维度会凭空消失。3.3.1 班级请假汇总的统计口径班级请假管理模块一般给辅导员一张聚合表统计某段时间每天请假人数和获批人数。参考 SQLSELECT DATE_FORMAT(create_time, %Y-%m-%d) AS leave_date, COUNT(*) AS apply_count, SUM(CASE WHEN status IN (2, 3, 5) THEN 1 ELSE 0 END) AS approved_count FROM t_leave WHERE class_id #{classId} AND create_time #{startDate} GROUP BY DATE_FORMAT(create_time, %Y-%m-%d) ORDER BY leave_date DESCapproved_count 统计状态 2、3、5因为状态 2 已经辅导员同意、只差院系领导签字从辅导员视角就是“批准通过”。如果只统计状态 3被上报的申请就全部漏掉。GROUP BY 的表达式和 SELECT 里保持一致否则 MySQL 5.6 在开启 ONLY_FULL_GROUP_BY 时会直接报错。3.4 院系领导审批与销假闭环院系领导端只查 status2 的申请同意改 3、驳回改 4。驳回是终态不会退回辅导员重新处理学生只能重新提交申请。销假是请假结束后登记回校。这里在 t_leave 上增加一个 complete_time 字段比单独建销假表更简单状态从 3 变成 5 就完成闭环。防止重复销假的思路和前面一致update 的条件带上 status3Transactional(rollbackFor Exception.class) public void complete(Integer leaveId, Integer studentId) { int rows leaveMapper.updateStatus(leaveId, LeaveStatus.APPROVED, LeaveStatus.COMPLETED, 学生已销假); if (rows 0) { throw new BusinessException(销假失败申请可能已销假或未通过审批); } }这段逻辑是验证事务是否生效的最好测试点销假成功后故意在汇总表插入一条记录并抛异常如果状态回滚成 3说明事务代理生效如果状态变成 5 而汇总缺失说明 Transactional 配置有问题优先检查 Service 类是否被 Controller 子容器重复扫描。4. JSP Bootstrap 页面组织与后端数据交互细节4.1 页面目录结构与静态资源引用项目的 JSP 页面按角色放在 /WEB-INF/jsp/student/、/WEB-INF/jsp/teacher/、/WEB-INF/jsp/dean/ 三个目录下。放在 WEB-INF 里的 JSP 不能被浏览器直接访问必须由 Controller 转发这样散落页面不会被直接输入 URL 拉到。Controller 里返回的字符串要和目录结构对应RequestMapping(/student/apply) public String applyPage() { return student/apply; }页面引用静态资源时建议用绝对上下文路径先在页面顶部定义${ctx}c:set varctx value${pageContext.request.contextPath}/然后 css 路径写${ctx}/static/css/bootstrap.min.css。如果使用相对路径在 /student/apply 这种一级 URL 下没问题嵌套到 /student/leave/detail 二级路径时 css 就会全部加载失败。ace.min.css、bootstrap.css、font-awesome.min.css 这些样式统一放在 webapp/static/ 下并在 springmvc.xml 配置mvc:resources mapping/static/** location/static//放行。4.2 JSP 渲染列表还是 AJAX 返回 JSON请假列表如果只是给当前登录用户看历史记录用 JSTL EL 服务端渲染即可直接返回 ModelAndView代码简单、便于调试。但“请假申请现状”这类页面需要实时刷新审批进度用整页 JSP 刷新体验比较差。常见做法是保留 JSP 页面骨架用 jQuery 的 $.ajax 请求返回 JSON 的接口RequestMapping(value /student/leave/detail/{id}, method RequestMethod.GET) ResponseBody public MapString, Object detail(PathVariable Integer id, HttpSession session) { LeaveForm form leaveService.getByIdAndStudent(id, currentUserId(session)); MapString, Object data new HashMap(); data.put(status, form.getStatus()); data.put(teacherOpinion, form.getTeacherOpinion()); data.put(deanOpinion, form.getDeanOpinion()); return data; }这里不需要在 JSP 里手动 import JSONArrayResponseBody 会调用 Jackson 把 Map 序列化成 JSON。如果老项目是 Spring 3.x需要检查消息转换器是否显式设置了 UTF-8否则返回的中文会变成乱码。这个接口里的 currentUserId 必须来自 session不能接收前端传的 studentId保证学生只能查自己名下的单。4.2.1 前端轮询审批进度不引入 WebSocket 的情况下用 setInterval 每 10 秒拉一次进度接口就能满足课设演示需求。前端代码function refreshStatus() { $.get(ctx /student/leave/detail/ leaveId, function (res) { if (res.status 3) { $(#statusLabel).text(已通过可销假); $(#completeBtn).removeAttr(disabled); } }); } setInterval(refreshStatus, 10000);页面销毁时记得 clearInterval否则隐藏页面还在持续发请求。后端根据 status 数字映射文案不要把状态判断散落在前端多个地方如果状态枚举变了只要改后端这一个映射函数前端不用跟着动。4.2.2 历史记录查询的分页与数据权限学生历史记录查询不要一次性把全学期数据拉出来Mapper 层直接做分页select idselectHistoryByStudent resultTypeLeaveForm SELECT * FROM t_leave WHERE student_id #{studentId} ORDER BY create_time DESC LIMIT #{offset}, #{limit} /selectoffset (pageNum - 1) * pageSize。studentId 从 session 里的登录对象取前端传上来的 id 只做二次校验。所有带明细查看的接口都要这样做否则把 URL 里的参数改成别人的 id 就能越权看到别人的请假理由和审批意见。4.3 学生销假页面的状态展示细节学生端销假入口一般放在“请假申请现状”页面里只有 status3 的申请才显示销假按钮。页面展示可以做成一个横向步骤条待辅导员审批 → 辅导员同意 → 院系领导审批 → 已通过 → 已销假后端返回 status前端根据当前状态点亮对应节点。这个步骤条用 Bootstrap 的 button 组加简单 CSS 就能实现不需要引入额外插件。重点是销假按钮点击后要二次确认因为状态从 3 改成 5 后无法自动退回误操作只能由管理员直接改数据库系统里没有那么细的角色。5. 部署运行与排错先看 JSP 编译目录5.1 从压缩包导入工程与 MySQL 5.6 连接配置源码包解压后先看根目录有没有 pom.xml没有就是典型的 Eclipse Dynamic Web Project。Eclipse 导入用 Existing Projects into WorkspaceIDEA 导入后需要重新补 Web Facet 和 Artifact否则 Tomcat 启动时找不到 Spring 的依赖 jar。数据库连接写在 jdbc.properties 里MySQL 5.6 建议配合 5.1.4x 版本的 mysql-connector-java 驱动驱动类名是 com.mysql.jdbc.Driver如果用了 8.x 驱动连 5.6 库连接串里必须加 useSSLfalse 和 allowPublicKeyRetrievaltrue否则起来直接报 SSL 握手失败。5.2 Tomcat 启动后怎么定位 JSP 和 Spring 的报错页面 500 时先看控制台异常JSP 自身的错误要等第一次访问才暴露。Tomcat 的 work 目录work/Catalina/localhost/项目名/org/apache/jsp/下能看到每个 JSP 编译后的 _jsp.java 和 _jsp.class 文件打开编译后的 Java 源码能直接定位到 JSP 第几行的 EL 表达式被解析成了空值或者哪个 JSTL 标签依赖的 jar 没打包进 lib。JSP 编译 class 文件保存的位置是这里别在整个工程里用文件名搜 _jsp.java那个是运行时生成物不在源码目录里。5.3 修改 JSP 不生效时清 work 目录开发中经常遇到改了 JSP 刷新页面还是旧内容、CtrlF5 也无效的情况。原因通常是文件时间戳被还原得比服务器端旧Tomcat 认为源文件没有变化、就不会重新编译。解决办法是把项目从 Tomcat 移除后删掉work/Catalina/localhost/项目名整个目录再重新部署让 Tomcat 强制重新编译所有 JSP。如果改完 Java 代码也不生效除了重新发布还要检查是否开了热部署的 reloadable 配置关闭 reloadable 时只能重启 Tomcat 才能加载新的 class。本文还有配套的精品资源点击获取
RELATED — 相关阅读

相关资讯

LATEST — 最新资讯

最新发布

TODAY — 本日精选

新闻

WEEKLY — 本周精选

新闻

MONTHLY — 本月精选

新闻