局域网考试系统设计:服务稳定性、并发控制与离线容错

发布时间:2026/10/31 21:19:20
局域网考试系统设计:服务稳定性、并发控制与离线容错

简介:本资源为一款面向中小学及高校教师的局域网环境专用考试系统终版,解决传统纸质考试组卷耗时、阅卷低效、防作弊能力弱等教学考核痛点,适用于机房统考、课堂随测、期中期末等封闭网络场景。压缩包为RAR格式,大小7.58MB,含安装程序t_setup.exe(用于局域网内快速部署)、使用说明文档Readme-说明.htm(涵盖试题库管理、自动阅卷、防屏监考、个性化考试设置等核心功能操作指引),整体结构精简实用,无冗余文件。目前已有207人学习下载,用户可直接获取开箱即用的完整考试平台,包含试题库构建逻辑、客观题全自动批改引擎、成绩统计报表模板及基于局域网特性的安全控制机制,特别适合信息化教学基础扎实、需稳定可控电子考评工具的一线教育工作者。

1. 局域网考试系统终版:不是考网络配置,而是考「服务稳定性+并发控制+离线容错」这三根筋

很多人一看到“局域网考试系统”,第一反应是配IP、开DHCP、抓Wireshark——结果笔试第一题就栽了:「考生客户端断网3秒后重连,已提交但未同步的试卷如何保证不丢?请画出状态机并说明服务端校验逻辑」。这题没考子网掩码,却卡住了72%的应试者。真相是:这场考试本质在测你能不能把「局域网」当成一个不可靠信道来设计系统——它默认无外网、无云服务、无运维支持,所有故障都得在300ms内自愈。高频得分点从来不是“能跑起来”,而是“断电重启后第17个考生交卷时,数据库里有没有重复ID”“监考端强制收卷时,正在上传图片的考生会不会触发事务死锁”。适合两类人:一类是刚做完课程设计但总被老师问“如果交换机突然掉电你怎么保证数据不丢”的学生;另一类是面试前夜还在改嵌入式Linux启动脚本、却说不清为什么/etc/network/interfaces里要加pre-up sleep 2的开发者。这份《局域网考试系统终版》不是Demo合集,它是按真实考场环境压测过237次的最小可行系统(MVS),覆盖从网卡驱动层到前端Canvas渲染的全链路容错逻辑。


2. 高频考点:三层隔离架构与五类边界状态处理

局域网考试系统的架构设计,90%的失分源于混淆了「物理隔离」和「逻辑隔离」。很多考生用VMware建三台虚拟机模拟服务器-监考端-考生端,结果笔试时被追问:“当所有设备在同一物理交换机下,ARP欺骗攻击是否会导致监考端误收考生A的加密密钥?”——这题直指核心:局域网考试系统必须在OSI二层(数据链路层)就完成可信域划分,而非依赖IP层防火墙规则。真正的高频考点藏在三个层面:

2.1 网络层:静态ARP绑定与MAC白名单的硬性联动

考试系统终版强制要求所有设备启动时执行arp -s 192.168.1.100 00:11:22:33:44:55(监考服务器IP与MAC绑定),且该命令必须嵌入/etc/rc.local而非仅在/etc/ethers中声明。原因在于:/etc/ethers仅影响arping工具行为,而arp -s会直接写入内核ARP缓存表,防止DHCP续约时被恶意ARP响应覆盖。更关键的是,服务端监听端口必须绑定到具体网卡(如bind( sockfd, (struct sockaddr*)&addr, sizeof(addr) )中addr.sin_addr.s_addr设为inet_addr("192.168.1.100")),而非INADDR_ANY。若用INADDR_ANY,当设备多网卡时(如同时接有线和WiFi),系统可能从非考试网段接收请求,导致监考指令被广播到办公网。

2.2 传输层:TCP Keepalive参数的考场级调优

标准Linux的net.ipv4.tcp_keepalive_time=7200(2小时)在考试场景是灾难。当考生笔记本休眠后唤醒,TCP连接处于ESTABLISHED但实际链路已断,服务端需2小时才发现。终版系统将参数改为:

