局域网考试系统实战:断网环境下的高并发交卷与断电恢复方案

发布时间:2026/10/14 3:50:48

局域网考试系统实战:断网环境下的高并发交卷与断电恢复方案 简介这是一套面向教学评估与考试组织场景的局域网考试系统完整源码包适合高校教师、教务管理人员及.NET开发者研究部署或二次定制。系统采用B/S架构覆盖试题库管理、组卷策略、考生信息维护、自动批阅、防作弊与成绩统计等核心模块可在本地网络中完成考试预约、答题与即时评分全流程。压缩包共84个文件约4.08MB以cs源码、resx资源文件、exe可执行程序、msi安装包及ini配置为主另含sln解决方案、mdb数据库与少量图片素材服务端与客户端工程结构清晰便于按模块阅读与调试。目前已有456人学习下载。借助这份含源码的资源读者可快速理解考试系统的整体设计思路掌握组卷算法、成绩管理与安全机制的实现方式并基于现有工程进行功能拓展或教学演示是学习.NET桌面与Web混合开发的实用参考案例。1. 局域网考试系统断网也能跑为什么越来越多考场选它断网、禁外设、屏蔽信号这是很多标准化考场的常规操作。可监考老师手里那台电脑还得实时收卷、判分、统计成绩——网络断了系统怎么活答案就是局域网考试系统。它把服务端和客户端部署在同一网段内不依赖公网考生机通过交换机直连服务器交卷、判分、成绩汇总全在本地闭环完成。适合学校机房、企业内训考核、认证考试中心这类对网络隔离有硬要求的场景。我做过几套类似的部署最大的感受是这东西的难点不在功能多花哨而在断网、断电、高并发交卷这三件事上能不能稳住。下面把我踩过的坑和能直接抄的配置讲清楚。2. 局域网考试系统的架构选型C/S 还是 B/S先想清楚这三件事2.1 两种架构的真实差异不在界面在部署和容灾很多人一上来就问 C/S 和 B/S 哪个好其实这个问题问偏了。局域网考试系统的核心约束是「服务端必须在内网可达」至于客户端是浏览器还是桌面程序取决于你的考场环境。B/S 架构的优势是零安装考生机只要有浏览器输入服务器 IP 就能进考试页。机房机器多、系统版本杂的时候这个优势非常明显。但 B/S 的短板也突出——浏览器兼容性、页面刷新丢答案、断网后 WebSocket 重连这些都是高频翻车点。C/S 架构需要每台考生机装客户端部署工作量大但客户端可以本地缓存答题记录断网瞬间不丢数据恢复后自动续传。如果你的考场对交卷可靠性要求极高C/S 更稳。我的建议是机房统一镜像、能批量装软件的走 C/S机器杂、临时考场、不想装东西的走 B/S但必须做本地缓存兜底。2.2 服务端选型的三个硬指标不管前端选什么服务端要盯住三个指标指标最低要求推荐值说明并发连接数考生数 × 1.5考生数 × 3留出重连和心跳冗余交卷写入延迟 500ms 100ms集中交卷时不能排队断电恢复时间 30s 10s服务重启后数据不丢并发连接数为什么要乘系数因为考生机在考试过程中会不断重连心跳超时、页面刷新、网络抖动实际连接数远大于考生人数。我见过 60 人的考场服务端只配了 80 个连接上限结果开考 10 分钟就有人连不上排查半天才发现是连接池满了。交卷写入延迟是另一个容易被忽略的点。如果每份答卷都走数据库事务集中交卷时数据库会成为瓶颈。常见做法是交卷先写内存队列或本地文件再由后台线程批量落库。2.3 数据存储为什么我最终选了 SQLite 文件双写很多教程一上来就推荐 MySQL但在局域网考试系统这个场景里SQLite 往往更合适。原因很简单考场环境通常只有一台服务器不需要分布式SQLite 零配置、单文件、断电恢复能力强。但纯 SQLite 也有风险——如果服务器硬盘故障数据全丢。所以我的做法是双写答题过程写 SQLite交卷时同时生成一份 JSON 文件存到另一个目录。这样即使数据库损坏也能从 JSON 恢复。import sqlite3 import json import os from datetime import datetime DB_PATH /data/exam/exam.db BACKUP_DIR /data/exam/backup def init_db(): 初始化数据库创建答题记录表 conn sqlite3.connect(DB_PATH) conn.execute( CREATE TABLE IF NOT EXISTS answers ( id INTEGER PRIMARY KEY AUTOINCREMENT, student_id TEXT NOT NULL, question_id TEXT NOT NULL, answer TEXT, submit_time TEXT, status TEXT DEFAULT draft ) ) conn.execute(CREATE INDEX IF NOT EXISTS idx_student ON answers(student_id)) conn.commit() return conn def save_answer(conn, student_id, question_id, answer): 保存单题答案status 为 draft 表示暂存 conn.execute( INSERT OR REPLACE INTO answers (student_id, question_id, answer, submit_time, status) VALUES (?, ?, ?, ?, draft), (student_id, question_id, answer, datetime.now().isoformat()) ) conn.commit() def submit_paper(conn, student_id): 交卷更新状态并写 JSON 备份 conn.execute( UPDATE answers SET statussubmitted WHERE student_id? AND statusdraft, (student_id,) ) conn.commit() # 查出该考生所有答案 rows conn.execute( SELECT question_id, answer FROM answers WHERE student_id? AND statussubmitted, (student_id,) ).fetchall() backup {qid: ans for qid, ans in rows} os.makedirs(BACKUP_DIR, exist_okTrue) path os.path.join(BACKUP_DIR, f{student_id}_{datetime.now().strftime(%Y%m%d%H%M%S)}.json) with open(path, w, encodingutf-8) as f: json.dump(backup, f, ensure_asciiFalse, indent2) return path这段代码的关键点INSERT OR REPLACE保证同一题重复作答时覆盖旧答案不会产生脏数据status字段区分暂存和已交卷避免误判交卷时先更新状态再写 JSON顺序不能反否则 JSON 里可能混入未完成的答案。BACKUP_DIR建议放在不同物理磁盘上如果只有一块盘至少放在不同分区。3. 把服务端跑起来从零到能收卷的最小步骤3.1 环境准备与依赖安装假设你用的是一台干净的 Linux 服务器Ubuntu 20.04 或 CentOS 7 都行先装 Python 3.8 和必要依赖。局域网考试系统不需要公网所以 pip 源可以用内网镜像或者提前把 whl 包拷进去。# 创建专用用户避免用 root 跑服务 sudo useradd -r -s /bin/false examsvc sudo mkdir -p /data/exam/backup sudo chown -R examsvc:examsvc /data/exam # 安装依赖离线环境提前下载 whl pip3 install fastapi uvicorn websockets为什么用 FastAPI 而不是 Flask因为局域网考试系统需要 WebSocket 做实时交卷通知和心跳检测FastAPI 原生支持异步和 WebSocket写起来更顺。uvicorn 作为 ASGI 服务器单进程就能扛几百并发足够中小考场用。3.2 服务端核心接口与 WebSocket 心跳服务端至少要有三个接口获取试卷、暂存答案、交卷。WebSocket 用来做两件事服务端主动通知考生「还有 5 分钟」以及客户端定时发心跳证明自己还在线。from fastapi import FastAPI, WebSocket, WebSocketDisconnect from pydantic import BaseModel import asyncio app FastAPI() conn init_db() class AnswerItem(BaseModel): student_id: str question_id: str answer: str app.post(/api/save) async def save(item: AnswerItem): 暂存单题答案前端每次切题或每 30 秒调用一次 save_answer(conn, item.student_id, item.question_id, item.answer) return {ok: True} app.post(/api/submit/{student_id}) async def submit(student_id: str): 交卷返回备份文件路径 path submit_paper(conn, student_id) return {ok: True, backup: path} app.websocket(/ws/{student_id}) async def ws_endpoint(ws: WebSocket, student_id: str): WebSocket 心跳与通知通道 await ws.accept() try: while True: # 等待客户端消息超时 30 秒视为断线 data await asyncio.wait_for(ws.receive_text(), timeout30) if data ping: await ws.send_text(pong) except asyncio.TimeoutError: # 超时未收到心跳记录断线但不立即踢出 print(f{student_id} heartbeat timeout) except WebSocketDisconnect: print(f{student_id} disconnected)/api/save设计成幂等接口前端可以放心重试。/api/submit的路径参数是学号交卷后返回备份路径方便排查。WebSocket 的 30 秒超时是经验值——太短会误判考生思考时不会一直动鼠标太长则断线发现不及时。实际部署时建议把超时时间做成配置项根据考场网络质量调整。3.3 客户端答题页的本地缓存策略B/S 架构最大的风险是页面刷新或浏览器崩溃导致答案丢失。解决办法是在前端用 localStorage 做本地缓存每次答案变更先写本地再异步发到服务端。// 答题页本地缓存先存本地再发服务端 const CACHE_KEY exam_answers_ studentId; function saveAnswer(questionId, answer) { // 1. 写本地缓存 let cache JSON.parse(localStorage.getItem(CACHE_KEY) || {}); cache[questionId] answer; localStorage.setItem(CACHE_KEY, JSON.stringify(cache)); // 2. 异步发服务端失败不阻塞 fetch(/api/save, { method: POST, headers: {Content-Type: application/json}, body: JSON.stringify({ student_id: studentId, question_id: questionId, answer: answer }) }).catch(err { console.warn(服务端暂存失败本地已保留, err); }); } // 页面加载时恢复本地缓存 function restoreCache() { const cache JSON.parse(localStorage.getItem(CACHE_KEY) || {}); for (const [qid, ans] of Object.entries(cache)) { const el document.querySelector([data-qid${qid}]); if (el) el.value ans; } }这段代码的核心逻辑是「本地优先」不管服务端通不通答案先落在浏览器里。catch里只打警告不弹窗避免干扰考生。restoreCache在页面加载时调用即使刷新也能恢复。注意 localStorage 有 5MB 上限纯文本答案完全够用但如果试卷里有图片附件需要改用 IndexedDB。4. 高并发交卷与断电恢复三个必须压测的场景4.1 集中交卷时数据库写入排队怎么破考试结束前 5 分钟是交卷高峰如果 60 个人同时点交卷每人 50 道题瞬间就是 3000 次写入。SQLite 默认的串行写入会排队导致部分考生看到「交卷中」转圈很久。解决办法是交卷走队列接口收到交卷请求后只把学号推入内存队列并立即返回「已收到」后台协程逐个处理落库。import asyncio from collections import deque submit_queue deque() queue_lock asyncio.Lock() app.post(/api/submit/{student_id}) async def submit(student_id: str): 交卷请求只入队立即返回 async with queue_lock: submit_queue.append(student_id) return {ok: True, msg: 已收到正在处理} async def submit_worker(): 后台协程逐个处理交卷队列 while True: async with queue_lock: if submit_queue: sid submit_queue.popleft() else: sid None if sid: try: submit_paper(conn, sid) print(f{sid} 交卷完成) except Exception as e: print(f{sid} 交卷失败: {e}) # 失败重新入队最多重试 3 次 async with queue_lock: submit_queue.append(sid) else: await asyncio.sleep(0.1) app.on_event(startup) async def startup(): asyncio.create_task(submit_worker())队列方案的关键是「先响应再处理」考生端不会卡。submit_worker每次只处理一个避免数据库锁竞争。失败重入队保证了可靠性但要注意加最大重试次数否则死循环。实际压测时60 人集中交卷从原来的 12 秒降到 2 秒以内。4.2 服务端断电后如何恢复到断点断电是考场最怕的事。如果服务端没有做持久化重启后所有暂存答案全丢。我的做法是除了 SQLite 的 WAL 模式再加一层「操作日志」。import logging from logging.handlers import RotatingFileHandler # 操作日志每次暂存和交卷都记一行 op_logger logging.getLogger(exam_op) op_logger.setLevel(logging.INFO) handler RotatingFileHandler(/data/exam/op.log, maxBytes10*1024*1024, backupCount5) handler.setFormatter(logging.Formatter(%(asctime)s %(message)s)) op_logger.addHandler(handler) def save_answer_with_log(conn, student_id, question_id, answer): 带操作日志的暂存 save_answer(conn, student_id, question_id, answer) op_logger.info(fSAVE {student_id} {question_id} {answer[:50]})WAL 模式让 SQLite 在写入时不会阻塞读断电后能自动恢复到最后一次检查点。操作日志则是最后一道保险——如果数据库文件损坏可以用日志重放恢复。日志按 10MB 滚动保留 5 个备份足够一场考试用。4.3 压测脚本模拟 60 人同时交卷上线前一定要压测。用 Python 的aiohttp写个简单脚本模拟 60 个考生同时交卷。import aiohttp import asyncio import time async def mock_submit(session, student_id): 模拟一个考生交卷 url fhttp://127.0.0.1:8000/api/submit/{student_id} start time.time() async with session.post(url) as resp: await resp.json() return time.time() - start async def main(): async with aiohttp.ClientSession() as session: tasks [mock_submit(session, fstu_{i:03d}) for i in range(60)] results await asyncio.gather(*tasks) print(f60 人交卷平均耗时 {sum(results)/len(results):.3f}s最大 {max(results):.3f}s) asyncio.run(main())压测时重点看三个数平均耗时、最大耗时、服务端 CPU 和内存。如果最大耗时超过 1 秒说明队列处理不过来需要调大 worker 数量或优化数据库写入。我一般会把submit_worker改成多个协程并行但要注意 SQLite 的写锁并行度不要超过 4。5. 局域网考试系统避坑这 5 个问题我全踩过5.1 考生机 IP 冲突导致部分人连不上现象开考后陆续有考生反馈「无法连接服务器」但同一排的其他机器正常。原因机房 DHCP 分配混乱或者有人手动改了 IP导致地址冲突。解决考前用静态 IP 绑定 MAC或者在服务端加一层连接来源校验发现同一 IP 多个学号登录时告警。更彻底的做法是让考生机通过主机名访问而不是 IP。5.2 浏览器自动更新导致答题页白屏现象考试中途部分考生浏览器自动更新页面刷新后白屏。原因B/S 架构依赖浏览器后台更新会强制重启标签页。解决考前关闭浏览器自动更新或者用 C/S 客户端彻底规避。如果只能用 B/S在页面里加beforeunload提示并且本地缓存必须可靠。5.3 交卷时提示「数据库锁定」现象集中交卷时部分考生看到「database is locked」错误。原因SQLite 默认的写锁是排他的多个写入同时来就会锁。解决开启 WAL 模式并且把交卷写入串行化就是前面说的队列方案。WAL 模式下读不阻塞写但写与写之间仍然互斥所以队列不能省。5.4 服务端时间不准导致倒计时错乱现象考生端倒计时和服务端不一致有人提前被强制交卷。原因服务器没配 NTP时间漂移或者客户端用本地时间算倒计时。解决倒计时以服务端时间为准客户端定期同步。服务端开 NTP考场内网如果有时间服务器就指向它没有就手动校准。5.5 断电后重启部分考生答案丢失现象断电重启后有考生反馈最后几道题答案没了。原因暂存接口是异步的断电时内存里还有未落库的数据。解决暂存接口改成同步写 SQLite虽然慢一点但可靠。或者用操作日志重放把断电前最后几秒的操作补回来。我现在的做法是暂存同步写、交卷走队列兼顾可靠和性能。6. 进阶技巧用快照回滚把故障恢复压到 10 秒内前面讲的都是「怎么不出事」这一章讲「出了事怎么最快恢复」。局域网考试系统最怕的不是故障而是故障后恢复太慢影响考试进度。我的做法是给服务端加一层「快照回滚」每 30 秒把 SQLite 数据库文件复制一份到内存盘tmpfs同时记录当前时间戳。如果服务崩溃重启后先从内存盘恢复最近快照再用操作日志重放快照之后的变更。# 创建内存盘挂载点重启后消失正好符合快照语义 sudo mkdir -p /mnt/exam_snap sudo mount -t tmpfs -o size512M tmpfs /mnt/exam_snap # 快照脚本每 30 秒复制一次数据库 while true; do cp /data/exam/exam.db /mnt/exam_snap/exam_snap.db date %s /mnt/exam_snap/snap_ts sleep 30 done恢复逻辑服务启动时检查/mnt/exam_snap/exam_snap.db是否存在存在则用它覆盖/data/exam/exam.db然后从snap_ts记录的时间点开始重放op.log。这样即使数据库文件损坏最多丢 30 秒数据。这个方案的关键参数是快照间隔。30 秒是权衡结果太短会增加磁盘 IO太长丢数据多。512MB 内存盘对纯文本答案来说绰绰有余一场 60 人的考试数据库通常不到 10MB。还有一个细节快照脚本和服务端要分开进程避免服务崩溃时快照也停了。我用 systemd 把快照脚本做成独立服务Restartalways挂了自动拉起。最后说个血泪教训千万别在考试进行中做数据库迁移或表结构变更。我有一次想优化索引结果锁表 3 分钟全场考生卡住。任何变更都要在考前完成考试期间只读不写结构。希望帮到你。本文还有配套的精品资源点击获取
延伸阅读

