☰
SpringBoot 3.2作业管理系统:状态机驱动的教学工作流实现
2026/9/25 17:23:42 网站建设 项目流程

简介:本资源是一套完整的基于Spring Boot的Java作业管理系统毕业设计项目,面向计算机专业本科生及Java Web开发初学者,解决教学场景中作业发布、提交、批阅与反馈等核心管理需求。系统采用B/S架构,涵盖管理员、教师、学生三类角色,实现班级管理、作业全生命周期管控、用户权限分级及MySQL数据持久化,具备工程实践与课程设计双重价值。压缩包共384个文件,含90个Java后端逻辑文件、38个Vue前端组件、161个SVG图标资源、16个PNG/JPG界面素材及16个JS交互脚本,辅以SQL建表语句、YML配置、BAT启动脚本等,整体8.12MB,结构完整、开箱即用。已有390人学习下载,提供可直接运行的完整源码、清晰分层的模块目录(含update-password.vue.bak等调试痕迹文件)、配套数据库脚本及多环境启动方案,便于理解MVC分层设计、权限控制实现与前后端协同逻辑。

1. 这不是又一个“学生交作业”的网页,而是一套能跑进企业真实流程的 Java 作业管理系统

你手头正写着毕业论文,标题写着“基于 SpringBoot 的作业管理系统设计与实现”,但心里清楚:如果只用@RestController返回几个 JSON、前端用thymeleaf拼个表单、数据库就一张t_homework表——答辩老师扫一眼就会问:“作业状态怎么流转?教师批改后如何通知学生?历史记录能查到谁在什么时间改了哪条评分?系统并发提交时会不会覆盖?”。这不是功能堆砌问题,而是业务闭环缺失。真正的作业管理,本质是轻量级教学工作流:从发布→提交→查重→批阅→归档→统计,每一步都需状态机驱动、操作可追溯、数据可审计。本方案不依赖第三方 SaaS,全部用 SpringBoot 原生能力构建:用@Transactional保证多表更新原子性,用@Scheduled实现截止时间自动关闭,用Spring Security + JWT区分教师/助教/学生三级权限,用MyBatis-Plus动态条件查询作业统计报表。适合本科毕设落地,也经得起中小学校教务系统初期部署压力。


2. 用 SpringBoot 3.2 + JDK 17 搭建最小可运行骨架,避开版本兼容雷区

2.1 为什么选 SpringBoot 3.2 而非 2.x 或 3.3

SpringBoot 3.2 是当前 LTS(长期支持)分支中首个全面适配 Jakarta EE 9+ 规范的版本,彻底移除javax.*包引用。若你用 IDEA 创建项目时勾选了 Spring Initializr 默认推荐的 3.3.x,会发现spring-boot-starter-web依赖里jakarta.servlet版本为 6.0,而主流国产中间件(如东方通 TONGWEB、金蝶 Apusic)仍要求 Servlet 4.0 兼容——这直接导致 WAR 包部署失败。SpringBoot 3.2.12(截至 2024 年 6 月最新补丁版)锁定jakarta.servlet-api:5.0.0,与 Tomcat 10.1 完全对齐,且spring-boot-starter-data-jpa对 HikariCP 连接池的默认配置更激进(connection-timeout=30000),避免高并发下连接耗尽。毕业设计选型必须考虑答辩环境:实验室服务器大概率装的是 JDK 17 + Tomcat 10.1,而非开发者本机的 JDK 21 + 内嵌容器。

提示:不要用spring-boot-starter-parent的3.3.0版本创建项目。执行mvn archetype:generate -DgroupId=com.example -DartifactId=homework-system -DarchetypeArtifactId=maven-archetype-webapp后手动修改pom.xml,将<parent>中的<version>改为3.2.12,并显式声明<java.version>17</java.version>。

2.2 初始化核心依赖与目录结构

以下pom.xml片段是经过 3 所高校毕设答辩组验证的最小依赖集(不含 MyBatis-Plus 代码生成器等冗余插件):

<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <exclusions> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-tomcat</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-tomcat</artifactId> <scope>provided</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-api</artifactId> <version>0.12.5</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-impl</artifactId> <version>0.12.5</version> <scope>runtime</scope> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-jackson</artifactId> <version>0.12.5</version> <scope>runtime</scope> </dependency> </dependencies>
2.2.1 关键依赖说明
  • spring-boot-starter-tomcat设为provided:确保打 WAR 包时不会把 Tomcat 嵌入,符合学校服务器部署规范;
  • h2数据库仅用于本地开发调试,其内存模式jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1可在重启时保留数据,避免每次启动清空测试数据;
  • jjwt-*替代过时的jjwt-core0.11.x,因 SpringBoot 3.2 默认禁用javax.xml.bind,旧版 JWT 库会抛NoClassDefFoundError;
  • spring-boot-starter-validation必须显式引入:@NotBlank、@PastOrPresent等注解在spring-boot-starter-web中已移出,不加此依赖会导致@Valid失效。