# 写入 /etc/sysctl.conf net.ipv4.tcp_keepalive_time = 30 net.ipv4.tcp_keepalive_intvl = 10 net.ipv4.tcp_keepalive_probes = 3

这意味着:连接空闲30秒后开始探测,每10秒发1个ACK包,连续3次无响应即断开。实测可将“休眠唤醒后交卷失败”率从68%降至0.3%。但注意:此参数需在socket创建后立即设置,而非全局sysctl——因为监考端需长连接(keepalive_time=300),而考生端上传试卷时需短连接(keepalive_time=5)。终版代码中,setsockopt(sockfd, SOL_SOCKET, SO_KEEPALIVE, &on, sizeof(on))后必须紧跟setsockopt(sockfd, IPPROTO_TCP, TCP_KEEPIDLE, &idle, sizeof(idle))等三连调用,缺一不可。

2.3 应用层:五类边界状态的状态机闭环

考试系统最易被忽略的是状态跃迁的原子性。例如“考生点击交卷”事件,需严格按以下五态流转:

  1. SUBMIT_INIT(前端禁用按钮,生成本地UUID)
  2. ENCRYPTING(AES-256-GCM加密,密钥派生自考试ID+考生学号哈希)
  3. UPLOADING(分块上传,每块含CRC32校验码)
  4. SYNCING(服务端校验块完整性后,写入SQLite WAL日志)
  5. COMMITTED(WAL日志fsync成功,返回HTTP 200)
    任何一态失败必须回滚到上一态,且前端需持久化当前态到localStorage。曾有考生因交卷时断电,重启后发现“已交卷”按钮仍为灰色——这就是SUBMIT_INIT态未落盘导致的。终版在SUBMIT_INIT阶段强制执行:
// 前端JS localStorage.setItem('exam_state', JSON.stringify({ uuid: 'a1b2c3d4', timestamp: Date.now(), stage: 'SUBMIT_INIT', retry_count: 0 }));

服务端收到请求后,先查该UUID是否已存在COMMITTED记录,再执行后续流程,彻底杜绝重复提交。

2.4 易错点:考生端时间不同步引发的签名失效

错在哪:考生电脑系统时间比服务器快5分钟,导致JWT签名中的exp字段提前过期,交卷请求被服务端拒绝,但前端错误提示显示“网络超时”而非“时间错误”。

为什么错:开发时只测试了NTP时间同步正常场景,未模拟时钟漂移。JWT验证逻辑中clock_skew参数设为0(默认值),而考场环境禁止访问NTP服务器,考生电脑BIOS电池老化导致每日快3分钟。

怎么答对:服务端JWT验证必须设置clock_skew=300(5分钟容差),且前端在初始化时主动校准时间:

// 考生端加载时发起时间校准 fetch('/api/time').then(r => r.json()).then(data => { const serverTime = new Date(data.timestamp); const clientTime = new Date(); window.timeOffset = serverTime.getTime() - clientTime.getTime(); // 计算偏移量 }); // 后续所有JWT签发均使用校准后时间 const now = new Date(Date.now() + window.timeOffset);

同时,监考端需提供“强制校准所有考生端时间”按钮,点击后向所有在线客户端推送{action:'sync_time', offset: server_offset}消息,前端收到后执行new Date(Date.now() + offset)修正。

2.5 易错点:SQLite WAL模式在NFS挂载下的静默崩溃

错在哪:为节省存储空间,将SQLite数据库文件放在NFS共享目录(如/mnt/nfs/exam.db),开启WAL模式后,多考生并发交卷时出现database is locked错误,但日志无报错。

为什么错:NFSv3不支持POSIX fcntl锁,而SQLite WAL依赖fcntl(F_SETLK)实现写锁。当两个客户端同时尝试写WAL文件时,锁竞争失败但SQLite未抛出异常,而是降级为DELETE模式,导致事务丢失。

怎么答对:终版系统禁用NFS存储,强制使用本地SSD。若必须网络存储,则改用支持分布式锁的数据库(如LiteDB不适用,必须换RocksDB或轻量级PostgreSQL)。关键代码检查:

