☰
SpringBoot课程进度管理系统实战:分层架构、鉴权与事务设计
2026/10/8 20:59:27 网站建设 项目流程

简介:面向Java毕业设计、课程设计场景的课程进度管理系统源码包,适合需要完成高校教务管理类项目的学生与开发者参考。系统基于SpringBoot框架与MySQL数据库(5.7/8)开发,采用JDK1.8环境,核心功能包括课程信息管理、学生进度跟踪、教师进度录入、进度查询与统计分析等模块,项目经过充分测试可正常运行。压缩包整体约38.27MB,内含完整工程源码与数据库脚本等配套内容,可直接导入eclipse/idea运行,也可借助Navicat完成数据库配置。当前已有38人学习下载。通过研读源码,可以学习SpringBoot项目分层设计、实体类与数据表映射、基于MVC的请求处理流程以及进度统计的业务逻辑,掌握一套完整管理系统从结构搭建到功能落地的实现思路,同时也能为后续扩展在线教育平台等功能提供基础,具有较高的实践参考价值。

1. 课程进度管理系统:毕设和课设都在用的 SpringBoot 实战项目

很多人在毕业设计和课程设计选题时绕不开一个尴尬:题目太简单,答辩时拿不出手;题目太复杂,时间又不够。课程进度管理系统恰好卡在一个舒服的位置——它有一眼能说清的业务场景,有足够的技术点可以展开讲MySQL表设计和MyBatis Plus的使用,又能轻松扩展出登录鉴权、进度统计、班级管理这些模块。这个资源基于Java SpringBoot搭建,前端用模板引擎渲染,后端分Controller、Service、Mapper三层,是把课堂上学的东西串成一个完整闭环的经典选择。适合需要快速交付一个可演示项目的在校生,也适合刚入行想练手SpringBoot全流程的初级开发者。我拆完这套源码之后的感觉是:它不炫技,但把课设答辩最常被追问的“表结构怎么设计的”“事务怎么控制的”“登录怎么拦截的”全都落到了代码里。

2. 从资源结构看实现:分层架构与课程进度表的设计思路

2.1 SpringBoot 项目结构:拿到源码后先看哪几个包

这套课程进度管理系统用的是标准的SpringBoot分层结构,源码解压后先不要急着运行,先把目录结构过一遍。常见的包结构是controller、service、mapper、entity、config这几个,我拆到的这个版本基本也是这个套路。

src/main/java/com/example/courseprogress/ ├── controller/ # 接收前端请求,返回页面或JSON ├── service/ # 业务逻辑层,处理进度更新、校验等 │ └── impl/ # Service接口的实现类 ├── mapper/ # MyBatis的Mapper接口,对应XML或注解SQL ├── entity/ # 数据库实体类,字段对应数据表 ├── config/ # 配置类,比如登录拦截器、跨域配置 └── CourseProgressApplication.java # SpringBoot启动类

这个结构对应的是教科书里的标准三层架构。Controller层只做参数接收和结果返回,不写业务逻辑;Service层处理核心规则,比如“进度值不能超过100”“只有授课教师才能修改进度”;Mapper层就是最简单的数据访问。你答辩的时候只要能把这一层关系说清楚,基本就能证明你不是把代码全塞在Controller里的那种写法。

资源里的application.yml建议优先打开看一眼,里面配置了数据源、MyBatis的驼峰映射和端口号。我用的时候只改了数据库连接和密码,其他配置保持原样就能跑起来。需要注意的一点是,如果你的本机MySQL版本是8.0以上,驱动依赖通常已经是com.mysql.cj.jdbc.Driver,而不是旧的com.mysql.jdbc.Driver,这个在避坑章节会细说。

2.2 进度管理系统的核心表设计:进度表为什么要单独建

课程进度管理系统里最重要的一张表不是课程表也不是学生表,而是课程进度记录表。很多课设会把进度字段直接挂在课程表上,比如给course表加一个progress字段,这种做法在演示的时候没问题,但答辩时老师很容易追问“修改记录怎么查”。我拆的这个项目把进度记录拆成了独立表,每次教师更新进度都插入一条新记录,既保留了历史轨迹,也给后续做进度趋势图留好了数据基础。

CREATE TABLE `course_progress` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键', `course_id` bigint(20) NOT NULL COMMENT '课程ID', `student_id` bigint(20) NOT NULL COMMENT '学生ID', `progress` int(11) NOT NULL DEFAULT '0' COMMENT '进度百分比,0-100', `remark` varchar(255) DEFAULT NULL COMMENT '本次进度说明', `create_by` bigint(20) DEFAULT NULL COMMENT '操作人ID,一般是教师', `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '录入时间', PRIMARY KEY (`id`), KEY `idx_course_student` (`course_id`,`student_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='课程进度记录表';

