当前位置: 首页 > news >正文

[YMOJ]现代化开源OJ(在线编程)平台技术分享

声明: 此文档可能部分与项目不契合,原因在于本项目在不断的更新迭代中,文档只是记录此项目大部分功能
已经初步完成,可部署上线使用

YMCloud 在线编程云平台

技术总结文档


YMCloud - 现代化在线判题系统

基于 Spring Boot 3 + Vue 3 + TypeScript 的在线编程平台


项目作者: Yemiao
项目版本: v3.0
文档日期: 2025年10月
开源协议: MIT License


项目仓库
https://gitee.com/yemiaoo/ymcloud


项目主页图片

在这里插入图片描述


摘要

中文摘要

随着信息技术的快速发展和在线教育的蓬勃兴起,编程教育已成为培养学生计算思维和问题解决能力的重要途径。在线判题系统(Online Judge,简称OJ)作为编程教育的重要支撑平台,为学习者提供了便捷的代码提交、自动评测和即时反馈服务。然而,传统的在线判题系统普遍存在架构陈旧、安全性不足、并发处理能力有限等问题,难以满足现代编程教育的多样化需求。

本项目设计并实现了一个名为 YMCloud 的现代化在线判题系统。系统采用 前后端分离架构,后端基于 Spring Boot 3.5.3 框架,采用模块化设计,划分为通用模块(ymcloud-common)、数据模型模块(ymcloud-pojo)、核心模块(ymcloud-core)、判题模块(ymcloud-judge)和 Web 接口模块(ymcloud-web)五大模块,确保了系统的高内聚低耦合特性。前端采用 Vue 3.5.13 + TypeScript 5.8 技术栈,配合 Vite 构建工具和 Pinia 状态管理,实现了响应式的用户界面和流畅的交互体验。

系统的核心功能涵盖五大模块:用户系统 实现了基于 JWT 的身份认证和 RBAC 权限管理模型;题库系统 支持题目的增删改查、标签分类和在线编程,集成 CodeMirror 编辑器提供语法高亮功能;竞赛系统 支持 ACM 和 OI 两种赛制,提供实时排名榜和比赛管理功能;判题系统 是本项目的技术核心,基于 Docker 容器技术 实现代码沙箱隔离,支持 Java、C++、C、Python 四种编程语言,通过 RabbitMQ 消息队列 实现异步判题,显著提升了系统的并发处理能力和安全性;管理系统 为管理员提供了完善的后台管理功能,包括用户管理、题目管理、比赛管理和系统监控。

本系统的主要创新点体现在:(1)采用 Docker 容器技术构建安全沙箱,对每个代码提交进行资源隔离和限制,有效防止了恶意代码攻击;(2)引入 RabbitMQ 消息队列实现异步判题架构,将代码提交与判题执行解耦,大幅提升了系统的并发处理能力;(3)基于 Spring Security 实现细粒度的 RBAC 权限控制模型,支持动态路由加载和权限验证;(4)采用 MyBatis-Plus 简化数据访问层开发,优化数据库查询性能。

系统已完成开发和测试,实现了预期的所有功能目标。经过性能测试,系统在并发环境下表现稳定,判题准确率高,具有良好的可扩展性和实用价值。本项目不仅为编程教育提供了一个功能完善的实践平台,也为在线判题系统的设计与实现提供了有价值的参考。

关键字: 在线判题系统;Docker 容器;异步判题;前后端分离;Spring Boot;Vue.js;代码沙箱;权限管理


英文摘要(Abstract)

With the rapid development of information technology and the flourishing of online education, programming education has become an important way to cultivate students’ computational thinking and problem-solving abilities. Online Judge (OJ) systems, as crucial supporting platforms for programming education, provide learners with convenient code submission, automatic evaluation, and instant feedback services. However, traditional online judge systems commonly suffer from outdated architecture, insufficient security, and limited concurrent processing capabilities, making it difficult to meet the diverse needs of modern programming education.

This project designs and implements a modern online judge system named YMCloud. The system adopts a front-end and back-end separation architecture. The back-end is based on the Spring Boot 3.5.3 framework with modular design, divided into five major modules: common module (ymcloud-common), data model module (ymcloud-pojo), core module (ymcloud-core), judge module (ymcloud-judge), and web interface module (ymcloud-web), ensuring high cohesion and low coupling characteristics of the system. The front-end uses Vue 3.5.13 + TypeScript 5.8 technology stack, combined with Vite build tool and Pinia state management, implementing a responsive user interface and smooth interaction experience.

The core functions of the system cover five major modules: The User System implements JWT-based authentication and RBAC permission management model; The Problem System supports CRUD operations, tag classification, and online programming, integrating CodeMirror editor for syntax highlighting; The Contest System supports ACM and OI competition modes, providing real-time ranking and competition management functions; The Judge System is the technical core of this project, implementing code sandbox isolation based on Docker container technology, supporting four programming languages (Java, C++, C, Python), and achieving asynchronous judging through RabbitMQ message queue, significantly improving the system’s concurrent processing capability and security; The Management System provides administrators with comprehensive backend management functions, including user management, problem management, contest management, and system monitoring.

The main innovations of this system are: (1) Using Docker container technology to build a secure sandbox, performing resource isolation and limitation for each code submission, effectively preventing malicious code attacks; (2) Introducing RabbitMQ message queue to implement asynchronous judging architecture, decoupling code submission from judging execution, significantly improving the system’s concurrent processing capability; (3) Implementing fine-grained RBAC permission control model based on Spring Security, supporting dynamic route loading and permission verification; (4) Using MyBatis-Plus to simplify data access layer development and optimize database query performance.

The system has completed development and testing, achieving all expected functional objectives. Through performance testing, the system performs stably under concurrent environments with high judging accuracy, demonstrating good scalability and practical value. This project not only provides a feature-rich practice platform for programming education but also offers valuable references for the design and implementation of online judge systems.

Keywords: Online Judge System; Docker Container; Asynchronous Judging; Front-End and Back-End Separation; Spring Boot; Vue.js; Code Sandbox; Permission Management


目录

主要章节

  • 摘要
  • 第一章 项目概述
    • 1.1 项目背景
    • 1.2 项目介绍
    • 1.3 系统特色与创新点
  • 第二章 需求分析
    • 2.1 功能性需求
    • 2.2 非功能性需求
    • 2.3 用例图与用例分析
  • 第三章 技术架构设计
    • 3.1 系统总体架构
    • 3.2 技术栈选型
    • 3.3 模块划分
    • 3.4 架构设计原则
  • 第四章 数据库设计
    • 4.1 数据库概述
    • 4.2 E-R 图设计
    • 4.3 数据表设计详解
    • 4.4 索引设计
  • 第五章 核心功能实现
    • 5.1 用户认证与权限管理
    • 5.2 题目管理系统
    • 5.3 在线判题系统
    • 5.4 比赛管理系统
    • 5.5 文件管理系统
  • 第六章 前端实现
    • 6.1 前端项目结构
    • 6.2 路由设计
    • 6.3 状态管理
    • 6.4 核心页面实现
    • 6.5 UI/UX 设计
  • 第七章 后端实现
    • 7.1 后端项目结构
    • 7.2 控制层设计
    • 7.3 服务层设计
    • 7.4 数据访问层
    • 7.5 安全设计
    • 7.6 性能优化
  • 第八章 系统部署
  • 第九章 项目总结与展望


第一章 项目概述

1.1 项目背景

1.1.1 在线编程教育的发展趋势

近年来,随着"互联网+"教育模式的深入发展,在线编程教育已成为计算机教育领域的重要组成部分。传统的编程教学受制于时间、空间和师资力量的限制,难以满足日益增长的学习需求。在线判题系统(Online Judge,简称 OJ)的出现,为编程学习者提供了一个不受时空限制的练习平台,学习者可以随时随地提交代码、获取评测结果和反馈,极大地提高了学习效率。

当前,全球范围内涌现了许多知名的在线判题平台,如 LeetCode、Codeforces、洛谷等,这些平台已成为程序员提升编程能力、准备面试、参加竞赛的重要工具。在国内,随着信息学奥林匹克竞赛(NOI)、ACM-ICPC 等编程竞赛的普及,以及高校计算机教育的发展,对在线判题系统的需求日益旺盛。

1.1.2 现有系统存在的问题

尽管在线判题系统已得到广泛应用,但现有的许多系统仍存在以下问题:

  1. 架构陈旧:部分系统采用传统的单体架构,难以应对高并发场景,系统可扩展性差。

  2. 安全性不足:判题过程中若未对用户提交的代码进行有效隔离,可能导致恶意代码危害服务器安全,如无限循环、fork 炸弹、文件系统破坏等。

  3. 并发处理能力有限:在高峰期或竞赛场景下,大量代码提交可能导致系统响应缓慢甚至崩溃。

  4. 用户体验欠佳:部分系统界面陈旧,交互不友好,缺乏现代化的用户体验设计。

  5. 功能单一:仅提供基本的题目浏览和代码提交功能,缺乏完善的用户管理、权限控制、比赛管理等扩展功能。

1.1.3 项目开发的动机与目标

基于上述背景,本项目旨在设计并实现一个现代化、高性能、安全可靠的在线判题系统。项目的主要目标包括:

  1. 现代化架构设计:采用前后端分离架构和模块化设计,提高系统的可维护性和可扩展性,为后续微服务化改造预留空间。

  2. 安全性保障:基于 Docker 容器技术实现代码沙箱隔离,确保每个代码提交运行在独立的隔离环境中,有效防止恶意代码攻击。

  3. 高并发处理:引入 RabbitMQ 消息队列实现异步判题,将代码提交与判题执行解耦,显著提升系统的并发处理能力。

  4. 优秀的用户体验:采用 Vue 3 + TypeScript 构建现代化的前端界面,提供流畅的交互体验和响应式设计。

  5. 完善的功能模块:实现用户管理、题库管理、比赛管理、权限控制等完整功能,满足多样化的应用场景需求。


1.2 项目介绍

1.2.1 系统定位与核心价值

YMCloud 是一个面向编程学习者、教育机构和竞赛组织者的现代化在线判题系统。系统定位于提供高性能、高安全性、易扩展的编程实践平台,支持个人练习、课程教学、编程竞赛等多种应用场景。

系统的核心价值体现在:

  • 为学习者提供便捷的编程练习环境:支持多种编程语言,提供即时的代码评测反馈,帮助学习者快速提升编程能力。

  • 为教育机构提供完善的教学工具:支持题目自定义、权限管理、学习数据统计等功能,满足课程教学和作业管理需求。

  • 为竞赛组织者提供可靠的比赛平台:支持 ACM、OI 等多种赛制,提供实时排名、比赛管理等功能,确保比赛公平公正进行。

  • 为开发者提供可扩展的系统架构:采用模块化设计和开放的技术栈,支持二次开发和功能扩展。

1.2.2 主要功能模块概览

YMCloud 系统由以下五大核心功能模块构成:

1. 用户系统模块

  • 用户注册、登录与身份认证(基于 JWT Token)
  • 基于角色的访问控制(RBAC)模型
  • 用户个人主页:展示解题统计、提交热力图、AC 记录等
  • 个人信息管理:头像、昵称、邮箱、学校等信息维护
  • 账号安全:密码修改、登录日志、异常登录检测

2. 题库系统模块

  • 题目管理:题目的增删改查、标签分类、难度分级
  • 在线编程:集成 CodeMirror 代码编辑器,支持多语言语法高亮
  • 题目浏览:题目列表展示、搜索筛选、题目详情查看
  • 测试用例管理:支持多组测试数据的上传、下载和管理
  • 题目权限控制:公开题目、私有题目、比赛专用题目

3. 判题系统模块(核心模块)

  • 多语言支持:Java、C++、C、Python 四种主流编程语言
  • Docker 沙箱隔离:每个代码提交运行在独立的容器中
  • 资源限制:时间限制、内存限制、进程数限制、网络隔离
  • 异步判题:基于 RabbitMQ 消息队列实现高并发判题
  • 多种评测模式:ACM 模式(通过即得分)、OI 模式(支持部分分)
  • 判题结果:AC、WA、TLE、MLE、RE、CE、SE、PA 等多种状态

4. 竞赛系统模块

  • 比赛管理:比赛创建、编辑、删除、克隆
  • 比赛模式:支持 ACM、OI 两种主流赛制
  • 比赛权限:公开比赛、私有比赛(密码保护)
  • 实时排名:支持实时计算和展示比赛排名榜
  • 比赛题目:题目添加、顺序调整、显示名称自定义
  • 比赛报名:用户报名管理、参赛权限验证

5. 管理系统模块

  • 管理员仪表盘:系统概览、数据统计、快速操作入口
  • 用户管理:用户列表、角色分配、权限管理、账号状态控制
  • 题目管理:题目编辑、测试用例上传、题目状态管理
  • 比赛管理:比赛配置、题目管理、参赛用户管理
  • 系统监控:服务器状态监控、在线用户统计、系统日志查看
  • 权限管理:角色配置、权限分配、动态路由加载
1.2.3 目标用户群体分析

YMCloud 系统面向以下主要用户群体:

1. 编程学习者

  • 在校学生:通过系统进行算法练习,准备 ACM、信息学奥赛等竞赛
  • 求职者:刷题提升编程能力,准备技术面试
  • 编程爱好者:利用碎片时间进行编程练习,提升技能

2. 教育工作者

  • 高校教师:将系统作为课程配套工具,布置编程作业,进行学习评估
  • 培训机构:组织编程课程,进行学员练习和能力测试

3. 竞赛组织者

  • 高校 ACM 社团:组织校内编程竞赛,选拔参赛选手
  • 企业 HR:组织编程挑战赛,选拔技术人才

4. 系统管理员

  • 平台运维人员:负责系统日常维护、用户管理、内容审核

1.3 系统特色与创新点

1.3.1 基于 Docker 的安全沙箱机制

技术特点:系统采用 Docker 容器技术构建代码执行沙箱,每个代码提交都在独立的容器中运行,与宿主机和其他容器完全隔离。

安全保障

  • 容器隔离:每个提交运行在独立的 Docker 容器中,防止代码间相互干扰
  • 资源限制:严格限制 CPU 使用率、内存大小、磁盘空间
  • 网络隔离:禁用容器网络访问,防止恶意网络操作
  • 文件系统保护:采用只读根文件系统,防止恶意文件操作
  • 进程限制:限制进程数量,防止 fork 炸弹攻击
  • 超时控制:超时自动终止容器,防止无限循环占用资源

创新点:相比传统的基于系统调用拦截的沙箱方案,Docker 容器方案更加安全可靠,且易于扩展到分布式环境。

1.3.2 基于 RabbitMQ 的异步判题架构

技术架构:系统引入 RabbitMQ 消息队列,将代码提交与判题执行解耦,实现异步化判题流程。

判题流程

用户提交代码 → Web 层创建判题记录 → 发送消息到 RabbitMQ 队列 
→ 判题服务监听队列获取任务 → Docker 容器执行判题 
→ 结果写回数据库 → 用户查询判题结果

优势

  • 高并发处理:消息队列天然支持高并发,可同时处理大量判题任务
  • 负载均衡:支持多个判题节点共同消费队列,实现负载均衡
  • 削峰填谷:在高峰期将任务缓存在队列中,平滑处理
  • 容错性:判题节点故障不影响消息持久化,系统具有良好的容错能力
  • 可扩展性:可轻松添加判题节点,实现水平扩展

创新点:异步判题架构使系统具备了分布式判题的能力,为后续微服务化改造奠定了基础。

1.3.3 细粒度的 RBAC 权限控制模型

权限模型:系统基于 Spring Security 实现了完整的 RBAC(基于角色的访问控制)权限模型,支持"用户-角色-权限"三层架构。

核心功能

  • JWT 身份认证:采用 JWT Token 实现无状态身份认证,支持分布式部署
  • 动态权限加载:用户登录后动态加载权限列表和可访问路由
  • 细粒度权限控制:支持到按钮级别的权限控制,如题目编辑、用户管理等
  • 权限注解:通过 @PreAuthorize 等注解实现方法级别的权限验证
  • 多角色支持:用户可拥有多个角色,权限合并计算
  • 前后端权限同步:前端根据权限动态渲染菜单和按钮,后端进行二次验证

创新点:权限系统设计灵活,支持运行时动态调整权限,满足复杂的权限管理需求。

1.3.4 前后端分离的现代化架构

架构设计

  • 前端:基于 Vue 3 Composition API + TypeScript 构建,采用 Vite 作为构建工具,实现快速开发和热更新
  • 后端:基于 Spring Boot 3 构建,采用 Maven 多模块管理,支持模块化开发
  • API 设计:遵循 RESTful 规范,提供统一的 API 接口,前后端通过 JSON 数据交互

优势

  • 职责分离:前后端独立开发、独立部署、独立维护
  • 团队协作:前后端团队可并行开发,提高开发效率
  • 技术选型灵活:前后端可独立选择最适合的技术栈
  • 易于扩展:可轻松替换前端框架或添加多端支持(如移动端)
1.3.5 模块化的后端设计

模块划分:后端项目采用 Maven 多模块管理,划分为五大模块:

  • ymcloud-common:通用模块,包含工具类、常量定义、自定义异常等
  • ymcloud-pojo:数据模型模块,包含实体类、DTO、VO、查询对象等
  • ymcloud-core:核心模块,包含安全认证、通用拦截器、数据库配置等
  • ymcloud-judge:判题模块,包含代码沙箱、判题策略、Docker 容器管理等
  • ymcloud-web:Web 接口模块,包含控制器、服务层、数据访问层等

设计原则

  • 高内聚低耦合:每个模块职责单一,模块间依赖关系清晰
  • 可扩展性:新增功能时只需修改对应模块,不影响其他模块
  • 可测试性:模块化设计便于单元测试和集成测试
  • 可维护性:代码结构清晰,易于理解和维护

创新点:模块化设计为系统后续微服务化改造预留了空间,支持按模块拆分为独立的微服务。

1.3.6 多编程语言支持

系统支持 Java、C++、C、Python 四种主流编程语言,每种语言都有对应的代码沙箱实现:

  • JavaSandBox:基于 OpenJDK 17 环境,支持 Java 代码编译和运行
  • CppSandBox:基于 GCC 编译器,支持 C++ 代码编译和运行
  • CSandBox:基于 GCC 编译器,支持 C 代码编译和运行
  • PythonSandBox:基于 Python 3 环境,支持 Python 代码解释执行

扩展性:通过统一的 SandBox 抽象类定义沙箱接口,可轻松扩展支持更多编程语言(如 Go、Rust、JavaScript 等)。


小结:本章从项目背景、项目介绍和系统特色三个方面对 YMCloud 系统进行了全面介绍。系统采用现代化的技术架构,实现了安全可靠的在线判题功能,具有良好的可扩展性和实用价值。后续章节将详细介绍系统的需求分析、技术架构设计、数据库设计、核心功能实现等内容。



第二章 需求分析

在线判题系统是一个面向多类型用户的复杂应用系统,需要满足编程学习者、教育工作者、竞赛组织者和系统管理员等不同角色的需求。本章从功能性需求和非功能性需求两个维度,对系统进行全面的需求分析,并通过用例图清晰地描述系统的主要功能和用户交互。

2.1 功能性需求

功能性需求描述了系统应当实现的具体功能和业务逻辑。根据系统的实际业务场景,YMCloud 的功能性需求主要包括用户管理、题目管理、判题管理、比赛管理和系统管理五大模块。

2.1.1 用户管理需求

(1)用户注册与登录

  • 用户可通过邮箱进行账号注册,系统对用户名和邮箱进行唯一性验证
  • 支持邮箱验证码验证,确保邮箱真实有效
  • 用户密码采用 BCrypt 加密算法进行加密存储,确保密码安全
  • 登录成功后系统生成 JWT Token,用于后续身份认证
  • 支持记住登录状态功能,Token 有效期可配置
  • 支持密码找回功能,通过邮箱验证码重置密码

(2)用户信息管理

  • 用户可查看和编辑个人信息,包括昵称、性别、学校、个性签名、博客链接等
  • 支持用户头像上传和更换
  • 支持用户密码修改,需验证原密码
  • 记录用户最后登录时间和 IP 地址

(3)用户主页

  • 展示用户基本信息:昵称、头像、学校、签名等
  • 展示用户解题统计:AC 题数、提交总数、通过率等
  • 展示用户提交热力图:按日期统计提交活跃度
  • 展示用户 AC 记录列表:已通过的题目列表及通过时间
  • 展示用户最近提交记录

(4)权限管理

  • 系统采用 RBAC(基于角色的访问控制)模型
  • 支持多角色分配:超级管理员、普通管理员、普通用户等
  • 支持细粒度权限控制:菜单权限、按钮权限、数据权限
  • 超级管理员(ID=1)拥有所有权限
  • 普通管理员根据角色动态加载权限和路由
  • 前端根据权限动态渲染菜单和功能按钮
2.1.2 题目管理需求

(1)题目浏览(用户端)

  • 支持分页展示题目列表,显示题目编号、标题、难度、通过率等信息
  • 支持按题目标题、题目 ID 进行关键字搜索
  • 支持按难度等级筛选:入门、普及、提高、省选及以上
  • 支持按标签分类筛选题目
  • 支持按通过状态筛选:全部题目、已通过、未通过、已尝试
  • 支持随机跳转功能:随机选择一道题目进行练习

(2)题目详情查看

  • 展示题目完整信息:标题、描述、输入格式、输出格式、样例、提示
  • 支持 Markdown 格式渲染,支持数学公式(KaTeX)渲染
  • 展示题目限制:时间限制、内存限制
  • 展示题目来源和作者信息
  • 展示题目标签和难度等级
  • 展示题目统计信息:提交总数、通过人数、通过率
  • 集成 CodeMirror 代码编辑器,支持多语言语法高亮
  • 支持选择编程语言:Java、C++、C、Python
  • 支持代码模板自动加载
  • 支持在线编译运行和提交

(3)题目管理(管理端)

  • 支持题目的增删改查操作
  • 题目信息包括:题目 ID、标题、内容、输入、输出、样例、来源、难度、评测模式、可见性、时间限制、内存限制、出题人信息等
  • 支持设置题目可见性:公开(所有人可见)、私有(仅创建者和管理员可见)、比赛专用(仅在比赛中可见)
  • 支持设置评测模式:ACM 模式、OI 模式
  • 支持题目标签管理:添加、删除题目标签
  • 支持题目批量删除操作
  • 支持按关键字、难度、可见性筛选题目

(4)测试用例管理

  • 支持上传测试用例文件(.in 输入文件、.out 输出文件)
  • 支持单个或批量上传测试用例
  • 支持查看题目所有测试用例列表
  • 支持下载测试用例文件
  • 支持删除测试用例
  • 测试用例文件存储在服务器指定目录,按题目 ID 组织
  • 测试用例命名规范:1.in、1.out、2.in、2.out 等
2.1.3 判题管理需求

(1)代码提交

  • 用户可在题目详情页面提交代码
  • 支持多种编程语言:Java、C++、C、Python
  • 提交前需选择编程语言
  • 记录提交者信息:用户 ID、用户名、提交时间、IP 地址
  • 记录代码信息:代码内容、代码长度
  • 支持比赛模式下的代码提交

(2)判题执行

  • 代码提交后立即创建判题记录,状态为"等待判题"
  • 判题任务通过 RabbitMQ 消息队列异步处理
  • 判题服务监听队列,获取判题任务
  • 判题流程:
    1. 创建 Docker 容器
    2. 保存用户代码到容器
    3. 编译代码(编译型语言)
    4. 逐个运行测试用例
    5. 比对输出结果
    6. 收集时间和内存使用情况
    7. 更新判题记录
    8. 销毁 Docker 容器
  • 判题过程中状态实时更新:等待判题 → 编译中 → 判题中 → 完成
  • 支持编译错误信息捕获和展示
  • 支持运行时错误信息捕获和展示

(3)判题结果

  • 支持多种判题状态:
    • AC(Accepted):答案正确,所有测试用例通过
    • WA(Wrong Answer):答案错误,输出与期望不符
    • TLE(Time Limit Exceeded):超时,执行时间超过限制
    • MLE(Memory Limit Exceeded):内存超限,内存使用超过限制
    • RE(Runtime Error):运行时错误,程序异常终止
    • CE(Compile Error):编译错误,代码无法编译
    • SE(System Error):系统错误,判题系统异常
    • PA(Partial Accepted):部分正确(仅 OI 模式)
  • 记录判题详细信息:状态、得分、时间、内存、错误信息
  • OI 模式下支持部分分,记录每个测试用例的得分
  • 用户可查看判题详情:每个测试用例的运行结果(如果题目允许)
  • 支持代码分享功能:用户可选择公开自己的 AC 代码

(4)提交记录查询

  • 支持查看所有提交记录列表(分页展示)
  • 支持按多个维度筛选:用户、题目、语言、状态
  • 支持查看提交详情:代码内容、判题结果、错误信息
  • 支持查看他人公开的 AC 代码
  • 支持语法高亮显示代码
  • 比赛模式下可查看比赛内的提交记录
2.1.4 比赛管理需求

(1)比赛浏览(用户端)

  • 支持分页展示比赛列表
  • 按比赛时间分类展示:未开始、进行中、已结束
  • 展示比赛信息:标题、主办方、比赛时间、参赛人数、比赛状态
  • 支持按关键字搜索比赛
  • 显示比赛类型:公开比赛、私有比赛(需密码)

(2)比赛详情

  • 展示比赛完整信息:标题、Logo、说明、主办方、比赛时间、赛制、参赛人数等
  • 展示比赛状态:未开始、进行中、已结束
  • 展示比赛剩余时间(倒计时)
  • 私有比赛需输入密码才能查看详情和参赛
  • 支持比赛报名功能
  • 已报名用户可进入比赛

(3)比赛进行

  • 展示比赛题目列表:题目序号(A、B、C…)、题目标题、通过人数、尝试人数
  • 支持点击题目进入题目详情页面
  • 支持在比赛中提交代码
  • 比赛题目页面显示:题目内容、代码编辑器、提交按钮
  • 支持查看比赛内的提交记录
  • 支持查看实时排名榜

(4)比赛排名

  • 实时计算和展示比赛排名
  • ACM 模式排名规则:
    • 按通过题数降序排序
    • 通过题数相同时,按罚时升序排序
    • 罚时 = 通过题目的提交时间之和 + 错误提交次数 × 20 分钟
  • OI 模式排名规则:
    • 按总得分降序排序
    • 得分相同时,按最后提交时间升序排序
  • 排名榜显示:排名、用户名、通过题数/总分、罚时/最后提交时间、每题状态
  • 支持查看其他用户在比赛中的表现

(5)比赛管理(管理端)

  • 支持比赛的增删改查操作
  • 比赛信息包括:标题、Logo、描述、主办方、赛制、类型、可见性、密码、开始时间、结束时间、时长等
  • 支持设置比赛赛制:ACM、OI、IOI
  • 支持设置比赛类型:其他、周赛、月赛、娱乐赛、挑战赛
  • 支持设置比赛可见性:公开、私有(需密码)
  • 支持设置是否允许比赛结束后提交
  • 支持设置是否计入 Rating 积分
  • 支持比赛克隆功能:基于现有比赛创建新比赛

(6)比赛题目管理

  • 支持查看比赛题目列表
  • 支持从题库中添加题目到比赛
  • 支持批量添加题目
  • 添加题目时自动将题目可见性设置为"比赛专用"
  • 支持设置题目在比赛中的展示信息:
    • 题目序号(A、B、C…)
    • 题目标题(可自定义)
    • 气球颜色(ACM 比赛使用)
  • 支持调整题目顺序
  • 支持从比赛中移除题目
  • 移除题目时将题目可见性恢复为"私有"
2.1.5 系统管理需求

(1)用户管理(管理端)

  • 支持查看用户列表(分页展示)
  • 支持按关键字搜索用户(用户名、邮箱、昵称)
  • 支持查看用户详细信息
  • 支持新增用户
  • 支持编辑用户信息
  • 支持批量删除用户
  • 支持修改用户状态:正常、禁用
  • 支持重置用户密码
  • 支持为用户分配角色

(2)角色与权限管理

  • 支持角色管理:查询、新增、编辑、删除角色
  • 支持为角色分配权限
  • 权限分类:菜单权限、按钮权限
  • 权限树形展示,支持批量勾选
  • 支持动态路由加载:根据用户权限动态加载可访问路由
  • 前端根据权限控制菜单和按钮的显示与隐藏
  • 后端接口通过 @PreAuthorize 注解进行权限验证

(3)公告管理

  • 支持公告的增删改查操作
  • 公告信息包括:标题、内容、权重、是否置顶、状态、发布人等
  • 支持 Markdown 格式编辑公告内容
  • 支持设置公告权重:权重越大越靠前
  • 支持设置是否置顶
  • 支持设置公告状态:可见、不可见
  • 首页展示置顶公告和最新公告

(4)文件管理

  • 支持文件上传功能
  • 文件类型包括:题目附件、题目测试数据、比赛 Logo、用户头像、公告附件等
  • 记录文件信息:文件名、文件类型、文件大小、存储路径、上传者、业务类型、业务 ID 等
  • 支持设置文件访问权限:公开、私有
  • 公开文件可通过 URL 直接访问
  • 私有文件需要权限验证才能访问

(5)标签管理

  • 支持题目标签的增删改查操作
  • 支持标签分类管理
  • 支持设置标签颜色
  • 支持标签树形展示(父子关系)
  • 题目可关联多个标签

(6)语言管理

  • 支持编程语言的增删改查操作
  • 语言信息包括:名称、版本、Docker 镜像、文件名、文件扩展名、编译命令、运行命令、代码模板等
  • 支持设置语言状态:启用、禁用
  • 支持设置语言类型:编译型、解释型
  • 支持为不同 OJ 平台配置不同的语言支持(扩展 VJudge 功能)

2.2 非功能性需求

非功能性需求描述了系统在性能、安全性、可靠性、可维护性等方面应当达到的指标和标准。

2.2.1 性能需求

(1)响应时间

  • 页面加载时间:首屏加载时间 ≤ 2 秒
  • API 接口响应时间:≤ 500 毫秒(正常情况下)
  • 判题处理时间:根据题目限制和测试用例数量,一般在 1-10 秒内完成
  • 数据库查询响应时间:≤ 200 毫秒

(2)并发处理能力

  • 系统支持至少 500 个并发用户同时在线
  • 判题系统支持至少 50 个并发判题任务
  • 比赛高峰期(如开始和结束前后)支持至少 1000 个并发请求
  • 通过 RabbitMQ 消息队列实现异步判题,理论上支持无限制任务排队

(3)吞吐量

  • 系统每秒可处理至少 100 个 API 请求(QPS)
  • 判题系统每小时可完成至少 1000 次判题
  • 数据库每秒可处理至少 500 次查询

(4)系统资源占用

  • 单个判题容器内存限制可配置,默认 512MB
  • 单个判题容器 CPU 使用率限制可配置
  • 判题容器数量可根据服务器资源动态调整
2.2.2 安全性需求

(1)身份认证与授权

  • 所有敏感操作需要用户登录认证
  • 采用 JWT Token 机制进行身份认证
  • Token 有效期可配置,默认 24 小时
  • Token 存储在 Redis 中,支持主动失效
  • 密码采用 BCrypt 加密算法加密存储,不可逆
  • 登录失败次数限制:连续失败 5 次后锁定账号 15 分钟

(2)权限控制

  • 基于 RBAC 模型实现细粒度权限控制
  • 后端接口通过 Spring Security 进行权限验证
  • 前端根据权限动态渲染菜单和按钮
  • 私有题目和比赛题目需要权限验证才能访问
  • 比赛题目仅在比赛期间对参赛用户可见

(3)代码沙箱安全

  • 每个代码提交运行在独立的 Docker 容器中
  • 容器严格资源限制:CPU、内存、磁盘、时间
  • 容器网络隔离:禁用所有网络访问
  • 容器文件系统保护:采用只读根文件系统
  • 容器进程数限制:防止 fork 炸弹攻击
  • 容器超时自动销毁:防止无限循环和资源泄漏
  • 禁止访问敏感系统调用

(4)数据安全

  • 数据库连接采用连接池,防止连接泄漏
  • 用户密码不可逆加密存储
  • 敏感信息(如比赛密码)加密传输
  • 定期备份数据库数据
  • 防止 SQL 注入:使用 MyBatis 参数化查询
  • 防止 XSS 攻击:前端对用户输入进行转义
  • 防止 CSRF 攻击:接口使用 Token 验证

(5)文件安全

  • 上传文件类型限制:仅允许特定类型文件上传
  • 上传文件大小限制:默认单个文件不超过 100MB
  • 文件存储路径隐藏:通过文件 ID 访问,不暴露真实路径
  • 测试用例文件权限保护:仅管理员和出题人可访问
2.2.3 可靠性需求

(1)系统可用性

  • 系统可用性目标:≥ 99.5%(全年停机时间 ≤ 43.8 小时)
  • 支持 7×24 小时不间断运行
  • 关键服务(判题、数据库、消息队列)支持故障自动恢复

(2)数据可靠性

  • 数据库数据定期备份,备份周期:每天一次全量备份
  • 消息队列持久化:判题任务消息持久化到磁盘,防止丢失
  • 判题记录和提交代码永久保存
  • 支持数据恢复机制

(3)容错性

  • 判题节点故障不影响整体系统运行
  • 判题任务失败支持重试机制
  • 数据库连接失败支持重连机制
  • Redis 连接失败不影响核心功能(降级处理)

(4)异常处理

  • 完善的异常处理机制:所有异常统一捕获和处理
  • 友好的错误提示:向用户展示清晰的错误信息
  • 异常日志记录:所有异常记录到日志文件,便于排查问题
2.2.4 可扩展性需求

