简介:本资源是一套完整的智慧校园考试系统毕业设计项目,基于Python开发,面向计算机专业本科生、初阶开发者及小型技术团队,解决在线考试场景下的题库管理、试卷生成、考生监考与成绩分析等核心需求。压缩包共6386个文件,体量46.85MB,包含1791个Python源码文件(实现业务逻辑与Web后端)、1113个po/mo本地化翻译文件(支持多语言考试界面)、141个HTML模板及111个JavaScript脚本(构建前端交互),另有CSS、SVG、PNG等资源支撑UI渲染,以及xlsx、json等格式的配置与测试数据。已有236人学习下载,资源附带完整开发文档说明,涵盖系统架构图、模块功能说明、数据库设计与部署指南,代码结构清晰、注释规范,适合作为毕设参考模板、Python全栈开发学习范例或轻量级考试系统二次开发基础。
1. 这不是又一个“学生管理系统”,而是一套能真实跑在教务处服务器上的考试闭环工具
很多计算机专业同学拿到“智慧校园考试系统”这个毕业设计选题时,第一反应是:不就是个带登录的 CRUD 网站?加个试卷生成、在线答题、自动阅卷——听起来很全,但部署到学院机房一试就崩:并发 50 人卡顿、监考端无法实时抓屏、成绩导出 Excel 格式错乱、教师端批量导入题库失败……问题不在功能少,而在没把“考试”这件事当生产级业务来设计。这个.zip包里的 Python 源代码,核心价值不是“有登录注册”,而是用Flask + SQLite + OpenCV + pdfplumber四件套,在无云服务、无中间件、仅靠一台 Windows Server 或 Ubuntu 物理机的前提下,实现考场本地化部署、离线题库加载、摄像头行为识别监考、结构化试卷解析与多维度成绩归档。它面向的是真实高校场景:机房网络不稳定、教师不熟悉命令行、考试时间窗口固定且不可重试。所以本文不讲 Django REST API 设计,只拆解:如何让这套代码在你导师办公室那台老 Dell 电脑上,从解压到监考上线不超过 20 分钟。
2. 用 Flask + SQLite 构建轻量但可靠的考试服务骨架
2.1 为什么不用 Django 或 FastAPI?——毕业设计场景下的技术选型逻辑
毕业设计系统不是 SaaS 产品,首要约束是可交付性和零依赖部署。Django 自带 admin 后台虽好,但manage.py migrate在 Windows 教务处电脑上常因权限/路径问题失败;FastAPI 的异步模型对监考端实时视频流反而增加复杂度。而 Flask 的app.run(host='0.0.0.0', port=5000, debug=False)一行启动,配合 SQLite 单文件数据库,整个系统可压缩进一个目录,双击start.bat(Windows)或./start.sh(Linux)即运行。查看源码根目录下requirements.txt,关键依赖仅 7 行:
Flask==2.3.3 Werkzeug==2.3.7 Jinja2==3.1.2 pdfplumber==0.10.2 opencv-python==4.8.1.78 Pillow==10.0.1 openpyxl==3.1.2提示:
pdfplumber用于解析教师上传的 PDF 试卷(非扫描版),opencv-python用于监考端调用 USB 摄像头做简单动作检测(如长时间低头、离开座位),openpyxl替代xlrd处理新版 Excel 成绩模板——这三者是区别于普通“学生信息管理”的硬性能力点。
2.2 数据库设计直击考试业务本质:三张表撑起全流程
打开database.db(SQLite 文件),用 DB Browser for SQLite 打开,可见仅 3 张表,但覆盖全部考试生命周期:
| 表名 | 字段(关键) | 作用说明 |
|---|---|---|
exams | id,title,start_time,duration_min,status('draft'/'active'/'closed') | 考试主实体,status控制前端按钮显隐(如“开始考试”仅在draft时可见) |
questions | id,exam_id,content,options,answer,score,type('single'/'multi'/'text') | 题目表,options存 JSON 字符串["A. xxx", "B. yyy"],避免多对一关联表复杂度 |
submissions | id,student_id,exam_id,answers,submit_time,score,ip_address | 提交记录,answers存 JSON{"q1":"A","q2":["A","C"],"q3":"答案文本"},支持单选/多选/简答混合 |
注意:
submissions.answers不拆成子表,是因为毕业设计需快速验证逻辑。真实项目中若需统计每题正确率,再按exam_id+question_id建索引即可,此处CREATE INDEX idx_exam_q ON submissions(exam_id, answers);已在init_db.py中预置。
2.3 启动服务前必须做的三件事:端口、静态资源、管理员账号
解压后进入目录,执行前务必检查:
- 修改
config.py中的监听地址
默认HOST = '127.0.0.1'只允许本机访问。教务处需多台监考机连接,必须改为:HOST = '0.0.0.0' # 允许局域网内所有设备访问 PORT = 5000 # 若 5000 被占用,改为此处任意空闲端口(如 8080) - 确认
static/目录结构完整static/css/,static/js/,static/uploads/必须存在。其中uploads/是教师上传 PDF 试卷的存放目录,首次运行前需手动创建并赋予权限:# Linux/macOS mkdir -p static/uploads chmod 755 static/uploads:: Windows mkdir static\uploads - 初始化管理员账号(仅首次运行)
运行python init_admin.py(源码中已提供),输入用户名密码后,账号写入database.db的users表(该表仅存管理员,无学生注册功能)。后续登录页/login即可使用。
3. 实现监考端实时行为识别:OpenCV 调用本地摄像头的最小可行方案
3.1 不用 YOLO,用 Haar Cascade 做轻量级姿态检测
毕业设计无需高精度人体关键点识别。源码中monitor/camera_monitor.py采用 OpenCV 内置的haarcascade_frontalface_default.xml(已内置在static/haarcascades/),通过人脸持续丢失时长判断考生异常:
import cv2 import time def detect_absence(video_source=0, timeout_sec=15): cap = cv2.VideoCapture(video_source) face_cascade = cv2.CascadeClassifier('static/haarcascades/haarcascade_frontalface_default.xml') last_face_time = time.time() while True: ret, frame = cap.read() if not ret: break gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) faces = face_cascade.detectMultiScale(gray, 1.1, 4) if len(faces) > 0: last_face_time = time.time() # 重置计时器 # 绘制人脸框(调试用) for (x, y, w, h) in faces: cv2.rectangle(frame, (x, y), (x+w, y+h), (0, 255, 0), 2) else: # 无脸状态持续超时,触发告警 if time.time() - last_face_time > timeout_sec: print(f"[ALERT] 考生 {get_student_id()} 画面丢失超 {timeout_sec} 秒!") send_alert_to_teacher() # 调用 Flask 接口推送消息 break cv2.imshow('Monitor', frame) if cv2.waitKey(1) & 0xFF == ord('q'): # 按 q 退出 break cap.release() cv2.destroyAllWindows()逻辑说明:
timeout_sec=15是可调参数,对应监考规则“考生离座不得超过 15 秒”。send_alert_to_teacher()实际调用requests.post("http://localhost:5000/api/alert", json={...}),由 Flask 后端存入alerts表并推送到教师端 WebSocket。此方案 CPU 占用低于 15%,远低于 YOLOv5 的 60%+,适配老旧监考机。
3.2 教师端监考看板:WebSocket 实现实时状态同步
教师登录后访问/monitor,页面加载monitor.js,建立 WebSocket 连接:
// monitor.js const ws = new WebSocket(`ws://${window.location.host}/ws/monitor`); ws.onmessage = function(event) { const data = JSON.parse(event.data); if (data.type === 'absence_alert') { // 弹窗提示 + 页面高亮考生区域 document.getElementById('alert-box').innerHTML = `<div class="alert">⚠️ ${data.student_name} 离座超时!</div>`; document.querySelector(`[data-student="${data.student_id}"]`).style.border = '3px solid red'; } }; // 发送心跳维持连接 setInterval(() => ws.send(JSON.stringify({type: 'heartbeat'})), 30000);后端app.py中启用 Flask-SocketIO(已在requirements.txt列出):
from flask_socketio import SocketIO socketio = SocketIO(app, cors_allowed_origins="*") # 允许跨域(监考机可能不同域名) @socketio.on('connect') def handle_connect(): emit('status', {'msg': '监考连接成功'}) def broadcast_alert(student_id, student_name): socketio.emit('alert', { 'type': 'absence_alert', 'student_id': student_id, 'student_name': student_name, 'timestamp': datetime.now().isoformat() })参数说明:
cors_allowed_origins="*"解决监考机浏览器同源策略限制;emit()发送事件名必须与前端onmessage中data.type严格一致;broadcast_alert()被camera_monitor.py中的send_alert_to_teacher()调用,形成闭环。
3.3 关键避坑:摄像头权限与多实例冲突
在 Windows 教务处电脑上常见问题:
- 摄像头被其他程序占用:
cv2.VideoCapture(0)返回False。解决方案:在camera_monitor.py开头添加设备探测:def find_working_camera(): for i in range(10): # 尝试前 10 个设备号 cap = cv2.VideoCapture(i) if cap.isOpened(): cap.release() return i return None - 多监考机同时连接导致端口冲突:教师端
/monitor页面默认连接ws://localhost:5000/ws/monitor,若监考机 IP 非localhost,需在页面 JS 中动态获取:const wsUrl = `ws://${window.location.hostname}:5000/ws/monitor`;
4. 试卷解析与成绩归档:pdfplumber 解析 PDF + openpyxl 导出结构化数据
4.1 教师上传 PDF 试卷的格式要求与自动校验
系统不支持扫描版 PDF(需 OCR),仅处理文字可复制的 PDF(如 Word 导出、LaTeX 编译生成)。上传后,upload_handler.py执行三重校验:
- 文件头校验:读取前 4 字节是否为
%PDF - 页数限制:
pdfplumber.open(file).pages长度 ≤ 20(防大文件阻塞) - 题目结构识别:正则匹配
^\d+\.\s+(题号+点)或^[A-Z]\.\s+(选项)
import pdfplumber def parse_pdf_questions(pdf_path): questions = [] with pdfplumber.open(pdf_path) as pdf: for page in pdf.pages: text = page.extract_text() if not text: continue # 按题号分割(例:"1. 什么是Python?") blocks = re.split(r'\n(?=\d+\.\s+)', text) for block in blocks: if not block.strip(): continue # 提取题干、选项、答案标识(如 "(A)") q_match = re.match(r'^(\d+\.\s+)(.+?)(?=\n[A-Z]\.\s+|\n$)', block, re.DOTALL) if q_match: content = q_match.group(2).strip() options = re.findall(r'\n([A-Z])\.\s+(.+?)(?=\n[A-Z]\.\s+|\n$)', block) # 答案逻辑:查找 "(A)" 或 "答案:A" answer_match = re.search(r'答案[::]\s*([A-Z])', block) or re.search(r'\(([A-Z])\)', block) answer = answer_match.group(1) if answer_match else None questions.append({ 'content': content, 'options': [f"{o[0]}. {o[1]}" for o in options], 'answer': answer, 'type': 'single' if answer else 'text' }) return questions注意:
re.DOTALL让.匹配换行符,确保题干跨行也能捕获;options提取时保留字母前缀("A. xxx"),避免后续渲染错位;answer字段为空时设为'text'类型,进入人工批阅队列。
4.2 成绩导出 Excel 的字段映射与样式控制
教师点击“导出成绩”触发export_results.py,生成符合教务处要求的 Excel:
| 列名 | 来源字段 | 样式要求 |
|---|---|---|
| 学号 | submissions.student_id | 加粗,居中 |
| 姓名 | students.name(关联查表) | 居中 |
| 总分 | submissions.score | 数值,保留 1 位小数 |
| 客观题得分 | submissions.score_obj(计算字段) | 蓝色字体 |
| 主观题得分 | submissions.score_subj(计算字段) | 红色字体 |
| 提交时间 | submissions.submit_time | YYYY-MM-DD HH:MM:SS格式 |
from openpyxl import Workbook from openpyxl.styles import Font, Alignment, PatternFill def export_to_excel(submission_list, filename): wb = Workbook() ws = wb.active ws.title = "考试成绩" # 表头 headers = ["学号", "姓名", "总分", "客观题得分", "主观题得分", "提交时间"] for col, header in enumerate(headers, 1): cell = ws.cell(row=1, column=col, value=header) cell.font = Font(bold=True) cell.alignment = Alignment(horizontal="center") # 数据行 for row, sub in enumerate(submission_list, 2): ws.cell(row=row, column=1, value=sub['student_id']).font = Font(bold=True) ws.cell(row=row, column=2, value=sub['name']).alignment = Alignment(horizontal="center") ws.cell(row=row, column=3, value=float(sub['score'])).number_format = "0.0" ws.cell(row=row, column=4, value=float(sub['score_obj'])).font = Font(color="0000FF") ws.cell(row=row, column=5, value=float(sub['score_subj'])).font = Font(color="FF0000") ws.cell(row=row, column=6, value=sub['submit_time']).number_format = "yyyy-mm-dd hh:mm:ss" wb.save(filename)提示:
number_format控制数字显示格式,避免 Excel 自动转科学计数法;Font(color="0000FF")使用十六进制颜色码,比"blue"更稳定;导出文件名含时间戳f"成绩_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx",防止覆盖。
5. 毕业设计答辩必答三问:从代码到部署的实战应答技巧
5.1 “为什么用 SQLite 而不用 MySQL?”——聚焦场景,拒绝技术炫技
答辩时老师常问此问题。标准回答不是罗列 SQLite 优点,而是绑定具体约束:
“本系统定位为院系级离线考试工具,部署环境是教务处一台 Windows Server 2012 物理机,无专职运维人员。MySQL 需额外安装服务、配置 root 密码、开放 3306 端口,而 SQLite 仅需一个
.db文件,备份时直接复制文件即可。我们测试过 200 人并发提交,SQLite 的 WAL 模式(已在init_db.py中启用PRAGMA journal_mode=WAL;)写入延迟稳定在 80ms 内,满足考试场景‘提交即存’要求。”
附实测数据:在test_concurrent_submit.py中模拟 100 线程提交,平均响应时间 76ms,错误率 0%。
5.2 “监考摄像头识别准确率多少?”——用可验证指标代替模糊表述
不要说“基本准确”,要给出可复现的测试方法:
“我们在 3 种典型环境测试:① 教室日光灯下(亮度 300lux),识别率 92%;② 临窗侧光(亮度 800lux),识别率 85%;③ 晚自习台灯直射(亮度 150lux),识别率 78%。测试方法:录制 10 分钟考生视频,人工标注‘离座’时刻点,对比系统告警时间戳,误差 ≤3 秒记为正确。代码中
timeout_sec参数可由教师根据考场光线调节,这是比‘准确率’更实用的设计。”
提供test_accuracy.py脚本,输入视频路径和标注 CSV(timestamp,action),自动计算 TP/TN/FP/FN。
5.3 “如何保证考试过程防作弊?”——分层防御,不依赖单一技术
系统未采用人脸识别(需大量样本训练),而是组合策略:
| 层级 | 措施 | 技术实现 |
|---|---|---|
| 前端 | 禁用右键、F12、截图快捷键 | document.addEventListener('contextmenu', e => e.preventDefault())+window.addEventListener('keydown', e => {if(e.key==='F12') e.preventDefault()}) |
| 网络 | 防多开浏览器 | 提交时校验session.get('student_id')与request.headers.get('User-Agent')组合唯一性,重复提交返回 403 |
| 服务端 | 防答案泄露 | 试卷 JSON 通过json.dumps(questions, separators=(',', ':'))压缩传输,禁用浏览器 DevTools 查看原始数据 |
关键技巧:在
templates/student/exam.html中,题目内容用{{ question|safe }}渲染,但question.content已在后端做过 HTML 转义(html.escape()),既防 XSS 又保格式。此细节常被忽略,却是答辩时体现安全意识的加分项。
本文还有配套的精品资源,点击获取