JavaWeb作业提交与批改系统毕设实战:SpringBoot+MyBatis核心实现与避坑指南
发布时间:2026/10/2 14:03:07来源:尧图网络
简介这是一套基于JavaWeb的作业提交与批改系统项目源码面向计算机相关专业做毕设的学生以及需要项目实战练习的Java学习者可直接作为毕业设计使用。系统采用B/S结构后台基于JSP、Servlet、JDBC开发以MySQL作为数据库开发环境为JDK、Eclipse与Tomcat并划分管理员、教师、学生三种角色。功能覆盖班级、教师、学生、公告、留言、资料及系统管理七大模块支持信息的录入、查询、删除与打印等操作界面美观、操作简单、管理便捷。资源包共3个文件包含项目源码压缩包、数据库脚本sql文件以及项目说明txt文档整体约4.51MB源码与脚本均已严格调试可确保运行。目前已有3583人学习下载适合希望快速搭建作业管理场景、理解JavaWeb分层开发与数据库设计的读者参考借鉴。1. 作业提交与批改系统的真实痛点为什么 JavaWeb 毕设总在“交作业”这一步翻车每年到毕设季基于 JavaWeb 的作业提交与批改系统都是计算机毕设选题里的高频选项。原因很直接需求场景清晰功能边界可控老师看得懂答辩好讲。但真正动手做过的人都知道这类系统最容易翻车的不是登录注册而是作业文件的上传下载、批改状态的流转、以及数据库脚本在别人机器上跑不起来。我见过太多项目在自己电脑上跑得好好的换台机器导入 SQL 就报外键约束错误或者上传的作业文件路径写死在本地 D 盘部署到服务器直接 404。这个系统的核心价值在于把“布置作业 → 学生提交 → 教师批改 → 成绩反馈”这条链路用代码固化下来。适合正在做毕设的计算机专业学生也适合想拿一个完整 JavaWeb 项目练手的初学者。技术栈上常见做法是 SpringBoot MyBatis MySQL Thymeleaf 或 Vue数据库脚本用 IDEA 导出或手写建表语句。下面我会按实际开发顺序把选型、建库、核心功能实现和踩坑记录一层层拆开讲。2. 技术选型与数据库设计从“能跑”到“好改”的取舍2.1 为什么我最终选了 SpringBoot MyBatis 而不是纯 JSP Servlet纯 JSP Servlet 是很多 JavaWeb 课程的标配黑马程序员的笔记里也大量涉及。但真做毕设纯 JSP 的痛点很明显页面逻辑和 Java 代码混在一起改一个字段要动三四个文件答辩时老师问“你的分层在哪里”很难回答。SpringBoot 的好处是起步依赖帮你把 Tomcat、Jackson、日志全配好了application.yml里改个端口就行不用折腾web.xml。MyBatis 相比 JPA 更适合毕设场景。作业提交与批改系统的查询条件往往很灵活——按班级查、按课程查、按提交状态查、按学生姓名模糊查。MyBatis 的 XML 映射可以直接写动态 SQLif teststatus ! null这种标签用起来很顺手。JPA 虽然也能做但复杂查询要么写 Specification 要么写原生 SQL对新手不够直观。前端我建议用 Thymeleaf 而不是前后端分离的 Vue。原因很现实毕设周期通常只有两三个月前后端分离要处理跨域、Token 刷新、接口联调时间成本翻倍。Thymeleaf 模板直接放在resources/templates下Controller 返回视图名就能渲染配合 Bootstrap 快速出效果。如果导师明确要求前后端分离那就用 Vue Axios后端接口统一返回ResultT包装类。2.2 数据库表设计五张核心表与字段类型选择作业提交与批改系统的数据库不需要太复杂但几个关键字段的类型选错后面改起来很痛苦。下面是我常用的建表脚本基于 MySQL 8.0字符集用utf8mb4排序规则utf8mb4_general_ci。-- 用户表区分学生、教师、管理员 CREATE TABLE sys_user ( user_id BIGINT NOT NULL AUTO_INCREMENT COMMENT 主键, username VARCHAR(50) NOT NULL COMMENT 登录名, password VARCHAR(100) NOT NULL COMMENT 密码BCrypt加密, real_name VARCHAR(50) DEFAULT NULL COMMENT 真实姓名, role TINYINT NOT NULL DEFAULT 0 COMMENT 0学生 1教师 2管理员, class_id BIGINT DEFAULT NULL COMMENT 班级ID学生用, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (user_id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表; -- 作业表教师布置的作业 CREATE TABLE homework ( hw_id BIGINT NOT NULL AUTO_INCREMENT, title VARCHAR(200) NOT NULL COMMENT 作业标题, content TEXT COMMENT 作业要求, teacher_id BIGINT NOT NULL COMMENT 布置教师, class_id BIGINT NOT NULL COMMENT 面向班级, deadline DATETIME NOT NULL COMMENT 截止时间, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (hw_id), KEY idx_teacher (teacher_id), KEY idx_class (class_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT作业表; -- 提交表学生提交记录 CREATE TABLE submission ( sub_id BIGINT NOT NULL AUTO_INCREMENT, hw_id BIGINT NOT NULL, student_id BIGINT NOT NULL, file_path VARCHAR(500) DEFAULT NULL COMMENT 文件相对路径, file_name VARCHAR(200) DEFAULT NULL COMMENT 原始文件名, submit_time DATETIME DEFAULT CURRENT_TIMESTAMP, status TINYINT DEFAULT 0 COMMENT 0未批改 1已批改, score DECIMAL(5,1) DEFAULT NULL COMMENT 分数, comment TEXT COMMENT 教师评语, PRIMARY KEY (sub_id), UNIQUE KEY uk_hw_student (hw_id,student_id), KEY idx_student (student_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT提交表;这里有几个设计决策值得展开。submission表上建了uk_hw_student唯一索引防止同一个学生对同一份作业重复提交。如果业务允许覆盖提交就在 Service 层做“先删后插”或“更新”逻辑但数据库约束要留着这是最后一道防线。file_path存相对路径而不是绝对路径比如/upload/2025/03/xxx.pdf这样换服务器或改存储目录时不用批量更新数据库。score用DECIMAL(5,1)而不是FLOAT避免浮点精度问题支持 0.5 分这种粒度。班级表和课程表按需扩展毕设里如果只做单班级演示可以简化成class_id直接存在用户表里。但建议至少留出class表答辩时老师问“多个班级怎么处理”你能答上来。2.3 IDEA 导出数据库脚本的两种方式与常见报错很多毕设要求提交“数据库脚本”用 IDEA 的 Database 工具导出最方便。右键数据库连接 → SQL Scripts → Generate DDL Data Source选好输出路径即可。但导出的脚本经常带AUTO_INCREMENTxxx和DROP TABLE IF EXISTS前者会让新环境插入数据时主键从奇怪的值开始后者在导入顺序不对时会报外键依赖错误。我一般会手动清理导出的脚本删掉所有AUTO_INCREMENT后面的数字保留DROP TABLE IF EXISTS但调整表顺序先删子表再删父表。如果用了外键约束导入时先执行SET FOREIGN_KEY_CHECKS 0;导入完成后再SET FOREIGN_KEY_CHECKS 1;。这个技巧在导入别人给的脚本时特别管用能省掉大量“Cannot add or update a child row”的排查时间。3. 核心功能实现文件上传、批改状态流转与分页查询3.1 作业文件上传从 MultipartFile 到磁盘落地的完整链路文件上传是这类系统的命脉也是最容易出安全漏洞的地方。SpringBoot 默认支持MultipartFile但有几个参数必须显式配置。在application.yml里spring: servlet: multipart: max-file-size: 20MB # 单文件上限 max-request-size: 50MB # 单次请求总上限 enabled: trueController 层接收文件并保存PostMapping(/submit) public ResultString submitHomework( RequestParam(hwId) Long hwId, RequestParam(file) MultipartFile file, HttpSession session) { // 1. 校验登录态 SysUser user (SysUser) session.getAttribute(loginUser); if (user null || user.getRole() ! 0) { return Result.error(请以学生身份登录); } // 2. 校验文件非空 if (file.isEmpty()) { return Result.error(请选择文件); } // 3. 校验扩展名白名单 String originalName file.getOriginalFilename(); String ext originalName.substring(originalName.lastIndexOf(.) 1).toLowerCase(); ListString allowExt Arrays.asList(pdf, doc, docx, zip, rar, java, py); if (!allowExt.contains(ext)) { return Result.error(不支持的文件类型 ext); } // 4. 生成存储路径按日期分目录避免单目录文件过多 String dateDir new SimpleDateFormat(yyyy/MM).format(new Date()); String uploadDir D:/upload/ dateDir; // 生产环境改为配置项 File dir new File(uploadDir); if (!dir.exists()) { dir.mkdirs(); } // 5. 用 UUID 重命名防止中文名和路径穿越 String newFileName UUID.randomUUID().toString().replace(-, ) . ext; File dest new File(dir, newFileName); try { file.transferTo(dest); } catch (IOException e) { return Result.error(文件保存失败); } // 6. 写数据库 Submission sub new Submission(); sub.setHwId(hwId); sub.setStudentId(user.getUserId()); sub.setFilePath(/upload/ dateDir / newFileName); sub.setFileName(originalName); submissionService.saveOrUpdate(sub); return Result.success(提交成功); }这段代码的关键点在于扩展名白名单必须做不能只靠前端accept属性UUID 重命名既避免了中文乱码也防止了../../这种路径穿越攻击按年月分目录是为了文件多了以后ls不卡。file.transferTo(dest)在 SpringBoot 里底层用的是StandardMultipartFile如果目标目录不存在会抛FileNotFoundException所以mkdirs()不能省。下载接口用ResponseEntityResource实现设置Content-Disposition时注意中文文件名要用URLEncoder.encode(fileName, UTF-8)处理否则浏览器下载下来是乱码。3.2 批改状态流转从“未批改”到“已批改”的原子操作批改功能看起来简单——教师填个分数和评语更新一下状态。但并发场景下容易出问题两个教师同时打开同一个提交记录A 先提交了分数B 后提交覆盖了 A 的批改结果。解决办法是在 SQL 更新时加状态条件UPDATE submission SET score #{score}, comment #{comment}, status 1 WHERE sub_id #{subId} AND status 0MyBatis 的update返回受影响行数Service 层判断返回值int rows submissionMapper.updateScore(subId, score, comment); if (rows 0) { throw new BusinessException(该作业已被批改请刷新后查看); }这个“乐观锁”思路在毕设里足够用了不需要引入版本号字段。另外批改完成后如果要做成绩统计建议在homework表加一个corrected_count字段每次批改成功后UPDATE homework SET corrected_count corrected_count 1 WHERE hw_id ?这样教师端首页能直接显示“已批改 23/45”不用每次 count 子查询。3.3 分页查询与动态条件MyBatis 的where和 PageHelper学生端要查“我的提交记录”教师端要查“某次作业的提交列表”管理员要查“所有用户”。这些查询条件组合多用 MyBatis 动态 SQL 最省事。引入 PageHelper 分页插件dependency groupIdcom.github.pagehelper/groupId artifactIdpagehelper-spring-boot-starter/artifactId version1.4.6/version /dependencyMapper XML 里写select idselectByCondition resultTypecom.example.entity.SubmissionVO SELECT s.*, u.real_name AS studentName, h.title AS hwTitle FROM submission s LEFT JOIN sys_user u ON s.student_id u.user_id LEFT JOIN homework h ON s.hw_id h.hw_id where if testhwId ! nullAND s.hw_id #{hwId}/if if teststudentId ! nullAND s.student_id #{studentId}/if if teststatus ! nullAND s.status #{status}/if if testkeyword ! null and keyword ! AND (u.real_name LIKE CONCAT(%, #{keyword}, %) OR h.title LIKE CONCAT(%, #{keyword}, %)) /if /where ORDER BY s.submit_time DESC /selectService 层调用PageHelper.startPage(pageNum, pageSize)后再执行查询返回的PageInfo对象直接传给前端渲染分页条。注意PageHelper.startPage必须紧挨着查询方法中间不能插入其他数据库操作否则分页会作用到错误的 SQL 上。这个坑我踩过表现为分页数量对不上排查了半天才发现是中间多了一次count查询。4. 避坑与排查那些让毕设答辩翻车的细节4.1 上传文件后刷新页面 404路径拼接少了斜杠现象文件上传成功数据库里file_path存的是/upload/2025/03/xxx.pdf但前端下载链接写成${path}直接拼在域名后面结果请求变成http://localhost:8080upload/...少了斜杠。原因Thymeleaf 里用th:href{/download?path ${sub.filePath}}时如果filePath以/开头浏览器解析相对路径会出问题。解决统一在 Controller 的下载接口用RequestParam接收路径后端拼完整 URL 返回给前端或者前端用th:href{/download(path${sub.filePath})}让 Thymeleaf 处理 URL 编码。4.2 导入 SQL 脚本报“Unknown character set: utf8mb4_0900_ai_ci”现象在自己电脑上 MySQL 8.0 导出的脚本拿到同学电脑 MySQL 5.7 上导入报错。原因MySQL 8.0 默认排序规则是utf8mb4_0900_ai_ci5.7 不认识。解决导出时显式指定utf8mb4_general_ci或者导入前用文本编辑器全局替换utf8mb4_0900_ai_ci为utf8mb4_general_ci。更稳妥的做法是在建库语句里写死DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_general_ci。4.3 教师批改页面看不到学生姓名VO 字段没对上现象SQL 里LEFT JOIN sys_user查出了real_name但前端显示空白。原因SubmissionVO类里字段名是studentName而 SQL 里别名写的是student_nameMyBatis 的mapUnderscoreToCamelCase没开启。解决在application.yml里加mybatis.configuration.map-underscore-to-camel-case: true或者 SQL 别名直接用驼峰AS studentName。建议开启下划线转驼峰一劳永逸。4.4 截止时间判断失效时区差了 8 小时现象作业截止时间设的是2025-03-20 23:59:59但学生 23:50 提交时提示“已过截止时间”。原因MySQL 连接 URL 没指定时区服务器 UTC 时间比北京时间早 8 小时。解决JDBC URL 加上serverTimezoneAsia/Shanghai完整示例jdbc:mysql://localhost:3306/homework_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse。4.5 部署到服务器后上传目录不存在绝对路径写死了现象本地开发时上传目录是D:/upload/打成 jar 包扔到 Linux 服务器上文件保存失败。原因代码里硬编码了 Windows 路径。解决把上传根路径抽到application.yml里用Value(${file.upload-dir})注入。生产环境配置成/data/upload/开发环境配置成D:/upload/。同时确保运行 jar 的用户对该目录有写权限chmod 755或chown一下。5. 进阶技巧用拦截器 注解做权限校验以及一个验证系统是否“真跑通”的笨办法权限校验是毕设答辩的必问点。很多项目在每個 Controller 方法里写if (user.getRole() ! 1) return error;代码重复且容易漏。更优雅的做法是自定义注解 HandlerInterceptor。先定义注解Target(ElementType.METHOD) Retention(RetentionPolicy.RUNTIME) public interface RequireRole { int[] value() default {}; // 允许的角色空数组表示登录即可 }拦截器实现public class AuthInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (!(handler instanceof HandlerMethod)) { return true; } HandlerMethod hm (HandlerMethod) handler; RequireRole anno hm.getMethodAnnotation(RequireRole.class); if (anno null) { return true; // 没加注解的接口放行 } HttpSession session request.getSession(); SysUser user (SysUser) session.getAttribute(loginUser); if (user null) { response.sendRedirect(/login); return false; } int[] roles anno.value(); if (roles.length 0) { boolean allowed Arrays.stream(roles).anyMatch(r - r user.getRole()); if (!allowed) { response.setStatus(403); response.getWriter().write(无权限访问); return false; } } return true; } }注册拦截器Configuration public class WebConfig implements WebMvcConfigurer { Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new AuthInterceptor()) .addPathPatterns(/**) .excludePathPatterns(/login, /doLogin, /css/**, /js/**); } }Controller 里用法就变成一行注解RequireRole({1, 2}) // 教师或管理员可访问 PostMapping(/correct) public ResultString correctHomework(RequestBody CorrectDTO dto) { // ... }这个方案的好处是权限规则集中管理新增接口时加个注解就行不会漏。答辩时老师问“怎么防止学生调用教师接口”你可以直接指出拦截器代码比口头解释有说服力。最后说一个验证系统是否真跑通的笨办法把项目打包成 jar在一台没装过开发环境的电脑上只装 JDK 和 MySQL按 README 步骤导入 SQL、改application.yml里的数据库密码、java -jar启动。如果能正常登录、上传、批改、下载说明依赖和配置都干净了。我自己的习惯是每次改完数据库脚本都会在本地先DROP DATABASE再重新导入一遍确认没有残留依赖。这个习惯帮我省掉了答辩前夜发现“换台电脑就跑不起来”的后悔药。希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网