简介《局域网考试系统终版》面向学校教师、教务管理者及需要组织课堂测验或校园考试的教学团队定位于在局域网内部署的电子考核工具重点解决出卷、阅卷、成绩统计与考试安全控制等环节的效率问题。压缩包约7.58MB包内文件总数与类型明细暂未提供从配套说明可知包含安装程序与使用手册便于在封闭网络环境中完成部署和上手。已有207人学习或下载具备一定参考热度。资源围绕试题库管理、自定义试卷、客观题自动批改、成绩报告生成、考试时间与题型设置、防作弊控制和答题数据分析展开可帮助教师减少人工批改负担快速掌握学生答题情况并调整教学界面与操作流程偏简洁适合中小规模课堂测验及校内考试场景使用。1. 局域网考试系统终版一间机房把外网拔了考试还得照常跑完机房里的局域网考试最怕的往往不是题难而是网断。外网一断云端考试系统集体白屏六十台机器同时举手监考老师只能干瞪眼。这套「局域网考试系统终版」针对的就是这个场景服务端跑在一台普通 PC 或讲台机上考生用浏览器访问内网 IP整套系统不依赖任何外部网络。它适合学校机房、培训机构机考、单位内部考核这类“网络封闭、人数几十到一两百、必须当天出成绩”的场合。终版之所以值得单独拿出来讲是因为它把考场上真正会出问题的几件事补齐了答案断线暂存、服务端崩溃后的数据恢复、收卷高峰的并发压制以及一个能让你在开考前十分钟发现问题的自检流程。下面按“骨架怎么搭、链路怎么走、故障怎么兜、坑在哪”的顺序拆开能直接照着复现。2. 拆开终版看骨架六张表、B/S 架构与十分钟能跑起来的部署拿到一套考试系统先别急着导题库。机房环境跟互联网产品不一样没有运维值守、没有灰度发布、没有回滚窗口考试铃一响就是生产环境。所以第一件事是看懂它的架构取舍和数据模型知道哪一层出问题会直接导致丢答案哪一层出问题只是页面难看。2.1 为什么考场天然适合 B/S 而不是 C/S老机房里不少考试系统是 C/S 的客户端装一遍配置一遍还要注册组件。平时没事一到考试就出问题某台机器还原卡把客户端干掉了某台机器 .NET 版本不对某台机器杀软把 exe 拦了。这类问题在考试当天基本无解因为你没有第二台机器可以换。B/S 把这些风险挪到了服务端。考生端就是一个浏览器地址栏输http://10.10.10.10:8000就能进考场机器还原了也不影响重启后重新输地址即可。代价是前端要自己兜底浏览器会缓存、会休眠标签页、会在切屏时挂起定时器这些东西在考场上都会被放大。维度C/S 客户端B/S 浏览器机房部署每台机器装一次还原卡重启即失效服务端一份考生端零安装改题改卷全机房重装或打补丁服务端改完考生刷新页面断网续考客户端本地库可离线答题依赖前端缓存加服务端重连故障定位逐台翻客户端日志服务端集中看浏览器 F12 辅助兼容风险与操作系统版本强绑定浏览器内核差异、缓存策略提示B/S 在考场上的隐性成本是“浏览器不可控”。老机房常见的双核机器配老内核浏览器前端别用太新的语法特性构建时把兼容目标压低一档比事后打补丁省事。2.2 数据模型六张表撑住一场考试整套系统的核心是六张表。判断一套考试系统靠不靠谱看它的作答表有没有唯一索引就够了——没有唯一索引自动保存就是“后写覆盖先写”考生改过的答案会被几秒前的一次重试冲掉这是最典型的丢分现场。-- 考试场次时间以服务端为准考生端只做展示 CREATE TABLE exam ( id BIGINT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(64) NOT NULL, start_at DATETIME NOT NULL, end_at DATETIME NOT NULL, duration INT NOT NULL, -- 分钟用于个人倒计时 status TINYINT NOT NULL DEFAULT 0 -- 0未开始 1进行中 2已收卷 3已判分 ); -- 考生机房用学号做主键避免重名带来的串号 CREATE TABLE student ( sno VARCHAR(20) PRIMARY KEY, name VARCHAR(32) NOT NULL, class_name VARCHAR(32) NOT NULL ); -- 题目type 决定判分策略answer 统一存 JSON 数组 CREATE TABLE question ( id BIGINT PRIMARY KEY AUTO_INCREMENT, type TINYINT NOT NULL, -- 1单选 2多选 3判断 4填空 5主观 stem TEXT NOT NULL, options JSON NULL, -- [{k:A,v:...}, ...] answer JSON NOT NULL, -- [A] / [A,C] / [对] / [TCP,传输控制协议] score DECIMAL(4,1) NOT NULL, knowledge VARCHAR(32) NOT NULL, -- 知识点标签组卷按它配额 difficulty TINYINT NOT NULL, -- 1易 2中 3难 content_hash CHAR(32) NOT NULL, UNIQUE KEY uk_hash (content_hash) -- 同一道题重复导入直接拦掉 ); -- 试卷一人一卷seed 保证可复现 CREATE TABLE paper ( id BIGINT PRIMARY KEY AUTO_INCREMENT, exam_id BIGINT NOT NULL, sno VARCHAR(20) NOT NULL, seed BIGINT NOT NULL, total DECIMAL(5,1) NOT NULL, created_at DATETIME NOT NULL, UNIQUE KEY uk_exam_sno (exam_id, sno) ); -- 试卷题目快照题目后续被改不影响已经发出去的卷 CREATE TABLE paper_question ( paper_id BIGINT NOT NULL, question_id BIGINT NOT NULL, seq INT NOT NULL, score DECIMAL(4,1) NOT NULL, PRIMARY KEY (paper_id, question_id) ); -- 作答主键就是幂等键重复提交不会产生第二条记录 CREATE TABLE answer_record ( paper_id BIGINT NOT NULL, question_id BIGINT NOT NULL, answer JSON NULL, updated_at DATETIME NOT NULL, PRIMARY KEY (paper_id, question_id) );paper_question这张快照表容易被省掉省掉的后果是考试进行到一半有老师发现某道题有问题去改了题干或答案已经发出去的卷子跟着变考生看到的内容和判分依据对不上。快照表把“发卷那一刻的题目状态”固定下来后续改题库只影响下一场。answer_record的主键设计是关键。写入走INSERT ... ON DUPLICATE KEY UPDATE接口天然幂等前端重试、浏览器重复提交、网络抖动导致的补发都不会写出两条记录。这条 SQL 后面还会用到。2.3 十分钟把服务端跑起来服务端常见做法是 Python 的 FastAPI 加 SQLAlchemy小机房直接用 SQLite人多或要多人同时评阅就换 MySQL。下列步骤从空机器开始一台能跑 Python 的机器就够。# 1. 建虚拟环境。离线机房要提前在有网机器上 pip download 好整包拷进内网装 python -m venv venv source venv/bin/activate pip install fastapi0.110.* uvicorn[standard] sqlalchemy pymysql openpyxl # 2. 建库建表DDL 放在 sql/schema.sql mysql -h127.0.0.1 -uroot -p exam_db sql/schema.sql # 3. 改配置数据库地址、监听端口、考生端可访问的静态目录 export DATABASE_URLmysqlpymysql://exam:exam127.0.0.1:3306/exam_db?charsetutf8mb4 # 4. 启动。workers 不要超过 CPU 核数4 核机房机就开 4 uvicorn app.main:app --host 0.0.0.0 --port 8000 --workers 4 # 5. 自检能在本机 curl 通再去别的机器试 curl -s http://127.0.0.1:8000/api/health参数建议值60 人机房说明--host0.0.0.0只绑 127.0.0.1 会导致考生端连不上这是最常见的第一晚翻车点--port8000机房防火墙若只放行 80/443就用 nginx 反代--workers4等于物理核数开太多反而在数据库连接上互相抢连接池pool_size20每个 worker 一份总量 workers × pool_size连接池max_overflow10留给收卷高峰的临时扩容服务端起来之后先在讲台机上用浏览器打开考生端完整走一遍“登录 → 抽卷 → 答两道题 → 交卷”再去任意一台考生机重复一遍。这一步看起来多余但它能提前暴露跨机器访问、时间格式、静态资源路径这三类问题比开考后排查便宜得多。3. 组卷到收卷的整条链路题库导入、分层抽题与状态机推进骨架搭好只是能跑真正决定考试体验的是中间这条链路题库怎么进来、卷子怎么抽出来、状态怎么推进、分数怎么落下去。这一段出问题现象往往很隐蔽——题抽重了、某类题不够、交卷时间和服务端差几分钟考完才发现。3.1 题库导入字段映射与三道校验老师手里的题库基本都是 Excel列名五花八门“题干”“题目”“试题内容”都可能是同一个意思。导入脚本的第一件事不是读数据而是把列名统一映射成内部字段映射不上就直接报错不要猜。Excel 列名内部字段必填说明题干stem是为空整行跳过并记录行号题型type是单选/多选/判断/填空/主观选项A~Doptions选择题必填判断题不填选项由系统生成答案answer是多选写AC填空多个答案用 分值score是小数最多一位知识点knowledge是组卷配额的依据不能空难度difficulty否空则默认中等import hashlib, json from openpyxl import load_workbook TYPE_MAP {单选: 1, 多选: 2, 判断: 3, 填空: 4, 主观: 5} HEADERS [题干, 题型, 选项A, 选项B, 选项C, 选项D, 答案, 分值, 知识点, 难度] def build_answer(qtype, raw): 把 Excel 里的答案文本规整成 JSON 数组判分时只认数组 raw str(raw or ).strip() if qtype in (1, 2): return [c for c in raw.upper() if c in ABCD] if qtype 4: # 填空允许多个等价答案例如 TCP||传输控制协议 return [s.strip() for s in raw.split(||) if s.strip()] return [raw] def import_rows(path, session): ws load_workbook(path, read_onlyTrue).active ok, skipped 0, [] for idx, row in enumerate(ws.iter_rows(min_row2, values_onlyTrue), start2): rec dict(zip(HEADERS, row)) stem str(rec.get(题干) or ).strip() if not stem: skipped.append((idx, 题干为空)) continue qtype TYPE_MAP.get(str(rec.get(题型) or ).strip()) if qtype is None: skipped.append((idx, f题型无法识别: {rec.get(题型)})) continue options [{k: k, v: str(rec.get(f选项{k}) or ).strip()} for k in ABCD if rec.get(f选项{k})] answer build_answer(qtype, rec.get(答案)) if not answer: skipped.append((idx, 答案为空)) continue # 内容哈希题干选项答案一起算同一道题导两遍会被唯一索引拦下 raw stem json.dumps(options, ensure_asciiFalse) json.dumps(answer, ensure_asciiFalse) session.execute( INSERT IGNORE INTO question(type, stem, options, answer, score, knowledge, difficulty, content_hash) VALUES (%s,%s,%s,%s,%s,%s,%s,%s), (qtype, stem, json.dumps(options, ensure_asciiFalse), json.dumps(answer, ensure_asciiFalse), float(rec.get(分值) or 0), str(rec.get(知识点) or 未分类), int(rec.get(难度) or 2), hashlib.md5(raw.encode()).hexdigest()) ) ok 1 session.commit() return ok, skippedINSERT IGNORE配合唯一索引做去重比在 Python 里查一遍再插入快得多也避免了并发导入时的竞态。返回的skipped一定要打出来给老师看行号加原因让他在导入阶段就把脏数据改掉——等到考生登录发现少了两道题那就不是改数据的问题了。注意难度列别让老师随意填。统一约定 1/2/3 三档其他值一律按 2 处理否则后面按难度分层抽题时会冒出第四层配额算不平。3.2 随机组卷分层抽样加固定种子“随机组卷”四个字听着简单实际要满足三件事题型题数对得上、难度分布别太偏、同一份卷子能复现。第三点最容易被忽略但它是申诉复查的唯一依据——考生说某题有问题你得能把当时那张卷原样还原出来。做法是给每张卷子生成一个seed用random.Random(seed)做抽样不用全局的random。同一 seed 抽出来的题目序列完全一致存库即可复现。import random from collections import defaultdict # 题型 - (题数, 每题分值) SPEC {1: (20, 2.0), 2: (10, 3.0), 3: (10, 1.0), 4: (5, 2.0), 5: (2, 10.0)} # 易 / 中 / 难 的卷面占比 DIFF_RATIO (0.5, 0.35, 0.15) def build_paper(bank, seed): rnd random.Random(seed) # 固定种子事后可原样复现 picked, shortage [], [] for qtype, (count, score) in SPEC.items(): pool [q for q in bank if q[type] qtype] buckets defaultdict(list) for q in pool: buckets[q[difficulty]].append(q) for b in buckets.values(): b.sort(keylambda q: q[id]) # 先定序再洗牌保证跨版本可复现 rnd.shuffle(b) # 按占比算每层要几道余数补到中等难度避免卷面过难或过易 target [round(count * r) for r in DIFF_RATIO] target[1] count - sum(target) chosen [] for diff, need in zip((1, 2, 3), target): take buckets[diff][:need] chosen.extend(take) if len(take) need: shortage.append((qtype, diff, need - len(take))) # 某一层不够时从同题型其他难度补齐宁可难度偏也不能缺题 if len(chosen) count: rest [q for q in pool if q not in chosen] rnd.shuffle(rest) chosen.extend(rest[:count - len(chosen)]) picked.extend((q, score) for q in chosen) return picked, shortageshortage的返回值是给排考人看的如果某场考试的“难题”配额差了 3 道脚本要明确报出来而不是静默从简单题里补。补题逻辑本身要保留因为缺题比难度偏更严重但缺口的数量必须暴露。q not in chosen在大题库上是 O(n²)两三千道题的规模完全够用题量上万就改成用 id 集合判断。3.3 状态机时间永远以服务端为准考试的四个状态之间只允许单向流转所有涉及时间的判断都走服务端NOW()考生端的倒计时只是展示。当前状态触发条件允许操作禁止操作0 未开始创建场次改题、改组卷规则、导名单考生登录作答1 进行中服务端时间 ≥start_at作答、暂存、提前交卷改组卷规则2 已收卷到达end_at或全部交卷只读、导出原始答卷任何写操作3 已判分判分任务跑完导出成绩、复核改客观题答案需走重判前端每秒刷新倒计时但这个倒计时不能用来做交卷判断。考生端把系统时间往回调一小时、改时区、休眠后唤醒都会让本地倒计时和服务端差出几分钟。常见的翻车现象是“考生端显示还有 5 分钟点提交却提示已截止”。正确做法是提交接口带上服务端的截止时间校验前端倒计时只负责提醒每次提交的响应里回传一个server_ts前端拿它修正本地偏差倒计时就不会越走越偏。3.4 客观题判分与主观题人工评阅的边界判分只做客观题单选、判断、填空按字符串比对多选按“漏选给一半分、错选零分”处理这个规则要在考前明确告知考生别在成绩出来后才解释。主观题不自动判分只把答卷快照落库交给人工评阅模块。这里有个容易忽视的细节主观题答案往往很长不能和客观题挤在同一张answer_record的 JSON 字段里频繁更新否则每次自动保存都在写一条几百 KB 的记录收卷高峰期数据库会被这些大字段拖慢。常见做法是主观题单独一张表只在“交卷”这个动作里写一次中途不暂存。4. 断网、断电、掉进程考场三类故障的兜底参数与恢复步骤前面都是顺利路径。考场的真实情况是网线被踢掉、插排被碰掉、服务端进程被某个杀软顺手结束。这三类故障不一定会发生但一旦发生没有兜底就是全机房重考。4.1 答案暂存与断线续考前端不能“答一题提交一次”那就是把数据库当消息队列用收卷前的高峰直接压垮写接口。常见做法是本地暂存加定时批量提交内存里维护一个待提交队列每 15 秒刷一次同时把答案同步写进localStorage断电后重进页面还能捞回来。// 每 15 秒批量提交一次答案同时落 localStorage断电兜底 const pending new Map(); const FLUSH_MS 15000; function markDirty(qid, answer) { pending.set(qid, answer); localStorage.setItem(ans:${PAPER_ID}:${qid}, JSON.stringify(answer)); } async function flush() { if (!pending.size) return; const batch [...pending.entries()].map(([qid, ans]) ({ question_id: qid, answer: ans })); pending.clear(); try { const rsp await fetch(/api/paper/${PAPER_ID}/answers, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ items: batch, client_ts: Date.now() }), }); if (!rsp.ok) throw new Error(rsp.status); const { server_ts } await rsp.json(); syncClock(server_ts); // 用服务端时间校正倒计时 batch.forEach(x localStorage.removeItem(ans:${PAPER_ID}:${x.question_id})); } catch (e) { // 提交失败原样放回队列下一轮重试这里绝对不能清 localStorage batch.forEach(x pending.set(x.question_id, x.answer)); } } setInterval(flush, FLUSH_MS); window.addEventListener(beforeunload, () { navigator.sendBeacon?.(/api/paper/${PAPER_ID}/answers/beacon, JSON.stringify([...pending])); });服务端写入用一条幂等 SQL 收口INSERT INTO answer_record (paper_id, question_id, answer, updated_at) VALUES (%s, %s, %s, NOW()) ON DUPLICATE KEY UPDATE answer VALUES(answer), updated_at VALUES(updated_at);批量提交的粒度值得琢磨。15 秒是经验值60 人机房每人平均每 15 秒改 3 到 5 题折算下来每秒 12 到 20 次写对 MySQL 是小负载间隔拉到 60 秒断电时最多丢一分钟的作答风险偏大。断线后重连前端先拉一次服务端已保存的答案做合并冲突时以“最后修改时间更晚”的一方为准并且把合并结果在页面上提示给考生别让他以为答案被吃掉了。4.2 服务端进程挂了怎么救小机房用 SQLite 的话三个 PRAGMA 必须设对# WAL 提升并发读synchronousFULL 保证每次事务真正落盘 sqlite3 exam.db SQL PRAGMA journal_mode WAL; PRAGMA synchronous FULL; PRAGMA wal_autocheckpoint 200; SQLsynchronousFULL会慢但考试系统里“慢一点”和“丢答案”之间没有选择题可做。用 MySQL 的话把innodb_flush_log_at_trx_commit设成 1别为了性能改成 2 或 0——省下的那点 IOPS 换不来一次丢数据的后悔药。进程被结束之后的重启要有脚本兜底不能靠人手动敲命令。常见的做法是一个每分钟跑一次的守护脚本加定时备份#!/usr/bin/env bash # 每分钟由计划任务调用进程不在就拉起顺带做一次增量备份 PID$(pgrep -f uvicorn app.main:app | head -1) if [ -z $PID ]; then cd /opt/exam nohup venv/bin/uvicorn app.main:app \ --host 0.0.0.0 --port 8000 --workers 4 logs/server.log 21 echo $(date %F %T) restarted logs/watchdog.log fi # 每 10 分钟落一份备份保留最近 12 份 if [ $(( $(date %M) % 10 )) -eq 0 ]; then mysqldump -uexam -pexam exam_db | gzip /data/backup/exam_$(date %H%M).sql.gz ls -t /data/backup/*.sql.gz | tail -n 13 | xargs -r rm -f fi重启后第一件事不是看页面能不能打开而是核对answer_record的条数和最后一次写入时间。如果时间戳停在崩溃前几分钟说明前端那几批提交确实没落库这时候要引导考生重进页面前端会从localStorage把没提交成功的答案补交上来——这套兜底链条要串起来才有意义只做一半等于没做。4.3 收卷高峰的并发压制收卷那两分钟是整个考试里唯一的流量尖峰。60 个人同时点交卷每次交卷触发判分、写成绩、更新状态如果没有约束就是 60 个并发写事务撞在同一批行上。参数建议值作用--workers4与核数一致多了在锁上互相等pool_size20单个 worker 的连接数max_overflow10尖峰时的临时连接nginxkeepalive_timeout65s要大于前端 15s 的提交间隔交卷接口超时30s小于浏览器默认超时保证能拿到错误码单场并发上限100留 40% 余量给断线重连做法是给交卷接口加一层轻量约束判分不放在请求里同步做交卷只负责翻转状态和落快照判分交给后台任务按队列跑。考生点完交卷立刻看到“已交卷”的确认页成绩延后十几秒出来体验反而更好。另一个细节是别在收卷时做全表统计成绩汇总放到考试结束后统一跑收卷那一刻只做单条更新。5. 避坑与排查五个能让整场考试当场翻车的细节下面这几条都是实际排障时反复遇到的现象看着玄学原因都很具体。5.1 考生端倒计时和服务端差了 3 分钟现象考生页面显示还剩 4 分钟点提交提示“考试已结束”或者反过来已经过了截止时间还能继续答。原因倒计时用的是浏览器本地时间。机房里机器长期不校时CMOS 电池老化差几分钟很正常标签页被切到后台时定时器还会被节流累计误差越走越大。解决所有截止判断都放在服务端接口里比对NOW()和end_at前端每次提交的响应里带回server_ts用它算一个偏移量修正显示。开考前把所有考生机的时间同步一遍是治标代码里不信本地时间才是治本。5.2 自动保存把上一题的答案覆盖成空现象考生明明选了 B交卷后该题显示未作答日志里能看到两条针对同一题的写入后一条是空值。原因前端把“这道题没动过”和“这道题被清空”混为一谈整卷定时提交时把未作答的题也一起打包发出去服务端老老实实按空值 upsert。解决只提交 dirty 的题用pending队列维护改动集合确实需要清空时显式传null而不是缺省。服务端加一层防御对选择题的空答案不覆盖已有非空值宁可留旧值也别写空。5.3 组卷抽出来的题和昨天模拟考高度重合现象连着两场考试前 20 道单选里有一多半是同一批题考生一眼认出来。原因抽样只用了全局random没有按 seed 隔离或者题库里某知识点题量太少分层抽样每层都只能取到那几道。解决每张卷子用独立random.Random(seed)组卷前先跑一次配额校验某知识点可用题量低于需求量的 2 倍就报警让老师补题。同一个考生跨场次去重也能加用paper_question的历史记录排除最近 N 场出过的题。5.4 收卷瞬间页面转圈 40 秒现象交卷按钮点下去没反应进度条转很久部分考生刷新后重复交卷。原因交卷接口里同步做了判分加统计一次请求扫了整张answer_record多个请求撞在一起行锁排队。解决交卷只翻转状态、写快照判分挪到后台任务接口加幂等键paper_id sno重复交卷直接返回上次结果。nginx 的proxy_read_timeout调大到 60s避免网关先超时断开而服务端还在跑。5.5 机房还原卡把考生端缓存的答案一起清了现象某几台机器中途重启重进页面后之前答的全没了。原因答案只存在localStorage而还原卡重启会把浏览器数据一起还原同时那几台机器的提交间隔恰好卡在断网窗口里。解决把提交间隔压到 15 秒以内缩短“只存在本地”的窗口服务端在考生重新登录时主动回推已保存的答案机房机器统一关掉“退出清除浏览数据”的策略或者干脆给浏览器指定一个非还原分区的用户数据目录。6. 进阶用法开考前十分钟的自检脚本与两条改造路线终版给的是能跑通的基线真要用进自己的考场流程还差一步把“开考前检查”这件事固化成脚本。人肉检查靠不住尤其是早上八点半谁都紧张。import socket, sqlite3, shutil def check(host, port, db_path, min_questions): problems [] # 1. 端口通不通绑了 127.0.0.1 是最常见的第一次翻车 with socket.socket() as s: s.settimeout(2) if s.connect_ex((host, port)) ! 0: problems.append(f{host}:{port} 连不上) # 2. 数据库能不能写只读挂载、磁盘满都会在这里暴露 try: con sqlite3.connect(db_path, timeout3) con.execute(CREATE TABLE IF NOT EXISTS _probe(id INT)) con.execute(DELETE FROM _probe) con.commit() except Exception as e: return problems [f数据库不可写: {e}] # 3. 题量够不够组卷别等考生登录才发现少题 n con.execute(SELECT COUNT(*) FROM question).fetchone()[0] if n min_questions: problems.append(f题库仅 {n} 题低于下限 {min_questions}) # 4. 磁盘余量日志写满盘是收卷失败的经典原因 free_gb shutil.disk_usage(db_path).free / 2**30 if free_gb 2: problems.append(f磁盘剩余仅 {free_gb:.1f} GB) return problems if __name__ __main__: result check(10.10.10.10, 8000, exam.db, 120) print(\n.join(result) if result else 自检通过)这个脚本的价值不在代码本身而在它把四件事变成了机械动作网络可达、数据库可写、题量充足、磁盘有余。四项里任何一项不通过就别开考。两条改造路线也顺带说一下。第一条是把 SQLite 换成 MySQL改动集中在连接串和那条 upsert 语句INSERT IGNORE换成ON DUPLICATE KEY UPDATE其余逻辑不动适合人数过百或者要多人同时在线评阅的场合。第二条是把组卷规则从代码里挪到配置文件题型配额、难度占比、知识点必选清单都做成可编辑的 JSON排考的人自己改不用每次找你改代码。第二条路走完这套东西才算真正交得出去。血泪经验是有一年模拟考我图省事没跑自检结果服务端绑在 127.0.0.1 上六十台机器全连不上临时改配置重启花了十二分钟那十二分钟里机房里安静得能听见风扇声。从那以后我每次开考前都强制走一遍自检脚本哪怕前一天晚上已经验证过。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?