基于Spring Boot的课程作业管理系统设计与实现
简介:基于Java的课程作业管理系统毕业设计资源包,面向高校计算机相关专业学生毕业设计、课程设计及Java Web学习人群,系统覆盖管理员、教师、学生三类角色,实现了从课程信息发布、学生选课、作业布置/提交/评分到课程评价与资源管理的完整业务闭环,同时包含班级、教师、学生、课程类型、公告信息等基础管理模块。资源共462个文件,以137个Java源码、50个Vue前端组件和161个SVG图标为主,另含数据库SQL脚本、启动部署批处理、毕业论文与开题报告文档等,压缩包整体约9.05MB。该资源已有131人学习。读者可直接获得可运行源码、数据库初始化脚本、部署说明,并配套论文与开题报告,既能用于毕业设计答辩准备,也能帮助深入理解基于Spring Boot的权限角色设计与业务模块划分。结合文件中的启动批处理与前端资源,可快速在本地搭建运行环境,通过对照源码与文档梳理管理端、教师端、学生端的操作逻辑,便于二次开发或学习借鉴。
1. 课程作业管理系统值不值得做:先看清它解决的是谁的痛点
每届毕业设计都有一批人选“课程作业管理系统”这个题目,但很多人做完才发现自己只是在写增删改查。这个系统的真实价值不在于“能存作业”,而在于把提交、迟交判定、查重标记、评分回传这一整条链路串起来,让老师不用对着邮箱收几十封附件,让学生不用反复确认“到底交没交上”。它适合两类人:一类是Java基础尚可、想用一个完整项目把SSM或Spring Boot体系串起来的应届生;另一类是实验室或小规模院系需要一套轻量、不加外网依赖的内部作业收集工具。
系统规模不大,但涉及的坑不少:文件上传的幂等性、时区导致迟交判断失误、权限绕过、以及答辩时被老师追问“表为什么这么设计”。这篇文章按我实际搭过的方案走一遍,从选型、建表到核心代码,再到最后写论文和PPT的组织方法,全程可复现。
2. 技术选型与工程搭建:Spring Boot + Thymeleaf 的最小可跑方案
2.1 选型逻辑:单体优先,前后端分离在这里是负资产
课程作业管理系统最合理的形态是单体应用,服务端渲染页面,而不是 Vue + Spring Boot 的前后端分离。原因很现实:毕设答辩时老师会要求你现场改代码看效果,前后端分离意味着你至少要维护两套工程、两套启动方式,跨域、Token 过期这些问题在演示时一旦出现就是致命伤。而用 Thymeleaf 做服务端渲染,一个 Spring Boot 进程起来,浏览器直接访问就能演示全部功能。
选 Spring Boot 而不是 SSM(Spring + Spring MVC + MyBatis)的考虑是:Spring Boot 的自动配置让项目从零到跑通只需要十分钟,而 SSM 的手动 XML 配置对于毕设周期来说属于纯耗时操作。但如果你对“配置”本身想多拿点分,可以在答辩时主动说一句“Spring Boot 本质上是约定优于配置,底层还是 Spring 的 IoC 和 AOP”,老师一般就不再深究了。
MyBatis-Plus 可以引入,它把单表 CRUD 的代码量压缩到几乎为零,但必须注意:不要把业务逻辑写在 Service 里用 QueryWrapper 硬拼条件,答辩时被问“这条 SQL 怎么优化”会很难看。我用它的场景只集中在分页查询和简单条件过滤,复杂的多表查询一律写 XML 里的手写 SQL。
2.2 从 JDK 到项目跑通:五步完成最小环境
第一步是 JDK 环境。毕设项目不要追新,JDK 8 足够,部分学校机房电脑装的是 JDK 8 或 11,你用一个 JDK 17 的新特性在答辩机器上很可能直接启动失败。JDK 8 的另一个好处是 Lambda 和 Stream 已经够用,面试被问 Java 基础时也主要在 8 这个范围内。环境变量配置就三件事:JAVA_HOME指向 JDK 安装目录,Path加%JAVA_HOME%\bin,CLASSPATH配.;%JAVA_HOME%\lib\dt.jar;%JAVA_HOME%\lib\tools.jar。配完在命令行执行java -version和javac -version,两个都有输出才算成功。
第二步是 MySQL。用 8.x 版本要注意驱动类名是com.mysql.cj.jdbc.Driver,老教程里的com.mysql.jdbc.Driver在 8.x 下会直接启动失败,这是最常见的一个入门级翻车点。
第三步创建 Spring Boot 工程。不依赖 IDEA 的 Spring Initializr 网络服务也可以,直接改 Maven 的pom.xml:
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.18</version> <relativePath/> </parent> <dependencies> <!-- 服务端渲染模板引擎 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <!-- Web MVC 核心 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- MyBatis-Plus 的 Spring Boot 适配 --> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3</version> </dependency> <!-- MySQL 驱动 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> </dependency> <!-- 密码加密工具 --> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.8.20</version> </dependency> </dependencies>依赖里说明两个细节。Hutool 不是必须的,但它的DigestUtil做 MD5 加盐、IdUtil做随机 ID 非常省事,少写很多工具类。MyBatis-Plus 版本和 Spring Boot 版本有个隐形匹配关系,3.5.3 对应 Spring Boot 2.x 是稳定的,如果你的父工程用了 Spring Boot 3.x,那 MyBatis-Plus 要换mybatis-plus-spring-boot3-starter这个独立坐标,否则启动时报 mapper 扫描不到。
2.3 配置文件里的三个必调参数
application.yml是启动前就要核对清楚的地方,我的配置习惯是这样:
server: port: 8080 servlet: # 所有请求加前缀,避免和静态资源路径交叉 context-path: / spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/course_homework?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true username: root password: "your_password" servlet: multipart: # 作业文件上限,按课件体积放大到 50MB max-file-size: 50MB max-request-size: 50MB thymeleaf: cache: false mybatis-plus: configuration: # 开启驼峰映射,数据库下划线自动转实体驼峰字段 map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: id-type: autoserverTimezone=Asia/Shanghai这一项不是可选项。MySQL 8 默认时区是 UTC,如果你不加这个参数,LocalDateTime存进去和查出来会差 8 小时,而作业系统最敏感的就是晚交判断——差 8 小时意味着晚上 23:30 提交可能被判定为次日早上 7:30,这个 bug 一旦出现在答辩演示里,基本就告别高分了。
max-file-size设到 50MB 是因为现在有的课程要求提交视频作业或者大附件,默认 1MB 肯定不够。thymeleaf.cache=false是开发期必备,改完页面模板刷新浏览器就能看到效果,不用重启整个服务,否则每改一行 HTML 都要重启一次,一天能浪费半小时。StdOutImpl是 MyBatis-Plus 的 SQL 日志打印,开发期打开能直观看到每步操作实际执行的 SQL,排查 N+1 查询时候特别有用,上线前记得关掉。
2.4 启动失败的排查顺序
环境配置完之后,最容易碰到的启动失败场景集中在三个位置:数据库连不上、mapper 扫描不到、端口占用。数据库连不上的日志会明确报Access denied或者Communications link failure,前者改用户名密码,后者查 MySQL 服务是否启动、端口是否是 3306。mapper 扫描不到的报错是Invalid bound statement,解决方式是在启动类上加@MapperScan("com.example.homework.mapper"),包路径写你自己的实际位置。端口占用则是在application.yml里换一个没被占用的端口,比如 8081、8082,不要动系统服务。
3. 数据库设计:作业系统六张核心表的字段与关系
3.1 三张主表:用户、课程与选课关系
课程作业管理系统的表设计不能只围绕“作业”,因为作业一定归属于某门课程,课程又有老师和学生的关系。最常见的合理拆法是六张表:sys_user、course、course_student、assignment、submission、review,再加一张attachment表用来存文件元信息也可以,但作业系统和文件系统不同,作业提交记录天然是一次性的,把文件路径直接存在submission表里更简单。
sys_user表是系统的地基,角色字段必须用整数类型而不是字符串,答辩时问你“为什么不用 varchar 存角色”,你可以回答:整数做判断效率更高,且配合枚举类可以避免魔法值满天飞。字段设计上要包含username、password、real_name、role、email五个核心字段,password存的是加盐后的哈希值而不是明文,这是答辩必问点。
CREATE TABLE `sys_user` ( `id` INT NOT NULL AUTO_INCREMENT, `username` VARCHAR(64) NOT NULL COMMENT '登录账号,唯一', `password` VARCHAR(128) NOT NULL COMMENT 'MD5加盐后的密文', `real_name` VARCHAR(32) NOT NULL COMMENT '真实姓名,页面展示用', `role` TINYINT NOT NULL DEFAULT 2 COMMENT '1=管理员, 2=教师, 3=学生', `email` VARCHAR(128) DEFAULT NULL, `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';密码加密不要用纯 MD5。MD5 本身已经被彩虹表打穿,正确做法是 MD5 加盐,盐值可以用用户名或随机 UUID,用 Hutool 的DigestUtil.md5Hex(password + salt)生成。答辩时如果老师问“为什么不用 BCrypt”,你答“BCrypt 更强但实现依赖 spring-security,本系统属于课程内部系统,MD5 加盐达到安全基线且实现轻量”,这是说得过去的。
course_student表的作用是处理“选课”这个多对多关系。有些同学偷懒直接在course表里加一个student_ids的 varchar 字段存逗号分隔的学生 ID,这是极其糟糕的设计,虽然写了一行省事,但后面的查询全部要FIND_IN_SET,无法走索引且代码里全是字符串切割。用关联表是标准答案,没有例外。
3.2 作业与提交:时间字段的类型和状态机设计
assignment表是“作业发布”的实体,字段围绕“布置了什么、什么时候截止、分值多少”来设计:
CREATE TABLE `assignment` ( `id` INT NOT NULL AUTO_INCREMENT, `course_id` INT NOT NULL COMMENT '所属课程ID', `title` VARCHAR(128) NOT NULL COMMENT '作业标题', `description` TEXT COMMENT '作业要求,支持纯文本或简单HTML', `deadline` DATETIME NOT NULL COMMENT '截止时间,用于迟交判断', `total_score` INT DEFAULT 100 COMMENT '满分,默认100', `allow_late` TINYINT DEFAULT 0 COMMENT '是否允许迟交:1允许,0不允许', `created_by` INT NOT NULL COMMENT '发布教师ID', `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_course_id` (`course_id`), KEY `idx_deadline` (`deadline`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='作业表';deadline字段是整个系统里最重要的字段,它直接决定了“迟交”怎么算。这里有个隐蔽的坑:如果程序里把 deadline 从数据库查出来是LocalDateTime,然后和服务器当前时间LocalDateTime.now()做比较,结果没问题;但如果你用的是new Date()或者时间戳毫秒值去比较,一旦你的程序部署的机器时区不是中国时区,判断就会差 8 小时。所以我统一了策略:所有时间字段用DATETIME+ JDBC 映射到LocalDateTime,比较操作也全部用LocalDateTime的isAfter/isBefore方法,不碰Date和Timestamp。
submission表承载学生的提交行为。它的设计要点不是字段多,而是要有一个“提交状态”字段来控制流程。这里的坑在于:学生可以多次提交、覆盖之前的作业,那么每次提交是生成新记录还是更新旧记录?我更倾向每次提交生成一条新记录,再用is_latest字段标记当前有效的那一条。这个设计在“教师查看作业”时的 SQL 写法会复杂一点,但换来的是完整的提交历史,学生、老师都可以看到哪次提交是什么时候做的,答辩讲故事时也有的说。
CREATE TABLE `submission` ( `id` INT NOT NULL AUTO_INCREMENT, `assignment_id` INT NOT NULL COMMENT '作业ID', `student_id` INT NOT NULL COMMENT '提交学生ID', `content` TEXT COMMENT '文字说明或备注', `file_path` VARCHAR(255) DEFAULT NULL COMMENT '上传文件的存储路径', `file_name` VARCHAR(255) DEFAULT NULL COMMENT '原始文件名,下载时用', `file_size` BIGINT DEFAULT 0 COMMENT '文件大小,字节', `submit_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, `is_late` TINYINT DEFAULT 0 COMMENT '是否迟交 1是 0否', `is_latest` TINYINT DEFAULT 1 COMMENT '是否是最新提交 1是 0否', PRIMARY KEY (`id`), KEY `idx_assignment_student` (`assignment_id`, `student_id`), KEY `idx_student` (`student_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='作业提交表';submit_time和is_late的赋值逻辑不要在数据库层做,必须在 Service 层用 Java 代码判断。判断的逻辑是:submit_time.isAfter(assignment.getDeadline())为真则is_late = 1,为假则0。如果你在数据库里用CURRENT_TIMESTAMP默认值,程序插入的时候不显式赋值,那么在测试环境可能看起来工作正常,但答辩演示时出问题,你排查的时间会远大于代码里多写一行赋值。提交功能里这个“判断 + 赋值”的动作,我放在第 4 章讲实现时会给出完整代码。
3.3 索引设计:组合索引与覆盖索引的取舍
很多毕设项目的表根本没有索引,或只在主键上建了索引,数据量小的时候没问题,但答辩老师一旦导入两千条数据测试就会出问题。作业系统里最频繁的查询是“教师查看某次作业的全部最新提交”,对应 SQL 是SELECT * FROM submission WHERE assignment_id = ? AND is_latest = 1。这个查询条件涉及两个字段,适当建立联合索引idx_assignment_latest (assignment_id, is_latest)就能避免走全表扫描。如果你只有单列索引,MySQL 会先按assignment_id过滤出几百行,再逐行判断is_latest,效率会低不少。
联合索引的字段顺序也有讲究,区分度高的放前面。assignment_id区分度远高于is_latest(后者只有 0 和 1 两个值),所以assignment_id放第一列是正确选择。如果查询还要加排序,比如按时间倒序展示,可以在索引末尾加submit_time,变成(assignment_id, is_latest, submit_time),查询就能用索引消除排序操作。但索引不是越多越好,每个索引都会拖慢写入速度,作业系统六张表的索引控制在每张三个以内比较合理。
4. 代码实现:上传、鉴权与查询三个关键链路
4.1 文件上传的幂等性与存储策略
文件上传是作业系统里最容易翻车的功能。首先要明确几个边界:文件大小限制在 50MB(已在配置里声明),扩展名只允许常见的.zip、.rar、.pdf、.doc、.docx、.ppt、.pptx、.jpg、.png,防止学生传可执行文件。存储路径不要用真实路径直接拼接,按/upload/2025/06/这种日期分目录的方式存储,文件名用 UUID 重命名,原始文件名单独记录在file_name字段里,下载时再恢复给用户。这样避免了两个问题:文件名冲突覆盖,以及中文文件名在部分操作系统的编码问题。
Controller 层接收文件用的是MultipartFile,Service 层做校验和存储。一个关键设计是“幂等提交”:学生提交时后端生成一个requestId存在submission表的隐藏字段或 session 里,如果学生重复点击提交按钮,第二次请求携带相同的requestId,后端直接返回第一次的结果,而不是再插入一条记录。实现上更简单的方式是:在提交前先查一下库里有没有assignment_id + student_id + is_latest=1的记录,如果有,就把旧记录的is_latest更新为 0,再插入新记录。这比 requestId 方案更直观,也完全避免了重复提交问题。
@Service public class SubmissionServiceImpl implements SubmissionService { @Autowired private SubmissionMapper submissionMapper; @Autowired private AssignmentMapper assignmentMapper; @Override @Transactional(rollbackFor = Exception.class) public SubmissionResult submitHomework(SubmissionRequest request, MultipartFile file) { // 1. 校验作业是否存在且仍在提交期内 Assignment assignment = assignmentMapper.selectById(request.getAssignmentId()); if (assignment == null) { throw new BizException("作业不存在"); } // 2. 将旧的最新提交标记为历史版本 Submission latest = submissionMapper.selectLatestByAssignmentAndStudent( request.getAssignmentId(), request.getStudentId()); if (latest != null) { latest.setIsLatest(false); submissionMapper.updateById(latest); } // 3. 处理文件存储(路径 + 原始文件名) String storedPath = fileService.store(file); // 4. 构建新提交记录 Submission submission = new Submission(); submission.setAssignmentId(request.getAssignmentId()); submission.setStudentId(request.getStudentId()); submission.setContent(request.getContent()); submission.setFilePath(storedPath); submission.setFileName(file.getOriginalFilename()); submission.setFileSize(file.getSize()); submission.setSubmitTime(LocalDateTime.now()); // 5. 迟交判断:核心逻辑,时间比较必须在服务端完成 boolean late = submission.getSubmitTime().isAfter(assignment.getDeadline()); submission.setIsLate(late); submission.setIsLatest(true); submissionMapper.insert(submission); return new SubmissionResult(submission.getId(), late); } }这段代码的关键在第 2 步和第 5 步。第 2 步先把旧提交的is_latest置为 false,再插入新记录,这样保证同一个学生同一作业始终只有一条is_latest=1的记录。第 5 步的isAfter判断用的是LocalDateTime,不会踩时区坑。@Transactional注解保证“更新旧记录 + 插入新记录 + 文件元信息入库”要么全部成功,要么全部回滚——如果不加事务,插入新记录成功但旧记录没更新成功,数据就会出现两条最新提交。文件存储本身是磁盘操作,不在事务控制范围内,所以我的习惯是“先存文件、后写数据库”,如果数据库写入失败,再删掉刚存的文件,避免磁盘垃圾文件积累,这一步也是血泪经验。
4.2 迟交判断策略:提交时校验与定时任务的边界
迟交判断理论上可以在两个地方做:提交时实时计算,或者用定时任务扫描截止时间后把所有过期未交的学生标记为缺交。不少同学倾向于写一个 Spring 定时任务去“半夜跑一遍”,但我建议优先用提交时判断,定时任务只做兜底。
原因很现实:如果学生 23:59 提交了作业,系统判定is_late=false,但定时任务在 00:00 跑了一遍,发现他 23:59 的提交已过期,把状态改成缺交——这就是两次判断的边界不一致导致的 bug。正确做法是:提交时判断并落库,定时任务只负责生成未交名单,不改任何submission的状态。定时任务的实现方式可以简单用 Spring 的@Scheduled(cron = "0 0 0 * * ?")在每天零点执行一次,扫描所有已截止但没有任何提交记录的学生,生成缺交名单表供教师查看。这件事不需要一个成熟的任务调度框架,Spring 内置的就够了。如果你在简历或论文里写了“引入 XXL-Job 做任务调度”,答辩时老师一定会追问为什么要上框架,你说不出合理的理由就会显得过度设计。
@Component public class AssignmentStatusTask { @Autowired private AssignmentMapper assignmentMapper; @Autowired private SubmissionMapper submissionMapper; @Scheduled(cron = "0 5 0 * * ?") @Transactional(rollbackFor = Exception.class) public void checkMissingSubmissions() { // 查询所有今日刚过截止时间且允许迟交的作业 List<Assignment> assignments = assignmentMapper.selectOverdueAssignments(); for (Assignment assignment : assignments) { List<Long> studentIds = assignmentMapper.selectStudentIdsByCourse(assignment.getCourseId()); for (Long studentId : studentIds) { Long submissionCount = submissionMapper.countByAssignmentAndStudent( assignment.getId(), studentId); if (submissionCount == null || submissionCount == 0) { // 这里只生成缺交记录,不改 submission submissionMapper.insertMissingFlag( assignment.getId(), studentId, LocalDateTime.now()); } } } } }cron表达式0 5 0 * * ?表示每天零点零五分执行,留出五分钟避免零点整点瞬时负载。selectOverdueAssignments的 SQL 条件是deadline < NOW() AND DATE(deadline) = CURDATE(),即“今天刚截止的作业”。这个定时任务写完后,在答辩演示时可以手动改数据库时间或直接调用一次接口来演示效果,不用真的等跨天,否则现场演示会很尴尬。
4.3 教师端的作业列表查询:必须手写 SQL 的场景
教师端“查看某次作业所有学生的提交情况”是最复杂的查询,涉及三张表:submission、sys_user、course_student。你需要查出“这门课有哪些学生、每个学生是否交了、交的是不是最新版、是否迟交、分数多少”。如果只用 MyBatis-Plus 的 QueryWrapper 一个个查,会出现经典的 N+1 问题——先查课程学生列表,再循环每个学生去查 submission,学生数 50,就要执行 51 条 SQL。
正确做法是手写一条多表关联 SQL,用左连接把没交的学生也带出来:
<select id="selectSubmissionOverview" resultType="com.example.homework.vo.SubmissionOverviewVO"> SELECT u.id AS studentId, u.real_name AS studentName, s.id AS submissionId, s.submit_time AS submitTime, s.is_late AS late, s.file_name AS fileName, s.file_path AS filePath, r.score AS score, r.comment AS comment, CASE WHEN s.id IS NULL THEN 0 ELSE 1 END AS submitted FROM course_student cs INNER JOIN sys_user u ON cs.student_id = u.id LEFT JOIN submission s ON s.assignment_id = #{assignmentId} AND s.student_id = u.id AND s.is_latest = 1 LEFT JOIN review r ON r.submission_id = s.id WHERE cs.course_id = #{courseId} ORDER BY u.id </select>这条 SQL 的LEFT JOIN是核心:course_student与sys_user是内连接,保证每个学生出现;与submission是左连接,且连接条件里带了is_latest = 1,这样才能做到每个学生只匹配到最新提交。review表存的是评分,评完分之后教师端列表要能直接显示分数和评语,不然老师还得点进详情去看。写成一条 SQL 后,查询次数固定为 1,和选课人数无关,这是答辩时“性能优化”的最佳例子。
5. 避坑清单:毕设答辩前最容易翻车的五个地方
5.1 中文乱码:不是所有乱码都是 UTF-8 的锅
现象:页面上显示的中文全部变成问号????,或者数据库中中文正常但页面乱码。
原因:三层乱码源——数据库连接字符串缺characterEncoding=utf8、页面模板没有声明charset="UTF-8"、Tomcat 的 URI 编码不是 UTF-8(请求参数带中文时出错)。
解决:application.yml里确认url参数带useUnicode=true&characterEncoding=utf8;HTML 的<head>里必须有<meta charset="UTF-8">;Spring Boot 的server.servlet.encoding.charset=UTF-8和force=true都加上。如果这三个都设置对了还乱码,检查数据库表字段的字符集是不是utf8mb4,大概率是建表时用了默认的latin1。
5.2 文件上传超过大小限制:报错信息拦在 Spring 层
现象:上传 60MB 的视频作业时,后台直接抛MaxUploadSizeExceededException,前端显示 500 错误页面,体验很差。
原因:spring.servlet.multipart.max-file-size设置的是单文件上限,Spring 会在进入 Controller 之前拦截,根本到不了你的业务代码。
解决:加一个全局异常处理器,捕获这个异常返回一个合理的 JSON 或错误页面,提示学生“附件不能超过 50MB”而不是报 500。同时别忘了一件事:个别课程可能传大文件,可以把上限放宽到 100MB,但服务器磁盘空间要确认足够,一次演示的时候传大文件把磁盘写满,后续所有上传就全挂了,这个成本不值得。
5.3 端口被占用:不是每次重启都成功
现象:启动时日志显示Web server failed to start. Port 8080 was already in use.
原因:上一次启动没有正常关闭,或者有别的进程占用了 8080 端口。
解决:命令行执行netstat -ano | findstr :8080,找出占用进程的 PID,再用taskkill /F /PID 进程号杀掉。不要反复改端口,开发时固定 8080 就好,免得改来改去上下文路径跟着乱。如果你用的是 IDEA,每次停止项目时确认控制台进程真的退出了,IDEA 的停止按钮有时候只断开连接不杀进程,这也是一个常见玄学问题。
5.4 权限校验只做了前端隐藏按钮:直接调接口就能绕过
现象:学生登录后,手动输入教师端的 URL 或直接调用后端接口,就能访问教师数据甚至删除作业。
原因:很多毕设只在页面上“隐藏”了教师端入口,后端接口没有任何权限拦截。这不是安全问题,而是你的系统根本没有权限设计。
解决:在 Spring Boot 里配置拦截器或过滤器,检查 session 里的用户角色。用一个LoginInterceptor实现HandlerInterceptor,在preHandle里校验是否是登录状态、访问的路径是否匹配角色,然后把静态资源和登录接口放行。这个实现不难,但它是答辩时老师最容易挑的刺,没有权限控制就相当于系统赤身裸体。
5.5 分页查询总条数不对:MyBatis-Plus 的一个隐藏行为
现象:学生列表或作业列表分页后总条数(total)是错误的,要么是 0,要么比实际少。
原因:MyBatis-Plus 的分页插件PaginationInnerInterceptor没有配置数据库类型,它默认按 H2 的方言处理,limit 语法和 MySQL 不一致。
解决:在 MyBatis-Plus 配置类里显式声明数据库类型:
@Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); PaginationInnerInterceptor pagination = new PaginationInnerInterceptor(DbType.MYSQL); pagination.setMaxLimit(500L); // 单页最多 500 条,防止拖垮数据库 interceptor.addInnerInterceptor(pagination); return interceptor; } }setMaxLimit(500L)是保护机制。如果某个页面的分页参数可被前端传入,恶意传一个size=1000000就是一次全表查询,加上这个限制后超出会被自动压回 500,这个细节在答辩时讲出来能加分。
6. 把系统讲清楚:论文、PPT 与开题报告的素材组织法
毕设答辩看的从来不只是代码跑没跑起来,而是你能不能把一个系统“讲明白”。论文的结构我建议顺着功能模块走,而不是按技术栈走。开题报告里写“拟解决的关键问题”,直接引用本系统的三个核心难点:迟交判定机制、多版本提交管理、基于角色的访问控制。论文正文按“需求分析 → 系统设计 → 数据库设计 → 功能实现 → 系统测试”这条线写,数据库设计的章节把第 3 章的六张表结构贴全,功能实现的章节把第 4 章的关键方法贴出来,测试部分写功能测试用例和结论即可,不要硬编 JMeter 压测数据。论文的图可以用 IDEA 的数据库图表导出功能生成 ER 图,这个工作量最小、视觉效果最清楚。
PPT 的核心只有一页:系统架构图。把你的 Spring Boot 工程画成三层,浏览器 → Controller → Service → Mapper → MySQL,配上文件存储的本地磁盘路径说明,这张图能讲 3 分钟,剩下的页放登录时序、作业提交流程图和五张核心表的关系截图。不要贴大段代码到 PPT 上,老师看不完,你自己也讲不完。
做完整套材料后我的习惯是查一遍论文里的每一个“实现”是否都能在代码里找到对应——答辩最怕的是论文写了定时任务、代码里却没有。我有一次就是论文里写“系统支持批量导入学生名单”,代码里没做,老师当场让演示,我在台上翻遍代码没找到,只能解释“该功能在下一个迭代计划中”,非常狼狈。确保论文、PPT、代码三者完全一致,否则宁可删掉论文里的功能描述也不要留着这个隐患。每次答辩前我都会走一遍“从部署到演示”的完整流程,包括重新配一次数据库、重新启动一次项目、重新上传一次作业,这些动作看起来简单,但每一步都可能踩到环境问题。这个系统的价值就在这了——不一定多惊艳,但每一层都能讲清楚,就是一个合格的毕设。把这条链路跑顺,希望帮到你。
本文还有配套的精品资源,点击获取