☰
JavaWeb教务管理系统源码解析:从部署到排课冲突检测实战
2026/10/18 12:04:59 网站建设 项目流程

简介:这是一份基于JavaWeb的教务管理系统项目源码,面向正在学习Servlet、JSP与MVC分层架构的初学者,也适合作为课程设计或毕业设计的参考。整个系统采用MVC设计模式、DAO数据访问模式、过滤器与MySQL数据库,业务上分为登录系统以及学生、教师、教务员、系统管理员四大角色模块,功能覆盖成绩查询、选修与考级报名、学籍信息查看与修改、考级成绩查询、教师个人信息维护、教务员成绩与报名名单管理、系统管理员用户管理等,逻辑链路完整。资源包为RAR压缩格式,共包含223个文件,压缩后大小约2.6兆。其中,82个Java源文件与对应的82个class文件构成了完整业务代码,38个JSP页面负责界面展示,6个jar包为运行所依赖的库,另有SQL脚本、XML配置文件、Eclipse工程配置与README说明文档,导入开发环境后即可配合Tomcat与JDK运行部署。截至目前,已有11657人学习下载;项目内部预留了部分待实现功能,学习者可以在现有代码框架上继续扩展,例如新增考核模块、优化界面或增强权限控制,从而深入理解基于三层架构的JavaWeb开发流程。

1. 教务管理系统 javaweb 项目:为什么我拆完源码后没去重写一遍

教务管理系统 javaweb 项目,网上一搜一大把,但真正能跑通、能改课表、能处理教师-班级-教室冲突的版本并不多。我前后拆过四套同类源码,这套是结构上最接近生产环境的:登录分离、权限走拦截器、排课带基本冲突检测,代码量不到 5 万行,适合做课程设计和毕设改造。本文按我从下载到二次开发的顺序写,先把模块和三层结构讲透,再贴出部署参数和排错记录。如果你正打算拿它交作业或做毕设底子,照着复现一遍,比你从零写省下至少两周时间。

2. 从登录到排课:这套 javaweb 系统的模块划分与三层架构落点

2.1 用户-角色-权限三张表的设计逻辑

大多数教务管理系统的权限模型都绕不开 RBAC,这套源码用的是最标准的五表实现:user、role、permission 三张主表,外加 user_role、role_permission 两张关联表。初次打开建库脚本时容易懵,为什么不直接给 user 表加一个 role 字段?原因很简单:真实教务场景下,一个教师可能同时是班主任和课程负责人,一个人的角色不是单选。如果加字符字段存“1,2,3”,查询时用 like '%2%',数据量一上来就是灾难。

实际表结构里,user 表只存 id、username、password、real_name、status 五个字段,status 用 tinyint 控制启用/停用,删除用户时不做物理删除,而是把 status 置 0。这是我从这套源码里学到的第一点:凡是涉及登录的系统,用户表默认保留而不是删除,否则历史课表关联的数据会全断。

角色表和权限表的字段更简单,role 表有 id、role_name、role_code、description;permission 表是 id、perm_name、perm_code、url。注意这里有一个坑:权限表的 url 字段存的是控制器路径,比如 /course/add、/teacher/update,和后面拦截器里匹配的字符串必须完全一致,大小写都不能差。我第一次跑的时候在权限配置里写了 /course/add/,结尾多了一个斜杠,拦截器永远匹配不上,页面一直提示“无权限”。后来我把所有权限 URL 都改成不带斜杠、不带问号的规范形式,这个问题才消失。

2.2 登录鉴权与拦截器实现

登录流程是这套代码里最容易看懂但也最容易改坏的部分。用户提交表单后,LoginController 先调 UserService.login(username, password),Service 层拿到输入后把密码用 MD5 加盐再比对数据库。这里有个细节:源码里的盐是写死在常量类里的,不是每个用户随机盐。安全角度说这不是最佳实践,但作为教学项目,它的优点是好讲、好验证。