更多相关文章

2026/10/14 3:50:48

@Lazy 关键字解决循环依赖

循环依赖报错,为什么给构造器参数加个 Lazy 就好了? 朋友的代码炸在启动阶段,日志第一行是这个: The dependencies of some of the beans in the application context form a cycle:┌─────┐ | orderService defined in fi…

2026/10/14 3:45:47

重症ICU数据库四大核心坑点与宽表构建实战指南

简介:本资源是面向医学信息学研究者、临床科研人员及医疗AI开发者的重症医学数据库选型与接入指南项目源码包,解决多源异构重症数据获取门槛高、申请流程不清晰、技术适配难等实际问题。压缩包共3个文件(6KB),含HTML格…

2026/10/14 3:45:47

GNSS与大地水准面模型的高程测量技术实践

1. 项目背景与核心价值高程测量一直是测绘工程中的基础性工作,传统水准测量虽然精度高,但存在作业效率低、成本高的痛点。我在参与西南某山区铁路勘测项目时,首次尝试将GNSS观测数据与大地水准面模型结合,实现了测区内85%区域的高…

2026/10/14 4:50:50

Pandas生态贯通:NumPy、Matplotlib与Scikit-learn数据流程实战

很多人学数据分析,喜欢一个库一个库地啃:先学NumPy,再学Pandas,又学Matplotlib,最后还报了个课学Scikit-learn。单看每个库的教程都能看懂,可真到项目里,一份CSV文件摆在面前,要清洗…