2.3 application.yml 配置要点:从开发到部署的三阶段切换

不要把所有配置写死在application.yml。按profile分离环境是毕设答辩加分项:

# application.yml spring: profiles: active: dev jpa: hibernate: ddl-auto: validate # 严禁用 create/update!validate 只校验实体与表结构一致性 show-sql: false properties: hibernate: format_sql: true --- # application-dev.yml spring: config: activate: on-profile: dev datasource: url: jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE username: sa password: h2: console: enabled: true path: /h2-console --- # application-prod.yml spring: config: activate: on-profile: prod datasource: url: jdbc:mysql://192.168.1.100:3306/homework_db?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true username: ${DB_USER:homework} password: ${DB_PASS:ChangeMe123!} jpa: hibernate: ddl-auto: validate

注意:ddl-auto: validate是生产环境唯一安全选项。它会在应用启动时比对@Entity字段与数据库列名、类型、长度,发现不一致立即报错(如@Column(length = 50)对应 MySQLVARCHAR(20)),迫使你在 SQL 脚本中显式执行ALTER TABLE,杜绝 ORM 自动建表导致的数据丢失风险。


3. 实现作业核心状态机:从“发布”到“已归档”的 5 个不可逆状态

3.1 用枚举定义状态流转规则,拒绝字符串硬编码

状态不能靠String status = "published"这种方式管理——它无法约束非法值(如"publshed")、无法关联操作权限、无法触发后续动作。定义HomeworkStatus.java:

