简介:这是一套基于Python Flask框架构建的在线评测系统(OJ)完整毕业设计项目,面向计算机类专业本科生及初学者,解决课程设计、毕设选题与Web后端+评测逻辑实践落地难题。资源包含276个文件,主体为33个核心Python源码(含Flask路由、判题引擎、数据库交互模块)、17个HTML模板页与配套前端资源(25个JS、20个CSS、41个SCSS),以及可直接运行的虚拟环境脚本(activate.bat、pyvenv.cfg等),整体压缩包仅8.47MB,轻量易部署。已有116人学习下载,项目经实测可稳定运行,附带详细使用文档与README说明,覆盖环境搭建、题目录入、代码提交、实时判题全流程。读者可直接用于毕设答辩或课设交付,亦可基于现有模块拓展多语言支持、测试用例管理或Vue前端对接,是理解OJ系统架构与工程化开发的优质入门范例。
1. 这不是个“能跑就行”的Flask Demo,而是一套可真实提交、实时判题、带前端交互的OJ闭环系统
很多同学拿到毕业设计选题时,看到“在线评测系统”第一反应是:不就是写个表单+后端接收+调用Python脚本跑测试用例?但真正跑起来才发现——输入输出格式错一位就WA,多进程并发判题卡死,超时没杀进程导致服务器负载飙升,前端提交后页面假死却不知判题状态……这套基于Python-Flask的OJ评测平台源码,恰恰绕开了这些新手陷阱:它用subprocess.run封装了安全沙箱式执行(非os.system),内置time_limit和memory_limit硬约束,前端Vue组件与Flask RESTful接口解耦清晰,连bootstrap.css和font-awesome.min.css都已预编译进静态资源目录。它不是教学Demo,而是按真实课程设计验收标准打磨过的完整工程——支持题目增删改查、用户注册登录、代码提交、实时状态轮询(/api/status)、判题日志回溯,甚至包含activate.bat一键激活虚拟环境的Windows适配逻辑。适合软件工程、计科、人工智能方向学生直接用于毕设答辩,也适合想深入理解Web服务与系统编程边界的开发者拆解学习。
2. Flask后端核心判题引擎:从代码沙箱到结果解析的全流程控制
2.1 判题流程设计原理:为什么不用Celery而用同步阻塞式执行?
该OJ平台未引入消息队列(如Celery),而是采用Flask路由内同步调用判题逻辑。这种设计并非技术落后,而是针对毕业设计场景的务实选择:
- 可控性优先:避免Celery worker部署、Redis依赖、任务丢失等额外运维复杂度,学生在本地Windows环境用
activate.bat启动即可全链路调试; - 状态透明:每个HTTP请求对应一次判题,
/submit接口返回JSON含status(pending→running→accepted/wrong_answer)、time_used_ms、memory_used_kb、output(标准输出截断)字段,便于前端轮询渲染; - 资源隔离刚需:通过
subprocess.run的timeout参数强制中断超时进程,配合ulimit -v(Linux)或psutil.Process().memory_info().rss(跨平台)限制内存,规避恶意代码耗尽服务器资源。
提示:源码中
judge/core.py的run_code()函数是判题核心,其关键参数timeout=3(秒)、memory_limit_mb=64(MB)需根据题目难度调整。若部署到Linux服务器,建议在/etc/security/limits.conf中为运行用户添加soft as 65536限制虚拟内存。
2.2 安全沙箱实现:如何防止import os; os.system('rm -rf /')类攻击?
判题模块未使用Docker或seccomp,而是通过三层防护构建轻量级沙箱:
- 进程级隔离:
subprocess.run启用shell=False,避免命令注入;cwd指定独立临时目录(如/tmp/judge_12345),所有I/O操作限定在此路径; - 系统调用过滤:Windows下通过
psutil监控子进程创建的子进程数(防fork bomb),Linux下用prctl(PR_SET_NO_NEW_PRIVS, 1)禁用提权; - 文件系统白名单:判题前清空临时目录,仅保留
main.py(用户代码)和test_input.txt(题目输入),禁止访问/etc/、/home/等敏感路径。
以下为judge/core.py中关键判题代码段(已脱敏):
# python judge/core.py import subprocess import tempfile import os import psutil from pathlib import Path def run_code(code_content: str, input_data: str, timeout: int = 3, memory_limit_mb: int = 64) -> dict: # 创建独立临时目录 with tempfile.TemporaryDirectory() as tmp_dir: code_path = Path(tmp_dir) / "main.py" input_path = Path(tmp_dir) / "test_input.txt" # 写入用户代码和测试输入 code_path.write_text(code_content, encoding="utf-8") input_path.write_text(input_data, encoding="utf-8") # 构建执行命令(Python3环境) cmd = ["python", str(code_path)] try: # 关键:设置超时、捕获stdout/stderr、限定工作目录 result = subprocess.run( cmd, input=input_data.encode("utf-8"), stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=tmp_dir, # 强制工作目录为临时目录 timeout=timeout, check=False # 不抛异常,便于捕获返回码 ) # 内存监控(Linux示例,Windows用psutil.Process().memory_info().rss) if os.name == 'posix': try: proc = psutil.Process(result.pid) mem_kb = proc.memory_info().rss // 1024 if mem_kb > memory_limit_mb * 1024: return {"status": "memory_limit_exceeded", "memory_used_kb": mem_kb} except (psutil.NoSuchProcess, psutil.AccessDenied): pass # 解析判题结果 output = result.stdout.decode("utf-8", errors="ignore")[:500] # 截断防爆屏 error = result.stderr.decode("utf-8", errors="ignore")[:200] if result.returncode == 0: return {"status": "accepted", "output": output, "time_used_ms": int(result.time * 1000)} elif result.returncode == 1: return {"status": "wrong_answer", "error": error, "output": output} else: return {"status": "runtime_error", "error": error} except subprocess.TimeoutExpired: return {"status": "time_limit_exceeded"} except Exception as e: return {"status": "system_error", "error": str(e)}2.2.1 参数说明与调试技巧
timeout:单位为秒,值过小导致正常代码被误判TLE,过大则影响并发吞吐。建议从2秒起步,对DFS/BFS类题目调至5秒;memory_limit_mb:需预留Python解释器基础开销(约10MB),实际可用内存≈limit - 10;output截断长度500字符:防止恶意代码输出GB级日志撑爆数据库,前端展示时用<pre class="text-truncate">处理;result.returncode判断逻辑:0为AC,1为WA(常见于算法错误),非0非1值(如-9)通常对应SIGKILL(超内存)或SIGTERM(超时)。
2.3 Flask路由与RESTful接口设计:如何支撑前端Vue的实时状态轮询?
平台采用纯RESTful风格设计API,无Session状态,全部通过JWT或简单Token鉴权(源码中auth.py实现)。关键接口如下表所示:
| 接口路径 | HTTP方法 | 功能说明 | 前端调用时机 |
|---|---|---|---|
/api/problems | GET | 获取题目列表(含ID、标题、描述、时间/内存限制) | 页面加载时 |
/api/problem/<int:pid> | GET | 获取单题详情(含样例输入/输出) | 点击题目时 |
/api/submit | POST | 提交代码(JSON body:{code, language, problem_id}) | 点击“提交”按钮 |
/api/status/<string:token> | GET | 查询判题状态(返回pending/running/accepted等) | 提交后每1.5秒轮询 |
app.py中/api/submit路由实现示例:
# python app.py from flask import Flask, request, jsonify from judge.core import run_code import uuid import json @app.route('/api/submit', methods=['POST']) def submit_code(): data = request.get_json() code = data.get('code', '') problem_id = data.get('problem_id') language = data.get('language', 'python3') # 校验输入 if not code.strip() or not problem_id: return jsonify({"error": "code and problem_id required"}), 400 # 模拟获取题目测试用例(实际从DB读取) test_input = get_test_input(problem_id) # 此函数需自行实现 # 生成唯一判题Token(替代数据库存储状态) token = str(uuid.uuid4()) # 同步执行判题(生产环境应改为异步,此处为简化) result = run_code(code, test_input, timeout=3, memory_limit_mb=64) result['token'] = token # 将结果缓存到内存字典(生产环境用Redis) # cache[token] = result return jsonify(result)注意:源码中
cache为内存字典,仅适用于单进程开发模式。若需多Worker部署,必须替换为Redis缓存,并在/api/status中查询Redis key。token字段是前端轮询的关键凭证,不可省略。
3. Vue前端与Flask静态资源协同:Bootstrap+Font Awesome的快速UI落地
3.1 静态资源组织结构:为什么bootstrap.min.css和font-awesome.min.css必须放在static/css/?
项目前端未使用Webpack/Vite构建,而是直接引用CDN或本地静态文件,这是课程设计的典型折中方案:
- 零构建依赖:
activate.bat启动Flask后,http://127.0.0.1:5000直接访问HTML,无需npm run serve; - 离线可用性:所有CSS/JS均下载到
static/目录,断网环境仍可运行(bootstrap.min.css压缩后仅200KB); - 版本锁定:
font-awesome.min.css对应v4.7.0(非v6),因v6需订阅且图标类名变更,旧版更适配毕业设计周期。
目录结构关键路径:
/static/ ├── css/ │ ├── bootstrap.min.css # Bootstrap 4.6.0 压缩版 │ ├── font-awesome.min.css # Font Awesome 4.7.0 图标库 │ └── custom.css # 自定义样式(如题目描述字体大小) ├── js/ │ ├── jquery-3.5.1.min.js # Bootstrap依赖 │ ├── bootstrap.min.js # Bootstrap JS组件 │ └── app.js # Vue实例与API交互逻辑 └── uploads/ # 用户头像等上传文件(需权限配置)3.2 Vue组件与Flask数据流:如何用原生fetch实现无框架提交?
static/js/app.js中Vue实例通过fetch调用Flask API,规避了Axios等第三方库依赖。核心提交逻辑如下:
// static/js/app.js new Vue({ el: '#app', data: { problems: [], currentProblem: {}, code: '', language: 'python3', status: 'idle', // idle / submitting / pending / accepted result: {} }, methods: { async submitCode() { this.status = 'submitting'; try { const response = await fetch('/api/submit', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ code: this.code, problem_id: this.currentProblem.id, language: this.language }) }); const data = await response.json(); if (response.ok) { this.status = 'pending'; this.result = data; this.pollStatus(data.token); // 开始轮询 } else { alert('提交失败:' + (data.error || '未知错误')); this.status = 'idle'; } } catch (e) { alert('网络错误:' + e.message); this.status = 'idle'; } }, async pollStatus(token) { if (this.status !== 'pending') return; try { const res = await fetch(`/api/status/${token}`); const data = await res.json(); if (data.status === 'pending' || data.status === 'running') { setTimeout(() => this.pollStatus(token), 1500); // 1.5秒轮询 } else { this.status = data.status; this.result = data; } } catch (e) { console.error('轮询失败', e); this.status = 'idle'; } } } });3.2.1 关键细节说明
fetch未设置credentials: 'include':因系统无登录态(或使用简单Token),避免CORS问题;setTimeout轮询而非WebSocket:降低部署复杂度,Flask原生支持;this.result直接绑定到DOM:Vue响应式更新<div v-if="status==='accepted'">等区块,无需额外状态管理库。
3.3 Bootstrap组件实战:用<table>和<progress>可视化判题过程
题目列表页使用Bootstrap 4的.table-hover和.table-striped增强可读性:
<!-- templates/problems.html --> <table class="table table-hover table-striped"> <thead> <tr> <th>ID</th> <th>题目</th> <th>时限</th> <th>内存</th> <th>操作</th> </tr> </thead> <tbody> {% for p in problems %} <tr> <td>{{ p.id }}</td> <td><a href="{{ url_for('problem', pid=p.id) }}">{{ p.title }}</a></td> <td>{{ p.time_limit }}ms</td> <td>{{ p.memory_limit }}MB</td> <td> <button class="btn btn-sm btn-primary" @click="selectProblem(p)"> <i class="fa fa-play"></i> 提交 </button> </td> </tr> {% endfor %} </tbody> </table>判题状态页用<progress>显示进度(语义化且无需JS绘制):
<!-- templates/judge_status.html --> <div v-if="status === 'running'"> <h5>判题中...</h5> <progress value="50" max="100" class="w-100"></progress> <small class="text-muted">正在执行您的代码,请稍候</small> </div>提示:
<progress>在Chrome/Firefox中默认样式美观,Safari需加CSS微调。若需精确百分比,可改用<div class="progress"><div class="progress-bar" role="progressbar" :style="{width: progress + '%'}"></div></div>。
4. 本地开发环境搭建:从activate.bat到pyvenv.cfg的Windows友好型配置
4.1activate.bat的隐藏逻辑:为什么它比Linux的source activate更鲁棒?
项目根目录下的activate.bat并非简单调用Scripts/activate.bat,而是包含三重保障机制:
@echo off :: activate.bat echo 正在检查Python环境... where python >nul 2>&1 if %ERRORLEVEL% neq 0 ( echo 错误:未找到Python,请先安装Python 3.8+ pause exit /b 1 ) :: 检查venv是否存在 if not exist "venv" ( echo 正在创建虚拟环境... python -m venv venv if %ERRORLEVEL% neq 0 ( echo 虚拟环境创建失败,请检查pip权限 pause exit /b 1 ) ) :: 激活并升级pip echo 正在激活环境并升级pip... call venv\Scripts\activate.bat python -m pip install --upgrade pip >nul 2>&1 :: 安装依赖(requirements.txt存在则用,否则用内置列表) if exist "requirements.txt" ( pip install -r requirements.txt ) else ( pip install flask==2.2.5 pytest==7.2.0 psutil==5.9.5 ) echo 环境准备完成!执行 python app.py 启动服务 pause4.1.1 关键设计点解析
where python检测全局Python:避免学生误装Python 2.x或路径未加入PATH;if not exist "venv"自动创建:解决新手忘记python -m venv venv步骤;pip install --upgrade pip:防止旧版pip无法安装Flask 2.2+(需pip≥21.0);- 回退到内置包列表:当
requirements.txt缺失时,保证核心依赖(Flask/psutil)必装。
4.2pyvenv.cfg配置项详解:include-system-site-packages=false的意义
venv/pyvenv.cfg是虚拟环境元数据文件,其内容直接影响依赖隔离效果:
home = C:\Python38 include-system-site-packages = false version = 3.8.10include-system-site-packages = false:强制隔离,确保import flask只从venv\Lib\site-packages\加载,避免系统级Flask(如pip install flask全局安装)干扰;home字段:指向Python解释器真实路径,venv\Scripts\python.exe通过此路径定位python38.dll;version:记录创建环境时的Python版本,Flask 2.2.5要求Python≥3.7,此字段用于兼容性校验。
提示:若遇到
ModuleNotFoundError: No module named 'flask',首先检查venv\Lib\site-packages\目录是否存在flask-2.2.5.dist-info文件夹,再确认pyvenv.cfg中include-system-site-packages为false。
4.3bootstrap-grid.css的响应式断点适配:如何让题目列表在手机端可滚动?
项目未使用Bootstrap 5的Flexbox网格,而是保留bootstrap-grid.css(v4.6.0),因其断点定义更明确:
/* static/css/bootstrap-grid.css 中的断点 */ @media (min-width: 576px) { ... } /* sm */ @media (min-width: 768px) { ... } /* md */ @media (min-width: 992px) { ... } /* lg */ @media (min-width: 1200px) { ... } /* xl */题目列表页添加移动端优化:
<!-- templates/base.html --> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <!-- 在表格外层包裹响应式容器 --> <div class="container-fluid px-0"> <div class="table-responsive"> <table class="table table-hover mb-0"> <!-- 表格内容 --> </table> </div> </div>table-responsive:为<table>添加水平滚动条,避免小屏设备文字挤压;container-fluid:移除固定宽度,占满屏幕;px-0:消除左右内边距,最大化利用空间。
5. 生产部署与性能调优:从单机Flask到Gunicorn+nginx的平滑过渡
5.1 单机Flask的瓶颈与临界点:何时必须切换WSGI服务器?
python app.py启动的开发服务器(Werkzeug)仅适用于调试,其瓶颈在于:
- 单线程阻塞:默认
threaded=False,同一时刻只能处理1个请求,提交代码时其他用户无法访问; - 无超时控制:
timeout参数在Werkzeug中不生效,判题超时可能永久挂起; - 无进程管理:崩溃后需手动重启,无健康检查。
当并发用户>5人或题目数量>20道时,必须迁移到Gunicorn。迁移步骤如下:
5.1.1 安装Gunicorn并配置启动脚本
# 在venv中安装 pip install gunicorn # 创建gunicorn.conf.py bind = "127.0.0.1:8000" workers = 4 # CPU核心数×2,4核机器设为8 worker_class = "sync" # 同步模式,适配判题IO密集型 timeout = 30 # 全局超时,覆盖Flask内部timeout keepalive = 5 accesslog = "gunicorn_access.log" errorlog = "gunicorn_error.log" loglevel = "info"5.1.2 修改app.py暴露应用实例
# app.py 末尾添加 if __name__ == '__main__': app.run(debug=False) # 开发模式保留 # 新增:供Gunicorn调用的入口 application = app # Gunicorn默认查找名为application的变量启动命令:
gunicorn -c gunicorn.conf.py app:application5.2 nginx反向代理配置:解决静态资源404与HTTPS支持
Gunicorn仅处理动态请求,静态文件(CSS/JS)需由nginx托管。nginx.conf关键配置:
server { listen 80; server_name oj.example.com; # 静态资源直接由nginx服务 location /static/ { alias /path/to/your/project/static/; expires 1h; add_header Cache-Control "public, immutable"; } # 动态请求转发给Gunicorn location / { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } }location /static/:避免Flask处理静态文件,提升30%+吞吐量;expires 1h:浏览器缓存CSS/JS,减少重复请求;X-Forwarded-*头:确保Flask中request.remote_addr获取真实IP。
5.3 判题性能压测:用pytest模拟并发提交验证稳定性
项目自带tests/test_judge.py,可验证高并发下判题可靠性:
# tests/test_judge.py import pytest import threading import time from judge.core import run_code def test_concurrent_judge(): """模拟10个线程同时判题""" def worker(i): code = f"print('Hello from thread {i}')" result = run_code(code, "", timeout=2) assert result["status"] in ["accepted", "time_limit_exceeded"] threads = [] for i in range(10): t = threading.Thread(target=worker, args=(i,)) threads.append(t) t.start() for t in threads: t.join(timeout=5) # 每个线程最多等待5秒 # 验证无资源泄漏(临时目录已清理) assert not any(Path("/tmp").glob("judge_*"))运行命令:
pytest tests/test_judge.py -v提示:若测试失败,重点检查
tempfile.TemporaryDirectory()是否被正确__exit__,以及psutil.Process()在Windows下是否有权限获取子进程信息。
本文还有配套的精品资源,点击获取