基于PHP的EMS在线模拟考试系统部署与优化实战 📅 发布时间:2026/9/14 5:50:25 👁 浏览次数: 简介一个基于PHP的EMS在线模拟考试系统源码包面向掌握PHP与MySQL基础、希望深入学习在线考试系统完整开发的读者。该项目覆盖用户登录与权限校验、试题分类管理、在线答题、考试计时、自动评分与成绩统计等典型模块可作为课程设计、毕业设计或中小型在线教育平台的功能参考。压缩包共1859个文件大小13.55MB内含516个tpl模板、336个php脚本、359个js脚本及42个css样式tpl负责模板渲染、php承载业务逻辑、js/css支撑前端交互另配有properties配置、SQL数据库脚本和png/gif图片基本构成一套可运行的Web应用骨架。压缩目录按功能划分便于定位控制器、模型与视图文件整体结构接近常见PHP项目已有64人下载学习。通过通读源码可掌握从表单提交、Session鉴权到数据库读写、分数计算的完整数据流理解MVC分层思想在真实业务中的落地细节是提升PHP项目实战能力的合适素材。1. 基于PHP的EMS在线模拟考试系统源码先别急着解压网盘里流传的“基于PHP的EMS在线模拟考试系统源码.zip”压缩包里的PHP代码不多核心逻辑却全在服务端题库管理、学生登录、考试倒计时、试题展示和提交判分。EMS在这里通常被扩展为Exam Management System也就是考试管理系统。因为PHP部署成本低、写法和部署链路人人都熟一台2C4G的云服务器就能支撑几百人规模的模拟考试所以培训机构、高校内部考核、企业入职测评里经常见到这类源码包。但能打开登录页和能顺利组织一场考试是两回事。我接手过几套类似的PHP考试系统最大的坑集中在PHP扩展没启用、zip文件中的目录结构不完整、MySQL版本不兼容以及并发交卷时成绩被覆盖这几个地方。下面按“从源码到可运行”的顺序把环境、表结构、组卷判分、性能优化和排错逐层拆开目标是让你拿到这套源码后能在半天内部署起来并把隐患提前堵住。2. 把zip包的结构拆开先摸清这套PHP考试系统的模块和数据表2.1 从目录清单判断它是原生PHP还是框架工程拿到zip后的第一件事不是双击运行而是解压到空目录看一眼结构。原因很简单“Online模拟考试系统”这类源码存在两种截然不同的组织方式。一种是原生PHP每个业务页面对应一个.php文件入口分散部署时直接扔进web根目录就能跑另一种是ThinkPHP/Laravel框架工程只有一个public/index.php入口路由由框架接管必须先执行composer install并做完伪静态配置才能正常工作。这两种结构的部署方式完全不同从目录层级一眼就能分辨。原生PHP的典型结构长这样exam_system/ ├── admin/ # 后台管理端 │ ├── login.php │ ├── question_list.php │ ├── paper_add.php │ ├── score_list.php │ └── template/ # 后台HTML模板 ├── index.php # 学生端入口 ├── exam.php # 考试进行页 ├── result.php # 成绩页 ├── conn.php # 数据库连接 ├── functions.php # 公共函数 ├── inc/ # 公共头尾文件 │ ├── header.php │ └── footer.php ├── install/ │ └── exam.sql # 建库脚本 └── static/ ├── css/ └── js/看到根目录有composer.json、artisan这类文件时就要按框架部署流程走入口固定到public/index.php静态资源要放在项目目录之外或者单独配上alias并先执行php composer install。原生PHP这种结构相对直观适合在内网部署也方便直接改逻辑但代价是没有统一的错误处理代码质量完全取决于写包的人当时怎么组织函数。压缩包的编码也要留意。Windows下打包的zip默认用GBK编码文件名直接拿Linux的unzip解压会出现一串乱码目录导致require/include路径全部失败。这个问题放到下一章同文件权限一起处理。2.2 核心数据表题库、试卷、考试记录、答题明细各负责什么这类系统的业务核心是四个表试卷表、题库表、考试记录表、答题明细表再加上管理员表和学生表。管理员在后台创建试卷时并不会把题目内容复制进试卷表而是通过paper_id把试卷和题库关联起来考试真正开始时才做抽题和组卷。负责关键状态和计分的数据表结构如下CREATE TABLE exam_paper ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, title VARCHAR(255) NOT NULL, duration_minutes INT UNSIGNED NOT NULL COMMENT 考试时长(分钟), start_time DATETIME NOT NULL, end_time DATETIME NOT NULL, question_total INT UNSIGNED NOT NULL DEFAULT 50, pass_score DECIMAL(5,2) NOT NULL DEFAULT 60.00, status TINYINT NOT NULL DEFAULT 1 COMMENT 0下架 1启用, KEY idx_status_time (status, start_time, end_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT试卷定义; CREATE TABLE exam_question_bank ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, paper_id INT UNSIGNED NOT NULL, question_type TINYINT NOT NULL COMMENT 1单选 2多选 3判断 4问答, content TEXT NOT NULL COMMENT 题干支持HTML, options TEXT NULL COMMENT 选项JSON如{A:选项A}, answer TEXT NOT NULL COMMENT 客观题答案多选用|分隔问答为空, score DECIMAL(4,2) NOT NULL DEFAULT 5.00, sort_order INT NOT NULL DEFAULT 0, status TINYINT NOT NULL DEFAULT 1, KEY idx_paper_type (paper_id, question_type) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT题库; CREATE TABLE exam_record ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, student_id INT UNSIGNED NOT NULL, paper_id INT UNSIGNED NOT NULL, start_time DATETIME NOT NULL, submit_time DATETIME NULL, used_seconds INT UNSIGNED DEFAULT 0, total_score DECIMAL(6,2) DEFAULT NULL, status TINYINT NOT NULL DEFAULT 0 COMMENT 0作答中 1已交卷 2已批改, UNIQUE KEY uk_student_paper (student_id, paper_id), KEY idx_status (status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT考试记录;再补一张exam_answer_detail表字段不多但最容易被忽略的是联合唯一索引。如果只建自增主键没有(record_id, question_id)的唯一约束学生同一场考试连续两次提交就可能插入两行答题明细最终判分结果完全不可信。部署时我会把联合唯一索引直接加在DDL里代替在业务代码里做重复判断。题库表里的options字段我建议用JSON而不是拆成option_a到option_d五列。JSON的好处是支持多选题、不定项和图片选项判分时一次json_decode就拿到完整数组代码判断起来也简单。而五列的写法后台表单好做但加一道新题型时就要动表结构还要处理大量空值判断。2.3 为什么随机组卷只能在服务端做“每个学生拿到的试卷不一样”是模拟考试系统的高频需求。部分粗糙的实现会把抽题结果返回给前端再靠JS控制展示这是个安全角度的低级错误考试时打开浏览器开发者工具网络响应里其实已经能看到全部题目防作弊直接失效。服务端组卷的标准做法是学生第一次进入考试页时PHP根据paper_id和题型比例抽题然后把抽题结果存成一份“本次考试快照”后续页面请求只从快照里取题不再重复随机。快照可以序列化成JSON存在exam_record表的一个字段里也可以放Redis。交卷评分读同一个快照无论学生怎么刷新页面题目顺序和内容都不变题库后面被管理员修改也不会影响已交试卷的判定依据。3. 把zip源码部署成可访问站点PHP环境、目录权限与Nginx配置3.1 先确定PHP版本再谈其他这类打包源码对PHP版本极其敏感。PHP 7.0删除了mysql_xxx系列函数PHP 8.0又进一步收紧了方法参数和类型判断老代码在8.1上经常直接抛Fatal Error而不是警告。我的建议是先看conn.php或install/install.php里有没有写环境要求没写就一律按最多兼容的方案处理本地调试用PHP 7.4 MySQL 5.7线上如果也想8.x就必须把全项目跑一遍冒烟测试。需要保证的PHP扩展是这几个pdo_mysql、mysqli、json、mbstring、openssl、gd、curl。最容易被漏掉的是gd漏掉以后验证码图片位置直接白屏没有任何PHP错误提示很迷惑。排查这类白屏时我会先写一个test.php临时文件?php // 临时探针文件定位结束后立即删除 phpinfo();用浏览器访问这个文件会得到完整的扩展列表搜索PHP Version下面的Configure Command和enabled扩展列表。这样判断少哪些扩展比盲改php.ini快得多。同时把PHP错误显示开关打开可以临时用下面这段放在入口文件顶部?php ini_set(display_errors, 1); error_reporting(E_ALL);调试生产环境时display_errors记得在确认完问题后关掉。很多这套源码出现500空白页本质是数据库连接失败错误被display_errorsOff吃掉了。3.2 用unzip解压源码时处理GBK乱码和目录权限在Linux上解压Windows打出来的zip直接用系统自带的unzip经常遇见乱码。原因侧面提过zip记录文件名用的编码是GBK而Linux默认期望UTF-8。一条命令解决cd /var/www/html unzip -O gbk exam_system.zip -d exam chown -R www-data:www-data exam find exam -type d -exec chmod 755 {} \; find exam -type f -exec chmod 644 {} \; chmod -R 755 exam/upload逐段解释-O gbk让解压程序按GBK解码zip内的文件名解出来才是正常的中文路径。chown把属主改成www-data避免php-fpm没权限读取文件。find配合chmod是为了把真实目录权限压缩到最小目录755、文件644upload目录单独再授权755保证PHP运行时能往里写学生上传的图片。install目录在部署完成后建议改名或删除防止别人通过浏览器再次进入安装程序重置数据库。如果你使用的服务器发行版自带unzip版本太老不支持-O参数可以用Python替代python3 -c import zipfile; zipfile.ZipFile(exam_system.zip).extractall(exam)但这样文件名仍是GBK乱码需要再做一次编码转换。实际操作中我宁可在Windows本地解压后重新用UTF-8打包再传到服务器省去一系列编码摩擦。3.3 在Nginx下配置原生PHP站点的伪静态与入口文件原生PHP的站点大多数靠query_string路由比如exam.php?actionstart并不依赖Apache的.htaccess重写。这种场景下Nginx配置比Apache还简单不需要把location /改成try_files去适配路由规则。可用的server块配置如下server { listen 80; server_name exam.example.internal; root /var/www/html/exam; index index.php; location / { try_files $uri $uri/ 404; } location ~ \.php$ { include fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; try_files $uri 404; fastcgi_pass 127.0.0.1:9000; fastcgi_read_timeout 300; } location ~* \.(css|js|png|jpg|jpeg|gif|ico|svg)$ { expires 7d; access_log off; } }说明几个关键位置fastcgi_pass的地址要和php-fpm实际监听方式一致用php-fpm.sock还是127.0.0.1:9000是由php-fpm.conf里listen参数决定的两者不只是写法区别socket模式下内核维护连接的开销更小。fastcgi_read_timeout设置成300秒是因为主观题答题时浏览器保持长连接PHP脚本偶尔处理慢如果默认60秒交卷时Post正好撞上超时会直接被nginx断开。最后一段location是静态资源的7天强缓存把css、js、图片请求从PHP转发中剥离对“打开试卷页第一屏快不快”影响巨大。判断源码是否需要Rewrite规则很简单根目录放有.htaccess并且里面写了RewriteRule说明代码依赖于URL重写如果没有按上面的配置直接跑就行。3.4 导入数据库与修改配置文件的前后顺序建库导入时要留意两件事库的字符集和MySQL默认认证插件。字符集不一致时中文字段的内容在页面显示成问号MySQL本身不会报错。所以在导入之前先把库定成utf8mb4mysql -u root -p -e CREATE DATABASE exam_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; mysql -u root -p exam_db /var/www/html/exam/install/exam.sql导入成功后去config目录或conn.php里检查数据库主机名、库名、用户名、密码、表前缀五个值。如果MySQL是8.0而源码用的是老版mysqli连接客户端很可能报错“Authentication plugin caching_sha2_password cannot be loaded”。这不是密码错误是认证协议不兼容。低成本的解决方式是把MySQL里的用户改为mysql_native_password但要意识到这只适合内网公网环境尽量还是升级代码去兼容新的认证插件。3.5 部署完成后的第一轮自检页面能打开之后我习惯按固定顺序做一轮冒烟测试管理员登录试卷列表是否正常学生端能否发起一场考试同一场考试用另一个浏览器登录旧浏览器是否还能继续答题故意不选某道题直接交卷看日志里是正常记录空答案还是抛致命错误。另外要重点验证重复交卷的情况。多数这套源码的exam_record表没有标志位交卷接口把total_score更新一次学生再次送交卷请求还会更新一次成绩表最终值是最后一次脚本跑出来的结果。至少要保证记录行的update语句带submit_time IS NULL条件否则评测结果谁都不敢信。4. 模拟考试核心链路拆解随机组卷、考试计时与自动判分4.1 组卷SQL从简单的ORDER BY RAND到可控的按题型抽题在线模拟考试系统里最容易引发MySQL性能事故的写法就是拿一张十万行题库直接ORDER BY RAND()。这个函数会对整张表逐行生成随机值再排序题库越大排序代价越离谱。几百个学生同时进入考试时每个请求都触发一次全表排序CPU和磁盘IO会同时被打满。先看一种最朴素但可用的实现按题型限定查询范围再随机取数量。-- 从指定试卷下按题型随机抽取题目 SELECT id, question_type, content, options, score FROM exam_question_bank WHERE paper_id 101 AND question_type 1 AND status 1 ORDER BY RAND() LIMIT 20;关键不是后面的LIMIT而是WHERE条件里的paper_id和question_type它们把随机范围从整张表缩小到一张试卷下的一个题型。这个写法在单试卷题目几千条以内完全够用不需要过度设计。再大一个数量级时就用主键ID集合加array_rand来抽题数据库只按主键查内容/** * 从候选题目ID数组中抽取指定数量 * param arrayint,int $candidates 题目ID数组 * param int $need 本次需要抽取数量 * return arrayint,int */ function pickRandomQuestions(array $candidates, int $need): array { $pickCount min($need, count($candidates)); $keys array_rand($candidates, $pickCount); if (!is_array($keys)) { $keys [$keys]; } $result []; foreach ($keys as $k) { $result[] $candidates[$k]; } return $result; }array_rand内部是对PHP数组的随机选择复杂度接近O(n)不涉及MySQL排序。拿到抽好的ID数组后再执行一次WHERE id IN (...)查询主键命中压力小得多。这里的代价是第一步要把该题型所有ID从MySQL读出来所以适合“试题ID总数在可接受范围但并发量高”的场景如果ID总数上百万就要考虑把题型ID列表预热到Redis按位或集合随机取出再从库查内容。无论哪种抽题方案服务端都要保存这次组卷的快照。比较稳妥的方式是在exam_record表加一个subjects MEDIUMTEXT字段把整份试卷的题目ID和分数JSON序列化存进去后续考试页和交卷接口都只按record_id读取快照不重新抽。4.2 考试倒计时的正确姿势服务端时间戳才是标准前端setInterval每秒递减剩余时间在正常网络下是看不到问题的但浏览器切后台、桌面休眠或CPU被其他任务占用时JS定时器的精度会大幅下降页面恢复时倒计时已经走了很久甚至走完。更危险的是用户手动改系统时间前端倒计时也会跟着失真。可靠做法是服务端记录开始时间每个请求进来都判断当前Unix时间戳是否已经越过截止线/** * 校验考试是否超时超时则强制交卷 * param array $record exam_record表中的一行 */ public function checkTimeWindow(array $record): void { $now time(); $deadline strtotime($record[start_time]) intval($record[duration_minutes]) * 60; if ($now $deadline) { $this-forceSubmit($record); throw new DomainException(考试时间已结束系统已自动交卷); } }这里forceSubmit内会走正常的交卷流程但它的内部实现必须做幂等判断。就是把submit_time字段作为锁只有submit_time为null时才能执行更新否则直接返回。这个字段天然充当了分布式锁不用引入Redis适合原生PHP直接使用。这道防线只在后端加还不够前端倒计时到了01:00:00时页面要在JS里先禁用答题按钮并显示“正在保存”同时发一次提前交卷请求作为兜底。真实场景里我见过两次提交间隔只有零点几秒的情况两个请求并行到达PHP如果后端幂等条件写得不够严谨成绩极有可能被第二次请求覆盖。4.3 自动判分多选题比较顺序问题和主客观题边界判断客观题答案时最容易出错的是多选题顺序。标准答案存的是A,B,C考生提交C,A,B肉眼判断没有差别但直接用字符串相等很容易判成错误。解决办法是把两个答案都归一化成数组、排序、再比较/** * 客观题判分支持单选、多选、判断 * param string $standard 正确答案多选用英文逗号分隔 * param string $answered 考生实际提交内容 */ public function judgeObjective(string $standard, string $answered): bool { $expected $this-normalizeAnswer($standard); $actual $this-normalizeAnswer($answered); if (count(array_diff($expected, $actual)) 0) { return false; } if (count(array_diff($actual, $expected)) 0) { return false; } return true; } /** * 将答案字符串统一转换为排序后的选项数组 */ private function normalizeAnswer(string $raw): array { $raw strtoupper(trim($raw)); $parts preg_split(/[,、\s|]/, $raw); $parts array_map(trim, $parts); $parts array_filter($parts, static fn($v) $v ! ); sort($parts); return array_values($parts); }双向array_diff是为了查出“漏选”和“错选”两类情况。第一步比较expected对actual的差集是判断标准答案里的选项考生有没有全部包含第二步反过来防止答题内容里出现多选的错误选项。两个条件都满足才是完全正确。这套函数没有区分漏选和错选如果考试规则要求“漏选得一半分”就需要把返回值从布尔改成带‘fullyCorrect’和‘partiallyCorrect’两个字段的结构。主观题判分的思路完全不同我在系统里通常把问答类题目单独标成question_type4交卷时把答案写进exam_answer_detailstatus字段标记为“待批改”。管理员在后台逐个学生打分分数按题目权重写入明细表。不把主观题交给PHP自动判的原因很简单中文语义判断的准确率在敏感场景下不可控人工阅卷起码能给出可追溯的判分依据。这里折中做法是先把答案按关键词打分再提供人工复核页但关键词命中只能作为初判参考不能直接进成绩单。5. 多人同时在线考试时的PHP与MySQL优化5.1 第一步不是加缓存而是打开慢查询日志系统变慢时不要急着上Redis。先把MySQL慢查询日志打开知道慢在哪条SQL再谈优化方案。一次性开启的临时方法SET GLOBAL slow_query_log ON; SET GLOBAL long_query_time 1; SET GLOBAL log_queries_not_using_indexes ON;这组参数只在MySQL当前运行状态下生效适合压测时在线观察。全部开启后再跑一轮并发慢日志里会持续输出执行时间超过1秒且没用上索引的SQL。重点看Rows_examined字段如果某条查询走了索引但Rows_examined依然有几十万说明当前的索引组合没有覆盖查询里所有过滤条件。这种考试系统里最常见的慢查询就是题库表的随机抽题-- 典型慢查询随机组卷时按试卷与题型筛选 SELECT id FROM exam_question_bank WHERE paper_id 101 AND question_type 1 AND status 1 ORDER BY RAND() LIMIT 20;这条查询需要调整索引把过滤列全部放进一个复合索引并且列的顺序要和SQL里WHERE条件的顺序一致。正确索引是KEY idx_composite (paper_id, question_type, status)只有前两列的话status1过滤还需要回表判断一次数据库选索引时也会更犹豫。注意不能依赖建表时写的旧索引先DROP掉再重建。5.2 Redis在考试系统里的两个具体用途快照缓存与交卷锁题库十万行级别、几百人同时考试时把组卷快照从MySQL提到Redis是很自然的优化。用法很简单key按考试记录ID设计$key exam:snapshot: . $recordId; $json $redis-get($key); if ($json false) { $json $this-loadSnapshotFromDb($recordId); $redis-setex($key, 7200, $json); } $snapshot json_decode($json, true);说明一下setex的7200秒有效期对应一场考试通常不超过两小时学生交卷后记录页仍然保留快照但以数据库为准所以过期时间只是一个兜底不是计分的依据。如果交卷后系统还要给学生展示“查看本次模拟考试答题详情”那就不能在交卷时直接删除快照否则重复打开详情页会反复回源MySQL退化成一个拖慢系统的反优化。Redis另一个更实用的角色是交卷接口的幂等锁。交卷过程涉及成绩写入和状态更新在原生PHP代码里没有任何原子性保护重复提交就会导致总成绩被覆盖。通过Redis加锁很简单$lockKey exam:submit:lock: . $recordId; $locked $redis-set($lockKey, 1, [NX, EX 10]); if (!$locked) { throw new RuntimeException(正在进行交卷操作请勿重复提交); } $redis-del($lockKey);set的NX参数确保锁只在key不存在时才写入成功EX10设置锁的自动过期时间。这里的10秒必须大于单次交卷事务耗时否则锁提前释放第一个请求还没执行完第二个请求就进来了。del操作一定要放在finally块里否则Redis里残留锁学生后续重试会被一直提示“重复提交”。不用Redis的场景下数据库层用submit_time IS NULL条件作为幂等约束也成立但那只能挡重复更新挡不住并发的读-改-写窗口处理并发还是要靠真锁。5.3 PHP-FPM进程参数与静态资源缓存调完SQL和Redis仍然可能觉得打开试卷页不顺畅。这时应该打开浏览器开发者工具看network面板如果大量CSS、JS、图片请求耗时明显问题就不在PHP而在静态资源传输。Nginx配置里加一段静态资源缓存前面那一章的7天过期设置就是最小成本方案。更激进的方案是把静态资源挪到CDN但考试系统的题干和图片属于核心题库资产放CDN等于把考题复制到第三方节点我不推荐这个做法。内网系统里直接用Nginx的gzip模块压缩css和js效果更实际。PHP-FPM的参数最需要关注的是pm.max_children。每个PHP进程在内存占用方面按40MB是常见估算值用云服务器内存总量除以单进程占用就能算出上限值。4G内存、单一PHP应用场景我一般这样设置pm dynamic pm.max_children 60 pm.start_servers 25 pm.min_spare_servers 10 pm.max_spare_servers 35这几项的含义分别是max_children是同时存在的进程上限start_servers是启动时预开的进程数min_spare_servers和max_spare_servers控制空闲进程的动态范围。数值不是越大越好max_children如果超过内存可承载水平Swap一打满页面反而会整体卡死。正确的顺序是先压测、看内存记录、再微调档位起步从小值开始逐步加。6. 老PHP源码的安全检查与防作弊加固6.1 部署前先过一遍后门扫描这类打包源码有风险来源不明的包里偶尔藏着被加过料的后门文件。部署前我会针对eval、assert、create_function这些高频危险函数做一次grepgrep -rnE eval\(|assert\(|create_function\(|base64_decode\(\s*[\].*[\] --include*.php .扫出来结果以后不要直接删除先看调用上下文。特别要检查uploads目录和template目录里是否有独立运行的php文件这两处是源码包最容易被塞后门的位置。确认是危险文件后连同整个上传目录一起清空防止旧文件被重复利用。6.2 防作弊参数设计切屏记录、答题时长检测、IP记录模拟考试的防作弊不能只靠禁复制和禁右键这两个限制用浏览器直接就能绕过。更有效的是在交卷后做答题行为分析记录每道题从开始作答到提交答案的时间间隔记录考生的IP、User-Agent和使用时长。切屏检测的实现思路简单前端监听visibilitychange事件页面每次从不可见切换回可见时向后台接口记录一次切换时间$data [ record_id $recordId, event_type BLUR, created_at date(Y-m-d H:i:s), ]; $pdo-prepare( INSERT INTO exam_behavior_log (record_id, event_type, created_at) VALUES (:record_id, :event_type, :created_at) )-execute($data);答题时长检测更讲究。正常完成一套50道客观题最短也不应该低于15分钟。交卷时如果used_seconds小于某个阈值就把这条考试记录标记为“存疑”让管理员决定是否有必要复核答卷。同一套系统里用很短时间做完且分数极高与用完整时长做完但分数很高对监考方有完全不同的证明力这块信息至少要能在管理端被查出来。6.3 验证异常答卷时直接看答题间隔分布判断异常答卷不需要做复杂统计从答题明细表里取相邻两题的时间差就够。-- 查看一场考试中每个学生的答题时间间隔 SELECT t.student_id, t.question_id, t.created_at, TIMESTAMPDIFF(SECOND, LAG(t.created_at) OVER (PARTITION BY t.record_id ORDER BY t.question_id), t.created_at) AS interval_seconds FROM exam_answer_detail t WHERE t.record_id 10086 ORDER BY t.question_id;这条SQL用了MySQL 8的窗口函数LAG把上一题的created_at带回到当前行直接算出间隔秒数。如果某些学生整张卷子相邻题目间隔都在几百毫秒以内说明这次考试几乎不是正常作答而是脚本提交。把它和切屏记录、IP归属做交叉核对然后挑出名单交给负责的老师人工判断。无论系统多自动化模拟考试的最终目的还是帮学生补上自己的知识缺口不是让后台数据分析反客为主这套规则也只需要做到“能辅助判断”就够了。本文还有配套的精品资源点击获取