2026/10/14 4:50:50

本地跑27B大模型实测:159 tok/s如何实现与AI编程体验

最近我在折腾本地大模型,把 Qwen3.8-27B 跑到了 159 tok/s。先说结论:这个速度已经接近甚至超过了部分云端 API 的响应体验,但“能不能替代云端”这个问题,答案没这么简单。这篇帖子就围绕这个标题,聊聊我实测的硬件配…

2026/10/14 4:50:50

NetApp FAS8300企业级存储安装与ONTAP初始化实战指南

简介:本资源是NetApp FAS8300企业级存储系统的官方级安装与配置实践指南,面向存储运维工程师、系统集成实施人员及中高级SAN/NAS架构学习者,聚焦真实交付场景中的初始化部署、高可用集群构建与多协议统一管理等核心任务。压缩包为单文件PDF文…

2026/10/14 4:50:50

Qt表格与Excel互转实战:QXlsx方案选型、导出导入与避坑指南

1. 从一次数据交付返工说起:为什么表格与Excel的互转值得认真对待做过桌面端数据管理工具的人,大概都遇到过这样的场景:用户用你的软件录了一整天的数据,临走前问一句“能导出成Excel吗,我要发给同事汇总”。如果你当时…

2026/10/14 4:45:50

LLM Agent可观测性实战:Tracing、Evals与成本追踪一体化落地

