简介:网上作业批改系统JAVA+Mysql.zip 是一套面向Java Web课程设计、毕业设计与入门实训的作业批改管理系统源码包,适合已掌握JSP、Servlet与基础数据库操作的学习者对照学习完整业务实现。系统围绕管理员端展开,提供班级、教师、学生、公告、留言、资料及管理员信息等模块,支持录入、查询、删除和打印等日常管理操作,并强调页面简洁、操作提示友好与基础安全性,便于理解权限划分和表单处理流程。压缩包共265个文件、约1.93MB,包含45个jsp页面、24个java源文件与24个class编译文件,另有gif、png、jpg等界面素材,css与js样式脚本,sql建库脚本,xml配置及doc、docx说明文档,基本覆盖从页面展示、控制逻辑到数据持久化的学习链路。已有1615人学习下载,可作为课程设计参考或功能扩展底稿,借助源码、数据库脚本和文档梳理模块职责、调试运行与二次开发思路。
1. 从一次收作业的混乱说起:网上作业批改系统要解决的四个真实问题
一个教学班四十多人,收一次作业能出现七八种命名格式,教师要在聊天记录、邮箱附件和表格之间来回翻;学生想知道“我到底交没交、老师改没改”,只能反复问。网上作业批改系统要解决的就是这条链路:作业发布、学生提交、教师批改、成绩统计与留痕。Java 负责把业务状态管住,MySQL 负责把每次提交、评分和修改记录存下来。这套资源适合正在做 Java 课程设计、MySQL 数据库课设或毕业设计的学生,也适合需要一套可演示、可扩展的作业管理原型的开发者。它不追求大而全,重点是把“交、改、查、导”四个动作跑通,并且让权限和状态经得起答辩追问。
2. 总体架构与技术选型:Java 分层与 MySQL 表结构如何对齐
2.1 分层边界:控制层、服务层、数据访问层各管什么
这套系统最容易写乱的地方,不是页面多,而是业务规则到处散。今天在 Controller 里判断截止时间,明天在 Mapper 里补一个if,后天导出成绩又写一遍权限判断,最后谁也说不清“一个作业到底能不能交”由谁负责。通常的做法是按职责切三层:控制层只接收参数、做格式校验、包装统一响应;服务层承担事务、状态流转、权限判断和文件校验;数据访问层只负责 SQL 和结果映射。分层不是形式主义,它决定了你改一个规则时要动几处。
以常见的 Java Web 骨架为例,包结构可以这样落:
src/main/java/com/example/homework ├── controller # 接收请求:登录、作业、提交、批改、导出 ├── service # 业务规则:截止时间、重复提交、权限、状态流转 ├── mapper # 数据访问:一张表或一组查询对应一个 Mapper ├── domain # 实体、枚举、请求/响应对象 └── common # 统一异常、文件工具、登录拦截、响应包装这份目录本身不复杂,关键是别把service写成controller的附属品。比如“学生提交作业”这个动作,Controller 只拿assignmentId、studentId、文件,Service 里按顺序做四件事:查作业是否存在且未截止、查学生是否属于该课程、校验文件类型和大小、写入提交记录和附件记录。Controller 不碰 SQL,Mapper 不判断“晚交”,页面只负责展示结果。这样后面加“迟交扣分”或“允许补交”时,只改 Service 一处。
统一响应也要在分层里定好。成功返回code=0和业务数据,失败返回可读的错误码,比如ASSIGNMENT_CLOSED、FILE_TYPE_NOT_ALLOWED、NOT_COURSE_MEMBER。不要直接把 Java 异常堆栈抛给页面,否则学生看到的是一串英文,教师也不知道该找谁。登录拦截放在common里,只做“有没有登录、角色是什么”,具体“这个教师能不能批改这门课”仍要回到 Service 判断。分层边界清楚,后面数据库表和接口才好对齐。
2.2 技术选型:为什么是 Java + MySQL,而不是一上来堆中间件
这套资源的核心技术栈是 Java 和 MySQL,这个组合放在作业批改场景里是合适的。Java 的强类型、事务控制和成熟的分层习惯,适合把“提交—批改—成绩”这种状态链路写清楚;MySQL 的关系模型适合表达用户、课程、作业、提交、成绩之间的强关联。一个学生可以选多门课,一门课有多次作业,一次作业对应一次或多次提交,一次提交对应一条批改记录,这些关系用外键思路或服务层校验都能落地。课程设计最怕“技术堆得很高,主流程跑不通”,Java + MySQL 至少能保证环境好找、调试直观、答辩时讲得清。
如果资源骨架是 Spring Boot,就按 Controller-Service-Mapper 分;如果是 Servlet/JSP,就按 Servlet-Service-DAO 分。名字不同,边界一样。不要因为看到别人用缓存就先上 Redis,作业批改系统的读多写少是相对的,真正麻烦的是文件、状态和权限,不是查询压力。消息队列也一样,除非要做“批改完成后异步通知”,否则引入后只会多一个需要解释的组件。通常的做法是先把单机事务跑通,再根据实际瓶颈加东西。技术选型要能回答一个问题:这个组件解决了哪个具体问题?答不上来就先不加。
MySQL 这边,建议统一使用 InnoDB 和utf8mb4,时间字段统一用DATETIME,状态字段用短字符串或TINYINT,但要在代码里定义枚举,别在 SQL 里散落魔法值。主键用自增BIGINT,业务唯一性靠唯一索引兜底。比如同一个学生对同一次作业的同一版本只能有一条提交记录,这种规则不要只写在 Service 的if里,数据库也要有唯一索引。课程设计里经常忽略这一点,最后测试时手动点快一点就出现两条记录,教师页面显示旧版本,学生坚称自己交了新版本,排查起来就是一笔糊涂账。
2.3 部署形态:单机部署与前后端分离的取舍
课程设计和毕设通常不需要复杂部署。单机部署最稳:Java 应用打成一个可执行包,MySQL 装在本机或实验环境,上传文件放在应用外的固定目录。这样做的好处是排错路径短,日志、数据库、文件目录都在一台机器上,出问题能马上定位。上传目录不要放在项目源码里,也不要用相对路径随启动目录漂移。配置里写绝对路径,例如:
# 上传根目录:按实际环境改成绝对路径,不要放在源码目录 homework.upload.dir=/data/homework/upload # 单文件大小上限,按教学场景调整 homework.upload.max-size=10MB # 允许的扩展名,逗号分隔 homework.upload.allowed-ext=pdf,doc,docx,zip这三个参数看着简单,实际决定了后面很多行为。homework.upload.dir是文件落盘的位置,数据库里只存相对路径或文件 ID,不存完整磁盘路径;max-size要和前端提示、后端校验、服务器限制三处一致,否则会出现“页面说能传、后端拒绝”的翻车;allowed-ext是白名单,不要用黑名单,作业场景常见的exe、bat不要放进来。文件重命名用“日期目录 + UUID + 原扩展名”,原始文件名单独存数据库,下载时再还原。这样既能避免同名覆盖,也能防止路径穿越。
如果要做前后端分离,可以保留同一套 Service,只把 Controller 的返回改成 JSON,前端用静态页面或单独工程。注意分离后登录态、跨域、文件下载鉴权要单独处理,课程设计如果时间紧,不建议一开始就拆。单机形态下,备份也简单:数据库导出 SQL,上传目录打包,两份放在不同位置。答辩前至少做一次“删掉上传目录里的某个文件,看系统是否给出可读错误”的检查,这比演示十遍正常流程更能说明你考虑过异常。
3. 数据库设计:从“交作业”到“出成绩”的六张核心表
3.1 用户与课程关系表:角色、班级、选课关系怎么落
用户表不要叫user,避免和数据库关键字打架,用sys_user更稳。字段至少要有用户名、密码哈希、真实姓名、角色、班级、状态和创建时间。密码只存哈希,不存明文;角色用STUDENT、TEACHER、ADMIN三个值,不要用 1、2、3 让后面的人猜。班级可以先放一个class_name字段,课程设计够用;如果要做完整组织架构,再拆班级表。课程表要有课程名、任课教师、学期和状态。选课关系表把学生和课程连起来,唯一索引保证同一个学生不会重复选同一门课。
CREATE TABLE sys_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(64) NOT NULL, password_hash VARCHAR(255) NOT NULL, real_name VARCHAR(64) NOT NULL, role VARCHAR(16) NOT NULL COMMENT 'STUDENT/TEACHER/ADMIN', class_name VARCHAR(64) DEFAULT NULL, status TINYINT NOT NULL DEFAULT 1 COMMENT '1正常 0禁用', created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_username (username) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE course ( id BIGINT PRIMARY KEY AUTO_INCREMENT, course_name VARCHAR(128) NOT NULL, teacher_id BIGINT NOT NULL, term VARCHAR(32) NOT NULL, status TINYINT NOT NULL DEFAULT 1, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_teacher (teacher_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE course_student ( id BIGINT PRIMARY KEY AUTO_INCREMENT, course_id BIGINT NOT NULL, student_id BIGINT NOT NULL, status TINYINT NOT NULL DEFAULT 1, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_course_student (course_id, student_id), KEY idx_student (student_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;这三张表的关键不是字段多,而是关系清楚。course.teacher_id指向教师,course_student记录选课,学生提交作业前先查course_student是否存在且状态正常,教师批改前先查course.teacher_id是不是当前登录人。唯一索引uk_course_student是防止重复选课的兜底,不要只靠页面按钮置灰。idx_teacher和idx_student是为了按教师查课程、按学生查课程时不用全表扫。真实项目里还可以加外键,但课程设计如果要做删除课程,外键会让删除顺序变复杂,通常的做法是逻辑删除加服务层校验。
3.2 作业与提交表:版本、附件、截止时间怎么存
作业表要存课程 ID、标题、描述、截止时间、是否允许迟交、满分、状态和创建人。截止时间用DATETIME,不要用字符串,否则比较大小会变成玄学。提交表要存作业 ID、学生 ID、文本内容、提交时间、状态和版本号。附件单独一张file_asset表,存原始文件名、存储名、相对路径、大小、MIME 类型、上传人和创建时间。提交表只存file_id,不要把文件二进制塞进数据库。版本号从 1 开始,每次重新提交加 1,唯一索引uk_submission_version保证同一作业、同一学生、同一版本只有一条记录。
CREATE TABLE assignment ( id BIGINT PRIMARY KEY AUTO_INCREMENT, course_id BIGINT NOT NULL, title VARCHAR(200) NOT NULL, description TEXT, deadline DATETIME NOT NULL, allow_late TINYINT NOT NULL DEFAULT 0 COMMENT '0不允许 1允许', max_score DECIMAL(5,2) NOT NULL DEFAULT 100.00, status VARCHAR(16) NOT NULL DEFAULT 'OPEN' COMMENT 'OPEN/CLOSED', created_by BIGINT NOT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_course_deadline (course_id, deadline) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE file_asset ( id BIGINT PRIMARY KEY AUTO_INCREMENT, original_name VARCHAR(255) NOT NULL, stored_name VARCHAR(255) NOT NULL, relative_path VARCHAR(500) NOT NULL, file_size BIGINT NOT NULL, mime_type VARCHAR(128) DEFAULT NULL, uploader_id BIGINT NOT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_stored_name (stored_name) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE submission ( id BIGINT PRIMARY KEY AUTO_INCREMENT, assignment_id BIGINT NOT NULL, student_id BIGINT NOT NULL, content TEXT, file_id BIGINT DEFAULT NULL, submit_time DATETIME NOT NULL, submit_status VARCHAR(16) NOT NULL DEFAULT 'SUBMITTED' COMMENT 'SUBMITTED/LATE/GRADED/REJECTED', version INT NOT NULL DEFAULT 1, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_submission_version (assignment_id, student_id, version), KEY idx_assignment_status (assignment_id, submit_status), KEY idx_student (student_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;这里有两个设计点值得展开。第一,allow_late和deadline分开存,不要用“截止时间加一小时”这种隐式规则,迟交是否允许、迟交是否扣分都应该是显式配置。第二,submit_status不要只存“已交/未交”,要把“迟交”和“已批改”分开,否则统计未交名单时会把迟交算错。唯一索引放在(assignment_id, student_id, version)上,重新提交就插入新版本,旧版本保留,教师默认看最新版本,学生也能看到历史。这样做比“覆盖更新”多一张记录,但换来的是可追溯,成绩争议时不用靠猜。
3.3 批改与成绩表:评分、评语、申诉记录怎么留痕
批改表要存提交 ID、教师 ID、分数、评语、批改时间和状态。分数用DECIMAL(5,2),不要用FLOAT,避免 89.999 这种展示问题。评语用TEXT,不要限制太短,教师可能写一段反馈。状态可以从SUBMITTED走到GRADED,如果要支持学生查看后确认,再加RETURNED。成绩修改要有留痕,最简单的做法是加一张grade_log,记录原分数、新分数、修改人、修改时间和原因。课程设计不一定做申诉流程,但至少要能回答“教师改了分,历史在哪”。
CREATE TABLE grade ( id BIGINT PRIMARY KEY AUTO_INCREMENT, submission_id BIGINT NOT NULL, teacher_id BIGINT NOT NULL, score DECIMAL(5,2) NOT NULL, teacher_comment TEXT, graded_at DATETIME NOT NULL, status VARCHAR(16) NOT NULL DEFAULT 'GRADED' COMMENT 'GRADED/RETURNED', updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_submission (submission_id), KEY idx_teacher (teacher_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE grade_log ( id BIGINT PRIMARY KEY AUTO_INCREMENT, grade_id BIGINT NOT NULL, old_score DECIMAL(5,2) DEFAULT NULL, new_score DECIMAL(5,2) NOT NULL, operator_id BIGINT NOT NULL, reason VARCHAR(255) DEFAULT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_grade (grade_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;uk_submission保证一次提交只有一条当前成绩,修改走UPDATE,同时往grade_log插一条记录。教师批改时,Service 里先查提交是否存在、是否属于该教师课程、当前状态是否允许批改,再在事务里更新提交状态和写成绩。不要先更新成绩再判断权限,否则越权请求已经落库才报错。成绩表不存学生 ID 和作业 ID,通过submission_id关联,避免三处冗余不一致。如果导出成绩要频繁联表,可以在查询里 join,不要为了省一次 join 把学生 ID 冗余到成绩表,后面改起来更麻烦。
4. 核心模块实现:提交、批改、统计三条主链路
4.1 作业提交链路:文件校验、重复提交与截止时间判断
提交链路是整套系统最容易翻车的地方。学生点一次“提交”,可能因为网络慢连点三次;文件可能超过大小;扩展名可能被改过;截止时间可能刚好卡在 23:59。合格的做法是把这些规则收在 Service 里,按固定顺序执行:先查作业和选课关系,再判断截止时间与迟交策略,再校验文件,最后在一个事务里写文件记录和提交记录。文件先落临时目录,校验通过后再移动到正式目录,数据库记录写相对路径。这样即使写库失败,也不会留下一个数据库里查不到的文件。
@Service public class SubmissionService { private final AssignmentMapper assignmentMapper; private final CourseStudentMapper courseStudentMapper; private final SubmissionMapper submissionMapper; private final FileStorageService fileStorageService; // 提交作业:顺序不能乱,先规则后落库 @Transactional public Long submit(Long assignmentId, Long studentId, String content, MultipartFile file) { Assignment assignment = assignmentMapper.selectById(assignmentId); if (assignment == null || !"OPEN".equals(assignment.getStatus())) { throw new BizException("ASSIGNMENT_NOT_OPEN"); } // 1. 选课关系校验,防止非课程学生提交 if (!courseStudentMapper.exists(assignment.getCourseId(), studentId)) { throw new BizException("NOT_COURSE_MEMBER"); } // 2. 截止时间判断,以服务端时间为准 LocalDateTime now = LocalDateTime.now(); boolean late = now.isAfter(assignment.getDeadline()); if (late && assignment.getAllowLate() == 0) { throw new BizException("ASSIGNMENT_CLOSED"); } // 3. 文件校验与存储,返回文件 ID Long fileId = null; if (file != null && !file.isEmpty()) { fileId = fileStorageService.save(file, studentId); } // 4. 版本号由数据库唯一索引兜底,这里取当前最大版本加一 int nextVersion = submissionMapper.nextVersion(assignmentId, studentId); Submission submission = new Submission(); submission.setAssignmentId(assignmentId); submission.setStudentId(studentId); submission.setContent(content); submission.setFileId(fileId); submission.setSubmitTime(now); submission.setSubmitStatus(late ? "LATE" : "SUBMITTED"); submission.setVersion(nextVersion); submissionMapper.insert(submission); return submission.getId(); } }这段代码的重点不是注解,而是顺序和边界。assignmentId、studentId来自登录态和请求参数,studentId不能由前端随便传,应该从会话或令牌里取。allowLate为 0 时,超过deadline直接拒绝;为 1 时允许提交但标记LATE。fileStorageService.save内部做扩展名白名单、大小限制、UUID 重命名和目录创建,返回file_asset.id。nextVersion是查询当前最大版本加一,高并发下仍可能撞唯一索引,所以 Service 要捕获唯一键冲突并提示“请勿重复提交”,或者加一个短锁。课程设计里更稳的做法是前端提交按钮置灰加后端唯一索引,双保险。
文件校验参数要写清楚:允许的扩展名用白名单,大小上限和配置一致,存储名不能用原始文件名拼路径,原始文件名只用于展示和下载。下载时先查file_asset和提交权限,再读文件,不要直接把上传目录暴露成静态资源。否则学生改一个 URL 就能下载别人的作业,答辩时被问到权限,很难圆回来。
4.2 教师批改链路:评分表单、批量批改与状态流转
批改链路的第一个问题是权限,第二个问题是状态。教师打开待批列表,只能看到自己课程下的提交;提交批改时,服务层要重新查一遍course.teacher_id,不能信任页面传来的teacherId。状态流转要单向:SUBMITTED或LATE才能批改,已批改的提交再次修改成绩要走“修改成绩”接口并写grade_log,不要直接覆盖。批量批改可以先把选中的提交 ID 传过来,逐条校验,全部通过再提交事务;如果一条失败就整批回滚,避免一半改了一半没改。
-- 批改时带状态条件,防止重复批改或越权覆盖 UPDATE submission SET submit_status = 'GRADED' WHERE id = ? AND submit_status IN ('SUBMITTED', 'LATE'); -- 只有影响行数为 1 才继续写成绩,否则说明状态已变 INSERT INTO grade (submission_id, teacher_id, score, teacher_comment, graded_at, status) VALUES (?, ?, ?, ?, NOW(), 'GRADED') ON DUPLICATE KEY UPDATE score = VALUES(score), teacher_comment = VALUES(teacher_comment), graded_at = VALUES(graded_at);这两条 SQL 要放在同一个事务里。第一条的WHERE submit_status IN (...)是状态机兜底,如果另一个教师已经批过,影响行数为 0,Service 直接返回“该提交已批改”,不要继续插成绩。第二条利用uk_submission做 upsert,但要注意:如果是“修改成绩”,应该先查旧分数写grade_log,再更新,不能直接用ON DUPLICATE KEY UPDATE把旧值覆盖掉。teacher_id从登录态取,score要在 Service 里校验0 <= score <= assignment.max_score,teacher_comment做长度限制和 HTML 转义,避免评语里带脚本。
批量批改的实现思路是“先校验后执行”:把提交 ID 列表查出来,逐条比对课程教师和状态,任何一条不合法就返回具体是哪一条,不进入更新。全部合法后在一个事务里循环更新。数据量大时不要一条条发 SQL,可以按 ID 批量更新,但课程设计的数据量通常不大,清晰比极致性能重要。批改完成后,学生端要能看到分数和评语,教师端列表要从未批改变为已批改,这两个展示都从数据库状态读,不要在前端自己改状态。
4.3 统计与导出:班级均分、未交名单、成绩单生成
统计功能最容易被低估。教师不只想要一个均分,还想知道谁没交、谁迟交、最高分最低分、每道作业的分布。这些都可以用 SQL 聚合出来,不要在 Java 里把全量数据查出来再循环。班级均分按课程和作业分组,未交名单用“课程学生左连接提交记录”找空值,成绩单按学生列出每次作业的分数。导出 CSV 时注意中文编码和公式注入:以=、+、-、@开头的单元格要加单引号前缀,否则用表格软件打开可能被当成公式。
-- 某次作业的提交与成绩概览 SELECT COUNT(DISTINCT cs.student_id) AS student_count, COUNT(DISTINCT s.student_id) AS submitted_count, SUM(CASE WHEN s.submit_status = 'LATE' THEN 1 ELSE 0 END) AS late_count, AVG(g.score) AS avg_score, MAX(g.score) AS max_score, MIN(g.score) AS min_score FROM course_student cs LEFT JOIN submission s ON s.student_id = cs.student_id AND s.assignment_id = ? LEFT JOIN grade g ON g.submission_id = s.id WHERE cs.course_id = ? AND cs.status = 1; -- 未交名单:选课学生里没有当前作业有效提交的人 SELECT u.id, u.username, u.real_name FROM course_student cs JOIN sys_user u ON u.id = cs.student_id LEFT JOIN submission s ON s.student_id = cs.student_id AND s.assignment_id = ? WHERE cs.course_id = ? AND cs.status = 1 AND s.id IS NULL;第一条 SQL 的?分别是作业 ID 和课程 ID,COUNT(DISTINCT ...)防止一个学生多个版本被重复计数。第二条用LEFT JOIN ... IS NULL找未交,注意如果允许重新提交,一个学生可能有多条提交,这里只判断“有没有”,不判断版本。导出成绩单时,先按学生分组查最新版本,再关联成绩,避免把旧版本分数导出去。CSV 文件用 UTF-8 带 BOM,Excel 打开不乱码;文件名用“课程名_作业名_日期.csv”,不要用中文加特殊字符拼路径。导出接口要加教师权限校验,不能只靠前端隐藏按钮。
5. 避坑与排查:六个最容易翻车的点
5.1 提交与文件相关:三个高频故障
现象一:文件上传成功,数据库里却没有提交记录,学生说交了,教师看不到。原因通常是先写文件再写库,写库异常后文件已经落在磁盘上,或者事务回滚了但文件没删。解决方式是把文件先放临时目录,数据库记录写成功后再移动到正式目录;如果移动失败,记录里标记异常并给出补偿任务。更简单的做法是文件落盘后立刻写file_asset,提交记录和文件记录在同一事务里,文件删除用定时任务清理“无引用文件”。排查时先看上传目录有没有文件、file_asset有没有记录、submission有没有记录,三段对照就能定位。
现象二:截止时间明明过了,学生页面还能点提交,或者页面说没截止但接口拒绝。原因是前端用浏览器本地时间判断,后端用服务器时间判断,两边时区或时钟不一致。解决方式是所有截止判断只在后端做,前端只展示服务器返回的截止时间和状态;数据库连接和 JVM 时区统一,不要一个用 UTC 一个用本地时间。接口返回错误码ASSIGNMENT_CLOSED,页面展示“已截止”,不要只弹一个“系统错误”。排查时打印服务端当前时间、作业截止时间、两者差值,别只看页面倒计时。
现象三:学生连点提交,教师看到两条记录,成绩不知道挂哪条。原因是提交按钮没有防重,后端也没有唯一约束。解决方式是前端提交后置灰,后端按(assignment_id, student_id, version)加唯一索引,捕获唯一键冲突后返回“请勿重复提交”;重新提交走新版本,教师默认看最新版本。排查时按学生和作业查全部版本,看version、submit_time、submit_status,不要只看最新一条。这块的血泪经验是:防重不能只靠前端,前端能绕过,数据库约束才是最后一道门。
5.2 批改与权限相关:三个高频故障
现象四:教师 A 能批改教师 B 的课程作业,或者学生能通过改 URL 看别人的提交。原因是接口只校验了“已登录”,没校验“是不是这门课的教师/学生”。解决方式是在 Service 里做资源级权限:批改前查course.teacher_id是否等于当前用户,查看提交前查course_student是否存在,下载文件前查提交归属。不要信任前端传来的teacherId、studentId,一律从登录态取。排查时用两个不同角色的账号互相访问对方资源,看返回是 403 还是 200,别只测正常路径。
现象五:教师改了成绩,学生申诉时说不清原来多少分。原因是成绩表直接覆盖更新,没有留痕。解决方式是每次修改成绩前查旧值,写一条grade_log,记录旧分数、新分数、操作人、原因和时间;成绩表保留当前值。修改接口和首次批改接口分开,修改要有原因必填。排查时按grade_id查日志,按时间排序,能还原每次变更。课程设计不一定做申诉流程,但至少要让“改过分”这件事有记录,这是答辩时很加分的细节。
现象六:导出 CSV 打开是乱码,或者学生姓名以等号开头导致表格异常。原因是编码没带 BOM,或者单元格内容没转义。解决方式是导出用 UTF-8 带 BOM,所有文本字段做 CSV 转义,以=、+、-、@开头的值前面加单引号。文件名不要带路径分隔符。排查时用 Excel 和文本编辑器各打开一次,看表头和中文是否正常。导出接口还要限制条数,避免一次导出几万条把内存打满。通常的做法是分页查询、流式写出,不要先查全量再拼字符串。
排查顺序可以固定成三步:先看日志里的错误码和请求参数,再查数据库对应记录的状态和版本,最后看文件目录和配置。不要一上来就改代码,先把“哪一步断的”定位清楚。这套系统没有黑匣子,日志、数据库、文件三处对得上,问题基本藏不住。
6. 验收与答辩:用一套可复现的检查表把方案讲清楚
验收不要从“页面好不好看”开始,而要从角色和状态走。准备三个账号:一个教师、一个学生、一个非本课程学生。教师建课、发布作业;学生提交、重新提交、下载自己的附件;教师批改、修改成绩;学生查看成绩;最后导出成绩单。每一步都记录预期结果和实际结果。下面这张表可以直接当验收清单用:
| 验收项 | 操作路径 | 预期结果 | 失败先看哪里 |
|---|---|---|---|
| 登录与角色 | 三个账号分别登录 | 菜单和接口权限一致 | 会话、角色字段 |
| 选课校验 | 非课程学生提交作业 | 返回非课程成员错误 | course_student |
| 截止控制 | 截止后提交 | 按allow_late拒绝或标记迟交 | 服务端时间、作业状态 |
| 文件校验 | 上传不允许的扩展名 | 拒绝并给出可读提示 | 白名单配置、文件工具 |
| 重复提交 | 连续点击提交 | 只产生一条当前版本,或明确新版本 | 唯一索引、按钮防重 |
| 批改权限 | 教师批改非本人课程 | 返回无权限 | course.teacher_id |
| 成绩留痕 | 修改已批改成绩 | 写日志,可查旧值 | grade_log |
| 导出 | 导出班级成绩 | 中文正常、无公式注入 | 编码、转义 |
答辩讲法我一般建议按“约束—取舍—验证”三段走。先讲约束:这是一个教学场景的网上作业批改系统,核心角色只有学生、教师、管理员,核心链路是发布、提交、批改、统计,技术栈是 Java + MySQL,不引入额外中间件。再讲取舍:为什么截止判断放服务端,为什么提交要留版本,为什么成绩修改要留日志,为什么文件不进数据库。最后讲验证:拿验收表走一遍,重点演示异常路径,比如非课程学生提交、截止后提交、越权批改。老师通常对“你怎么防止重复提交”“怎么保证教师只能改自己的课”这类问题更感兴趣,提前把唯一索引和权限校验点准备好,比背页面流程有用。
从那以后我每次带人做这类系统,都强制先走一遍这张验收清单,再谈优化和扩展。先把主链路跑通,再补通知、申诉、批量导入,心里才有底。希望帮到你。
本文还有配套的精品资源,点击获取