构建 C++ 在线评测系统的完整实践
一、项目背景与需求分析
1.1 为什么要做这个系统
去年开始接触青少年编程教育领域,发现市面上的 OJ 系统要么太老旧(界面像上世纪的产物),要么功能过于复杂(面向 ACM 竞赛,对初学者不友好)。GESP、CSP-J/S 这些比赛的备赛需求很具体:
最终成果:百炼谷在线评测系统 - 一个面向青少年编程教育的 OJ 平台
- 需要按难度分级的题库(入门、普及、提高)
- 需要班级管理功能,老师能布置作业、查看进度
- 需要竞赛模式,模拟真实比赛环境
- 代码编辑器要对 C++ 友好,有语法提示
1.2 核心需求拆解
| 模块 | 核心功能 | 技术难点 |
|---|---|---|
| 用户系统 | 注册登录、角色权限、班级管理 | 密码安全、JWT 刷新机制 |
| 题目系统 | 题面展示、测试用例管理、标签分类 | Markdown 渲染、富文本编辑 |
| 评测系统 | 代码编译执行、资源限制、结果判定 | 沙箱安全、并发控制 |
| 竞赛系统 | 计时、排行榜、防作弊 | 实时数据同步 |
| 教学系统 | 课程管理、学习轨迹、作业批改 | 数据统计分析 |
1.3 技术选型决策
后端语言选择
在 Java 和 Go 之间犹豫过。Java 生态成熟,但启动慢、内存占用高;Go 编译快、部署简单,适合这种需要频繁部署迭代的项目。最终选择 Go + go-zero 框架。
评测沙箱选择
调研了几个方案:
- 自己基于 Docker 实现:灵活但工作量大,安全性需要自己把控
- 使用开源 go-judge:基于 nsjail 的沙箱,功能完善,社区活跃
- 使用洛谷的评测内核:不开源
最终选择 go-judge,通过 HTTP 接口调用,把评测逻辑和业务逻辑解耦。
二、系统架构设计
2.1 整体架构
┌─────────────────────────────────────────────────────────────┐
│ 前端层 │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ 学生端 │ │ 管理后台 │ │ 竞赛页面 │ │
│ │ Vue3 + TS │ │ Vue3 + TS │ │ Vue3 + TS │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ API 网关层 (go-zero) │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ JWT 认证 │ │ 限流熔断 │ │ 日志追踪 │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 业务服务层 │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ 用户服务 │ │ 题目服务 │ │ 评测服务 │ │ 竞赛服务 │ │
│ │ (auth) │ │ (problem)│ │(submission)│ │(contest) │ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
└─────────────────────────────────────────────────────────────┘
│
┌─────────────────────┼─────────────────────┐
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ MySQL │ │ Redis │ │ go-judge │
│ (主存储) │ │ (缓存/会话) │ │ (评测沙箱) │
└──────────────┘ └──────────────┘ └──────────────┘
2.2 目录结构设计
coder-go/
├── api/coder/ # 主 API 服务
│ ├── desc/ # API 定义文件 (.api)
│ │ └── modules/ # 按模块拆分
│ │ ├── user.api
│ │ ├── problem.api
│ │ ├── submission.api
│ │ └── ...
│ ├── internal/
│ │ ├── handler/ # HTTP 处理器(自动生成)
│ │ ├── logic/ # 业务逻辑层
│ │ ├── svc/ # 服务上下文
│ │ └── config/ # 配置定义
│ └── etc/
│ └── coder-api.yaml # 配置文件
├── model/ # 数据模型层(goctl 生成)
│ ├── usersmodel.go
│ ├── problemsmodel.go
│ └── ...
├── pkg/ # 公共包
│ ├── judge/ # 评测客户端
│ ├── jwtx/ # JWT 工具
│ ├── errorx/ # 错误处理
│ └── ...
└── admin/ # 管理后台前端
├── src/
│ ├── views/ # 页面组件
│ ├── api/ # API 接口
│ └── components/ # 公共组件
└── package.json
三、核心模块实现
3.1 用户认证系统
3.1.1 双 Token 机制
考虑到用户体验和安全性,采用 Access Token + Refresh Token 方案:
- Access Token:有效期 2 小时,存储在内存中,频繁使用
- Refresh Token:有效期 7 天,存储在 HTTP-only Cookie 中,用于续期
// pkg/jwtx/jwtx.go
// GenerateAccessToken 生成访问令牌
func GenerateAccessToken(secret string, expireSeconds int64, claims *Claims) (string, error) {
claims.TokenType = TokenTypeAccess
claims.RegisteredClaims = jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Duration(expireSeconds) * time.Second)),
IssuedAt: jwt.NewNumericDate(time.Now()),
Issuer: "coder-go",
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString([]byte(secret))
}
// GenerateRefreshToken 生成刷新令牌(带版本号)
func GenerateRefreshToken(secret string, expireSeconds int64, claims *RefreshClaims) (string, error) {
claims.TokenType = TokenTypeRefresh
claims.RegisteredClaims = jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Duration(expireSeconds) * time.Second)),
IssuedAt: jwt.NewNumericDate(time.Now()),
Issuer: "coder-go",
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString([]byte(secret))
}
Refresh Token 带版本号的设计是为了支持”踢人下线”功能。当管理员禁用用户或用户修改密码时,递增版本号,旧的 Refresh Token 就失效了。
3.1.2 密码安全
前端使用 RSA 公钥加密密码,后端用私钥解密后再用 bcrypt 哈希存储:
// api/coder/internal/service/auth/loginservice.go
func (s *LoginService) decryptPassword(encryptedPassword string) (string, error) {
if s.svcCtx.Encrypt == nil {
return encryptedPassword, nil
}
return s.svcCtx.Encrypt.DecryptPassword(encryptedPassword)
}
// 验证密码
func verifyPassword(passwordHash, password string) bool {
err := bcrypt.CompareHashAndPassword([]byte(passwordHash), []byte(password))
return err == nil
}
3.1.3 登录锁定
防止暴力破解,连续失败 5 次锁定账户 30 分钟:
// pkg/loginlock/loginlock.go
type LoginLock struct {
redis *redis.Redis
}
func (l *LoginLock) RecordFailedLogin(ctx context.Context, username string) error {
key := fmt.Sprintf("login_fail:%s", username)
count, _ := l.redis.Incr(key)
if count == 1 {
l.redis.Expire(key, 30*60) // 30分钟过期
}
if count >= 5 {
lockKey := fmt.Sprintf("login_lock:%s", username)
l.redis.Setex(lockKey, "1", 30*60)
}
return nil
}
3.2 题目管理系统
3.2.1 数据库设计
题目表的核心字段:
CREATE TABLE problems (
id BIGINT PRIMARY KEY COMMENT '分布式ID',
problem_no VARCHAR(20) UNIQUE COMMENT '题号如 P1001',
title VARCHAR(200) NOT NULL COMMENT '题目标题',
description TEXT COMMENT '题面描述(Markdown)',
input_format TEXT COMMENT '输入格式说明',
output_format TEXT COMMENT '输出格式说明',
constraints TEXT COMMENT '数据范围与提示',
difficulty TINYINT DEFAULT 1 COMMENT '1-简单 2-中等 3-困难',
time_limit INT DEFAULT 1000 COMMENT '时间限制(ms)',
memory_limit INT DEFAULT 128 COMMENT '内存限制(MB)',
source VARCHAR(100) COMMENT '题目来源',
is_public TINYINT DEFAULT 1 COMMENT '是否公开',
status TINYINT DEFAULT 1 COMMENT '0-下架 1-上架',
submit_count INT DEFAULT 0 COMMENT '提交次数',
accepted_count INT DEFAULT 0 COMMENT '通过次数',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
测试用例单独存储,支持多组测试:
CREATE TABLE problem_test_cases (
id BIGINT PRIMARY KEY,
problem_id BIGINT NOT NULL COMMENT '关联题目ID',
input TEXT COMMENT '输入数据',
output TEXT COMMENT '期望输出',
score INT DEFAULT 0 COMMENT '该测试点分值',
is_hidden TINYINT DEFAULT 0 COMMENT '是否隐藏测试点',
sort_order INT DEFAULT 0 COMMENT '排序',
INDEX idx_problem_id (problem_id)
);
3.2.2 题面编辑器
管理后台使用 md-editor-v3 组件,支持 Markdown 编辑和实时预览:
<template>
<md-editor
v-model="form.description"
:toolbars="toolbars"
:upload-images="handleUploadImages"
/>
</template>
图片上传走 MinIO 对象存储,题面中引用图片使用相对路径,前端渲染时拼接 CDN 域名。
3.3 评测系统(核心)
3.3.1 评测流程
用户提交代码
│
▼
创建提交记录(status=pending)
│
▼
调用 go-judge 编译
│
├── 编译失败 → 返回 compile_error
│
▼
逐个执行测试用例
│
├── 任意测试点超时 → time_limit_exceeded
├── 任意测试点超内存 → memory_limit_exceeded
├── 任意测试点答案错误 → wrong_answer
│
▼
所有测试点通过 → accepted
3.3.2 流式响应实现
评测过程可能持续数秒,采用 SSE(Server-Sent Events)实时推送进度:
// api/coder/internal/logic/submission/runcodestreamlogic.go
func (l *RunCodeStreamLogic) RunCodeStream(req *types.RunCodeReq, client chan<- *types.RunCodeResp) error {
// 1. 参数校验
if err := ValidateRequest(req); err != nil {
SendError(l.ctx, client, err.Error())
return nil
}
userID := GetUserID(l.ctx)
if userID == 0 {
SendError(l.ctx, client, "未登录或登录已过期")
return nil
}
// 2. 判断是提交还是测试运行
if req.IsSubmit {
return l.handleSubmit(req, userID, client)
}
return l.handleRun(req, client)
}
// 处理代码提交
func (l *RunCodeStreamLogic) handleSubmit(req *types.RunCodeReq, userID int64, client chan<- *types.RunCodeResp) error {
// 发送开始消息
SendMessageWithStatus(l.ctx, client, "开始提交...", constants.StatusPending)
// 获取题目信息
problem, err := l.svcCtx.ProblemsModel.FindOne(l.ctx, problemID)
if err != nil {
SendError(l.ctx, client, "题目不存在")
return nil
}
// 创建提交记录
submissionID := l.svcCtx.IDGen.NextID()
submission := &model.Submissions{
Id: submissionID,
ProblemId: problemID,
UserId: userID,
Code: req.Code,
Language: req.Language,
Status: constants.StatusPending,
}
l.svcCtx.SubmissionsModel.InsertWithID(l.ctx, submissionID, submission)
// 编译
SendMessageWithStatus(l.ctx, client, "正在编译...", constants.StatusCompiling)
compileResult := judge.Compile(req.Code, req.Language)
if compileResult.Status != judge.StatusAccepted {
l.updateSubmissionStatus(submissionID, string(judge.StatusCompileError), 0, 0, 0, compileResult.Error)
SendFinalResult(l.ctx, client, judge.StatusCompileError, 0)
return nil
}
// 执行测试用例
testCases, _ := l.svcCtx.ProblemTestCasesModel.FindByProblemId(l.ctx, problemID)
totalScore := 0
maxMemory := int64(0)
totalTime := int64(0)
for i, tc := range testCases {
SendMessageWithStatus(l.ctx, client,
fmt.Sprintf("正在运行测试点 %d/%d...", i+1, len(testCases)),
constants.StatusRunning)
result := judge.Run(compileResult.Executable, tc.Input, problem.TimeLimit, problem.MemoryLimit)
if result.Status != judge.StatusAccepted {
l.updateSubmissionStatus(submissionID, string(result.Status), totalScore, totalTime, maxMemory, result.ErrorMessage)
SendFinalResult(l.ctx, client, result.Status, totalScore)
return nil
}
totalScore += tc.Score
if result.Memory > maxMemory {
maxMemory = result.Memory
}
totalTime += result.Runtime
}
// 全部通过
l.updateSubmissionStatus(submissionID, string(judge.StatusAccepted), totalScore, totalTime, maxMemory, "")
SendFinalResult(l.ctx, client, judge.StatusAccepted, totalScore)
return nil
}
3.3.3 go-judge 调用封装
// pkg/judge/judge.go
type Client struct {
endpoint string
client *http.Client
}
// Compile 编译代码
func (c *Client) Compile(ctx context.Context, code, language string) (*CompileResult, error) {
langConfig, ok := Languages[language]
if !ok {
return nil, ErrLanguageNotSupported
}
req := &Request{
Cmd: []Cmd{{
Args: langConfig.CompileCmd,
Env: []string{"PATH=/usr/bin:/bin"},
Files: []File{
{Content: &code},
},
CPULimit: DefaultCompileCPUTime.Nanoseconds(),
MemoryLimit: DefaultCompileMemory,
ProcLimit: DefaultCompileProcLimit,
CopyIn: map[string]File{
langConfig.SourceFile: {Content: &code},
},
CopyOutCached: []string{"a.out"},
}},
}
resp, err := c.sendRequest(ctx, req)
if err != nil {
return nil, err
}
result := resp[0]
if result.Status != "Accepted" {
return &CompileResult{
Status: judge.StatusCompileError,
Error: result.Files.Stderr,
}, nil
}
return &CompileResult{
Status: judge.StatusAccepted,
FileID: result.FileIds["a.out"],
Executable: result.Files.Stdout,
}, nil
}
// Run 执行代码
func (c *Client) Run(ctx context.Context, fileID, input string, timeLimit, memoryLimit int64) (*RunResult, error) {
req := &Request{
Cmd: []Cmd{{
Args: []string{"a.out"},
Env: []string{"PATH=/usr/bin:/bin"},
Files: []File{
{Content: &input},
},
CPULimit: timeLimit * 1e6, // 转换为纳秒
MemoryLimit: memoryLimit << 20,
ProcLimit: DefaultRunProcLimit,
CopyIn: map[string]File{
"a.out": {FileId: fileID},
},
CopyOut: []string{"stdout", "stderr"},
}},
}
resp, err := c.sendRequest(ctx, req)
if err != nil {
return nil, err
}
result := resp[0]
status := SandboxStatusMap[result.Status]
return &RunResult{
Status: status,
Runtime: result.Time / 1e6, // 转换为毫秒
Memory: result.Memory,
Output: result.Files.Stdout,
Stderr: result.Files.Stderr,
}, nil
}
3.3.4 语言配置
目前支持 C++11/14/17:
var Languages = map[string]LanguageConfig{
"cpp11": {
Name: "C++11",
FileExt: ".cpp",
SourceFile: "main.cpp",
CompileCmd: []string{"/usr/bin/g++", "-std=c++11", "-O2", "-o", "a.out", "main.cpp"},
RunCmd: []string{"./a.out"},
},
"cpp14": {
Name: "C++14",
FileExt: ".cpp",
SourceFile: "main.cpp",
CompileCmd: []string{"/usr/bin/g++", "-std=c++14", "-O2", "-o", "a.out", "main.cpp"},
RunCmd: []string{"./a.out"},
},
"cpp17": {
Name: "C++17",
FileExt: ".cpp",
SourceFile: "main.cpp",
CompileCmd: []string{"/usr/bin/g++", "-std=c++17", "-O2", "-o", "a.out", "main.cpp"},
RunCmd: []string{"./a.out"},
},
}
3.4 工程规范
3.4.1 代码生成工作流
项目使用 goctl 工具链实现代码生成:
# Makefile
api-coder:
cd api/coder/desc && goctl api go -api coder.api -dir ../
db-model:
goctl model mysql datasource \
--url "$(DB_URL)" \
--table "$(TABLE)" \
--dir "./model" \
--cache=false \
--style gozero
开发流程:
- 修改
.api文件定义接口 - 执行
make api-coder生成 Handler、Logic、Types - 在 Logic 层实现业务逻辑
- 需要新表时执行
make db-model TABLE=xxx
3.4.2 分层约束
三大禁令:
- Logic 层禁止直接 SQL:所有数据库操作通过 Model 层方法
- 禁止 for 循环 SQL:批量查询使用
FindByIDs - 多表变更必须事务:使用
TransactCtx
// ❌ 错误:Logic 层直接写 SQL
func (l *Logic) GetUserProblems(userID int64) ([]Problem, error) {
rows, _ := l.svcCtx.DB.Query("SELECT * FROM problems WHERE user_id = ?", userID)
// ...
}
// ✅ 正确:调用 Model 层
func (l *Logic) GetUserProblems(userID int64) ([]Problem, error) {
return l.svcCtx.ProblemsModel.FindByUserID(l.ctx, userID)
}
// ❌ 错误:N+1 查询
for _, id := range problemIDs {
p, _ := l.svcCtx.ProblemsModel.FindOne(l.ctx, id)
problems = append(problems, p)
}
// ✅ 正确:批量查询
problems, _ := l.svcCtx.ProblemsModel.FindByIDs(l.ctx, problemIDs)
3.4.3 错误处理
统一错误码:
// pkg/errorx/errorx.go
const (
CodeSuccess = 0
CodeInvalidParam = 4000
CodeUnauthorized = 4010
CodePermissionDeny = 4030
CodeResourceNotFound = 4040
CodeInternalError = 5000
CodeCreateFailed = 5001
CodeUpdateFailed = 5002
CodeDeleteFailed = 5003
)
type Error struct {
Code int `json:"code"`
Message string `json:"message"`
}
func NewError(code int, message string) error {
return &Error{Code: code, Message: message}
}
3.4.4 分布式 ID
使用雪花算法生成 64 位 ID,对外暴露时使用 sqids 转换为短码:
// pkg/sqidx/sqidx.go
func EncodeInt64(id int64) (string, error) {
s, _ := sqids.New(sqids.Options{
Alphabet: "abcdefghijklmnopqrstuvwxyz0123456789",
})
return s.Encode([]uint64{uint64(id)})
}
func DecodeInt64(code string) (int64, error) {
s, _ := sqids.New(sqids.Options{
Alphabet: "abcdefghijklmnopqrstuvwxyz0123456789",
})
ids, _ := s.Decode(code)
if len(ids) == 0 {
return 0, errors.New("invalid code")
}
return int64(ids[0]), nil
}
这样数据库里存 123456789,API 返回 a7x9k2,更友好且防止遍历。
四、前端实现
4.1 技术栈
- Vue 3:Composition API,逻辑复用更方便
- TypeScript:类型安全,IDE 提示友好
- Element Plus:组件库,快速搭建界面
- Monaco Editor:VS Code 同款编辑器
- Pinia:状态管理,比 Vuex 简洁
- Tailwind CSS:原子化 CSS
4.2 代码编辑器
<!-- admin/src/components/CodeEditor/MonacoEditor.vue -->
<template>
<div ref="editorContainer" class="monaco-editor-container"></div>
</template>
<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount, watch } from 'vue'
import * as monaco from 'monaco-editor'
const props = defineProps<{
modelValue: string
language?: string
theme?: string
}>()
const emit = defineEmits<['update:modelValue']>()
const editorContainer = ref<HTMLElement>()
let editor: monaco.editor.IStandaloneCodeEditor
onMounted(() => {
editor = monaco.editor.create(editorContainer.value!, {
value: props.modelValue,
language: props.language || 'cpp',
theme: props.theme || 'vs-dark',
automaticLayout: true,
minimap: { enabled: false },
fontSize: 14,
lineNumbers: 'on',
roundedSelection: false,
scrollBeyondLastLine: false,
readOnly: false,
})
editor.onDidChangeModelContent(() => {
emit('update:modelValue', editor.getValue())
})
})
onBeforeUnmount(() => {
editor?.dispose()
})
</script>
4.3 流式评测结果展示
<script setup lang="ts">
import { fetchEventSource } from '@microsoft/fetch-event-source'
const runCode = async () => {
const ctrl = new AbortController()
await fetchEventSource('/api/submissions/run/stream', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`,
},
body: JSON.stringify({
problem_id: problemId,
code: code.value,
language: language.value,
is_submit: true,
}),
signal: ctrl.signal,
onmessage(msg) {
const data = JSON.parse(msg.data)
status.value = data.status
message.value = data.message
if (data.status === 'accepted' || data.status === 'wrong_answer') {
// 评测结束
ctrl.abort()
}
},
})
}
</script>
五、部署与运维
5.1 部署架构
服务器
├── Docker Compose
│ ├── coder-api # Go API 服务
│ ├── coder-judge # go-judge 沙箱
│ ├── mysql # 数据库
│ ├── redis # 缓存
│ └── minio # 对象存储
├── Nginx
│ ├── 反向代理到 coder-api
│ └── 静态文件服务(前端)
5.2 Makefile 工具
# 部署命令
make deploy ENV=test # 部署测试环境
make deploy ENV=prod # 部署生产环境
# 开发命令
make api-coder # 生成 API 代码
make db-model TABLE=problems # 生成 Model 代码
make run-api # 本地运行 API
make run-f # 本地运行前端
# 运维命令
make status ENV=test # 查看服务状态
make logs ENV=test # 查看日志
make restart ENV=test # 重启服务
5.3 配置管理
# api/coder/etc/coder-api.yaml
Name: coder-api
Host: 0.0.0.0
Port: 8002
Timeout: 30000
Mode: dev
Mysql:
DataSource: root:xxx@tcp(127.0.0.1:3306)/coder?charset=utf8mb4&parseTime=true
Redis:
Host: 127.0.0.1:6379
Type: node
Pass: "xxx"
Auth:
AccessSecret: "xxx"
AccessExpire: 7200
RefreshSecret: "xxx"
RefreshExpire: 604800
GoJudge:
Endpoint: http://127.0.0.1:5050
MinIO:
Endpoint: 127.0.0.1:9000
AccessKeyID: xxx
SecretAccessKey: xxx
BucketName: coder
六、遇到的问题与解决方案
6.1 评测超时问题
现象:某些代码在本地运行正常,但在沙箱中超时。
原因:go-judge 的 CPU 时间限制和真实时间限制混淆。
解决:明确区分 cpuLimit 和 realLimit,评测时主要限制 CPU 时间。
CPULimit: timeLimit * 1e6, // CPU 时间(纳秒)
RealLimit: timeLimit * 3 * 1e6, // 真实时间放宽 3 倍
6.2 内存泄漏
现象:评测服务运行一段时间后内存占用持续增长。
原因:go-judge 的缓存文件没有清理。
解决:定期调用 go-judge 的清理接口,或设置缓存过期时间。
6.3 数据库连接池耗尽
现象:高峰期出现 too many connections 错误。
解决:调整连接池参数:
// 在配置中设置
MaxOpenConns: 100
MaxIdleConns: 10
ConnMaxLifetime: 3600
6.4 前端编辑器加载慢
现象:Monaco Editor 首次加载需要 3-5 秒。
解决:
- 使用 CDN 加载编辑器资源
- 开启 gzip 压缩
- 按需加载语言包,只加载 C++ 相关
七、总结
这个项目从需求分析到上线用了大约 3 个月时间。最大的收获是:
-
技术选型要务实:不要追求最新最酷的技术,go-judge 虽然不如自研沙箱灵活,但稳定可靠,节省了大量时间。
-
代码生成提效:goctl 的代码生成能力让 API 开发效率提升很多,特别是前后端联调时,接口定义一确定,代码就生成好了。
-
工程规范很重要:三大禁令虽然刚开始觉得约束多,但避免了后期很多坑,特别是 N+1 查询问题。
-
评测系统是核心:评测的稳定性和安全性直接决定产品可用性,这方面值得投入时间打磨。
目前系统已经支撑了几次校内竞赛和日常教学,累计提交代码超过 10 万次。下一步计划:
- 接入 AI 辅助判题,对错误代码给出提示
- 基于用户行为数据做个性化题目推荐
- 支持更多编程语言(Python、Go)
如果你也在做类似系统,欢迎交流。