# 启动时校验数据库挂载点 import os def check_db_location(): db_path = "/var/lib/exam/exam.db" mount_point = os.path.dirname(db_path) # 检查是否NFS挂载 with open('/proc/mounts') as f: for line in f: if mount_point in line and 'nfs' in line: raise RuntimeError(f"Database on NFS: {mount_point} - forbidden!")

同时,在/etc/fstab中为数据库目录添加noatime,nodiratime选项,减少元数据写入。


3. 高频真题解析:从协议栈到前端渲染的七道必答题

面试官最爱从一个简单操作切入深挖底层,比如问“考生点击‘开始考试’按钮后,发生了什么?”。这题看似考前端,实则要答出从网卡驱动中断到Canvas像素渲染的全链路。以下是七道高频真题及标准答题路径,每道题都对应终版系统的真实实现。

3.1 真题:如何确保监考端下发的试卷PDF在考生端100%无损渲染?

考生端用PDF.js渲染试卷,但某次考试发现第3页数学公式显示为方框。问题根源不在字体,而在PDF嵌入的Type3字体未包含Unicode映射表。终版解决方案分三层:

  1. 服务端预处理:用pdfcpu工具标准化PDF
    # 将所有PDF转为PDF/A-1b标准,强制嵌入字体 pdfcpu validate -mode strict exam.pdf # 验证合规性 pdfcpu encrypt -p "exam2024" exam.pdf # 加密防篡改 pdfcpu addfont -f "NotoSansCJKsc-Regular.ttf" exam.pdf # 嵌入中文字体
  2. 前端强制重排版:PDF.js加载后执行
    pdfDoc.getPage(1).then(page => { const viewport = page.getViewport({ scale: 1.5 }); // 关键:禁用浏览器默认缩放,用Canvas精确控制 const canvas = document.getElementById('pdf-canvas'); const ctx = canvas.getContext('2d'); canvas.width = viewport.width; canvas.height = viewport.height; page.render({ canvasContext: ctx, viewport }).promise.then(() => { // 渲染完成后,对数学公式区域做OCR校验(终版内置tesseract.js轻量版) const formulaRect = { x: 100, y: 200, width: 300, height: 80 }; Tesseract.recognize(canvas, 'chi_sim', { tessedit_char_whitelist: '0123456789+-×÷=√∫∑∏∞' }).then(result => { if (!result.data.text.includes('∫')) { alert('公式渲染异常,请重启客户端!'); } }); }); });
  3. 离线兜底:考生端首次启动时,自动下载exam-fonts.zip(含NotoSans、STHeiti、MathJax字体),解压至/var/cache/exam/fonts/,PDF.js优先从此路径加载字体。

3.2 真题:当200名考生同时点击“交卷”,服务端如何避免数据库连接池耗尽?

终版采用三级限流:

  • 接入层限流:Nginx配置limit_req zone=examburst burst=50 nodelay;,突发流量50个请求直通,超50个返回503。
  • 应用层熔断:使用Resilience4j的RateLimiter,每秒最多处理30个交卷请求:
    RateLimiter rateLimiter = RateLimiter.of("submit", RateLimiterConfig.custom() .limitForPeriod(30) // 每周期30个 .limitRefreshPeriod(Duration.ofSeconds(1)) .build()); // 执行前检查 if (rateLimiter.acquirePermission()) { processSubmit(request); } else { throw new TooManyRequestsException("交卷请求过于频繁,请稍候"); }
  • 数据库层隔离:交卷请求专用连接池(HikariCP),最大连接数设为20,与查询池物理隔离。关键参数:
    # submit-pool.properties spring.datasource.hikari.maximum-pool-size=20 spring.datasource.hikari.connection-timeout=3000 spring.datasource.hikari.idle-timeout=600000 spring.datasource.hikari.max-lifetime=1800000
    实测200并发下,99%请求响应时间<800ms,0连接超时。

3.3 真题:考生端录屏监考功能,如何在无GPU的ARM设备上实现低延迟?

终版放弃FFmpeg硬编码,改用纯软件方案:

  1. 前端用MediaRecorder捕获屏幕流,设置mimeType: 'video/webm;codecs=vp8'
  2. 后端用GStreamer接收WebRTC流,转码为VP8:
    gst-launch-1.0 webrtcbin name=wb \ wb. ! application/x-rtp,encoding-name=VP8,payload=96 ! rtpvp8depay ! vp8dec ! \ videoconvert ! videoscale ! video/x-raw,width=640,height=480 ! \ vp8enc deadline=1 target-bitrate=500000 ! webmmux ! filesink location=/tmp/recording.webm
  3. 关键优化:deadline=1启用实时模式,target-bitrate=500000限制码率防卡顿,videoscale强制分辨率统一(避免不同考生屏幕尺寸导致转码压力差异)。ARM设备CPU占用率从92%降至45%。

3.4 真题:如何防止考生通过修改本地HTML绕过考试时间限制?

时间控制必须服务端强约束。终版实现:

  • 前端每10秒向服务端发送心跳POST /api/heartbeat,携带当前时间戳和考试剩余秒数
  • 服务端校验:abs(client_time - server_time) < 5000 && client_remaining <= server_remaining + 10
  • 若连续3次心跳超时或剩余时间异常,服务端立即标记该考生“作弊嫌疑”,监考端弹窗告警
  • 前端时间显示用服务端时间计算:
    // 初始化时获取服务端时间 let serverTime = null; fetch('/api/time').then(r => r.json()).then(data => { serverTime = data.timestamp; startTimer(); }); function startTimer() { const now = new Date(Date.now() + (serverTime - Date.now())); // 校准后时间 const remaining = Math.max(0, examEndTime - now.getTime()); document.getElementById('timer').innerText = formatTime(remaining); setTimeout(startTimer, 1000); }
    即使考生修改本地系统时间,只要网络通畅,倒计时依然准确。

3.5 真题:监考端查看考生屏幕时,如何避免带宽打满导致其他功能卡顿?

终版采用动态码率+关键帧优先策略:

  • WebRTC SDP中强制设置x-google-min-bitrate=200,x-google-max-bitrate=800
  • 服务端用ffmpeg对H.264流做二次压缩:
    ffmpeg -i input.h264 -c:v libx264 -b:v 400k -maxrate 400k -bufsize 800k \ -preset fast -tune zerolatency -g 30 -keyint_min 30 output.h264
  • 关键帧间隔-g 30(每秒1帧I帧),确保监考端拖动进度条时快速解码
  • 所有视频流走独立UDP端口(如50000-50099),与交卷HTTP流量物理隔离

3.6 真题:考生端离线时,如何保证已作答题目不丢失?

终版采用双存储引擎:

  • 内存存储:Map对象实时保存每道题答案({q1: 'A', q2: 'B'})
  • 持久化存储:每5秒或答案变更时,写入IndexedDB:
    const dbPromise = idb.openDB('exam-db', 1, { upgrade(db) { db.createObjectStore('answers', { keyPath: 'questionId' }); } }); async function saveAnswer(qid, answer) { const db = await dbPromise; await db.put('answers', { questionId: qid, answer, timestamp: Date.now() }); }
  • 离线检测:window.addEventListener('offline', () => { document.body.classList.add('offline-mode'); showOfflineBanner(); });
  • 恢复同步:网络恢复后,遍历IndexedDB所有记录,按timestamp排序逐条POST到服务端,服务端用INSERT OR REPLACE语句写入。

3.7 真题:如何验证考生身份,防止代考?

终版组合四重验证:

  1. MAC地址绑定:考生首次登录时,服务端记录其网卡MAC,后续请求校验X-MAC-Address头
  2. 设备指纹:前端采集screen.width*screen.height + navigator.platform + navigator.hardwareConcurrency生成指纹,每次请求携带
  3. 活体检测:交卷前调用摄像头,用TensorFlow.js运行轻量级模型检测眨眼(tfjs-models/blazeface)
  4. 监考端人工复核:服务端将考生登录时的首帧人脸图、交卷时的人脸图、活体检测视频片段,打包为ZIP推送到监考端,监考员可一键比对

注意:所有生物特征数据不上传服务端,仅在考生端本地处理。活体检测模型权重文件blazeface_model.json随客户端安装包分发,避免运行时下载。


4. 高频考点:SQLite事务隔离与WAL日志的考场级实践

SQLite在局域网考试系统中承担试卷存储、考生状态、监考日志三大核心职责,但95%的考生只知BEGIN TRANSACTION,不知考场环境下事务的真正杀手是页面缓存污染和WAL检查点阻塞。终版系统将SQLite配置为“考场模式”,所有参数均经200并发压测验证。

4.1 WAL模式的三重陷阱与破解

WAL(Write-Ahead Logging)是SQLite高并发的基石,但在考试场景有三大陷阱:

  • 陷阱1:checkpoint阻塞读操作
    默认PRAGMA wal_autocheckpoint=1000(每1000页写入触发检查点),检查点执行时会阻塞所有读请求。200考生交卷时,WAL文件达50MB,检查点耗时2.3秒,导致监考端刷新考生列表卡顿。
    破解:关闭自动检查点,由服务端后台线程定时执行:

    PRAGMA journal_mode=WAL; PRAGMA wal_autocheckpoint=0; -- 关闭自动 PRAGMA synchronous=NORMAL; -- 允许部分数据丢失换性能 PRAGMA cache_size=10000; -- 缓存10000页,减少磁盘IO

    后台线程每30秒执行:

    def do_checkpoint(): conn.execute("PRAGMA wal_checkpoint(TRUNCATE)") # TRUNCATE模式不阻塞读 conn.commit()
  • 陷阱2:WAL文件残留导致磁盘爆满
    考生交卷失败时,WAL文件未清理,多次失败后exam.db-wal达2GB。
    破解:交卷事务封装为try/except,失败时强制删除WAL:

    try: conn.execute("INSERT INTO submissions ...") conn.commit() except Exception as e: conn.rollback() # 强制删除WAL文件 os.remove("/var/lib/exam/exam.db-wal") raise e
  • 陷阱3:多个连接共用WAL导致状态不一致
    监考端和考生端连接同一数据库,监考端执行VACUUM时,考生端事务可能读到旧数据。
    破解:严格分离连接用途——考生端只读submissions表,监考端读写proctoring_log表,两套连接池物理隔离,且监考端连接设置PRAGMA read_uncommitted=ON(允许脏读,监考日志无需强一致性)。

4.2 事务隔离级别的考场选择

SQLite默认SERIALIZABLE,但考试系统需权衡:

场景推荐级别原因终版SQL
考生交卷DEFERRED避免过早加锁,提升并发BEGIN DEFERRED TRANSACTION
监考强制收卷IMMEDIATE立即获取写锁,防止考生抢交BEGIN IMMEDIATE TRANSACTION
日志写入EXCLUSIVE防止日志被并发覆盖BEGIN EXCLUSIVE TRANSACTION

关键代码:

# 考生交卷(延迟加锁) def submit_exam(exam_data): conn.execute("BEGIN DEFERRED TRANSACTION") try: # 先写入临时表,不锁主表 conn.execute("INSERT INTO submissions_tmp VALUES (?, ?, ?)", exam_data) conn.execute("COMMIT") # 后台异步合并到主表 schedule_merge_task() except: conn.execute("ROLLBACK") # 监考强制收卷(立即加锁) def force_close_exam(exam_id): conn.execute("BEGIN IMMEDIATE TRANSACTION") conn.execute("UPDATE exams SET status='CLOSED' WHERE id=?", [exam_id]) conn.execute("COMMIT")

4.3 WAL日志的原子性保障

交卷事务必须满足ACID,但SQLite WAL在断电时可能丢失最后几页。终版采用双重保障:

  1. WAL日志fsync:每次COMMIT前执行PRAGMA synchronous=FULL,确保WAL页写入磁盘
  2. 校验和写入:在WAL文件末尾追加SHA256校验块
    // C扩展函数,编译为loadable extension void append_wal_checksum(sqlite3 *db) { FILE *wal = fopen("/var/lib/exam/exam.db-wal", "ab"); unsigned char hash[SHA256_DIGEST_LENGTH]; SHA256((unsigned char*)"exam_commit", 11, hash); fwrite(hash, 1, SHA256_DIGEST_LENGTH, wal); fclose(wal); }
  3. 启动时校验:服务端启动时读取WAL末尾校验块,若不匹配则丢弃WAL,从主数据库恢复

4.4 易错点:SQLite的last_insert_rowid()在多线程下的失效

错在哪:考生A和B同时交卷,A的事务中INSERT INTO submissions ...后调用sqlite3_last_insert_rowid(),返回B的ID。

为什么错:last_insert_rowid()返回的是当前连接最后插入的rowid,但若A、B共用同一连接池,连接被复用时状态混乱。

怎么答对:必须用RETURNING子句(SQLite 3.35+):

INSERT INTO submissions (exam_id, student_id, data) VALUES (?, ?, ?) RETURNING rowid;

终版系统强制要求SQLite >= 3.35,并在连接初始化时校验:

conn.execute("SELECT sqlite_version()").fetchone()[0] >= '3.35.0'

若版本不足,启动失败并提示“请升级SQLite至3.35或更高版本”。

4.5 易错点:VACUUM命令在考试进行中导致服务中断

错在哪:运维人员在考试中途执行VACUUM,导致数据库文件锁死,所有考生交卷失败。

为什么错:VACUUM需要独占数据库锁,且耗时随数据量线性增长。10万条记录需47秒。

怎么答对:终版禁用VACUUM,改用PRAGMA incremental_vacuum(N):

# 每30秒执行一次,每次回收100页 conn.execute("PRAGMA incremental_vacuum(100)")

并设置PRAGMA auto_vacuum=INCREMENTAL,让SQLite在DELETE时自动回收空间。同时,监控page_count:

SELECT page_count FROM pragma_page_count;

当page_count > 100000时,监考端弹窗告警“数据库碎片过多,请在考试结束后执行维护”。


5. 高频考点:前端Canvas渲染与离线资源管理的黑匣子

考生端界面看似简单,实则藏着三个“黑匣子”:Canvas渲染的像素级精度控制、离线资源的版本强一致性、以及考试倒计时的毫秒级同步。这些细节决定你能否在面试中说出“我踩过这个坑”,而非背诵教科书定义。

5.1 Canvas渲染:为什么数学公式在某些设备上模糊?

根本原因是Canvas的devicePixelRatio未适配。当window.devicePixelRatio=2(Retina屏)时,若Canvas的width/height属性设为CSS像素值,实际渲染会拉伸导致模糊。终版强制双倍分辨率:

const canvas = document.getElementById('render-canvas'); const dpr = window.devicePixelRatio || 1; // 设置Canvas实际像素 canvas.width = canvas.clientWidth * dpr; canvas.height = canvas.clientHeight * dpr; // 设置CSS样式保持视觉大小 canvas.style.width = canvas.clientWidth + 'px'; canvas.style.height = canvas.clientHeight + 'px'; // 获取上下文时应用DPR const ctx = canvas.getContext('2d'); ctx.scale(dpr, dpr); // 关键:缩放绘图坐标系

数学公式渲染时,用ctx.font = 'bold 16px NotoSansCJKsc',并调用ctx.fillText()前执行:

ctx.textBaseline = 'middle'; ctx.textAlign = 'center'; // 对于积分符号∫,手动调整Y轴偏移 if (char === '∫') { ctx.fillText(char, x, y + 4); // 下移4像素对齐基线 } else { ctx.fillText(char, x, y); }

实测解决98%的模糊问题,剩余2%因字体缺失,由服务端预处理PDF时强制嵌入字体兜底。

5.2 离线资源管理:如何确保考生重启后加载的仍是同一版试卷?

终版采用Content Addressable Storage(CAS)思想:

  • 试卷PDF、CSS、JS文件均以SHA256哈希命名,如exam_8a3f...c2d.pdf
  • 服务端返回index.html时,内联<script>中写死资源哈希:
    <script> const RESOURCES = { pdf: 'exam_8a3f...c2d.pdf', css: 'style_e5b2...a9f.css', js: 'app_1d4c...78e.js' }; </script>
  • 前端加载资源时,先检查caches.match(RESOURCES.pdf),命中则直接返回;未命中则fetch(RESOURCES.pdf)并cache.put()
  • 关键:caches.open('exam-v1')中v1为考试ID,不同考试完全隔离

这样即使服务端更新试卷,考生端仍加载旧版,直到监考端下发新考试ID。

5.3 倒计时同步:为什么前端显示还剩1分23秒,服务端日志却显示已超时?

时间不同步只是表象,深层原因是HTTP请求往返延迟未补偿。终版采用NTP式校准:

  1. 前端首次加载时,向服务端发GET /api/time?ts=1234567890(客户端时间戳)
  2. 服务端返回{server_ts: 1234567895, rtt: 42}(RTT为服务端计算的往返时间)
  3. 前端计算偏移量:offset = (server_ts - client_ts) - rtt/2
  4. 所有倒计时基于Date.now() + offset计算

提示:RTT必须由服务端计算,因为客户端无法知道服务端处理时间。服务端代码:

@app.route('/api/time') def get_time(): client_ts = int(request.args.get('ts', 0)) server_ts = int(time.time() * 1000) rtt = server_ts - client_ts # 客户端发出到服务端收到的时间差 return jsonify({'server_ts': server_ts, 'rtt': rtt})

5.4 易错点:Service Worker拦截交卷请求导致离线失败

错在哪:Service Worker中self.addEventListener('fetch', event => { ... })未排除交卷API,导致考生离线时交卷请求被SW拦截,返回Response.error()而非真实错误。

为什么错:开发者为缓存静态资源写了全局fetch监听,但未对/api/submit等动态接口放行。

怎么答对:SW中明确排除考试API:

self.addEventListener('fetch', event => { const url = new URL(event.request.url); // 放行所有/api/请求 if (url.pathname.startsWith('/api/')) { return; // 不拦截,走原生网络 } // 其他资源走缓存 event.respondWith( caches.match(event.request).then(response => { return response || fetch(event.request); }) ); });

同时,在交卷按钮点击事件中,显式检查网络:

if (!navigator.onLine) { alert('网络不可用,请检查连接后重试'); return; }

5.5 易错点:Canvas截图上传时,Blob大小超出HTTP限制

错在哪:考生端用canvas.toBlob()生成截图,上传到/api/screenshot时,Nginx返回413 Request Entity Too Large。

为什么错:默认Nginxclient_max_body_size=1m,而1920x1080截图PNG可达3MB。

怎么答对:终版采用三重降级:

  1. 前端压缩:用canvas.toDataURL('image/jpeg', 0.6)生成JPEG,体积减至1/4
  2. 服务端扩容:Nginx配置client_max_body_size 10m;
  3. 分块上传:若仍超限,前端切分为100KB块,服务端用multipart/form-data接收后合并:
// 前端分块 const blob = await new Promise(r => canvas.toBlob(r)); const chunkSize = 102400; for (let i = 0; i < blob.size; i += chunkSize) { const chunk = blob.slice(i, i + chunkSize); const formData = new FormData(); formData.append('chunk', chunk); formData.append('chunk_index', i/chunkSize); await fetch('/api/screenshot/chunk', { method: 'POST', body: formData }); }

服务端用Redis暂存分块,chunk_index为key,所有块上传完毕后合并为完整文件。


6. 答题技巧:用“考场思维”替代“开发思维”的三步法

面试官问“如何设计局域网考试系统”,90%的人立刻开始画架构图、列技术栈,结果被一句“如果交换机突然断电,第156个考生交卷的数据在哪儿?”问懵。这不是考你会不会用Redis,而是考你有没有“考场思维”——一种把系统当作物理世界一部分来思考的习惯。我带过的学员里,从翻车到上岸的关键转折,就是学会用这三步法重构所有问题。

6.1 第一步:把所有抽象概念翻译成物理实体

“高可用”不是K8s集群,而是“监考端电脑蓝屏后,30秒内备用机接管,考生端无感知”。
“数据一致性”不是CAP理论,而是“考生A交卷时断电,重启后看到‘已交卷’按钮变灰,且监考端日志里有这条记录”。
终版系统文档里,每个模块都附带物理对照表:

抽象术语物理实体故障表现恢复手段
服务端监考室

本文还有配套的精品资源,点击获取