资讯详情

在线C语言教学系统从零搭建:Flask+Vue+Docker判题全攻略

📅 2026/10/20 13:14:02 | 华诺云谱 👁 阅读
在线C语言教学系统从零搭建:Flask+Vue+Docker判题全攻略
在线C语言教学系统这个题目在课程设计和毕业设计里出现的频率高得惊人。我自己带过不少学生做类似的项目也帮人改过不少半成品代码发现绝大多数人卡住的点其实高度雷同技术栈选型犹豫不决、前后端联调绕弯路、代码判题逻辑不知道怎么设计。这篇就把这个项目从构思到落地的完整思路捋一遍重点讲清楚“为什么这么做”而不是光堆代码。1. 项目概述与技术选型思路先聊核心问题在线C语言教学系统到底要做什么。一个真正能用的C语言教学平台抛开花里胡哨的UI不谈核心就三件事课程内容管理、代码在线编写与运行、作业/考试判题。如果再延伸一点还有用户管理、学习进度追踪、题库管理这些模块。很多初学者一上来就把系统设计得特别宏大涉及十几个数据表、七八个功能模块结果做到一半代码根本维护不下去。我的建议是第一版老老实实做减法把“管理员、教师、学生”三个角色围绕“课程—作业—判题”这条主线打通就已经是一个相当完整且有说服力的系统了。技术栈选择是另一个纠结的重灾区。题目里出现了Flask、Django、Vue、PyCharm这几个关键词这其实代表了项目常见的几种技术路线选择。我先说结论后端优先选Flask前端用Vue 3 Element Plus开发工具用PyCharm专业版最好。Flask和Django的对比很多文章写得太玄乎我说点实际的。Django自带ORM、Admin后台、认证系统全家桶体验确实好但你一旦用了Django Admin做管理端前后端分离的架构就不伦不类了。而且Django的模板系统和Vue的模板语法都用了双大括号{{ }}二者共存时会出现变量插值冲突你要么改Django模板语法要么改Vue的delimiters总之得额外处理。Flask框架本身非常轻你完全可以用”Flask Flask-SQLAlchemy Flask-RESTful“组装出一个适合前后端分离的轻量后端心智负担比Django小很多。再加上很多学生在学校课程里最早接触的就是Flask用自己最顺手的框架做开发效率绝对比强行上Django高得多。前端选Vue的理由更简单在国内招聘市场和教学场景里Vue的生态和学习曲线比React更友好Element Plus组件库做后台管理界面效率极高表格、表单、弹窗、分页全都是现成的拖过来就能用。这套组合也是目前中小型项目和毕设里最成熟、参考资料最多的技术栈遇到问题搜解决方案一搜一大把。所以最终的架构定下来是后端Flask 2.x Flask-SQLAlchemy Flask-JWT-Extended做Token认证前端Vue 3 Vite Element Plus Axios数据库MySQL 8.x生产 / SQLite本地快速开发开发工具PyCharm Professional 内置Terminal跑前端在线判题集成一个支持Docker隔离的代码执行沙箱服务PyCharm这块多说一句。许多同学喜欢用VSCode写前后端也不是不行但PyCharm Professional对Flask工程的支持真的很到位——自动识别项目结构、一键创建虚拟环境、自带数据库工具面板、前端代码格式化和调试器一应俱全而且还能直接用PyCharm的Terminal同时管理后端和前端两个进程。一个IDE搞定全部开发流程对专注度不够的人比如我来说少切换一个窗口就是多节省一份精力。2. 核心功能模块化的设计拆解2.1 用户角色权限模型在线教学系统天然多角色。我见过很多人在权限设计上翻车把角色判断写死在每一个接口里后续想加权限维度就要全局改代码。建议采用RBAC基于角色的访问控制模型但不需要做太复杂的三张关联表用户表、角色表、用户角色关系表。在这个项目规模里用一张用户表加一个role字段就够了class User(db.Model): __tablename__ users id db.Column(db.Integer, primary_keyTrue) username db.Column(db.String(50), uniqueTrue, nullableFalse) password_hash db.Column(db.String(128), nullableFalse) role db.Column(db.String(20), defaultstudent) # admin / teacher / student real_name db.Column(db.String(50)) created_at db.Column(db.DateTime, defaultdatetime.utcnow)权限控制的粒度第一版不需要做到每个接口都鉴权只需要在”需要鉴权的接口“上统一用一个装饰器判断Token里的角色信息from functools import wraps from flask_jwt_extended import get_jwt_identity def role_required(*allowed_roles): def decorator(fn): wraps(fn) def wrapper(*args, **kwargs): current_user get_jwt_identity() if current_user.get(role) not in allowed_roles: return {msg: 权限不足}, 403 return fn(*args, **kwargs) return wrapper return decorator这个方案的好处是后面你想扩展”助教“或者”课程管理员“这种角色只需要在装饰器参数里加一个角色名不用动任何接口的业务逻辑。2.2 课程管理模块课程管理是教学系统的地基。在这个模块里三个角色的需求是完全不同的管理员需要管理所有课程、教师与学生的关联关系控制启停状态教师需要发布课程章节、上传课件、管理自己课程下的作业与考试学生需要查看课程内容、参与代码练习、查看成绩与反馈实现上建议让course表持有一个teacher_id外键一名教师可拥有多门课程。课程的”报名“用一张course_student中间表来表示一个学生可以加入多门课程一门课程可以有多个学生标准的多对多关系course_student db.Table( course_student, db.Column(course_id, db.Integer, db.ForeignKey(courses.id), primary_keyTrue), db.Column(user_id, db.Integer, db.ForeignKey(users.id), primary_keyTrue) ) class Course(db.Model): __tablename__ courses id db.Column(db.Integer, primary_keyTrue) title db.Column(db.String(100), nullableFalse) description db.Column(db.Text) cover_url db.Column(db.String(255)) teacher_id db.Column(db.Integer, db.ForeignKey(users.id)) status db.Column(db.String(10), defaultdraft) # draft / published / closed为什么强调status字段因为教学系统里课程必须有“进行中/已结束/草稿”这种生命周期状态否则学生可以看到还没编辑完的半成品课程体验极差。2.3 在线C语言编辑器与判题模块这是整个系统最有含金量的模块也是面试官或者评阅老师最关注的部分。在线C语言编辑器前端层面核心就是一个代码编辑器组件。用CodeMirror自定义语言支持C语法高亮而不是用Monaco EditorVSCode同款编辑器原因是CodeMirror体积更小、API更简洁、上手更快。配置也不复杂import { basicSetup } from codemirror; import { EditorView } from codemirror/view; import { cLanguage } from codemirror/lang-c; const editor new EditorView({ doc: #include stdio.h\n\nint main() {\n printf(Hello, World!\\n);\n return 0;\n}, extensions: [ basicSetup, cLanguage(), EditorView.lineWrapping ], parent: document.getElementById(editor) });后端判题才是重头戏。你不能直接在服务器上用os.system(gcc xxx.c)跑学生代码这会导致两个致命问题一是服务器被恶意代码搞崩二是判题条件太弱。更靠谱的做法是用Docker容器隔离执行。每次学生提交代码后端生成一个临时目录把源代码写进去然后拉起一个gcc镜像容器执行编译和运行并限定CPU时间、内存上限和输出长度import docker client docker.from_env() def run_code_in_docker(source_code, input_data, time_limit2, memory_limit64): # 宿主机临时目录 workdir f/tmp/code_sandbox/{uuid.uuid4().hex} # 容器的核心配置 container client.containers.run( imagegcc:latest, command[ sh, -c, fecho {source_code} main.c fgcc main.c -o main 2 compile.err fecho {input_data} | ./main ], mem_limitf{memory_limit}m, cpu_period100000, cpu_quota200000, # 限制2核 network_disabledTrue, detachTrue, working_dir/workspace, ) # 设置超时防止死循环 result container.wait(timeouttime_limit) logs container.logs().decode(utf-8) container.remove() return logs有两处细节尤其值得注意。第一必须用network_disabledTrue把容器网络关掉不然学生可以通过代码发起网络请求把你的服务器当跳板。第二不能直接让学生在代码里system(rm -rf /)这种命令产生真实危害容器销毁后一切归零但这不代表可以省略资源限制。死循环代码如果不好好限制时间和内存一样会把容器所在主机卡到崩溃。判题逻辑可以分层第一层是编译结果检测编译失败直接记0分并给编译报错信息第二层是运行结果比对把程序输出和标准输出逐字符比对忽略结尾换行差异第三层是性能测试针对特定题目用大样本数据跑一轮判断代码是否超时或超内存docker.from_env()这种方式要求服务器上已经装好了Docker。如果实在没有Docker环境还有一个降级方案用subprocess配合resource模块限制子进程CPU和内存资源虽然隔离不如Docker彻底但是演示和本地开发绰绰有余。顺便提一句真实场景下踩过的一个坑如果Docker容器以宿主机root权限运行学生代码里有mount这类操作是有风险的。解决方法是给容器加pids_limit和只读根文件系统或者在启动容器时指定user参数但这属于进阶加固第一版做到内存/CPU/网络隔离就已经超出平均水平了。3. 从零搭建前后端开发环境PyCharm落地实操3.1 初始化Flask后端项目我习惯的工具链是pyenv管理Python版本3.10或3.11venv建虚拟环境pip装依赖Flask-Migrate管理数据表迁移。# 创建虚拟环境 python -m venv venv # 激活虚拟环境windows venv\Scripts\activate # mac/linux source venv/bin/activate # 安装核心依赖 pip install flask flask-sqlalchemy flask-migrate flask-jwt-extended flask-cors pymysql # 安装Redis可选做缓存 pip install redis项目目录结构我建议分模块化别把路由全塞在一个app.py里online_c_teaching/ ├── app.py # 入口文件 ├── config.py # 配置文件 ├── extensions.py # 扩展对象实例化db, jwt, cors ├── models/ # 数据库模型 │ ├── __init__.py │ ├── user.py │ ├── course.py │ ├── assignment.py │ └── submission.py ├── resources/ # 路由/视图函数 │ ├── __init__.py │ ├── auth.py │ ├── course.py │ ├── assignment.py │ └── judge.py └── utils/ # 工具函数 ├── __init__.py ├── decorators.py # 权限装饰器 └── sandbox.py # 代码沙箱执行为什么把extensions.py单独拎出来因为如果直接在app.py里db SQLAlchemy()然后models模块再来from app import db就形成循环引用项目一大必报错。把db放在独立模块里谁要用谁引用干净利落。3.2 配置Vue 3前端工程前端脚手架不用自己搭webpack配置直接用Vitenpm create vitelatest frontend -- --template vue cd frontend npm install npm install element-plus axios vue-router4 pinia几个核心配置点开发环境代理。前后端分离最大的痛点是跨域。Vite开发服务器的代理配置能解决99%的跨域困扰// vite.config.js export default defineConfig({ plugins: [vue()], server: { port: 5173, proxy: { /api: { target: http://127.0.0.1:5000, changeOrigin: true, rewrite: (path) path.replace(/^\/api/, ) } } } });这个配置的作用是前端请求/api/courses时Vite开发服务器帮你把请求转发到http://127.0.0.1:5000/courses。前端写代码时直接用相对路径后端接口不用加/api前缀也行一举两得。Axios统一封装。每个请求都在组件里写一遍token逻辑实在太痛苦。建议做一个request.js工具模块import axios from axios; import { ElMessage } from element-plus; const request axios.create({ baseURL: /api, timeout: 10000 }); // 请求拦截器自动携带token request.interceptors.request.use(config { const token localStorage.getItem(token); if (token) { config.headers.Authorization Bearer ${token}; } return config; }); // 响应拦截器统一处理错误 request.interceptors.response.use( response response.data, error { if (error.response?.status 401) { localStorage.removeItem(token); window.location.href /login; } else { ElMessage.error(error.response?.data?.msg || 网络错误); } return Promise.reject(error); } ); export default request;这个拦截器一旦写好后面所有接口调用都只要request.get(/courses)这种简单的写法不用在业务代码里反复处理错误逻辑。3.3 数据库设计与迁移MySQL建库时注意字符集不然中文会乱码CREATE DATABASE online_c_teaching CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;然后用Flask-Migrate管理模型变更。这是被低估的一步尤其是学生阶段改模型很频繁手写SQL去改表结构加个字段改个类型非常容易出错。用迁移脚本一键同步flask db init flask db migrate -m create tables flask db upgrade以后改模型只需要重新跑migrate和upgrade数据库自动同步。4. 核心业务代码逐段解析4.1 JWT用户认证流程JWT认证在前后端分离项目里是标配。后端在登录接口签发一个带过期时间的access_token前端存储到localStorage每次请求时放在Authorization请求头里。这里有个细节很多人不知道access_token的生命周期不要设置太长我习惯1小时左右因为一旦泄露风险可控。但每个小时都要重新登录又很烦所以一般配合refresh_token做无感刷新前端拦截401后自动调刷新接口拿新的token。登录接口from flask_jwt_extended import create_access_token, create_refresh_token from werkzeug.security import check_password_hash auth_bp.post(/login) def login(): data request.get_json() username data.get(username) password data.get(password) user User.query.filter_by(usernameusername).first() if not user or not check_password_hash(user.password_hash, password): return {msg: 用户名或密码错误}, 401 access_token create_access_token( identity{id: user.id, role: user.role}, expires_deltatimedelta(hours1) ) refresh_token create_refresh_token( identity{id: user.id, role: user.role} ) return { access_token: access_token, refresh_token: refresh_token, user: {id: user.id, username: user.username, role: user.role} }密码存储一定要用werkzeug.security的generate_password_hash千万别明文存数据库。这个坑几乎是每年都会见到一个在线教学系统被评阅老师翻代码库发现用户表里的密码全是明文直接判定项目不合格。安全性是教学系统项目评分里的硬指标容不得马虎。4.2 课程与作业关联查询课程和作业的主线逻辑class Assignment(db.Model): __tablename__ assignments id db.Column(db.Integer, primary_keyTrue) course_id db.Column(db.Integer, db.ForeignKey(courses.id)) title db.Column(db.String(100), nullableFalse) description db.Column(db.Text) start_time db.Column(db.DateTime) end_time db.Column(db.DateTime) test_cases db.Column(db.Text) # JSON字符串存标准输入输出 ...test_cases字段是我特意加进去的。判题系统的数据就用它来存格式大概长这样[ {input: 1\n2\n, output: 3\n, score: 50}, {input: 100\n200\n, output: 300\n, score: 50} ]教师在前端编辑题目时直接填多组输入输出即可判题逻辑读这个字段来逐一比对。课程详情接口用一条SQLAlchemy关联查询把课程、教师、作业数量、学生数量一并拿回来避免前端调多个接口再拼数据course_bp.get(/int:course_id) def get_course_detail(course_id): course Course.query.options( db.joinedload(Course.teacher), db.joinedload(Course.students) ).get(course_id) if not course: return {msg: 课程不存在}, 404 return { id: course.id, title: course.title, description: course.description, teacher_name: course.teacher.real_name if course.teacher else , student_count: len(course.students), assignment_count: Assignment.query.filter_by(course_idcourse.id).count() }joinedload的作用是预先用JOIN把关联数据取出来避免查询完课程后再逐个查询教师和学生造成N1问题。数据量小的时候效果不明显一旦课程多了不用它会明显卡顿。4.3 前端页面与状态管理Vue 3前端建议用Vite的组件模式配合Pinia做全局状态管理。为什么用Pinia不用Vuex因为Pinia更轻、TypeScript友好、API简洁而且是Vue官方现在推荐的方案。// store/user.js import { defineStore } from pinia; export const useUserStore defineStore(user, { state: () ({ token: localStorage.getItem(token) || , userInfo: JSON.parse(localStorage.getItem(userInfo) || {}) }), actions: { setLoginInfo(token, userInfo) { this.token token; this.userInfo userInfo; localStorage.setItem(token, token); localStorage.setItem(userInfo, JSON.stringify(userInfo)); }, logout() { this.token ; this.userInfo {}; localStorage.removeItem(token); localStorage.removeItem(userInfo); } } });页面路由用Vue Router时务必加导航守卫不然用户直接在地址栏输入/admin就能进入管理页面等于权限控制白做了router.beforeEach((to, from, next) { const token localStorage.getItem(token); if (to.meta.requiresAuth !token) { next(/login); } else if (to.meta.requiresAdmin userStore.userInfo.role ! admin) { next(/forbidden); } else { next(); } });前端页面骨架就不展开画原型图了简单提几个关键路径首页课程列表 推荐课程 系统公告课程详情页章节导航 课程内容 作业列表编辑器页面左侧题目说明、右侧代码编辑区、底部运行结果区教师台作业创建、学生管理、成绩统计管理后台用户管理、课程审核、系统配置5. 在线判题的完整闭环实现这里单独拉一章讲判题因为它是整个系统的技术制高点。判题流程是这样的学生点击”提交“前端把assignment_id和source_code发给后端的/api/judge接口后端查数据库拿这道题的所有test_cases逐条把测试用例的输入喂给沙箱运行的代码比对输出结果记录每条用例得分汇总分数写入submissions表返回运行结果和得分给前端核心判题函数不能用同步方式。因为用户提交代码后可能要等Docker启动、编译、运行耗时几秒到几十秒如果同步阻塞Flask的单线程开发服务器直接卡住。正确做法是用后台任务数据库状态轮询import threading def submit_assignment(assignment_id, source_code, user_id): # 先创建一条待判记录状态为 pending submission Submission( assignment_idassignment_id, user_iduser_id, source_codesource_code, statuspending, score0 ) db.session.add(submission) db.session.commit() # 后台线程执行判题 thread threading.Thread( targetjudge_task, args(submission.id, assignment_id, source_code) ) thread.start() return {submission_id: submission.id, status: pending}前端拿到submission_id后设置一个定时器每隔2秒发起一次查询直到状态变成success或failed。这个”异步提交轮询结果“的模式在前后端分离项目里很常见也是你可以在答辩时重点讲解的设计亮点。6. 生产环境部署与性能优化项目做完要能演示很多课程设计和毕业设计需要一个可访问的线上地址。这里我分享一套最省钱的方案6.1 云服务器部署服务器用Linux Gunicorn Nginx的组合。Gunicorn是Python Web应用的常用WSGI容器它能同时处理多个并发请求开发模式的app.run()不行它单线程且性能极弱。# /etc/nginx/sites-available/online_c server { listen 80; server_name your_domain_or_ip; # 前端静态资源 root /var/www/online_c/dist; location / { try_files $uri $uri/ /index.html; } # 后端API反向代理 location /api { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }前端构建产物放到/var/www/online_c/distNginx可以直接托管。后端启动指令gunicorn -w 4 -b 127.0.0.1:8000 app:app-w 4开4个worker进程普通教学系统并发量下绰绰有余。6.2 性能优化三板斧缓存热点数据课程列表、题目列表这类读多写少的数据用Redis缓存起来。比如课程列表缓存10分钟过期后自动回源数据库。数据库索引submissions表查询最多的是user_id和assignment_id建联合索引能显著提升查询速度。资源懒加载前端列表页用懒加载组件第三方的编辑器组件只在编辑器页面加载首页加载体积直接砍掉一半。7. 常见问题排查与避坑指南这个章节我毫无保留分享实操中遇到的坑和排查方法。7.1 Flask跨域问题症状前端调用接口时浏览器报CORS policy错误。常见原因有三个一是后端没装flask-cors二是后端装了但没配置allow_origins三是走了Vite代理但代理路径写错。排查方式先看Network面板里请求头有没有Access-Control-Allow-Origin没有就是后端问题有但是还报错就要检查前端请求路径是否和代理匹配。最笨但有效的验证方式是直接用浏览器访问后端接口地址能通就说明后端正常问题在前端路径或代理。7.2 Vue路由刷新404症状点击页面内部链接一切正常但浏览器刷新或直接输地址访问时报404。这不是后端接口问题是Vue Router的history模式问题。部署到Nginx后静态文件服务器找不到/courses这种前端路由对应的物理文件。解决方法就是上面Nginx配置里的try_files $uri $uri/ /index.html;把所有路径重写到前端入口。7.3 Docker容器时区与编码问题症状容器里运行的C代码输出中文时乱码。原因gcc官方镜像默认locale不是UTF-8。解决方案是在运行容器时加环境变量container client.containers.run( imagegcc:latest, environment{ LANG: C.UTF-8, LC_ALL: C.UTF-8 }, ... )这个坑我当年排查了快一天以为是前端编码问题结果是在容器环境上。7.4 sqlite和mysql的数据差异本地开发用SQLite确实方便文件和库都在本地重启不丢数据。但如果部署到线上换成了MySQL两个库的字段类型差异可能导致隐性问题。比如SQLite对DateTime字段可以宽松存储MySQL对日期格式有严格要求SQLite的布尔值存储为0/1MySQL有原生的TINYINT(1)。开发时最好从一开始就用MySQL省得部署时踩坑。7.5 代码编辑器内容回显学生正在编辑器里写的代码因为刷新页面或意外关闭浏览器导致丢失这个问题对教学场景杀伤力极大。建议加一个草稿功能在编辑器组件里监听onchange事件节流防抖后把内容存到localStorage进入页面时检测到草稿就提示用户是否恢复。这个功能做起来不大但对使用体验的提升非常明显。8. 个人体会与扩展建议做完整个系统我最大的感受是这个项目的难点不在于某个技术点有多深而在于把所有模块串成一整个流畅闭环。从课程创建到作业发布再到学生写代码提交、系统判题返回结果每一步都涉及前后端的数据交互和状态流转。任何一个环节衔接不顺都会导致整个系统的体验断掉。如果时间允许后续可以往这几个方向再扩展加入代码相似度检测查重防止学生互相抄代码加入学习路径推荐根据学生的错题记录推荐相关练习把判题沙箱升级成真正的评测队列用消息队列而不是线程处理高并发提交。这些都是锦上添花的功能核心架构不变的情况下都是可以逐步迭代的。最后说个实际心得系统开发的过程里一定要养成随时写测试用例的习惯尤其是判题模块和用户认证模块。哪怕只是简单的pytest脚本覆盖登录、提交、判题几条主流程都能在改代码之后快速发现问题避免改一个bug引出三个新bug。我在实际开发里吃过亏所以现在接这类项目第一件事就是把核心判题链路的自动化测试补上后面开发效率明显提升。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