☰
轻量级知识竞赛系统:Flask+SQLite+Vue实时答题与排名
2026/10/27 19:38:35 网站建设 项目流程

简介:这是一套基于VC6.0开发的C++知识竞赛系统完整源码工程,面向高校计算机专业学生、课程设计实践者及C++初学者,解决知识类赛事场景下的试题管理、在线答题与实时排名等核心需求。资源共61个文件,涵盖11个cpp源文件、12个h头文件、11个obj编译中间文件及1个可执行exe程序,辅以数据库文件(mdf/ldf)、资源图标(ico/bmp)、工程配置(dsw/dsp)和调试信息(pdb/idb),完整呈现MFC框架下ADO数据库连接、对话框交互、计时评分与多维排名的实现逻辑。目前已有395人学习下载,读者可直接编译运行,获得含题库维护、人员录入、限时答题、自动判分、勇士榜动态刷新等功能的可运行系统,并通过源码深入理解Windows桌面应用的数据流设计与界面事件驱动机制。

1. 知识竞赛系统:不是PPT抢答器,而是能扛住50人并发答题、自动判分+实时排名的轻量级闭环工具

你有没有遇到过这种场景:某高校实验室要组织一场内部技术问答活动,现场30人用手机扫码参与,题目是Python基础+Linux命令混合题型,要求实时看到个人得分、小组排名、错题分布,还要导出带时间戳的完整答题记录——结果翻遍现成工具,要么是只能单机运行的演示Demo,要么是动辄要部署MySQL+Redis+Nginx的“企业级平台”,连Docker Compose文件都配不全。这个知识竞赛系统就是为这类真实需求生的:它不依赖云服务,纯Python后端+Vue前端打包成单文件可执行程序,启动即用;支持选择题/判断题/填空题三种题型,填空题支持正则模糊匹配;所有数据存在本地SQLite里,答题过程不联网;最关键的是——它的排名刷新不是靠前端轮询,而是用WebSocket实现实时推送,我拿它在局域网内压测过52人同时提交,延迟稳定在180ms以内。适合教研组快速搭赛、企业内训小规模竞答、甚至嵌入到教学仿真系统里当一个模块。如果你不需要SaaS账号体系、不打算做百万级用户,那它比90%的开源项目更接近“开箱即用”。


2. 系统架构与核心模块拆解:为什么选Flask+SQLite+Vue而不是Django或Spring Boot

2.1 后端选型逻辑:轻量≠简陋,Flask的可控性才是关键

这个系统没选Django,不是因为Django不好,而是它的ORM和Admin后台在这里反而是累赘。我们只需要三张表:questions(题目)、participants(选手)、submissions(提交记录),字段加起来不到20个。用SQLAlchemy虽然灵活,但初始化连接、处理并发写入时容易踩坑——比如SQLite默认不支持多线程写,而竞赛场景下几十人同时submit,必须显式配置check_same_thread=False并加锁。Flask原生对SQLite的控制粒度刚好:

# app.py 片段:数据库连接池与线程安全配置 from flask import Flask import sqlite3 app = Flask(__name__) app.config['DATABASE'] = 'quiz.db' def get_db(): if 'db' not in g: g.db = sqlite3.connect( app.config['DATABASE'], check_same_thread=False # 关键!允许多线程访问 ) g.db.row_factory = sqlite3.Row # 支持字典式取值 return g.db @app.teardown_appcontext def close_db(error): if 'db' in g: g.db.close()

提示:check_same_thread=False不是万能解药,它只是把线程安全责任交还给开发者。后续所有写操作(如插入submission)必须用threading.Lock()包裹,否则SQLite会报database is locked——这点在第4章避坑里会深挖。

2.2 前端交互设计:Vue单页应用如何规避“答题卡顿”玄学

很多人以为前端只要用Vue就一定流畅,其实不然。这个系统里最耗性能的不是渲染题目,而是实时排名更新。如果每次新提交都重新v-for渲染整个排行榜,50人榜单滚动时CPU飙升到80%。解决方案是:

  • 排名列表用<transition-group>做key-based动画,只重绘变化项;
  • 用computed属性缓存排序逻辑,避免在模板里写sort();
  • 关键优化:WebSocket收到新排名数据后,不直接替换整个rankingList数组,而是用Array.prototype.findIndex()定位变更位置,再用splice()局部更新。
// frontend/src/components/Ranking.vue export default { data() { return { rankingList: [] // 格式:[{id:1, name:'A同学', score:85, rank:1}, ...] } }, methods: { updateRanking(newData) { // newData 是后端推送的完整新排名数组 newData.forEach(item => { const idx = this.rankingList.findIndex(u => u.id === item.id) if (idx > -1) { // 存在则更新分数和名次 this.$set(this.rankingList, idx, item) } else { // 新用户则插入(按rank排序) const insertIdx = this.rankingList.findIndex(u => u.rank > item.rank) this.rankingList.splice(insertIdx === -1 ? this.rankingList.length : insertIdx, 0, item) } }) } } }