1. 这不是“又一个AI监控工具”,而是LLM应用落地的生存指南你有没有遇到过这样的情况:模型在测试环境里回答得头头是道,一上线就频繁“胡言乱语”;用户投诉说“昨天还能正常用,今天突然卡住半天没反应”;运…

2026/10/13 5:19:27

Python调用Gemini Structured Outputs实现工单路由门禁

客服工单最怕的不是模型“答错一句话”,而是它给出一段看起来合理的说明,程序却从中猜错优先级。通俗做法是:要求模型只交 JSON(JavaScript Object Notation,轻量数据格式),再让代码验证它。Gem…

2026/10/13 17:09:18

Spring Boot超市进销存系统毕设实战:从需求拆解到答辩通关

最近带的一个学生项目组里,有A同学跑来问我:选什么毕设题目最稳妥,既能让评审老师觉得工作量够,又不会在答辩时被问到语无伦次。我第一反应就是推荐基于Spring Boot的超市仓库管理系统——也就是超市进销存系统。这个题目乍一看平…

2026/10/14 2:32:55

Flutter StatefulWidget 生命周期核心解析

很多刚开始接触 Flutter 的朋友,在看完一堆“Hello World”和基础组件之后,大概率都会撞上同一堵墙:StatefulWidget 里那堆 initState、build、dispose 方法,到底什么时候被调用?为什么顺序是那样?在里面到…

2026/10/14 0:00:33

DFS算法解决飞机降落调度问题实战

1. 项目背景与核心思路飞机降落问题是一个经典的调度优化场景,也是算法竞赛和面试中的高频题目。这个题目要求我们合理安排多架飞机的降落顺序和时间,确保所有飞机都能安全着陆而不发生冲突。在实际航空管制中,这类问题直接关系到飞行安全和机…

2026/10/14 0:00:33

macOS/iOS高精度计时:mach_absolute_time原理与实战

1. 项目概述:为什么mach_absolute_time是 macOS/iOS 时间测量的“黄金标准”在 macOS 和 iOS 开发中,我见过太多人用CFAbsoluteTimeGetCurrent()或NSDate.timeIntervalSinceReferenceDate做性能打点,结果在高精度场景下被系统时钟跳变、NTP 同…

2026/10/14 0:00:33

SCORM课件演示与测试全解析:从API通信到自动化验证

简介:这是一套遵循SCORM标准的交互式课件演示与测试资源,面向E-Learning课程开发者、LMS平台测试人员及在线教学从业者,用于验证课件在不同学习管理系统中的识别、传递与跟踪能力。压缩包共28个文件,约12.07MB,以swf动…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