登录成功后,代码把 user 对象塞进 session,key 叫 LOGIN_USER,同时写入 user_id 和 real_name。拦截器那边三步走。第一步,判断请求路径是否在白名单里。白名单写在 springmvc-servlet.xml 里,默认放行 /login、/login.jsp、/css/、/js/、/images/**。第二步,从 session 里取 LOGIN_USER,取不到就重定向到 /login.jsp。第三步,取到用户后,把请求 URL 和该用户拥有的权限 URL 集合做匹配,匹配通过才放行。

我一般会把拦截器的第三步单独拿出来看,因为这决定了你加新页面时要不要同步配权限。这套源码用的是 SimpleUrlHandlerMapping 的 excludePathPatterns 来设置外部忽略路径,内部权限校验用自定义 PermissionInterceptor。如果你的项目里新增了一个 /timetable/export 控制器,必须先在 permission 表插入对应记录,再给角色分配权限,否则拦截器会把你拦在门外。这不是 bug,是设计如此。很多同学在这里翻车,以为自己写的代码没问题,实际是漏了数据库权限配置。

2.3 排课模块的核心数据流动

排课是教务系统的心脏,这套源码的排课功能不算智能,只有一个“手动排课 + 冲突校验”。数据流动大概是:页面选择学期、教师、课程、班级、教室、星期几、第几节,提交到 CourseArrangeController,控制器接收后封装成 CourseArrange 对象,传给 Service,Service 先调 checkConflict() 做校验,通过后调 DAO 插入数据库。

冲突校验的规则在 Service 里写得很直白。校验分三类:同班级同一时间段不能有两门课;同教师同一时间段不能有两门课;同教室同一时间段不能有多门课。代码实现不复杂,就是从 course_arrange 表里按 teacher_id、class_id、classroom_id 分别 count。但要注意它的时间维度:表里存的是 week_day(1-7 表示周一到周日)和 period(1-4 表示上午一二节、三四节、下午五六节、七八节),没有存具体周次。也就是说,这套系统不支持单双周排课,所有课默认每周上都上。如果你要做毕设,这就是一个明显的可扩展点:加一个 start_week 和 end_week 字段就能支持按周次排课。

课程表查询那块,代码里用的是三表 join,course_arrange 关联 course 表和 teacher 表,外加一个 classroom 表。SQL 写在 CourseArrangeMapper.xml 里,有一个比较值得关注的细节:查询列表时没有按学期过滤,只在页面层用下拉框选了学期然后传给 SQL 的 term_id。如果你复制这份代码改项目,建议在 DAO 层就把 term_id 作为必传参数加入 SQL 条件,否则列表会越积越多,查询越来越慢。这不是危言耸听,我见过有同学拿这套系统运行了一个学期,课表页面打开要 6 秒,原因就是列表页没做分页、也没做学期筛选。

3. 跑通项目:从部署环境到第一次登录的管理员账号

3.1 环境准备:JDK、Tomcat、MySQL 版本怎么配才不玄学

拿到源码压缩包之后,先别急着解压,先把环境版本对齐。这套教务管理系统基于 Java 8 编写,项目里用了不少 Lambda 表达式和 stream 操作,所以 JDK 必须 8 及以上,建议直接装 8。Tomcat 我用的是 8.5,Tomcat 9 也能跑,但有一个点要注意:Tomcat 10 把 javax.servlet 包名改成 jakarta.servlet,而 maven 依赖里默认导入的是 javax,直接用 Tomcat 10 会报 ClassNotFoundException。这不是代码问题,是版本兼容问题。

MySQL 建议用 5.7 或 8.0。建库脚本里默认是 utf8mb4 字符集,MySQL 5.5 以下不支持 utf8mb4,所以 5.5 及以下版本直接跳过。数据库连接驱动用的是 mysql-connector-java 8.0.11,如果本地 MySQL 是 5.7,这个驱动完全兼容;如果你换用 MariaDB,需要把驱动改成 mariadb-java-client。这个地方属于最常见的环境翻车点,我见到太多人卡在“DriverManager.getConnection 报 Communications link failure”上,最后发现是驱动版本和数据库版本不匹配。

3.2 导入工程与建库脚本执行

解压后的目录结构一般是:sql/、src/、WebContent/、pom.xml。先打开 SQL 目录,里面通常会有一个 edu_admin.sql 或者 init.sql。用 Navicat 或命令行执行。这里我习惯用命令行,步骤是:

mysql -u root -p -e "CREATE DATABASE edu_admin DEFAULT CHARACTER SET utf8mb4;" mysql -u root -p edu_admin < ./sql/edu_admin.sql

执行完建库脚本后,进数据库看一下表数量和初始数据:

mysql -u root -p -e "USE edu_admin; SHOW TABLES; SELECT * FROM user;"

注意:源码自带的 admin 账号初始密码大多也是 admin 或者 123456,如果 user 表里密码字段是密文,你需要先用脚本里的 MD5 工具类生成新密文然后 update。我一般直接查表,确认 admin 存在再改,避免登录后下一步动作被密码卡住。脚本执行失败时,优先检查 SQL 文件编码。很多源码的 SQL 文件是 UTF-8 BOM,在 Windows 命令行导入会报第一个字段名错误,用 Notepad++ 转成 UTF-8 无 BOM 再导就好。

3.3 配置文件里的三处必改参数

工程导入 IDE 后,打开 src/jdbc.properties(或用 spring-datasource.xml)找到数据库连接配置。默认写的是:

jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/edu_admin?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false jdbc.username=root jdbc.password=123456

这里三处必改。第一,jdbc.password 改成你自己的数据库密码,别用默认的 123456。第二,jdbc.url 里的 serverTimezone=Asia/Shanghai 必须保留,MySQL 8.0 默认时区是 UTC,不加这个参数会报 The server time zone 相关错误。第三,如果数据库名称改了,同步改 edu_admin 这个库名。改完这个文件,再改 WebContent/WEB-INF/web.xml 里的 spring 配置文件路径,确认 contextConfigLocation 指向的是 classpath:springmvc-servlet.xml,而不是写死的绝对路径。最后改连接池参数。默认的 dbcp 连接池初始连接数是 5,最大连接数是 20,如果部署到云服务器内存 256M,建议初始连接数改成 2,最大连接数改成 10,否则启动后频繁 GC 影响性能。

3.4 启动顺序与常见启动失败信号

启动顺序建议是:先启动 MySQL,再启动 Tomcat。如果先启动 Tomcat,Tomcat 初始化 Spring 容器时连接不到数据库,会直接启动失败并报 DataSource 初始化异常。很多人在 Tomcat 日志里看到这个报错就以为代码有问题,其实只是数据库没起来。

启动 Tomcat 后,重点看 catalina.out 里有没有出现 “Initializing Spring root WebApplicationContext” 和 “Loaded JDBC driver” 这两行。看到说明 Spring 容器加载完成。接着访问 http://localhost:8080/项目名/login.jsp 登录。这里的项目名如果你是直接用 Eclipse 的 Dynamic Web Project 运行,默认是工程名;如果是打成 war 包丢到 Tomcat 的 webapp 下,就是你 war 包的名字,比如 edu_admin.war 访问路径就是 /edu_admin。

如果页面 404,先检查是不是 URL 大小写错了。Tomcat 的 URL 是区分大小写的。如果页面出来了但样式丢了,检查控制台有没有报 /css/bootstrap.min.css 404。这个问题几乎都是因为 jsp 页面里引用了绝对路径/styles而项目部署名不是 ROOT。解决方式是把页面里的静态资源链接改成${pageContext.request.contextPath}/styles,或者在 jsp 顶部加<base href="<%=basePath%>">。这是我们这个项目最常见的部署期问题之一,后面避坑清单里还会重点讲。

4. 教务管理系统避坑清单:拦截器失效、乱码、Tomcat 路径与四大常见问题

4.1 登录页加载了但 ajax 请求全部 404

现象:登录页正常显示,输入账号密码后点登录,F12 里看到 POST /login 返回 404,而 GET 打开登录页是好的。

原因:Ajax 请求的 URL 用的相对路径。登录页在这个系统里是 login.jsp,位于 WebContent 根目录,如果你用 form action="login" 提交,浏览器会把它解析为当前目录下的 login,即 /项目名/login,这个是通的。但如果你在子目录的 JSP 页面里写 ajax url: "login" 或者 "doLogin",就会带到子路径下,映射不到控制器。

解决:统一使用${pageContext.request.contextPath},代码改成:

$.ajax({ url: "${pageContext.request.contextPath}/login", type: "POST", data: { username: username, password: password }, success: function(res) { if (res.code === 200) { window.location.href = "${pageContext.request.contextPath}/index.jsp"; } } });

这段代码的逻辑在于把部署路径拼进请求地址,避免子页面引发路径错乱。contextPath 是容器自动生成的,不管 war 包改名还是部署成 ROOT,它都会正确解析。从那以后我规定自己项目里所有 JSP 动态资源引用必带 contextPath,不再写裸相对路径。

4.2 后台管理页面中文乱码

现象:登录进来后,左侧菜单正常,但页面表格里的学生姓名全部显示为问号?。

原因:JSP 页面默认不强制指定响应编码,导致服务器以 ISO-8859-1 输出中文字符。数据库已经是 utf8mb4,但 response 的 contentType 没有声明 charset。

解决:在 JSP 顶部加一行,保证所有页面统一 UTF-8:

<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="utf-8" %>

同时检查 web.xml 里有没有配置 CharacterEncodingFilter。这个系统源码里默认没有加,需要自己补。我给很多项目加过这种过滤器,代码一般写在 web.xml 的 servlet 映射之前,确保它最先执行:

<filter> <filter-name>encodingFilter</filter-name> <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class> <init-param> <param-name>encoding</param-name> <param-value>utf-8</param-value> </init-param> </filter> <filter-mapping> <filter-name>encodingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>

加完过滤器后重启 Tomcat,再刷新页面,中文基本恢复。如果还有部分字段乱码,再检查 MySQL 连接串里的 characterEncoding=utf8,这个参数在 jdbc.properties 里已经存在,确认没被注释即可。

4.3 明明配了拦截器却放行了所有请求

现象:登录失效后,直接输入 index.jsp 地址也能进后台,拦截器形同虚设。

原因:拦截器类被注册到了 SpringMVC 的拦截器链里,但没有在 springmvc-servlet.xml 里配置 mvc:interceptors 的 mapping path。很多 Spring 项目的拦截器注册方式是在 Spring 容器里配一个 Bean 就行,但这个项目用的是注解扫描和 XML 混合模式,必须在配置文件里显式声明拦哪个路径。

解决:检查 springmvc-servlet.xml 里这一段:

<mvc:interceptors> <mvc:interceptor> <mvc:mapping path="/**"/> <mvc:exclude-mapping path="/login.jsp"/> <mvc:exclude-mapping path="/login"/> <bean class="com.edusys.interceptor.PermissionInterceptor"/> </mvc:interceptor> </mvc:interceptors>

关键点是 path="/" 要写全,之前有人写了个 path="/" 只匹配根路径,等于没拦。另外 exclude-mapping 里如果漏掉 /js/和 /css/**,静态资源也会被拦截,页面样式全丢。配完重启后再试一次:不登录直接访问 /index.jsp,如果被重定向到 /login.jsp,说明拦截生效了。这个项目里还有一个细节:自定义的 PermissionInterceptor 里如果调用了 req.getSession() 并且拿不到 session 就返回 false,一定要在配置里把当前控制器方法设为无需登录也可以访问,否则会死循环重定向到登录页。

4.4 课表查询慢:SQL 没走索引

现象:课表页面在数据量 3000 条以上时,打开要 3-5 秒;班级多的大专业,甚至直接超时。

原因:course_arrange 表只建了主键索引,而查询条件是 teacher_id + week_day + period,完全没有索引可用。源码里 checkConflict 方法要查同教师同时间段冲突,这条 SQL 是全表扫描。数据量小时没问题,一学期排课超过两千条就开始卡。

解决:给 course_arrange 表补两条联合索引。执行以下 SQL:

ALTER TABLE course_arrange ADD INDEX idx_teacher_time (teacher_id, week_day, period); ALTER TABLE course_arrange ADD INDEX idx_class_time (class_id, week_day, period);

索引字段顺序有讲究:最左边放等值条件,放 teacher_id 或 class_id,因为查询一定是先按教师或班级过滤,再用时间条件过滤。如果把 week_day 放第一列,查询时会用不到索引前缀,等于零效果。加完索引后再跑一次原查询,如果执行计划 type 从 ALL 变成 ref 才算成功。不要一股脑给所有字段都加索引,写操作会变慢,而且磁盘占用会变大。

4.5 导出 Excel 时报内存溢出

现象:点击“导出课表”按钮后,Tomcat 控制台直接抛 java.lang.OutOfMemoryError: Java heap space。

原因:系统导出用了一个公共工具类,直接把整个 List 的所有数据一次性装载进内存再生成 Excel。数据量从几百跳到几千,堆内存不够了。Tomcat 8.5 默认的 JVM 堆是 256M,这个体量确实扛不住。

解决:先不改代码,把 Tomcat 启动参数加一下。在 catalina.sh 的 JAVA_OPTS 里加上:

JAVA_OPTS="-Xms512m -Xmx1024m -XX:MaxMetaspaceSize=256m $JAVA_OPTS"

这个参数的含义是:初始堆 512M,最大堆 1024M,元空间 256M。小规模项目足够用。如果数据量继续涨,就需要换导出方案,把 POI 的 SXSSFWorkbook 做成流式写入,边查边写,不把全量数据留在内存。这个属于进阶改动,我一般建议在毕设阶段先把 JVM 参数加上,展示时不出错优先,源码改造留到答辩加分项。

5. 改成课程冲突检测:用一个周末给系统加上防撞课校验

5.1 冲突检测的三维判定规则

原版系统在排课提交时已经做了基础冲突校验,但它是三个独立 count,存在一个漏洞:如果同一教师在同一时间段被排到不同教室,原方法在教师维度能查出来,但班级维度查出来的结果是不同班级不同教室互不干扰,这条记录其实不应该写入。我的做法是把三维判定合并成一条 SQL 查重。规则是:当新增记录和已存在记录满足以下三个条件同时成立时,判定为冲突:

  • 周几相同
  • 第几节相同
  • 教师、班级、教室三个资源至少一个相同

注意这里的不是三维交集,而是“资源有重叠就算冲突”。这是教务场景的常见口径:同一个教室同一天同一节只能安排一门课,同一个班级同一天同一节只能安排一门课,同一个教师同一天同一节只能安排一门课。我拆代码时发现原版把教室校验漏了,补上之后才算完整。

5.2 新增 DAO 层查询方法与 Service 层业务逻辑

在 CourseArrangeMapper.xml 里加一条查重 SQL:

<select id="countConflict" resultType="int"> SELECT COUNT(*) FROM course_arrange WHERE week_day = #{weekDay} AND period = #{period} AND ( teacher_id = #{teacherId} OR class_id = #{classId} OR classroom_id = #{classroomId} ) </select>

这条 SQL 的逻辑是:把满足时间条件的记录筛出来,再做资源重叠判断。OR 条件的写法注意括号,漏掉括号会变成所有记录都满足。DAO 接口里对应加:

int countConflict(ArrangeQuery query);

Service 层校验方法改成:

public boolean checkConflict(CourseArrange arrange) { int conflictCount = arrangeDao.countConflict(arrange); if (conflictCount > 0) { return false; } return true; }

这里的 intent 是“有冲突就给前端返回 false”,而不是继续插入。参数对象 CourseArrange 里需要包含 weekDay、period、teacherId、classId、classroomId 五个字段,前端传参时一个都不能少。如果漏传 classroomId,count 结果会变小,冲突查不出来,这是最容易踩的测试坑。

5.3 在 Controller 里接入校验结果并回显错误

原 Controller 的插入逻辑是直接调 save 方法,我不动它的主流程,只在校验处加判断:

if (!courseArrangeService.checkConflict(arrange)) { model.addAttribute("error", "该时段的教师/班级/教室存在排课冲突"); return "arrange/add"; } courseArrangeService.save(arrange); return "redirect:/arrange/list";

这里有一个关键点:校验失败后必须 return 当前页面,而不是 simple 字符串,否则表单数据全部丢失。同时要把 arrange 对象放回 model 里,前端表单才能回显已填数据。这个习惯是从反复丢失表单里学来的,我现在每个编辑页都会先想“校验失败后用户填的东西还在不在”。

5.4 验证效果:用同一教师和同一时段做回归测试

改完代码后,验证这一步不能省。我常用的验证方案是造两组测试数据。第一组:同一教师 A,周一第 1-2 节,分别排给两个不同班级不同教室。预期结果:第二次提交被拦截,页面显示冲突提示。第二组:同一教室,周一第 1-2 节,安排不同教师不同班级。预期结果同样被拦截。

测试时只走页面点按钮,不看数据库,因为拦截器和前端交互才算闭环。如果页面提示了冲突但没回显填写内容,说明 model.addAttribute 回显没做好,再回头改。验证完成后,再去排课列表确认没有插入冲突记录,这一步确认的是数据库层面没有脏数据。

说个实话,我第一次改这个功能时,漏了 classroom_id 的条件,只查了教师和班级两个维度,测试用例全部通过。后来学校同一天有两个班在同一教室被排重了,才知道原因。从那以后我每次改排课逻辑,都会强制走一遍“同教师 + 同教室 + 同班级”三个角度各测一次,缺一不可。希望这些踩坑记录能帮你把这个项目啃得更稳,不只是让它跑起来,而是让它在你手里变成一套能答辩、能演示、敢改代码的系统。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询