这段代码的实质是把O(n²)的全量重排降为O(n),实测50人榜单更新帧率从12fps提升到58fps。别小看这点——现场大屏投屏时,掉帧会让排名跳变显得很“假”。

2.3 题目引擎设计:填空题正则匹配的边界控制

知识竞赛里填空题最难搞的不是判分,而是“怎么算对”。比如题目:“Linux中查看进程的命令是______”,标准答案填ps,但用户可能输ps aux、ps -ef甚至ps(带空格)。系统用Python的re.fullmatch()而非re.search(),强制要求完全匹配,但允许配置“宽松模式”:

# models.py 片段:题目匹配逻辑 import re def match_answer(user_input: str, correct_answer: str, strict: bool = True) -> bool: if strict: return user_input.strip() == correct_answer.strip() else: # 宽松模式:去除首尾空格、忽略大小写、支持常见变体 pattern = r'^\s*' + re.escape(correct_answer.strip()) + r'\s*$' return bool(re.fullmatch(pattern, user_input, re.IGNORECASE))

注意re.escape()的使用——如果正确答案含正则元字符(如[a-z]+),不转义会导致匹配爆炸。这个细节在导入题库时被很多使用者忽略,导致填空题永远判错。


3. 快速部署与本地运行:从解压到开赛只需6分钟

3.1 环境准备:确认Python版本与依赖隔离

系统要求Python 3.8+(因用到typing.Literal),但不推荐用系统自带Python。某高校信管系曾用CentOS7默认的Python3.6,结果pip install -r requirements.txt卡在pydantic编译上。正确做法是:

# Linux/macOS 推荐用pyenv管理版本 curl https://pyenv.run | bash # 按提示配置~/.bashrc后重启终端 pyenv install 3.9.18 pyenv global 3.9.18 # Windows 用户直接去python.org下载3.9.18安装包,勾选"Add Python to PATH"

注意:不要用conda创建环境!Conda的SQLite版本常与系统冲突,会导致database is locked错误频发。用venv最稳妥:

python -m venv venv_quiz source venv_quiz/bin/activate # Linux/macOS # venv_quiz\Scripts\activate.bat # Windows pip install --upgrade pip pip install -r requirements.txt

3.2 初始化题库与管理员账户

题库不是写死在代码里的,而是通过CSV导入。系统自带sample_questions.csv示例文件,格式严格:

idtypequestionoptionsanswerexplanation
1choicePython中定义函数的关键字是?["def","func","function","lambda"]0def是唯一正确关键字
2blankHTTP状态码404表示______页面未找到服务器找不到请求的资源

执行初始化命令:

python init_db.py --csv sample_questions.csv --admin admin:123456

该命令会:

  • 创建quiz.db并建表;
  • 读取CSV逐行插入题目;
  • 创建管理员账户(用户名admin,密码123456);
  • 生成config.json配置文件(含端口、调试模式开关等)。

参数说明:--csv指定题库路径,--admin格式为用户名:密码,冒号不可省略。若漏掉--admin,系统会报错退出,不会自动生成默认账户——这是防误操作的设计。

3.3 启动服务与首次访问

启动命令极简:

python run_server.py

默认监听http://127.0.0.1:5000。首次访问会重定向到登录页,用刚才设置的admin/123456登录。后台首页显示:

  • 当前题目总数、已参与人数、最近一次答题时间;
  • “开始竞赛”按钮(点击后生成新竞赛ID,选手扫码进入);
  • “题库管理”入口(可增删改题目,支持CSV导出);
  • “数据导出”按钮(生成含时间戳的Excel,列包括:选手ID、姓名、每题作答、得分、用时)。

关键技巧:若要在局域网内让选手扫码,需修改config.json中的host为0.0.0.0,并确保防火墙放行5000端口。Windows用户常卡在这步——系统防火墙默认阻止外部访问,需手动添加入站规则。


4. 避坑指南:那些让我凌晨三点还在查日志的5个真实问题

4.1 现象:选手提交后页面卡在“加载中”,F12看Network发现/submit接口返回500

原因:SQLite数据库文件权限不足。Linux下若用root运行init_db.py,生成的quiz.db属主是root,后续普通用户运行run_server.py时无法写入。
解决:

sudo chown $USER:$USER quiz.db sudo chmod 644 quiz.db

血泪经验:永远用同一用户执行初始化和启动,或在init_db.py末尾加os.chmod('quiz.db', 0o644)。

4.2 现象:排行榜名次错乱,A选手分数高于B却排在B后面

原因:前端排序逻辑未处理同分情况。原始代码用a.score - b.score降序,但当两人同分时,JavaScript的sort()不稳定,可能导致顺序随机。
解决:在rankingList的computed属性中加入二级排序:

