简介:一套基于Java核心技术的实验教学管理系统完整源码,面向教育技术开发者、Java初学者及高校实验管理人员,覆盖用户管理、课程安排、实验报告提交等典型场景。资源包总文件131个,约1.95MB,包含73个Java后端源文件、12个XML配置、12个Vue组件与12个JavaScript前端文件,以及SQL数据库脚本、properties配置和可执行JAR包,清晰呈现从后端逻辑到前端交互的完整技术栈。已有320人学习下载,便于参考企业级项目分层与前后端分离实践。借助该系统源码,可快速掌握Spring/SSM类配置方式与Vue组件化开发思路,同时获得可直接运行的部署包和数据库脚本,适合用于课程设计、毕业设计或教学系统二次开发。
1. 为什么实验教学管理系统总在排课上翻车
高校实验课和理论课最大的区别在于受场地设备约束:一间实验室只有几十个工位,同一时段最多容纳一个班级。传统做法是管理员用 Excel 手工登记预约,报告靠 U 盘拷贝,成绩期末突击补录。流程在实验项目少时勉强能转,课程一多、班级交错,时间冲突和漏登几乎不可避免。
用 Java 写一套实验教学管理系统,核心是把「排课、预约、提交、评分」这条链路数字化。系统面向三类角色:管理员维护实验室与课程,教师发布实验安排并评分,学生在线预约并上传报告。对做毕业设计或课程项目的开发者来说,难点不在增删改查,而在时间冲突判定、文件上传路径和权限控制三个点上。
2. Java 技术选型:为什么把 Spring Boot + MyBatis 当默认解
2.1 单体应用加三层架构就够用
实验教学管理系统的真实并发量很低,实验课预约分时段进行,同时在线人数通常不过几百,没有理由引入微服务和分布式事务。一个 Spring Boot 单体应用配合 MySQL 就是最可靠的方案,源码里也不会有跨服务调用带来的排错负担,部署时一个 jar 包就能跑起来,对实验室服务器这种资源有限的环境尤其合适。
分层沿用经典的 Controller-Service-Mapper:Controller 只做参数校验和路由,Service 处理业务规则,Mapper 负责数据访问。这个项目里最重要的业务规则是「同一实验室在同一时段不能冲突」,它应该写在 Service 层而不是依赖数据库唯一索引,因为冲突判定同时涉及时间区间和实验室编号两个维度,数据库的普通唯一约束表达不了这种组合条件。
2.2 MyBatis 还是 JPA:统计报表决定选型
选 MyBatis 而不是 Spring Data JPA,不是 JPA 不好,而是这类系统里有大量多表关联统计,比如「实验项目平均分按班级分布」「实验室使用率按月汇总」。这类报表 SQL 用注解或 XML 维护更直观,排查问题时把 SQL 拷到 Navicat 里单独执行,定位性能瓶颈也比看 JPQL 容易。下面是标准的依赖清单:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.3.1</version> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency>参数说明:mybatis-spring-boot-starter 2.3.x 对应 Spring Boot 2.7,如果项目升到 Spring Boot 3.x,要换成 mybatis-spring-boot-starter 3.0 以上版本,否则启动时 SqlSessionFactory 创建直接失败。Lombok 在实体类里省掉 getter/setter,能减少近一半样板代码,但 IDE 必须装对应插件,否则编译期报符号找不到,这个问题在团队成员换电脑后最容易出现。
2.3 五个核心子系统的边界
功能上把系统拆成五个子系统,每个子系统对应一个包和一组 Mapper,后面写权限控制时粒度最清晰:
| 子系统 | 核心表 | 主要功能 |
|---|---|---|
| 系统管理 | sys_user, sys_role | 登录、角色分配、用户维护 |
| 实验项目管理 | experiment | 实验基本信息、开放时段维护 |
| 预约管理 | reservation | 学生预约、教师审批、时间冲突校验 |
| 报告管理 | report | 报告上传、退回、重新提交 |
| 成绩管理 | grade | 教师评分、班级统计、导出 |
这五个模块按依赖方向排列:预约依赖实验项目和用户,报告依赖预约,成绩依赖报告和实验。开发时按这个顺序推进,每层都能先跑通再进下一层,不会出现做到一半发现表结构要大改的情况。权限上管理员、教师、学生三种角色分别落在不同的 Controller 前缀上,用拦截器按路径匹配控制访问,比在注解里堆角色名更好排错。
3. 实验教学管理系统数据库设计:八张表撑起业务闭环
3.1 核心表结构和字段取舍
实验教学管理系统不需要二十张表,把业务闭环完整走通,八张表足够:用户表、角色表、实验项目表、预约表、报告表、成绩表、实验室表、公告表。设计时最高频的返工是把「实验项目」和「实验安排」混成一张表,结果同一门课的两个班级无法复用实验内容,被迫在代码里复制数据。
| 表名 | 职责 |
|---|---|
| sys_user | 三类用户统一起存,用 role_id 区分 |
| experiment | 实验项目与具体安排合并,含时间、容量 |
| reservation | 学生预约记录,一学生一实验一条 |
| report | 报告文件元数据,存路径和原始文件名 |
| grade | 评分记录,实验加学生唯一 |
实验表的结构决定了预约和评分的实现方式,SQL 定义如下:
CREATE TABLE experiment ( id BIGINT PRIMARY KEY AUTO_INCREMENT, course_name VARCHAR(64) NOT NULL COMMENT '课程名称', teacher_id BIGINT NOT NULL COMMENT '授课教师id', lab_id BIGINT NOT NULL COMMENT '实验室id', start_time DATETIME NOT NULL, end_time DATETIME NOT NULL, capacity INT DEFAULT 30 COMMENT '最大人数', status TINYINT DEFAULT 1 COMMENT '1开放 0关闭' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;字段设计说明:course_name 直接存课程名而不是课程表外键,这个场景不需要维护完整课程体系,冗余一个字段能少两张表,查询也少一次 join;capacity 是人数硬上限,预约时先查已预约数再比较。teacher_id 直接挂在 experiment 上而不是通过中间表关联,取数方便,代价是实验模板无法多个班级共享,实际使用中影响很小。
3.2 时间冲突怎么在 SQL 层预判
预约模块最核心的 SQL 是「查询某实验室在某个时间段内是否已有安排」,时间区间重叠的判定条件是start_time < 新结束时间 AND end_time > 新开始时间。这个条件在 Java 面试里也常作为基础八股被问到,写错边界条件的概率很高:
SELECT COUNT(*) FROM experiment WHERE lab_id = #{labId} AND status = 1 AND start_time < #{newEndTime} AND end_time > #{newStartTime};参数说明:newStartTime 和 newEndTime 是本次预约想要占用实验室的时间区间,count 大于 0 就说明该实验室这段时间已被占用,直接提示用户更换时段。注意必须用<和>而不是<=和>=,否则前一个安排 10:00 结束、后一个 10:00 开始这种合法衔接会被误判为冲突,在排课密集的实验室里会频繁误报。
3.3 索引设计与常见误用
时间字段和 lab_id 是查询频率最高的条件,联合索引应该建在(lab_id, start_time, end_time)上:先按实验室过滤,再按时间范围过滤,索引选择性最好。初学者常见的做法是 start_time 和 end_time 各建一个索引,MySQL 在范围查询时通常只走其中一个,另一个索引白白占用空间和维护成本。
成绩表上建唯一索引uk_exp_student (experiment_id, student_id),从数据库层面防止教师对同一学生重复打分。这里有个坑:批量插入成绩时一旦遇到重复值,唯一索引冲突会让整批事务回滚,所以批量导入前先按主键查一遍,把已存在的记录改成更新而不是插入,否则一个脏数据会拖垮整批成绩录入。
4. 实验教学管理系统核心功能实现:冲突校验、上传与评分
4.1 预约接口:事务与并发控制
预约接口是整个系统并发问题最集中的地方,典型场景是两个学生同时抢最后一个名额。如果只用「先查询剩余容量再插入」,两条请求可能同时查到剩余 1 个名额,都执行插入后实际超员。常见做法是用 SELECT ... FOR UPDATE 锁住实验记录,再做容量判断:
@Transactional public boolean reserve(Long experimentId, Long studentId) { // 行锁锁定实验记录,防止并发抢名额 Experiment exp = experimentMapper.selectByIdForUpdate(experimentId); if (exp == null || exp.getStatus() != 1) { throw new BusinessException("实验不存在或已关闭"); } if (exp.getReservedCount() >= exp.getCapacity()) { throw new BusinessException("名额已满"); } reservationMapper.insert(experimentId, studentId); // 原子自增已预约人数,配合行锁双保险 return experimentMapper.incrementReserved(experimentId) > 0; }代码说明:selectByIdForUpdate 对应的 SQL 是SELECT * FROM experiment WHERE id = #{id} FOR UPDATE,在事务内对这条记录加行锁,事务提交或回滚后锁才释放,保证同一时刻只有一个请求能通过容量判断。incrementReserved 执行UPDATE experiment SET reserved_count = reserved_count + 1 WHERE id = #{id},用原子自增做二次防护。要注意 @Transactional 只能写在 public 方法上,且不能出现同类内部调用,否则 Spring 的 AOP 代理不生效,锁形同虚设。
4.2 报告上传:路径与大小配置
报告上传是这个项目里最容易翻车的功能。开发时本地跑通,部署到服务器后文件传不上去,十有八九是路径问题——代码里写死 D:/upload,Linux 服务器上根本没有这个目录。正确的做法是把上传目录放到配置里,用环境变量注入:
# application.yml spring: servlet: multipart: max-file-size: 50MB # 单个文件大小上限 max-request-size: 60MB # 单次请求总大小 file: upload-dir: ${UPLOAD_DIR:./upload}参数说明:UPLOAD_DIR 不配置时默认当前目录下的 upload 文件夹,部署时通过--UPLOAD_DIR=/data/teaching/upload覆盖,代码里不需要出现任何绝对路径。max-file-size 控制单个文件上限,Spring Boot 默认只有 1MB,实验报告经常包含截图和视频,不调大就会抛 MaxUploadSizeExceededException。
String realPath = uploadDir + "/" + UUID.randomUUID() + "_" + fileName; File dest = new File(realPath); if (!dest.getParentFile().exists()) { dest.getParentFile().mkdirs(); } file.transferTo(dest);文件落盘后不能只存文件名,原始文件名要单独写进 report 表的 original_name 字段,下载时再从库里取回来拼到 Content-Disposition 响应头里。用 UUID 重命名是为了避免中文文件名和重名覆盖,这两类问题在 Windows 服务器上尤其常见。
提示:上传目录不要放在项目 jar 包同级或临时目录 /tmp 下,服务器重启或重新部署时文件会被清掉,持久化目录和程序运行目录要分开管理。
4.3 成绩录入:校验与事务
成绩录入流程:教师按实验项目列出已交报告的学生,逐个打分后批量提交。批量提交必须在一个事务里完成,任何一条数据校验失败都不能部分入库。Service 里循环校验后逐条插入:
@Transactional public void saveGrades(List<GradeDTO> list) { for (GradeDTO dto : list) { // 分数范围校验,失败立即回滚整批 if (dto.getScore() == null || dto.getScore() < 0 || dto.getScore() > 100) { throw new BusinessException("分数必须在0-100之间"); } gradeMapper.insert(dto); } }校验放在 Service 层而不是数据库层,目的是给出明确的中文错误提示。数据库唯一索引 uk_exp_student 仍然保留作为最后防线,插入时捕获 DuplicateKeyException 转成「该学生已评分」的业务异常,避免用户看到一长串 SQL 报错。事务的坑在于,循环内任何一条抛异常,前面已插入的记录都会随回滚消失,所以批量导入前先做完整校验再入库,比在循环里逐条接收前端数据更稳妥。
| 异常类型 | 处理方式 |
|---|---|
| BusinessException | 直接返回中文提示,事务回滚 |
| DuplicateKeyException | 捕获后转成「该学生已评分」返回 |
这套约定的效果是,前端只需要处理两类返回——可预期的业务提示和系统异常,而不是把 MySQL 的约束名暴露给用户。
5. 实验教学管理系统的部署验证与三处高频报错排查
项目验收前把部署链路整体走一遍。mvn clean package 打出可执行 jar,放到服务器上执行nohup java -jar teaching-system.jar --spring.profiles.active=prod &启动。验证时先跑核心闭环:管理员建实验室、教师发布实验、学生预约、上传报告、教师评分,五步全通后再测边界场景——容量已满时预约是否报错、重复提交报告是否覆盖旧文件、教师重复打分是否被拦截。
三个高频报错值得提前排查。第一,MyBatis 的 XML 映射文件里出现>或<符号时会被当成标签解析,控制台报语法错误,解决方式是把比较符替换成>和<,或者用 CDATA 包裹。第二,上传超过限制大小抛 MaxUploadSizeExceededException,确认 spring.servlet.multipart.max-file-size 是否配置生效,注意单位是 MB 不是 Mb。第三,时间字段从 MySQL 查出来少了 8 小时,检查 JDBC URL 是否带了 serverTimezone=Asia/Shanghai,以及表字段用的是 DATETIME 还是 TIMESTAMP,两者对时区的处理逻辑不同。
最后一个实用技巧:用一个 AOP 切面对所有 Controller 接口打印入参、耗时和异常堆栈,排查线上问题时比本地断点调试验证快得多。后台再加一个「使用情况统计」接口,查询各实验项目的预约率,这是答辩或汇报时最直观的一张数据表。
本文还有配套的精品资源,点击获取