做这个项目的起因其实特别简单:身边有不少朋友和学弟学妹在学 Python 和 C 语言,但普遍存在“视频刷了不少,动手一写就懵”的情况。市面上的刷题平台要么只支持单一语言,要么题目对零基础不友好,要么压根没有反馈机制。所以我自己动手做了一个 Python/C 语言学习辅导网站,前端选了 Vue 3 这套生态,后端用 Python 技术栈实现,把“看知识点、做练习、在线写代码、看评测结果、讨论提问”这几个环节串成一条完整的路径。这个项目很适合课程设计、毕业设计,或者作为想入门前端和后端开发的练手项目,也能直接改造成公司内部的笔试平台或培训系统。
项目做完后回头再看,真正有价值的不在“页面多好看”,而在整个学习闭环是否转得起来:用户能不能从零开始跟着知识点走,能不能在网页上写代码并立刻知道运行结果,做错的题能不能进入错题本。这篇文章我会把思路设计、技术选型、核心模块和踩过的坑都摊开讲,尽量做到能够复现。
1. 需求解析与整体设计思路
1.1 这个网站到底要解决什么学习问题
先别急着打开 IDE 写代码,把需求想清楚比什么都重要。当时我调研了一圈目标用户,主要分三类:
- 刚接触编程的大学生,需要 Python 和 C 语言的基础语法、练习题和错误讲解;
- 正在准备期中期末上机考试的在校生,需要按照知识点拆解的题库和模拟练习;
- 作为课程设计作品时,还必须有教师或管理员的视角,能管理题目、查看学生学习记录。
为什么不做成普通的博客加视频站?因为视频的反馈链路太慢,看三分钟忘了两分钟,学习质量的把控全凭自觉。学习辅导网站的核心是“闭环”:知识点学习、示例代码、动手练习、评测反馈、错题复习,这五件事件缺一件都容易出现“眼睛会了手不会”的状态。所以在设计第一版时,我就把“做题 + 在线评测 + 结果反馈”作为网站的地基。
另一个容易被忽略的需求是“代码运行环境统一”。学生在自己电脑上运行 C 语言代码,不同系统、不同编译器版本的差异很大。网站上提供统一评测环境以后,判断结果的依据就是“程序输出是不是和标准答案完全一致”,这也避免了学生在自己电脑上出现了环境问题却以为是代码问题的连锁挫败感。
1.2 为什么技术栈选择 Python 后端 + Vue 前端
这套技术搭配放在今天已经算是比较成熟的学习型全栈组合。
后端的每个模块都围绕 Python 生态来设计:
| 后端组件 | 用途 | 选择理由 |
|---|---|---|
| FastAPI | 提供 REST API | 自带 OpenAPI 文档,写接口联调时能省很多事,类型提示对课程设计也很友好 |
| SQLAlchemy | ORM 数据访问 | 设计表结构后直接用 Python 操作数据库,方便后续迁移 |
| Uvicorn | ASGI 服务器 | FastAPI 的标准搭配,调试和部署都稳定 |
| Docker SDK / 子进程调用 | 在线代码评测 | 利用 Docker 做隔离,保证用户代码不会影响主服务 |
为什么不用 Node.js 或 Spring Boot?一方面因为这个网站的代码评测功能天然需要和 Python 生态打交道,后面如果给学生做“爬虫入门”“量化策略小项目”这类专题,后端可以用 Python 直接调用和演示;另一方面 Python 在课程设计场景里代码量更少、可解释性更强,答辩的时候好讲通透。
前端选择 Vue 则是因为 Vue 的中文资料、社区生态和上手难度都很适合学习型项目。Vue 3 的 Composition API 配合<script setup>写业务界面非常顺手,需要做题目列表、代码编辑器、提交记录列表这类交互组件时可以拆得很干净。脚手架用 Vite 而不是 Vue CLI,构建速度快很多,而且插件的安装方式也简单。
我不想刻意追逐新框架,但 Vue 3 的生态已经足够成熟。配合 Vue Router 做路由、Pinia 做状态管理、Axios 做请求,这套东西在真实项目中非常常见,做完以后学到的技能也能迁移到实习和工作中。
1.3 第一版功能边界怎么划分
做项目最怕“什么都想要”。我第一版锁定的功能是这样几条基线:
- 用户注册登录、JWT 鉴权、学生与教师两种基础角色;
- 知识点目录与课程内容展示,支持 Markdown 渲染;
- Python 和 C 语言分学科题库,题目支持单选、判断、编程题;
- 网页代码编辑器,支持 Python 和 C 语言提交;
- 在线评测沙箱,反馈 AC、WA、TLE、RE 等状态;
- 提交记录、排行榜、错题收藏、简单问答讨论区;
- 管理后台:题目管理、学生提交查看、基础统计。
那些一听就很诱人但会拖慢进度的功能,比如实时聊天、个性化 AI 出题、APP 端等,全部留到二期。不是说这些不重要,而是第一版必须保证核心链路能完整走通。范围控制是这个项目里我认为最值得分享的一点:先做一个能用的闭环,再考虑让体验变高级。
2. 系统架构与核心功能模块拆解
2.1 前后端分离与评测服务的边界
整个系统的部署结构大致上可以分成三层:浏览器端 Vue 应用、后端 API 服务、评测执行服务。前端只负责渲染和交互;后端负责业务逻辑、数据库读写、权限校验;评测执行服务则是业务服务通过 Docker 隔离用户代码后独立运行的。
这个边界划分有个现实原因:不能把用户提交的代码直接扔进后端进程里跑。C 语言的程序理论上可以调用系统函数,Python 代码虽然没那么危险,但也可能写出占用无限内存的死循环。评测服务和主服务隔离以后,就算用户代码把评测容器跑崩了,主站的注册登录和题库浏览也不受影响,本质上是把“不可信代码”和“可信服务”隔离开了。
我当时是在一台普通 Linux 服务器上部署的,评测模块直接用 Docker 命令创建一次性容器,容器内挂载学生代码目录。更大型的平台会用 Kubernetes 管理评测 Pod,但这个量级用 Docker 就够。通过 Docker 的--rm参数,容器运行完自动删除,不会堆积垃圾容器。
2.2 功能模块地图与关键说明
先把模块的大致职责整理成一张表:
| 模块 | 关键功能 | 数据载体 |
|---|---|---|
| 用户中心 | 注册、登录、个人资料、角色权限 | MySQL 用户表、Redis Session 可选项 |
| 课程中心 | 知识点章节、文章内容、Markdown 渲染 | 章节表、文章表 |
| 题库中心 | 选择题、判断题、编程题,支持标签和难度筛选 | 题目表、标签表、题目-标签关联表 |
| 在线做题 | 代码编辑器、Python/C 语言切换、提交 | 提交记录表、用户-题目状态表 |
| 评测服务 | 编译执行用户代码、比对输出 | 独立服务,核心为 Docker 容器 |
| 讨论社区 | 发布问题、回复、按题目标签检索 | 问题表、回复表 |
| 管理后台 | 题目管理、用户管理、统计数据查看 | 依赖上面所有表 |
题目表的设计在这里多说几句。编程题一定要和选择题分开存,表面上都是“题”,但字段差别很大。编程题需要存样例输入、样例输出、评测输入、评测输出、时间限制、内存限制、题目难度、关联的知识点标签;选择题只需要题干、选项和正确答案。所以题库层面我用“基础题目表 + 扩展类型表”的方式,而不是把所有字段塞进一张大表。如果做单体小项目一张表硬撑也行,可后面维护会越来越难受。
评测部分的抽象也要提前做好。后端不能直接写死“只支持 Python 3”,而是设计成:每种语言对应一个提交配置,比如python3对应镜像python:3.11-slim,c对应镜像gcc:12,扩展名、编译命令、执行命令都从配置读取。这样后面再加 Go、Java 时只是新增一个配置模块,而不是把提交逻辑再复制一份。
2.3 数据模型设计的一个小思考
很多课设项目的数据库表设计是“想到哪写到哪”。我这次尝试换了一种思路:先画核心状态流转。用户的做题状态是一个状态机:
未开始 -> 开始做题 -> 已提交 -> 评测中 -> 通过 / 未通过所以除了题目表和用户表,还要有一张user_problem_status表,里面保存用户卡在哪道题、最近一次提交是否正确;提交记录表submissions则记录每一次答题的代码快照、语言类型、评测状态和消耗时间。状态表负责“当前进度”,提交表负责“历史过程”,两个层次分开以后,个人中心展示“继续学习”还是“查看历史”都非常直接。
还有一点很实用:题目与知识点之间用多对多关系,通过中间表关联。当学生在 C 语言“指针”章节学完以后,系统可以把所有打了“指针”标签的题筛出来,形成章节练习卷。这样的设计让“课程学习”和“题库刷题”两个模块不孤立,学习路径才真正成立。
3. 前端 Vue 部分的工程化落地
3.1 前端工程初始化与依赖安装细节
前端部分我选用 Vite 来创建 Vue 3 项目,记得初始化时 Node.js 版本别太老,我用的是 18+,否则依赖安装会出一堆莫名其妙的兼容性报错。
npm create vue@latest这个命令会生成一个带 TypeScript 选项的项目模板。对于只想快速做课设的同学,我建议直接选 JavaScript 而不是 TypeScript,不然前期类型定义的时间会占掉不少开发周期。如果你的项目想长期维护,或团队成员比较多,再考虑 TS。
初始化后需要安装的依赖比较固定:
npm install vue-router@4 pinia axios npm install @codemirror/lang-python @codemirror/lang-cpp codemirror npm install marked dompurify npm install hls.js这里重点是代码编辑器选型。CodeMirror 6 是现在比较省心的方案,支持语法高亮、行号、自动缩进。通过@codemirror/lang-python和@codemirror/lang-cpp就能覆盖 Python 和 C 语言的编辑需求。如果你的网站还涉及 markdown 渲染题目信息,一定要用 DOMPurify 清洗 Markdown 转换后的 HTML,避免用户在题解区输入恶意脚本。
在仓库里我习惯把目录结构固定成这样:
src/ api/ # axios 封装和接口模块 assets/ # 静态资源 components/ # 全局公共组件 router/ # 路由配置文件 stores/ # Pinia 状态管理 views/ # 页面级组件3.2 路由规划与权限控制
Vue Router 在项目里的作用不只是页面跳转,还承担了“路由守卫”的职责。访客、普通学生、教师和管理员能看到的菜单不同,但不能只靠菜单隐藏做安全控制,前端路由守卫只是一道体验层,真正的权限校验必须放后端。
路由规划大致这样:
const routes = [ { path: '/', component: HomeView }, { path: '/learn', component: LearnView }, { path: '/problem/list', component: ProblemListView }, { path: '/problem/:id', component: ProblemDetailView }, { path: '/submission/:submissionId', component: SubmissionResultView }, { path: '/discuss', component: DiscussListView }, { path: '/admin', component: AdminLayout, meta: { requiresAuth: true, roles: ['teacher', 'admin'] }, children: [ { path: '', component: DashboardView }, { path: 'problem/edit/:id?', component: ProblemEditView } ] } ]路由守卫里最常踩的坑是“刷新后用户信息丢失”。如果用户信息只存在 Pinia 里,刷新页面就会清空,所以登录后要同步把用户基本信息存到localStorage,路由守卫里先从 Pinia 取,取不到再回 localStorage 恢复。
3.3 代码编辑器与提交流程实现
题目详情页大概分成左右两栏:左边是题目描述、样例输入输出、提示;右边是代码编辑器和提交按钮。用户在编辑器里选的默认语言来自题目的主要语言,比如 Python 题目默认 Python,C 题目默认 C,也可以手动切换。
点击提交以后,前端需要先校验代码是否为空,然后调用后端接口把代码、语言和题目 ID 传过去。后端返回提交记录 ID 后,前端进入轮询或等待状态。我一开始用setInterval每 2 秒拉一次提交状态,后来发现接口请求频率太高,改成每 3 秒拉一次,并且页面切走时取消定时器。
评测状态的界面要友好。大家平时用 LeetCode 应该都习惯“通过/不通过”的显示,我在本项目中把评价状态扩展成一张完整的标签:AC(通过)、WA(答案错误)、TLE(超时)、MLE(超出内存)、RE(运行时错误)、CE(编译错误)。学生看到CE时的第一反应通常是自己代码哪里写错了,这时最好在下方展示编译错误信息,方便定位。
4. 后端核心逻辑与在线评测实现细节
4.1 后端接口设计与 JWT 鉴权
后端采用 FastAPI 的工程结构,按模块拆目录:
app/ main.py # 应用入口 api/ # 路由层 models/ # SQLAlchemy 模型 schemas/ # pydantic 校验模型 services/ # 核心业务逻辑 core/ # 配置、安全依赖用户认证采用 JWT 是无状态方案,登录成功后服务端返回一个 token,前端以后每次请求都往 Authorization 头带上。相较于 Session,JWT 对前后端分离项目更友好。
JWT 生成的核心代码不复杂,但注意密钥要放在环境变量里,不能写死在代码里:
from datetime import datetime, timedelta, timezone import jwt SECRET_KEY = "your-secret-key" ALGORITHM = "HS256" ACCESS_TOKEN_EXPIRE_MINUTES = 60 * 12 def create_access_token(user_id: int, role: str): expire = datetime.now(timezone.utc) + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) payload = {"sub": str(user_id), "role": role, "exp": expire} token = jwt.encode(payload, SECRET_KEY, algorithm=ALGORITHM) return token需要注意的是 JWT 的sub字段建议传字符串,不同库对 integer 类型的处理不太一样,容易在解析时踩坑。用户角色校验做成一个 FastAPI 依赖,这样在路由里声明dependencies=[Depends(require_role("teacher"))]就能保护接口。
4.2 提交代码与异步评测的设计
最早的版本我让提交接口同步等待评测结果,一个学生写了个死循环,请求直接卡死几十秒,体验非常差。后来改成异步模式:提交接口把提交记录写入数据库,状态为PENDING,然后马上把评测任务交给后台执行,前端再轮询查询评测状态。
这里要注意一个细节,评测服务和 API 服务如果放在同一个进程里,虽然代码上简单,但 CPU 密集的编译工作会阻塞 FastAPI 的事件循环。更好的做法是把评测任务放到独立进程中,或者用一个简单的任务队列。由于项目规模不大,我用进程加队列实现,效果已经足够。
提交接口的代码结构可以长这样:
from fastapi import APIRouter, Depends, HTTPException from sqlalchemy.orm import Session @router.post("/problems/{problem_id}/submissions") def create_submission(problem_id: int, payload: SubmissionIn, db: Session = Depends(get_db), current_user=Depends(get_current_user)): problem = db.query(Problem).filter(Problem.id == problem_id).first() if not problem: raise HTTPException(status_code=404, detail="题目不存在") submission = Submission( user_id=current_user.id, problem_id=problem_id, code=payload.code, language=payload.language, status="PENDING" ) db.add(submission) db.commit() db.refresh(submission) # 交给评测服务 judge_service.submit( submission_id=submission.id, language=payload.language, code=payload.code, problem_id=problem_id ) return {"submission_id": submission.id, "status": "PENDING"}这里建议在设计表时给submissions表加一个status索引,因为个人中心和题目列表都会频繁按用户和状态查询。
4.3 用 Docker 搭建 C 语言与 Python 评测沙箱
这是全网热搜词里被问得最多的地方,也是网站最有技术含量的部分。在线评测后台不能让用户提交的代码在本机直接跑,否则两个风险无法接受:
- 安全风险:C 语言的
system("rm -rf /")、Python 的os.remove可以直接删除服务器文件; - 资源风险:没有限制的
while(1)或异常分配内存会让整台服务器卡死。
所以我设计了基于 Docker 的一次性容器方案:
timeout 10 docker run --rm \ --network none \ --memory 128m \ --cpus 0.5 \ --pids-limit 64 \ -v /tmp/judge/{submission_id}:/workspace \ -w /workspace \ gcc:12 bash -c "gcc main.c -o main -O2 -Wall -lm && ./main < input.txt > user_output.txt"参数含义做一个快速解释:
--rm:容器退出后自动删除;--network none:禁止容器联网,防止恶意代码外传数据;--memory 128m:限制容器最大内存;--cpus 0.5:限制 CPU 使用率;--pids-limit 64:限制进程数量,防止 fork 炸弹;-v:把刚刚写好的代码目录挂载到容器里;- 外层
timeout 10:兜底防止 Docker 启动卡住等意外情况。
Python 语言评测基本一样的思路,镜像换成python:3.11-slim,执行命令换成python3 main.py。容器里不需要任何多余服务,网络关闭能少掉一大批安全漏洞。
评测流程细化以后是这样的:
- 后端生成以
submission_id命名的临时目录; - 将学生代码写入
main.c或main.py; - 把该题的标准输入写入
input.txt; - 运行容器编译/执行代码,得到用户输出;
- 读取用户输出,和标准答案输出逐字比对;
- 清理临时目录;
- 将状态写入数据库。
输出比对这一块我最初使用了完全字符串相等判断,结果一堆学生因为行末空格、最后一行没有换行而 WA。后来优化成了“忽略行末空白字符、忽略文件末尾空行”的宽松比较,更贴合学习场景。同时保留一个严格模式开关,在后面做竞赛题时再开启。
4.4 评测状态与反馈信息优化
评测结果不能只给一个“错误”就完事,那样学生根本不知道怎么改。我在前端和后端都做了分层的反馈:
| 评测状态 | 含义 | 页面上额外展示内容 |
|---|---|---|
| AC | 通过 | 消耗时间和内存 |
| CE | 编译错误 | 展示编译器输出的完整错误信息 |
| WA | 答案错误 | 展示用户输出和标准输出前 200 个字符,方便对比 |
| TLE | 超出时间限制 | 无额外信息 |
| MLE | 超出内存限制 | 无额外信息 |
| RE | 运行时错误 | 展示程序 exit code 或错误摘要 |
从教学角度看,CE 的编译信息是最有用的。比如学生写 C 代码出现了“unreferenced label”这个报错,很多新人完全看不懂,我就在页面错误提示下面加了简单的解释文案。unreferenced label通常就是代码里写了标签但从来没有被goto跳到,虽然不影响精确编译,但很多编译器给了警告,如果页面不显示,学生完全不知道哪里有问题。把这些细节处理好,对初学者真的能带来完全不同的体验。
5. 联调过程中的踩坑记录与排查方法
5.1 前端多环境接口请求与跨域问题
本地开发时,Vue 页面跑在 5173 端口,FastAPI 跑在 8000 端口,浏览器直接跨域访问会被拦截。解决办法不是在 FastAPI 里盲目加CORSMiddleware允许所有来源,而是通过 Vite 的 dev server 做代理,让前端发请求时保持同一源。
// vite.config.js export default defineConfig({ server: { port: 5173, proxy: { '/api': { target: 'http://127.0.0.1:8000', changeOrigin: true, } } } })前端所有请求统一走/api前缀:
const request = axios.create({ baseURL: '/api', timeout: 15000 })开发调试阶段一切正常,部署到生产环境后 Nginx 也要做一次类似的代理转发:
location /api/ { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; }我建议从一开始就用统一的/api前缀,避免后续部署时一个个改请求地址。跨域问题本身不难,难的是一开始没规划好,后面要改的牵连太多。
5.2 Vue 刷新 404 与前端鉴权细节
Vue Router 使用history模式时,直接在浏览器地址栏访问/problem/12,服务器会找不到对应文件,因为请求发给了后端而非前端。开发环境没问题,生产环境必须在 Nginx 配置try_files:
location / { try_files $uri $uri/ /index.html; }还有一个容易忽略的点:前端的路由守卫只是控制“显示”,接口层必须也做保护。有些同学的课设前端隐藏了管理入口,然后直接手动输入接口 URL 就能访问管理接口,这是个漏洞。JWT 鉴权中间件必须覆盖每个非公开路由,并做角色判断。
5.3 m3u8 视频资源和静态资源加载问题
课程章节里如果加了录播视频,很多同学第一次会踩坑。我第一版直接把视频转成 mp4 扔进 static 目录,后来视频文件越来越大,浏览器加载非常慢,后面选择将长视频切成 HLS 流,前端用 hls.js 播放 m3u8 文件。
Vue 中使用 hls.js 的基本流程是:
import Hls from 'hls.js' function playM3u8(videoElement, url) { if (Hls.isSupported()) { const hls = new Hls() hls.loadSource(url) hls.attachMedia(videoElement) hls.on(Hls.Events.MANIFEST_PARSED, () => { videoElement.play() }) } }生产环境容易出现的问题是:页面是 HTTPS 页面,但 m3u8 分段地址写的是 HTTP,浏览器会拦截掉,播放器一直加载不出来。排查时先打开浏览器控制台,看到 Mixed Content 就会被秒定位,最好的方法是从一开始就统一使用相对地址或全链路 HTTPS。
5.4 C语言题库设计中的几个高频问题
题目来源和学习反馈这件事,我做了一版以后才真正理解热门搜索里的那些学习难点为什么会反复出现。
C 语言学得久的人会知道“字符串逆序”是 PTA 等平台的经典题。这道题看着简单,但初学者会在“原地逆序、用指针还是用数组”上纠结。再比如“字符出现次数统计”这类问题,许多学生到时会用if (str[i]=='a')判断很多次,每次都要重新扫一遍字符串,在真正限制时间的用例下就会超时。设计题库时要把这些经典场景纳入成不同难度的关卡,不是单纯罗列一大堆题。
另外我做了“文件读写操作”的专题,很多学生在这块吃亏不是因为不会读文件,而是没搞清楚路径问题。学生在本地跑的路径和评测容器里的工作目录不一致,导致读取不到文件。后来我在题目要求的模板代码里规范了相对路径读取,同时在页面上的“特别提示”区域标注“请使用相对路径,如data.txt,不要使用绝对路径”。这一步有效减少了无效提交的次数。
评测服务里,很多 C 语言的崩溃来自野指针、数组越界,容器内会把程序运行错误统一标记成 RE,页面提示段错误。但段错误信息太抽象了,我在帮助文档里增加了常见的 RE 自查清单:有没有数组下标越界?有没有访问空指针?有没有忘记初始化变量?这样学生排查问题时才有方向。
5.5 Docker 评测环境的稳定性治理
评测服务上线跑了两周后,遇到一个很经典的问题:Docker 容器启动大约需要 1-3 秒,如果有十几个学生同时提交,同一时间启动十几个容器没问题,但每次启动都有编译开销,接口返回结果的时间会变得很慢。后来做了几个动作:题库管理支持小范围预热,热门题目的镜像宿主机缓存好;评测服务用独立的 CPU 核心绑定,不跟 API 服务抢资源;一次性容器使用--network none避免对宿主机网络资源的竞争。
还有一次线上事故让我印象深刻:C 语言评测临时目录创建后,有学生提交代码后立刻刷新页面,前端轮询时读到PENDING状态,于是再次点击提交,产生了大量垃圾容器和临时目录。我做的处理是:在提交接口里加了个简单的防抖判断,同一用户对同一道题如果在 10 秒内重复提交相同代码,直接拒绝,并提示“已有评测中的提交”。做这种平台型项目,防重复提交一定别忘了。
6. 项目复盘与二次开发建议
6.1 从架构角度复盘最有价值的三个决策
如果重新做一次,我仍然会保留三个设计决策。
首先,代码评测做成独立服务,而不是塞进业务接口。哪怕是课设,也建议学一学这种隔离思路,以后面对真实比赛系统或面试笔试平台时思路是相通的。代码提交的安全风险比其他业务都高,必须单独处理。
其次,题库的数据结构标准化。所有题目有统一 ID、难度、标签、内容类型;编程题有统一的输入输出定义;提交记录表、用户状态表有统一的字段命名。现在想加一个“错题本随机出题”功能,几乎不用改表结构。
第三个有价值的选择是按“状态”而不是按“动作”保存数据。比如用户做题状态,我只关注“当前状态”,而提交动作只作为历史记录存在。这样做出的个人中心会非常稳定,不会因为一次接口请求失败导致进度错乱。
6.2 在线代码的启发式辅导还能怎么升级
学习辅导类网站比起刷题平台更强调引导而不仅仅是判断对错。项目上线后我收到最多反馈是“我 WA 了,谁能告诉我思路”。因此后面可以考虑加入一个基于关键词与错误类型的提示系统:当代码提交 WA 后,系统根据提交代码结构、错误用例和题目标签给出提示。这里目前是规则匹配,例如 C 语言题目中如果发现错误输出里多为 0 或空,那么大概率是变量未初始化或数组越界,可以提示学生先检查循环边界。等积累到足够的提交数据后,就可以尝试用简单模型预测学生的知识薄弱点。
我看到一些搜索词也很有启发,比如“Python 量化交易策略代码”“免费 Python 源码大全”“爬虫入门”这类内容,说明纯语法题的结果反馈并不能满足所有学习者。可以扩展出一个“项目实训库”,把任务变成多步骤导学,每个阶段配置一个检查脚本,比如 Python 爬虫项目按“获取网页、解析数据、清洗保存”分阶段评测,C 语言方向则延伸嵌入式场景的函数指针、结构体封装例子。这些扩展并不需要推翻现有的架构,只需要在题目类型中扩展出一种“阶段型编程题”。
6.3 最后的落地建议
如果你确实想照着这个思路完整复现,建议搭环境的时候按顺序做:先本地启动 FastAPI 并确认接口文档能打开,再创建前端工程联调注册登录,然后接题库模块,最后做 Docker 评测。这一步顺序不要反过来,因为评测服务是最容易受环境影响的部分,如果一开始就做评测,后面再联调其他模块时遇到环境问题,排查来源会很痛苦。
C 语言评测前可以在本机装一下gcc并测试几个最简单的程序,确保操作系统上的 Docker 可以正常拉取镜像。如果开发机是 Windows,建议评测服务老老实实用一台 Linux 机器或者虚拟机跑,文件挂载、权限控制和timeout命令在 Linux 上都要省心很多。我在本地 Windows 上折腾 Docker 文件挂载耗费的时间,比写评测模块本身还长。
这个项目让我体会最深的是:学习辅导网站不只是把题目放到网上,代码评测也不是简单执行一条命令。真正的难点在于如何把学生的每一次错误翻译成有方向的学习反馈,这需要业务逻辑和工程实现的配合。做的时候多站在学生视角去想,他们看到这个页面能不能知道下一步做什么,这样产品才能从“能跑”变成“好用”。