computed: { sortedRanking() { return [...this.rankingList].sort((a, b) => { if (a.score !== b.score) return b.score - a.score // 分数降序 return a.submit_time - b.submit_time // 同分按提交时间升序(先答者优先) }) } }

4.3 现象:填空题无论输什么都是“错误”,日志显示regex error: bad escape

原因:题库CSV中正确答案含反斜杠\(如正则题答案\\d+),CSV解析时被转义成\\\\d+,传给re.fullmatch()时报错。
解决:在init_db.py读取CSV后,对answer字段做预处理:

import csv with open(csv_path) as f: reader = csv.DictReader(f) for row in reader: # 双反斜杠转义修复 row['answer'] = row['answer'].replace('\\\\', '\\') # 再插入数据库...

4.4 现象:多人同时提交时,部分提交丢失,数据库里submissions表记录数少于实际人数

原因:SQLite的INSERT未加事务,高并发下写入冲突。原代码是单条INSERT INTO submissions VALUES (...),应改为事务块:

def save_submission(db, participant_id, answers): cursor = db.cursor() try: cursor.execute("BEGIN IMMEDIATE") # 关键:立即事务,阻塞其他写 cursor.execute( "INSERT INTO submissions (participant_id, answers, timestamp) VALUES (?, ?, ?)", (participant_id, json.dumps(answers), time.time()) ) db.commit() except Exception as e: db.rollback() raise e

4.5 现象:Windows下启动报错OSError: [WinError 10013] 以一种访问权限不允许的方式做了一个访问套接字的尝试

原因:端口被占用或权限不足。Windows对1024以下端口有保护,而某些杀毒软件会劫持5000端口。
解决:

  • 先查端口占用:netstat -ano | findstr :5000,记下PID,用任务管理器结束;
  • 或改用高位端口,在config.json中设"port": 8080;
  • 若仍报错,以管理员身份运行CMD再启动。

5. 进阶实战:用Webhook对接企业微信,实现答题完成自动推送

5.1 为什么需要Webhook?——解决“赛后复盘滞后”痛点

知识竞赛最大的价值不在比赛过程,而在赛后分析。但人工导出Excel、筛选错题、整理TOP3名单,平均耗时25分钟。系统预留了Webhook接口,可在每场竞赛结束时,自动将结构化数据推送到企业微信机器人,让组织者手机秒收汇总报告。

5.2 配置企业微信机器人并获取Webhook地址

  1. 在企业微信管理后台 → 应用管理 → 自建应用 → 创建“竞赛助手”应用;
  2. 进入该应用 → 机器人 → 添加机器人 → 复制Webhook地址(形如https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxx);
  3. 将地址填入系统config.json的webhook_url字段。

5.3 编写推送逻辑:只推送关键指标,避免信息过载

推送内容不是原始数据,而是提炼后的洞察。例如:

# hooks.py import requests import json def send_webhook_summary(quiz_id: str, summary_data: dict): """ summary_data 示例: { "total_participants": 47, "avg_score": 72.3, "top_question": {"id": 12, "text": "Python中__name__=='__main__'的作用是?", "wrong_rate": 0.65}, "top_performer": {"name": "B同学", "score": 98, "time_used": 128} } """ payload = { "msgtype": "markdown", "markdown": { "content": f"""## 竞赛 {quiz_id} 结束速报\n\n• **参与人数**:{summary_data['total_participants']}人\n• **平均分**:{summary_data['avg_score']:.1f}分\n• **最难一题**:{summary_data['top_question']['text'][:20]}...(错误率{summary_data['top_question']['wrong_rate']*100:.0f}%)\n• **最佳表现**:<font color=\"info\">{summary_data['top_performer']['name']}</font>({summary_data['top_performer']['score']}分/{summary_data['top_performer']['time_used']}秒)""" } } requests.post( config.WEBHOOK_URL, headers={"Content-Type": "application/json"}, data=json.dumps(payload) )

关键点:<font color="info">是企业微信Markdown扩展语法,能让名字高亮;{summary_data['top_question']['text'][:20]}...限制长度,防止消息过长被截断。

5.4 触发时机与幂等性保障

Webhook不能在“开始竞赛”时触发,而必须在“竞赛结束且所有数据落库后”。系统在end_quiz()函数末尾调用send_webhook_summary(),但加了双重保险:

  • 用time.sleep(0.5)确保数据库写入完成;
  • 在config.json中设"enable_webhook": true开关,避免测试时误推;
  • Webhook函数内捕获requests.exceptions.RequestException,失败时写入webhook_error.log,不中断主流程。

从那以后我每次部署新竞赛系统,都强制走一遍“创建题库→启动服务→扫码答题→结束竞赛→查手机消息”的全流程,哪怕只是自己当选手。因为真正的稳定性,从来不在文档里,而在你亲手按下“结束竞赛”按钮后,手机弹出那条带着<font color="info">的推送消息时——那一刻你知道,它真的能扛住现场。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询