(1)系统扩展性

  • 采用前后端分离架构,前后端可独立扩展
  • 采用模块化设计,支持功能模块独立部署
  • 支持水平扩展:可添加多个判题节点
  • 支持微服务化改造:模块间低耦合,易于拆分为微服务

(2)功能扩展性

  • 支持扩展更多编程语言:通过实现 SandBox 接口添加新语言支持
  • 支持扩展新的判题模式:如 Special Judge、交互题等
  • 支持扩展 VJudge 功能:对接其他 OJ 平台(如洛谷、Codeforces 等)
  • 支持扩展更多存储方式:如 OSS、MinIO 等

(3)数据扩展性

  • 数据库支持分库分表:当数据量增长时可进行分表
  • 支持缓存扩展:可添加更多 Redis 节点实现集群
  • 支持消息队列集群:RabbitMQ 支持集群部署
2.2.5 易用性需求

(1)用户界面

  • 界面简洁美观,符合现代化设计规范
  • 响应式设计:适配不同尺寸设备(PC、平板、手机)
  • 交互流畅:使用 Vue 3 实现流畅的页面切换和数据加载
  • 操作简单:核心功能不超过 3 步即可完成

(2)代码编辑

  • 集成 CodeMirror 代码编辑器
  • 支持语法高亮和代码折叠
  • 支持代码自动缩进和括号匹配
  • 支持代码字体大小调整
  • 支持代码主题切换

(3)信息反馈

  • 操作成功/失败提示:使用 Toast 消息提示
  • 加载状态提示:使用 Loading 动画提示数据加载中
  • 判题状态实时反馈:实时显示判题进度(编译中、判题中)
  • 错误信息友好展示:编译错误、运行时错误等信息清晰展示

(4)帮助文档

  • 提供系统使用说明文档
  • 提供常见问题解答(FAQ)
  • 提供判题状态说明
  • 提供编程语言版本说明
2.2.6 可维护性需求

(1)代码质量

  • 代码规范:遵循 Java 和 TypeScript 编码规范
  • 代码注释:关键方法和复杂逻辑添加注释
  • 代码结构清晰:采用分层架构,职责分明
  • 避免代码重复:提取公共方法和工具类

(2)日志记录

  • 完善的日志记录机制:使用 Slf4j + Logback
  • 日志分级:DEBUG、INFO、WARN、ERROR
  • 关键操作记录日志:用户登录、代码提交、判题执行、异常信息等
  • 日志文件按日期归档,定期清理

(3)监控与运维

  • 支持系统监控:服务器资源使用情况、在线用户数、判题队列长度等
  • 支持数据统计:题目统计、用户统计、提交统计等
  • 支持日志查看:管理员可查看系统日志

(4)部署与配置

  • 支持 Docker 容器化部署
  • 支持配置文件外部化:数据库、Redis、RabbitMQ 等配置可通过配置文件修改
  • 支持多环境配置:开发环境、测试环境、生产环境

2.3 用例图与用例分析

用例图是描述系统功能和用户交互的重要工具。本节通过用例图清晰地描述 YMCloud 系统的主要用例和参与者。

2.3.1 系统参与者

系统包含以下主要参与者(Actor):

  1. 普通用户(User):编程学习者,系统的主要使用者
  2. 管理员(Admin):系统管理员,负责系统维护和内容管理
  3. 出题人(Problem Setter):题目创建者,可能是教师或管理员
  4. 竞赛组织者(Contest Organizer):比赛创建者和管理者
  5. 判题系统(Judge System):后台判题服务,作为外部系统参与者
2.3.2 主要用例

(1)普通用户用例

普通用户可以执行以下用例:

  • 用户认证相关

    • 注册账号:填写用户名、邮箱、密码,接收验证码完成注册
    • 用户登录:输入用户名和密码登录系统
    • 找回密码:通过邮箱验证码重置密码
    • 退出登录:清除登录状态
  • 个人中心相关

    • 查看个人主页:查看个人信息、解题统计、提交热力图
    • 修改个人信息:修改昵称、头像、学校、签名等
    • 修改密码:验证原密码后修改新密码
  • 题目相关

    • 浏览题目列表:分页查看所有公开题目
    • 搜索题目:按关键字、难度、标签搜索题目
    • 查看题目详情:查看题目描述、样例、限制等信息
    • 提交代码:在代码编辑器中编写代码并提交
    • 查看提交记录:查看自己的提交历史
    • 查看他人 AC 代码:学习他人的解题思路
  • 比赛相关

    • 浏览比赛列表:查看正在进行和即将开始的比赛
    • 查看比赛详情:查看比赛说明、时间、赛制等信息
    • 报名参赛:注册参加比赛(公开比赛直接报名,私有比赛需输入密码)
    • 参加比赛:在比赛期间查看题目、提交代码
    • 查看排名榜:实时查看比赛排名
  • 数据统计相关

    • 查看用户排行榜:查看全站用户排行
    • 查看题目通过率:查看题目统计信息

(2)管理员用例

管理员除了拥有普通用户的所有权限外,还可以执行以下用例:

  • 用户管理

    • 查询用户列表:分页查看所有用户
    • 新增用户:手动创建用户账号
    • 编辑用户:修改用户信息
    • 删除用户:批量删除用户
    • 禁用/启用用户:修改用户状态
    • 重置用户密码:为用户重置密码
    • 分配角色:为用户分配管理员或其他角色
  • 题目管理

    • 创建题目:新建题目并设置题目信息
    • 编辑题目:修改题目内容和配置
    • 删除题目:批量删除题目
    • 管理测试用例:上传、下载、删除测试用例文件
    • 设置题目可见性:公开、私有、比赛专用
    • 管理题目标签:为题目添加或删除标签
  • 比赛管理

    • 创建比赛:新建比赛并设置比赛信息
    • 编辑比赛:修改比赛配置
    • 删除比赛:删除已创建的比赛
    • 克隆比赛:基于现有比赛创建新比赛
    • 管理比赛题目:添加、移除、编辑比赛题目
    • 查看参赛用户:查看已报名用户列表
  • 系统管理

    • 角色管理:创建、编辑、删除角色
    • 权限管理:为角色分配权限
    • 公告管理:发布、编辑、删除系统公告
    • 标签管理:管理题目标签和分类
    • 语言管理:配置支持的编程语言
    • 文件管理:管理系统上传的文件
    • 系统监控:查看系统运行状态和统计数据

(3)判题系统用例

判题系统作为后台服务,自动执行以下用例:

  • 监听判题队列:从 RabbitMQ 获取判题任务
  • 创建判题容器:为每个判题任务创建 Docker 容器
  • 编译代码:对编译型语言进行编译
  • 执行测试用例:逐个运行测试用例并比对结果
  • 收集执行结果:记录时间、内存使用情况
  • 更新判题记录:将判题结果写入数据库
  • 销毁判题容器:判题完成后清理容器资源
2.3.3 用例图(文字描述)

由于 Markdown 格式限制,此处以文字形式描述用例图结构。在正式文档中,建议使用 UML 工具(如 PlantUML、draw.io)绘制标准用例图。

系统边界:YMCloud 在线判题系统参与者:
- 普通用户(User)
- 管理员(Admin)继承自普通用户
- 判题系统(Judge System)普通用户用例:- 账号管理- 注册账号- 用户登录- 找回密码- 修改个人信息- 题目练习- 浏览题目- 查看题目详情- 提交代码 [extends: 查看题目详情]- 查看提交记录- 比赛参与- 浏览比赛- 报名比赛- 参加比赛- 查看排名榜- 查看统计- 个人主页- 用户排行榜管理员用例(继承普通用户用例并新增):- 用户管理- 查询用户- 新增用户- 编辑用户- 删除用户- 重置密码- 题目管理- 创建题目- 编辑题目- 删除题目- 管理测试用例- 比赛管理- 创建比赛- 编辑比赛- 删除比赛- 管理比赛题目- 系统管理- 权限管理- 公告管理- 系统监控判题系统用例:- 执行判题 [triggered by: 提交代码]- 创建容器- 编译代码- 运行测试- 更新结果
2.3.4 核心用例详细描述

以下选取几个核心用例进行详细描述:

用例1:提交代码并判题

  • 用例名称:提交代码并判题
  • 参与者:普通用户、判题系统
  • 前置条件:用户已登录,正在查看题目详情页面
  • 基本流程
    1. 用户在代码编辑器中编写代码
    2. 用户选择编程语言
    3. 用户点击"提交"按钮
    4. 系统验证用户登录状态和题目权限
    5. 系统创建判题记录,状态为"等待判题"
    6. 系统将判题任务发送到 RabbitMQ 队列
    7. 系统返回提交成功信息和提交 ID
    8. 判题系统从队列获取任务
    9. 判题系统创建 Docker 容器
    10. 判题系统编译代码(编译型语言)
    11. 判题系统运行测试用例
    12. 判题系统更新判题结果
    13. 判题系统销毁容器
    14. 用户查询判题结果
  • 后置条件:系统记录判题结果,用户可查看详细信息
  • 异常流程
    • 用户未登录:跳转到登录页面
    • 题目权限不足:提示权限错误
    • 代码为空:提示代码不能为空
    • 编译错误:记录编译错误信息,状态为 CE
    • 运行超时:终止执行,状态为 TLE
    • 内存超限:终止执行,状态为 MLE
    • 系统错误:记录错误日志,状态为 SE

用例2:创建比赛并添加题目

  • 用例名称:创建比赛并添加题目
  • 参与者:管理员/竞赛组织者
  • 前置条件:用户已登录且拥有比赛管理权限
  • 基本流程
    1. 管理员进入比赛管理页面
    2. 管理员点击"创建比赛"按钮
    3. 系统显示比赛创建表单
    4. 管理员填写比赛信息:标题、描述、时间、赛制、类型、权限等
    5. 管理员点击"保存"按钮
    6. 系统验证比赛信息(时间、密码等)
    7. 系统创建比赛记录
    8. 系统返回比赛 ID 并跳转到比赛题目管理页面
    9. 管理员点击"添加题目"按钮
    10. 系统显示可添加的题目列表
    11. 管理员选择要添加的题目
    12. 管理员设置题目展示信息(序号、标题、颜色)
    13. 管理员点击"确认添加"
    14. 系统将题目添加到比赛
    15. 系统将题目可见性设置为"比赛专用"
  • 后置条件:比赛创建成功,题目已关联到比赛
  • 异常流程
    • 权限不足:提示权限错误
    • 比赛时间无效:提示时间设置错误
    • 私有比赛未设置密码:提示必须设置密码
    • 题目已在比赛中:提示题目已存在

小结:本章从功能性需求和非功能性需求两个维度,详细分析了 YMCloud 在线判题系统的需求。功能性需求涵盖了用户管理、题目管理、判题管理、比赛管理和系统管理五大模块;非功能性需求从性能、安全性、可靠性、可扩展性、易用性和可维护性六个方面提出了明确的指标和标准。通过用例图和用例描述,清晰地展示了系统的主要功能和用户交互流程,为后续的系统设计和实现奠定了坚实的基础。



第三章 技术架构设计

技术架构是系统实现的基础和骨架,直接影响系统的性能、可扩展性和可维护性。本章详细介绍 YMCloud 系统的技术架构设计,包括总体架构、技术栈选型、模块划分和架构设计原则,为系统实现提供清晰的技术路线图。

3.1 系统总体架构

YMCloud 采用 前后端分离的现代化架构,将前端展示层和后端业务层完全解耦,通过 RESTful API 进行数据交互。后端采用 分层架构 + 模块化设计,前端采用 组件化开发模式,判题系统通过 消息队列实现异步化,整体架构具有高内聚、低耦合、易扩展的特点。

3.1.1 系统架构层次

系统从逻辑上可以分为以下五个层次:

1. 表现层(Presentation Layer)

  • 技术实现:Vue 3 + TypeScript + Element Plus + Naive UI
  • 主要职责
    • 用户界面展示和交互
    • 路由管理和页面跳转
    • 状态管理(Pinia)
    • API 请求封装
    • 前端权限控制(菜单和按钮显示)
  • 通信方式:通过 Axios 发送 HTTP 请求与后端 API 层交互

2. API 接口层(API Layer)

  • 技术实现:Spring Boot + Spring MVC
  • 主要职责
    • 接收前端 HTTP 请求
    • 请求参数验证
    • 身份认证与权限验证(Spring Security + JWT)
    • 调用业务服务层
    • 统一响应格式封装
    • 异常统一处理
  • 实现位置ymcloud-web 模块的 controller

3. 业务服务层(Business Layer)

  • 技术实现:Spring Boot + Spring AOP
  • 主要职责
    • 实现核心业务逻辑
    • 业务流程编排
    • 事务管理
    • 业务规则验证
    • 缓存管理(Redis)
    • 消息队列发送(RabbitMQ)
  • 实现位置ymcloud-web 模块的 service

4. 数据访问层(Data Access Layer)

  • 技术实现:MyBatis-Plus
  • 主要职责
    • 数据库 CRUD 操作
    • 复杂 SQL 查询
    • 数据库事务控制
    • 分页查询
  • 实现位置ymcloud-web 模块的 mapper 包 和 ymcloud-core 模块的 dao

5. 数据存储层(Data Storage Layer)

  • 技术实现:MySQL + Redis + 文件系统
  • 主要职责
    • 持久化存储业务数据(MySQL)
    • 缓存热点数据和 Session(Redis)
    • 存储文件资源(文件系统)
3.1.2 系统架构图

系统架构采用经典的三层架构,并引入消息队列实现异步化处理:

┌────────────────────────────────────────────────────────────────┐
│                        前端层(Vue 3)                          │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐  ┌──────────┐      │
│  │  用户端  	│  │  管理端  	│  │  比赛端  	│  │ 代码编辑  │      │
│  └────┬─────┘  └────┬─────┘  └────┬─────┘  └────┬─────┘      │
│       │             │             │             │              │
│       └─────────────┴─────────────┴─────────────┘              │
│                         │                                       │
│                    HTTP/JSON                                    │
└─────────────────────────┼─────────────────────────────────────┘│▼
┌────────────────────────────────────────────────────────────────┐
│              后端层(Spring Boot 3)                            │
│  ┌──────────────────────────────────────────────────────────┐ │
│  │              API 接口层(Controller)                     │ │
│  │  ┌──────┐  ┌──────┐  ┌──────┐  ┌──────┐  ┌──────┐     │ │
│  │  │用户API│  │题目API│  │判题API│  │比赛API│  │系统API│     │ │
│  │  └──┬───┘  └──┬───┘  └──┬───┘  └──┬───┘  └──┬───┘     │ │
│  └─────┼─────────┼─────────┼─────────┼─────────┼───────────┘ │
│        │         │         │         │         │               │
│  ┌─────▼─────────▼─────────▼─────────▼─────────▼───────────┐ │
│  │     Spring Security + JWT 认证与权限控制                 │ │
│  └────────────────────────┬──────────────────────────────────┘ │
│                           │                                     │
│  ┌────────────────────────▼──────────────────────────────────┐ │
│  │              业务服务层(Service)                         │ │
│  │  ┌──────┐  ┌──────┐  ┌──────┐  ┌──────┐  ┌──────┐     │ │
│  │  │用户服务│  │题目服务│  │判题服务│  │比赛服务│  │系统服务│     │ │
│  │  └──┬───┘  └──┬───┘  └──┬───┘  └──┬───┘  └──┬───┘     │ │
│  └─────┼─────────┼─────────┼─────────┼─────────┼───────────┘ │
│        │         │         │         │         │               │
│        │         │         │         │         │               │
│  ┌─────▼─────────▼─────────┴─────────▼─────────▼───────────┐ │
│  │              数据访问层(Mapper/DAO)                     │ │
│  │  ┌──────────────────────────────────────────────────┐   │ │
│  │  │           MyBatis-Plus(ORM框架)                │   │ │
│  │  └───────────────────┬──────────────────────────────┘   │ │
│  └────────────────────────────────────────────────────────────┘ │
│                         │                                       │
│                         │                                       │
│            ┌────────────┼────────────┐                         │
│            │            │            │                         │
│      Redis Cache  判题队列发送   MySQL 查询                    │
│            │            │            │                         │
└────────────┼────────────┼────────────┼─────────────────────────┘│            │            │▼            ▼            ▼
┌────────────────────────────────────────────────────────────────┐
│                     基础设施层                                  │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐  ┌──────────┐     │
│  │  MySQL   │  │  Redis   │  │ RabbitMQ │  │  Docker  │     │
│  │ 数据库    │  │  缓存    │  │ 消息队列  │  │  容器    │     │
│  └──────────┘  └──────────┘  └────┬─────┘  └──────────┘     │
└──────────────────────────────────┼──────────────────────────────┘│▼┌────────────────────────┐│  异步判题系统(Judge)    ││  ┌──────────────────┐  ││  │  消息监听器        │  ││  └────────┬─────────┘  ││           │            ││  ┌────────▼─────────┐  ││  │ Docker 容器管理   │  ││  └────────┬─────────┘ ││           │           ││  ┌────────▼─────────┐ ││  │  代码沙箱执行    │ ││  └────────┬─────────┘ ││           │           ││  ┌────────▼─────────┐ ││  │  结果写回数据库  │ ││  └──────────────────┘ │└────────────────────────┘
3.1.3 异步判题架构

判题系统是 YMCloud 的核心技术亮点,采用 消息队列 + Docker 容器 的异步化架构:

判题流程:

  1. 代码提交阶段(Web 层)

    • 用户在前端提交代码
    • API 接口层接收请求,验证用户权限
    • 业务服务层创建判题记录(状态:等待判题)
    • 将判题任务封装为消息,发送到 RabbitMQ 队列
    • 立即返回提交成功响应(异步)
  2. 任务调度阶段(RabbitMQ)

    • 判题任务消息持久化到队列
    • 支持多个判题节点监听队列(负载均衡)
    • 消息重试机制:失败后自动重试(最多 3 次)
    • 超过重试次数后发送到错误队列,人工处理
  3. 判题执行阶段(Judge 模块)

    • 判题服务监听队列,获取判题任务
    • 更新判题状态为"编译中"
    • 创建 Docker 容器(独立隔离环境)
    • 保存用户代码到容器工作目录
    • 执行编译(编译型语言)
    • 更新判题状态为"判题中"
    • 逐个运行测试用例:
      • 将测试输入传入程序
      • 限制执行时间和内存
      • 捕获程序输出
      • 比对输出与期望结果
      • 记录时间和内存使用情况
    • 收集所有测试用例结果
    • 计算最终判题状态和得分
  4. 结果更新阶段

    • 将判题结果写回数据库
    • 更新判题记录状态
    • 销毁 Docker 容器,释放资源
    • 用户查询判题结果

架构优势:

  • 高并发:消息队列天然支持高并发,可同时处理大量判题任务
  • 负载均衡:多个判题节点共同消费队列,自动实现负载均衡
  • 削峰填谷:高峰期任务缓存在队列中,平滑处理
  • 容错性:判题节点故障不影响消息持久化
  • 可扩展:可轻松添加判题节点,实现水平扩展
3.1.4 前后端分离架构

前后端职责划分:

层次技术栈主要职责部署方式
前端Vue 3 + TypeScript• UI 渲染
• 用户交互
• 路由管理
• 状态管理
• 前端权限控制
独立部署
Nginx 静态服务器
后端Spring Boot 3• 业务逻辑
• 数据处理
• 权限验证
• API 提供
• 数据持久化
独立部署
JAR 包运行

