基于SSM的在线作业提交批改系统:从权限设计到文件上传全解析
简介一套面向Java毕业设计与课程设计的在线作业提交批改系统完整项目基于SSMSpringSpringMVCMyBatis框架构建覆盖管理员、教师、学生三大角色。管理员主要负责教师和学生账号管理教师可创建课程针对每门课程布置作业并设定提交期限下载学生提交的Word文档线下批改在系统内录入评价与分数并支持将全部作业数据导出为Excel学生登录后可浏览、下载作业内容在期限内可重复提交且后一次覆盖前一次。资源共454个文件压缩包仅16.81MB包含Java源码、class编译文件、XML配置、SQL数据库脚本、HTML/CSS/JS前端页面以及ECharts相关文件目录结构完整便于直接运行学习或二次开发。整套系统可作为毕业设计完整项目参考帮助读者理解用户分权登录、作业上传覆盖、文件下载与Excel导出等典型业务实现也可辅助论文撰写与答辩准备。已有210人学习/下载对正在做SSM相关课题的同学具有较高参考价值。1. 基于JAVA的在线作业提交批改系统这套SSM代码能直接当毕设打底如果你正在为JAVA毕业设计选题犯愁或者被课程设计逼到想直接把“在线作业提交批改系统”六个字写进任务书那这套基于SSM架构的源码就是你最该先拆开看的东西。它不是一个空壳演示而是把管理员、教师、学生三个角色真正跑通了的完整项目教师建课程、布置作业、定截止时间学生在期限内反复提交Word文档后提交的覆盖前面的老师下载批改后回填分数和评语最后整个作业数据能导出Excel。听起来功能不多但恰好覆盖了毕设答辩最看重的几个点权限区分、文件上传下载、时间限制、数据导出。对新手来说这套代码的模块边界很清晰Controller层的命名也规矩拿来改改就能当自己的东西交。2. 角色权限与功能边界先把三个身份的分工理清代码才不会乱2.1 三种登录入口与权限校验的常规做法这个系统最值得先看的部分就是登录和权限设计。入口分成教师、学生、管理员三个表面上只是登录页多了一个下拉框实际后端对应的是三套不同的校验逻辑。常见的做法是登录成功后把用户ID和角色写进session再通过一个拦截器Interceptor拦截所有需要鉴权的请求根据session里的角色判断能不能访问某个URL。我在拆这套代码时第一件事就是找它的拦截器配置。SSM项目里一般会在spring-mvc.xml里注册拦截器类似下面这样mvc:interceptors mvc:interceptor mvc:mapping path/**/ mvc:exclude-mapping path/login/ mvc:exclude-mapping path/register/ bean classcom.system.interceptor.LoginInterceptor/ /mvc:interceptor /mvc:interceptors这段配置的意思是除了登录和注册其它所有请求都要先过LoginInterceptor。拦截器里的逻辑一般是先从session取当前登录用户取不到就重定向到登录页取到了再看这个请求是否在该角色的允许列表里。比如学生不应该访问/admin/**教师也不应该去操作学生管理接口。这个设计的妙处在于Controller层可以完全集中精力写业务不用每个方法里都写一遍“if(session.getAttribute(user)null)”。如果你打算把这个项目改成别的系统我建议连最后的角色判断也放进拦截器用路径前缀区分权限比如/admin/**、/teacher/**、/student/**这样新增接口时只需要按前缀归类不用改拦截器代码。2.2 模块划分管理员管人教师管课学生管提交从类名就能看出模块归属AdminInfoController处理管理员对教师和学生的增删改查TeacherInfoController和StudentInfoController分别是教师端和学生端的个人信息与业务操作KechengInfoController管课程SubmitInfoController管作业提交EchartsController输出统计数据。这种按角色拆Controller的习惯比把所有接口堆在一个类里好维护得多。我把功能整理成下面这张表你照着这张表去源码里找对应方法效率会高很多模块对应Controller核心功能关键方法名推测/常见管理员AdminInfoController教师与学生账号管理addTeacher, deleteStudent, listUsers教师TeacherInfoController课程维护、作业下发、批改评分addKecheng, addZuoye, savePingfen学生StudentInfoController注册、浏览作业、提交Wordregister, submitHomework, downloadZuoye作业提交SubmitInfoController提交覆盖、截止时间校验、文件存储doSubmit, checkDeadline课程KechengInfoController课程定义与查询listKecheng, getDetail统计EchartsController提交率、分数分布等图表数据getSubmitStats, getScoreDist这里要注意一个细节注册功能只开放给教师和学生管理员是系统内置的唯一账号。所以注册接口里必须加一个用户类型字段后端收到注册请求后根据类型把数据插到对应的教师表或学生表里。有些毕设把三张表合成一张用户表用role字段区分那样更简单但如果你按这个项目的原始设计走就得保持三张表分离注意注册时密码要加密存储别用明文。2.3 作业的生命周期从布置到导出Excel的完整链路一个作业从创建到归档在这套系统里是这样流转的教师先创建课程然后在课程下布置作业设定截止时间学生登录后能看到这门课的作业列表点击下载作业要求文件在截止时间之前学生可以上传自己的Word文档重复上传时后来的文件覆盖旧的截止后系统拒绝上传教师看到学生提交记录后逐个下载Word线下批改再回到系统录入分数和评语期末时教师可以把某门课的所有作业提交记录导出成Excel。这条链路里的核心代码集中在SubmitInfoController。作业提交时的覆盖逻辑很典型先根据作业ID和学生ID查数据库里有没有已提交记录有的话就把文件路径更新成新上传的同时删除服务器上的旧文件没有就新增一条记录。这里常见的坑是先删旧文件再更新数据库如果删除成功但更新失败数据就丢了。稳妥的做法是先更新数据库提交成功后再删旧文件或者把删除旧文件放到最终确认之后。3. 数据库设计与提交覆盖逻辑把这四张表看懂项目就懂了一半3.1 核心数据表的结构与字段关系在线作业系统的数据库设计不需要很复杂但表之间的关系必须对。这套项目里最少要四张核心表用户相关表管理员、教师、学生、课程表、作业表、提交记录表。如果是三张用户表分开存那教师表和学生表至少要有id、username、password、real_name这几个字段课程表要有id、course_name、teacher_id作业表要有id、course_id、title、deadline、file_path提交表要有id、homework_id、student_id、submit_time、file_path、score、comment。课程和教师是多对一作业和课程是多对一提交记录和作业、学生都是多对一。我用SQL给你建一个提交记录表的例子这种结构在毕设里完全够用CREATE TABLE submit_info ( id int(11) NOT NULL AUTO_INCREMENT, homework_id int(11) NOT NULL COMMENT 作业ID, student_id int(11) NOT NULL COMMENT 学生ID, submit_time datetime DEFAULT NULL COMMENT 提交时间, file_path varchar(255) DEFAULT NULL COMMENT Word文件存储路径, score int(11) DEFAULT NULL COMMENT 批改分数, comment varchar(500) DEFAULT NULL COMMENT 教师评语, PRIMARY KEY (id), KEY idx_homework (homework_id), KEY idx_student (student_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;注意我在homework_id和student_id上建了普通索引因为后面“按作业查所有提交”“按学生查历史提交”都是高频查询。联合唯一索引建议加在(homework_id, student_id)上这样数据库层面就保证了同一个学生同一份作业只能有一条提交记录不会被并发请求插出两条来。如果你发现项目原表没加唯一约束我建议你二开时补上。3.2 提交覆盖的完整代码实现先查后写注意事务提交作业的核心逻辑我一般这么写你对照源码能看明白它的思路Transactional public String submitHomework(Integer homeworkId, Integer studentId, MultipartFile file) { // 1. 查询作业信息获取截止时间 Homework homework homeworkMapper.selectById(homeworkId); if (homework null) { return 作业不存在; } // 2. 校验是否已过期当前时间晚于截止时间直接拒绝 if (new Date().after(homework.getDeadline())) { return 已超过提交截止时间无法提交; } // 3. 查询该学生是否已经提交过这份作业 SubmitInfo existing submitMapper.selectByHomeworkAndStudent(homeworkId, studentId); // 4. 保存新文件到服务器得到存储路径 String newPath fileStorage.save(file); if (existing null) { // 5a. 首次提交插入新记录 SubmitInfo info new SubmitInfo(); info.setHomeworkId(homeworkId); info.setStudentId(studentId); info.setSubmitTime(new Date()); info.setFilePath(newPath); submitMapper.insert(info); } else { // 5b. 重复提交先更新数据库再删旧文件 String oldPath existing.getFilePath(); existing.setFilePath(newPath); existing.setSubmitTime(new Date()); existing.setScore(null); // 重新提交后清空原分数避免学生拿旧分数找茬 existing.setComment(null); submitMapper.updateById(existing); fileStorage.delete(oldPath); } return 提交成功; }这段代码把三个关键点都照顾到了截止时间判断放在文件保存之前避免白存一个文件再告诉你超时重复提交时先更新数据库再删旧文件防止出现“数据库指向新文件但旧文件还在”的脏状态重新提交后把分数和评语清空这个细节很多学生做的项目想不到但答辩时老师会问。注意Transactional不是随便加的。这里只有两步写操作如果删旧文件也放在事务里一旦删除失败事务会回滚导致新文件路径更新也回滚学生重新上传反而失败。所以我的习惯是数据库更新和文件删除不要放在同一个事务里文件系统不归数据库管。3.3 超期不可提交用数据库时间还是服务器时间这一节是很多人翻车的地方。判断“超期”到底以谁的时间为准我见过有同学直接拿客户端传过来的时间比较学生改一下系统时间就能绕过限制。正确做法是用服务器时间也就是Java的new Date()配合MySQL的NOW()来兜底。更稳的方案是在数据库层面做约束但SSM项目里一般不需要那么严格只要在Java代码里用服务器时间判断就够了。另外一个细节是截止时间的格式。前端表单提交的deadline是字符串比如2025-06-30 23:59后端要用SimpleDateFormat或LocalDateTime解析千万别直接存成字符串去比较2025-06-30 23:59.compareTo(2025-07-01 00:00)的结果看起来对但遇到2025-06-30 9:00这种不带前导零的写法就废了。统一用DateTimeFormatter解析成LocalDateTime再写库比较时也统一成时间类型。4. 本地部署与运行配置从导入项目到看见登录页的实操笔记4.1 环境准备与项目导入拿到这套源码后先别急着点运行。我推荐的环境组合是JDK 1.8 Maven 3.6 Tomcat 8.5 MySQL 5.7。如果代码里用了较新的语法也可以换JDK 11但SSM项目用1.8最稳很多老项目在JDK 17上跑起来会报模块访问错误折腾半天不值得。导入步骤很简单在IntelliJ IDEA里选择File - New - Project from Existing Sources选中项目的pom.xml等Maven把依赖下载完。这里有个坑如果下载依赖时卡住优先检查Maven镜像在settings.xml里配置成阿里云镜像再重启IDE。另外注意Maven项目的jdk.version属性在pom.xml里看到source1.8/source和target1.8/target就对。4.2 数据库初始化与JDBC配置项目里一般会提供一个sql目录里面是建库建表脚本。你需要先手动创建一个数据库再把脚本导入mysql -uroot -p123456 -e CREATE DATABASE IF NOT EXISTS homework_system DEFAULT CHARACTER SET utf8mb4; mysql -uroot -p123456 homework_system homework_system.sql导入后打开jdbc.properties配置文件把数据库地址、用户名、密码改成你自己的。我习惯把配置写在下面这样jdbc.drivercom.mysql.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/homework_system?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai jdbc.usernameroot jdbc.password123456这里的serverTimezoneAsia/Shanghai必须加。MySQL 8.x默认时区不是中国时区不加的话查日期时间字段会差8小时而且SSM项目里经常会报The server time zone value Öйú±ê׼ʱ¼ä这种乱码错误。另外characterEncodingutf8和useUnicodetrue能避免中文乱码这个比在代码里反复转码省心得多。4.3 Tomcat部署与文件上传临时目录如果你是用IDEA跑项目点击Add Configuration添加Tomcat Server LocalDeployment里选war explodedApplication context建议写成/或者/homework。这里注意一个细节文件上传保存路径是写死的绝对路径比如/data/uploads或者D:/uploads在Windows上要提前创建这个目录否则文件上传时会报FileNotFoundException。SpringMVC的文件上传解析器也要检查在spring-mvc.xml里bean idmultipartResolver classorg.springframework.web.multipart.commons.CommonsMultipartResolver property namedefaultEncoding valueUTF-8/ property namemaxUploadSize value10485760/ /beanmaxUploadSize是10MB如果学生提交的Word超过这个大小会被直接弹回。毕设演示一般Word文档都不大但如果你要改造成支持视频作业必须调大这个值同时还要在Tomcat的server.xmlConnector里配置maxPostSize默认只有2MB这个不调传大文件会报Number of files exceeds the allowed或者直接请求失败。部署成功后访问首页能看见三种登录入口就说明基础环境通了。接下来用内置管理员账号登录后台创建一个教师账号再注册一个学生账号走一遍“建课→布置作业→学生提交→批改→导出”的完整流程整个项目就算跑通了。5. 避坑与排查文件上传、路径、日期比较这几个老大难5.1 上传的Word打不开文件字节是0现象学生明明上传成功提交记录里也有路径但教师下载后文件损坏或者打开是乱码。查服务器存储目录发现文件大小是0KB。原因SpringMVC的CommonsMultipartResolver没有生效或者请求的enctype不是multipart/form-data。最常见的是表单里忘记加enctypemultipart/form-data导致文件字段变成普通字符串后端拿到的是一个空MultipartFile。解决检查前端form标签确保三元素齐全methodpost、enctypemultipart/form-data、input的name和后端RequestParam(file)一致。另外确认spring-mvc.xml里multipartResolver的bean id必须是multipartResolverSpringMVC只认这个id。5.2 文件能传但存不到期望的目录现象上传报错日志里出现java.io.FileNotFoundException或No such file or directory但自己明明创建了目录。原因要么是相对路径被解析成了Tomcat临时目录要么是部署时路径里带的空格或中文无法识别。SSM项目里如果用File.separator和System.getProperty(user.dir)拼路径在IDEA里跑和打war包跑user.dir完全不同。解决我一般会在配置类里定义常量比如UPLOAD_DIR D:/uploads/homework启动时用new File(UPLOAD_DIR).mkdirs()主动创建。千万别在Controller里写相对路径。如果你用Linux服务器建议统一放在/home/app/uploads下并给Tomcat用户授权。5.3 截止时间判断失效过了点还能提交现象教师设置的截止时间是晚上23:59学生凌晨1点还能提交成功。原因时间比较时用了前端传的字符串或者数据库里的deadline字段是varchar类型。更隐蔽的问题是MySQL连接的serverTimezone没设导致时间字段读出来是UTC时间比北京时间慢8小时23:59被读成15:59自然判断不了。解决把deadline字段改成datetime类型JDBC连接串里加上serverTimezoneAsia/Shanghai。日期比较统一用new Date().after(deadline)不要用String.compareTo。如果你已经上线了老数据记得用UPDATE homework SET deadline DATE_ADD(deadline, INTERVAL 8 HOUR)修一次历史数据。5.4 重复提交覆盖了旧文件但回收站里没有后悔药现象学生不小心传错文档再次上传覆盖后旧文件再也找不回来。学生哭着找老师说传错了老师也没法恢复。原因覆盖逻辑直接删除了旧文件没有留备份。对于毕设来说这种需求可能不是必须的但答辩时被问到“如果学生传错了怎么办”会很尴尬。解决我不建议真的做文件版本管理工作量大。一个取巧的办法是覆盖时把旧文件改名成filename_时间戳.docx保留一个或者干脆不删旧文件只在数据库里把路径指向新文件。但这样会占磁盘空间。最平衡的方案是把旧文件移到backup目录语义上叫“软删除”数据库里加一个previous_file_path字段存上上次的路径。这个改动也就半小时但能让你在答辩时多说一个亮点。5.5 Excel导出乱码中文列名全变问号现象导出的Excel打开后表头中文全部变成???或者用Excel打开正常但用WPS打开乱码。原因生成Excel时没有设置单元格的编码或者导出的响应头里没有正确的ContentType和字符集。POI操作空单元格时会写默认值遇到中文就会乱码。解决写Excel时统一设置单元格样式字体为宋体输出响应用application/vnd.ms-excel;charsetUTF-8同时设置Content-Disposition时对文件名做URL编码response.setContentType(application/vnd.ms-excel;charsetUTF-8); String fileName URLEncoder.encode(作业提交统计.xlsx, UTF-8); response.setHeader(Content-Disposition, attachment;filename fileName);另外如果你用的是new HSSFWorkbook()导出的.xls文件最大只能65536行学生数量大的时候会报Row number exceeds异常。建议直接用XSSFWorkbook输出.xlsx格式支持百万行也不需要额外引入POI以外的包。5.6 学生注册时提示用户名已存在但不一定是真的存在现象注册时报唯一键冲突但去数据库查根本没有这个用户名。原因多半是数据库表中该字段虽然设置了唯一索引但存在大小写敏感问题。MySQL的默认排序规则是utf8_general_ci不区分大小写ZhangSan和zhangsan会被当成同一个。如果你在前端做了校验而后端没做就会出现这种奇怪现象。解决注册逻辑里后端的判断最好用WHERE username ?同时把表字段的排序规则改成utf8_bin或者utf8mb4_bin区别是_bin按二进制比较大小写敏感。不过这个改动会影响全表建议只改用户名字段。另外注册接口一定要做后端唯一校验不能只靠前端提示。6. 二次开发把Echarts统计和按分数段导出接到你自己的项目里这套项目里的EchartsController是个加分项很多毕设题目里带“数据可视化”但实际没有真正实现。如果你拿到手的版本里只是简单返回几个数字我建议你把它扩展成一个真正可用的统计接口。最常见的需求是管理员想看到每个教师名下有多少门课程、每门课的作业提交率、以及学生成绩的分数段分布。提交率的计算其实不复杂用作业ID查学生总数再查已提交的学生数两个数一除就是百分比。分数段分布可以用SQL的CASE WHEN一步算出来SELECT SUM(CASE WHEN score 90 THEN 1 ELSE 0 END) AS s90, SUM(CASE WHEN score 80 AND score 90 THEN 1 ELSE 0 END) AS s80, SUM(CASE WHEN score 70 AND score 80 THEN 1 ELSE 0 END) AS s70, SUM(CASE WHEN score 70 THEN 1 ELSE 0 END) AS s60 FROM submit_info WHERE homework_id #{homeworkId} AND score IS NOT NULL;这个查询返回一行数据四个字段代表各分数段人数。接口层把它塞到Model里前端用Echarts柱状图渲染即可。要注意的是score IS NOT NULL这个条件不能省不然没批改的学生会被算进0分段。关于导出Excel我再补充一个实操习惯导出的字段不要硬编码在Controller里而是建一个ExportVO类把要导出的列名用List维护这样以后加列只改一个地方。我二开时会加一个“按分数段导出”的功能就是先按分数范围查出学生ID集合再把这些学生的提交记录导出来用于教师做分层教学。逻辑上是在SubmitInfoMapper里加一个带动态条件的SQL用where标签实现可选条件这样分数段、课程、时间范围都能组合查询。整个系统跑通后我对这个项目的评价是它最大的价值不在代码量而在角色的边界划分得很干净。哪怕你最终不选这个题目把这套角色权限拆法和文件上传覆盖逻辑搬到别的系统里也足够撑起一次答辩。我自己的习惯是拿到源码后第一时间把数据库脚本导出来用NoSQL的方式重新梳理一遍字段关系再回头看Controller就会非常快。之后每次改上传逻辑我都会强制走一遍“学生提交→覆盖提交→教师下载→导出Excel”的完整链路确认没有文件残留和脏数据才收工。这套流程也推荐给你毕设最怕的就是演示时在提交这一步翻车。希望这篇文章能把你在选型和部署上的折腾减到最少拿到源码后能少走弯路。本文还有配套的精品资源点击获取