资讯动态

SpringBoot+MyBatis+MySQL教务管理系统课程设计全攻略

发布时间:2026/9/16 11:18:23 来源:尧图企业网站定制
简介这套基于 Java Spring Boot Mybatis 的教务管理系统项目是一份面向高校教务场景的完整实战源码包适合毕业设计、Java Web 课程设计以及希望熟悉 SSM 开发的初学者参考。系统围绕管理员、教师、学生三种角色构建覆盖课程维护、学生与教师信息管理、选课退选、成绩打分和密码重置等核心教务流程权限划分清晰。后端整合 Shiro 安全框架、Mybatis 持久层与 PageHelper 分页插件前端使用 Bootstrap日志与连接池直接依托 Spring Boot 内置能力技术选型简洁实用。压缩包共361个文件以62个java源码、144个xml配置与映射、83个class编译文件及32个jsp页面为主另附SQL脚本和项目说明总体仅3.91MB导入 IDE 后即可对照学习。资源附带管理后台与三类测试账号目前已有474人学习下载从角色权限设计到数据模型搭建都能为毕业设计或课程设计提供成熟思路。1. 为什么 SpringBootMyBatis 是教务管理系统课程设计的最稳组合一个教务管理系统真正麻烦的地方不在登录注册而在“选课、成绩、权限”这三条链路上同一门课被两百人选成绩按班级统计停开课程要连带清理选课记录——这些操作落到 MySQL 是事务和 SQL落到后端是接口和数据模型。SpringBootMyBatisMySQL 的组合在毕业设计和 java web 课程设计里几乎是标准答案SpringBoot 省掉 SSH 时代的大量 XML 配置MyBatis 把 SQL 留在你能直接看到的 mapper 文件里MySQL 负责所有数据落地。这套方案不引入微服务、缓存、消息队列这些答辩时不好解释的组件却能把 Controller、Service、Mapper 三层架构完整撑起来。下面按“建表 → 查询 → 业务 → 权限 → 优化”的顺序推演版本、参数、易错点会单独指出来。2. 用 SpringBootMyBatis 跑通教务系统版本选型与最小可运行工程2.1 三件套版本怎么选先定 JDK再定其余毕业生拿到 SpringBoot 项目第一步就栽在版本上的情况太多了。SpringBoot 2.7.x 支持 JDK 8SpringBoot 3.x 强制要求 JDK 17而且包名从javax.servlet换成了jakarta.servlet。很多学校机房和旧 Tomcat 还在 JDK 8所以课程设计最稳的组合是 JDK 8 SpringBoot 2.7.x mybatis-spring-boot-starter 2.3.x MySQL 8.0。如果你机器上已经装了 JDK 17 并且不想降级再考虑 SpringBoot 3.x但所有涉及javax的依赖都要换成jakarta这两个坑在答辩现场很容易被老师问住。组件课程设计推荐说明JDK8 或 11兼容学校机房和旧 IDESpringBoot2.7.x稳定资料多踩坑可搜MyBatis startermybatis-spring-boot-starter 2.3.x与 SpringBoot 2.x 配套MySQL8.0驱动类名com.mysql.cj.jdbc.Driver连接池HikariCPSpringBoot 默认无需额外引入MyBatis 本身不需要手写版本交给spring-boot-starter-parent的依赖管理即可。这里不用 MyBatis-Plus 不是因为 Plus 不好而是课程设计里老师更希望看到手写的 SQL 和 resultMap原生的 MyBatis 更能讲清楚“ORM 到底做了什么”。2.2 建表三张核心表把字段和索引一次定好教务系统常见的 ER 简化模型就是三张表sys_user存学生、教师、管理员三种角色course存课程elective存选课记录和成绩。下面是 MySQL 8 可直接执行的建表 SQL。CREATE TABLE sys_user ( id INT NOT NULL AUTO_INCREMENT, user_no VARCHAR(32) NOT NULL COMMENT 学号/工号, username VARCHAR(64) NOT NULL, password VARCHAR(128) NOT NULL COMMENT MD5加盐后的密文, role TINYINT NOT NULL DEFAULT 3 COMMENT 1管理员 2教师 3学生, class_name VARCHAR(64) DEFAULT NULL COMMENT 学生所在班级, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_user_no (user_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE course ( id INT NOT NULL AUTO_INCREMENT, course_no VARCHAR(32) NOT NULL COMMENT 课程编号, course_name VARCHAR(128) NOT NULL, teacher_id INT NOT NULL COMMENT 关联 sys_user.id, credit DECIMAL(3,1) DEFAULT 2.0 COMMENT 学分, capacity INT DEFAULT 60 COMMENT 选课人数上限, selected_count INT DEFAULT 0 COMMENT 已选人数, PRIMARY KEY (id), KEY idx_teacher (teacher_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE elective ( id INT NOT NULL AUTO_INCREMENT, student_id INT NOT NULL COMMENT 关联 sys_user.id, course_id INT NOT NULL, score DECIMAL(5,2) DEFAULT NULL COMMENT 成绩未录入为NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_stu_course (student_id, course_id), KEY idx_course (course_id), CONSTRAINT fk_elective_stu FOREIGN KEY (student_id) REFERENCES sys_user(id), CONSTRAINT fk_elective_course FOREIGN KEY (course_id) REFERENCES course(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;elective表是整张 ER 图的核心关系表它承载了“哪个人选了哪门课”这个多对多关系还额外挂了一个score属性字段所以相当于把选课和成绩合并成一张表。uk_stu_course这个联合唯一索引是防重复选课的第一道闸比应用层先查再插更可靠外键在课程设计阶段建议保留ER 图和数据库关系上一目了然生产环境再去讨论删外键的事。2.3 application.yml 配置驱动、URL 参数和 SQL 日志server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/academic?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue username: root password: 你的密码 hikari: maximum-pool-size: 10 connection-timeout: 30000 mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.academic.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl logging: level: com.example.academic.mapper: debug这里几个参数值得专门记一下。allowPublicKeyRetrievaltrue是 MySQL 8 连接失败的常见原因不加上去偶尔会报 Public Key Retrieval 异常serverTimezoneAsia/Shanghai解决时区导致的时间字段偏移。map-underscore-to-camel-case让student_id自动映射成studentId少写大量 resultMaplog-impl和logging.level是两条打印 SQL 的路径调试阶段都开着答辩演示接口时把日志亮出来比空口讲 SQL 有说服力得多。2.4 最小链路Controller → Service → Mapper 跑通第一条查询环境配好后先别急着写业务跑通一条最简单的查询链路确认所有配置生效。// UserMapper.java Mapper public interface UserMapper { User findByUserNo(Param(userNo) String userNo); }!-- UserMapper.xml -- mapper namespacecom.example.academic.mapper.UserMapper select idfindByUserNo resultTypeUser SELECT id, user_no, username, password, role, class_name, create_time FROM sys_user WHERE user_no #{userNo} /select /mapperService public class AuthService { private final UserMapper userMapper; public AuthService(UserMapper userMapper) { this.userMapper userMapper; } public User login(String userNo, String rawPassword) { User user userMapper.findByUserNo(userNo); if (user ! null user.getPassword().equals(md5(rawPassword))) { return user; } throw new RuntimeException(账号或密码错误); } private String md5(String input) { return DigestUtils.md5DigestAsHex(input.getBytes()); } }查询用#{userNo}是预编译占位符MySQL 执行的是?占位天然防 SQL 注入${}是字符串拼接能不用就不用。Param注解保证 XML 里的参数名和 Java 方法参数严格对应。登录密码这里用 MD5 加密课程设计足够但答辩时如果能主动说出“MD5 可被彩虹表破解生产环境应该用 BCrypt”已经超过大多数同学的水平。3. 教务核心数据模型与选课/成绩的 SQL 落地3.1 ER 关系怎么画以及选课表为什么是关系核心先理清实体关系再写业务比反过来改代码省事得多。sys_user和course原本是多对多关系一个学生选多门课一门课被多个学生选。数据库落地多对多不能靠两张表直接做必须引入中间关系表elective它把多对多拆成了两个一对多一个学生对多条选课记录一门课程对多条选课记录。又因为“选课”这个行为还带score这个属性所以elective并不只是两张表的外键关系表而是一个关系实体。答辩 PPT 里画 ER 图时矩形框加菱形框的标准画法就是用了这张中间表。表关系核心索引备注sys_user三种角色一张表uk_user_norole 字段区分身份course教师开设课程idx_teachercapacity 做选课上限elective多对多关系实体uk_stu_coursescore 挂在这里3.2 防重复选课唯一索引加一条 INSERT 解决并发问题选了同一门课两次是教务系统第一高频 bug。初学者习惯“先查有没有、没有再插入”这个思路本身没问题但两个请求同时进来时都会发现“没有”然后各自插入一条重复数据就出现了。正确方案是让数据库兜底uk_stu_course唯一索引在数据库层面拒绝重复再用一条 SQL 把校验和插入合并。INSERT INTO elective (student_id, course_id) SELECT #{studentId}, #{courseId} FROM sys_user u WHERE u.id #{studentId} AND u.role 3 AND NOT EXISTS ( SELECT 1 FROM elective e WHERE e.student_id #{studentId} AND e.course_id #{courseId} );INSERT INTO ... SELECT的写法让插入前校验发生在 MySQL 内部比 Java 层先查再插少一个网络往返。NOT EXISTS负责逻辑上的防重唯一索引负责物理上的兜底。紧接着还要更新课程的已选人数两步需要在同一个事务里完成Transactional public void selectCourse(Integer studentId, Integer courseId) { int inserted electiveMapper.insertElective(studentId, courseId); if (inserted 0) { throw new RuntimeException(不能重复选课); } int updated courseMapper.increaseSelectedCount(courseId); if (updated 0) { throw new RuntimeException(课程已满); } }Transactional放在 Service 方法上保证选课记录和课程人数增量要么都成功要么都回滚。increaseSelectedCount的条件是selected_count capacityMySQL 的行锁会串行化同一条课程的并发选课这是课程设计里最稳妥的方案。3.3 成绩统计三行 SQL 替代三十行 Java 循环成绩统计是答辩时最容易出彩的地方。新手往往把 elective 表全部查回 Java 内存然后用 for 循环算平均分、及格率数据量小看不出问题面试官问一句“一千门课二十万条成绩怎么算”就可能卡住。教务系统的成绩统计本质上是一个聚合查询SQL 天然擅长这件事。SELECT c.course_no, c.course_name, COUNT(e.id) AS total_cnt, ROUND(AVG(e.score), 2) AS avg_score, MAX(e.score) AS max_score, MIN(e.score) AS min_score, SUM(CASE WHEN e.score 60 THEN 1 ELSE 0 END) / COUNT(e.id) AS pass_rate FROM course c JOIN elective e ON e.course_id c.id WHERE c.id #{courseId} GROUP BY c.id, c.course_no, c.course_name;CASE WHEN在聚合函数内部逐行判断达到 60 分返回 1否则返回 0SUM加起来就是及格人数除以总数得到及格率。GROUP BY后面必须包含 select 列表里的全部非聚合字段MySQL 的only_full_group_by模式下缺一个就直接报错。ROUND(..., 2)控制小数位精度避免出现 89.333333 这种不好看的分数。3.4 多条件查询和批量写入的 MyBatis 动态 SQL 写法课程管理页必然有“按课程名模糊查、按教师精确查”这种多条件组合。MyBatis 的where加if是标准写法select idpageByCondition resultTypeCourseVO SELECT c.id, c.course_no, c.course_name, c.credit, c.capacity, c.selected_count, u.username AS teacher_name FROM course c JOIN sys_user u ON u.id c.teacher_id where if testcourseName ! null and courseName ! AND c.course_name LIKE CONCAT(%, #{courseName}, %) /if if testteacherId ! null and teacherId ! 0 AND c.teacher_id #{teacherId} /if /where ORDER BY c.id DESC LIMIT #{offset}, #{limit} /selectwhere标签会自动去掉条件块最前面的AND比手动写WHERE 11干净得多。CONCAT(%, #{courseName}, %)是标准的事先拼好通配符的方式注意不要在#{}里直接写%${courseName}%。分页用LIMIT #{offset}, #{limit}offset 在 Service 层用(pageNum - 1) * pageSize计算。批量写入同样用动态 SQL 的foreach实现。管理员导入选课名单时几百条数据逐条 INSERT 的耗时和批量插入不是一个数量级insert idbatchInsertElective INSERT INTO elective (student_id, course_id, create_time) VALUES foreach collectionlist itemitem separator, (#{item.studentId}, #{item.courseId}, NOW()) /foreach /insertseparator,负责拼接多条 VALUES 之间的逗号。批量大小建议每批 500 条左右一次塞 3000 条会把 SQL 文本拉得过长可能触发 MySQL 的max_allowed_packet限制。3.5 一对多查询的 resultMap老师查自己课程的学生列表教师端“查看我教的课有哪些学生”是典型的一对多查询。用一个resultMap的collection标签可以一次查出两级数据避免在 Java 里循环调 Mapper 造成 N1 查询。resultMap idcourseWithStudents typeCourseVO id propertyid columnid/ result propertycourseName columncourse_name/ collection propertystudentList ofTypeUser id propertyid columnstudent_id/ result propertyusername columnstudent_name/ result propertyclass_name columnclass_name/ /collection /resultMap select idselectCourseWithStudents resultMapcourseWithStudents SELECT c.id, c.course_name, u.id AS student_id, u.username AS student_name, u.class_name FROM course c JOIN elective e ON e.course_id c.id JOIN sys_user u ON u.id e.student_id WHERE c.teacher_id #{teacherId} /selectid标签用于唯一标识主键MyBatis 在合并结果时靠它区分行的归属。ofTypeUser说明集合里装的是 User 对象。这种方式一次 JOIN 查出所有数据MyBatis 内部根据id列自动完成“一门课对应多条学生记录”的组装。课程设计阶段这种查询写完基本就是最优解。4. 毕业设计最常见的坑权限、会话与 MyBatis 细节4.1 三种角色一张表登录态交给拦截器教务系统必须有角色概念管理员维护课程和学生教师录入成绩学生选课查分。很多课程设计选择引入 Spring Security 或者 Shiro但自定义 Session 加拦截器的方案更值得做——代码量不大而且每一个环节都能在答辩时讲出原理覆盖。Component public class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (handler instanceof HandlerMethod) { HandlerMethod method (HandlerMethod) handler; // 登录接口、静态资源放行 if (method.hasMethodAnnotation(AllowAnonymous.class)) { return true; } // 未登录跳转登录页 Object loginUser request.getSession().getAttribute(loginUser); if (loginUser null) { response.sendRedirect(/login); return false; } // 角色校验 RequireRole requireRole method.getMethodAnnotation(RequireRole.class); if (requireRole ! null) { SysUser user (SysUser) loginUser; if (user.getRole() ! requireRole.value()) { response.setStatus(403); return false; } } } return true; } }Configuration public class WebConfig implements WebMvcConfigurer { private final LoginInterceptor loginInterceptor; public WebConfig(LoginInterceptor loginInterceptor) { this.loginInterceptor loginInterceptor; } Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(loginInterceptor) .addPathPatterns(/**) .excludePathPatterns(/login, /css/**, /js/**, /images/**); } }配套两个自定义注解AllowAnonymous和RequireRole(1)Controller 方法上标RequireRole(1)就表示只有管理员能访问。excludePathPatterns里的静态资源路径必须在所有 Web 课设里都配好否则登录页的 CSS 加载不出来会被老师当成大问题。Session 超时默认 30 分钟原理是 Tomcat 的 session-timeout 配置答辩被问到可以直接说出来。4.2 MyBatis 高频坑位单个数字字符比较、缓存、版本不一致以下几类问题在 MyBatis 项目里出现频率极高课程设计阶段提前排查能省去大量调试时间。坑位现象正确解法if testrole1不生效条件恒不成立或直接抛异常用testrole 1外层单引号内层双引号一级缓存同一次 SqlSession 里第二次查询返回旧值理解 SqlSession 生命周期调试时加flushCache二级缓存吃完关联查询脏数据课程设计保持默认关闭不要全局开启SpringBoot 3 包名变化javax.servlet报 ClassNotFound换jakarta.servlet或退回 SpringBoot 2.7yml 密码明文代码提交到公开仓库后被爬虫扫库用${DB_PASSWORD:root}环境变量注入for 循环查 Mapper接口响应越来越慢出现大量 SQL改成 JOIN 一次性查出结果单个数字字符比较是 MyBatis 动态 SQL 里最容易踩的 OGNL 坑。if testrole1中1被 OGNL 解析为字符字面量而 role 是字符串类型两者比较方式不符合直觉。稳妥写法是把属性值放到双引号里if testrole ! null and role 1 AND u.role #{role} /ifyml 里的数据库密码靠环境变量兜底${DB_PASSWORD:root}的含义是优先读取系统环境变量DB_PASSWORD取不到就用默认值 root。这个写法在答辩现场讲出来能让老师意识到你接触过配置安全的问题。4.3 事务的位置和批量操作的取舍事务注解放在 Service 层方法上而不是 Controller 层是因为事务要覆盖完整业务单元而 Controller 只负责接收参数和返回结果。选课加扣减库存这个场景事务必须同时覆盖两步但如果只是单表查询加不加事务无所谓的点也要能说清楚。批量写入不要盲目提高 batch size数据库不开启rewriteBatchedStatements时JDBC 的批量插入其实是逐条执行的细节在下一章展开。5. 答辩前必做的三个优化索引验证、连接池参数与批量写入5.1 用 EXPLAIN 验证索引而不是靠感觉说话教师查自己课程列表、学生查自己选课列表这两个接口的 WHERE 条件都命中联合索引的左前缀。答辩前在 Navicat 里执行一次EXPLAIN SELECT e.id, c.course_name FROM elective e JOIN course c ON c.id e.course_id WHERE e.student_id 101;看返回结果里的type和key字段type是ref、key显示uk_stu_course说明查询走了索引如果type是ALL说明全表扫描。重点是能说出 rows 字段的含义MySQL 估算需要扫描的行数rows 越小越好。这个习惯带到工作里也受用。5.2 MySQL 连接串与 HikariCP 的两个参数影响体感连接串加上rewriteBatchedStatementstrue配合cachePrepStmtstrueuseServerPrepStmtstrue让 JDBC 驱动把多次批量插入重写成多值 INSERT。连接池参数不用多HikariCP 默认的maximum-pool-size是 10课设项目这个值够用调大了反而增加 MySQL 压力。spring: datasource: url: jdbc:mysql://localhost:3306/academic?rewriteBatchedStatementstruecachePrepStmtstrueuseServerPrepStmtstrue hikari: minimum-idle: 2 maximum-pool-size: 10 connection-timeout: 30000 max-lifetime: 1800000max-lifetime建议小于 MySQL 的 wait_timeout否则连接被数据库端回收后应用还在拿旧连接会偶发通信链路异常。这三个参数组合是 MySQL 8 连接池最常被问到的配置。5.3 3000 条选课记录批量写入的演示脚本把第 3 章的batchInsertElective用起来分批写入是防止 SQL 文本过大ListElective batch new ArrayList(); for (int i 0; i 3000; i) { batch.add(new Elective(studentId, courseId)); if (batch.size() 500) { electiveMapper.batchInsertElective(batch); batch.clear(); } } if (!batch.isEmpty()) { electiveMapper.batchInsertElective(batch); }对比一下链接串加不加rewriteBatchedStatementstrue的耗时差距通常能达到三倍以上。验证方式很简单开启 MyBatis 的日志输出批量执行时如果 SQL 日志里打印的是一条包含 500 组 VALUES 的长 INSERT说明参数已生效如果打印的是 500 条单独 INSERT那就去检查 URL 里的参数拼写。打开日志确认 Preparing 只出现一次然后把这条日志和连接池参数一起放进答辩 PPT 的“性能优化”页比贴十行理论都有效。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价