这个表的设计有几个点值得在答辩时主动提出来。一是用联合索引idx_course_student支撑最频繁的查询场景——查某个学生某门课的进度历史;二是progress字段用int而不是decimal,因为进度本身就是百分比整数;三是create_time用数据库默认值CURRENT_TIMESTAMP,这样插入记录时不用在代码里手动塞时间。如果你想把表设计得更完整,还可以加一个update_time字段用ON UPDATE CURRENT_TIMESTAMP自动更新,这套资源里没有加,我建议自己补上,成本极低但显得考虑周全。

2.3 核心查询逻辑:MyBatis Plus 的条件构造器

我拆到的这个版本在数据访问层用的是MyBatis Plus而不是原生MyBatis。选它的理由很直接:单表CRUD不需要写XML,实体类加几个注解就能直接用BaseMapper提供的方法,这让课设代码量少了一大截。

@Override public List<CourseProgressVO> listProgress(Long courseId, Long studentId) { LambdaQueryWrapper<CourseProgress> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(courseId != null, CourseProgress::getCourseId, courseId) .eq(studentId != null, CourseProgress::getStudentId, studentId) .orderByDesc(CourseProgress::getCreateTime); List<CourseProgress> records = courseProgressMapper.selectList(wrapper); // 这里把实体转成VO返回给前端 return records.stream().map(this::convertToVO).collect(Collectors.toList()); }

这里eq方法第一个参数是boolean类型,意思是“当courseId不为空时才拼接这个条件”。这个写法在实际开发中很常用,比手动拼SQL字符串安全得多,也避免了写一堆if判空。换个场景,如果后端是纯查询不分页,这个方法足够用;如果要做分页,MyBatis Plus也提供了Page对象,把selectList换成selectPage就行。对于课设来说,把LambdaQueryWrapper的条件构造器讲明白,就已经超出“只会用selectById”的档次了。

3. 把系统跑起来:登录鉴权、进度录入与进度查询的完整实现路径

3.1 登录鉴权的实现:拦截器 + Session 的方式最贴合课设场景

课程进度管理系统里通常有三种角色:管理员、教师、学生。这个资源里用的是Session登录机制加SpringBoot拦截器。相比引入Spring Security或者JWT,Session这种方式在课设阶段更直观——你可以在答辩时说清楚“登录后把用户ID存在Session里,拦截器校验Session是否存在”,这一句话就体现了你对Web会话机制的理解。

@Component public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { Object userId = request.getSession().getAttribute("userId"); if (userId == null) { // 未登录,重定向到登录页 response.sendRedirect("/login"); return false; } return true; } }

有了拦截器之后,还要在配置类里注册它,并指定哪些路径需要拦截、哪些路径放行。一般的配置是登录页、登录接口、注册接口放行,其余业务接口全部拦截,静态资源比如CSS和JS也要放行,否则页面样式会全部丢失。我在第一次跑这套源码时就在这踩了坑,忘记放行静态资源导致登录页能打开但排版完全错乱,一度以为是前端代码有问题,实际上拦的是自己人。

参数设计上,登录接口接收用户名和密码,密码在数据库里存的是MD5加密后的值。MD5在真实生产环境中已经被视为不安全,但课设项目用它是没问题的,答辩时老师一般不追究,除非你自己主动提。如果你想显得更专业,可以改成BCrypt加密,Spring Security里自带这个工具类,改造量也不大。

3.2 进度录入的后端逻辑:事务、校验与操作记录一条龙

进度录入是系统的核心业务,逻辑上需要做三件事:校验参数合法性、写入进度记录、更新课程表的汇总进度。这个过程中必须加事务,保证进度记录和汇总数据要么同时成功要么同时失败。

@Transactional(rollbackFor = Exception.class) public void updateProgress(ProgressUpdateDTO dto) { // 1. 参数校验:进度必须在0到100之间 if (dto.getProgress() < 0 || dto.getProgress() > 100) { throw new ServiceException("进度值必须在0到100之间"); } // 2. 校验当前用户是否有权操作该课程 Course course = courseMapper.selectById(dto.getCourseId()); if (course == null || !course.getTeacherId().equals(dto.getOperatorId())) { throw new ServiceException("无权操作该课程"); } // 3. 插入进度记录 CourseProgress progress = new CourseProgress(); progress.setCourseId(dto.getCourseId()); progress.setStudentId(dto.getStudentId()); progress.setProgress(dto.getProgress()); progress.setRemark(dto.getRemark()); courseProgressMapper.insert(progress); // 4. 更新课程表的最新进度汇总 courseMapper.updateLatestProgress(dto.getCourseId(), dto.getStudentId(), dto.getProgress()); }

这套流程里最容易被忽视的是第4步的语义——课程表里的latest_progress本质上是一个冗余字段,用来在课程列表页直接展示,避免每次都要从course_progress表里取最新一条。这个设计叫“冗余字段换查询性能”,课设数据库设计题里偶尔会考。@Transactional注解里rollbackFor = Exception.class建议加上,因为Spring默认只对运行时异常回滚,如果你抛的是自定义Exception,不加这个参数事务是不生效的,这是我见过最多的Spring事务翻车点。

3.3 进度查询与可视化:给前端返回什么格式的数据

查询接口返回的数据格式直接决定了前端好不好写。这个资源里的进度查询接口返回的是每个学生的最近一条进度记录,外加课程总进度平均值。前端拿到数据后渲染成表格或者简单的进度条。

public CourseProgressVO convertToVO(CourseProgress progress) { CourseProgressVO vo = new CourseProgressVO(); vo.setId(progress.getId()); vo.setCourseId(progress.getCourseId()); vo.setStudentId(progress.getStudentId()); vo.setProgress(progress.getProgress()); vo.setRemark(progress.getRemark()); vo.setCreateTime(progress.getCreateTime()); // 额外查一下学生姓名和课程名,方便前端直接展示 Student student = studentMapper.selectById(progress.getStudentId()); vo.setStudentName(student != null ? student.getName() : ""); return vo; }

这里有个细节值得说:VO层负责组装前端需要的数据,比如把studentId翻译成studentName。这是典型的后端开发习惯,前端不需要自己拿ID再去查名字,后端一次给全。如果你在前端代码里看到有额外的Ajax调用去查名字,说明那个项目的分层做得不够彻底。课设答辩时你主动提这个设计,可以直接引导老师往你的优势方向上问。

4. 部署交付与避坑记录:本机运行到演示环境的五个常见问题

4.1 环境准备与数据库初始化

课程进度管理系统要跑起来需要准备JDK 8或更高版本、Maven 3.6以上、MySQL 5.7或8.0。资源包里通常带有SQL脚本,用Navicat或者命令行执行即可。我一般建议先把脚本里的库名看清楚,默认通常是course_progress或者类似的命名,然后和application.yml里的数据源配置对齐。

# 创建数据库并导入脚本 mysql -u root -p -e "CREATE DATABASE IF NOT EXISTS course_progress DEFAULT CHARACTER SET utf8mb4;" mysql -u root -p course_progress < sql/course_progress.sql # 启动项目 mvn spring-boot:run

项目启动后访问http://localhost:8080/login就能看到登录页。如果你改成前后端分离的架构,后端接口返回JSON,前端用Vue或React渲染,那需要额外处理跨域问题;这套资源是模板引擎渲染,所以不存在跨域配置。启动日志里看到“Started CourseProgressApplication”就说明Tomcat已经跑起来了,端口默认8080,如果和本地其他项目冲突,在application.yml里把server.port改掉。

4.2 坑一:数据库连接时报 Public Key Retrieval is not allowed

现象:项目启动或首次查询时直接报错,错误信息里有“Public Key Retrieval is not allowed”字样,数据库是MySQL 8.0版本。

原因:MySQL 8.0默认的认证插件是caching_sha2_password,JDBC驱动在连接时想获取服务器的公钥来做RSA加密,但url里没有配置允许这个操作。

解决:在jdbc连接串后追加参数allowPublicKeyRetrieval=true,同时记住配useSSL=false。改完的url长这样:

spring: datasource: url: jdbc:mysql://localhost:3306/course_progress?useUnicode=true&characterEncoding=utf8&useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai username: root password: 你的密码 driver-class-name: com.mysql.cj.jdbc.Driver

这里serverTimezone=Asia/Shanghai也要带上,否则日期字段容易出现相差8小时的问题。这三个参数属于课设阶段最常见的一组坑,我几乎每次帮人看项目都要改一遍。

4.3 坑二:Mapper 接口注入报空指针

现象:Service里用@Autowired或@Resource注入Mapper接口,项目启动不报错,但一调方法就NullPointerException。

原因:最常见的场景是测试类里手动new了Service对象,没经过Spring容器管理,所有自动注入的字段全是空的。

解决:不要在测试类里new Service,用@SpringBootTest配合@Autowired注入。另外一个可能性是你的Mapper接口忘了加@Mapper注解,或者启动类上的@MapperScan路径没扫到。我用的时候在启动类上加@MapperScan("com.example.courseprogress.mapper"),一劳永逸,每个Mapper接口就不用单独加注解了。

4.4 坑三:登录后页面一直重定向回登录页

现象:输入正确的账号密码登录成功后,访问业务页面又被弹回登录页。

原因:重定向逻辑出问题,最典型是三种——Session写入失败、拦截器放行路径配置错误、Cookie被浏览器禁用。如果是本地开发,第三种概率极低。

解决:先在LoginController里打印Session值确认确实写进去了,再检查拦截器注册代码。我这里给一个正确的注册方式:

@Configuration public class WebConfig implements WebMvcConfigurer { @Resource private LoginInterceptor loginInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(loginInterceptor) .addPathPatterns("/**") .excludePathPatterns("/login", "/user/login", "/css/**", "/js/**", "/images/**"); } }

注意顺序,放行的路径里一定要包含静态资源目录。如果你的页面用了WebJars引入前端库,还需要放行/webjars/**,这个忘记加的话,页面样式偶尔加载出来偶尔加载不出来,排查起来非常消耗时间。

4.5 坑四:时间字段在数据库和页面显示相差 8 小时

现象:数据库表里存的时间是14:00,页面上显示的是22:00,或者反过来。

原因:JDBC连接串里的serverTimezone配置和本机时区不一致。MySQL驱动在转换Timestamp时会按连接串指定的时区处理,如果没指定就用JVM默认时区,两者不一致就差了8小时。

解决:连接串统一配serverTimezone=Asia/Shanghai。同时检查MySQL服务端的时区,用SET GLOBAL time_zone = '+08:00'可以临时修正。更保险的方法是在实体时间字段上用@JsonFormat注解指定格式和时区,比如@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8"),这样即使前端解析JSON也不会偏差。

4.6 坑五:Maven 依赖下载失败或版本冲突

现象:mvn spring-boot:run时卡在下载依赖,或者启动时NoSuchMethodError、ClassNotFoundException。

原因:大概率是Maven镜像源是国外地址,下载慢或超时;版本冲突一般是SpringBoot和MyBatis Plus版本不匹配。

解决:在Maven的settings.xml里配置阿里云镜像,然后clean一下本地仓库重新拉取。版本方面,我建议SpringBoot用2.7.x或3.x时,MyBatis Plus配套使用mybatis-plus-boot-starter 3.5.x,这个组合稳定性是我验证过的。如果你用的是SpringBoot 3.x,特别注意有些老版本的MyBatis Plus还停留在javax包名上,会直接启动报错,需要升级到支持jakarta包名的版本。

5. 改造成自己的课设:三个让项目差异化的进阶技巧

课设答辩最怕的不是项目简单,而是两个同学拿出来的东西一模一样。这套课程进度管理系统作为基础框架很完整,但你要做的是加一个别人没有的功能点。我这里说三个改造方向,按工作量从小到大排序。

第一个方向是加进度趋势折线图。现在的系统里course_progress表已经保留了每次录入的历史记录,这天然适合做趋势展示。前端可以用ECharts,后端只需要为“学生ID+课程ID”提供一个查询全部历史进度记录并返回按时间排序的接口,前端把数据塞给折线图就行。这个改造大概需要写一个Controller方法、一个ServiceImpl方法、一个JavaScript图表初始化函数,总计也就100行左右的代码量,但展示效果直接上升一个档次。关键代码逻辑不复杂,就是查全部记录然后按create_time升序返回,前端顾问用两个数组接收日期和进度值。

第二个方向是把Excel导入导出加上。课设里课程进度数据经常有批量录入的需求,用EasyExcel或者Apache POI做一个导入模板,支持教师下载Excel模板填写学生进度后上传,后端解析文件后批量插入记录。这个方向的好处是实用场景非常清晰,答辩时你说“真实学校场景中老师不可能一条条录入,所以做了批量导入”,老师一听就觉得你考虑了实际问题。导入模板一般需要三列:学号、进度值、备注说明。解析时留意学号可能是字符串,用String接收而不是int,否则以0开头的学号会被吞掉。

第三个方向是把权限控制从拦截器级提升到方法级。现在系统用的是拦截器统一判断是否登录,但没细分教师和管理员的权限差异。你可以用Spring AOP自定义一个@RequireRole注解,标注在Controller方法上,通过切面判定当前Session里的角色是否有权访问。这个改造加分项在于它展示了你对AOP的理解,这在课设阶段几乎没人会用。

@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface RequireRole { String value() default "TEACHER"; }

切面里获取Session中的角色值,和注解要求的角色比对,不一致就返回403页面或者JSON错误信息。核心代码也就是十几行,但答辩时能承接住“你怎么控制访问权限”和“登录拦截和权限控制有什么区别”两个高频追问。

从实际交付的角度说,我的习惯是代码改完之后强制走一遍完整流程:删库重建、导入SQL、启动项目、用教师账号录入一条进度、退出登录、用学生账号查看进度、关掉项目重新启动确认Session失效。这一套流程走下来,项目里的低级问题基本都暴露了。从那以后我每次交付课设或毕设前都会强制做一遍这个验收,换数据库、换端口、换账号角色,三个变量轮番试一遍,哪个环节报错就当场修掉,不把问题留给演示现场。希望帮到你,也祝你答辩顺利。

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

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

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

立即咨询