资讯详情

JAVA+MySQL网上作业批改系统源码:提交批改链路与避坑

📅 2026/10/29 20:11:26 | 华诺云谱 👁 阅读
JAVA+MySQL网上作业批改系统源码:提交批改链路与避坑
简介「网上作业批改系统JAVAMysql.zip」是一套面向高校课程设计与毕业设计场景的 Java Web 完整源码适合正在学习 JSPServletMySQL 技术栈、需要可运行参考项目的学生与初级开发者。系统以操作简单、提示友好为设计出发点管理员端覆盖班级、教师、学生、公告、留言与资料六大信息模块均支持录入、查询、删除及打印等常规操作并配有系统管理模块实现管理员账号的增删查页面简洁、错误提示明确对恶意操作也有基本防护。压缩包共 265 个文件约 1.93MB以 gif 界面素材与 jsp 页面为主体配合 java 源文件、编译后的 class、js 脚本、css 样式以及 sql 建库脚本和说明文档层次清晰便于按模块阅读与二次改造。目前已有 1615 人学习下载。1. 网上作业批改系统这套 JAVAMySQL 源码值不值得你花一个晚上跑通先说一个反直觉的判断作业批改系统真正的难点不在“打分”而在“同一次作业、同一个学生、只能有一条有效提交记录”这件事上。我见过不少同学拿到的版本学生连点两次提交按钮数据库里就多出一条记录教师后台同一个作业下面出来两行名字最后算平均分直接算错。这套网上作业批改系统 JAVAMySQL 源码要解决的是完整闭环教师发布作业、学生在线提交并上传附件、教师逐条批阅打分写评语、成绩落库可导出。它适合三类人需要完整角色权限和文件上传链路的毕业设计选题、要给内部培训搭一个轻量提交批改流程的小团队、以及想拿真实 Java Web 工程练手读写分离以外那些基本功的人。规模不大但表结构和权限边界清楚改起来有支点。2. 跑起来之前JDK、MySQL 版本对齐与三处必改配置这套代码在我手上第一次启动花了四十分钟其中三十分钟耗在版本上。老 Java Web 工程的坑几乎全部集中在“包名换了一轮”这件事上所以动手之前先把版本这件事定死能省掉后面一大半排查时间。2.1 为什么 JDK 8 Tomcat 8.5 MySQL 5.7/8.0 是最省事的组合判断依据很简单翻一下源码里的 import 语句。如果看到的是javax.servlet.http.HttpServletRequest、javax.servlet.annotation.MultipartConfig那这个工程是 Jakarta EE 改名之前的写法必须跑在 Tomcat 9 及以下的容器里。Tomcat 10 起把javax.*整体换成了jakarta.*你把这套代码丢进去第一屏报错就是ClassNotFoundException而且是几百行的连锁报错看都看不完。JDK 同理。JDK 8 编译出来的老工程直接上 JDK 17 跑常见的是 JSP 编译期的Unsupported class file major version或者用了反射内省的地方抛InaccessibleObjectException。这不是代码写错了是运行时把私有字段的访问口子收紧了。我一般会先确认本机有没有 JDK 8没有就用版本管理工具切一个过去。MySQL 这边要注意驱动和数据库的对应关系。mysql-connector-java5.1.x 配 MySQL 5.7 基本无感如果数据库是 8.0驱动建议升到 8.0.x并且连接串上必须带时区参数否则启动时直接抛The server time zone value ... is unrecognized。这一步很多人卡住其实加个参数就过去了。先确认工程是 Maven 还是普通 Web 工程# 有 pom.xml 就是 Maven 工程依赖版本在 pom 里改 ls -l pom.xml 2/dev/null grep -n mysql-connector\|servlet-api\|jstl pom.xml # 没有 pom.xml 就是普通 Web 工程依赖 jar 在 WEB-INF/lib 下 ls -l WebContent/WEB-INF/lib 2/dev/null || ls -l src/main/webapp/WEB-INF/lib 2/dev/null这两条命令的结果决定你后面改哪里。Maven 工程改 pom 后重新拉依赖普通 Web 工程必须手工把 jar 丢进WEB-INF/lib而且放进去了还得在 IDE 的构建路径里确认一次只拷贝不注册是典型的“代码没错但编译不过”。如果是 Maven 工程依赖段大致长这样!-- pom.xml 关键依赖版本按你本地实际库版本对齐不要照抄 -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.28/version !-- 数据库是 5.7 就降到 5.1.49 -- /dependency dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version4.0.1/version scopeprovided/scope !-- 容器自带打 war 时必须排除否则和 Tomcat 冲突 -- /dependency dependency groupIdjavax.servlet/groupId artifactIdjstl/artifactId version1.2/version /dependencyprovided这个 scope 值得单独说一句。Servlet API 由 Tomcat 提供如果你用默认的 compile 打进去war 包里就会同时存在两份HttpServlet启动时报ClassCastException或者莫名其妙的 404现象非常迷惑。JSTL 反过来必须是 compile因为它不在容器里JSP 页面上一堆c:forEach不生效八成就是这里漏了。2.2 建库与导入字符集先定再灌数据顺序很关键。先建库并显式指定utf8mb4再导入建表脚本最后灌初始数据。反过来做的话脚本里的DEFAULT CHARSET会以库的默认字符集为准某些安装包默认是latin1你后面存中文作业标题就是一堆问号而且已经写进去的数据改字符集也救不回来。-- 建库时就把字符集和排序规则写死别依赖服务器默认值 CREATE DATABASE homework_db DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_general_ci; USE homework_db; -- 导入前先确认当前库的字符集输出必须是 utf8mb4 SELECT character_set_database, collation_database;确认无误后再执行工程自带的*.sql文件# 命令行导入注意 --default-character-set 要和库保持一致 mysql -uroot -p --default-character-setutf8mb4 homework_db homework_db.sql导入完成后别急着启动先验证一遍表和初始账号-- 看表是否齐全重点确认有没有用户表、作业表、提交表 SHOW TABLES; -- 看初始管理员账号是否存在密码字段名各版本不一样先看结构 DESC t_user; -- 抽查一条确认中文没变成问号 SELECT id, username, role FROM t_user LIMIT 5;这里有个细节如果初始账号的密码是明文存的第一次登录成功后请立刻改成密文方式别把它带到任何对外环境。老工程里明文密码很常见能跑不代表能上线。2.3 三处必改配置连接串、上传目录、上下文路径工程能启动但页面报 500八成是这三处没改。第一处是数据库连接配置通常在src下的jdbc.properties、db.properties或application.properties里# 连接串四要素缺一不可 jdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://127.0.0.1:3306/homework_db?useUnicodetruecharacterEncodingutf8mb4serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue jdbc.usernameroot jdbc.password改成你自己的 # 上传根目录必须是绝对路径相对路径在容器里会落到 Tomcat 的 bin 目录下 upload.basePath/data/homework/upload/参数逐个说清楚characterEncodingutf8mb4保证连接层不丢四字节字符serverTimezone不写会在 MySQL 8 上直接抛异常useSSLfalse是本地开发的省事做法正式环境要开并配证书allowPublicKeyRetrievaltrue只在 MySQL 8 默认认证插件下才需要。驱动类名 5.1.x 是com.mysql.jdbc.Driver8.0.x 是com.mysql.cj.jdbc.Driver写错了报ClassNotFoundException。上传目录用绝对路径这条是血泪经验。用./upload这种相对路径时File对象的当前工作目录取决于容器从哪个目录启动你在 IDE 里跑是工程根目录打成 war 部署后又变成 Tomcat 的bin文件写到哪完全不可控判断“上传成功但找不到文件”就先看这里。第三处是上下文路径。war 包名决定访问路径homework.war就要访问http://localhost:8080/homework/很多人直接访问http://localhost:8080/然后拿到 404以为部署失败了。想固定路径就在 Tomcat 的server.xml里配Context的path或者干脆把 war 改名叫ROOT.war。3. 核心链路拆解作业发布、提交、批改三段代码链路是三个动作串起来的教师发作业、学生提交、教师批改。三段代码各自的失败模式完全不同所以拆开看。3.1 表结构设计唯一索引才是防重复提交的正解先看两张核心表。设计上最容易出问题的不是字段够不够而是约束有没有下沉到数据库。应用层的if (已存在) return在并发下必然漏两个请求同时查到“不存在”然后各插一条。-- 作业表一条记录就是一次布置 CREATE TABLE t_assignment ( 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 截止时间提交时强校验, status TINYINT NOT NULL DEFAULT 1 COMMENT 1进行中 2已截止, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_class_deadline (class_id, deadline) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 提交表唯一索引锁死一人一作业一条记录 CREATE TABLE t_submission ( id BIGINT NOT NULL AUTO_INCREMENT, assignment_id BIGINT NOT NULL, student_id BIGINT NOT NULL, content TEXT COMMENT 文字作答内容, file_path VARCHAR(500) COMMENT 附件相对路径, file_name VARCHAR(255) COMMENT 附件原始文件名, submit_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, status TINYINT NOT NULL DEFAULT 0 COMMENT 0未批 1已批, score DECIMAL(5,2) COMMENT 百分制得分, comment VARCHAR(1000) COMMENT 教师评语, reviewer_id BIGINT COMMENT 批改人, review_time DATETIME COMMENT 批改时间, PRIMARY KEY (id), UNIQUE KEY uk_assign_student (assignment_id, student_id), KEY idx_status_submit (status, submit_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;uk_assign_student这一行是整个设计的地基。有了它重复提交在数据库层面就会被拒绝应用层只需要决定“拒绝”还是“覆盖”。idx_status_submit是给教师后台列表用的未批改的记录永远是最常查的那批没有这个索引数据量上到几万条后列表页会明显卡。score用DECIMAL(5,2)而不是FLOAT因为分数是要参与求平均的浮点累加会出现87.99999999这种结果导出到表格后被教务那边打回来重做很烦人。3.2 学生提交截止时间校验和覆盖式更新提交接口要处理两件事过没过截止时间、是不是重复提交。第一件事必须在服务端做前端那个倒计时只是给人看的。// SubmissionService.java —— 提交作业的核心逻辑 public String submit(long assignmentId, long studentId, String content, String filePath, String fileName) { // 1. 截止时间强校验只信任数据库里的 deadline Assignment a assignmentDao.findById(assignmentId); if (a null) { return 作业不存在; } if (a.getDeadline().before(new Date())) { return 已过截止时间提交关闭; } // 2. 成绩已出的不允许再改避免批改后内容被替换 Submission old submissionDao.findByAssignmentAndStudent(assignmentId, studentId); if (old ! null old.getStatus() 1) { return 该作业已批改不能再修改提交; } // 3. 走 upsert靠唯一索引兜住并发重复提交 // 重复提交视为覆盖submit_time 刷新status 重置为未批 submissionDao.upsert(assignmentId, studentId, content, filePath, fileName); return ok; }逻辑说明三个判断的先后顺序是有讲究的。先查作业是否存在避免拿一个不存在的 id 去查提交表再判截止时间这是最便宜的拦截最后才查已有提交因为这一步要读一条可能带大字段的记录。如果反过来先查提交记录一个过期作业的每次提交都要多读一次大字段白白浪费。参数说明filePath存的是相对于upload.basePath的路径比如2024/06/xxx.docx不要存绝对路径。绝对路径一旦换服务器就全废而且会把文件系统的目录结构暴露在接口返回值里。对应的 DAO 层用一条 SQL 解决-- upsert存在则覆盖不存在则插入 INSERT INTO t_submission (assignment_id, student_id, content, file_path, file_name, submit_time, status) VALUES (?, ?, ?, ?, ?, NOW(), 0) ON DUPLICATE KEY UPDATE content VALUES(content), file_path VALUES(file_path), file_name VALUES(file_name), submit_time NOW(), status 0, -- 重置为未批教师侧要重新看到 score NULL, -- 旧分数必须清掉否则出现未批但有分 comment NULL;ON DUPLICATE KEY UPDATE里的status 0和score NULL这两行是关键。如果漏了会出现一个很诡异的状态学生重新提交了教师看列表还是“已批”点进去看到旧分数配新内容。这种数据在期末统计时最难解释。3.3 教师批改状态位和分数必须原子更新批改接口看着简单其实两个字段必须一起动score和status。分开 update 会出现中间态尤其是列表页和详情页并发读的时候。// ReviewService.java —— 批改与成绩回写 public String review(long submissionId, long teacherId, BigDecimal score, String comment) { // 分数范围在此处兜底前端校验可以被绕过 if (score null || score.compareTo(BigDecimal.ZERO) 0 || score.compareTo(new BigDecimal(100)) 0) { return 分数必须在 0 到 100 之间; } // 批改意见长度限制超过列宽会抛 DataTooLong if (comment ! null comment.length() 1000) { comment comment.substring(0, 1000); } // 一条 update 同时写分数、状态、批改人和时间 int rows submissionDao.review(submissionId, teacherId, score, comment); if (rows 0) { return 记录不存在或已被删除; } return ok; }-- 原子更新四件事一条 SQL 做完不给自己留中间态 UPDATE t_submission SET score ?, comment ?, status 1, reviewer_id ?, review_time NOW() WHERE id ? AND status 0; -- 条件里带 status0天然防重复批改WHERE ... AND status 0这个写法值得记住。它让第二次批改请求返回影响行数 0接口就能给出“该记录已被批改”的提示而不是悄悄覆盖别人的分数。多人协作批同一个班的时候这个细节能省掉很多扯皮。至于成绩回写如果还有一张单独的t_score汇总表那就必须放进同一个事务里// 两张表要么一起成功要么一起回滚 Connection conn null; try { conn dataSource.getConnection(); conn.setAutoCommit(false); // 关闭自动提交 submissionDao.review(conn, submissionId, teacherId, score, comment); scoreDao.upsertFinalScore(conn, assignmentId, studentId, score); conn.commit(); // 全部成功才提交 } catch (SQLException e) { if (conn ! null) conn.rollback(); // 任何一步失败全撤 throw new RuntimeException(批改失败, e); } finally { if (conn ! null) { conn.setAutoCommit(true); conn.close(); } }注意setAutoCommit(false)之后DAO 里的每个方法必须接收同一个Connection参数不能各自去连接池取。这是老工程里最常见的写法错误——方法签名里没传连接事务开在 A 连接上SQL 跑在 B 连接上回滚等于没回滚。4. 附件上传与批改意见最容易在上线第一天炸的两块作业系统不带附件基本没法用而附件又是整个工程里最容易出问题的一块。上传和下载是两个独立的失败点中间还有文件名编码这个经典坑。4.1 上传目录、大小限制与文件名净化如果工程用的是 Servlet 3.0 的注解方式MultipartConfig决定了一切// UploadServlet.java —— 附件上传 WebServlet(/api/upload) MultipartConfig( maxFileSize 10 * 1024 * 1024, // 单文件 10MB maxRequestSize 12 * 1024 * 1024, // 整个请求 12MB留 2MB 给表单字段 fileSizeThreshold 1024 * 1024 // 超过 1MB 才写临时文件小文件走内存 ) public class UploadServlet extends HttpServlet { protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { long studentId (Long) req.getSession().getAttribute(uid); if (studentId 0L) { // 未登录直接拒 resp.setStatus(401); return; } Part part req.getPart(file); String rawName part.getSubmittedFileName(); // 1. 只取文件名部分挡住 ../ 这类目录穿越 String safeName new File(rawName).getName(); // 2. 白名单校验扩展名不校验 MIME客户端可伪造 String ext safeName.substring(safeName.lastIndexOf(.) 1).toLowerCase(); if (!Arrays.asList(doc, docx, pdf, zip, txt, jpg, png).contains(ext)) { resp.getWriter().write({\ok\:false,\msg\:\不支持的文件类型\}); return; } // 3. 落盘文件名用 UUID原始名单独存库 String storeName UUID.randomUUID().toString().replace(-, ) . ext; String dateDir new SimpleDateFormat(yyyy/MM).format(new Date()); File dir new File(uploadBasePath, dateDir); if (!dir.exists() !dir.mkdirs()) { throw new IOException(上传目录创建失败 dir.getAbsolutePath()); } part.write(new File(dir, storeName).getAbsolutePath()); // 4. 入库相对路径 submissionDao.updateFile(assignmentId, studentId, dateDir / storeName, safeName); resp.getWriter().write({\ok\:true}); } }逻辑说明第一步的new File(rawName).getName()是必须的。有些客户端会在文件名里带路径../../webapps/ROOT/x.jsp这种如果直接拼到上传目录后面就是一个上传攻击面。第二步只查扩展名而不查Content-Type因为请求头里的类型完全由客户端决定形同虚设。参数说明maxFileSize和maxRequestSize的关系要理清前者是单个 part 的上限后者是包括所有 part 加表单字段的总上限配置时后者必须大于前者。fileSizeThreshold控制什么时候用磁盘临时文件设太小会让每个小文件都写盘设太大则大文件全压在内存里1MB 是个平衡点。如果工程用的是 Spring MVC对应写法是接收MultipartFile但校验逻辑一模一样别因为在框架里就省掉扩展名白名单。Spring Boot 场景下还要额外配spring.servlet.multipart.max-file-size两处都对上才不会出现“Controller 里拿不到文件但也不报错”的情况。4.2 下载鉴权与中文文件名乱码下载比上传更容易出事因为很多人直接拼接文件路径返回静态资源完全没有鉴权。这意味着只要知道 URL任何人都能下别人的作业。// DownloadServlet.java —— 带鉴权的附件下载 protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { long uid (Long) req.getSession().getAttribute(uid); String role (String) req.getSession().getAttribute(role); long sid Long.parseLong(req.getParameter(sid)); Submission s submissionDao.findById(sid); if (s null) { resp.sendError(404); return; } // 鉴权学生只能下自己的教师不限 boolean isOwner (s.getStudentId() uid); boolean isTeacher teacher.equals(role) || admin.equals(role); if (!isOwner !isTeacher) { resp.sendError(403); return; } File f new File(uploadBasePath, s.getFilePath()); // 二次确认解析后的真实路径必须仍在上传根目录内 if (!f.getCanonicalPath().startsWith(new File(uploadBasePath).getCanonicalPath())) { resp.sendError(403); return; } if (!f.exists()) { resp.sendError(404, 附件已丢失); return; } resp.setContentType(application/octet-stream); // 中文文件名用 RFC 5987 写法同时保留一份 ASCII 兜底 String encoded URLEncoder.encode(s.getFileName(), UTF-8).replace(, %20); resp.setHeader(Content-Disposition, attachment; filename\file s.getId() \; filename*UTF-8 encoded); try (InputStream in new FileInputStream(f); OutputStream out resp.getOutputStream()) { byte[] buf new byte[8192]; int n; while ((n in.read(buf)) ! -1) { out.write(buf, 0, n); } } }getCanonicalPath()那次判断是防止数据库里被写进../../etc/passwd这类路径后下载接口变成一个任意文件读取口子。虽然前面的上传已经做了净化但历史数据、手工改库、导入脚本都可能带进来脏路径加一层不吃亏。Content-Disposition里同时给filename和filename*是有原因的前者给老浏览器兜底后者按 RFC 5987 用 UTF-8 编码现代浏览器优先识别后者。只写前者中文文件名会变成乱码只写后者个别老客户端会拿到一个空文件名。4.3 批改意见的存储别让长度和格式把你坑了评语字段看着是个文本框实际有三个隐性问题。第一个是长度。VARCHAR(1000)在 utf8mb4 下最多占 4000 字节如果教师习惯粘贴一大段格式化文本超长会直接抛Data too long for column而错误堆栈指向的是 SQL 层排查时容易误以为是连接问题。稳妥做法是服务端截断或者在表里直接改成TEXT。第二个是换行。前端 textarea 里的换行是\n存进库没问题但展示到 JSP 页面上会被 HTML 合并成一个空格看起来就是“评语挤成一坨”。解决办法是展示前做一次替换把\n换成br/或者用pre标签包起来。// 展示前处理注意先转义再替换顺序反了会有 XSS 风险 public String formatComment(String raw) { if (raw null) return ; String escaped raw.replace(, amp;).replace(, lt;).replace(, gt;); return escaped.replace(\n, br/); }第三个是并发。同一个提交记录被两个教师同时打开批改后提交的覆盖先提交的而且没有任何痕迹。要留痕就得在批改时把旧值写进一张日志表这条放在后面进阶部分展开。5. 排查与避坑五条上线前一定要走一遍的血泪记录下面五条是我按“实际遇到频率”排的每条都按现象、原因、解决三步写。建议在交付前照着单子过一遍。5.1 现象登录成功但页面一直跳回登录页原因这是最典型的拦截器问题。登录接口把用户信息放进了 session但拦截器判断的 key 和写入的 key 不一样比如写入用的是user判断用的是loginUser。还有一种情况是 session 超时时间配成了 0 或者负数容器认为 session 立即失效请求进来时 session 已经是新的。解决先打个日志确认 key 名。// 登录成功后打印一次和拦截器里的常量对比 System.out.println(session key req.getSession().getId()); System.out.println(user req.getSession().getAttribute(user));确认无误后再查web.xml里的session-configsession-timeout单位是分钟写成0表示永不过期写成负数或漏配在某些容器上会出问题。我一般显式写 30。5.2 现象中文作业标题存进库变成一串问号原因链路上有三处字符集任何一处不是 utf8 都会断。数据库/表/列的字符集、JDBC 连接串的characterEncoding、以及 Tomcat 的URIEncoding。最常见的遗漏是连接串写了characterEncodingutf8但库里是utf8mb4三字节能过、四字节比如某些符号和生僻字就丢。解决三处一起核。-- 库、表、列三级都看一眼 SHOW CREATE DATABASE homework_db; SHOW CREATE TABLE t_assignment; SHOW FULL COLUMNS FROM t_assignment LIKE title;连接串统一写characterEncodingutf8mb4Tomcat 的server.xml里 Connector 加URIEncodingUTF-8。改完之后已经变成问号的存量数据是救不回来的只能重新录入。5.3 现象上传提示成功但下载报 404 或提示文件丢失原因三种可能。一是上传目录用了相对路径文件被写到了容器的bin目录下下载时按绝对路径去找当然找不到二是数据库里存的是绝对路径换台机器路径前缀全变了三是上传时目录不存在但mkdirs()的返回值没检查静默失败。解决把upload.basePath统一改成绝对路径数据库只存相对路径并且在启动时打印一次真实路径做确认。// 应用启动时执行一次日志里能直接看出目录对不对 File base new File(uploadBasePath); System.out.println(upload base base.getCanonicalPath() , writable base.canWrite());canWrite()返回 false 就直接说明是权限问题Linux 部署时上传目录属主不是运行容器的用户这个错比路径错更难发现。5.4 现象学生能通过改 URL 参数下载到别人的作业原因下载接口只校验了“登录”没校验“这条记录是不是你的”。这是权限设计里最基础的横向越权但老工程里非常普遍因为开发时只用同一个账号测过。解决在下载和详情两个接口里都加上归属判断判断逻辑不要分散在各处抽成一个方法。// 统一鉴权入口所有涉及单条提交记录的接口都调它 private boolean canAccess(Submission s, long uid, String role) { if (teacher.equals(role) || admin.equals(role)) return true; return s.getStudentId() uid; }加完之后一定要用两个不同学生账号交叉测一遍测试用例本身比代码更重要。5.5 现象教师批改完列表页还是显示“未批”原因批改 SQL 只更新了score没更新status或者更新了但列表查询用的是缓存。前一种情况更常见尤其是 SQL 是分两次写的时候只改了一处。解决把状态位和分数放进同一条 update见第 3 章的写法并在批改完后立刻用同一个submissionId查一次库做断言。-- 批改后立即自查status 必须是 1 SELECT id, status, score, reviewer_id, review_time FROM t_submission WHERE id ?;如果这条查出来是对的但页面还是旧的那就是缓存或者浏览器缓存的问题接口响应加Cache-Control: no-store能挡住大部分。6. 二次开发把批改痕迹做成可追溯的再用三条 SQL 验收这套源码拿来直接交作业够用但稍微认真一点就会被问一个问题分数被人改过能查出来是谁在什么时候改的吗t_submission里只有最后一次的reviewer_id和review_time历史记录是覆盖掉的。要把这块补上成本其实很低。加一张批改日志表在批改的同一个事务里插一条CREATE TABLE t_review_log ( id BIGINT NOT NULL AUTO_INCREMENT, submission_id BIGINT NOT NULL, reviewer_id BIGINT NOT NULL, old_score DECIMAL(5,2), new_score DECIMAL(5,2), old_comment VARCHAR(1000), new_comment VARCHAR(1000), op_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_submission (submission_id, op_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;关键在于插入日志和更新分数必须在同一个事务里否则会出现“分数改了但日志没写”的情况追溯链就断了。实现上把第 3 章那段事务代码扩一下在review()调用前后分别查出旧值写日志时带上。旧值查询要用SELECT ... FOR UPDATE锁住那一行不然两个并发批改会读到同一个旧值。改完之后怎么验证这套东西是好的我一般跑三条 SQL比点页面快得多验收项SQL 关注点合格标准重复提交是否被拦住SELECT assignment_id, student_id, COUNT(*) FROM t_submission GROUP BY 1,2 HAVING COUNT(*) 1结果为空是否存在“未批却有分”SELECT COUNT(*) FROM t_submission WHERE status 0 AND score IS NOT NULL结果为 0已批记录是否都有批改人SELECT COUNT(*) FROM t_submission WHERE status 1 AND reviewer_id IS NULL结果为 0第一条能查出来说明唯一索引没建或者被删了第二条和第三条能查出来说明批改逻辑里字段更新不全。这三条我每次改完批改相关代码都会跑比手工造数据点页面靠谱得多。再往深一层如果要做成绩统计别在 Java 里把全表捞出来再循环求平均交给数据库-- 按作业统计提交数、已批数、平均分、最高最低分 SELECT a.id, a.title, COUNT(s.id) AS submit_cnt, SUM(CASE WHEN s.status 1 THEN 1 ELSE 0 END) AS reviewed_cnt, ROUND(AVG(CASE WHEN s.status 1 THEN s.score END), 2) AS avg_score, MAX(s.score) AS max_score, MIN(s.score) AS min_score FROM t_assignment a LEFT JOIN t_submission s ON s.assignment_id a.id GROUP BY a.id, a.title ORDER BY a.deadline DESC;AVG里套CASE WHEN是为了把未批改的记录排除掉否则NULL会参与分母计算平均分被拉低。LEFT JOIN保证一条提交都没有的作业也能出现在列表里这对教师端很重要——没人交的作业才是最该被看见的。从那以后我每次接手这类带“提交、批改、回写”三段链路的工程都会先把唯一索引和状态位这两件事核一遍再去动业务代码因为这两处错了后面所有统计都是错的而且错得很安静。希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