资讯详情 断网机房局域网考试系统:B/S架构部署与自动保存避坑
📅 2026/10/31 7:57:09
简介局域网考试系统含源码是一套面向教学机构、企业培训与开发者的局域网在线考试平台支持题库管理、自动组卷、考生信息维护、自动阅卷与成绩统计并兼顾防作弊与运行稳定性可用于课堂测验、培训考核及二次开发学习。压缩包共84个文件、约4.08MB以C#源码和resx资源文件为主体配有可执行程序、MSI安装包、ini配置、使用说明文档及图标素材涵盖客户端、主考端与服务端三类工程目录按模块组织便于检索、部署与调试。已有460人学习下载适合需要快速搭建局域网考试环境或研究B/S架构考试系统实现的中级开发者参考。读者可从中获得完整工程结构与解决方案文件理解组卷策略、考试流程、成绩管理等模块的代码组织方式并借助安装包和说明文档完成本地运行与定制改造。1. 一间断网机房里的刚需局域网考试系统到底解决什么问题去年冬天帮一个教学单位收拾过一间机房六十台学生机外网全断只有教师机能碰内网第二天早上八点开考。教务给的方案是让学生登录一个在线平台我当场就摇头——没网什么平台都是白搭。后来换成这套局域网考试系统教师机跑一个后端服务学生机打开浏览器输入一个内网地址就能登录答题题库、组卷、开考、自动保存、交卷判分、成绩导出全在局域网内闭环。学校机房、培训机构教室、企业内部考核、技能竞赛都能用会双击一个启动脚本就能开考。真正上手做过的都知道难点从来不是把题目显示出来而是断网、断电、时间不同步这些破事发生时学生那二十分的选择题还在不在。2. 架构与技术选型教师机当服务端学生机只留一个浏览器这套局域网考试系统的形态一句话就能说完教师机是唯一的服务端学生机是零安装的瘦客户端。听着简单但真正决定它好不好用的是几个部署层面的取舍——能不能在没有任何外网的环境里装完能不能在考前十分钟快速铺开六十台机器能不能在考场里不依赖任何运维。2.1 部署形态决定一切为什么不用云也不用装客户端先说要避开的两个坑。第一个是云部署机房里没外网就算有几十台机器同时上传答题请求出口带宽一旦抖动学生那边就是转圈。局域网考试系统的价值恰恰在于不出机房所有流量走内网交换机延迟基本在个位数毫秒断外网对考试毫无影响。第二个是装客户端给六十台机器装 exe 再逐台配置服务器地址考前二十分钟根本做不完而且学生机普遍装了还原卡重启一次装的东西全没了。所以主流做法是 B/S 结构——学生机只需要一个浏览器地址栏敲http://教师机IP:8080就能进考试页。教师机这边把后端和前端打成一个可执行 jar前端静态资源塞进static目录启动就是java -jar不用装 Tomcat不用配 Nginx。这种形态的代价是所有压力都压在教师机一台机器上所以后面在连接池、WebSocket、批量插入上要专门做优化这一点在第 5 章会细说。另一条常见路线是 C/S用桌面框架写教师端和学生端好处是能调用系统 API 做锁屏、禁切屏、禁用 U 盘防作弊更硬。但分发成本高版本升级要重装全机房。我一般的判断标准是如果考务方明确要求锁屏、禁切屏就走 C/S 或给浏览器端配一个轻量的全屏锁如果只是常规机考B/S 足够出问题也更好救。2.2 技术栈与目录结构一个 jar 包 一个 static 目录后端用 Spring Boot 2.7 MyBatis-Plus数据库 MySQL 8缓存不上 Redis局域网场景没必要多一个要装的服务考试状态直接用数据库字段 本地缓存。实时监控用 WebSocket 原生ServerEndpoint不引 STOMP少一层依赖少一个出错点。前端 Vue 3 Vite Element Plus教师端和学生端写在同一个工程里靠路由前缀区分。这种项目我习惯把结构切成三块服务端、前端、运维脚本。目录大致长这样exam-lan/ ├── exam-server/ 后端工程 │ ├── pom.xml │ └── src/main/ │ ├── java/com/exam/ │ │ ├── ExamApplication.java 启动类 │ │ ├── config/ WebSocket、跨域、登录拦截器 │ │ ├── controller/ 教师端 学生端接口 │ │ ├── service/ 组卷、判分、答卷、监控 │ │ ├── mapper/ MyBatis-Plus Mapper │ │ ├── entity/ 与表一一对应的实体 │ │ └── common/ 统一返回 R、异常、ID 生成 │ └── resources/ │ ├── application.yml 端口、数据源、上传目录 │ ├── mapper/*.xml 手写 SQL组卷、upsert │ └── static/ 前端打包产物直接对外 ├── exam-web/ 前端工程 │ ├── src/ │ │ ├── views/teacher/ 题库、组卷、监考、成绩 │ │ ├── views/student/ 登录、答题、交卷 │ │ ├── api/ axios 封装 │ │ └── router/index.js │ └── vite.config.js build.outDir 指向后端 static └── deploy/ ├── start.bat Windows 教师机一键启动 ├── start.sh └── sql/init.sql 建库建表 默认管理员把vite.config.js的build.outDir直接指到../exam-server/src/main/resources/static前端npm run build之后打 jar 就自带页面机房现场只用拷一个 jar 过去。这个细节看起来小但省掉了现场配 Nginx 反向代理的所有麻烦。2.3 数据表怎么设计从题库到答卷的六张核心表数据模型决定了后面能改什么。这套系统的最小闭环是题库 → 试卷 → 考试 → 考生答卷 → 作答记录 → 成绩。我一般保留这几张表其余字段按需加表名作用关键字段question题库type题型、options_json选项、answer标准答案、score分值paper试卷total_score总分、creator_id出卷人paper_question试卷题目关联paper_id、question_id、sort题序、score本卷分值exam考试场次start_time/end_time时间窗、duration时长、status状态exam_student考生与考试的关系status未开始/答题中/已交卷、score总分answer_record每条作答联合唯一键(exam_id, student_id, question_id)这里最值钱的是answer_record上的那个联合唯一索引它让保存答案可以写成一条INSERT ... ON DUPLICATE KEY UPDATE不用先查再判再写。学生每 20 秒保存一次、切一题保存一次、交卷前再刷一次一路高频写入也不会产生重复行更不会因为并发插两条而把判分搞乱。-- 考试场次时间一律存服务端本地时区前端不做任何时间换算 CREATE TABLE exam ( id BIGINT NOT NULL AUTO_INCREMENT, paper_id BIGINT NOT NULL COMMENT 关联试卷, name VARCHAR(100) NOT NULL COMMENT 考试名称, start_time DATETIME NOT NULL COMMENT 开考时间以服务端为准, end_time DATETIME NOT NULL COMMENT 收卷时间, duration INT NOT NULL COMMENT 考试时长分钟, shuffle TINYINT DEFAULT 0 COMMENT 0 不打乱题序 1 打乱, status TINYINT DEFAULT 0 COMMENT 0 未开始 1 进行中 2 已结束, PRIMARY KEY (id), KEY idx_status_start (status, start_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 作答记录联合唯一键是自动保存能写成 upsert 的前提 CREATE TABLE answer_record ( id BIGINT NOT NULL AUTO_INCREMENT, exam_id BIGINT NOT NULL, student_id BIGINT NOT NULL, question_id BIGINT NOT NULL, answer TEXT COMMENT 多选存 JSON 数组如 [A,C], score DECIMAL(5,1) DEFAULT NULL COMMENT 主观题批改后回填, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_exam_stu_q (exam_id,student_id,question_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;注意answer字段用TEXT而不是VARCHAR(255)填空题、简答题、编程题的作答长度不可控。见过有人图省事用VARCHAR(500)结果语文考试的作文题一提交就报字段超长考到一半全考场炸锅。3. 跑起来从建库到六十台学生机同时登录的启动链路很多人拿到这类项目第一反应是先看看代码其实更该先跑一遍。局域网考试系统的启动链路比普通 Web 项目多两步教师机得固定 IP数据库得预置初始账号。这两步没做后面全是对着空气调试。3.1 环境准备与 application.yml 里必须改的四个参数教师机上需要三样东西JDK 8 或 17跟着 pom 里的maven.compiler.source走、MySQL 8、以及构建阶段用的 Node 18 和 Maven。运行阶段学生机什么都不用装只要浏览器不是 IE 就行——Vue 3 打的包在 IE 上直接白屏机房如果还留着一批老系统要么换浏览器要么上兼容方案这点必须提前确认。环境装完application.yml里有四个参数是必须按现场改的其他的保持默认即可server: port: 8080 # 若被占用改成 8081学生机地址同步改 tomcat: threads: max: 200 # 一个机房 60~120 台同时交卷够用 max-connections: 2000 spring: datasource: url: jdbc:mysql://127.0.0.1:3306/lan_exam?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue username: exam password: exam123 hikari: maximum-pool-size: 30 # 默认 10交卷高峰会排队见第 5 章 connection-timeout: 5000 exam: upload-path: D:/exam-data/upload # 别放 C 盘考试图片会越积越多 auto-save-seconds: 20 # 前端自动保存间隔需与后端一致 late-submit-grace: 120 # 收卷后允许补交的宽限秒数serverTimezone必须显式写死别指望默认值。机房里见过太多次考试显示未开始的事故追到底就是数据库连接时区跟 JVM 时区差了八小时。upload-path同理图片和附件落磁盘比存 BLOB 靠谱数据库备份体积也可控。3.2 建库导数据与首次启动建库、导表、灌一个管理员账号三步做完就能起服务。初始化脚本里我会顺手塞几条测试题和一场两小时后的模拟考试方便第一次跑通全流程# 1. 建库并导入表结构与初始数据 mysql -uroot -p -e CREATE DATABASE IF NOT EXISTS lan_exam DEFAULT CHARSET utf8mb4; mysql -uroot -p lan_exam deploy/sql/init.sql # 2. 单独建一个只能本地连的应用账号别用 root 跑业务 mysql -uroot -p -e CREATE USER examlocalhost IDENTIFIED BY exam123; mysql -uroot -p -e GRANT ALL ON lan_exam.* TO examlocalhost; FLUSH PRIVILEGES; # 3. 后端编译打包跳过测试第一次跑通不用等单测 cd exam-server mvn clean package -DskipTests # 4. 启动控制台看到 Started ExamApplication 即成功 java -jar target/exam-server-1.0.0.jar启动成功后先别急着开浏览器在教师机上访问http://localhost:8080/api/ping确认后端活着再用同一个地址换localhost为教师机内网 IP在另一台学生机上访问。这一步是在验证别人能不能连进来很多人只在本机测通就宣布开工结果开考时发现学生机全打不开原因是 Windows 防火墙拦了 8080。提示init.sql里默认管理员的密码是明文初始值第一次登录后必须改。考场里学生看到教师端后台不是没有过。3.3 打包分发与教师机固定 IP教师机必须设静态 IP。DHCP 分配的地址重启一次就可能变你前一天在黑板上写的192.168.1.100第二天变成192.168.1.137六十个学生挨个改地址开考铃早就响了。设置路径在系统网络设置里顺手把 DNS 也固定反正机房不出网。分发环节分两种。机房有还原卡的把 jar、启动脚本、jre 一起放到一个不被还原的目录或者考试当天用 U 盘/共享盘拷过去跑没有还原卡的直接把start.bat放到启动项开机自启服务端。学生机那边完全不动作考前在黑板上写一行地址或者用广播教学软件推一个快捷方式过去。echo off chcp 65001 nul cd /d %~dp0 echo 正在启动局域网考试系统请勿关闭此窗口... java -Xms512m -Xmx1024m -jar exam-server-1.0.0.jar ^ --spring.datasource.urljdbc:mysql://127.0.0.1:3306/lan_exam?serverTimezoneAsia/Shanghai ^ --server.port8080 pause-Xmx1024m是给堆内存留的余量六十台机器同时交卷会有一次明显的对象创建高峰堆太小会触发频繁 GC老师端看着就是点了交卷没反应。chcp 65001是为了控制台里的中文日志不乱码这个细节在考场上很有用——出问题时你得能读懂日志。4. 核心链路拆解组卷、答题保存、交卷判分与实时监控跑通之后再看代码重点盯四条链路。这四条链路撑起了整套系统也几乎覆盖了所有二次开发的入口。4.1 组卷按题型配额随机抽题抽不满直接失败组卷有两种做法手动挑题和规则抽题。真正用在考场的是后者老师只填单选 20 道、多选 10 道、判断 10 道系统从题库里按题型和难度抽。核心代码不多但有个容易被忽略的判断——抽不满必须抛异常绝不能生成一份残缺试卷。Service public class PaperGenerateService { Resource private QuestionMapper questionMapper; Resource private PaperQuestionMapper paperQuestionMapper; Resource private PaperMapper paperMapper; /** * param quota 题型 - 题量如 {single:20,multi:10,judge:10} * param difficulty 1 易 2 中 3 难传 null 表示不限 */ Transactional(rollbackFor Exception.class) public Long generate(String title, MapString, Integer quota, Integer difficulty, Long creatorId) { Paper paper new Paper(); paper.setTitle(title); paper.setCreatorId(creatorId); paperMapper.insert(paper); int sort 1; BigDecimal total BigDecimal.ZERO; for (Map.EntryString, Integer e : quota.entrySet()) { ListQuestion picked questionMapper.randomPick(e.getKey(), difficulty, e.getValue()); // 抽不满就整单回滚宁可让老师补题也不能生成残缺试卷 if (picked.size() e.getValue()) { throw new BizException(题型 e.getKey() 数量不足需要 e.getValue() 实际 picked.size()); } for (Question q : picked) { PaperQuestion pq new PaperQuestion(); pq.setPaperId(paper.getId()); pq.setQuestionId(q.getId()); pq.setSort(sort); pq.setScore(q.getScore()); paperQuestionMapper.insert(pq); total total.add(q.getScore()); } } paper.setTotalScore(total); paperMapper.updateById(paper); return paper.getId(); } }对应的抽取 SQL 走ORDER BY RAND()select idrandomPick resultTypecom.exam.entity.Question SELECT id, type, stem, options_json, answer, score, difficulty FROM question WHERE type #{type} AND status 1 if testdifficulty ! null AND difficulty #{difficulty} /if ORDER BY RAND() LIMIT #{count} /selectORDER BY RAND()在小题库上完全够用几千道题排序耗时也就几十毫秒组卷是低频操作不值得为它引入更复杂的随机算法。题库上万之后再改成按 ID 区间随机采样 内存洗牌那是另一个量级的事。还有一个必须做但常被漏掉的点试卷下发时要剥掉答案。教师端的接口返回完整实体学生端的接口必须走独立的 VOanswer、analysis这些字段一律不出现。见过有人图省事直接复用同一个 Controller学生打开浏览器开发者工具就能看见标准答案。4.2 答题保存三处触发 唯一索引兜底这是整个系统最容易出问题的地方也是考场上最不能出问题的地方。只写一个setInterval定时保存是不够的——学生点完选项立刻关机那 20 秒的答案就没了。我一般做三层// 学生答题页切题、定时、交卷前三处都要触发保存 const flushCurrent async () { const q list[currentIndex.value] if (!q || !dirtySet.has(q.id)) return // 没改过就不发请求 try { await api.post(/student/answer/save, { examId, questionId: q.id, answer: answerMap[q.id] }) dirtySet.delete(q.id) localStorage.setItem(draft_${examId}, JSON.stringify(answerMap)) } catch (e) { pendingQueue.push({ questionId: q.id, answer: answerMap[q.id] }) // 失败入队 } } // 1) 切题时立即保存别等定时器 watch(currentIndex, async () { await flushCurrent() }) // 2) 定时兜底间隔与后端 exam.auto-save-seconds 保持一致 const timer setInterval(flushCurrent, 20000) // 3) 交卷前强制刷一次并把失败的请求重放 const submit async () { clearInterval(timer) await flushCurrent() for (const item of pendingQueue) { await api.post(/student/answer/save, { examId, ...item }) } await api.post(/student/exam/submit, { examId }) }后端的写入走一条 upsert配合前面的联合唯一索引insert idupsert INSERT INTO answer_record (exam_id, student_id, question_id, answer) VALUES (#{examId}, #{studentId}, #{questionId}, #{answer}) ON DUPLICATE KEY UPDATE answer VALUES(answer), updated_at NOW() /insertController 里有一处不能省状态校验必须在服务端做。前端传上来的examId是什么就是什么但这场考试现在能不能作答必须后端查库判断不能信前端带过来的status。收卷之后学生如果还开着页面一个手动请求就能改答案这种漏洞在正式考试里后果很严重。PostMapping(/answer/save) public RVoid save(RequestBody Valid AnswerSaveDTO dto, HttpServletRequest req) { Long studentId (Long) req.getAttribute(userId); Exam exam examService.getById(dto.getExamId()); // 只接受进行中的考试并留出收卷宽限时间 if (exam null || exam.getStatus() ! 1 || LocalDateTime.now().isAfter(exam.getEndTime().plusSeconds(grace))) { return R.fail(考试已结束本次作答未保存); } answerService.upsert(dto.getExamId(), studentId, dto.getQuestionId(), dto.getAnswer()); return R.ok(); }AnswerSaveDTO里answer要做长度上限校验比如 5000 字避免有人贴一整篇文章把TEXT写爆。参数grace从配置的late-submit-grace读默认 120 秒专门应对收卷铃响了学生还在点提交的情况。断线重连也在这里一并解决。学生机浏览器崩溃、网线被踢掉、误按 F5重新打开页面后前端先拿localStorage里的草稿渲染再请求/api/student/exam/{id}/progress拉服务端已存的记录做合并以服务端为准。这条链路做通了考场里少说能救回五六个人的卷子。4.3 交卷判分客观题自动主观题进队列交卷的判分逻辑本身不复杂难的是题型扩展。用switch硬写能跑但加一种题型就得改一处核心方法。我一般把它收成一个策略接口判分规则集中在一个类里方便教务按自己的评分标准改public BigDecimal judge(Question q, String studentAnswer) { if (StringUtils.isBlank(studentAnswer)) return BigDecimal.ZERO; switch (q.getType()) { case single: return q.getAnswer().trim().equalsIgnoreCase(studentAnswer.trim()) ? q.getScore() : BigDecimal.ZERO; case multi: { // 多选全对满分少选给一半错选 0 分按考务规则改这里 SetString std splitToSet(q.getAnswer()); SetString stu splitToSet(studentAnswer); if (!std.containsAll(stu)) return BigDecimal.ZERO; return std.equals(stu) ? q.getScore() : q.getScore().multiply(new BigDecimal(0.5)); } case judge: return q.getAnswer().equals(studentAnswer) ? q.getScore() : BigDecimal.ZERO; default: return null; // 主观题返回 null进人工批改队列 } }返回null而不是 0 是关键设计。0 分和未批改在业务上是两回事成绩表里必须能区分否则导出成绩单时主观题全显示 0 分老师还得挨个改。交卷时按answer_record逐条判分、回填score再把总分写进exam_student整个流程放在一个事务里中途失败就整体回滚避免出现总分算了一半的脏数据。成绩导出走 EasyExcel 之类的工具一行一条学生记录附带各题得分。导出的字段顺序建议跟教务的模板对齐不然老师拿到手还得手动调列。4.4 考场监控WebSocket 房间广播切屏与断线监考页面是老师最常用的功能本质是一个实时看板谁在线、谁交卷了、谁切屏超过三次、谁掉线了。轮询也能做但六十台机器每 3 秒轮询一次教师机的连接数会被无谓地占满所以用 WebSocket 更合适。按考试 ID划分房间教师端和学生端都连同一个 endpoint服务端按房间广播Component ServerEndpoint(/ws/monitor/{examId}/{token}) public class MonitorSocket { private static final MapLong, SetSession ROOMS new ConcurrentHashMap(); OnOpen public void onOpen(Session session, PathParam(examId) Long examId, PathParam(token) String token) { if (!tokenService.valid(token)) { // 必须先验身份否则谁都能围观考场 try { session.close(); } catch (IOException ignored) {} return; } ROOMS.computeIfAbsent(examId, k - ConcurrentHashMap.newKeySet()).add(session); } OnClose public void onClose(Session session, PathParam(examId) Long examId) { SetSession set ROOMS.get(examId); if (set ! null) set.remove(session); } /** 学生端检测到切屏/断线时调用教师端实时收到 */ public static void broadcast(Long examId, Object payload) { SetSession set ROOMS.getOrDefault(examId, Collections.emptySet()); String json JSON.toJSONString(payload); for (Session s : set) { if (s.isOpen()) s.getAsyncRemote().sendText(json); } } }session.getAsyncRemote()而不是getBasicRemote()前者是非阻塞发送六十个连接同时广播时不会把线程卡住。切屏检测用document.visibilitychange学生切出去一次上报一次服务端在exam_student上累加计数超过阈值就在监考页面上标红。这套机制防不住有心人但能挡住大部分无意或半故意的行为对常规机考足够了。5. 机房实测避坑五件最容易翻车的事纸面上跑通和考场里跑通是两码事。下面五条都是我在真实机房里踩出来的按现象、原因、解决写照着排查能省不少时间。5.1 网络与环境类问题教师机 IP 第二天变了全考场打不开页面。现象是前一天测试一切正常第二天开考时学生机集体白屏。原因是教师机走 DHCP重启后从路由器拿到新地址。解决办法是考前把教师机改成静态 IP并在路由器上做 MAC 绑定同时在学生机桌面留一个考试入口.url快捷方式别让学生手敲地址。学生机中途掉线重连后提示考试不在进行中。现象是个别机器网络闪断学生再登录时被拒。原因多半是断线期间触发了前端的自动交卷逻辑把状态改成了已交卷。解决办法是前端断线只做重试、不做自动交卷重连后调progress接口恢复现场后端把grace宽限时间和心跳超时分开配心跳超时设 90 秒允许短暂断网。服务器时间和学生机时间对不上出现未到开考时间。现象是明明到点了一部分机器能进一部分进不去。原因是比较时间时用了客户端时间。解决办法是前端不做任何时间判断全部由后端基于LocalDateTime.now()返回剩余秒数前端只负责倒数。这条血泪经验值一条命。5.2 数据与并发类问题交卷高峰期大量请求超时学生点了交卷没反应。现象是收卷前五分钟教师机 CPU 打满日志里一片Connection is not available。原因是 HikariCP 默认最大连接 10六十台机器同时交卷每条交卷要读写多条记录连接池瞬间排满。解决办法是把maximum-pool-size调到 30交卷逻辑改成先批量写answer_record再算总分并把单条判分改成批量提交同时把connection-timeout设成 5 秒让排不上的请求快速失败而不是无限等。答案丢失学生说我明明选了。现象是考后复核发现个别学生某几题空白。原因通常只有两个一是只做了定时保存学生关机时最后一段没落库二是前端把答案只存在内存里页面一跳转就丢。解决办法就是第 4 章那三层保存再加上localStorage草稿兜底服务端和本地双写任何一边活着都能恢复。前端更新后部分学生机还在跑旧版本。现象是修复了一个判分 bug重启服务后有的机器正常有的还是老样子。原因是浏览器缓存了旧的index.html和静态资源。解决办法是构建时给文件名加 hashVite 默认就会做并在部署脚本里把index.html的缓存响应头设成no-cache紧急情况下让学生按CtrlF5强制刷新。6. 二次开发把通用考试系统改成你们考务规则的样子这套系统的骨架是通用的但每家的考务规则都不一样二次开发避不开。最常见的两类需求是加题型和换存储。6.1 新增题型的扩展点在哪里加一种题型改动点集中在四个位置按顺序来基本不会漏question.type的取值约定前端下拉选项、答题组件的渲染分支学生端 Vue 组件里按 type 分发、判分方法里的case分支、以及成绩导出时的列定义。填空题的坑最多因为有多个空、答案可能有多个等价写法我一般把标准答案存成 JSON 数组判分时对每个空做 trim 忽略大小写的比较需要模糊匹配的再挂一个同义词表。如果考务要求编程题自动判分别自己写判题机。局域网环境里跑学生代码本身就有风险比较稳的做法是把编程题降级为文本作答 人工批改或者接一个独立部署的判题服务考试系统只负责收答案和存分数。6.2 改造前后对照与验证方式动手改之前先把改动点和风险列清楚比改完再回头补测试省事得多改造需求主要改动文件风险点新增填空题QuestionType、判分策略类、答题组件多空答案的分隔符要与历史数据兼容换 SQLite 免安装application.yml、pom.xml驱动、建表脚本ON DUPLICATE KEY UPDATE要换成 SQLite 的ON CONFLICT加锁屏防作弊学生端页面、全屏 API浏览器权限差异大考前必须逐台试加考试归档exam.status、定时任务归档后不能影响历史成绩查询验证方式我一般走三步。第一步单测覆盖判分策略把每种题型的边界都写一遍空答案、多选少选、多选错选、大小写差异。第二步单机模拟并发开三到五个浏览器窗口用不同学生账号同时答题、同时交卷盯日志里有没有连接池告警。第三步才是真机演练找十台机器中途拔一台的网线、关一台的浏览器、让一台切屏五次看监考页面能不能正确反映成绩能不能对上。这三步走完考场上的意外基本能覆盖住。改这类系统有个隐形成本容易被低估历史数据兼容。加字段容易改字段语义难。answer从字符串改成 JSON 数组这种改动一定要写迁移脚本把老数据转过去并且在判分方法里同时兼容两种格式跑一段时间再清掉兼容分支。从那以后我每次动这类考试系统的数据模型都强制先跑一遍老数据回放——把上一次考试的完整答卷导进测试库用新代码重新判一遍分分数跟归档成绩对不上就不许上线。这个习惯救过我两次其中一次是多选判分规则改了但没兼容空答案一跑回放就发现有人从 8 分变成 0 分。希望帮到你。本文还有配套的精品资源点击获取