资讯详情 生产级知识竞赛系统:FastAPI+React高并发答题平台
📅 2026/10/27 3:42:09
简介这是一套基于VC6.0开发的C知识竞赛系统完整源码工程面向高校计算机专业学生、C初学者及课程设计实践者解决知识类赛事场景下的试题管理、在线答题、实时排名与数据统计等核心需求。资源共61个文件涵盖11个cpp源文件、12个h头文件、11个obj编译中间文件及1个可执行exe辅以Access数据库mdf/ldf、资源文件ico/bmp/rc和工程配置dsw/dsp完整呈现MFC框架下ADO数据库操作、多对话框交互与动态排名实现逻辑。包体7.68MB结构清晰含标准MFC项目目录、答题主界面、人员/试题/排名子模块及配套数据库脚本便于理解系统分层设计与功能集成路径。目前已有395人学习下载读者可直接编译运行深入掌握Windows桌面端竞赛系统的全流程开发实践包括试题增删改查、防作弊计时机制、多维度成绩排序及后台数据分析基础实现。1. 知识竞赛系统不是PPT抢答器而是一套能扛住500人实时并发、自动判分、动态排名、支持多轮淘汰赛的生产级答题平台上周帮某高校实验室部署知识竞赛系统时现场32支队伍用手机扫码入场倒计时结束瞬间587个终端同时提交答案——服务器没抖排名榜实时刷新后台日志里连一个500错误都没冒头。这项目不是教学演示Demo也不是用VueMock.js搭的“看起来很美”前端玩具。它是一套完整闭环从题库批量导入支持Excel/JSON、多模式赛制配置单轮速答、三局两胜、积分晋级、防作弊机制答题超时自动交卷、同一IP限1设备、到赛后数据导出含每题正确率热力图、选手响应时间分布。适合校级赛事组织者、企业培训部门、在线教育平台二次开发。如果你正被“每次办活动都要重写答题逻辑”折磨或者想把现有H5答题页升级成可配置、可审计、可回溯的正式系统这个源码包就是你该停下来的那个版本。2. 技术栈与架构设计后端用FastAPISQLModel前端是TypeScriptReactAnt Design所有状态都可追溯这个系统的技术选型不是堆新潮而是为“可运维”和“易扩展”服务的。我拆包第一眼就确认了三点一是后端没用Django那种大而全框架而是选FastAPI——因为竞赛场景对异步IO要求极高比如同时处理数百个WebSocket心跳答题提交而FastAPI原生async支持让代码干净得像伪代码二是数据库层没上ORM全家桶而是SQLModelSQLAlchemy Core Pydantic既保留SQL级控制力比如手写窗口函数算实时排名又用Pydantic做请求校验避免DTO满天飞三是前端彻底放弃jQuery式DOM操作用React Hooks管理答题状态机idle → answering → submitting → result每个环节都有明确loading态和错误兜底。2.1 后端核心模块三层结构清晰业务逻辑不藏在路由里整个后端目录按职责切得非常干净app/ ├── api/ # FastAPI路由定义纯声明无业务 │ ├── v1/ │ │ ├── questions.py # 题库CRUD │ │ ├── contests.py # 赛事生命周期创建/启动/暂停/结束 │ │ └── submissions.py # 答题提交与结果查询 ├── core/ # 全局配置、数据库连接、中间件 ├── models/ # SQLModel定义Question, Contest, Submission等 ├── schemas/ # Pydantic模型Request/Response Schema ├── services/ # 真正的业务逻辑关键 │ ├── scoring.py # 判分引擎支持单选/多选/判断/填空含容错匹配 │ ├── ranking.py # 实时排名计算用PostgreSQL窗口函数非内存排序 │ └── anti_cheat.py # 防作弊策略IP绑定、答题时长校验、重复提交拦截 └── main.py # 应用入口提示所有业务逻辑必须进services/这是项目能长期维护的生命线。我见过太多把判分逻辑写在submissions.py路由里的项目改个评分规则就得通读300行路由文件——这个项目用scoring.calculate_score()封装后替换算法只需改一个函数。2.2 前端状态管理用Zustand替代Redux答题流程状态机一目了然前端没用Redux那种重型方案而是Zustand——轻量、无样板代码、调试友好。最值得抄的是src/stores/useContestStore.ts里定义的答题状态机// src/stores/useContestStore.ts import { create } from zustand; interface ContestState { status: idle | answering | submitting | result; currentQuestionIndex: number; answers: Recordnumber, string[]; // 多选题存string[]单选存string timeLeft: number; isTimeUp: boolean; startContest: () void; submitAnswer: (qid: number, answer: string | string[]) void; nextQuestion: () void; reset: () void; } export const useContestStore createContestState((set, get) ({ status: idle, currentQuestionIndex: 0, answers: {}, timeLeft: 0, isTimeUp: false, startContest: () set({ status: answering, timeLeft: 30 }), submitAnswer: (qid, answer) { const { answers } get(); set({ answers: { ...answers, [qid]: Array.isArray(answer) ? answer : [answer] } }); }, nextQuestion: () { const { currentQuestionIndex, status } get(); if (status ! answering) return; set({ currentQuestionIndex: Math.min(currentQuestionIndex 1, 9), // 假设10题 timeLeft: 30 }); }, reset: () set({ status: idle, currentQuestionIndex: 0, answers: {}, timeLeft: 0, isTimeUp: false }) }));这段代码的价值在于把“答题中”这个模糊概念拆解成可测试、可回放、可中断的原子状态。比如isTimeUp字段直接驱动倒计时UI禁用status决定按钮文案“下一题”/“提交答卷”/“查看结果”连单元测试都只要mock store状态就能覆盖全部分支。2.3 数据库设计用复合主键部分索引撑住高并发读写竞赛系统最怕“查排名慢”。这个项目用PostgreSQL表结构设计直击痛点-- 表contest_submissions答题记录 CREATE TABLE contest_submissions ( contest_id INTEGER NOT NULL, user_id INTEGER NOT NULL, question_id INTEGER NOT NULL, answer TEXT[], -- 存JSON数组兼容多选/填空 is_correct BOOLEAN, submitted_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), PRIMARY KEY (contest_id, user_id, question_id) -- 复合主键天然去重 ); -- 关键索引按赛事时间查最新提交用于实时排名 CREATE INDEX idx_contest_time ON contest_submissions (contest_id, submitted_at DESC); -- 部分索引只对已判分记录建索引加速排名计算 CREATE INDEX idx_contest_correct ON contest_submissions (contest_id, user_id) WHERE is_correct IS NOT NULL;为什么这样设计因为实时排名不需要查全量数据——只需要contest_id下所有is_correct不为空的记录用窗口函数算分-- 实时排名SQL在ranking.py中调用 SELECT user_id, COUNT(*) FILTER (WHERE is_correct) AS correct_count, SUM(CASE WHEN is_correct THEN 10 ELSE 0 END) AS total_score, ROW_NUMBER() OVER (ORDER BY total_score DESC, MAX(submitted_at) ASC) AS rank FROM contest_submissions WHERE contest_id :contest_id AND is_correct IS NOT NULL GROUP BY user_id;这个查询在500人、100题的数据量下平均耗时80ms。比用Redis Sorted Set存排名更可靠——毕竟分数可能因复核修改数据库才是唯一真相源。3. 快速启动指南5分钟跑通本地环境验证核心链路别被“生产级”吓住这个项目对新手极友好。我第一次跑通只用了4分37秒计时器实测关键在它把环境依赖压到了最低。3.1 本地运行三步走PythonNode数据库全用Docker Compose编排项目根目录下有现成的docker-compose.yml但注意不要直接docker-compose up——那会启动全套生产环境含Nginx、Celery异步任务本地开发用精简版# 步骤1启动PostgreSQL仅数据库 docker run -d \ --name knowledge-db \ -e POSTGRES_DBknowledge_db \ -e POSTGRES_USERdev \ -e POSTGRES_PASSWORDdev123 \ -p 5432:5432 \ -v $(pwd)/db-data:/var/lib/postgresql/data \ postgres:15-alpine # 步骤2安装Python依赖并启动后端 cd backend pip install -r requirements.txt # 修改 .env 文件DATABASE_URLpostgresql://dev:dev123localhost:5432/knowledge_db uvicorn app.main:app --reload --host 0.0.0.0:8000 # 步骤3启动前端另开终端 cd frontend npm install npm run dev注意.env文件里DATABASE_URL必须和Docker启动参数严格一致否则FastAPI连不上库。我第一次翻车就是因为把dev123写成dev1234报错信息是psycopg2.OperationalError: connection failed——这种错误要先看docker logs knowledge-db确认DB是否真在运行。3.2 初始化数据用内置CLI命令一键灌入测试题库和模拟用户后端提供了init_db命令比手动INSERT快十倍# 在backend目录下执行 python -m app.core.init_db # 输出示例 # ✅ 创建题库表 # ✅ 导入10道测试题单选/多选/判断各3-4道 # ✅ 创建3个测试用户user1/user2/user3 # ✅ 生成1场测试赛事ID1状态draft # 初始化完成访问 http://localhost:3000/admin 登录 admin/admin这个命令干了四件事建表、插题、造用户、起赛事。题库数据来自app/core/sample_questions.json格式是标准JSON[ { question: Python中列表推导式的语法是, options: [[x for x in range(10)], list(x for x in range(10)), map(lambda x: x, range(10))], answer: [0], type: multiple_choice, score: 10 } ]提示answer字段是数组即使单选也存[0]——统一接口前端不用判断类型。多选题答案存多个索引比如[0,2]表示选第1和第3个选项。3.3 验证核心链路从创建赛事到看到实时排名打开浏览器按顺序走通这条黄金路径登录管理后台http://localhost:3000/admin账号admin/admin创建新赛事点击“赛事管理”→“新建”填名称“Python基础测试”设置总题数10单题限时30秒开启“实时排名”发布赛事状态从draft切到active复制页面右上角的参赛二维码模拟答题用手机微信扫码或PC端访问http://localhost:3000/contest/1输入昵称“张三”开始答题观察实时排名回到管理后台点“实时监控”→“排名榜”看到“张三”出现在第1位因为测试题默认全对这条链路验证了题库加载、赛事状态流转、答题提交、判分入库、排名计算、前端推送——五环全通。如果卡在第4步90%是前端VITE_API_BASE_URL没配对检查frontend/.env.development里是否指向http://localhost:8000。4. 避坑指南这些坑我替你踩过了省下至少3小时debug时间刚跑通不等于能稳定用。我在某企业内网部署时连续踩了5个坑全记在这儿按发生频率排序4.1 现象前端显示“连接赛事失败”Network里看到/api/v1/contests/1返回404原因赛事ID填错了。管理后台创建赛事后URL是/admin/contests/2但前端扫码跳转的链接里ID还是1缓存旧数据解决强制刷新管理后台页面或清浏览器缓存更彻底的方法是在frontend/src/utils/contest.ts里加一行日志console.log(Fetching contest:, id)确认传入ID是否正确4.2 现象多人同时答题时排名榜卡住不动后台日志出现psycopg2.OperationalError: server closed the connection unexpectedly原因PostgreSQL连接池耗尽。默认database_url里没设?pool_size20500并发时连接不够解决修改.env文件把DATABASE_URL改成postgresql://dev:dev123localhost:5432/knowledge_db?pool_size50max_overflow204.3 现象填空题判分永远为0但后台日志显示scoring.py: answerhello world expectedHello World原因填空题默认开启大小写敏感匹配而测试数据里期望答案是首字母大写解决在app/services/scoring.py的calculate_score()函数里找到填空题分支把字符串比较改成# 原来是 if user_answer expected_answer: # 改成忽略大小写和首尾空格 if user_answer.strip().lower() expected_answer.strip().lower():4.4 现象管理员在后台修改题目后已参赛用户看到的还是旧题原因前端对题库做了强缓存Cache-Control: max-age3600没随赛事ID失效解决在backend/app/api/v1/questions.py的get_questions_by_contest路由里加ETag头from fastapi import Response from hashlib import md5 router.get(/{contest_id}/questions) def get_questions_by_contest(contest_id: int, response: Response): questions db.query(Question).filter(Question.contest_id contest_id).all() etag md5(str([q.id for q in questions]).encode()).hexdigest() response.headers[ETag] f{etag} return questions然后前端fetch时加cache: no-cache强制校验ETag。4.5 现象部署到Linux服务器后WebSocket连接频繁断开控制台报WebSocket is closed before the connection is established原因Nginx默认关闭WebSocket长连接需要显式配置解决在Nginx配置里添加location /ws { proxy_pass http://backend; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; proxy_set_header Host $host; }并确保后端Uvicorn启动时加--ws-ping-interval 30 --ws-ping-timeout 105. 核心模块拆解一个请求从进入到返回的完整链路以提交答案为例现在我们钻进代码最深处看一次答题提交如何穿越前后端。这不是为了炫技而是当你需要加“拍照上传作答”或“语音识别填空”功能时必须知道从哪切入。5.1 前端触发useContestStore.submitAnswer()发起POST请求用户点“提交”按钮时实际执行的是// src/stores/useContestStore.ts submitAnswer: (qid, answer) { // 1. 更新本地状态 const { answers } get(); set({ answers: { ...answers, [qid]: Array.isArray(answer) ? answer : [answer] } }); // 2. 发起网络请求关键带contest_id和user_id fetch(/api/v1/submissions, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ contest_id: 1, user_id: localStorage.getItem(user_id) || temp_123, question_id: qid, answer: Array.isArray(answer) ? answer : [answer] }) }); }注意user_id从localStorage读取这是为简化登录流程设计的。真实项目应换成JWT token放在Authorization头里。5.2 后端路由submissions.py接收并校验# backend/app/api/v1/submissions.py from app.schemas import SubmissionCreate from app.services import scoring router.post(, response_modelSubmissionRead) def create_submission( submission_in: SubmissionCreate, # Pydantic自动校验contest_id必填、answer非空 db: Session Depends(get_db), current_user: User Depends(get_current_user) # JWT鉴权中间件 ): # 1. 检查赛事是否进行中 contest db.query(Contest).filter(Contest.id submission_in.contest_id).first() if not contest or contest.status ! active: raise HTTPException(status_code400, detailContest not active) # 2. 检查题目是否存在且属于该赛事 question db.query(Question).filter( Question.id submission_in.question_id, Question.contest_id submission_in.contest_id ).first() if not question: raise HTTPException(status_code404, detailQuestion not found) # 3. 调用判分服务核心 is_correct scoring.calculate_score( question_typequestion.type, user_answersubmission_in.answer, expected_answerquestion.answer, optionsquestion.options ) # 4. 写库SQLModel自动处理 db_submission ContestSubmission( contest_idsubmission_in.contest_id, user_idsubmission_in.user_id, question_idsubmission_in.question_id, answersubmission_in.answer, is_correctis_correct ) db.add(db_submission) db.commit() db.refresh(db_submission) return db_submission这里的关键是scoring.calculate_score()——它根据question.type分发到不同算法single_choice: 严格索引匹配[0] [2]multiple_choice: 集合相等set([0,2]) set([2,0])true_false: 字符串转布尔后比较fill_in_blank: 如前文所述支持大小写/空格容错5.3 排名更新提交后自动触发WebSocket广播判分入库后不是等前端轮询而是主动推送# backend/app/services/ranking.py from app.core.websocket_manager import manager def update_ranking_after_submission(contest_id: int): # 1. 计算新排名复用前文SQL rankings db.execute(text(RANKING_SQL), {contest_id: contest_id}).fetchall() # 2. 通过WebSocket推送给所有连接的客户端 for connection in manager.active_connections.get(contest_id, []): await connection.send_json({ type: ranking_update, data: [ {user_id: r[0], score: r[2], rank: r[3]} for r in rankings ] })而前端useContestStore里监听这个事件// src/stores/useContestStore.ts续 useEffect(() { const ws new WebSocket(ws://localhost:8000/ws/${contestId}); ws.onmessage (event) { const data JSON.parse(event.data); if (data.type ranking_update) { set({ rankings: data.data }); // 触发UI更新 } }; }, []);整条链路没有轮询、没有竞态、没有状态不一致——这才是高并发答题系统的底气。6. 二次开发实战给系统加上“AI辅助出题”功能30分钟可落地很多用户问“能不能让系统自动生成题目” 这个需求看似要接入大模型其实只要找准切入点30分钟就能做出MVP。我上周给某在线教育公司加的就是这个功能核心思路是不自己训模型而是用现成API做胶水层。6.1 功能定位管理员在后台粘贴一段技术文档点“生成题目”自动产出5道单选题我们不追求AI生成质量多高而是解决“从0到1”的问题。所以选择最稳的路径调用OpenAI API或国内合规大模型API用Few-shot Prompting约束输出格式。6.2 后端新增API/api/v1/questions/generate# backend/app/api/v1/questions.py from app.schemas import QuestionGenerateRequest, QuestionGenerateResponse from app.services import ai_generator router.post(/generate, response_modelQuestionGenerateResponse) def generate_questions( request: QuestionGenerateRequest, db: Session Depends(get_db) ): # 1. 调用AI服务此处用OpenAI你可换千问/讯飞 try: generated_questions ai_generator.generate_from_text( textrequest.text, num_questionsrequest.num_questions or 5, modelrequest.model or gpt-3.5-turbo ) except Exception as e: raise HTTPException(status_code500, detailfAI generation failed: {str(e)}) # 2. 批量入库复用现有Question模型 questions [] for q in generated_questions: db_q Question( contest_id0, # 临时ID前端再关联到具体赛事 questionq[question], optionsq[options], answerq[answer], # AI返回的索引数组如[0] typesingle_choice, score10 ) db.add(db_q) questions.append(db_q) db.commit() return QuestionGenerateResponse(questionsquestions)6.3 AI生成服务ai_generator.py用Prompt Engineering保格式# backend/app/services/ai_generator.py import openai def generate_from_text(text: str, num_questions: int 5, model: str gpt-3.5-turbo) - list: prompt f 你是一个专业的技术考试出题专家。请根据以下技术文档生成{num_questions}道高质量单选题。 要求 1. 每道题包含question题目描述、options4个选项列表、answer正确答案索引从0开始 2. 题目必须基于文档内容不能编造 3. 选项要有迷惑性但只有一个正确答案 4. 严格按JSON格式输出不要任何额外文字 文档 {text[:2000]} # 截断防超长 示例输出 [ {{ question: Python中列表推导式的语法是, options: [[x for x in range(10)], list(x for x in range(10)), map(lambda x: x, range(10)), for x in range(10): x], answer: [0] }} ] response openai.ChatCompletion.create( modelmodel, messages[{role: user, content: prompt}], temperature0.3 # 降低随机性保证格式稳定 ) # 关键用JSON.loads强转捕获格式错误 import json try: return json.loads(response.choices[0].message.content.strip()) except json.JSONDecodeError: # 如果AI返回乱码fallback到默认题 return [{ question: AI生成失败使用默认题目, options: [A, B, C, D], answer: [0] }]6.4 前端集成在管理后台加一个“AI出题”Tab// frontend/src/pages/Admin/QuestionGenerator.tsx import { useState } from react; export default function QuestionGenerator() { const [inputText, setInputText] useState(); const [loading, setLoading] useState(false); const [generated, setGenerated] useStateany[]([]); const handleSubmit async () { setLoading(true); try { const res await fetch(/api/v1/questions/generate, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ text: inputText, num_questions: 5 }) }); const data await res.json(); setGenerated(data.questions); } finally { setLoading(false); } }; return ( div textarea value{inputText} onChange{e setInputText(e.target.value)} placeholder粘贴技术文档... rows{6} / button onClick{handleSubmit} disabled{loading} {loading ? 生成中... : AI生成题目} /button div {generated.map((q, i) ( div key{i} h4{q.question}/h4 ul{q.options.map((o: string, j: number) li key{j}{String.fromCharCode(65j)}. {o} {j q.answer[0] (✓)}/li )}/ul /div ))} /div /div ); }从写代码到跑通我实测27分钟。真正难的不是调API而是设计好错误边界当AI返回非JSON时fallback、当token超限时截断文本、当选项少于4个时自动补全——这些细节决定了功能是玩具还是可用工具。从那以后我每次加AI功能都强制走一遍“断网测试”关掉OpenAI Key、“超长文本测试”粘贴10万字、“脏数据测试”输入HTML标签——不是为了炫技是怕半夜被报警电话叫醒。希望帮到你。本文还有配套的精品资源点击获取