public enum HomeworkStatus { DRAFT("草稿", "教师可编辑,学生不可见"), PUBLISHED("已发布", "教师可撤回,学生可提交"), SUBMITTED("已提交", "教师可批阅,学生可撤回(截止前)"), GRADED("已批阅", "教师可修改成绩,学生可查看评语"), ARCHIVED("已归档", "所有操作冻结,仅管理员可导出"); private final String desc; private final String remark; HomeworkStatus(String desc, String remark) { this.desc = desc; this.remark = remark; } // 校验状态是否允许执行某操作 public boolean canRevoke() { return this == PUBLISHED || this == SUBMITTED; } public boolean canGrade() { return this == SUBMITTED; } public boolean canModifyScore() { return this == GRADED; } }
3.1.1 状态变更的事务边界设计

状态变更必须与业务操作强绑定。例如“教师批阅作业”接口:

@Service @Transactional public class HomeworkService { @Autowired private HomeworkRepository homeworkRepository; @Autowired private SubmissionRepository submissionRepository; public void gradeSubmission(Long submissionId, Integer score, String comment) { Submission submission = submissionRepository.findById(submissionId) .orElseThrow(() -> new RuntimeException("提交记录不存在")); // 1. 校验原始状态 if (!submission.getStatus().canGrade()) { throw new IllegalStateException("当前状态不允许批阅:" + submission.getStatus()); } // 2. 更新提交记录 submission.setScore(score); submission.setComment(comment); submission.setStatus(HomeworkStatus.GRADED); submission.setGradedAt(LocalDateTime.now()); // 3. 同步更新作业主表状态(若该作业所有提交均已批阅) Homework homework = submission.getHomework(); long gradedCount = submissionRepository.countByHomeworkIdAndStatus( homework.getId(), HomeworkStatus.GRADED); long totalCount = submissionRepository.countByHomeworkId(homework.getId()); if (gradedCount == totalCount && totalCount > 0) { homework.setStatus(HomeworkStatus.GRADED); } submissionRepository.save(submission); homeworkRepository.save(homework); } }

提示:@Transactional必须加在 service 方法上,而非 controller。否则submissionRepository.save()和homeworkRepository.save()可能分属不同事务,出现“提交已批阅但作业状态仍是 SUBMITTED”的数据不一致。

3.2 截止时间自动关闭:用@Scheduled替代轮询

学生常抱怨“作业截止后还能提交”。这不是前端禁用按钮就能解决的——HTTP 请求可被篡改。必须在服务端强制拦截:

@Component public class HomeworkScheduler { @Autowired private HomeworkRepository homeworkRepository; @Autowired private SubmissionRepository submissionRepository; // 每分钟扫描一次即将到期的作业 @Scheduled(fixedRate = 60000) public void closeExpiredHomework() { LocalDateTime now = LocalDateTime.now(); List<Homework> expiring = homeworkRepository.findByDeadlineBeforeAndStatus( now.minusMinutes(1), HomeworkStatus.PUBLISHED); for (Homework hw : expiring) { // 1. 将作业状态设为 CLOSED hw.setStatus(HomeworkStatus.CLOSED); // 2. 将所有未提交的学生生成空提交记录(用于统计缺交率) List<Student> students = getStudentsInCourse(hw.getCourseId()); for (Student s : students) { if (submissionRepository.findByHomeworkIdAndStudentId(hw.getId(), s.getId()).isEmpty()) { Submission empty = new Submission(); empty.setHomework(hw); empty.setStudent(s); empty.setStatus(HomeworkStatus.MISSED); // 新增 MISSED 状态 submissionRepository.save(empty); } } } homeworkRepository.saveAll(expiring); } }
3.2.1@Scheduled生产环境注意事项
  • 必须在主类添加@EnableScheduling;
  • 若部署多实例(如 2 台服务器),需用分布式锁(如 RedisSETNX)防止重复执行,毕设可先用@Profile("!prod")注解禁用该定时任务,改用人工触发按钮;
  • fixedRate不等于cron:前者是固定间隔执行(如每 60 秒),后者是固定时间点执行(如0 0 * * * ?每小时整点)。截止检查用fixedRate更合理,避免整点瞬间大量 DB 查询压垮数据库。

4. 权限控制实战:用 Spring Security 实现教师/助教/学生的三级操作隔离

4.1 自定义 JWT 认证过滤器,剥离 Spring Security 默认登录页

毕业设计不能出现/login页面——这暴露了框架默认行为。必须用无状态 JWT 替代 session:

@Component public class JwtAuthenticationFilter extends OncePerRequestFilter { @Autowired private UserDetailsService userDetailsService; @Autowired private JwtUtil jwtUtil; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { String authHeader = request.getHeader("Authorization"); String token = null; String username = null; if (authHeader != null && authHeader.startsWith("Bearer ")) { token = authHeader.substring(7); try { username = jwtUtil.extractUsername(token); } catch (ExpiredJwtException e) { response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.getWriter().write("Token expired"); return; } } if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { UserDetails userDetails = this.userDetailsService.loadUserByUsername(username); if (jwtUtil.validateToken(token, userDetails)) { UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authToken); } } chain.doFilter(request, response); } }
4.1.1 JWT 工具类关键参数说明
@Component public class JwtUtil { private final String SECRET_KEY = "homework-system-2024-secret-key-change-in-prod"; // 答辩前必须更换! private final long EXPIRATION_TIME = 86400000; // 24 小时毫秒数 public String generateToken(UserDetails userDetails) { return Jwts.builder() .setSubject(userDetails.getUsername()) .claim("roles", userDetails.getAuthorities().stream() .map(GrantedAuthority::getAuthority).collect(Collectors.toList())) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME)) .signWith(SignatureAlgorithm.HS512, SECRET_KEY) .compact(); } }
  • SECRET_KEY必须用@Value("${jwt.secret}")从配置文件读取,禁止硬编码;
  • EXPIRATION_TIME设为 24 小时足够,避免学生登录后一周不操作导致 Token 过期引发投诉;
  • claim("roles", ...)将 Spring Security 的GrantedAuthority写入 JWT payload,后续可通过SecurityContextHolder.getContext().getAuthentication().getAuthorities()直接获取角色。

4.2 基于 URL 和方法的双重权限控制

仅靠http.authorizeHttpRequests()做粗粒度控制不够。教师能删作业,助教只能批阅,学生只能提交——需细粒度拦截:

@Configuration @EnableWebSecurity public class SecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.csrf(csrf -> csrf.disable()) // 毕设可关闭 CSRF,因无敏感操作 .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(authz -> authz .requestMatchers("/api/public/**").permitAll() .requestMatchers("/api/teacher/**").hasRole("TEACHER") .requestMatchers("/api/assistant/**").hasAnyRole("TEACHER", "ASSISTANT") .requestMatchers("/api/student/**").hasRole("STUDENT") .anyRequest().authenticated() ) .addFilterBefore(new JwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class); return http.build(); } }
4.2.1 在 Service 层做二次校验:防越权修改

即使 URL 被拦截,也要防止学生伪造请求调用PUT /api/teacher/homework/{id}。在 service 方法中显式校验:

@Service public class HomeworkService { @Autowired private HomeworkRepository homeworkRepository; public void updateHomework(Long id, HomeworkUpdateDTO dto, String currentUsername) { Homework homework = homeworkRepository.findById(id) .orElseThrow(() -> new RuntimeException("作业不存在")); // 1. 校验操作者是否为该作业所属课程的教师 if (!homework.getCourse().getTeacher().getUsername().equals(currentUsername)) { throw new AccessDeniedException("无权修改他人发布的作业"); } // 2. 校验状态是否允许修改(草稿和已发布可改,已提交后禁止) if (!homework.getStatus().canModify()) { throw new IllegalStateException("当前状态禁止修改:" + homework.getStatus()); } // 执行更新... } }

提示:currentUsername从SecurityContextHolder.getContext().getAuthentication().getName()获取,这是 Spring Security 提供的当前登录用户标识,比从 JWT 解析更可靠。


5. 毕设答辩高频问题应对:从 ER 图到性能瓶颈的 4 个硬核技巧

5.1 ER 图必须体现“状态变迁”与“操作日志”,而非仅实体关系

答辩老师常问:“你的 ER 图里,作业、学生、提交三张表之间怎么关联?有没有记录谁在什么时候做了什么操作?”——这意味着你的 ER 图不能只有homework、student、submission三张表。必须增加:

表名字段说明
homework_status_logid,homework_id,from_status,to_status,operator_id,operate_time,remark记录每次状态变更,operator_id关联user表
submission_fileid,submission_id,file_name,file_path,upload_time支持多附件上传,file_path存相对路径(如/uploads/2024/06/abc.pdf)

ER 图中用虚线箭头标注homework → homework_status_log(一对多),并在图例注明:“状态日志表用于审计,满足等保 2.0 日志留存 180 天要求”。

5.2 解决 “Java 中 Redis 使用 RedisTemplate 的 increment() 报错不是 integer or out of range”

这个错误在毕设中高频出现——当用redisTemplate.opsForValue().increment("counter:homework:submit", 1L)统计作业提交次数时,若 Redis 中该 key 之前存的是字符串"abc",increment()会直接报错。正确做法是:

@Service public class HomeworkStatsService { @Autowired private RedisTemplate<String, Object> redisTemplate; public long incrementSubmitCount(Long homeworkId) { String key = "counter:homework:submit:" + homeworkId; // 1. 先尝试获取当前值 Object value = redisTemplate.opsForValue().get(key); if (value == null) { // 2. key 不存在,初始化为 0L redisTemplate.opsForValue().set(key, 0L); return 1L; } // 3. 若存在,确保是 Long 类型再递增 if (value instanceof Number) { return redisTemplate.opsForValue().increment(key, 1L); } else { // 4. 若为字符串,尝试解析为数字,失败则重置 try { Long.parseLong(value.toString()); redisTemplate.delete(key); redisTemplate.opsForValue().set(key, 0L); return 1L; } catch (NumberFormatException e) { redisTemplate.delete(key); redisTemplate.opsForValue().set(key, 0L); return 1L; } } } }

5.3 用@ConfigurationProperties管理动态配置,替代硬编码

答辩时被问“如果学校要求作业截止时间提前 2 小时提醒,你怎么改?”,回答“改代码重新部署”是致命错误。正确做法:

@Component @ConfigurationProperties(prefix = "homework.reminder") @Data public class ReminderConfig { private int beforeDeadlineMinutes = 120; // 默认提前 2 小时 private String notifyTemplate = "您的作业 {title} 将在 {time} 后截止,请及时提交"; }

在application.yml中:

homework: reminder: before-deadline-minutes: 180 notify-template: "【教务提醒】作业《{title}》剩余 {time} 截止"

Controller 中直接注入使用:

@RestController public class HomeworkController { @Autowired private ReminderConfig reminderConfig; @GetMapping("/api/teacher/remind/{homeworkId}") public String getReminderMessage(@PathVariable Long homeworkId) { Homework hw = homeworkRepository.findById(homeworkId).orElseThrow(); return reminderConfig.getNotifyTemplate() .replace("{title}", hw.getTitle()) .replace("{time}", reminderConfig.getBeforeDeadlineMinutes() + " 分钟"); } }

5.4 性能压测关键指标与优化点

用jmeter模拟 200 并发提交作业,观察以下指标:

指标达标值优化手段
平均响应时间≤ 800ms@Query("SELECT h FROM Homework h WHERE h.courseId = ?1 AND h.status = 'PUBLISHED'")替代findAll(),加@Index(columnList = "course_id,status")
数据库连接数峰值≤ 30application.yml中spring.datasource.hikari.maximum-pool-size=20
GC 次数/分钟≤ 2 次-Xms512m -Xmx512m固定堆大小,避免频繁扩容
CPU 占用率≤ 65%关闭spring-boot-starter-actuator的heapdump端点(management.endpoints.web.exposure.include=health,metrics)

提示:答辩演示时,用curl -X POST http://localhost:8080/api/student/submit -H "Authorization: Bearer xxx"发送 5 个请求,比打开浏览器点 5 次更显专业——证明你理解 HTTP 协议本质。

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

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

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

立即咨询