通信机制:

  • 协议:HTTP/HTTPS
  • 数据格式:JSON
  • 认证方式:JWT Token(请求头 Authorization
  • 跨域处理:Spring Boot 配置 CORS 允许跨域请求
  • 代理配置:开发环境下 Vite 配置代理,将 /api 请求代理到后端

前端代理配置(Vite):

server: {proxy: {'/api': {target: 'http://localhost:9091',  // 后端地址changeOrigin: true,rewrite: (path) => path.replace(/^\/api/, '')}}
}

3.2 技术栈选型

技术栈的选择直接影响系统的开发效率、运行性能和维护成本。YMCloud 项目基于现代化的技术栈,充分考虑了技术的成熟度、社区活跃度和团队掌握程度。

3.2.1 后端技术栈
技术版本用途选型理由
Spring Boot3.5.3应用框架• 简化 Spring 开发
• 约定优于配置
• 丰富的生态
• 内嵌 Tomcat 服务器
Spring Security6.5.1安全框架• 强大的认证与授权
• 与 Spring 无缝集成
• 支持多种认证方式
• 灵活的权限控制
MyBatis-Plus3.5.10.1ORM 框架• 简化 MyBatis 开发
• 内置 CRUD 操作
• 自动分页
• 强大的条件构造器
MySQL8.0.39关系型数据库• 开源免费
• 性能优秀
• 生态成熟
• 支持事务和外键
Redis6.0+缓存数据库• 高性能内存数据库
• 支持多种数据结构
• 持久化机制
• 分布式缓存
RabbitMQ3.8+消息队列• 可靠的消息传递
• 支持多种消息模式
• 高可用集群
• 易于管理
Docker20.0+容器化技术• 轻量级虚拟化
• 快速部署
• 资源隔离
• 镜像复用
Docker Java API3.4.0Docker 客户端• Java 调用 Docker API
• 容器管理
• 镜像操作
JWT0.9.1身份认证• 无状态认证
• 跨域友好
• 支持分布式
• 轻量级
Hutool5.8.38工具类库• 丰富的工具方法
• 简化开发
• 中文文档友好
Fastjson22.0.57JSON 解析• 高性能
• 功能强大
• 国产开源
Knife4j4.4.0API 文档• 基于 Swagger
• 美观的 UI
• 在线调试
Druid1.2.23数据库连接池• 高性能
• 监控功能
• SQL 防火墙
Lombok1.18.38代码简化• 减少样板代码
• 提高开发效率
JavaMail1.6.2邮件发送• 发送验证码
• 密码找回

Spring Boot 版本选择:

  • 使用最新稳定版 3.5.3
  • 要求 JDK 17+
  • 支持 Jakarta EE 规范
  • 性能和安全性优化

数据库选择:

  • MySQL 8.0:主数据库,存储业务数据
  • Redis 6.0+:缓存热点数据、用户 Session、Token
  • 选择 Redis 而非 Memcached 的原因:
    • 支持更多数据结构(字符串、哈希、列表、集合、有序集合)
    • 支持持久化
    • 支持主从复制和集群

消息队列选择:

  • RabbitMQ 而非 Kafka 的原因:
    • 更适合中小规模应用
    • 消息可靠性高(持久化、确认机制)
    • 易于部署和管理
    • 支持多种消息模式
    • 判题场景不需要 Kafka 的超高吞吐量
3.2.2 前端技术栈
技术版本用途选型理由
Vue.js3.5.13前端框架• Composition API
• 响应式设计
• 虚拟 DOM
• 生态丰富
TypeScript5.8.0类型系统• 类型安全
• 代码提示
• 重构友好
• 适合大型项目
Vite6.2.4构建工具• 极速开发服务器
• 快速热更新
• 优化的生产构建
• 原生 ESM 支持
Pinia3.0.1状态管理• Vue 3 官方推荐
• TypeScript 友好
• 简洁的 API
• 模块化设计
Vue Router4.5.0路由管理• Vue 官方路由
• 支持动态路由
• 路由守卫
• 懒加载
Element Plus2.10.7UI 组件库• 基于 Vue 3
• 组件丰富
• 中文文档
• 企业级应用
Naive UI2.42.0UI 组件库• TypeScript 编写
• 主题定制
• 组件质量高
• 轻量级
CodeMirror5.65.19代码编辑器• 语法高亮
• 代码折叠
• 多语言支持
• 可扩展
ECharts5.6.0数据可视化• 图表丰富
• 性能优秀
• 交互性强
• 中文文档
Axios1.9.0HTTP 客户端• 支持 Promise
• 请求/响应拦截
• 自动转换 JSON
• 取消请求
Marked16.1.2Markdown 解析• 解析 Markdown
• 性能优秀
• 可扩展
KaTeX-数学公式渲染• 渲染数学公式
• 性能优于 MathJax
• 无需引入字体
DOMPurify3.2.6XSS 防护• 清理 HTML
• 防止 XSS 攻击
• 轻量级

Vue 3 选择理由:

  • Composition API:更灵活的代码组织方式
  • 性能提升:虚拟 DOM 重写,渲染性能提升 ~2倍
  • TypeScript 支持:更好的类型推导
  • Tree-shaking 优化:更小的打包体积
  • 多个根节点:组件可以有多个根节点

Vite 选择理由(相比 Webpack):

  • 开发服务器启动速度:快 10-100 倍
  • 热更新速度:秒级响应
  • 生产构建:基于 Rollup,体积更小
  • 原生 ESM:无需打包,按需加载

双 UI 框架策略:

  • Element Plus:管理后台主要使用,企业级风格
  • Naive UI:前台用户端主要使用,现代化风格
  • 按需引入,不影响打包体积
3.2.3 开发工具与环境
工具/环境版本用途
JDK17+Java 运行环境
Maven3.8+项目构建管理
Node.js18+前端运行环境
npm/pnpm-前端包管理器
IDEA2024+Java 开发 IDE
VSCode-前端开发编辑器
Git2.x版本控制
Docker Desktop20.0+容器运行环境
ApiFox-API 测试
Navicat-数据库管理
Another Redis Desktop Manager-Redis 管理

3.3 模块划分

YMCloud 后端采用 Maven 多模块管理,将系统划分为五大模块,每个模块职责单一,依赖关系清晰,便于独立开发、测试和维护。

3.3.1 后端模块结构
ymcloud (父模块)
├── ymcloud-common      (通用模块)
├── ymcloud-pojo        (数据模型模块)
├── ymcloud-core        (核心模块)
├── ymcloud-judge       (判题模块)
└── ymcloud-web         (Web接口模块)

模块依赖关系:

ymcloud-web└── 依赖 ymcloud-judgeymcloud-judge└── 依赖 ymcloud-coreymcloud-core├── 依赖 ymcloud-pojo└── 依赖 ymcloud-commonymcloud-pojo└── 无外部模块依赖ymcloud-common└── 无外部模块依赖
3.3.2 ymcloud-common(通用模块)

职责:提供通用的工具类、常量定义、自定义异常、通用结果封装等。

主要内容:

  1. 常量定义(constant 包)

    • CacheConstants:Redis 缓存键常量
    • Constants:通用常量(如验证码有效期)
    • ContestConstants:比赛相关常量(赛制、类型、可见性)
    • FileConstants:文件类型常量
    • HttpStatus:HTTP 状态码常量
    • JudgeConstants:判题状态常量(AC、WA、TLE、MLE 等)
    • LanguageConstants:编程语言常量
    • ProblemConstants:题目相关常量(难度、可见性)
    • RabbitMQConstants:消息队列交换机和路由键常量
    • UserConstants:用户相关常量(状态、密码错误次数)
  2. 自定义异常(exception 包)

    • 基础异常ServiceException(服务异常基类)
    • 用户异常UserExceptionUserNotExistsExceptionUserPasswordNotMatchExceptionCaptchaException
    • 题目异常ProblemExceptionProblemNotExistExceptionProblemPermissionException
    • 判题异常JudgeExceptionCompileExceptionTimeLimitExceededExceptionMemoryLimitExceededExceptionRuntimeErrorExceptionSandBoxException
    • 比赛异常ContestExceptionContestNotExistExceptionContestNotAccessException
    • 文件异常FileExceptionFileNotExistsExceptionFileTypeNotMatchException
    • 消息队列异常MqException
  3. 工具类(utils 包)

    • FileUtils:文件操作工具(上传、下载、删除、路径处理)
    • RedisCache:Redis 缓存操作封装
    • MessageQueueUtils:RabbitMQ 消息发送封装
    • MailUtils:邮件发送工具
    • ServletUtils:Servlet 工具(获取请求参数、响应)
    • IpUtils:IP 地址获取工具
    • HttpHelper:HTTP 请求工具
    • Arith:精确计算工具
    • ShortSnowflake:短ID生成器
  4. 统一响应结果(result 包)

    • Result<T>:统一 API 响应格式
      {"code": 200,"msg": "操作成功","data": { ... }
      }
      

设计特点:

  • 无业务逻辑:只包含通用工具和定义
  • 被所有模块依赖:作为基础模块
  • 高复用性:工具类可在任何地方使用
3.3.3 ymcloud-pojo(数据模型模块)

职责:定义系统中所有的数据模型,包括实体类、DTO、VO、查询对象等。

主要内容:

  1. 实体类(entity 包)

    • 对应数据库表的实体类
    • 使用 MyBatis-Plus 注解(@TableName、@TableId、@TableField)
    • 主要实体:
      • User:用户实体
      • Problem:题目实体
      • Judge:判题记录实体
      • JudgeCase:判题用例结果实体
      • Contest:比赛实体
      • ContestProblem:比赛题目关联实体
      • Language:编程语言实体
      • RoleAuth:角色和权限实体
      • TagProblemTag:标签和题目标签关联实体
      • Announcement:公告实体
      • File:文件信息实体
  2. 数据传输对象(dto 包)

    • 用于接收前端请求参数
    • 包含参数校验注解(@NotNull、@NotBlank、@Size 等)
    • 主要 DTO:
      • JudgeDTO:判题请求 DTO
      • ProblemDTO:题目提交 DTO
      • ContestDTO:比赛提交 DTO
      • UserDTO:用户信息 DTO
      • 各种操作的请求 DTO
  3. 视图对象(vo 包)

    • 用于返回前端展示数据
    • 可能包含关联查询的数据
    • 主要 VO:
      • ProblemVO:题目详情 VO(包含标签、作者等)
      • UserProfileVO:用户资料 VO
      • ContestVO:比赛详情 VO
      • SubmissionVO:提交记录 VO
      • PageVO<T>:分页结果 VO
  4. 查询对象(query 包)

    • 封装查询条件和分页参数
    • 主要 Query:
      • ProblemPageQuery:题目分页查询
      • ContestPageQuery:比赛分页查询
      • AdminUserPageQuery:用户管理分页查询
  5. 业务对象(bo 包)

    • 业务逻辑中使用的临时对象
    • 主要 BO:
      • JudgeTask:判题任务对象
      • JudgeResult:判题结果对象
      • TestCase:测试用例对象
      • CodeRunResult:代码运行结果

设计特点:

  • 数据模型集中管理:所有数据结构定义在一个模块
  • 职责单一:只包含数据定义,不包含业务逻辑
  • 类型安全:使用 TypeScript 和 Java 泛型确保类型安全
3.3.4 ymcloud-core(核心模块)

职责:提供系统核心配置、安全认证、通用拦截器、数据访问基础服务等。

主要内容:

  1. 配置类(config 包)

    • SecurityConfig:Spring Security 安全配置
      • 配置 JWT 过滤器
      • 配置认证和授权规则
      • 配置异常处理器
      • 禁用 CSRF,启用无状态 Session
    • RedisConfig:Redis 配置
      • 配置 RedisTemplate
      • 配置序列化器(FastJson2JsonRedisSerializer)
      • 配置缓存管理器(RedisCacheManager)
    • RabbitMQConfig:RabbitMQ 配置
      • 配置消息转换器(Jackson2JsonMessageConverter)
      • 消息自动生成唯一 ID
    • RabbitMQErrorConfig:消息队列错误处理配置
      • 创建错误交换机和队列
      • 超过重试次数后将消息发送到错误队列
    • MybatisConfig:MyBatis-Plus 配置
      • 配置分页插件(PaginationInnerInterceptor)
      • 配置乐观锁插件
    • FilterConfig:过滤器配置
      • 注册 RepeatableFilter(可重复读取请求体)
    • JacksonConfig:Jackson JSON 配置
      • 配置日期格式化
      • 配置时区
  2. 安全认证(security 包)

    • JWT 过滤器
      • JwtAuthenticationTokenFilter:Token 认证过滤器
        • 从请求头获取 Token
        • 验证 Token 有效性
        • 将用户信息写入 SecurityContext
    • 用户详情服务
      • UserDetailsServiceImpl:实现 Spring Security 的 UserDetailsService
        • 根据用户名加载用户信息
        • 加载用户角色和权限
        • 密码验证和试错次数控制
    • Token 服务
      • TokenService:Token 生成、验证、刷新
        • 生成 JWT Token
        • 验证 Token 有效期
        • Token 刷新(延长有效期)
        • Token 存储到 Redis
    • 密码服务
      • PasswordService:密码验证和错误次数控制
        • 记录密码错误次数(Redis)
        • 达到最大错误次数后锁定账号
    • 异常处理器
      • AuthenticationEntryPointImpl:认证失败处理(401)
      • CustomAccessDeniedHandler:权限不足处理(403)
      • LogoutSuccessHandlerImpl:登出成功处理
    • 认证上下文
      • AuthenticationContextHolder:ThreadLocal 存储认证信息
    • 安全工具类
      • SecurityUtils:获取当前用户、加密密码等
  3. 数据访问基础服务(dao 包)

    • 基于 MyBatis-Plus 的 IService 接口
    • 提供基础 CRUD 操作
    • 所有实体类对应一个 EntityService
    • 例如:
      • UserEntityService extends IService<User>
      • ProblemEntityService extends IService<Problem>
      • JudgeEntityService extends IService<Judge>
  4. Mapper 接口(mapper 包)

    • MyBatis Mapper 接口
    • 定义复杂 SQL 查询方法
    • 对应 XML 文件在 resources/mapper 目录
  5. 拦截器(interceptor 包)

    • RepeatSubmitInterceptor:防重复提交拦截器
    • SameUrlDataInterceptor:同一 URL 数据缓存拦截器
  6. 自定义注解(annotation 包)

    • @RepeatSubmit:防重复提交注解

设计特点:

  • 核心配置集中:所有 Spring 配置类集中管理
  • 安全认证完整:完整的 JWT + Spring Security 实现
  • 基础服务封装:封装 MyBatis-Plus 基础服务
3.3.5 ymcloud-judge(判题模块)

职责:负责代码判题的核心逻辑,包括 Docker 容器管理、代码沙箱、判题策略等。

主要内容:

  1. Docker 配置(config 包)

    • DockerConfig:Docker 客户端配置
      • 创建 DockerClient Bean
      • 配置连接超时和响应超时
      • 最大连接数:100
  2. Docker 容器管理(docker 包)

    • 容器抽象类
      • DockerContainer:容器基类
        • 容器启动、停止、重启、暂停、恢复、销毁
        • 执行命令(execCmd)
        • 查看容器信息(inspectContainer)
        • 检查容器状态(isRunning、isOOMKilled)
    • 代码执行容器
      • CodeExecContainer extends DockerContainer
        • 继承容器基类
        • 添加工作目录属性
    • 容器工厂
      • DockerContainerFactory<T>:容器工厂接口
      • CodeExecContainerFactory:代码执行容器工厂
        • 创建容器配置:
          • 内存限制:可配置(默认 512MB)
          • 禁用 Swap
          • CPU 限制:单核
          • 网络隔离:禁用网络(network=none
          • 文件系统保护:只读根文件系统(readonlyRootfs=true
          • 进程数限制:64 个(防 fork 炸弹)
          • 挂载工作目录:宿主机目录挂载到容器
  3. 代码沙箱(sandbox 包)

    • 沙箱抽象类
      • SandBox:沙箱基类
        • compile():编译代码
        • execute():执行代码
        • buildCompileCommand():构建编译命令(抽象方法)
        • buildRunCommand():构建运行命令(抽象方法)
        • saveCode():保存代码到容器
        • 超时控制:使用 timeout 命令
        • 时间和内存获取:使用 /usr/bin/time 命令
    • 语言沙箱实现
      • JavaSandBox:Java 沙箱
        • 编译命令:javac Main.java
        • 运行命令:java Main
      • CppSandBox:C++ 沙箱
        • 编译命令:g++ main.cpp -o main -O2 -std=c++17
        • 运行命令:./main
      • CSandBox:C 沙箱
        • 编译命令:gcc main.c -o main -O2 -std=c11
        • 运行命令:./main
      • PythonSandBox:Python 沙箱
        • 无需编译
        • 运行命令:python3 main.py
    • 沙箱工厂
      • SandBoxFactory:根据语言名称创建对应沙箱
  4. 判题策略(strategy 包)

    • JudgeRun:判题逻辑处理
      • execute():执行判题
        • 获取测试用例
        • 通知开始编译
        • 创建 Docker 容器
        • 启动容器
        • 选择沙箱策略
        • 保存代码
        • 编译代码
        • 通知开始评测
        • 逐个运行测试用例
        • 比对输出结果
        • 收集时间和内存使用情况
        • 计算最终结果
        • 销毁容器
        • 发送结果消息
  5. 消息监听器(listeners 包)

    • JudgeListener:判题队列监听器
      • 监听 RabbitMQ 判题队列
      • 获取判题任务
      • 调用 JudgeRun 执行判题
      • 更新判题结果
  6. 判题服务(service 包)

    • JudgeService:判题服务接口
    • JudgeServiceImpl:判题服务实现
      • 提交评测(submitJudge)
      • 比赛评测(contestJudge)
      • 测试评测(testJudge)
      • 重新评测(rejudge)
  7. 业务对象(bo 包)

    • JudgeTask:判题任务对象
    • JudgeResult:判题结果对象
    • TestCase:测试用例对象
    • TestCaseResult:测试用例结果对象
    • CodeRunResult:代码运行结果对象
    • CmdExecResult:命令执行结果对象

设计特点:

  • 独立判题模块:可独立部署为判题服务
  • 容器化隔离:每个判题任务独立容器
  • 策略模式:不同语言不同沙箱策略
  • 工厂模式:容器工厂和沙箱工厂
  • 异步消息:基于 RabbitMQ 的异步判题
3.3.6 ymcloud-web(Web 接口模块)

职责:暴露 RESTful API 接口,实现业务逻辑,连接前端和后端服务。

主要内容:

  1. 控制器层(controller 包)

    • OJ 端控制器(oj 包):
      • UserController:用户相关接口
      • ProblemController:题目相关接口
      • JudgeController:判题相关接口
      • ContestController:比赛相关接口
      • StatusController:提交记录接口
      • AnnouncementController:公告接口
    • 管理端控制器(admin 包):
      • AdminUserController:用户管理
      • AdminProblemController:题目管理
      • AdminContestController:比赛管理
      • AdminRoleController:角色管理
      • AdminAuthController:权限管理
      • AdminFileController:文件管理
      • AdminTagController:标签管理
      • AdminLanguageController:语言管理
      • AdminSystemController:系统监控
    • 公共控制器(common 包):
      • LoginController:登录认证
      • RegisterController:用户注册
      • CaptchaController:验证码
      • FileController:文件上传下载
  2. 业务服务层(service 包)

    • OJ 端服务(oj 包):
      • UserService:用户服务
      • ProblemService:题目服务
      • ContestService:比赛服务
      • StatusService:提交记录服务
    • 管理端服务(admin 包):
      • AdminUserService:用户管理服务
      • AdminProblemService:题目管理服务
      • AdminContestService:比赛管理服务
      • AdminRoleService:角色管理服务
      • 其他管理服务…
    • 实现类(impl 包):
      • 所有服务接口的实现类
  3. 配置类(config 包)

    • WebMvcConfig:Spring MVC 配置
      • CORS 跨域配置
      • 拦截器注册
    • Knife4jConfig:API 文档配置

设计特点:

  • RESTful API:遵循 RESTful 规范
  • 统一响应格式:Result 封装
  • 统一异常处理:GlobalExceptionHandler
  • 参数验证:使用 @Validated 注解
  • 权限控制:使用 @PreAuthorize 注解
3.3.7 前端模块结构
ymcloud-vue/
├── src/
│   ├── api/                # API 请求封装
│   ├── assets/             # 静态资源
│   ├── components/         # 公共组件
│   │   ├── admin/         # 管理端组件
│   │   ├── common/        # 通用组件
│   │   └── oj/            # OJ端组件
│   ├── constants/          # 常量定义
│   ├── router/             # 路由配置
│   │   ├── modules/       # 路由模块
│   │   └── index.ts       # 路由入口
│   ├── stores/             # 状态管理(Pinia)
│   │   ├── user.ts        # 用户状态
│   │   ├── permission.ts  # 权限状态
│   │   └── app.ts         # 应用状态
│   ├── utils/              # 工具函数
│   ├── views/              # 页面组件
│   │   ├── admin/         # 管理端页面
│   │   ├── oj/            # OJ端页面
│   │   └── error/         # 错误页面
│   ├── type/               # TypeScript类型定义
│   ├── App.vue             # 根组件
│   └── main.ts             # 入口文件
├── public/                 # 公共资源
├── dist/                   # 构建输出
├── index.html              # HTML 模板
├── package.json            # 依赖配置
├── vite.config.ts          # Vite 配置
└── tsconfig.json           # TypeScript 配置

模块说明:

  1. api 模块

    • 封装所有 API 请求
    • 统一错误处理
    • 请求/响应拦截器
  2. components 模块

    • 可复用的UI组件
    • 区分管理端和用户端组件
  3. router 模块

    • 路由配置和管理
    • 路由守卫
    • 动态路由加载
  4. stores 模块(Pinia)

    • user.ts:用户信息、登录状态、权限判断
    • permission.ts:动态路由加载
    • app.ts:应用全局状态
  5. utils 模块

    • storage.ts:LocalStorage 封装
    • request.ts:Axios 封装
    • utils.ts:通用工具函数

3.4 架构设计原则

YMCloud 的架构设计遵循了以下核心原则,确保系统的质量和可维护性。

3.4.1 高内聚低耦合

高内聚

  • 模块职责单一:每个模块只负责一类功能
    • ymcloud-common:通用工具和常量
    • ymcloud-pojo:数据模型
    • ymcloud-core:核心配置和安全
    • ymcloud-judge:判题逻辑
    • ymcloud-web:业务接口
  • 包职责明确:每个包的功能边界清晰
    • controller:接收请求,参数验证
    • service:业务逻辑实现
    • mapper:数据访问

低耦合

  • 依赖倒置:依赖抽象而非具体实现
    • 判题服务依赖 SandBox 接口,而非具体沙箱实现
    • 使用 Spring 的依赖注入(DI)
  • 接口隔离:每个模块暴露清晰的接口
    • 前后端通过 RESTful API 交互
    • 判题模块通过消息队列与 Web 模块交互
  • 最小依赖:模块间依赖最小化
    • ymcloud-common 不依赖任何其他模块
    • ymcloud-judge 不依赖 ymcloud-web
3.4.2 分层架构

表现层 → 控制层 → 服务层 → 数据层

好处

  • 职责分明:每层只关注自己的职责
  • 易于测试:可以对每层独立测试
  • 易于替换:可以替换某一层的实现,不影响其他层

示例:题目查询流程

1. 表现层(Vue):用户点击查询按钮
2. API 调用:发送 HTTP GET 请求 /api/problem/list
3. 控制层(Controller):ProblemController.queryList()- 接收请求参数- 参数验证
4. 服务层(Service):ProblemService.pageQuery()- 构建查询条件- 调用数据访问层- 业务逻辑处理
5. 数据层(Mapper):ProblemMapper.selectPage()- 执行 SQL 查询- 返回数据库结果
6. 逐层返回:Service → Controller → 前端
3.4.3 面向接口编程

原则:依赖抽象接口,而非具体实现类。

实践

  • Service 层定义接口
    public interface ProblemService {PageVO<ProblemVO> pageQuery(ProblemPageQuery query);
    }
    
  • 实现类实现接口
    @Service
    public class ProblemServiceImpl implements ProblemService {@Overridepublic PageVO<ProblemVO> pageQuery(ProblemPageQuery query) {// 实现逻辑}
    }
    
  • 依赖注入接口
    @RestController
    public class ProblemController {@Autowiredprivate ProblemService problemService;  // 依赖接口
    }
    

好处

  • 易于扩展:可以添加新的实现类
  • 易于测试:可以使用 Mock 对象
  • 解耦:调用者不依赖具体实现
3.4.4 设计模式应用

1. 工厂模式(Factory Pattern)

  • 容器工厂CodeExecContainerFactory
    • 根据配置创建不同的 Docker 容器
  • 沙箱工厂SandBoxFactory
    • 根据编程语言创建对应的沙箱实现

2. 策略模式(Strategy Pattern)

  • 判题策略:不同语言使用不同的沙箱策略
    • JavaSandBoxCppSandBoxPythonSandBox
    • 通过多态实现不同的编译和运行命令

3. 单例模式(Singleton Pattern)

  • Spring Bean:默认单例
    • DockerClient:全局唯一的 Docker 客户端
    • RedisTemplate:全局唯一的 Redis 模板

4. 模板方法模式(Template Method Pattern)

  • SandBox 抽象类
    • 定义判题流程模板(compile → execute)
    • 子类实现具体的编译和运行命令

5. 观察者模式(Observer Pattern)

  • RabbitMQ 消息监听
    • JudgeListener 监听判题队列
    • 有新消息时自动触发判题
3.4.5 安全性设计

1. 身份认证

  • JWT Token 认证机制
  • Token 存储在 Redis,支持主动失效
  • Token 有效期可配置

2. 权限控制

  • RBAC 权限模型
  • 方法级权限验证(@PreAuthorize)
  • 前后端双重验证

3. 数据安全

  • 密码 BCrypt 加密
  • SQL 参数化查询(防 SQL 注入)
  • XSS 防护(DOMPurify)
  • CSRF 防护(Token 验证)

4. 代码沙箱安全

  • Docker 容器隔离
  • 资源限制(CPU、内存、时间)
  • 网络隔离
  • 文件系统保护
  • 进程数限制
3.4.6 可扩展性设计

1. 水平扩展

  • 前后端独立部署,可分别扩展
  • 判题模块可部署多个节点
  • RabbitMQ 自动负载均衡

2. 功能扩展

  • 新增编程语言:实现 SandBox 接口
  • 新增判题模式:扩展判题策略
  • 新增存储方式:实现文件存储接口

3. 微服务化改造

  • 模块间低耦合,易于拆分
  • 可按模块拆分为独立的微服务
  • 使用 Spring Cloud 进行服务治理

小结:本章详细介绍了 YMCloud 系统的技术架构设计。系统采用前后端分离架构,后端基于 Spring Boot 3 构建,划分为五大模块;前端基于 Vue 3 + TypeScript 构建,采用组件化开发模式。判题系统通过 RabbitMQ 消息队列实现异步化,使用 Docker 容器实现代码沙箱隔离。架构设计遵循高内聚低耦合、分层架构、面向接口编程等原则,并应用了多种设计模式,确保系统具有良好的可维护性、可扩展性和安全性。后续章节将详细介绍数据库设计、核心功能实现等内容。


第四章 数据库设计

数据库是系统的数据持久化层,承担着存储、管理和检索业务数据的重要职责。本章详细介绍 YMCloud 在线判题系统的数据库设计,包括数据库选型、表结构设计、索引优化和数据关系设计,确保系统数据的完整性、一致性和高效访问。

4.1 数据库设计概述

4.1.1 数据库选型

YMCloud 系统采用 MySQL 8.0.39 作为主数据库,选择理由如下:

技术特性:

  • 关系型数据库:适合 OJ 系统中复杂的业务关系(用户-题目-提交-比赛等)
  • ACID 事务支持:保证数据一致性,特别是用户 AC 题目记录、比赛排名等关键业务
  • 丰富的索引类型:支持主键、唯一索引、组合索引,优化查询性能
  • JSON 支持:MySQL 8.0 原生支持 JSON 类型,便于存储测试用例等灵活数据

性能优势:

  • InnoDB 引擎:支持行级锁、外键约束、事务回滚
  • 查询优化器:MySQL 8.0 的查询优化器显著提升
  • 索引下推:减少回表次数,提升查询效率

生态成熟度:

  • 开源免费,社区活跃
  • 工具链完善(Navicat、MySQLWorkbench)
  • 与 MyBatis-Plus 无缝集成
4.1.2 数据库设计原则

1. 规范化设计

  • 遵循第三范式(3NF),减少数据冗余
  • 避免插入异常、删除异常和更新异常
  • 合理冗余:为提升查询性能,部分字段适当冗余(如 judge 表中冗余 usernameproblem_title

2. 字段设计原则

  • 主键设计:统一使用 bigint UNSIGNED 自增主键(id
  • 时间字段:统一使用 datetime 类型,自动维护 created_timeupdated_time
  • 状态字段:使用 tinyint 存储枚举状态,节省空间
  • 字符编码:统一使用 utf8mb4 字符集,支持 Emoji 和特殊字符
  • NOT NULL 约束:核心字段设置 NOT NULL,避免空指针异常

3. 索引设计原则

  • 主键索引:每张表必有主键索引
  • 唯一索引:唯一性字段(如 usernameemail)建立唯一索引
  • 组合索引:高频查询条件建立组合索引,遵循最左前缀原则
  • 覆盖索引:尽量让索引包含查询字段,避免回表

4. 性能优化原则

  • 合理使用 TEXT 类型(如题目描述),避免影响索引性能
  • 大字段(如代码)使用 LONGTEXT,单独存储
  • 避免过多外键约束,采用应用层控制数据一致性
  • 分表策略:当 judge 表数据量过大时,可按时间分表
4.1.3 数据库结构统计

YMCloud 数据库共包含 18 张数据表,按业务模块划分如下:

业务模块数据表数量说明
用户模块user, user_role, role, role_auth, auth, user_acproblem6用户信息、角色权限、AC题目记录
题目模块problem, problem_tag, tag, tag_classification4题目信息、标签分类
判题模块judge, judge_case, language3判题记录、测试用例结果、编程语言
比赛模块contest, contest_problem, contest_user3比赛信息、比赛题目、参赛用户
系统模块announcement, file2公告、文件管理
合计-18-

4.2 用户模块数据表设计

用户模块负责管理用户信息、身份认证和权限控制,采用 RBAC(基于角色的访问控制) 模型。

4.2.1 user(用户表)

表说明:存储系统用户的基本信息和账号状态。

表结构:

字段名数据类型约束默认值说明
idbigint UNSIGNEDPK, AUTO_INCREMENT-用户ID(主键)
usernamevarchar(50)NOT NULL, UNIQUE-登录用户名(唯一)
passwordvarchar(255)NOT NULL-密码(BCrypt加密)
nicknamevarchar(50)NULLNULL用户昵称
sextinyintNULL0性别:0=保密,1=男,2=女
emailvarchar(255)NOT NULL, UNIQUE-邮箱(唯一)
avatarvarchar(512)NULLNULL头像URL
schoolvarchar(255)NULLNULL学校
signaturetextNULLNULL个性签名
blogvarchar(512)NULLNULL博客链接
login_ipvarchar(45)NULLNULL最后登录IP
login_datedatetimeNULLNULL最后登录时间
statustinyint(1)NOT NULL1账号状态:1=正常,0=禁用
created_timedatetimeNOT NULLCURRENT_TIMESTAMP创建时间
updated_timedatetimeNOT NULLCURRENT_TIMESTAMP ON UPDATE更新时间

索引设计:

  • PRIMARY KEYid(主键索引)
  • UNIQUE INDEXusername(用户名唯一索引)
  • UNIQUE INDEXemail(邮箱唯一索引)

设计说明:

  • usernameemail 必须唯一,通过唯一索引保证
  • password 存储 BCrypt 加密后的密码,长度 255
  • login_ip 使用 varchar(45) 支持 IPv6 地址
  • status 用于账号封禁功能
4.2.2 role(角色表)

表说明:定义系统中的角色,如管理员、普通用户等。

表结构:

字段名数据类型约束默认值说明
idbigint UNSIGNEDPK, AUTO_INCREMENT-角色ID
codevarchar(64)NOT NULL-角色编码(如 ADMIN, USER)
namevarchar(64)NOT NULL-角色名称(如 管理员,普通用户)
descriptionvarchar(255)NULLNULL角色描述
statustinyint(1)NOT NULL1角色状态:0=禁用,1=启用
created_timedatetimeNOT NULLCURRENT_TIMESTAMP创建时间
updated_timedatetimeNOT NULLCURRENT_TIMESTAMP ON UPDATE更新时间

索引设计:

  • PRIMARY KEYid

设计说明:

  • code 用于程序中判断角色(如 hasRole("ADMIN")
  • name 用于前端展示
  • 支持角色的启用/禁用
4.2.3 auth(权限表)

表说明:定义系统中的权限点,支持菜单权限和按钮权限。

表结构:

字段名数据类型约束默认值说明
idbigint UNSIGNEDPK, AUTO_INCREMENT-权限ID
codevarchar(128)NULLNULL权限编码(如 problem:add, user:delete)
namevarchar(128)NULLNULL权限名称(如 新增题目,删除用户)
parent_idbigint UNSIGNEDNULLNULL父权限ID(支持树形结构)
pathvarchar(255)NULLNULL前端路由路径
route_namevarchar(128)NULLNULL路由名称
componentvarchar(255)NULLNULL前端组件路径
iconvarchar(64)NULLNULL前端菜单图标
typevarchar(255)NULLNULL权限类型(菜单/按钮)
statustinyint(1)NOT NULL1权限状态:0=禁用,1=启用
created_timedatetimeNOT NULLCURRENT_TIMESTAMP创建时间
updated_timedatetimeNOT NULLCURRENT_TIMESTAMP ON UPDATE更新时间

索引设计:

  • PRIMARY KEYid

设计说明:

  • code 用于后端权限验证(如 @PreAuthorize("hasAuthority('problem:add')")
  • parent_id 支持权限的树形结构(菜单 → 子菜单 → 按钮)
  • component 存储前端组件路径,用于动态路由
4.2.4 user_role(用户角色关联表)

表说明:建立用户与角色的多对多关系。

表结构:

字段名数据类型约束默认值说明
idbigint UNSIGNEDPK, AUTO_INCREMENT-主键ID
user_idbigint UNSIGNEDNOT NULL-用户ID
role_idbigint UNSIGNEDNOT NULL-角色ID
created_timedatetimeNOT NULLCURRENT_TIMESTAMP创建时间
updated_timedatetimeNOT NULLCURRENT_TIMESTAMP ON UPDATE更新时间

索引设计:

  • PRIMARY KEYid
  • UNIQUE INDEXuniq_user_role(user_id, role_id)(防止重复分配)
  • INDEXidx_user_id(user_id)(按用户查询角色)
  • INDEXidx_role_id(role_id)(按角色查询用户)

设计说明:

  • 多对多关系:一个用户可以有多个角色,一个角色可以分配给多个用户
  • 唯一索引防止重复分配
4.2.5 role_auth(角色权限关联表)

表说明:建立角色与权限的多对多关系。

表结构:

字段名数据类型约束默认值说明
idbigint UNSIGNEDPK, AUTO_INCREMENT-主键ID
role_idbigint UNSIGNEDNOT NULL-角色ID
auth_idbigint UNSIGNEDNOT NULL-权限ID
created_timedatetimeNOT NULLCURRENT_TIMESTAMP创建时间
updated_timedatetimeNOT NULLCURRENT_TIMESTAMP ON UPDATE更新时间

索引设计:

  • PRIMARY KEYid
  • UNIQUE INDEXuniq_role_auth(role_id, auth_id)(防止重复授权)
  • INDEXidx_role_id(role_id)(按角色查询权限)
  • INDEXidx_auth_id(auth_id)(按权限查询角色)

设计说明:

  • 多对多关系:一个角色可以有多个权限,一个权限可以授予多个角色
  • 唯一索引防止重复授权
4.2.6 user_acproblem(用户AC题目记录表)

表说明:记录用户已通过(AC)的题目,用于统计和快速查询。

表结构:

字段名数据类型约束默认值说明
idbigint UNSIGNEDPK, AUTO_INCREMENT-主键ID
submission_idbigint UNSIGNEDNOT NULL-提交ID
user_idbigint UNSIGNEDNOT NULL-用户ID
usernamevarchar(50)NOT NULL-用户名(冗余)
problem_idbigint UNSIGNEDNOT NULL-题目ID
display_idvarchar(255)NOT NULL-题目展示ID(如 YMOJ-1001)
created_timedatetimeNOT NULLCURRENT_TIMESTAMP创建时间
updated_timedatetimeNOT NULLCURRENT_TIMESTAMP ON UPDATE更新时间

索引设计:

  • PRIMARY KEYid
  • INDEXidx_user_problem(user_id, problem_id)(用户题目组合索引)
  • INDEXidx_user_id(user_id)(按用户查询)

设计说明:

  • 当用户首次AC某题时插入记录
  • 用于快速统计用户AC题目数量
  • usernamedisplay_id 冗余存储,避免关联查询

4.3 题目模块数据表设计

题目模块负责管理题目信息、测试数据和标签分类。

4.3.1 problem(题目表)

表说明:存储题目的详细信息,包括题面、测试数据限制等。

表结构:

字段名数据类型约束默认值说明
idbigint UNSIGNEDPK, AUTO_INCREMENT-题目ID(主键)
problem_idvarchar(255)NOT NULL, UNIQUE-题目展示ID(如 YMOJ-1000)
titlevarchar(255)NOT NULL-题目标题
contenttextNULLNULL题目描述内容(题干)
is_remotetinyint(1)NULL0是否为VJudge判题
inputtextNULLNULL输入描述
outputtextNULLNULL输出描述
examplestextNULLNULL题面样例(JSON格式)
sourcevarchar(255)NULLNULL题目来源
difficultytinyintNOT NULL0难度:0=未评定,1=入门,2=普及,3=提高,4=省选及以上
modetinyintNOT NULL0评测模式:0=ACM,1=OI
visibilitytinyintNOT NULL1可见性:1=公开,2=私有,3=比赛专用
open_case_resulttinyint(1)NOT NULL0是否允许查看测试样例结果
time_limitbigintNOT NULL1000时间限制(ms)
memory_limitbigintNOT NULL65536内存限制(KB)
author_idbigint UNSIGNEDNULLNULL出题人ID
author_usernamevarchar(255)NULLNULL出题人用户名(冗余)
created_timedatetimeNOT NULLCURRENT_TIMESTAMP创建时间
updated_timedatetimeNOT NULLCURRENT_TIMESTAMP ON UPDATE更新时间

索引设计:

  • PRIMARY KEYid
  • UNIQUE INDEXproblem_id(题目展示ID唯一索引)

设计说明:

  • problem_id 是题目的唯一标识,用于URL展示
  • contentinputoutput 使用 text 类型,支持 Markdown
  • examples 存储 JSON 格式的样例数据
  • time_limit 默认 1000ms(1秒)
  • memory_limit 默认 65536KB(64MB)
  • difficulty 参考洛谷难度分级
  • mode 支持 ACM 和 OI 两种评测模式
4.3.2 tag(标签表)

表说明:存储题目标签,支持树形结构。

表结构:

字段名数据类型约束默认值说明
idbigint UNSIGNEDPK, AUTO_INCREMENT-标签ID
namevarchar(255)NOT NULL-标签名称
parent_idbigint UNSIGNEDNULLNULL父标签ID
colorvarchar(255)NULLNULL标签颜色(如 #ff6600)
created_timedatetimeNOT NULLCURRENT_TIMESTAMP创建时间
updated_timedatetimeNOT NULLCURRENT_TIMESTAMP ON UPDATE更新时间

索引设计:

  • PRIMARY KEYid

设计说明:

  • 支持树形结构(通过 parent_id
  • color 用于前端标签颜色展示
4.3.3 tag_classification(标签分类表)

表说明:标签的顶层分类(如算法、数据结构、数学等)。

表结构:

字段名数据类型约束默认值说明
idbigint UNSIGNEDPK, AUTO_INCREMENT-主键ID
namevarchar(255)NOT NULL-分类名称
priorityint UNSIGNEDNOT NULL1优先级(数值越小优先级越高)
created_timedatetimeNOT NULLCURRENT_TIMESTAMP创建时间
updated_timedatetimeNOT NULLCURRENT_TIMESTAMP ON UPDATE更新时间

索引设计:

  • PRIMARY KEYid

设计说明:

  • 标签的顶层分类
  • priority 控制前端展示顺序
4.3.4 problem_tag(题目标签关联表)

表说明:建立题目与标签的多对多关系。

表结构:

字段名数据类型约束默认值说明
idbigint UNSIGNEDPK, AUTO_INCREMENT-主键ID
problem_idbigint UNSIGNEDNOT NULL-题目ID
tag_idbigint UNSIGNEDNOT NULL-标签ID
created_timedatetimeNOT NULLCURRENT_TIMESTAMP创建时间
updated_timedatetimeNOT NULLCURRENT_TIMESTAMP ON UPDATE更新时间

索引设计:

  • PRIMARY KEYid
  • UNIQUE INDEXuk_problem_tag(problem_id, tag_id)(防止重复打标签)

设计说明:

  • 多对多关系:一个题目可以有多个标签,一个标签可以关联多个题目
  • 唯一索引防止重复打标签

4.4 判题模块数据表设计

判题模块是系统的核心,负责记录代码提交、判题结果和测试用例执行情况。

4.4.1 judge(判题记录表)

表说明:记录每一次代码提交的详细信息和判题结果,是系统最核心的表之一。

表结构:

字段名数据类型约束默认值说明
idbigint UNSIGNEDPK, AUTO_INCREMENT-评测ID(主键)
problem_idbigint UNSIGNEDNULLNULL题目ID
problem_titlevarchar(255)NULLNULL题目标题(冗余)
display_idvarchar(100)NULLNULL题目展示ID(如 YMOJ-1001)
user_idbigint UNSIGNEDNOT NULL-用户ID
usernamevarchar(100)NULLNULL用户名(冗余)
languagevarchar(100)NOT NULL-编程语言名称
codelongtextNULLNULL用户提交代码
code_lengthintNULLNULL代码长度(字符)
statusintNULL0评测状态(对应 JudgeStatus 枚举)
scoreintNULLNULL得分(OI模式使用)
timebigintNULL0执行耗时(ms)
memorybigintNULL0内存使用(KB)
error_messagemediumtextNULLNULL编译错误/运行时错误信息
sharetinyintNOT NULL0是否公开代码:0=私有,1=公开
is_manualtinyintNOT NULL0是否为人工评测:0=自动,1=人工
contest_idbigint UNSIGNEDNOT NULL0所属比赛ID(非比赛默认为0)
contest_problem_idbigint UNSIGNEDNULL0比赛题目ID
judge_servervarchar(100)NULLNULL执行判题的机器标识
ipvarchar(255)NULLNULL提交用户IP
versionintNULL1乐观锁版本号
vjudge_submit_idbigint UNSIGNEDNULLNULLVJudge在其它OJ的提交ID
vjudge_usernamevarchar(255)NULLNULLVJudge提交用户名
vjudge_passwordvarchar(255)NULLNULLVJudge提交密码
created_timedatetimeNOT NULLCURRENT_TIMESTAMP创建时间
updated_timedatetimeNOT NULLCURRENT_TIMESTAMP ON UPDATE更新时间

索引设计:

  • PRIMARY KEYid
  • INDEXidx_user_created(user_id, created_time)(用户提交历史查询)
  • INDEXidx_contest_created(contest_id, created_time)(比赛提交记录查询)
  • INDEXidx_contest_display_status(contest_id, display_id, status)(比赛排行榜查询)
  • INDEXidx_user_problem_contest(user_id, problem_id, contest_id)(用户题目提交查询)
  • INDEXidx_status(status)(按状态查询)
  • INDEXidx_problem_id(problem_id)(题目提交统计)

设计说明:

  • 数据量最大的表:随着系统运行,提交记录会持续增长
  • code 使用 longtext,支持大代码量
  • status 对应 JudgeStatus 枚举(-10 到 15)
  • version 用于乐观锁,防止并发更新冲突
  • problem_titleusername 冗余存储,减少关联查询
  • 多个组合索引优化高频查询场景

JudgeStatus 枚举值:

-10: Not Submitted (未提交)
-4: Cancelled (取消)
-3: Presentation Error (格式错误)
-2: Compile Error (编译错误)
-1: Wrong Answer (答案错误)
0: Accepted (通过)
1: Time Limit Exceeded (超时)
2: Memory Limit Exceeded (内存超限)
3: Runtime Error (运行错误)
4: System Error (系统错误)
5: Pending (等待评测)
6: Compiling (编译中)
7: Judging (判题中)
8: Partial Accepted (部分正确)
9: Submitting (提交中)
10: Submitted Failed (提交失败)
15: No Status (无状态)
4.4.2 judge_case(判题样例结果表)

表说明:记录判题过程中每个测试用例的执行结果,用于详细展示测试用例通过情况。

表结构:

字段名数据类型约束默认值说明
idbigint UNSIGNEDPK, AUTO_INCREMENT-评测样例记录ID
judge_idbigint UNSIGNEDNOT NULL-关联的判题记录ID(judge.id)
case_indexintNOT NULL1样例顺序编号(从1开始)
statustinyintNOT NULL0评测结果(对应 JudgeStatus)
timebigintNULLNULL运行耗时(ms)
memorybigintNULLNULL运行内存(KB)
scoreintNULLNULL该样例得分(OI模式)
outputtextNULLNULL用户程序输出(部分信息)
error_messagelongtextNULLNULL运行错误信息
created_timedatetimeNOT NULLCURRENT_TIMESTAMP记录创建时间
updated_timedatetimeNOT NULLCURRENT_TIMESTAMP ON UPDATE记录更新时间

索引设计:

  • PRIMARY KEYid
  • INDEXidx_judge_id(judge_id)(按判题ID查询所有用例)
  • INDEXidx_judge_status(judge_id, status)(查询失败的用例)

设计说明:

  • 一条 judge 记录对应多条 judge_case 记录(一对多)
  • case_index 表示测试用例的顺序
  • output 存储用户程序的部分输出,用于调试
  • OI 模式下每个用例有独立得分
4.4.3 language(编程语言表)

表说明:配置系统支持的编程语言和编译/运行命令。

表结构:

字段名数据类型约束默认值说明
idbigint UNSIGNEDPK, AUTO_INCREMENT-语言ID
namevarchar(100)NOT NULL, UNIQUE-语言名称(如 C++ 17)
imagevarchar(100)NOT NULL-Docker镜像(如 gcc:10)
envvarchar(100)NOT NULL-自定义镜像环境(如 oj-cpp-env)
versionvarchar(100)NULLNULL语言版本信息(如 g++ 9.4)
content_typevarchar(100)NULLNULLMIME类型(用于代码高亮)
file_namevarchar(100)NOT NULL-默认文件名(如 Main.java)
file_extensionvarchar(20)NOT NULL-文件扩展名(如 .cpp)
compile_cmdtextNULLNULL编译指令(如 g++ main.cpp -o main)
run_cmdtextNULLNULL运行指令(如 ./main)
template_codelongtextNULLNULL默认代码模板
code_templatelongtextNULLNULL填空题嵌套模板
typetinyintNOT NULL0语言类型:0=编译型,1=解释型
statustinyintNOT NULL1启用状态:1=启用,0=停用
ojvarchar(100)NULL‘myy’归属OJ标识
created_timedatetimeNOT NULLCURRENT_TIMESTAMP创建时间
updated_timedatetimeNOT NULLCURRENT_TIMESTAMP ON UPDATE更新时间

索引设计:

  • PRIMARY KEYid
  • UNIQUE INDEXname(语言名称唯一索引)

设计说明:

  • 支持动态配置编程语言,无需修改代码
  • imageenv 对应 Docker 镜像
  • compile_cmdrun_cmd 由沙箱模块使用
  • type 区分编译型(C/C++/Java)和解释型(Python)语言

4.5 比赛模块数据表设计

比赛模块负责管理编程竞赛,包括比赛信息、题目配置和参赛用户管理。

4.5.1 contest(比赛表)

表说明:存储比赛的基本信息和配置。

表结构:

字段名数据类型约束默认值说明
idbigint UNSIGNEDPK, AUTO_INCREMENT-比赛ID(主键)
creator_idbigint UNSIGNEDNULLNULL创建者用户ID
creator_usernamevarchar(255)NULLNULL创建者用户名或主办方
imagevarchar(512)NULLNULL比赛Logo图片路径
titlevarchar(255)NOT NULL-比赛标题
descriptiontextNULLNULL比赛说明内容
source_idbigint UNSIGNEDNULL0来源比赛ID(0=原创,其它=克隆赛ID)
ratedtinyint(1)NOT NULL0是否计入Rating积分
modetinyintNOT NULL0赛制模式:0=ACM,1=OI,2=IOI
styletinyintNOT NULL0比赛类型:0=其他,1=周赛,2=月赛,3=娱乐赛,4=挑战赛
visibilitytinyintNOT NULL0访问权限:0=公开,1=私有
passwordvarchar(255)NULLNULL比赛密码(私有模式使用)
start_timedatetimeNULLNULL比赛开始时间
end_timedatetimeNULLNULL比赛结束时间
durationbigintNULLNULL比赛时长(分钟)
allow_end_submittinyint(1)NULL0是否允许比赛结束后提交
created_timedatetimeNOT NULLCURRENT_TIMESTAMP创建时间
updated_timedatetimeNOT NULLCURRENT_TIMESTAMP ON UPDATE更新时间

索引设计:

  • PRIMARY KEYid

设计说明:

  • mode 支持三种赛制:ACM(通过题数+罚时)、OI(部分分)、IOI(实时排名)
  • style 用于前端展示比赛类型标签
  • password 私有比赛需要密码才能参加
  • source_id 用于比赛克隆功能
  • duration 冗余存储,便于计算
4.5.2 contest_problem(比赛题目表)

表说明:配置比赛中包含的题目及其顺序。

表结构:

字段名数据类型约束默认值说明
idbigint UNSIGNEDPK, AUTO_INCREMENT-主键ID
display_idvarchar(255)NOT NULL-题目在比赛中的顺序ID(如 A, B, C)
contest_idbigint UNSIGNEDNOT NULL-比赛ID
problem_idbigint UNSIGNEDNOT NULL-题目ID
display_titlevarchar(255)NOT NULL-题目在比赛中的标题
colorvarchar(255)NULLNULL气球颜色(ACM比赛)
created_timedatetimeNOT NULLCURRENT_TIMESTAMP创建时间
updated_timedatetimeNOT NULLCURRENT_TIMESTAMP ON UPDATE更新时间

索引设计:

  • PRIMARY KEYid
  • INDEXidx_contest_display(contest_id, display_id)(按比赛查询题目)
  • INDEXidx_contest_problem(contest_id, problem_id)(防止重复添加)

设计说明:

  • 一场比赛可以包含多个题目
  • display_id 通常为 A、B、C…,用于比赛中的题目标识
  • display_title 可自定义,不同于题目原标题
  • color 用于 ACM 比赛的气球颜色
4.5.3 contest_user(比赛报名表)

表说明:记录参赛用户,用于权限控制和统计。

表结构:

字段名数据类型约束默认值说明
idbigint UNSIGNEDPK, AUTO_INCREMENT-主键ID
contest_idbigint UNSIGNEDNOT NULL-比赛ID
user_idbigint UNSIGNEDNOT NULL-用户ID
created_timedatetimeNOT NULLCURRENT_TIMESTAMP创建时间
updated_timedatetimeNOT NULLCURRENT_TIMESTAMP ON UPDATE更新时间

索引设计:

  • PRIMARY KEYid

设计说明:

  • 记录用户报名参赛
  • 用于判断用户是否有权限参加比赛
  • 可用于统计参赛人数

4.6 系统模块数据表设计

系统模块提供辅助功能,包括公告发布和文件管理。

4.6.1 announcement(公告表)

表说明:系统公告和通知管理。

表结构:

字段名数据类型约束默认值说明
idbigint UNSIGNEDPK, AUTO_INCREMENT-公告ID
titlevarchar(255)NOT NULL-公告标题
contenttextNOT NULL-公告内容(支持HTML/Markdown)
priorityintNOT NULL0公告权重(值越大越靠前)
pinnedtinyint(1)NOT NULL0是否置顶:1=置顶,0=不置顶
statustinyint(1)NOT NULL11=可见,0=不可见
author_idbigint UNSIGNEDNULLNULL发布人ID
author_usernamevarchar(255)NULLNULL发布人用户名(冗余)
created_timedatetimeNOT NULLCURRENT_TIMESTAMP创建时间
updated_timedatetimeNOT NULLCURRENT_TIMESTAMP ON UPDATE更新时间

索引设计:

  • PRIMARY KEYid

设计说明:

  • priority 控制公告显示顺序
  • pinned 支持公告置顶功能
  • status 用于隐藏公告
4.6.2 file(文件信息表)

表说明:统一管理系统中的文件上传,包括题目数据、附件、头像等。

表结构:

字段名数据类型约束默认值说明
idbigint UNSIGNEDPK, AUTO_INCREMENT-文件ID
file_namevarchar(255)NOT NULL-原始文件名(如 data.zip)
file_typevarchar(50)NULLNULL文件MIME类型(如 application/zip)
file_sizebigint UNSIGNEDNULL0文件大小(字节)
file_extvarchar(50)NULLNULL文件扩展名(如 zip, png)
storage_pathvarchar(500)NOT NULL-文件存储相对路径
storage_typetinyintNOT NULL0存储方式:0=本地,1=OSS,2=MinIO
urlvarchar(1000)NULLNULL文件可访问URL
uploader_idbigint UNSIGNEDNULLNULL上传者用户ID
uploader_usernamevarchar(100)NULLNULL上传者用户名(冗余)
biz_typetinyintNOT NULL0业务类型:0=通用,1=题目附件,2=题目数据包,3=比赛附件,4=公告附件,5=用户头像
biz_idbigint UNSIGNEDNULLNULL业务ID(如题目ID、比赛ID)
is_publictinyintNOT NULL1是否公开访问:0=私有,1=公开
created_timedatetimeNOT NULLCURRENT_TIMESTAMP上传时间
updated_timedatetimeNOT NULLCURRENT_TIMESTAMP ON UPDATE更新时间

索引设计:

  • PRIMARY KEYid

设计说明:

  • 统一的文件管理表,避免每个模块单独管理文件
  • biz_type 区分业务类型
  • biz_id 关联具体业务记录
  • storage_type 支持多种存储方式(本地、OSS、MinIO)
  • is_public 控制文件访问权限

4.7 数据库关系设计

4.7.1 表关系总览

一对多关系:

  1. userjudge:一个用户可以有多次提交
  2. useruser_acproblem:一个用户可以AC多个题目
  3. problemjudge:一个题目可以有多次提交
  4. problemcontest_problem:一个题目可以在多个比赛中使用
  5. contestcontest_problem:一个比赛包含多个题目
  6. contestcontest_user:一个比赛有多个参赛用户
  7. contestjudge:一个比赛有多次提交
  8. judgejudge_case:一次判题有多个测试用例结果

多对多关系(通过关联表):

  1. userrole(通过 user_role):用户与角色
  2. roleauth(通过 role_auth):角色与权限
  3. problemtag(通过 problem_tag):题目与标签

树形结构:

  1. authparent_id 字段支持权限树形结构
  2. tagparent_id 字段支持标签树形结构
4.7.2 核心业务流程的数据关系

用户权限验证流程:

user → user_role → role → role_auth → auth

查询用户所有权限时,需要关联4张表。

题目提交判题流程:

user + problem + language → judge → judge_case

用户提交代码后,创建 judge 记录,判题后创建多条 judge_case 记录。

比赛排行榜查询:

contest → contest_problem → judge (filter by contest_id)

查询比赛排行榜时,需要关联比赛题目和判题记录。

用户AC题目统计:

user → user_acproblem → problem

快速统计用户AC题目数量,无需扫描全部 judge 记录。


4.8 索引优化设计

4.8.1 索引设计原则

YMCloud 数据库的索引设计遵循以下原则:

1. 主键索引

  • 所有表统一使用 id 作为主键
  • 数据类型为 bigint UNSIGNED AUTO_INCREMENT
  • 保证唯一性和快速访问

2. 唯一索引

  • 具有唯一性约束的字段建立唯一索引
  • 示例:
    • user.username
    • user.email
    • problem.problem_id
    • language.name

3. 普通索引

  • 高频查询字段建立普通索引
  • 示例:
    • judge.status:按状态查询提交记录
    • judge.problem_id:按题目查询提交统计

4. 组合索引

  • 多字段联合查询建立组合索引
  • 遵循最左前缀原则
  • 高选择性字段放在前面
4.8.2 核心索引分析

judge 表索引(最重要):

索引名索引字段用途使用场景
PRIMARYid主键索引根据ID查询单条记录
idx_user_createduser_id, created_time用户提交历史个人提交记录页面
idx_contest_createdcontest_id, created_time比赛提交记录比赛Status页面
idx_contest_display_statuscontest_id, display_id, status比赛排行榜实时排行榜计算
idx_user_problem_contestuser_id, problem_id, contest_id用户题目提交判断用户是否提交过某题
idx_statusstatus按状态查询查看所有AC/WA的提交
idx_problem_idproblem_id题目提交统计题目AC率统计

最左前缀原则示例:

-- idx_contest_display_status(contest_id, display_id, status)-- 可以使用索引的查询:
SELECT * FROM judge WHERE contest_id = 1;
SELECT * FROM judge WHERE contest_id = 1 AND display_id = 'A';
SELECT * FROM judge WHERE contest_id = 1 AND display_id = 'A' AND status = 0;-- 无法使用索引的查询:
SELECT * FROM judge WHERE display_id = 'A';
SELECT * FROM judge WHERE status = 0;
4.8.3 索引优化建议

1. 避免过度索引

  • 索引会占用存储空间
  • 写入操作(INSERT/UPDATE)需要维护索引,影响性能
  • 权衡查询频率和写入频率

2. 定期分析索引使用情况

-- 查看未使用的索引
SELECT * FROM sys.schema_unused_indexes;-- 查看索引统计信息
SHOW INDEX FROM judge;

3. 大表优化策略

  • judge 表数据量最大,可考虑分表策略:
    • 按时间分表(如按月、按季度)
    • 按比赛分表(比赛专用表)
  • 历史数据归档(如1年前的提交记录)

4. 慢查询优化

  • 开启慢查询日志,监控慢查询
  • 使用 EXPLAIN 分析查询计划
  • 对慢查询添加索引或优化SQL

4.9 数据库设计总结

YMCloud 数据库设计具有以下特点:

1. 结构清晰

  • 18张表按业务模块清晰划分
  • 表命名规范,易于理解
  • 字段命名统一(如 created_timeupdated_time

2. 规范化设计

  • 遵循第三范式,减少数据冗余
  • 合理冗余(如 usernameproblem_title),提升查询性能
  • 使用关联表实现多对多关系

3. 扩展性强

  • 预留扩展字段(如 language 表的配置化设计)
  • 支持树形结构(如 authtag
  • 支持业务扩展(如 file 表的 biz_type

4. 性能优化

  • 合理的索引设计,覆盖高频查询场景
  • 组合索引优化多条件查询
  • 乐观锁(judge.version)处理并发更新

5. 数据安全

  • 密码加密存储(BCrypt)
  • 敏感信息(如比赛密码)独立存储
  • 逻辑删除(可扩展)而非物理删除

6. 可维护性

  • 所有表统一使用 created_timeupdated_time
  • 状态字段使用枚举值,语义清晰
  • 注释完整,便于团队协作

小结:本章详细介绍了 YMCloud 在线判题系统的数据库设计。系统采用 MySQL 8.0.39 作为主数据库,设计了18张数据表,覆盖用户管理、题目管理、判题系统、比赛管理和系统功能五大模块。数据库设计遵循规范化原则,采用 RBAC 权限模型,支持多种业务场景。通过合理的索引设计和性能优化策略,确保系统能够高效处理大量数据访问请求。数据表之间关系清晰,扩展性强,为系统的稳定运行和未来功能扩展奠定了坚实的数据基础。


第五章 核心功能实现

本章详细介绍 YMCloud 在线判题系统的核心功能实现,包括用户认证与权限管理、在线判题系统、题目管理和比赛系统。所有内容基于项目的真实代码实现,展示关键技术细节和实现思路。

5.1 用户认证与权限管理

用户认证与权限管理是系统安全的基础。YMCloud 采用 JWT + Spring Security + RBAC 的完整解决方案,实现了无状态的身份认证和细粒度的权限控制。

5.1.1 JWT 认证机制实现

JWT(JSON Web Token) 是一种无状态的认证方式,适合分布式系统和前后端分离架构。

核心类:TokenService

位置:ymcloud-core/src/main/java/com/ymcloud/core/security/service/TokenService.java

主要功能:

  1. 生成 Token
public String createToken(LoginUser loginUser) {// 生成32位无连字符的UUID作为唯一标识String token = IdUtil.simpleUUID();// 设置用户代理信息(IP、地址、浏览器、操作系统)setUserAgent(loginUser);// 设置Redis的唯一标识键loginUser.setToken(token);// 刷新用户信息到RedisrefreshToken(loginUser);// 设置JWT携带的信息Map<String, Object> claims = new HashMap<>();claims.put(Constants.LOGIN_USER_KEY, token);claims.put(Constants.JWT_USERNAME, loginUser.getUsername());// 使用HS512算法签名return createToken(claims);
}

JWT生成过程:

  • 使用 jjwt 库的 Jwts.builder() 构建Token
  • 签名算法:HS512
  • 密钥:配置文件中的 token.secret(默认为 “Ymcloud”)
  • 不设置过期时间:Token的过期由Redis控制,而非JWT自身
  1. 验证 Token
public void verifyToken(LoginUser loginUser) {long expireTime = loginUser.getExpireTime();long currentTime = System.currentTimeMillis();// 滑动窗口机制:如果剩余时间少于20分钟,自动续期if (expireTime - currentTime <= MILLIS_MINUTE_TWENTY) {refreshToken(loginUser);}
}

滑动窗口机制:

  • 检测Token剩余有效期
  • 若剩余时间 ≤ 20分钟,自动刷新Redis中的过期时间
  • 实现类似"活跃用户自动续期"的效果
  1. 从请求中获取 LoginUser
public LoginUser getLoginUser(HttpServletRequest request) {// 从请求头获取TokenString token = getToken(request);if (StrUtil.isNotEmpty(token)) {try {// 解析TokenClaims claims = parseToken(token);// 获取UUIDString uuid = (String) claims.get(Constants.LOGIN_USER_KEY);String userKey = getTokenKey(uuid);// 从Redis获取用户信息return redisCache.getCacheObject(userKey);} catch (Exception e) {log.error("获取用户信息异常{}", e.getMessage());}}return null;
}

Token存储策略:

  • Token本身只携带UUID和用户名
  • 完整的用户信息(包括角色、权限)存储在Redis中
  • Redis Key格式:login_tokens:{uuid}
  • 有效期:配置文件中的 token.expireTime(默认60分钟)

核心类:JwtAuthenticationTokenFilter

位置:ymcloud-core/src/main/java/com/ymcloud/core/security/filter/JwtAuthenticationTokenFilter.java

过滤器实现:

@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {@Autowiredprivate TokenService tokenService;@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {// 1. 获取LoginUserLoginUser loginUser = tokenService.getLoginUser(request);// 2. 如果loginUser不为空,且当前没有认证信息if(loginUser != null && SecurityContextHolder.getContext().getAuthentication() == null) {log.info("当前登录用户:{}", loginUser);// 3. 验证Token并自动续期tokenService.verifyToken(loginUser);// 4. 封装为Spring Security的认证对象UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));// 5. 将认证信息写入SecurityContextSecurityContextHolder.getContext().setAuthentication(authenticationToken);}// 6. 放行请求filterChain.doFilter(request, response);}
}

过滤器工作流程:

  1. 每次请求都会经过此过滤器
  2. 从请求头 Authorization 获取Token
  3. Token格式:Bearer {token}
  4. 从Redis获取完整的用户信息
  5. 验证Token有效期,必要时自动续期
  6. 将用户信息写入Spring Security上下文
  7. 后续的Controller可以通过 SecurityUtils.getUser() 获取当前用户
5.1.2 登录认证流程

控制器:LoginController

位置:ymcloud-web/src/main/java/com/ymcloud/web/controller/common/LoginController.java

登录接口:

@PostMapping("/login")
public Result<String> login(@Validated @RequestBody LoginDTO loginDTO) {log.info("用户登录:{}", loginDTO.getUsername());String token = userService.login(loginDTO);return Result.ok(token).msg("登录成功");
}

请求参数(LoginDTO):

  • username:用户名
  • password:密码
  • captcha:验证码
  • uuid:验证码的Redis键

登录业务逻辑流程:

  1. 验证码校验

    • 从Redis中获取验证码
    • 比对用户输入的验证码
    • 验证通过后删除Redis中的验证码
  2. Spring Security 认证

    • 调用 AuthenticationManager.authenticate()
    • 触发 UserDetailsServiceImpl.loadUserByUsername()
  3. 加载用户详细信息

核心类:UserDetailsServiceImpl

位置:ymcloud-core/src/main/java/com/ymcloud/core/security/service/UserDetailsServiceImpl.java

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {// 1. 从数据库中根据用户名查询用户User user = Db.lambdaQuery(User.class).eq(User::getUsername, username).one();// 2. 校验用户是否存在if (user == null) {throw new UserException("该用户不存在");}// 3. 校验用户状态if(UserStatus.DISABLE == user.getStatus()){throw new UserException("该用户被停用");}// 4. 校验密码以及试错次数控制passwordService.validate(user);// 5. 查询角色信息List<String> roles = roleMapper.selectRolesByUserId(user.getId());// 6. 查询权限信息List<String> permissions = new ArrayList<>();if (user.getId() == 1L) {// 超级管理员拥有所有权限permissions = authMapper.selectAllPerms();} else {// 普通用户查询分配的权限permissions = authMapper.selectPermsByUserId(user.getId());}// 7. 返回LoginUser对象return new LoginUser(user, roles, permissions);
}

密码试错次数控制(PasswordService):

  • 密码错误次数存储在Redis中
  • Key格式:pwd_err_cnt:{username}
  • 最大错误次数:配置文件中的 user.password.maxRetryCount(默认5次)
  • 锁定时间:配置文件中的 user.password.lockTime(默认10分钟)
  • 达到最大错误次数后,账号锁定,锁定期间无法登录
  1. 生成 Token

    • 认证成功后,调用 TokenService.createToken() 生成JWT
    • 将用户信息存入Redis
    • 更新用户的最后登录IP和登录时间
    • 返回Token给前端
  2. 前端存储 Token

    • 前端将Token存储在 LocalStorage 中
    • Key:Authorization
    • 后续请求在请求头中携带:Authorization: Bearer {token}
5.1.3 RBAC 权限模型实现

YMCloud 采用 RBAC(Role-Based Access Control) 权限模型,实现了细粒度的权限控制。

权限模型结构:

User(用户) → UserRole(用户角色关联) → Role(角色) → RoleAuth(角色权限关联) → Auth(权限)

权限类型:

  1. 菜单权限:控制前端菜单的显示
  2. 按钮权限:控制页面中按钮的显示
  3. 接口权限:控制后端API的访问

权限编码格式:

  • 格式:模块:功能:操作
  • 示例:
    • problem:problem:list:题目列表查询权限
    • problem:problem:add:题目新增权限
    • user:user:delete:用户删除权限
    • contest:contest:update:比赛修改权限

后端权限验证:

使用 Spring Security 的 @PreAuthorize 注解:

@PreAuthorize("hasAuthority('problem:problem:add')")
@PostMapping("/add")
public Result<?> addProblem(@Validated @RequestBody ProblemDTO problemDTO) {adminProblemService.addProblem(problemDTO);return Result.ok().msg("添加成功");
}

权限判断逻辑:

  1. 请求到达Controller方法前,Spring Security AOP拦截
  2. 从SecurityContext获取当前用户的权限列表
  3. 判断是否包含 problem:problem:add 权限
  4. 有权限则放行,无权限则抛出 AccessDeniedException(403)

超级管理员特殊处理:

  • user.id == 1L 的用户自动拥有所有权限
  • 无需在数据库中配置权限关联
5.1.4 动态路由加载(前端)

前端使用 Pinia 进行状态管理,实现基于权限的动态路由加载。

核心 Store:usePermissionStore

位置:ymcloud-vue/src/stores/permission.ts

主要功能:

  1. 生成侧边栏路由
const generateSidebarRouters = async () => {await getMenus().then(res => {const menus = res.data.data;// 过滤后端返回的菜单,生成前端路由sidebarRouters.value = filterAsyncRouter(menus);// 动态添加路由到Vue Routerconst asyncRoutes = filterDynamicRoutes(dynamicRoutes);asyncRoutes.forEach(route => { router.addRoute(route); });setRoutesLoaded(true);})return sidebarRouters.value;
}
  1. 过滤异步路由
const filterAsyncRouter = (asyncRouterMap: any[]) => {return asyncRouterMap.filter(route => {if (route.component) {// 组件处理if (route.component === 'Layout') {route.component = markRaw(Layout);} else {// 动态加载组件route.component = loadView(route.component);}}if (route.children != null && route.children && route.children.length) {// 递归处理子路由route.children = filterAsyncRouter(route.children);} else {delete route['children'];}return true;})
}
  1. 动态加载组件
const loadView = (view: string) => {let res: any;// 导入views/admin目录下的所有vue文件const modules = import.meta.glob('../views/admin/**/*.vue')for (const path in modules) {const dir = path.split('views/')[1].split('.vue')[0]if (dir === view) {res = () => modules[path]();}}return res;
}

路由守卫(Router Guard):

位置:ymcloud-vue/src/router/index.ts

router.beforeEach(async (to, from, next) => {// 1. 设置页面标题document.title = to.meta.title || 'YMCloud'// 2. 获取用户登录状态const userStore = useUserStore()// 3. 如果已登录if (userStore.isLogin) {// 刷新用户信息if (userStore.refreshUser) {await userStore.getUserProfile()}// 4. 如果是管理端路由,需要加载动态路由if (to.path.startsWith('/admin')) {const permissionStore = usePermissionStore()if (!permissionStore.isRoutesLoaded) {// 第一次访问管理端,加载动态路由await permissionStore.generateSidebarRouters()// 重新跳转next({...to, replace: true})return}}next()} else {// 5. 未登录,重定向到登录页if (whiteList.includes(to.path)) {next()} else {next('/login')}}
})

动态路由加载流程:

  1. 用户登录成功后,前端获取Token
  2. 首次访问管理端路由时,调用后端 /admin/getMenus 接口
  3. 后端根据用户权限返回可访问的菜单列表
  4. 前端解析菜单数据,生成Vue Router路由对象
  5. 使用 router.addRoute() 动态添加路由
  6. 渲染侧边栏菜单
5.1.5 前端权限控制

除了路由级别的权限控制,前端还实现了按钮级别的权限控制。

用户 Store(useUserStore):

位置:ymcloud-vue/src/stores/user.ts

权限判断方法:

// 判断用户是否拥有任意一个角色
const hasAnyRole = (roles: string[]): boolean => {if (isNull(user.value)) {return false;}return hasAnyValue(user.value.roles, roles);
}// 判断用户是否拥有所有权限
const hasEveryPermission = (permissions: string[]): boolean => {if (isNull(user.value)) {return false;}return hasEveryValue(user.value.permissions, permissions);
}

按钮权限控制示例:

<template><!-- 只有拥有 'problem:problem:add' 权限的用户才能看到此按钮 --><el-button v-if="userStore.hasEveryPermission(['problem:problem:add'])"@click="handleAdd">新增题目</el-button>
</template>

5.2 在线判题系统实现

在线判题是 YMCloud 的核心功能,采用 RabbitMQ 异步判题 + Docker 沙箱隔离 的架构,支持多种编程语言和评测模式。

5.2.1 代码提交流程

前端提交界面

位置:ymcloud-vue/src/views/oj/problem/Problem.vue

组件结构:

  • 左侧:题目描述、测试用例、提交记录
  • 右侧:代码编辑器(CodeMirror)、语言选择、提交按钮

代码编辑器配置:

import CodeMirror from 'codemirror'
import 'codemirror/mode/clike/clike'  // C/C++/Java
import 'codemirror/mode/python/python'  // Python
import 'codemirror/addon/edit/closebrackets'  // 自动括号匹配
import 'codemirror/addon/fold/foldcode'  // 代码折叠

提交代码:

const handleSubmit = async () => {const judgeDTO = {problemId: problem.value.problemId,code: code.value,language: language.value,isContest: false}const res = await judgeSubmit(judgeDTO)if (res.data.code === 200) {const submissionId = res.data.data// 开始轮询查询判题结果startPolling(submissionId)}
}

判题结果轮询:

const startPolling = (submissionId: number) => {const timer = setInterval(async () => {const res = await getJudgeStatus(submissionId)const result = res.data.data// 判题结束的状态码const endStatuses = [0, -1, -2, -3, -4, 1, 2, 3, 4]if (endStatuses.includes(result.status)) {// 判题结束,停止轮询clearInterval(timer)showResult(result)}}, 1000)  // 每秒查询一次
}

后端提交接口

位置:ymcloud-web/src/main/java/com/ymcloud/web/controller/oj/JudgeController.java

@RepeatSubmit  // 防重复提交注解
@PostMapping("/submit")
public Result<Long> judge(@Validated @RequestBody JudgeDTO judgeDTO) {log.info("评测请求:{}", judgeDTO);Long submissionId = judgeService.submitJudge(judgeDTO);return Result.ok(submissionId).msg("评测提交成功");
}

防重复提交机制:

  • 使用 @RepeatSubmit 注解
  • 通过AOP拦截器实现
  • 在Redis中记录用户的提交时间戳
  • 5秒内重复提交会被拦截
5.2.2 判题业务逻辑

核心类:JudgeServiceImpl

位置:ymcloud-judge/src/main/java/com/ymcloud/judge/service/impl/JudgeServiceImpl.java

提交评测流程:

@Override
public Long submitJudge(JudgeDTO judgeDTO) {// 1. 判断是否为比赛提交if(judgeDTO.getIsContest()){return contestJudge(judgeDTO);}// 2. 获取当前用户User user = SecurityUtils.getUser();// 3. 查询题目信息String problemId = judgeDTO.getProblemId();Problem problem = problemEntityService.lambdaQuery().eq(Problem::getProblemId, problemId).one();// 4. 校验题目是否存在if(problem == null){throw new ProblemNotExistException();}// 5. 判断题目权限if(problem.getVisibility() == ProblemConstants.CONTEST_PROBLEM){throw new JudgeException("请从比赛官网进行提交评测");}// 6. 私有题目权限校验if(problem.getVisibility() == ProblemConstants.PRIVATE && !ObjectUtil.equals(user.getId(), problem.getAuthorId())){throw new ProblemPermissionException();}// 7. 查询语言配置Language language = languageEntityService.lambdaQuery().eq(Language::getName, judgeDTO.getLanguage()).eq(Language::getOj, problem.getSource()).eq(Language::getStatus, LanguageConstants.ABLE).one();if(language == null){throw new JudgeException("该题目不支持此编程语言");}// 8. 创建判题记录Judge judge = Judge.builder().problemId(problem.getId()).problemTitle(problem.getTitle()).displayId(problem.getProblemId()).userId(user.getId()).username(user.getUsername()).language(language.getName()).status(JudgeStatus.STATUS_PENDING)  // 等待评测.code(judgeDTO.getCode()).codeLength(judgeDTO.getCode().length()).judgeServer(JudgeConstants.LOCAL_JUDGE_SERVER).ip(IpUtils.getIpAddr()).build();// 9. 保存到数据库if(!judgeEntityService.save(judge)){throw new JudgeException("提交失败,请稍后再试");}// 10. 向Redis中保存临时结果(用于前端轮询)JudgeResultVO judgeResult = new JudgeResultVO();judgeResult.setStatus(JudgeStatus.STATUS_PENDING);redisCache.setCacheObject(CacheConstants.JUDGE_KEY + judge.getId(),judgeResult,5, TimeUnit.MINUTES);// 11. 构造判题任务JudgeTask task = JudgeTask.builder().submissionId(judge.getId()).problemId(problem.getId()).code(judgeDTO.getCode()).languageConfig(language).timeLimit(problem.getTimeLimit()).memoryLimit(problem.getMemoryLimit()).mode(problem.getMode()).strategy(JudgeConstants.DEFAULT_JUDGE).build();// 12. 发送消息到RabbitMQ(延迟5秒)messageQueueUtils.sendWithDelay(RabbitMQConstants.JUDGE_EXCHANGE,RabbitMQConstants.JUDGE_ROUTING_KEY,task,5000L);// 13. 返回提交IDreturn judge.getId();
}

为什么延迟5秒发送消息?

  • 避免提交过于频繁
  • 给用户缓冲时间(可以查看提交记录)
  • 减轻判题服务器压力
5.2.3 异步判题机制

消息队列配置:

位置:ymcloud-core/src/main/java/com/ymcloud/core/config/RabbitMQConfig.java

@Bean
public MessageConverter Jackson2JsonMessageConverter() {Jackson2JsonMessageConverter jsonMessageConverter = new Jackson2JsonMessageConverter();// 消息自动生成唯一标识IDjsonMessageConverter.setCreateMessageIds(true);return jsonMessageConverter;
}

消息监听器:JudgeListener

位置:ymcloud-judge/src/main/java/com/ymcloud/judge/listeners/JudgeListener.java

@RabbitListener(bindings = @QueueBinding(value = @Queue(name = RabbitMQConstants.JUDGE_QUEUE, durable = "true"),exchange = @Exchange(value = RabbitMQConstants.JUDGE_EXCHANGE,delayed = "true",  // 延迟交换机type = ExchangeTypes.TOPIC),key = RabbitMQConstants.JUDGE_ROUTING_KEY
))
public void listenJudge(JudgeTask task) {log.info("[评测消息队列]=>评测机收到评测消息:{}", task.getSubmissionId());judgeRun.execute(task);
}

交换机和队列配置:

  • 交换机类型:Topic(主题交换机)
  • 交换机名称judge.direct
  • 队列名称judge.queue
  • 路由键judge.routing
  • 延迟交换机:支持延迟消息(需要安装 rabbitmq_delayed_message_exchange 插件)

判题任务执行:JudgeRun

位置:ymcloud-judge/src/main/java/com/ymcloud/judge/strategy/JudgeRun.java

public void execute(JudgeTask task) {// 判断是默认评测还是自测if(JudgeConstants.DEFAULT_JUDGE.equals(task.getStrategy())){// 默认评测(有测试数据)judge(task);} else if(JudgeConstants.TEST_JUDGE.equals(task.getStrategy())){// 自测评测(用户提供输入输出)selfTest(task);}
}
5.2.4 Docker 沙箱判题

判题核心流程:

private void judge(JudgeTask task) {try {// 1. 获取题目的测试用例List<TestCase> testCases = getTestCases(task.getProblemId());// 2. 更新状态为"编译中"updateStatus(task.getSubmissionId(), JudgeStatus.STATUS_COMPILING);// 3. 创建Docker容器CodeExecContainer container = createContainer(task);container.start();// 4. 选择对应语言的沙箱SandBox sandBox = SandBoxFactory.createSandBox(task.getLanguageConfig().getName(),container,task.getLanguageConfig());// 5. 保存代码到容器sandBox.saveCode(task.getCode());// 6. 编译代码(编译型语言)if (task.getLanguageConfig().getType() == 0) {CmdExecResult compileResult = sandBox.compile();if (compileResult.getExitCode() != 0) {// 编译错误handleCompileError(task, compileResult);return;}}// 7. 更新状态为"判题中"updateStatus(task.getSubmissionId(), JudgeStatus.STATUS_JUDGING);// 8. 逐个运行测试用例List<TestCaseResult> results = new ArrayList<>();long maxTime = 0;long maxMemory = 0;for (int i = 0; i < testCases.size(); i++) {TestCase testCase = testCases.get(i);// 运行测试用例CodeRunResult runResult = sandBox.execute(testCase.getInput(),task.getTimeLimit(),task.getMemoryLimit());// 记录结果TestCaseResult caseResult = TestCaseResult.builder().judgeId(task.getSubmissionId()).caseIndex(i + 1).status(runResult.getStatus()).time(runResult.getTime()).memory(runResult.getMemory()).output(runResult.getOutput()).errorMessage(runResult.getErrorMessage()).build();results.add(caseResult);// 更新最大时间和内存maxTime = Math.max(maxTime, runResult.getTime());maxMemory = Math.max(maxMemory, runResult.getMemory());// 如果不是AC,提前结束(ACM模式)if (task.getMode() == JudgeConstants.ACM && runResult.getStatus() != JudgeStatus.STATUS_ACCEPTED) {break;}}// 9. 计算最终结果JudgeResult finalResult = calculateFinalResult(task, results, maxTime, maxMemory);// 10. 清理容器container.stop();container.remove(true);// 11. 发送结果消息sendResultMessage(finalResult);} catch (Exception e) {log.error("判题异常:{}", e.getMessage());handleJudgeError(task, e);}
}

Docker 容器创建:

private CodeExecContainer createContainer(JudgeTask task) {// 容器工厂CodeExecContainerFactory factory = new CodeExecContainerFactory(dockerClient,hostWorkingDir,containerWorkingDir,task.getMemoryLimit(),  // 内存限制task.getLanguageConfig().getEnv()  // Docker镜像);return factory.createDockerContainer();
}

容器配置:

位置:ymcloud-judge/src/main/java/com/ymcloud/judge/docker/factory/CodeExecContainerFactory.java

@Override
public CodeExecContainer createDockerContainer() {HostConfig hostConfig = new HostConfig();// 限制内存hostConfig.withMemory(memoryLimitKb * 1024L);// 禁止使用SwaphostConfig.withMemorySwap(0L);// 单核CPUhostConfig.withCpuCount(1L);// 关闭网络hostConfig.withNetworkMode("none");// 只读根文件系统(防止恶意代码写系统目录)hostConfig.withReadonlyRootfs(true);// 限制进程数(防fork炸弹)hostConfig.withPidsLimit(64L);// 挂载工作目录hostConfig.withBinds(new Bind(hostWorkingDir, new Volume(containerWorkingDir)));// 创建容器CreateContainerResponse container = dockerClient.createContainerCmd(imageName).withHostConfig(hostConfig).withWorkingDir(containerWorkingDir).withAttachStdout(true).withAttachStderr(true).withAttachStdin(true).withTty(true)  // 挂载伪终端,保持容器运行.exec();return new CodeExecContainer(dockerClient,container.getId(),hostWorkingDir);
}

沙箱抽象类:SandBox

位置:ymcloud-judge/src/main/java/com/ymcloud/judge/sandbox/SandBox.java

编译方法:

public CmdExecResult compile() throws Exception {// 1. 获取编译命令String[] compileCommand = buildCompileCommand();// 2. 执行编译CmdExecResult result = container.execCmd(compileCommand);// 3. 检查编译结果if (result.getExitCode() != 0) {throw new CompileException(result.getStderr());}return result;
}

执行方法:

public CodeRunResult execute(String input, Long timeLimit, Long memoryLimit) throws Exception {// 1. 获取运行命令String[] runCommand = buildRunCommand();// 2. 添加时间和内存限制String[] commandWithLimit = addResourceLimit(runCommand, timeLimit, memoryLimit);// 3. 执行程序CmdExecResult result = container.execCmd(commandWithLimit, input);// 4. 解析运行结果return parseRunResult(result, timeLimit, memoryLimit);
}

不同语言的沙箱实现:

  1. JavaSandBox
@Override
protected String[] buildCompileCommand() {return new String[]{"javac", fileName};
}@Override
protected String[] buildRunCommand() {return new String[]{"java", fileNameWithoutExt};
}
  1. CppSandBox
@Override
protected String[] buildCompileCommand() {return new String[]{"g++", fileName, "-o", "main", "-O2", "-std=c++17"};
}@Override
protected String[] buildRunCommand() {return new String[]{"./main"};
}
  1. PythonSandBox
@Override
protected String[] buildCompileCommand() {// Python是解释型语言,无需编译return null;
}@Override
protected String[] buildRunCommand() {return new String[]{"python3", fileName};
}

时间和内存限制实现:

使用Linux的 /usr/bin/time 命令:

/usr/bin/time -v timeout {timeLimit}s {runCommand}

输出解析:

__TIME__:{real_time}
__MEMORY__:{max_memory}

从标准错误流中提取时间和内存使用情况。

判题结果判定:

private JudgeStatus judgeStatus(String userOutput, String expectedOutput, long time, long memory, long timeLimit, long memoryLimit) {// 1. 时间超限if (time > timeLimit) {return JudgeStatus.STATUS_TIME_LIMIT_EXCEEDED;}// 2. 内存超限if (memory > memoryLimit) {return JudgeStatus.STATUS_MEMORY_LIMIT_EXCEEDED;}// 3. 比对输出if (compareOutput(userOutput, expectedOutput)) {return JudgeStatus.STATUS_ACCEPTED;  // AC} else {return JudgeStatus.STATUS_WRONG_ANSWER;  // WA}
}

输出比对策略:

  • 去除行尾空格
  • 去除文件末尾空行
  • 逐行比对
5.2.5 判题结果更新

结果监听器:

@RabbitListener(bindings = @QueueBinding(value = @Queue(name = RabbitMQConstants.JUDGE_RESULT_QUEUE, durable = "true"),exchange = @Exchange(value = RabbitMQConstants.JUDGE_EXCHANGE,delayed = "true",type = ExchangeTypes.TOPIC),key = RabbitMQConstants.JUDGE_RESULT_ROUTING_KEY
))
public void listenJudgeResultUpdate(JudgeResult result) {log.info("[评测结束更新状态队列]=>收到提交id为:{}的消息", result.getSubmissionId());// 1. 查询判题记录Judge judge = judgeEntityService.getById(result.getSubmissionId());// 2. 更新判题结果judge.setStatus(result.getStatus());judge.setTime(result.getTime());judge.setMemory(result.getMemory());judge.setScore(result.getScore());judge.setErrorMessage(result.getErrorMessage());judgeEntityService.updateById(judge);// 3. 插入测试用例结果List<TestCaseResult> testCaseResults = result.getTestCaseResults();if(!CollUtil.isEmpty(testCaseResults)){List<JudgeCase> judgeCases = BeanUtil.copyToList(testCaseResults, JudgeCase.class);judgeCaseEntityService.saveBatch(judgeCases);}// 4. 更新AC题目表(非比赛提交且AC)if(judge.getStatus() == JudgeStatus.STATUS_ACCEPTED && judge.getContestId() == 0){// 查询是否已经通过了该题目boolean exists = userAcproblemEntityService.lambdaQuery().eq(UserAcproblem::getUserId, judge.getUserId()).eq(UserAcproblem::getProblemId, judge.getProblemId()).exists();if(!exists){// 首次通过则插入记录UserAcproblem userAcproblem = UserAcproblem.builder().submissionId(judge.getId()).userId(judge.getUserId()).username(judge.getUsername()).problemId(judge.getProblemId()).displayId(judge.getDisplayId()).build();userAcproblemEntityService.save(userAcproblem);}}
}

更新Redis缓存:

判题结果更新后,也需要更新Redis中的临时结果:

JudgeResultVO judgeResultVO = JudgeResultVO.builder().status(result.getStatus()).time(result.getTime()).memory(result.getMemory()).errorMessage(result.getErrorMessage()).testCaseResults(result.getTestCaseResults()).build();redisCache.setCacheObject(CacheConstants.JUDGE_KEY + result.getSubmissionId(),judgeResultVO,5, TimeUnit.MINUTES
);

前端获取结果:

前端通过轮询 /judge/status?submissionId={id} 接口获取判题结果:

@GetMapping("/status")
public Result<JudgeResultVO> getStatus(@RequestParam("submissionId") Long submissionId) {log.info("定时评测轮询查询:{}", submissionId);String key = CacheConstants.JUDGE_KEY + submissionId;JudgeResultVO judge = redisCache.getCacheObject(key);if(judge == null){throw new JudgeException("对不起,评测已结束或超时");}return Result.ok(judge);
}

5.3 题目管理实现

题目管理是OJ系统的基础功能,包括题目的增删改查、测试数据管理和Markdown渲染。

5.3.1 题目查询与展示

前端题目页面:

位置:ymcloud-vue/src/views/oj/problem/Problem.vue

页面布局:

  • 分割面板:使用 el-splitter 实现左右分割
  • 左侧:题目详情、提交记录(Tab切换)
  • 右侧:代码编辑器、提交控制

题目详情展示:

<template><n-card :title="problem.title"><div class="content-head-detail"><n-flex vertical><p>题目 ID: {{ problem.problemId }}</p><p>出题人: {{ problem.authorUsername }}</p><p><el-tag type="warning">时间限制</el-tag>C/C++ {{ formatSubmissionTime(problem.timeLimit)}}, 其他语言 {{ formatSubmissionTime(2*problem.timeLimit) }}</p><p><el-tag>空间限制</el-tag> C/C++ {{ formatSubmissionMemory(problem.memoryLimit)}}, 其他语言 {{ formatSubmissionMemory(2*problem.memoryLimit) }}</p></n-flex></div></n-card><!-- Markdown渲染 --><n-card title="题目描述"><MarkdownPreview :content="problem.content"/></n-card>
</template>

Markdown渲染组件:

使用 marked 库解析Markdown:

import { marked } from 'marked'
import DOMPurify from 'dompurify'// 渲染Markdown
const renderMarkdown = (content: string) => {// 1. 使用marked解析const html = marked.parse(content)// 2. 使用DOMPurify清理(防XSS攻击)return DOMPurify.sanitize(html)
}

支持的Markdown特性:

  • 标题、段落、列表
  • 代码块(语法高亮)
  • 表格
  • 数学公式(KaTeX)
  • 图片
5.3.2 题目管理后台

管理端控制器:AdminProblemController

位置:ymcloud-web/src/main/java/com/ymcloud/web/controller/admin/problem/AdminProblemController.java

主要功能:

  1. 题目列表查询
@PreAuthorize("hasAuthority('problem:problem:list')")
@GetMapping("/list")
public Result<PageVO<ProblemVO>> queryList(@Validated AdminProblemPageQuery query) {log.info("管理端查询题目列表:{}", query);PageVO<ProblemVO> pageVO = adminProblemService.queryList(query);return Result.ok(pageVO);
}

查询条件:

  • 题目ID或标题(模糊查询)
  • 难度筛选
  • 可见性筛选
  • 分页参数(pageNum、pageSize)
  1. 新增题目
@PreAuthorize("hasAuthority('problem:problem:add')")
@PostMapping("/add")
public Result<?> addProblem(@Validated @RequestBody ProblemDTO problemDTO) {log.info("管理端新增题目:{}", problemDTO);adminProblemService.addProblem(problemDTO);return Result.ok().msg("添加成功");
}

题目实体包含:

  • 基本信息(题目ID、标题、来源)
  • 题目描述(题干、输入、输出、样例)
  • 限制条件(时间限制、内存限制)
  • 评测配置(评测模式、可见性)
  • 出题人信息
  1. 上传测试数据
@PreAuthorize("hasAuthority('problem:testcase:upload')")
@PostMapping("/{id}/testcase/upload")
public Result<?> uploadTestCaseFiles(@PathVariable("id") Long problemId,@RequestParam("files") MultipartFile[] files
) {log.info("上传测试数据:题目ID={}, 文件数={}", problemId, files.length);adminProblemService.uploadTestCaseFiles(problemId, files);return Result.ok().msg("上传成功");
}

测试数据格式:

  • 每个测试点包含两个文件:{index}.in{index}.out
  • 存储路径:files/problem/{problemId}/testcase/
  • 支持批量上传ZIP压缩包

测试数据读取:

判题时从文件系统读取测试数据:

private List<TestCase> getTestCases(Long problemId) {String testcaseDir = baseDir + "/problem/" + problemId + "/testcase/";File dir = new File(testcaseDir);File[] files = dir.listFiles((d, name) -> name.endsWith(".in"));List<TestCase> testCases = new ArrayList<>();for (File inFile : files) {String input = FileUtils.readFileToString(inFile, StandardCharsets.UTF_8);String outFile = inFile.getAbsolutePath().replace(".in", ".out");String output = FileUtils.readFileToString(new File(outFile), StandardCharsets.UTF_8);testCases.add(new TestCase(input, output));}return testCases;
}
5.3.3 题目标签系统

标签关联:

题目与标签是多对多关系,通过 problem_tag 表关联。

添加标签:

public void addTagsToProblem(Long problemId, List<Long> tagIds) {// 删除原有标签problemTagEntityService.lambdaUpdate().eq(ProblemTag::getProblemId, problemId).remove();// 添加新标签List<ProblemTag> problemTags = tagIds.stream().map(tagId -> {ProblemTag problemTag = new ProblemTag();problemTag.setProblemId(problemId);problemTag.setTagId(tagId);return problemTag;}).collect(Collectors.toList());problemTagEntityService.saveBatch(problemTags);
}

查询题目标签:

public List<Tag> getProblemTags(Long problemId) {// 1. 查询题目标签关联List<ProblemTag> problemTags = problemTagEntityService.lambdaQuery().eq(ProblemTag::getProblemId, problemId).list();// 2. 获取标签ID列表List<Long> tagIds = problemTags.stream().map(ProblemTag::getTagId).collect(Collectors.toList());// 3. 查询标签详情if (CollUtil.isEmpty(tagIds)) {return new ArrayList<>();}return tagEntityService.listByIds(tagIds);
}

5.4 比赛系统实现

比赛系统支持ACM、OI、IOI三种赛制,提供完整的比赛管理和排行榜功能。

5.4.1 比赛创建与配置

比赛实体配置:

Contest contest = Contest.builder().creatorId(user.getId()).creatorUsername(user.getUsername()).title(contestDTO.getTitle()).description(contestDTO.getDescription()).mode(contestDTO.getMode())  // 赛制:0=ACM, 1=OI, 2=IOI.style(contestDTO.getStyle())  // 类型:周赛、月赛等.visibility(contestDTO.getVisibility())  // 公开/私有.password(contestDTO.getPassword())  // 私有赛密码.startTime(contestDTO.getStartTime()).endTime(contestDTO.getEndTime()).duration(duration)  // 时长(分钟).allowEndSubmit(contestDTO.getAllowEndSubmit()).build();

添加题目到比赛:

public void addProblemsToContest(Long contestId, List<ContestProblemDTO> problems) {List<ContestProblem> contestProblems = problems.stream().map(dto -> {ContestProblem cp = new ContestProblem();cp.setContestId(contestId);cp.setProblemId(dto.getProblemId());cp.setDisplayId(dto.getDisplayId());  // A, B, C...cp.setDisplayTitle(dto.getDisplayTitle());cp.setColor(dto.getColor());  // 气球颜色return cp;}).collect(Collectors.toList());contestProblemEntityService.saveBatch(contestProblems);
}
5.4.2 比赛权限控制

比赛访问检查:

private void checkContestAccess(Long contestId, User user) {Contest contest = contestEntityService.getById(contestId);// 1. 检查比赛是否开始if (LocalDateTime.now().isBefore(contest.getStartTime())) {throw new ContestNotStartException();}// 2. 私有赛权限检查if (contest.getVisibility() == ContestConstants.VISIBILITY_PRIVATE) {boolean hasAccess = contestUserEntityService.lambdaQuery().eq(ContestUser::getContestId, contestId).eq(ContestUser::getUserId, user.getId()).exists();if (!hasAccess) {throw new ContestNotAccessException();}}
}

比赛报名:

public void registerContest(Long contestId, String password) {User user = SecurityUtils.getUser();Contest contest = contestEntityService.getById(contestId);// 1. 检查密码(私有赛)if (contest.getVisibility() == ContestConstants.VISIBILITY_PRIVATE) {if (!contest.getPassword().equals(password)) {throw new ContestException("密码错误");}}// 2. 检查是否已报名boolean exists = contestUserEntityService.lambdaQuery().eq(ContestUser::getContestId, contestId).eq(ContestUser::getUserId, user.getId()).exists();if (exists) {throw new ContestException("您已报名该比赛");}// 3. 保存报名记录ContestUser contestUser = ContestUser.builder().contestId(contestId).userId(user.getId()).build();contestUserEntityService.save(contestUser);
}
5.4.3 比赛提交与判题

比赛提交与普通提交的主要区别:

  1. 权限检查更严格

    • 检查比赛是否开始
    • 检查用户是否有权限参赛
    • 检查比赛是否结束(根据配置)
  2. 立即判题(无延迟)

// 比赛立即评测(不延迟5秒)
messageQueueUtils.send(RabbitMQConstants.JUDGE_EXCHANGE,RabbitMQConstants.JUDGE_ROUTING_KEY,task
);
  1. 评测模式以比赛为准
// 以比赛的评测模式为准,而不是题目的
.mode(contest.getMode())
  1. 不计入AC题目统计
// 非比赛提交才会计入AC题目表
if(judge.getStatus() == JudgeStatus.STATUS_ACCEPTED && judge.getContestId() == 0){// 插入user_acproblem表
}
5.4.4 比赛排行榜

ACM赛制排行榜:

排名规则:

  1. 通过题目数多的排名靠前
  2. 通过题目数相同时,罚时少的排名靠前
  3. 罚时 = 所有AC题目的提交时间之和 + 错误提交数 × 20分钟

查询逻辑:

public ContestRankTableVO getContestRankList(ContestRankPageQuery query) {Contest contest = contestEntityService.getById(query.getContestId());// 1. 查询所有比赛提交List<Judge> submissions = judgeEntityService.lambdaQuery().eq(Judge::getContestId, query.getContestId()).orderByAsc(Judge::getCreatedTime).list();// 2. 按用户分组统计Map<Long, UserRankVO> rankMap = new HashMap<>();for (Judge submission : submissions) {Long userId = submission.getUserId();String displayId = submission.getDisplayId();UserRankVO userRank = rankMap.computeIfAbsent(userId, k -> {UserRankVO rank = new UserRankVO();rank.setUserId(userId);rank.setUsername(submission.getUsername());rank.setProblemStatus(new HashMap<>());return rank;});// 获取该题目的状态ProblemStatusVO problemStatus = userRank.getProblemStatus().computeIfAbsent(displayId, k -> new ProblemStatusVO());// 如果已经AC,跳过if (problemStatus.isAc()) {continue;}if (submission.getStatus() == JudgeStatus.STATUS_ACCEPTED) {// ACproblemStatus.setAc(true);problemStatus.setAcTime(Duration.between(contest.getStartTime(), submission.getCreatedTime()).toMinutes());problemStatus.setWrongCount(problemStatus.getWrongCount());// 更新通过题数和罚时userRank.setSolvedCount(userRank.getSolvedCount() + 1);userRank.setPenalty(userRank.getPenalty() + problemStatus.getAcTime() + problemStatus.getWrongCount() * 20);} else {// 错误提交problemStatus.setWrongCount(problemStatus.getWrongCount() + 1);}}// 3. 排序List<UserRankVO> rankList = new ArrayList<>(rankMap.values());rankList.sort((a, b) -> {// 先按通过题数降序int solvedCompare = Integer.compare(b.getSolvedCount(), a.getSolvedCount());if (solvedCompare != 0) {return solvedCompare;}// 再按罚时升序return Long.compare(a.getPenalty(), b.getPenalty());});// 4. 设置排名for (int i = 0; i < rankList.size(); i++) {rankList.get(i).setRank(i + 1);}// 5. 分页return ContestRankTableVO.builder().total(rankList.size()).ranks(paginateList(rankList, query.getPageNum(), query.getPageSize())).build();
}

OI赛制排行榜:

排名规则:

  1. 总分高的排名靠前
  2. 总分相同时,最后一次提交时间早的排名靠前

计算逻辑:

// OI模式:每题取最高分
for (Judge submission : submissions) {ProblemStatusVO problemStatus = userRank.getProblemStatus().computeIfAbsent(displayId, k -> new ProblemStatusVO());// 更新最高分if (submission.getScore() > problemStatus.getScore()) {problemStatus.setScore(submission.getScore());problemStatus.setLastSubmitTime(submission.getCreatedTime());}
}// 计算总分
int totalScore = userRank.getProblemStatus().values().stream().mapToInt(ProblemStatusVO::getScore).sum();
userRank.setTotalScore(totalScore);

小结:本章详细介绍了 YMCloud 在线判题系统的核心功能实现。用户认证采用 JWT + Spring Security + RBAC 架构,实现了无状态认证和细粒度权限控制;在线判题系统采用 RabbitMQ 异步判题 + Docker 沙箱隔离,支持多种编程语言和评测模式;题目管理系统支持 Markdown 渲染、测试数据管理和标签分类;比赛系统支持 ACM、OI、IOI 三种赛制,提供完整的排行榜功能。所有功能的实现都基于真实的代码,展示了系统的技术细节和实现思路。


第六章 前端技术实现

本章详细介绍 YMCloud 在线判题系统前端的技术实现,包括项目构建配置、Vue 3组合式 API 应用、Pinia 状态管理、Vue Router 路由设计、Axios 请求封装、CodeMirror 编辑器集成以及核心组件设计等内容。所有内容基于实际项目代码,展示前端技术栈的完整应用。

6.1 前端项目构建与配置

6.1.1 Vite 构建工具配置

YMCloud 前端项目采用 Vite 作为构建工具,相比 Webpack,Vite 具有更快的冷启动速度和热更新性能。

核心配置文件:vite.config.ts

位置:ymcloud-vue/vite.config.ts

完整配置:

import { fileURLToPath, URL } from 'node:url';
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
// 仅引入用到的组件与样式
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver, NaiveUiResolver } from 'unplugin-vue-components/resolvers'
// gzip静态资源压缩配置
import viteCompression from 'vite-plugin-compression';export default defineConfig({plugins: [vue(),// 自动导入组件Components({resolvers: [ElementPlusResolver(), NaiveUiResolver()],}),// gzip静态资源压缩viteCompression({verbose: true,disable: false,threshold: 10240,  // 大于10KB的文件才压缩algorithm: 'gzip',ext: '.gz',})],base: '/',resolve: {alias: {'@': fileURLToPath(new URL('./src', import.meta.url))},},// 开发服务器配置server: {host: true,open: false,port: 5173,proxy: {'/api': {target: 'http://localhost:9091',changeOrigin: true,secure: false,rewrite: (path) => path.replace(/^\/api/, '')},'/files': {target: 'http://localhost:9091',changeOrigin: true,secure: false}}},// 生产构建配置build: {sourcemap: false,minify: 'terser',cssCodeSplit: true,chunkSizeWarningLimit: 1000,terserOptions: {compress: {drop_console: true,      // 删除consoledrop_debugger: true      // 删除debugger}},rollupOptions: {input: 'index.html',output: {// 静态资源分类打包chunkFileNames: 'assets/js/[name]-[hash].js',entryFileNames: 'assets/js/[name]-[hash].js',assetFileNames: 'assets/[ext]/[name]-[hash].[ext]',// 第三方库单独打包manualChunks(id) {if (id.includes('node_modules')) {return id.toString().split('node_modules/')[1].split('/')[0].toString();}}}},},
})

关键配置说明:

  1. 自动导入组件插件
Components({resolvers: [ElementPlusResolver(), NaiveUiResolver()],
})
  • 使用 unplugin-vue-components 插件
  • 自动按需导入 Element Plus 和 Naive UI 组件
  • 无需手动引入,提高开发效率
  • 自动 Tree-shaking,减小打包体积
  1. 开发代理配置
proxy: {'/api': {target: 'http://localhost:9091',changeOrigin: true,secure: false,rewrite: (path) => path.replace(/^\/api/, '')}
}
  • 解决开发环境跨域问题
  • 请求 /api/* 自动代理到后端服务器
  • changeOrigin: true:修改请求头的 origin 字段
  • rewrite:去除 /api 前缀
  1. 生产构建优化
build: {minify: 'terser',              // 使用terser压缩cssCodeSplit: true,            // CSS代码分割terserOptions: {compress: {drop_console: true,        // 移除console.logdrop_debugger: true        // 移除debugger}}
}
  • 移除生产环境的调试代码
  • CSS 代码分割,减小单文件大小
  • 第三方库单独打包,提高缓存效率
  1. Gzip 压缩
viteCompression({threshold: 10240,      // 10KB以上的文件才压缩algorithm: 'gzip',ext: '.gz',
})
  • 静态资源 Gzip 压缩
  • 大幅减小文件传输大小
  • 提升加载速度
6.1.2 项目入口配置

main.ts 文件:

位置:ymcloud-vue/src/main.ts

import ElementPlus from 'element-plus';
import 'element-plus/dist/index.css';
import zhCn from 'element-plus/dist/locale/zh-cn.mjs';
import * as ElementPlusIconsVue from '@element-plus/icons-vue';
import { createApp } from 'vue';
import { createPinia } from 'pinia';
import App from './App.vue';
import router from './router';const app = createApp(App);// 安装Pinia状态管理
app.use(createPinia());// 安装Vue Router
app.use(router);// 安装Element Plus(中文语言包)
app.use(ElementPlus, {locale: zhCn});// 全局注册Element Plus图标
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {app.component(key, component);
}app.mount('#app');

初始化顺序:

  1. 创建 Vue 应用实例
  2. 安装 Pinia(必须在路由之前)
  3. 安装 Vue Router
  4. 配置 UI 库(Element Plus + 中文)
  5. 全局注册图标组件
  6. 挂载到 DOM

为什么 Pinia 要在 Router 之前?

  • Router 的导航守卫可能需要访问 Store
  • Store 需要先初始化才能被使用
6.1.3 TypeScript 类型定义

YMCloud 前端全面使用 TypeScript,提供完整的类型安全。

核心类型定义:

位置:ymcloud-vue/src/type/types.ts

// 用户信息
export interface UserProfile {id: number;username: string;nickname: string;email: string;sex: number;avatar: string;school: string;roles: string[];permissions: string[];
}// 题目信息
export interface ProblemVO {id: number;problemId: string;title: string;content: string;difficulty: number;mode: number;timeLimit: number;memoryLimit: number;submitCount: number;acceptedCount: number;tags: TagVO[];
}// 判题结果
export interface JudgeResult {submissionId: number;status: string;time: number;memory: number;score: number;errorMessage: string;input: string;expectedOutput: string;output: string;testCaseResults: TestCaseResult[];
}// 语言配置
export interface LanguageVO {name: string;contentType: string;  // CodeMirror的mode
}// 编辑器缓存
export interface EditorCache {code: string;language: string;theme: string;
}

类型安全的好处:

  • 编译时类型检查,减少运行时错误
  • IDE 智能提示,提高开发效率
  • 重构更加安全,不会遗漏类型变化
  • 代码可读性更强

6.2 Vue 3 组合式 API 实践

YMCloud 前端全面使用 Vue 3 的组合式 API(Composition API),相比选项式 API 具有更好的逻辑复用和类型推导能力。

6.2.1 组合式 API 基础结构

典型组件结构:

<script setup lang="ts">
import { ref, computed, onMounted, watch } from 'vue';
import type { ProblemVO } from '@/type/types';
import { getProblemDetail } from '@/api/api';// 响应式数据
const problem = ref<ProblemVO>({} as ProblemVO);
const loading = ref<boolean>(false);// 计算属性
const difficulty = computed(() => {const diffMap = ['简单', '中等', '困难'];return diffMap[problem.value.difficulty] || '未知';
});// 方法
const fetchProblem = async (id: string) => {loading.value = true;try {const res = await getProblemDetail(id);problem.value = res.data.data;} finally {loading.value = false;}
};// 生命周期
onMounted(() => {const problemId = route.params.id as string;fetchProblem(problemId);
});// 监听器
watch(() => problem.value.id, (newId) => {console.log('Problem changed:', newId);
});
</script>

组合式 API 的优势:

  1. 逻辑组织更清晰:相关逻辑可以组织在一起
  2. 更好的类型推导:TypeScript 支持更完善
  3. 逻辑复用更方便:可以提取为组合式函数(Composables)
  4. 代码更易维护:减少 this 的使用
6.2.2 Ref 与 Reactive 的使用

Ref - 基本类型和对象:

// 基本类型
const count = ref<number>(0);
const message = ref<string>('Hello');// 对象类型
const user = ref<UserProfile>({} as UserProfile);// 访问值需要 .value
console.log(count.value);
user.value.username = 'admin';// 在模板中自动解包,不需要.value
// <div>{{ count }}</div>

Reactive - 响应式对象:

const state = reactive({count: 0,message: 'Hello',user: {name: 'admin'}
});// 直接访问属性
console.log(state.count);
state.user.name = 'user';

YMCloud 的使用策略:

  • 基本类型:统一使用 ref
  • 对象类型:优先使用 ref,便于类型定义
  • 多个相关状态:可以使用 reactive 组合
6.2.3 生命周期钩子

CodeMirror 编辑器组件的生命周期应用:

位置:ymcloud-vue/src/components/common/Codemirror.vue

import { onMounted, onUnmounted } from 'vue';// 组件挂载时初始化编辑器
onMounted(() => {// 从缓存恢复编辑器状态const key = props.isContest && props.contestId > 0 && props.problemId? `Problem_${props.contestId}_${props.problemId}`: `Problem_${props.problemId}`;const cache = Storage.get<EditorCache>(key);if (cache) {codeValue.value = cache.code;selectedLanguage.value = cache.language;selectedTheme.value = cache.theme;}
});// 组件卸载时清理资源
onUnmounted(() => {// 清除评测定时器,防止内存泄漏clearTimeoutTask();
});

常用生命周期钩子:

  • onMounted:组件挂载后执行,适合 DOM 操作、数据请求
  • onUnmounted:组件卸载前执行,适合清理定时器、事件监听器
  • onBeforeMount:挂载前执行
  • onBeforeUnmount:卸载前执行

6.3 Pinia 状态管理实现

Pinia 是 Vue 3 官方推荐的状态管理库,相比 Vuex 更加轻量和类型友好。

6.3.1 用户状态管理(useUserStore)

核心Store:useUserStore

位置:ymcloud-vue/src/stores/user.ts

import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
import type { UserProfile } from '@/type/types';
import Storage from '@/utils/storage';export const useUserStore = defineStore('userInfo', () => {// State: ref() 就是 state 属性const user = ref<UserProfile>({} as UserProfile);const token = ref<string>(Storage.get<string>('Authorization') || "");// Getters: computed() 就是 gettersconst isLogin = computed<boolean>(() => {return !isNull(token.value);});const refreshUser = computed<boolean>(() => {return !isNull(token.value) && isNull(user.value);});// Actions: function() 就是 actionsconst doLogin = async (username: string, password: string, code: string, uuid: string) => {try {const res = await login({ username, password, code, uuid });token.value = res.data.data;Storage.set('Authorization', res.data.data);await getUserInfo();// 判断是否处于admin目录const isAdmin = route.path.split('/')[1] === 'admin';const homePath = isAdmin ? '/admin' : '/';const redirectPath = route.query && route.query.redirect as string || homePath;// 过滤redirect参数const query = route.query;const otherQueryParams = Object.keys(query).reduce((acc: any, cur: string) => {if (cur !== "redirect") {acc[cur] = query[cur];}return acc;}, {});router.push({ path: redirectPath, query: otherQueryParams });} catch (error) {throw error;}};const getUserInfo = async () => {if (refreshUser.value) {try {const res = await getUserProfile();user.value = res.data.data;} catch (error) {throw error;}}};const doLogout = async () => {try {await logout();token.value = "";user.value = {} as UserProfile;Storage.remove('Authorization');router.push('/login');} catch (error) {throw error;}};// 权限判断const hasAnyRole = (roles: string[]): boolean => {if (isNull(user.value)) {return false;}return hasAnyValue(user.value.roles, roles);};const hasEveryPermission = (permissions: string[]): boolean => {if (isNull(user.value)) {return false;}return hasEveryValue(user.value.permissions, permissions);};return {user,token,isLogin,refreshUser,doLogin,getUserInfo,doLogout,hasAnyRole,hasEveryPermission};
});

Setup Store 模式的特点:

  1. 使用 ref/reactive 定义状态
const user = ref<UserProfile>({} as UserProfile);
  1. 使用 computed 定义计算属性
const isLogin = computed<boolean>(() => {return !isNull(token.value);
});
  1. 使用普通函数定义 actions
const doLogin = async (username: string, password: string) => {// action logic
};
  1. 返回需要暴露的状态和方法
return {user,token,isLogin,doLogin,doLogout
};

在组件中使用:

<script setup lang="ts">
import { useUserStore } from '@/stores/user';const userStore = useUserStore();// 访问状态
console.log(userStore.user);
console.log(userStore.isLogin);// 调用方法
const handleLogin = () => {userStore.doLogin(username, password, code, uuid);
};// 权限判断
if (userStore.hasEveryPermission(['problem:problem:add'])) {// 有权限
}
</script>
6.3.2 权限状态管理(usePermissionStore)

核心Store:usePermissionStore

位置:ymcloud-vue/src/stores/permission.ts

import { defineStore } from 'pinia';
import { ref, markRaw } from 'vue';
import { getMenus } from '@/api/api';
import Layout from '@/components/admin/AdminLayout.vue';
import router from '@/router';// 导入views/admin目录下的所有vue文件
const modules = import.meta.glob('../views/admin/**/*.vue');export const usePermissionStore = defineStore('permission', () => {const userStore = useUserStore();const sidebarRouters = ref<any[]>([]);const isRoutesLoaded = ref<boolean>(false);const setRoutesLoaded = (value: boolean) => {isRoutesLoaded.value = value;};// 生成侧边栏路由const generateSidebarRouters = async () => {await getMenus().then(res => {const menus = res.data.data;// 过滤后端返回的菜单sidebarRouters.value = filterAsyncRouter(menus);// 动态路由添加const asyncRoutes = filterDynamicRoutes(dynamicRoutes);asyncRoutes.forEach(route => { router.addRoute(route); });setRoutesLoaded(true);});return sidebarRouters.value;};// 动态路由遍历,验证是否具备权限const filterDynamicRoutes = (routes: any[]) => {const res: any[] = [];routes.forEach(route => {if (route.permissions) {if (userStore.hasEveryPermission(route.permissions)) {res.push(route);}} else if (route.roles) {if (userStore.hasAnyRole(route.roles)) {res.push(route);}}});return res;};// 过滤异步路由const filterAsyncRouter = (asyncRouterMap: any[]) => {return asyncRouterMap.filter(route => {if (route.component) {// 组件处理if (route.component === 'Layout') {route.component = markRaw(Layout);} else {route.component = loadView(route.component);}}if (route.children != null && route.children && route.children.length) {// 递归处理子路由route.children = filterAsyncRouter(route.children);} else {delete route['children'];}return true;});};// 动态加载组件const loadView = (view: string) => {let res: any;for (const path in modules) {const dir = path.split('views/')[1].split('.vue')[0];if (dir === view) {res = () => modules[path]();}}return res;};return {sidebarRouters,generateSidebarRouters,isRoutesLoaded,setRoutesLoaded};
});

动态路由加载流程:

  1. 获取后端菜单数据
const menus = await getMenus();
  1. 过滤权限路由
sidebarRouters.value = filterAsyncRouter(menus);
  1. 动态加载组件
route.component = loadView(route.component);
  1. 添加到路由实例
router.addRoute(route);

import.meta.glob 的使用:

  • Vite 提供的特性
  • 批量导入匹配的模块
  • 返回异步导入函数
  • 支持懒加载
const modules = import.meta.glob('../views/admin/**/*.vue');
// modules = {
//   '../views/admin/user/UserList.vue': () => import('...'),
//   '../views/admin/problem/ProblemList.vue': () => import('...'),
//   ...
// }
6.3.3 应用状态管理(useAppStore)

简单Store示例:

位置:ymcloud-vue/src/stores/app.ts

import { defineStore } from 'pinia';
import { ref } from 'vue';export const useAppStore = defineStore('app', () => {// 侧边栏折叠状态const isCollapse = ref<boolean>(false);return { isCollapse };
});

在组件中使用:

<script setup lang="ts">
import { useAppStore } from '@/stores/app';const appStore = useAppStore();// 切换侧边栏折叠
const toggleCollapse = () => {appStore.isCollapse = !appStore.isCollapse;
};
</script><template><el-aside :width="appStore.isCollapse ? '64px' : '200px'"><!-- 侧边栏内容 --></el-aside>
</template>

6.4 Vue Router 路由设计

YMCloud 前端采用 Vue Router 4 实现单页应用路由,支持动态路由加载和导航守卫。

6.4.1 路由配置结构

路由入口文件:

位置:ymcloud-vue/src/router/index.ts

import { createRouter, createWebHistory } from 'vue-router';
import { useUserStore } from '@/stores/user';
import { usePermissionStore } from '@/stores/permission';// OJ端路由
import ojRoutes from './modules/oj';
// 管理端路由
import adminRoutes from './modules/admin';
// 错误页面路由
import errorRoutes from './modules/error';const router = createRouter({history: createWebHistory(import.meta.env.BASE_URL),routes: [{path: '/',redirect: '/home'},...ojRoutes,...adminRoutes,...errorRoutes]
});export default router;

OJ端路由配置:

位置:ymcloud-vue/src/router/modules/oj.ts

export default [{path: '/home',name: 'Home',component: () => import('@/views/oj/home/HomeDetail.vue'),meta: { title: 'YMCloud' }},{path: '/problem/:id',name: 'Problem',component: () => import('@/views/oj/problem/Problem.vue'),meta: { title: '题目详情', requiresAuth: true }},{path: '/problem-list',name: 'ProblemList',component: () => import('@/views/oj/problem/ProblemList.vue'),meta: { title: '题目列表' }},{path: '/contest',name: 'ContestList',component: () => import('@/views/oj/contest/contestList.vue'),meta: { title: '比赛列表' }},{path: '/contest/:id',name: 'ContestDetail',component: () => import('@/views/oj/contest/contestDetail.vue'),meta: { title: '比赛详情', requiresAuth: true }}
];

管理端路由配置:

位置:ymcloud-vue/src/router/modules/admin.ts

export default [{path: '/admin',redirect: '/admin/dashboard',component: () => import('@/components/admin/AdminLayout.vue'),meta: { title: '管理后台', requiresAuth: true },children: [{path: 'dashboard',name: 'Dashboard',component: () => import('@/views/admin/Dashboard.vue'),meta: { title: '仪表板' }}]}
];// 动态路由(需要权限验证)
export const dynamicRoutes = [{path: '/admin/user/profile',name: 'UserProfile',component: () => import('@/views/admin/user/UserProfile.vue'),permissions: ['user:user:query'],meta: { title: '用户详情' }},{path: '/admin/problem/add',name: 'ProblemAdd',component: () => import('@/views/admin/problem/ProblemAdd.vue'),permissions: ['problem:problem:add'],meta: { title: '新增题目' }}
];
6.4.2 导航守卫实现

全局前置守卫:

router.beforeEach(async (to, from, next) => {// 1. 设置页面标题document.title = to.meta.title || 'YMCloud';// 2. 获取用户登录状态const userStore = useUserStore();// 3. 如果已登录if (userStore.isLogin) {// 刷新用户信息if (userStore.refreshUser) {await userStore.getUserProfile();}// 4. 如果是管理端路由,需要加载动态路由if (to.path.startsWith('/admin')) {const permissionStore = usePermissionStore();if (!permissionStore.isRoutesLoaded) {// 第一次访问管理端,加载动态路由await permissionStore.generateSidebarRouters();// 重新跳转(replace: true 避免产生历史记录)next({ ...to, replace: true });return;}}next();} else {// 5. 未登录// 白名单路由直接放行const whiteList = ['/home', '/login', '/register', '/problem-list', '/contest'];if (whiteList.includes(to.path)) {next();} else {// 需要登录的路由,重定向到登录页next({path: '/login',query: { redirect: to.fullPath }  // 记录重定向路径});}}
});

导航守卫的执行顺序:

  1. 全局前置守卫(beforeEach
  2. 路由独享守卫(beforeEnter
  3. 组件内守卫(beforeRouteEnter
  4. 全局解析守卫(beforeResolve
  5. 全局后置钩子(afterEach

动态路由加载的关键点:

// 判断是否已加载动态路由
if (!permissionStore.isRoutesLoaded) {// 加载动态路由await permissionStore.generateSidebarRouters();// 重新跳转,确保动态路由生效next({ ...to, replace: true });return;
}

为什么要重新跳转?

  • router.addRoute() 动态添加路由后
  • 当前的导航还是在旧的路由表中
  • 需要重新导航才能匹配到新添加的路由
  • 使用 replace: true 避免在历史记录中留下记录
6.4.3 路由元信息的应用

路由元信息(meta):

{path: '/problem/:id',meta: {title: '题目详情',requiresAuth: true,permissions: ['problem:problem:view'],keepAlive: true}
}

在导航守卫中使用:

router.beforeEach((to, from, next) => {// 设置标题document.title = to.meta.title || 'YMCloud';// 判断是否需要登录if (to.meta.requiresAuth && !userStore.isLogin) {next('/login');return;}// 判断是否有权限if (to.meta.permissions) {if (!userStore.hasEveryPermission(to.meta.permissions)) {next('/403');return;}}next();
});

6.5 Axios 请求封装

YMCloud 对 Axios 进行了二次封装,统一处理请求和响应,简化 API 调用。

6.5.1 Axios 实例配置

请求封装核心文件:

位置:ymcloud-vue/src/api/request.ts

import axios from "axios";
import { ElMessage } from 'element-plus';
import Storage from "../utils/storage";
import router from "@/router";// 创建axios实例
const service = axios.create({baseURL: '/api',withCredentials: true,timeout: 5000
});// 请求拦截器
service.interceptors.request.use(config => {// 从Storage获取tokenlet token = Storage.get<string>("Authorization");if (token && config.headers) {// 在请求头中添加tokenconfig.headers['Authorization'] = token;}return config;},error => {console.log(error);return Promise.reject(error);}
);// 响应拦截器
service.interceptors.response.use(response => {// 如果是文件操作的返回,由后续进行处理if (response.config.responseType === 'blob') {return response;}const res = response.data;// 操作成功(200)if (res.code === 200) {return Promise.resolve(response);}// 业务异常(500)if (res.code === 500) {ElMessage({message: res.msg,grouping: true,type: 'error',});}// 未登录(401)if (res.code === 401) {ElMessage({message: res.msg,grouping: true,type: 'error',});// 清除缓存Storage.remove("Authorization");// 重定向到登录页router.push('/login');}// 参数错误(400)if (res.code === 400) {ElMessage({message: res.msg,grouping: true,type: 'warning',});}// 权限不足(403)if (res.code === 403) {ElMessage({message: res.msg,grouping: true,type: 'error',});}// 未找到(404)if (res.code === 404) {ElMessage({message: res.msg,grouping: true,type: 'error',});}return Promise.reject(response);},error => {if (error.response) {let { message } = error;if (message == "Network Error") {message = "后端接口连接异常";} else if (message.includes("timeout")) {message = "系统接口请求超时";} else if (message.includes("Request failed with status code")) {message = "系统接口" + message.substr(message.length - 3) + "异常";}ElMessage({message: message,grouping: true,type: 'error',});return Promise.reject(error);}}
);// 导出request函数
function request(options: any) {options.method = options.method || "get";return service(options);
}export default request;

请求拦截器的作用:

  1. 自动添加Token:从Storage读取token并添加到请求头
  2. 统一配置:baseURL、timeout等统一配置

响应拦截器的作用:

  1. 统一错误处理:根据状态码统一处理错误
  2. 自动弹出提示:错误信息自动展示给用户
  3. Token失效处理:401自动清除token并跳转登录页
  4. 文件下载处理:blob类型响应特殊处理
6.5.2 API 接口定义

API 接口文件:

位置:ymcloud-vue/src/api/api.ts

import request from './request';
import type { LoginDTO, RegisterDTO, JudgeDTO } from '@/type/types';/*** 获取验证码*/
export function getCaptcha() {return request({url: "/captcha",method: "GET"});
}/*** 用户登录*/
export function login(data: LoginDTO) {return request({url: "/login",method: "POST",data});
}/*** 用户注册*/
export function register(data: RegisterDTO) {return request({url: "/register",method: "POST",data});
}/*** 获取当前用户信息*/
export function getUserProfile() {return request({url: "/user/profile",method: "GET"});
}/*** 获取题目详情*/
export function getProblemDetail(problemId: string) {return request({url: `/problem/${problemId}`,method: "GET"});
}/*** 提交代码评测*/
export function submitJudge(data: JudgeDTO) {return request({url: "/judge/submit",method: "POST",data});
}/*** 自测评测*/
export function testJudge(data: any) {return request({url: "/judge/test",method: "POST",data});
}/*** 获取评测状态*/
export function getJudgeStatus(params: any) {return request({url: "/judge/status",method: "GET",params});
}/*** 获取菜单列表(动态路由)*/
export function getMenus() {return request({url: "/admin/getMenus",method: "GET"});
}

在组件中使用:

<script setup lang="ts">
import { getProblemDetail, submitJudge } from '@/api/api';// 获取题目详情
const fetchProblem = async (id: string) => {try {const res = await getProblemDetail(id);problem.value = res.data.data;} catch (error) {console.error('获取题目失败:', error);}
};// 提交代码
const handleSubmit = async () => {try {const data = {problemId: problem.value.problemId,language: language.value,code: code.value,isContest: false,contestId: 0};const res = await submitJudge(data);submissionId.value = res.data.data;} catch (error) {console.error('提交失败:', error);}
};
</script>

6.6 CodeMirror 编辑器集成

YMCloud 集成了 CodeMirror 作为在线代码编辑器,支持多种编程语言的语法高亮和代码提示。

6.6.1 CodeMirror 配置

编辑器组件:

位置:ymcloud-vue/src/components/common/Codemirror.vue

引入 CodeMirror 资源:

import Codemirror from 'codemirror-editor-vue3';// 引入CSS文件
import 'codemirror/lib/codemirror.css';// 引入主题
import 'codemirror/theme/idea.css';
import "codemirror/theme/monokai.css";
import "codemirror/theme/solarized.css";// 引入语言模式
import 'codemirror/mode/clike/clike.js';     // C, C++, Java
import 'codemirror/mode/python/python.js';  // Python// 代码提示功能
import 'codemirror/addon/hint/show-hint.css';
import 'codemirror/addon/hint/show-hint';// 代码折叠
import "codemirror/addon/fold/foldgutter.css";
import "codemirror/addon/fold/foldgutter.js";// 括号匹配和自动闭合
import "codemirror/addon/edit/matchbrackets.js";
import "codemirror/addon/edit/closebrackets.js";
import "codemirror/addon/fold/brace-fold.js";
import "codemirror/addon/fold/indent-fold.js";

编辑器配置对象:

const cmOptions = ref({// 语言及语法模式mode: 'text/x-csrc',theme: 'solarized',// 显示行号line: true,lineNumbers: true,// 代码折叠foldGutter: true,gutters: ["CodeMirror-linenumbers", "CodeMirror-foldgutter"],lineWrapping: true,tabSize: 2,matchBrackets: true,      // 括号匹配autoCloseBrackets: true,  // 自动闭合括号// 代码提示功能hintOptions: {completeSingle: false,  // 避免单个提示自动填充},
});

语言模式映射:

const languages: LanguageVO[] = [{ name: "C", contentType: "text/x-csrc" },{ name: "C++", contentType: "text/x-c++src" },{ name: "Java", contentType: "text/x-java" },{ name: "Python", contentType: "text/x-python" }
];
6.6.2 编辑器功能实现

1. 代码自动提示:

const onReady = (editor: any) => {// 输入字母时自动显示提示editor.on('inputRead', function (cm: any, location: any) {if (/[a-zA-Z]/.test(location.text[0])) {cm.showHint();}});
};

2. 切换编程语言:

const updateLanguage = (language: string) => {const lang = languages.find(e => e.name === language);if (lang) {// 更新modecmOptions.value.mode = lang.contentType;// 保存到缓存updateEditor();}
};

3. 切换主题:

const themes = [{ label: "solarized", value: "solarized" },{ label: "idea", value: "idea" },{ label: "monokai", value: "monokai" },
];const updateTheme = (value: string) => {cmOptions.value.theme = value;updateEditor();
};

4. 代码缓存:

const updateEditor = () => {let param: EditorCache = {code: codeValue.value,language: selectedLanguage.value,theme: selectedTheme.value,};// 根据是否是比赛,设置缓存keylet key = null;if (props.isContest && props.contestId > 0 && props.problemId) {key = `Problem_${props.contestId}_${props.problemId}`;} else if (props.problemId) {key = `Problem_${props.problemId}`;}// 设置缓存if (key) {Storage.set<EditorCache>(key, param);}
};

5. 代码上传:

const uploadCode = () => {const fileInput = document.createElement('input');fileInput.type = 'file';fileInput.accept = '.txt,.c,.cpp,.java,.py';fileInput.addEventListener('change', (event: Event) => {const target = event.target as HTMLInputElement;const file = target.files ? target.files[0] : null;if (file) {const reader = new FileReader();reader.onload = (e: ProgressEvent<FileReader>) => {if (e.target && e.target.result) {codeValue.value = e.target.result.toString();}};reader.readAsText(file);}});fileInput.click();
};
6.6.3 判题结果轮询

轮询获取判题状态:

// 评测定时器
const refreshStatus = ref<number | null>(null);
// 轮询计数器
const pollCount = ref<number>(0);
// 最大轮询次数(2秒一次,最多15次,即30秒)
const MAX_POLL_COUNT = 15;const checkJudgeStatus = (params: any) => {// 清理之前的定时器clearTimeoutTask();// 重置轮询计数器pollCount.value = 0;const checkStatus = () => {// 检查是否超过最大轮询次数if (pollCount.value >= MAX_POLL_COUNT) {ElMessage.warning("评测超时,已停止轮询");judgeResult.value = { status: "System Error" } as JudgeResult;judgeLoading.value = false;clearTimeoutTask();return;}// 获取评测结果getJudgeStatus(params).then((res) => {judgeResult.value = res.data.data;const status = judgeResult.value.status;// 判断是否还在判题中if (status === 'Pending' || status === 'Compiling' || status === 'Judging' || status === 'Submitting') {pollCount.value++;// 2秒后再次查询refreshStatus.value = setTimeout(checkStatus, 2000);} else {// 判题结束judgeLoading.value = false;showFullScreen.value = true;activeTabName.value = "result";clearTimeoutTask();}}).catch((err) => {console.log(err);ElMessage.warning("获取评测结果中途失败,已停止轮询");judgeResult.value = { status: "No status" } as JudgeResult;judgeLoading.value = false;clearTimeoutTask();});};// 立即开始轮询checkStatus();
};// 清理轮询任务
const clearTimeoutTask = () => {if (refreshStatus.value) {clearTimeout(refreshStatus.value);refreshStatus.value = null;}pollCount.value = 0;
};// 组件卸载时清理
onUnmounted(() => {clearTimeoutTask();
});

轮询机制的关键点:

  1. 超时控制:最多轮询15次(30秒)
  2. 状态判断:只有特定状态才继续轮询
  3. 资源清理:组件卸载时清除定时器,防止内存泄漏
  4. 错误处理:网络错误自动停止轮询

6.7 核心组件设计

6.7.1 管理后台布局组件

AdminLayout.vue - 管理后台主布局

位置:ymcloud-vue/src/components/admin/AdminLayout.vue

<template><el-container class="admin-container"><!-- 侧边栏 --><el-aside :width="appStore.isCollapse ? '64px' : '200px'"><AdminAside /></el-aside><!-- 主内容区 --><el-container><!-- 头部 --><el-header><AdminHeader /></el-header><!-- 内容区 --><el-main><router-view v-slot="{ Component }"><keep-alive><component :is="Component" /></keep-alive></router-view></el-main></el-container></el-container>
</template><script setup lang="ts">
import { useAppStore } from '@/stores/app';
import AdminAside from './AdminAside.vue';
import AdminHeader from './AdminHeader.vue';const appStore = useAppStore();
</script>

AdminAside.vue - 侧边栏组件

<template><el-menu:default-active="activeMenu":collapse="appStore.isCollapse":unique-opened="true"background-color="#304156"text-color="#bfcbd9"active-text-color="#409EFF"router><SidebarItemv-for="route in permissionStore.sidebarRouters":key="route.path":item="route":base-path="route.path"/></el-menu>
</template><script setup lang="ts">
import { computed } from 'vue';
import { useRoute } from 'vue-router';
import { useAppStore } from '@/stores/app';
import { usePermissionStore } from '@/stores/permission';
import SidebarItem from './SidebarItem.vue';const route = useRoute();
const appStore = useAppStore();
const permissionStore = usePermissionStore();// 当前激活的菜单
const activeMenu = computed(() => {const { path } = route;return path;
});
</script>

SidebarItem.vue - 递归菜单项

<template><!-- 有子菜单 --><el-sub-menu v-if="item.children && item.children.length > 0" :index="item.path"><template #title><el-icon v-if="item.icon"><component :is="item.icon" /></el-icon><span>{{ item.name }}</span></template><SidebarItemv-for="child in item.children":key="child.path":item="child":base-path="resolvePath(child.path)"/></el-sub-menu><!-- 单个菜单项 --><el-menu-item v-else :index="resolvePath(item.path)"><el-icon v-if="item.icon"><component :is="item.icon" /></el-icon><template #title>{{ item.name }}</template></el-menu-item>
</template><script setup lang="ts">
import { defineProps } from 'vue';const props = defineProps({item: {type: Object,required: true},basePath: {type: String,default: ''}
});// 解析路径
const resolvePath = (routePath: string) => {if (routePath.startsWith('/')) {return routePath;}return `${props.basePath}/${routePath}`;
};
</script>

递归组件的关键点:

  • 组件内部调用自身实现递归
  • 判断是否有子菜单决定渲染类型
  • 路径拼接处理绝对路径和相对路径
6.7.2 Markdown 渲染组件

MarkdownPreview.vue

位置:ymcloud-vue/src/components/common/MarkdownPreview.vue

<template><div class="markdown-body" v-html="renderedContent"></div>
</template><script setup lang="ts">
import { computed } from 'vue';
import { marked } from 'marked';
import DOMPurify from 'dompurify';
import 'github-markdown-css';const props = defineProps({content: {type: String,default: ''}
});// 渲染Markdown
const renderedContent = computed(() => {if (!props.content) return '';// 1. 使用marked解析Markdownconst html = marked.parse(props.content);// 2. 使用DOMPurify清理HTML,防止XSS攻击return DOMPurify.sanitize(html);
});
</script><style scoped>
.markdown-body {box-sizing: border-box;min-width: 200px;max-width: 980px;margin: 0 auto;padding: 45px;
}
</style>

安全性处理:

  • 使用 marked 解析 Markdown
  • 使用 DOMPurify 清理 HTML
  • 防止 XSS 攻击
6.7.3 头像组件

Avatar.vue

<template><el-avatar :size="size" :src="avatarUrl" :icon="UserFilled"/>
</template><script setup lang="ts">
import { computed } from 'vue';
import { UserFilled } from '@element-plus/icons-vue';const props = defineProps({avatar: {type: String,default: ''},size: {type: Number,default: 40}
});// 处理头像URL
const avatarUrl = computed(() => {if (!props.avatar) return '';// 如果是完整URL,直接返回if (props.avatar.startsWith('http')) {return props.avatar;}// 否则拼接文件服务器地址return `/files${props.avatar}`;
});
</script>

小结:本章详细介绍了 YMCloud 在线判题系统的前端技术实现。采用 Vite 构建工具提供快速的开发体验,Vue 3 组合式 API 实现高质量的组件开发,Pinia 提供轻量级的状态管理,Vue Router 实现动态路由加载和权限控制,Axios 封装统一处理网络请求,CodeMirror 集成提供专业的代码编辑体验。整个前端架构设计合理、代码规范、类型安全,为用户提供了流畅的使用体验。


第七章 后端实现详解

本章详细介绍 YMCloud 在线判题系统后端的实现细节,包括项目的分层架构设计、控制层的职责划分、服务层的业务逻辑实现、数据访问层的设计模式、安全机制的实施以及性能优化策略。所有内容基于项目实际代码,聚焦架构设计和实现思路的讲解。

7.1 后端项目结构设计

YMCloud 后端采用 Maven 多模块 架构,实现了代码的模块化和职责分离,为后续微服务化改造预留了扩展空间。

7.1.1 Maven 多模块架构

项目采用父子工程结构,根 POM 作为父工程,管理所有子模块的依赖版本和构建配置。

模块划分原则:

  1. ymcloud-common(通用模块)

    • 职责定位:提供系统通用的工具类、常量定义、异常定义和基础组件
    • 核心内容
      • 常量类(ConstantsHttpStatus、各业务模块常量)
      • 工具类(Redis缓存、文件处理、邮件发送、IP地址解析等)
      • 异常体系(业务异常、用户异常、文件异常、判题异常等)
      • 统一结果封装(Result类)
    • 依赖关系:不依赖其他模块,被所有模块依赖
    • 设计优势:抽取公共代码,避免重复,提高代码复用率
  2. ymcloud-pojo(实体模块)

    • 职责定位:定义系统的实体类、DTO(数据传输对象)、VO(视图对象)、BO(业务对象)
    • 核心内容
      • Entity(数据库实体,对应表结构)
      • DTO(接收前端请求参数)
      • VO(返回给前端的数据)
      • BO(业务对象,用于模块间传递)
      • 枚举类(用户状态、判题状态、文件业务类型等)
    • 依赖关系:仅依赖 MyBatis-Plus 注解,被所有模块依赖
    • 设计优势:实现数据模型的统一管理,便于维护和扩展
  3. ymcloud-core(核心模块)

    • 职责定位:提供系统核心功能和基础设施支持
    • 核心内容
      • 安全框架配置(Spring Security、JWT Token)
      • 数据访问层(Mapper接口、EntityService)
      • 配置类(Redis、MyBatis、RabbitMQ、Jackson等)
      • 拦截器(防重复提交、跨域处理)
      • 注解定义(自定义注解)
    • 依赖关系:依赖 common 和 pojo 模块
    • 设计优势:集中管理核心配置和基础服务,避免配置分散
  4. ymcloud-judge(判题模块)

    • 职责定位:实现在线判题的核心功能
    • 核心内容
      • 判题策略(JudgeRun策略)
      • Docker容器管理(容器创建、配置、销毁)
      • 代码沙箱(不同语言的沙箱实现)
      • 消息队列监听器(接收判题任务)
      • 测试用例管理
    • 依赖关系:依赖 common、pojo、core 模块
    • 设计优势:判题功能独立模块化,便于横向扩展和分布式部署
  5. ymcloud-web(Web模块)

    • 职责定位:提供HTTP接口,处理前端请求
    • 核心内容
      • Controller 层(OJ端、管理端、公共接口)
      • Service 层业务逻辑
      • 全局异常处理器
      • 定时任务
      • 配置类(Swagger、Web配置)
    • 依赖关系:依赖所有其他模块,作为应用启动入口
    • 设计优势:作为对外暴露的服务层,整合所有功能模块

模块依赖关系图:

┌─────────────┐
│ ymcloud-web │ (应用入口)
└──────┬──────┘│ 依赖↓
┌──────────────┐
│ ymcloud-judge│ (判题模块)
└──────┬───────┘│ 依赖↓
┌──────────────┐
│ ymcloud-core │ (核心模块)
└──────┬───────┘│ 依赖↓
┌──────────────┐  ┌──────────────┐
│ ymcloud-pojo │  │ ymcloud-common│
│  (实体模块)   │  │  (通用模块)   │
└──────────────┘  └──────────────┘
7.1.2 包结构设计规范

YMCloud 采用经典的分层架构,每一层职责清晰,便于理解和维护。

Controller 层(控制层)

位置:ymcloud-web/src/main/java/com/ymcloud/web/controller/

  • 职责边界

    • 接收前端HTTP请求
    • 参数校验(使用 @Validated 注解)
    • 调用Service层业务逻辑
    • 返回统一格式的响应(Result 对象)
    • 不包含业务逻辑处理
  • 分包策略

    • oj/:OJ端接口(题目、比赛、用户、判题等)
    • admin/:管理端接口(用户管理、题目管理、系统监控等)
    • common/:公共接口(登录、验证码、文件下载等)
  • 命名规范

    • 类名:XxxController
    • 方法名:RESTful风格(queryListaddUserupdateProblem等)

Service 层(服务层)

位置:ymcloud-web/src/main/java/com/ymcloud/web/service/

  • 职责边界

    • 实现核心业务逻辑
    • 事务管理(使用 @Transactional 注解)
    • 调用 Mapper/EntityService 完成数据操作
    • 业务异常抛出和处理
    • 跨模块协调(如判题涉及多个表的操作)
  • 接口与实现分离

    • 接口:定义业务方法规范
    • 实现类:位于 impl/ 目录,实现具体业务逻辑
  • 分包策略

    • oj/:OJ端业务(题目浏览、比赛参与、提交记录等)
    • admin/:管理端业务(数据管理、权限配置等)

Mapper 层(数据访问层)

位置:ymcloud-core/src/main/java/com/ymcloud/core/web/mapper/

  • 职责边界

    • 定义数据库操作接口
    • 自定义SQL查询(复杂查询、统计查询、关联查询)
    • 继承 MyBatis-Plus 的 BaseMapper,获得基础CRUD能力
  • 命名规范

    • 接口名:XxxMapper
    • 方法名:selectinsertupdatedelete 开头

EntityService 层(实体服务层)

位置:ymcloud-core/src/main/java/com/ymcloud/core/web/dao/

  • 职责边界

    • 封装单表的CRUD操作
    • 继承 MyBatis-Plus 的 IService 接口
    • 提供链式查询能力(Lambda表达式)
    • 批量操作支持
  • 设计意图

    • 简化Service层代码,避免在业务层直接使用 Mapper
    • 提供更高层次的数据操作抽象
    • 支持灵活的链式查询
7.1.3 依赖管理策略

父POM统一管理版本:

根 POM 定义了所有依赖的版本号,子模块无需指定版本,确保版本一致性。

核心依赖版本:

  • Spring Boot:3.5.3
  • Spring Security:6.5.1
  • MyBatis-Plus:3.5.9
  • MySQL Driver:8.0.39
  • Redis:集成Spring Data Redis
  • RabbitMQ:集成Spring AMQP
  • Docker Java:3.3.6
  • Lombok:简化代码
  • Hutool:Java工具库
  • Knife4j:API文档

依赖传递管理:

  • common 模块的依赖会传递给所有依赖它的模块
  • 使用 <scope>provided</scope> 避免不必要的传递

7.2 控制层(Controller)设计

控制层是系统对外的接口层,负责接收前端请求、参数校验、调用业务逻辑和返回响应。

7.2.1 RESTful API 设计规范

URL 设计原则:

  1. 资源导向:URL表示资源,使用名词而非动词

    • 正确:GET /problem/{id}
    • 错误:GET /getProblem/{id}
  2. HTTP方法语义化

    • GET:查询资源
    • POST:创建资源
    • PUT:更新资源(全量)
    • PATCH:更新资源(部分)
    • DELETE:删除资源
  3. 版本控制

    • 通过路径控制:/api/v1/problem
    • 通过请求头:Accept: application/vnd.myapi.v1+json
    • YMCloud 当前为初版,暂未加版本号
  4. 路径层级设计

    • OJ端:/problem/contest/judge
    • 管理端:/admin/problem/admin/user
    • 公共接口:/login/captcha

Controller 实现规范:

  1. 类级注解

    • @RestController:标识为 REST 控制器,方法返回JSON
    • @RequestMapping:定义基础路径
    • @Slf4j:日志记录
  2. 方法级注解

    • @GetMapping@PostMapping@PutMapping@DeleteMapping
    • @PreAuthorize:权限控制(Spring Security)
    • @RepeatSubmit:防重复提交(自定义注解)
  3. 参数接收

    • @PathVariable:路径变量
    • @RequestParam:查询参数
    • @RequestBody:请求体(JSON)
    • @Validated:参数校验
  4. 统一返回格式

    • 成功:Result.ok(data).msg("操作成功")
    • 失败:Result.fail().msg("操作失败")
    • 返回类型:Result<T>
7.2.2 参数校验机制

JSR-303 验证框架:

YMCloud 使用 javax.validationhibernate-validator 实现参数校验。

常用校验注解:

  • @NotNull:不能为null
  • @NotBlank:字符串不能为空
  • @NotEmpty:集合/数组不能为空
  • @Size(min, max):字符串/集合长度
  • @Min@Max:数值范围
  • @Email:邮箱格式
  • @Pattern(regexp):正则匹配

校验流程:

  1. DTO类字段上添加校验注解
  2. Controller方法参数前加 @Validated 注解
  3. 校验失败自动抛出 BindException
  4. 全局异常处理器统一处理校验异常

自定义校验注解:

项目中实现了自定义的防重复提交注解 @RepeatSubmit,通过AOP拦截实现。

防重复提交原理:

  • 在Redis中记录用户的请求时间戳
  • Key格式:repeat_submit:{用户ID}:{请求URI}
  • 默认间隔时间:10秒
  • 在间隔时间内重复请求会被拦截
7.2.3 统一异常处理

全局异常处理器:GlobalExceptionHandler

使用 @RestControllerAdvice 注解实现全局异常捕获和处理。

异常处理层次:

  1. Spring Security 异常

    • AccessDeniedException:权限不足(403)
    • 拦截后返回统一的权限不足提示
  2. Spring MVC 异常

    • NoHandlerFoundException:404 Not Found
    • HttpRequestMethodNotSupportedException:405 Method Not Allowed
    • MethodArgumentTypeMismatchException:参数类型不匹配(400)
    • BindException:参数校验失败(400)
    • HttpMessageConversionException:请求体格式错误(400)
  3. 业务异常

    • ServiceException:业务逻辑异常基类
    • 子类异常:UserExceptionProblemExceptionJudgeException
    • 携带自定义错误码和错误信息
  4. 未知异常

    • Exception:兜底处理所有未捕获异常
    • 记录详细日志,返回通用错误提示(避免暴露系统信息)

异常处理优势:

  • 统一错误格式:所有异常返回统一的 Result 结构
  • 日志记录:记录请求URI、异常信息,便于问题排查
  • 信息安全:避免将敏感的系统信息暴露给前端
  • 用户友好:返回简洁易懂的错误提示
7.2.4 接口文档生成

Knife4j 集成:

YMCloud 集成了 Knife4j(Swagger增强版)自动生成API文档。

配置要点:

  1. 引入 knife4j-openapi3-jakarta-spring-boot-starter 依赖
  2. 配置文件中启用 Knife4j
  3. Controller类和方法添加 @Tag@Operation 等注解
  4. 访问路径:/doc.html

文档内容:

  • 接口分组:按模块划分(用户、题目、比赛等)
  • 请求参数:自动识别参数类型、是否必填
  • 响应示例:根据返回类型生成示例
  • 在线调试:直接在文档页面测试接口

7.3 服务层(Service)设计

服务层是业务逻辑的核心实现层,负责复杂的业务处理、事务管理和跨模块协调。

7.3.1 业务逻辑分层原则

职责划分:

  1. Controller 层

    • 只做请求接收和响应返回
    • 不包含业务逻辑
    • 参数校验后直接调用Service
  2. Service 层

    • 实现核心业务逻辑
    • 权限判断(辅助Spring Security)
    • 数据组装和转换(DTO → Entity → VO)
    • 调用 EntityService 或 Mapper 完成数据操作
    • 发送消息队列(如判题任务)
  3. EntityService 层

    • 封装单表的CRUD操作
    • 不包含业务逻辑
    • 提供链式查询能力
  4. Mapper 层

    • 定义自定义SQL
    • 复杂查询、统计查询、多表关联

接口与实现分离:

所有Service都定义为接口,实现类放在 impl/ 包下。

优点:

  • 面向接口编程,降低耦合
  • 便于单元测试(Mock实现类)
  • 支持多种实现(如不同的数据源)
7.3.2 事务管理策略

声明式事务:

使用 Spring 的 @Transactional 注解实现事务管理。

事务配置原则:

  1. 事务边界

    • 在Service层方法上添加 @Transactional
    • Controller和Mapper不使用事务注解
    • 一个业务方法对应一个事务
  2. 事务传播行为

    • 默认:REQUIRED(如果当前存在事务则加入,否则新建)
    • 嵌套调用:同一个事务中执行
    • 独立事务:使用 REQUIRES_NEW
  3. 只读事务

    • 查询方法使用 @Transactional(readOnly = true)
    • 优化性能,提示数据库进行优化
  4. 异常回滚

    • 默认回滚:RuntimeExceptionError
    • 自定义回滚:@Transactional(rollbackFor = Exception.class)
    • 捕获异常要注意事务是否需要回滚

事务使用场景示例:

场景1:用户注册

  • 插入用户记录
  • 分配默认角色
  • 两个操作在同一事务中,要么都成功,要么都失败

场景2:比赛提交判题

  • 创建判题记录
  • 发送消息队列
  • 判题记录保存失败则不发送消息

场景3:题目删除

  • 删除题目记录
  • 删除题目标签关联
  • 删除测试用例文件
  • 所有操作在同一事务中
7.3.3 DTO/VO/Entity 转换

对象类型说明:

  1. Entity(实体类)

    • 对应数据库表结构
    • 使用MyBatis-Plus注解(@TableName@TableId等)
    • 字段与数据库字段一一对应
  2. DTO(Data Transfer Object)

    • 接收前端请求参数
    • 可能包含多个Entity的字段组合
    • 用于输入校验
  3. VO(View Object)

    • 返回给前端的数据
    • 可能包含计算字段、格式化字段
    • 隐藏敏感信息(如密码)

转换策略:

  1. 手动转换

    • 适用于简单对象
    • 使用 BeanUtils.copyProperties() 或手动set
  2. 工具转换

    • 使用 Hutool 的 BeanUtil.copyProperties()
    • 批量转换:BeanUtil.copyToList()
  3. 转换时机

    • Controller → Service:DTO → Entity
    • Service → Controller:Entity → VO
    • 在Service层完成转换,Controller层不做转换

转换注意事项:

  • 避免循环引用
  • 注意字段类型匹配
  • null值处理
  • 集合嵌套转换
7.3.4 缓存使用策略

Redis 缓存应用场景:

  1. 用户登录信息

    • Key:login_tokens:{uuid}
    • 存储:LoginUser 对象(包含用户信息、角色、权限)
    • 过期时间:60分钟(可配置)
    • 滑动窗口:剩余时间 ≤ 20分钟自动续期
  2. 验证码

    • Key:captcha_codes:{uuid}
    • 存储:验证码字符串
    • 过期时间:5分钟
    • 使用后删除
  3. 判题结果

    • Key:judge:{submissionId}
    • 存储:JudgeResultVO 对象
    • 过期时间:5分钟
    • 用于前端轮询查询
  4. 用户AC排名

    • Key:cache:userAcRank
    • 存储:排名列表
    • 刷新策略:每天凌晨1点定时刷新
    • 减少数据库查询压力
  5. 防重复提交

    • Key:repeat_submit:{userId}:{uri}
    • 存储:提交时间戳
    • 过期时间:10秒
    • 用于拦截重复请求

缓存工具封装:RedisCache

位于 ymcloud-common 模块,封装了常用的Redis操作:

  • setCacheObject:设置缓存对象
  • getCacheObject:获取缓存对象
  • deleteObject:删除缓存
  • setCacheList:设置列表
  • getCacheList:获取列表
  • 支持过期时间设置

缓存使用原则:

  • 热点数据:频繁访问且不常变化的数据
  • 过期策略:合理设置过期时间,避免数据不一致
  • 缓存穿透:查询不存在的数据时,缓存空结果
  • 缓存雪崩:避免大量缓存同时过期
  • 缓存击穿:热点数据过期时使用分布式锁

7.4 数据访问层设计

数据访问层负责与数据库的交互,YMCloud 采用 MyBatis-Plus 简化开发。

7.4.1 MyBatis-Plus 集成

核心组件:

  1. BaseMapper 接口

    • MyBatis-Plus 提供的基础接口
    • 包含常用CRUD方法(insert、delete、update、select)
    • 所有Mapper接口继承此接口
  2. IService 接口

    • 提供Service层的CRUD封装
    • 支持批量操作(saveBatch、updateBatchById)
    • EntityService实现类继承 ServiceImpl
  3. Lambda查询

    • 类型安全的查询构造器
    • 使用方法引用避免字符串拼接
    • 支持链式调用

配置要点:

  1. 分页插件

    • 类型:PaginationInnerInterceptor
    • 数据库类型:MySQL
    • 单页限制:50条(防止一次查询过多数据)
  2. 乐观锁插件

    • 类型:OptimisticLockerInnerInterceptor
    • 应用场景:judge 表的并发更新
    • 使用 @Version 注解标记版本字段
  3. Mapper扫描

    • 扫描路径:com.ymcloud.core.web.mapper
    • 自动注册所有Mapper接口
7.4.2 Lambda 查询实践

查询构造器优势:

传统方式(字符串拼接,易出错):

"username = ? AND status = ?"

Lambda方式(类型安全,IDE提示):

.eq(User::getUsername, username)
.eq(User::getStatus, status)

常用查询方法:

  1. 条件查询

    • eq:等于
    • ne:不等于
    • gt/ge:大于/大于等于
    • lt/le:小于/小于等于
    • like:模糊查询
    • in:IN查询
    • isNull/isNotNull:null判断
  2. 逻辑运算

    • and:并且
    • or:或者
    • nested:嵌套条件
  3. 排序

    • orderByAsc:升序
    • orderByDesc:降序
  4. 分页

    • page(Page<T> page):分页查询

查询示例场景:

场景1:查询用户列表(条件+分页)

  • 根据用户名模糊查询
  • 根据状态筛选
  • 分页返回结果

场景2:查询比赛题目

  • 根据比赛ID查询
  • 根据显示顺序排序
  • 只查询特定字段

场景3:统计用户AC数

  • 关联 user_acproblem 表
  • 按用户分组统计
  • 降序排列
7.4.3 自定义 SQL 实现

Mapper XML 配置:

位置:ymcloud-core/src/main/resources/mapper/

复杂查询、多表关联、统计查询需要自定义SQL。

SQL 编写规范:

  1. 参数传递

    • 单个参数:直接使用 #{param}
    • 多个参数:使用 @Param 注解
    • 对象参数:#{对象.字段}
  2. 动态SQL

    • <if>:条件判断
    • <where>:自动处理 WHERE 关键字
    • <foreach>:循环拼接(IN查询)
    • <choose>:多分支选择
  3. 结果映射

    • resultType:简单类型映射
    • resultMap:复杂对象映射
    • 关联查询:一对一、一对多

自定义SQL场景:

  1. 权限查询

    • 根据用户ID查询所有权限
    • 多表关联(user_role → role_auth → auth)
    • 返回权限编码列表
  2. 比赛排行榜

    • 统计用户在比赛中的AC数、总分、罚时
    • 关联 judge、contest_user 表
    • 按规则排序
  3. 题目统计

    • 统计题目的提交数、通过数
    • 计算通过率
7.4.4 EntityService 封装

设计意图:

在Service业务层和Mapper数据层之间增加一层EntityService,统一管理单表操作。

优势分析:

  1. 简化Service代码

    • Service层无需直接调用Mapper
    • 链式查询更加简洁
    • 减少样板代码
  2. 职责分离

    • EntityService:单表CRUD
    • Service:业务逻辑编排
    • Mapper:自定义SQL
  3. 批量操作支持

    • saveBatch:批量插入
    • updateBatchById:批量更新
    • 自动分批处理,避免SQL过长

EntityService 使用示例:

所有EntityService接口都继承 IService<Entity>,实现类继承 ServiceImpl<Mapper, Entity>

链式查询示例:

List<User> users = userEntityService.lambdaQuery().eq(User::getStatus, UserStatus.NORMAL).like(User::getUsername, keyword).orderByDesc(User::getCreatedTime).list();

分页查询示例:

Page<Problem> page = new Page<>(pageNum, pageSize);
Page<Problem> result = problemEntityService.lambdaQuery().eq(Problem::getVisibility, ProblemConstants.PUBLIC).page(page);

7.5 安全设计实现

安全是系统的重要保障,YMCloud 实现了多层次的安全防护机制。

7.5.1 Spring Security 配置

安全配置核心类:SecurityConfig

关键配置项:

  1. CSRF防护

    • 禁用CSRF(前后端分离,使用Token认证)
    • 适用于无状态的RESTful API
  2. 会话管理

    • 会话策略:STATELESS(无状态)
    • 不创建HttpSession
    • 通过JWT Token维护用户状态
  3. 认证入口点

    • 自定义 AuthenticationEntryPoint
    • 未登录访问返回401
    • 返回JSON格式错误信息
  4. 访问拒绝处理器

    • 自定义 AccessDeniedHandler
    • 权限不足返回403
    • 返回JSON格式错误信息
  5. 登出处理器

    • 自定义 LogoutSuccessHandler
    • 清除Redis中的Token
    • 返回登出成功信息
  6. 过滤器链

    • 添加 JwtAuthenticationTokenFilter
    • UsernamePasswordAuthenticationFilter 之前执行
    • 每个请求都经过Token验证

URL权限配置:

  • 公开接口permitAll()

    • 登录、注册、验证码
    • 题目列表、比赛列表(游客可访问)
  • 需要认证authenticated()

    • 题目详情、代码提交
    • 个人信息、提交记录
  • 需要权限:通过 @PreAuthorize 注解控制

    • 管理端所有接口
    • 细粒度到每个操作
7.5.2 密码加密与验证

密码加密策略:

  1. 加密算法

    • 使用 BCrypt 算法
    • Spring Security 提供的 BCryptPasswordEncoder
    • 每次加密结果不同(含随机盐值)
  2. 加密时机

    • 用户注册时加密密码
    • 用户重置密码时加密
    • 管理员重置用户密码时加密
  3. 密码验证

    • Spring Security 自动验证
    • PasswordService 增加试错次数控制

密码试错次数控制:

核心类:PasswordService

控制机制:

  1. 密码错误次数存储在Redis
  2. Key格式:pwd_err_cnt:{username}
  3. 最大错误次数:5次(可配置)
  4. 锁定时间:10分钟(可配置)
  5. 密码正确后清除错误次数
  6. 达到最大次数后账号锁定

安全优势:

  • 防止暴力破解
  • 自动锁定异常账号
  • 锁定期自动解除
  • 错误次数实时更新
7.5.3 防重复提交机制

自定义注解:@RepeatSubmit

工作原理:

  1. 注解定义

    • interval:间隔时间(毫秒,默认10秒)
    • message:提示消息
  2. AOP拦截器

    • 实现 HandlerInterceptor 接口
    • preHandle 方法中拦截
  3. 防重机制

    • 生成唯一键:用户ID + 请求URI + 请求参数
    • 在Redis中记录最后请求时间
    • 计算时间间隔
    • 间隔小于设定值则拦截
  4. 缓存清理

    • 设置Redis过期时间为间隔时间
    • 自动清理过期数据

应用场景:

  • 代码提交接口
  • 订单创建接口
  • 数据导入接口
  • 评论发表接口

使用方式:
在Controller方法上添加注解即可,无需额外代码。

7.5.4 XSS 防护策略

跨站脚本攻击防护:

  1. 前端防护

    • Markdown渲染时使用 DOMPurify 清理HTML
    • 防止恶意脚本注入
  2. 后端防护

    • 参数校验(长度、格式)
    • 特殊字符转义
    • 不信任前端数据
  3. 数据库防护

    • 使用参数化查询(MyBatis默认)
    • 避免SQL拼接

SQL注入防护:

MyBatis的 #{} 占位符自动进行预编译,有效防止SQL注入。

避免使用 ${}

  • #{} :预编译,安全
  • ${} :直接拼接,不安全(仅在特殊场景如ORDER BY使用)
7.5.5 接口限流设计

限流策略:

  1. 全局限流

    • 使用Nginx限流模块
    • 限制单IP访问频率
    • 防止DDoS攻击
  2. 接口级限流

    • 使用Redis+Lua实现令牌桶算法
    • 控制单个接口的访问频率
    • 保护核心接口
  3. 用户级限流

    • 控制单个用户的操作频率
    • 代码提交:每秒最多1次
    • 查询接口:每秒最多10次

7.6 性能优化策略

7.6.1 数据库查询优化

索引优化:

  1. 主键索引

    • 所有表都使用 bigint UNSIGNED 作为主键
    • 使用雪花算法生成分布式ID
  2. 唯一索引

    • user.username:用户名唯一
    • problem.problem_id:题目ID唯一
    • 加速查询并保证唯一性
  3. 普通索引

    • 频繁查询的字段添加索引
    • judge.user_idjudge.problem_id
  4. 组合索引

    • 多条件查询使用组合索引
    • (contest_id, user_id)
    • 遵循最左前缀原则

查询优化原则:

  1. **避免SELECT ***:

    • 只查询需要的字段
    • 减少网络传输和内存占用
  2. 分页查询

    • 限制单页最大数量
    • 深分页优化(使用索引覆盖)
  3. 批量操作

    • 使用 MyBatis-Plus 的 saveBatch
    • 减少数据库交互次数
  4. 延迟加载

    • 关联查询使用懒加载
    • 按需加载数据
7.6.2 缓存优化策略

多级缓存架构:

  1. 本地缓存

    • 使用 Caffeine 缓存热点数据
    • 如系统配置、常量数据
    • 减少Redis访问
  2. Redis缓存

    • 用户登录信息
    • 判题结果
    • 统计数据
  3. 数据库

    • 持久化存储
    • 兜底数据源

缓存更新策略:

  1. Cache Aside(旁路缓存):

    • 读:先查缓存,没有则查数据库并更新缓存
    • 写:先更新数据库,再删除缓存
  2. Write Through(写穿):

    • 先写缓存,缓存负责写数据库
  3. Write Behind(写回):

    • 先写缓存,异步写数据库

YMCloud主要使用 Cache Aside 模式。

缓存问题处理:

  1. 缓存穿透

    • 查询不存在的数据,每次都打到数据库
    • 解决:缓存空对象或使用布隆过滤器
  2. 缓存雪崩

    • 大量缓存同时过期
    • 解决:过期时间加随机值
  3. 缓存击穿

    • 热点数据过期瞬间大量请求打到数据库
    • 解决:使用分布式锁或永不过期
7.6.3 异步处理机制

异步任务场景:

  1. 判题任务

    • 代码提交后异步判题
    • 通过RabbitMQ消息队列实现
    • 判题结果异步更新
  2. 邮件发送

    • 注册验证、密码重置异步发送邮件
    • 不阻塞主流程
  3. 定时任务

    • 用户AC排名定时刷新
    • 使用 @Scheduled 注解
    • Cron表达式配置执行时间

消息队列使用:

  1. 延迟消息

    • 普通题目提交延迟5秒判题
    • 防止频繁提交
    • 使用RabbitMQ延迟插件
  2. 消息确认

    • 手动ACK模式
    • 确保消息不丢失
  3. 死信队列

    • 处理失败的消息
    • 记录错误日志
7.6.4 静态资源优化

文件存储策略:

  1. 本地文件系统

    • 测试数据、题目附件存储在本地
    • 路径:files/ 目录
    • Nginx直接提供静态文件服务
  2. 访问路径映射

    • 后端:/files/**
    • 实际路径:物理磁盘路径
    • Nginx配置直接访问
  3. 文件上传限制

    • 大小限制:100MB
    • 类型限制:根据业务场景
    • 病毒扫描(可选)

前端资源优化:

  1. Gzip压缩

    • Vite构建时生成.gz文件
    • Nginx配置gzip_static on
  2. 资源分割

    • 第三方库单独打包
    • 提高缓存命中率
  3. 懒加载

    • 路由懒加载
    • 图片懒加载
7.6.5 定时任务设计

Spring Task 集成:

定时任务配置:

  • 启动类添加 @EnableScheduling 注解
  • 任务类添加 @Component 注解
  • 方法添加 @Scheduled 注解

Cron表达式:

  • 格式:秒 分 时 日 月 周
  • 示例:0 0 1 * * ?(每天凌晨1点)

定时任务应用:

  1. 用户AC排名刷新

    • 执行时间:每天凌晨1点
    • 任务内容:统计用户AC数并缓存到Redis
    • 目的:减少实时查询压力
  2. 日志清理(可扩展):

    • 清理过期日志文件
    • 释放磁盘空间
  3. 数据备份(可扩展):

    • 定期备份数据库
    • 上传到云存储

注意事项:

  • 定时任务不要执行耗时操作
  • 分布式环境需要考虑任务重复执行问题
  • 可使用分布式锁或任务调度框架(如XXL-JOB)

小结:本章详细讲解了 YMCloud 在线判题系统的后端实现。通过Maven多模块架构实现了代码的模块化和职责分离;控制层遵循RESTful规范,统一异常处理和参数校验;服务层实现业务逻辑编排和事务管理;数据访问层使用MyBatis-Plus简化开发;安全层面实现了JWT认证、RBAC权限控制、密码加密、防重复提交等多重防护;性能优化涵盖了数据库查询、缓存策略、异步处理等多个维度。整个后端架构设计合理、代码规范、安全可靠,为系统的稳定运行提供了坚实保障。


第八章 系统部署

本章介绍 YMCloud 在线判题系统的部署方案,包括开发环境配置、生产环境部署、服务器环境要求、数据库初始化、依赖服务安装以及系统启动与运维等内容。

8.1 部署环境要求

8.1.1 硬件环境要求

服务器最低配置:

  • CPU:4核及以上(推荐8核)
  • 内存:8GB及以上(推荐16GB)
  • 磁盘:100GB及以上(SSD推荐)
  • 网络:100Mbps及以上带宽

推荐配置说明:

  • 判题模块:需要运行Docker容器,CPU和内存需求较高
  • 数据库:MySQL需要足够内存用于查询缓存
  • Redis:缓存服务需要一定内存
  • 文件存储:题目测试数据、用户提交代码、图片等需要磁盘空间
8.1.2 软件环境要求

操作系统:

  • 推荐:Linux(Ubuntu 20.04/22.04、CentOS 7.9)
  • 支持:Windows 10/11(开发环境)
  • 不推荐:Windows Server(Docker支持不完善)

必备软件及版本:

软件版本要求用途备注
JDK17及以上Java运行环境建议使用OpenJDK或Oracle JDK
Maven3.8.0及以上项目构建工具用于编译打包
MySQL8.0.39及以上关系型数据库存储业务数据
Redis6.0及以上缓存数据库缓存热点数据
RabbitMQ3.12及以上消息队列异步判题任务
Docker20.10及以上容器化平台代码沙箱隔离
Node.js18.0及以上前端构建环境用于前端打包
Nginx1.20及以上Web服务器反向代理、静态资源

可选软件:

  • Git:版本控制,用于代码拉取
  • Docker Compose:容器编排(如果使用容器化部署)
  • Nacos:服务注册与配置中心(微服务化改造时使用)

8.2 依赖服务安装与配置

8.2.1 MySQL 数据库安装

安装步骤(Ubuntu为例):

  1. 更新软件源
sudo apt update
  1. 安装MySQL Server
sudo apt install mysql-server
  1. 启动MySQL服务
sudo systemctl start mysql
sudo systemctl enable mysql
  1. 安全配置
sudo mysql_secure_installation

数据库初始化:

  1. 登录MySQL
mysql -u root -p
  1. 创建数据库
CREATE DATABASE ymcloud DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
  1. 创建用户并授权
CREATE USER 'yemiao'@'localhost' IDENTIFIED BY 'your_password';
GRANT ALL PRIVILEGES ON ymcloud.* TO 'yemiao'@'localhost';
FLUSH PRIVILEGES;
  1. 导入数据库结构
mysql -u yemiao -p ymcloud < sql/ymcloud-struct.sql
  1. 导入初始数据(可选)
mysql -u yemiao -p ymcloud < sql/ymcloud.sql

MySQL配置优化:

编辑 /etc/mysql/mysql.conf.d/mysqld.cnf

[mysqld]
# 字符集
character-set-server=utf8mb4
collation-server=utf8mb4_general_ci# 连接数
max_connections=500# InnoDB配置
innodb_buffer_pool_size=2G
innodb_log_file_size=256M
innodb_flush_log_at_trx_commit=2
innodb_flush_method=O_DIRECT# 慢查询日志
slow_query_log=1
slow_query_log_file=/var/log/mysql/slow.log
long_query_time=2

重启MySQL使配置生效:

sudo systemctl restart mysql
8.2.2 Redis 缓存服务安装

安装步骤:

  1. 安装Redis
sudo apt install redis-server
  1. 修改配置文件

编辑 /etc/redis/redis.conf

# 绑定地址(如需远程访问则改为0.0.0.0)
bind 127.0.0.1# 端口
port 6379# 设置密码
requirepass 123456# 持久化配置
save 900 1
save 300 10
save 60 10000# AOF持久化
appendonly yes
appendfilename "appendonly.aof"# 最大内存(根据服务器配置调整)
maxmemory 2gb
maxmemory-policy allkeys-lru# 数据库数量
databases 16
  1. 启动Redis服务
sudo systemctl start redis-server
sudo systemctl enable redis-server
  1. 验证安装
redis-cli -a 123456 ping
# 返回PONG表示成功
8.2.3 RabbitMQ 消息队列安装

安装步骤:

  1. 安装Erlang(RabbitMQ依赖)
sudo apt install erlang
  1. 安装RabbitMQ
sudo apt install rabbitmq-server
  1. 启动RabbitMQ服务
sudo systemctl start rabbitmq-server
sudo systemctl enable rabbitmq-server
  1. 启用管理插件
sudo rabbitmq-plugins enable rabbitmq_management
  1. 安装延迟消息插件
cd /usr/lib/rabbitmq/lib/rabbitmq_server-<version>/plugins
wget https://github.com/rabbitmq/rabbitmq-delayed-message-exchange/releases/download/v3.12.0/rabbitmq_delayed_message_exchange-3.12.0.ez
sudo rabbitmq-plugins enable rabbitmq_delayed_message_exchange
sudo systemctl restart rabbitmq-server
  1. 创建用户和虚拟主机
# 添加用户
sudo rabbitmqctl add_user yemiao 147k369# 设置角色
sudo rabbitmqctl set_user_tags yemiao administrator# 创建虚拟主机
sudo rabbitmqctl add_vhost /ymcloud# 授权
sudo rabbitmqctl set_permissions -p /ymcloud yemiao ".*" ".*" ".*"
  1. 访问管理界面
  • URL:http://服务器IP:15672
  • 用户名:yemiao
  • 密码:147k369ym
8.2.4 Docker 容器引擎安装

安装步骤:

  1. 卸载旧版本
sudo apt remove docker docker-engine docker.io containerd runc
  1. 安装依赖
sudo apt install apt-transport-https ca-certificates curl gnupg lsb-release
  1. 添加Docker官方GPG密钥
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
  1. 设置稳定版仓库
echo "deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
  1. 安装Docker Engine
sudo apt update
sudo apt install docker-ce docker-ce-cli containerd.io
  1. 启动Docker服务
sudo systemctl start docker
sudo systemctl enable docker
  1. 验证安装
sudo docker run hello-world
  1. 将当前用户添加到docker组(可选)
sudo usermod -aG docker $USER
newgrp docker

拉取判题所需镜像:

# Java镜像
docker pull openjdk:17-slim# C/C++镜像
docker pull gcc:latest# Python镜像
docker pull python:3.11-slim

8.3 后端项目部署

8.3.1 项目编译打包

环境变量配置:

确保已安装JDK 17和Maven 3.8+:

java -version
mvn -version

克隆项目代码:

git clone <项目仓库地址>
cd ymcloud

修改生产环境配置:

编辑 ymcloud-web/src/main/resources/application-prod.yml

# 文件路径配置
ymcloud:file:base-dir: /home/ymcloud/files  # 文件存储路径docker:host-working-dir: /home/ymcloud/files/tmp  # Docker工作目录container-working-dir: /app# 数据库配置
spring:datasource:url: jdbc:mysql://localhost:3306/ymcloud?useUnicode=true&characterEncoding=utf8username: yemiaopassword: your_password  # 修改为实际密码# Redis配置data:redis:host: localhostport: 6379password: 123456  # 修改为实际密码database: 3# RabbitMQ配置rabbitmq:host: localhostport: 5672virtual-host: /ymcloudusername: yemiaopassword: 1234  # 修改为实际密码# Token配置
token:secret: your_secret_key  # 修改为随机字符串expireTime: 30# 邮箱配置
mail:from: your_email@example.compass: your_email_password

执行Maven打包:

# 跳过测试打包
mvn clean package -DskipTests# 或者执行测试后打包
mvn clean package

打包成功后,在 ymcloud-web/target/ 目录下会生成 ymcloud-web-0.0.1-SNAPSHOT.jar

8.3.2 应用启动方式

方式一:命令行直接启动(开发测试)

cd ymcloud-web/target
java -jar ymcloud-web-0.0.1-SNAPSHOT.jar

方式二:后台运行(生产环境)

nohup java -jar ymcloud-web-0.0.1-SNAPSHOT.jar > app.log 2>&1 &

方式三:使用systemd服务管理(推荐)

创建服务文件 /etc/systemd/system/ymcloud.service

[Unit]
Description=YMCloud Online Judge System
After=network.target mysql.service redis.service rabbitmq-server.service[Service]
Type=simple
User=ymcloud
WorkingDirectory=/home/ymcloud/app
ExecStart=/usr/bin/java -jar -Dspring.profiles.active=prod /home/ymcloud/app/ymcloud-web-0.0.1-SNAPSHOT.jar
Restart=on-failure
RestartSec=10# JVM参数
Environment="JAVA_OPTS=-Xms1g -Xmx2g -XX:+UseG1GC"# 日志输出
StandardOutput=journal
StandardError=journal[Install]
WantedBy=multi-user.target

启动服务:

# 重载systemd配置
sudo systemctl daemon-reload# 启动服务
sudo systemctl start ymcloud# 设置开机自启
sudo systemctl enable ymcloud# 查看状态
sudo systemctl status ymcloud# 查看日志
sudo journalctl -u ymcloud -f
8.3.3 JVM 参数优化

常用JVM参数配置:

java -jar \-Xms2g \                          # 初始堆内存2G-Xmx4g \                          # 最大堆内存4G-XX:+UseG1GC \                    # 使用G1垃圾回收器-XX:MaxGCPauseMillis=200 \        # GC最大暂停时间-XX:+HeapDumpOnOutOfMemoryError \ # OOM时生成堆转储-XX:HeapDumpPath=/var/log/ymcloud/heap_dump.hprof \-Dspring.profiles.active=prod \   # 激活生产环境配置ymcloud-web-0.0.1-SNAPSHOT.jar

内存分配建议:

  • 小型部署(<100并发):-Xms1g -Xmx2g
  • 中型部署(100-500并发):-Xms2g -Xmx4g
  • 大型部署(>500并发):-Xms4g -Xmx8g

8.4 前端项目部署

8.4.1 前端项目构建

安装Node.js和npm:

# 使用NodeSource仓库安装Node.js 18
curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash -
sudo apt install nodejs# 验证安装
node -v
npm -v

构建前端项目:

cd ymcloud-vue# 安装依赖
npm install# 生产环境构建
npm run build

构建完成后,dist/ 目录包含所有静态文件。

8.4.2 Nginx 配置

安装Nginx:

sudo apt install nginx

创建Nginx配置文件:

创建 /etc/nginx/sites-available/ymcloud

# 上游服务器配置
upstream ymcloud_backend {server 127.0.0.1:9090;keepalive 32;
}server {listen 80;server_name your_domain.com;  # 修改为实际域名或IP# 前端静态资源root /var/www/ymcloud/dist;index index.html;# Gzip压缩gzip on;gzip_vary on;gzip_min_length 1024;gzip_types text/plain text/css text/xml text/javascript application/javascript application/json application/xml+rss;# 前端路由(History模式)location / {try_files $uri $uri/ /index.html;add_header Cache-Control "no-cache";}# 静态资源缓存location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {expires 7d;add_header Cache-Control "public, immutable";}# 后端API代理location /api/ {proxy_pass http://ymcloud_backend/;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;# 超时配置proxy_connect_timeout 30s;proxy_send_timeout 30s;proxy_read_timeout 30s;# WebSocket支持(如需要)proxy_http_version 1.1;proxy_set_header Upgrade $http_upgrade;proxy_set_header Connection "upgrade";}# 文件服务location /files/ {alias /home/ymcloud/files/;autoindex off;add_header Cache-Control "public";expires 1h;}# API文档(生产环境建议关闭或限制访问)location /doc.html {proxy_pass http://ymcloud_backend/doc.html;}# 错误页面error_page 404 /404.html;error_page 500 502 503 504 /50x.html;
}

启用配置并重启Nginx:

# 创建软链接
sudo ln -s /etc/nginx/sites-available/ymcloud /etc/nginx/sites-enabled/# 测试配置
sudo nginx -t# 重启Nginx
sudo systemctl restart nginx

部署前端文件:

# 创建目录
sudo mkdir -p /var/www/ymcloud# 复制构建产物
sudo cp -r ymcloud-vue/dist/* /var/www/ymcloud/# 设置权限
sudo chown -R www-data:www-data /var/www/ymcloud
8.4.3 HTTPS 配置(可选)

使用Let’s Encrypt免费SSL证书:

# 安装Certbot
sudo apt install certbot python3-certbot-nginx# 自动配置HTTPS
sudo certbot --nginx -d your_domain.com# 自动续期
sudo certbot renew --dry-run

8.5 系统运维

8.5.1 日志管理

应用日志:

YMCloud使用Logback记录日志,日志文件位置可在配置文件中指定。

日志轮转配置:

创建 /etc/logrotate.d/ymcloud

/var/log/ymcloud/*.log {dailyrotate 30missingoknotifemptycompressdelaycompresscopytruncate
}

查看实时日志:

# systemd服务日志
sudo journalctl -u ymcloud -f# 应用日志
tail -f /var/log/ymcloud/app.log
8.5.2 监控与告警

系统监控指标:

  1. 服务可用性

    • 后端API健康检查
    • 前端页面访问
    • 数据库连接状态
  2. 性能指标

    • CPU使用率
    • 内存使用率
    • 磁盘IO
    • 网络流量
  3. 业务指标

    • 判题队列长度
    • 判题成功率
    • API响应时间
    • 在线用户数

推荐监控工具:

  • Prometheus + Grafana:指标采集与可视化
  • Spring Boot Actuator:应用健康检查
  • Druid监控:数据库连接池监控(已集成)
8.5.3 备份与恢复

数据库备份:

创建备份脚本 /home/ymcloud/backup_db.sh

#!/bin/bash
DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_DIR="/home/ymcloud/backups"
DB_NAME="ymcloud"
DB_USER="yemiao"
DB_PASS="your_password"# 创建备份目录
mkdir -p $BACKUP_DIR# 备份数据库
mysqldump -u$DB_USER -p$DB_PASS $DB_NAME > $BACKUP_DIR/ymcloud_$DATE.sql# 压缩备份
gzip $BACKUP_DIR/ymcloud_$DATE.sql# 删除30天前的备份
find $BACKUP_DIR -name "*.sql.gz" -mtime +30 -deleteecho "Backup completed: ymcloud_$DATE.sql.gz"

添加定时任务:

# 编辑crontab
crontab -e# 每天凌晨2点执行备份
0 2 * * * /home/ymcloud/backup_db.sh

文件备份:

# 备份文件目录
tar -czf /home/ymcloud/backups/files_$(date +%Y%m%d).tar.gz /home/ymcloud/files/

数据恢复:

# 恢复数据库
gunzip < ymcloud_20250101_020000.sql.gz | mysql -u yemiao -p ymcloud# 恢复文件
tar -xzf files_20250101.tar.gz -C /
8.5.4 故障处理

常见问题及解决方案:

问题1:应用启动失败

  • 检查端口占用netstat -tunlp | grep 9090
  • 查看日志sudo journalctl -u ymcloud -n 100
  • 检查依赖服务:确保MySQL、Redis、RabbitMQ已启动

问题2:判题异常

  • 检查Docker状态docker ps
  • 查看Docker日志docker logs <container_id>
  • 检查RabbitMQ队列:登录管理界面查看消息堆积情况
  • 检查磁盘空间df -h

问题3:数据库连接超时

  • 检查连接池配置:Druid连接池参数
  • 查看慢查询日志:优化SQL语句
  • 检查网络连接telnet localhost 3306

问题4:Redis连接失败

  • 检查Redis服务systemctl status redis-server
  • 验证密码redis-cli -a password ping
  • 检查防火墙sudo ufw status

问题5:内存溢出(OOM)

  • 分析堆转储文件:使用MAT工具
  • 调整JVM参数:增加堆内存
  • 排查内存泄漏:检查代码中的对象引用

8.6 性能调优

8.6.1 数据库调优

慢查询优化:

  1. 启用慢查询日志
  2. 分析慢查询SQL
  3. 添加合适的索引
  4. 优化查询语句
  5. 考虑读写分离

连接池优化:

根据并发量调整Druid连接池参数:

  • initial-size:初始连接数
  • min-idle:最小空闲连接
  • max-active:最大活跃连接
  • max-wait:获取连接最大等待时间
8.6.2 缓存优化

Redis优化:

  1. 合理设置过期时间:避免内存占用过大
  2. 使用合适的数据结构:String、Hash、List、Set、ZSet
  3. Pipeline批量操作:减少网络往返
  4. 避免大Key:单个Key不超过10KB

本地缓存:

使用Caffeine缓存系统配置、常量等不常变化的数据。

8.6.3 应用层优化

代码优化:

  • 异步处理耗时操作
  • 使用连接池复用连接
  • 避免大事务
  • 优化循环和递归

接口优化:

  • 添加接口限流
  • 实现接口缓存
  • 减少数据库查询
  • 返回必要字段

小结:本章详细介绍了 YMCloud 在线判题系统的部署方案。从环境要求、依赖服务安装、项目编译打包、应用启动、前端部署、Nginx配置到系统运维、监控告警、备份恢复和性能调优,覆盖了生产环境部署的完整流程。通过systemd服务管理、Nginx反向代理、日志轮转、定时备份等措施,确保系统稳定可靠运行。


第九章 项目总结与展望

本章对 YMCloud 在线判题系统进行全面总结,分析项目的技术亮点、实现难点、解决方案以及未来的优化方向和功能扩展计划。

9.1 项目技术总结

9.1.1 技术栈选型总结

后端技术栈:

YMCloud 后端采用了成熟稳定的技术组合:

  1. Spring Boot 3.5.3

    • 选型理由:生态成熟、社区活跃、开发效率高
    • 应用效果:快速搭建项目骨架,约定优于配置理念降低了学习成本
    • 经验总结:Spring Boot 3.x 对Jakarta EE的支持需要注意依赖包的兼容性
  2. Spring Security 6.5.1

    • 选型理由:强大的安全框架,支持多种认证方式
    • 应用效果:实现了JWT无状态认证、RBAC权限控制、方法级权限验证
    • 经验总结:过滤器链的配置需要注意顺序,权限注解要与数据库权限编码保持一致
  3. MyBatis-Plus 3.5.10

    • 选型理由:简化CRUD操作,提供强大的Lambda查询
    • 应用效果:大幅减少SQL代码量,链式查询提高代码可读性
    • 经验总结:分页插件配置要设置最大限制,避免单次查询过多数据
  4. RabbitMQ

    • 选型理由:成熟的消息队列,支持延迟消息
    • 应用效果:实现异步判题,解耦代码提交与判题执行
    • 经验总结:需要处理消息重复消费、消息丢失等问题,使用手动ACK更可靠
  5. Docker

    • 选型理由:容器化技术成熟,隔离性强
    • 应用效果:实现代码沙箱,每个提交独立运行,安全可靠
    • 经验总结:容器资源限制、网络隔离、文件系统保护缺一不可

前端技术栈:

  1. Vue 3 + TypeScript

    • 选型理由:组合式API逻辑清晰,TypeScript类型安全
    • 应用效果:代码可维护性强,IDE提示完善
    • 经验总结:TypeScript学习成本较高,但长期收益显著
  2. Pinia

    • 选型理由:轻量级状态管理,对TypeScript友好
    • 应用效果:状态管理简洁明了,Setup Store模式灵活
    • 经验总结:相比Vuex更加简单直观
  3. Vite

    • 选型理由:快速的开发体验,构建速度快
    • 应用效果:热更新秒级响应,开发效率提升明显
    • 经验总结:插件生态丰富,配置简单
9.1.2 架构设计总结

模块化设计:

Maven多模块架构实现了清晰的职责分离:

  • ymcloud-common:通用工具和常量,零业务依赖
  • ymcloud-pojo:数据模型统一管理,便于维护
  • ymcloud-core:核心配置和基础服务,避免配置分散
  • ymcloud-judge:判题模块独立,便于横向扩展
  • ymcloud-web:Web接口层,整合所有功能

优势体现:

  • 模块间依赖清晰,避免循环依赖
  • 功能内聚,易于理解和维护
  • 便于后续微服务化改造

分层架构:

经典的Controller-Service-Mapper三层架构:

  • Controller层:只负责接口暴露,不包含业务逻辑
  • Service层:业务逻辑实现,事务管理
  • Mapper层:数据访问,支持自定义SQL
  • EntityService层:单表CRUD封装,简化Service代码

优势体现:

  • 职责明确,代码结构清晰
  • 易于单元测试
  • 便于多人协作开发
9.1.3 核心功能实现总结

1. 异步判题系统

实现方案:RabbitMQ消息队列 + Docker容器沙箱

关键技术点

  • 消息队列解耦提交和判题
  • 延迟消息防止频繁提交
  • Docker容器实现代码隔离
  • Linux time 命令监控资源消耗
  • 判题结果Redis缓存供前端轮询

技术亮点

  • 支持高并发判题
  • 容器资源严格限制(内存、CPU、进程、网络)
  • 支持分布式部署

2. RBAC权限系统

实现方案:Spring Security + JWT + 动态路由

关键技术点

  • JWT Token无状态认证
  • 五表权限模型(User-UserRole-Role-RoleAuth-Auth)
  • 前后端权限同步
  • 动态路由加载

技术亮点

  • 细粒度权限控制(到按钮级别)
  • 支持多角色
  • 权限动态调整

3. 比赛管理系统

实现方案:多模式支持 + 实时排名

关键技术点

  • 三种赛制(ACM、OI、IOI)
  • 比赛权限控制(公开、私有、密码)
  • 排名算法(AC数、总分、罚时)
  • 榜单缓存优化

技术亮点

  • 支持多种竞赛模式
  • 实时排名计算
  • 比赛状态控制

9.2 实现难点与解决方案

9.2.1 Docker 沙箱安全设计

难点描述:

用户提交的代码可能包含恶意操作:

  • 无限循环占用CPU
  • Fork炸弹耗尽进程资源
  • 读写敏感文件破坏系统
  • 网络攻击
  • 超大内存分配

解决方案:

  1. 容器资源限制

    • 内存限制:--memory
    • CPU限制:--cpus
    • 进程数限制:--pids-limit
    • 禁用Swap:--memory-swap=0
  2. 网络隔离

    • 网络模式:--network=none
    • 禁止所有网络访问
  3. 文件系统保护

    • 只读根文件系统:--read-only
    • 工作目录挂载:--volume
    • 限制文件权限
  4. 执行超时控制

    • 时间限制:Linux timeout 命令
    • 容器自动销毁

效果评估:

  • 有效防止恶意代码攻击
  • 资源使用可控
  • 系统安全稳定
9.2.2 判题并发性能优化

难点描述:

比赛场景下可能出现大量代码同时提交,需要:

  • 快速响应用户提交
  • 高效处理判题任务
  • 避免系统过载

解决方案:

  1. 异步化处理

    • RabbitMQ消息队列缓冲任务
    • 前端轮询查询结果
    • 判题结果Redis临时缓存
  2. 容器池化

    • 预创建镜像
    • 复用容器(开发中)
    • 减少启动时间
  3. 负载均衡

    • 支持多判题节点
    • 消息队列天然负载均衡
    • 水平扩展能力
  4. 资源控制

    • RabbitMQ prefetch限制
    • 控制并发判题数量
    • 防止系统过载

效果评估:

  • 单节点支持100+并发判题
  • 响应时间<2秒
  • 可横向扩展
9.2.3 权限动态加载实现

难点描述:

管理端需要根据用户权限动态加载菜单和路由:

  • 前端不知道用户有哪些权限
  • 路由需要动态添加
  • 菜单需要动态渲染

解决方案:

  1. 后端返回权限数据

    • 用户登录后查询权限
    • 存储在JWT Token或Redis中
    • 前端请求时返回菜单结构
  2. 前端动态路由

    • 路由导航守卫拦截
    • 判断是否加载动态路由
    • router.addRoute() 动态添加
    • 重新跳转确保路由生效
  3. 菜单递归渲染

    • 递归组件渲染多级菜单
    • 根据路由结构生成菜单
    • 支持图标、名称等配置

效果评估:

  • 权限控制灵活
  • 支持运行时权限调整
  • 前后端权限一致
9.2.4 测试数据管理

难点描述:

题目测试数据管理复杂:

  • 多组测试用例
  • 输入输出文件匹配
  • 文件上传下载
  • 存储路径管理

解决方案:

  1. 文件命名规范

    • 输入文件:1.in2.in、…
    • 输出文件:1.out2.out、…
    • 配置文件:info.json
  2. 目录结构

    files/private/problem/{problemId}/├── 1.in├── 1.out├── 2.in├── 2.out└── info.json
    
  3. 上传处理

    • 批量文件上传
    • 自动解压ZIP文件
    • 验证文件格式
    • 更新数据库记录
  4. 判题使用

    • 读取info.json获取测试用例列表
    • 顺序执行每组测试用例
    • 对比输出结果

效果评估:

  • 文件管理清晰
  • 上传操作便捷
  • 判题逻辑简洁

9.3 项目亮点与创新

9.3.1 技术亮点
  1. Docker容器化判题

    • 与传统沙箱相比,隔离性更强
    • 支持多种编程语言
    • 资源限制更精确
    • 易于扩展到分布式
  2. 异步化架构设计

    • RabbitMQ解耦提交和判题
    • 延迟消息防止频繁提交
    • 支持高并发场景
    • 提升用户体验
  3. 细粒度权限控制

    • 五表RBAC模型
    • 方法级权限验证
    • 动态路由加载
    • 权限与菜单分离
  4. 前后端分离架构

    • RESTful API设计
    • JWT无状态认证
    • 独立部署扩展
    • 提升开发效率
9.3.2 工程亮点
  1. 模块化设计

    • Maven多模块管理
    • 职责清晰分离
    • 易于理解维护
    • 支持微服务化
  2. 代码规范性

    • 统一异常处理
    • 参数校验完善
    • 日志记录规范
    • 注释文档完整
  3. 可扩展性强

    • 支持多判题节点
    • 数据库分表预留
    • 缓存层级设计
    • 插件化扩展
  4. 用户体验优化

    • 代码编辑器集成
    • 实时结果轮询
    • Markdown渲染
    • 响应式设计

9.4 不足与改进方向

9.4.1 当前不足
  1. 微服务化程度不足

    • 当前为单体应用
    • 虽已模块化,但未完全拆分
    • 扩展受单机资源限制
  2. 监控告警不完善

    • 缺少完整的监控体系
    • 告警机制待完善
    • 日志分析工具缺失
  3. 测试覆盖率较低

    • 单元测试不完善
    • 集成测试缺失
    • 压力测试未进行
  4. 部分功能待完善

    • 讨论区功能未实现
    • 用户间私信未实现
    • 题解系统未实现
    • 数据统计图表较少
9.4.2 未来优化方向

1. 微服务化改造

目标:提升系统的可扩展性和可维护性

改造方案

  • 用户服务:用户管理、权限认证
  • 题目服务:题目管理、标签分类
  • 判题服务:代码判题、沙箱管理(已独立模块)
  • 比赛服务:比赛管理、排名计算
  • 网关服务:统一入口、路由转发

技术选型

  • Spring Cloud Alibaba:微服务框架
  • Nacos:服务注册与配置中心
  • Gateway:API网关
  • Sentinel:限流熔断
  • Seata:分布式事务

2. 性能优化

数据库优化

  • 读写分离(主从复制)
  • 分库分表(judge表、problem表)
  • 索引优化(定期分析慢查询)
  • 查询缓存(Redis缓存热点数据)

缓存优化

  • 多级缓存(Caffeine + Redis)
  • 缓存预热
  • 缓存更新策略优化
  • 布隆过滤器防止穿透

容器优化

  • 容器池化(复用容器)
  • 镜像精简(减小镜像体积)
  • 资源动态调整
  • 判题节点自动扩容

3. 功能扩展

题解系统

  • 用户发布题解
  • Markdown编辑器
  • 题解点赞评论
  • 优质题解推荐

讨论区功能

  • 题目讨论区
  • 发帖回帖功能
  • 内容审核机制
  • 敏感词过滤

数据统计

  • 用户能力图谱
  • 题目通过率统计
  • 热门题目推荐
  • 学习进度跟踪

在线IDE增强

  • 多文件编辑
  • 调试功能
  • 代码格式化
  • 智能补全

4. 监控与运维

监控体系建设

  • 应用监控:Spring Boot Actuator + Prometheus
  • 日志分析:ELK(Elasticsearch + Logstash + Kibana)
  • 链路追踪:SkyWalking
  • 告警通知:钉钉、邮件、短信

自动化运维

  • CI/CD流程(Jenkins、GitLab CI)
  • 容器编排(Kubernetes)
  • 自动扩缩容
  • 灰度发布

5. 安全加固

接口安全

  • 接口加密(敏感数据)
  • 签名验证
  • 防重放攻击
  • WAF防护

数据安全

  • 敏感数据脱敏
  • 数据库加密
  • 定期安全审计
  • 漏洞扫描

9.5 项目价值与意义

9.5.1 技术价值
  1. 技术架构实践

    • 验证了前后端分离架构的可行性
    • 探索了Docker容器在代码沙箱中的应用
    • 实现了完整的RBAC权限控制系统
    • 积累了微服务前期模块化设计经验
  2. 技术能力提升

    • 掌握了Spring Boot全家桶的使用
    • 深入理解了Docker容器技术
    • 熟练运用消息队列解决异步问题
    • 提升了系统架构设计能力
9.5.2 应用价值
  1. 教育场景

    • 可用于高校编程课程
    • 支持作业在线提交和自动批改
    • 减轻教师批改负担
    • 提供即时反馈
  2. 竞赛场景

    • 支持ACM、OI等竞赛模式
    • 实时排名系统
    • 公平公正的评测环境
  3. 自学场景

    • 提供算法练习平台
    • 题目分类和标签管理
    • 提交历史记录
    • 错题回顾
9.5.3 社会价值
  1. 降低教育成本

    • 减少硬件投入
    • 在线学习不受地域限制
    • 资源共享
  2. 提升学习效率

    • 即时反馈
    • 自主学习
    • 碎片化学习
  3. 促进公平教育

    • 开源免费
    • 降低学习门槛
    • 教育资源均衡化

9.6 个人收获与感悟

9.6.1 技术成长

通过本项目的开发,在以下方面获得了显著提升:

  1. 全栈开发能力

    • 独立完成前后端开发
    • 掌握完整的项目开发流程
    • 积累了丰富的实战经验
  2. 架构设计能力

    • 理解了分层架构的设计思想
    • 掌握了模块化设计方法
    • 学会了权衡各种技术选型
  3. 问题解决能力

    • 遇到问题能够快速定位
    • 善于查阅文档和资料
    • 积累了大量实战经验
  4. 工程实践能力

    • 规范的代码风格
    • 完善的文档编写
    • 系统的测试方法
9.6.2 项目管理经验
  1. 需求分析:深入理解用户需求,避免盲目开发
  2. 任务拆分:大项目分解为小任务,逐步实现
  3. 版本控制:Git规范使用,分支管理
  4. 文档管理:及时记录设计思路和技术决策
9.6.3 未来展望

个人技术方向:

  • 深入学习微服务架构
  • 研究分布式系统设计
  • 探索云原生技术栈
  • 提升系统性能优化能力

项目发展方向:

  • 完善功能模块
  • 优化用户体验
  • 提升系统性能
  • 扩展应用场景

小结:本章对 YMCloud 在线判题系统进行了全面总结。从技术选型、架构设计、核心功能实现到实现难点、解决方案、项目亮点进行了深入分析;指出了当前的不足和未来的优化方向,包括微服务化改造、性能优化、功能扩展、监控运维和安全加固;阐述了项目的技术价值、应用价值和社会价值,以及个人在项目中的收获与成长。YMCloud 项目是一次完整的全栈开发实践,为后续的技术深入和项目优化奠定了坚实基础。


结语

YMCloud 在线判题系统从需求分析、架构设计、功能实现到系统部署,是一次完整的软件工程实践。项目采用了前后端分离架构、Maven多模块设计、Docker容器化技术、消息队列异步处理等现代化技术方案,实现了一个安全、高效、可扩展的在线判题平台。

项目的成功离不开对技术的深入理解和持续学习。在开发过程中,遇到了许多技术难题,通过查阅资料、实验验证、反复优化,最终找到了合适的解决方案。这些经验和教训都成为了宝贵的财富。

展望未来,YMCloud 还有很大的优化和扩展空间。微服务化改造、性能优化、功能完善、监控体系建设等工作都在规划中。相信随着技术的不断进步和项目的持续迭代,YMCloud 将会成为一个更加完善、更加强大的在线判题平台。

感谢每一位支持和关注 YMCloud 项目的朋友!


参考文献

  1. Spring Boot官方文档. https://spring.io/projects/spring-boot
  2. Spring Security官方文档. https://spring.io/projects/spring-security
  3. MyBatis-Plus官方文档. https://baomidou.com/
  4. Docker官方文档. https://docs.docker.com/
  5. RabbitMQ官方文档. https://www.rabbitmq.com/documentation.html
  6. Vue 3官方文档. https://vuejs.org/
  7. TypeScript官方文档. https://www.typescriptlang.org/
  8. Pinia官方文档. https://pinia.vuejs.org/
  9. Element Plus官方文档. https://element-plus.org/
  10. Naive UI官方文档. https://www.naiveui.com/

文档编写完成时间:2025年10月

项目骨架,约定优于配置理念降低了学习成本

  • 经验总结:Spring Boot 3.x 对Jakarta EE的支持需要注意依赖包的兼容性
  1. Spring Security 6.5.1

    • 选型理由:强大的安全框架,支持多种认证方式
    • 应用效果:实现了JWT无状态认证、RBAC权限控制、方法级权限验证
    • 经验总结:过滤器链的配置需要注意顺序,权限注解要与数据库权限编码保持一致
  2. MyBatis-Plus 3.5.10

    • 选型理由:简化CRUD操作,提供强大的Lambda查询
    • 应用效果:大幅减少SQL代码量,链式查询提高代码可读性
    • 经验总结:分页插件配置要设置最大限制,避免单次查询过多数据
  3. RabbitMQ

    • 选型理由:成熟的消息队列,支持延迟消息
    • 应用效果:实现异步判题,解耦代码提交与判题执行
    • 经验总结:需要处理消息重复消费、消息丢失等问题,使用手动ACK更可靠
  4. Docker

    • 选型理由:容器化技术成熟,隔离性强
    • 应用效果:实现代码沙箱,每个提交独立运行,安全可靠
    • 经验总结:容器资源限制、网络隔离、文件系统保护缺一不可

前端技术栈:

  1. Vue 3 + TypeScript

    • 选型理由:组合式API逻辑清晰,TypeScript类型安全
    • 应用效果:代码可维护性强,IDE提示完善
    • 经验总结:TypeScript学习成本较高,但长期收益显著
  2. Pinia

    • 选型理由:轻量级状态管理,对TypeScript友好
    • 应用效果:状态管理简洁明了,Setup Store模式灵活
    • 经验总结:相比Vuex更加简单直观
  3. Vite

    • 选型理由:快速的开发体验,构建速度快
    • 应用效果:热更新秒级响应,开发效率提升明显
    • 经验总结:插件生态丰富,配置简单
9.1.2 架构设计总结

模块化设计:

Maven多模块架构实现了清晰的职责分离:

  • ymcloud-common:通用工具和常量,零业务依赖
  • ymcloud-pojo:数据模型统一管理,便于维护
  • ymcloud-core:核心配置和基础服务,避免配置分散
  • ymcloud-judge:判题模块独立,便于横向扩展
  • ymcloud-web:Web接口层,整合所有功能

优势体现:

  • 模块间依赖清晰,避免循环依赖
  • 功能内聚,易于理解和维护
  • 便于后续微服务化改造

分层架构:

经典的Controller-Service-Mapper三层架构:

  • Controller层:只负责接口暴露,不包含业务逻辑
  • Service层:业务逻辑实现,事务管理
  • Mapper层:数据访问,支持自定义SQL
  • EntityService层:单表CRUD封装,简化Service代码

优势体现:

  • 职责明确,代码结构清晰
  • 易于单元测试
  • 便于多人协作开发
9.1.3 核心功能实现总结

1. 异步判题系统

实现方案:RabbitMQ消息队列 + Docker容器沙箱

关键技术点

  • 消息队列解耦提交和判题
  • 延迟消息防止频繁提交
  • Docker容器实现代码隔离
  • Linux time 命令监控资源消耗
  • 判题结果Redis缓存供前端轮询

技术亮点

  • 支持高并发判题
  • 容器资源严格限制(内存、CPU、进程、网络)
  • 支持分布式部署

2. RBAC权限系统

实现方案:Spring Security + JWT + 动态路由

关键技术点

  • JWT Token无状态认证
  • 五表权限模型(User-UserRole-Role-RoleAuth-Auth)
  • 前后端权限同步
  • 动态路由加载

技术亮点

  • 细粒度权限控制(到按钮级别)
  • 支持多角色
  • 权限动态调整

3. 比赛管理系统

实现方案:多模式支持 + 实时排名

关键技术点

  • 三种赛制(ACM、OI、IOI)
  • 比赛权限控制(公开、私有、密码)
  • 排名算法(AC数、总分、罚时)
  • 榜单缓存优化

技术亮点

  • 支持多种竞赛模式
  • 实时排名计算
  • 比赛状态控制

9.2 实现难点与解决方案

9.2.1 Docker 沙箱安全设计

难点描述:

用户提交的代码可能包含恶意操作:

  • 无限循环占用CPU
  • Fork炸弹耗尽进程资源
  • 读写敏感文件破坏系统
  • 网络攻击
  • 超大内存分配

解决方案:

  1. 容器资源限制

    • 内存限制:--memory
    • CPU限制:--cpus
    • 进程数限制:--pids-limit
    • 禁用Swap:--memory-swap=0
  2. 网络隔离

    • 网络模式:--network=none
    • 禁止所有网络访问
  3. 文件系统保护

    • 只读根文件系统:--read-only
    • 工作目录挂载:--volume
    • 限制文件权限
  4. 执行超时控制

    • 时间限制:Linux timeout 命令
    • 容器自动销毁

效果评估:

  • 有效防止恶意代码攻击
  • 资源使用可控
  • 系统安全稳定
9.2.2 判题并发性能优化

难点描述:

比赛场景下可能出现大量代码同时提交,需要:

  • 快速响应用户提交
  • 高效处理判题任务
  • 避免系统过载

解决方案:

  1. 异步化处理

    • RabbitMQ消息队列缓冲任务
    • 前端轮询查询结果
    • 判题结果Redis临时缓存
  2. 容器池化

    • 预创建镜像
    • 复用容器(开发中)
    • 减少启动时间
  3. 负载均衡

    • 支持多判题节点
    • 消息队列天然负载均衡
    • 水平扩展能力
  4. 资源控制

    • RabbitMQ prefetch限制
    • 控制并发判题数量
    • 防止系统过载

效果评估:

  • 单节点支持100+并发判题
  • 响应时间<2秒
  • 可横向扩展
9.2.3 权限动态加载实现

难点描述:

管理端需要根据用户权限动态加载菜单和路由:

  • 前端不知道用户有哪些权限
  • 路由需要动态添加
  • 菜单需要动态渲染

解决方案:

  1. 后端返回权限数据

    • 用户登录后查询权限
    • 存储在JWT Token或Redis中
    • 前端请求时返回菜单结构
  2. 前端动态路由

    • 路由导航守卫拦截
    • 判断是否加载动态路由
    • router.addRoute() 动态添加
    • 重新跳转确保路由生效
  3. 菜单递归渲染

    • 递归组件渲染多级菜单
    • 根据路由结构生成菜单
    • 支持图标、名称等配置

效果评估:

  • 权限控制灵活
  • 支持运行时权限调整
  • 前后端权限一致
9.2.4 测试数据管理

难点描述:

题目测试数据管理复杂:

  • 多组测试用例
  • 输入输出文件匹配
  • 文件上传下载
  • 存储路径管理

解决方案:

  1. 文件命名规范

    • 输入文件:1.in2.in、…
    • 输出文件:1.out2.out、…
    • 配置文件:info.json
  2. 目录结构

    files/private/problem/{problemId}/├── 1.in├── 1.out├── 2.in├── 2.out└── info.json
    
  3. 上传处理

    • 批量文件上传
    • 自动解压ZIP文件
    • 验证文件格式
    • 更新数据库记录
  4. 判题使用

    • 读取info.json获取测试用例列表
    • 顺序执行每组测试用例
    • 对比输出结果

效果评估:

  • 文件管理清晰
  • 上传操作便捷
  • 判题逻辑简洁

9.3 项目亮点与创新

9.3.1 技术亮点
  1. Docker容器化判题

    • 与传统沙箱相比,隔离性更强
    • 支持多种编程语言
    • 资源限制更精确
    • 易于扩展到分布式
  2. 异步化架构设计

    • RabbitMQ解耦提交和判题
    • 延迟消息防止频繁提交
    • 支持高并发场景
    • 提升用户体验
  3. 细粒度权限控制

    • 五表RBAC模型
    • 方法级权限验证
    • 动态路由加载
    • 权限与菜单分离
  4. 前后端分离架构

    • RESTful API设计
    • JWT无状态认证
    • 独立部署扩展
    • 提升开发效率
9.3.2 工程亮点
  1. 模块化设计

    • Maven多模块管理
    • 职责清晰分离
    • 易于理解维护
    • 支持微服务化
  2. 代码规范性

    • 统一异常处理
    • 参数校验完善
    • 日志记录规范
    • 注释文档完整
  3. 可扩展性强

    • 支持多判题节点
    • 数据库分表预留
    • 缓存层级设计
    • 插件化扩展
  4. 用户体验优化

    • 代码编辑器集成
    • 实时结果轮询
    • Markdown渲染
    • 响应式设计

9.4 不足与改进方向

9.4.1 当前不足
  1. 微服务化程度不足

    • 当前为单体应用
    • 虽已模块化,但未完全拆分
    • 扩展受单机资源限制
  2. 监控告警不完善

    • 缺少完整的监控体系
    • 告警机制待完善
    • 日志分析工具缺失
  3. 测试覆盖率较低

    • 单元测试不完善
    • 集成测试缺失
    • 压力测试未进行
  4. 部分功能待完善

    • 讨论区功能未实现
    • 用户间私信未实现
    • 题解系统未实现
    • 数据统计图表较少
9.4.2 未来优化方向

1. 微服务化改造

目标:提升系统的可扩展性和可维护性

改造方案

  • 用户服务:用户管理、权限认证
  • 题目服务:题目管理、标签分类
  • 判题服务:代码判题、沙箱管理(已独立模块)
  • 比赛服务:比赛管理、排名计算
  • 网关服务:统一入口、路由转发

技术选型

  • Spring Cloud Alibaba:微服务框架
  • Nacos:服务注册与配置中心
  • Gateway:API网关
  • Sentinel:限流熔断
  • Seata:分布式事务

2. 性能优化

数据库优化

  • 读写分离(主从复制)
  • 分库分表(judge表、problem表)
  • 索引优化(定期分析慢查询)
  • 查询缓存(Redis缓存热点数据)

缓存优化

  • 多级缓存(Caffeine + Redis)
  • 缓存预热
  • 缓存更新策略优化
  • 布隆过滤器防止穿透

容器优化

  • 容器池化(复用容器)
  • 镜像精简(减小镜像体积)
  • 资源动态调整
  • 判题节点自动扩容

3. 功能扩展

题解系统

  • 用户发布题解
  • Markdown编辑器
  • 题解点赞评论
  • 优质题解推荐

讨论区功能

  • 题目讨论区
  • 发帖回帖功能
  • 内容审核机制
  • 敏感词过滤

数据统计

  • 用户能力图谱
  • 题目通过率统计
  • 热门题目推荐
  • 学习进度跟踪

在线IDE增强

  • 多文件编辑
  • 调试功能
  • 代码格式化
  • 智能补全

4. 监控与运维

监控体系建设

  • 应用监控:Spring Boot Actuator + Prometheus
  • 日志分析:ELK(Elasticsearch + Logstash + Kibana)
  • 链路追踪:SkyWalking
  • 告警通知:钉钉、邮件、短信

自动化运维

  • CI/CD流程(Jenkins、GitLab CI)
  • 容器编排(Kubernetes)
  • 自动扩缩容
  • 灰度发布

5. 安全加固

接口安全

  • 接口加密(敏感数据)
  • 签名验证
  • 防重放攻击
  • WAF防护

数据安全

  • 敏感数据脱敏
  • 数据库加密
  • 定期安全审计
  • 漏洞扫描

9.5 项目价值与意义

9.5.1 技术价值
  1. 技术架构实践

    • 验证了前后端分离架构的可行性
    • 探索了Docker容器在代码沙箱中的应用
    • 实现了完整的RBAC权限控制系统
    • 积累了微服务前期模块化设计经验
  2. 技术能力提升

    • 掌握了Spring Boot全家桶的使用
    • 深入理解了Docker容器技术
    • 熟练运用消息队列解决异步问题
    • 提升了系统架构设计能力
9.5.2 应用价值
  1. 教育场景

    • 可用于高校编程课程
    • 支持作业在线提交和自动批改
    • 减轻教师批改负担
    • 提供即时反馈
  2. 竞赛场景

    • 支持ACM、OI等竞赛模式
    • 实时排名系统
    • 公平公正的评测环境
  3. 自学场景

    • 提供算法练习平台
    • 题目分类和标签管理
    • 提交历史记录
    • 错题回顾
9.5.3 社会价值
  1. 降低教育成本

    • 减少硬件投入
    • 在线学习不受地域限制
    • 资源共享
  2. 提升学习效率

    • 即时反馈
    • 自主学习
    • 碎片化学习
  3. 促进公平教育

    • 开源免费
    • 降低学习门槛
    • 教育资源均衡化

9.6 个人收获与感悟

9.6.1 技术成长

通过本项目的开发,在以下方面获得了显著提升:

  1. 全栈开发能力

    • 独立完成前后端开发
    • 掌握完整的项目开发流程
    • 积累了丰富的实战经验
  2. 架构设计能力

    • 理解了分层架构的设计思想
    • 掌握了模块化设计方法
    • 学会了权衡各种技术选型
  3. 问题解决能力

    • 遇到问题能够快速定位
    • 善于查阅文档和资料
    • 积累了大量实战经验
  4. 工程实践能力

    • 规范的代码风格
    • 完善的文档编写
    • 系统的测试方法
9.6.2 项目管理经验
  1. 需求分析:深入理解用户需求,避免盲目开发
  2. 任务拆分:大项目分解为小任务,逐步实现
  3. 版本控制:Git规范使用,分支管理
  4. 文档管理:及时记录设计思路和技术决策
9.6.3 未来展望

个人技术方向:

  • 深入学习微服务架构
  • 研究分布式系统设计
  • 探索云原生技术栈
  • 提升系统性能优化能力

项目发展方向:

  • 完善功能模块
  • 优化用户体验
  • 提升系统性能
  • 扩展应用场景

小结:本章对 YMCloud 在线判题系统进行了全面总结。从技术选型、架构设计、核心功能实现到实现难点、解决方案、项目亮点进行了深入分析;指出了当前的不足和未来的优化方向,包括微服务化改造、性能优化、功能扩展、监控运维和安全加固;阐述了项目的技术价值、应用价值和社会价值,以及个人在项目中的收获与成长。YMCloud 项目是一次完整的全栈开发实践,为后续的技术深入和项目优化奠定了坚实基础。


结语

YMCloud 在线判题系统从需求分析、架构设计、功能实现到系统部署,是一次完整的软件工程实践。项目采用了前后端分离架构、Maven多模块设计、Docker容器化技术、消息队列异步处理等现代化技术方案,实现了一个安全、高效、可扩展的在线判题平台。

项目的成功离不开对技术的深入理解和持续学习。在开发过程中,遇到了许多技术难题,通过查阅资料、实验验证、反复优化,最终找到了合适的解决方案。这些经验和教训都成为了宝贵的财富。

展望未来,YMCloud 还有很大的优化和扩展空间。微服务化改造、性能优化、功能完善、监控体系建设等工作都在规划中。相信随着技术的不断进步和项目的持续迭代,YMCloud 将会成为一个更加完善、更加强大的在线判题平台。

感谢每一位支持和关注 YMCloud 项目的朋友!


参考文献

  1. Spring Boot官方文档. https://spring.io/projects/spring-boot
  2. Spring Security官方文档. https://spring.io/projects/spring-security
  3. MyBatis-Plus官方文档. https://baomidou.com/
  4. Docker官方文档. https://docs.docker.com/
  5. RabbitMQ官方文档. https://www.rabbitmq.com/documentation.html
  6. Vue 3官方文档. https://vuejs.org/
  7. TypeScript官方文档. https://www.typescriptlang.org/
  8. Pinia官方文档. https://pinia.vuejs.org/
  9. Element Plus官方文档. https://element-plus.org/
  10. Naive UI官方文档. https://www.naiveui.com/

文档编写完成时间:2025年10月

文档版本:v1.0

http://www.dtcms.com/a/443143.html

相关文章:

  • 长沙自适应网站制作wordpress 页面 插件
  • 徐州的网站设计公司企业管理
  • 企业网站优化方案模板学做php网站有哪些
  • 网站建设需准备什么软件北京网站搭建开发
  • 网站稳定期怎么做海外精品网站建设
  • apache 配置网站公司域名注册查询
  • 浅谈高校图书馆网站建设公司装修费分几年摊销
  • 从工具到语境:Anthropic 双文启示下的 AI 代理工程实践心得
  • 做5173这样的网站要多少人5 网站建设进度表
  • 广东专业的网站制作江门cms模板建站
  • 教育机构网站制作模板宁波seo外包代运营
  • 网站建设的作用和用途做网站 学什么
  • 成都网站推广优化公司形容网站做的好处
  • Java学习笔记Day15
  • 湘潭自助建站系统展览馆设计公司排名
  • 电子商务网站规划建设方案网络销售的方法和技巧
  • 阿里巴巴网站的营销策略专用主机网站建设
  • 网站设计制作程序网站策划选题
  • 长沙手机网站公司开发项目的流程
  • 初识MYSQL —— 库和表的操作
  • 怎样做网站推广自己网站建设要维护
  • 常州网站建设公司报价网站安全监测
  • 网站建设的中期报告网页qq登陆聊天
  • 什么网站做执法仪商业网站开发设计报告
  • 海南省建设局网站搜索咋样做班级主页网站
  • 成品网站源码1688体验区网站图片列表怎么做
  • 网站建站网站设计以绿色为主的网站
  • 嘉兴免费网站建站模板化工类 网站模板
  • 网站建设整个流程图威联通怎么建设网站
  • Spring AI 从入门到实战-目录