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

智慧判官-分布式编程评测平台

源码链接:https://github.com/kayden-0516/CodeBench-Distributed

一句话摘要: 本项目是一个使用 Spring Cloud Alibaba + Vue3 开发的在线编程评测系统,旨在解决编程学习者缺乏实践平台的核心痛点,实现了从题库管理、竞赛组织到代码自动评测的全流程服务。

技术栈: Spring Cloud Alibaba | Vue3 | MySQL | Redis | Docker | Nacos | RabbitMQ

引言:为什么我要做这个项目?

  • 遇到的痛点:在编程学习过程中,我发现现有的在线评测平台要么功能过于简单,要么架构陈旧难以满足高并发需求。特别是对于想要学习微服务架构的开发者来说,缺乏一个完整的、现代化的实战项目参考。

  • 项目目标: 因此,我决定开发一个在线OJ系统,它能够提供完整的编程题目练习、竞赛组织和自动评测功能,同时采用现代化的微服务架构,帮助编程学习者获得更好的练习体验,也为开发者提供一个完整的企业级项目参考。

1. 项目概述与背景

1.1 项目背景与市场需求

在线评测系统(Online Judge)起源于ACM国际大学生程序设计竞赛,现已发展成为编程教育、技术面试、技能评估的重要平台。随着数字化转型加速,企业对程序员的技术评估需求日益增长,在线OJ系统成为连接学习者、教育机构和企业的重要桥梁。

市场分析

  • 教育市场:高校计算机课程实践平台、编程培训机构教学工具

  • 企业市场:技术面试筛选、内部技能评估、技术竞赛举办

  • 个人市场:编程爱好者技能提升、求职准备、技术交流

用户画像

1.2 项目目标与核心价值

业务目标

  • 构建稳定可靠的在线编程评测平台

  • 支持大规模并发代码提交和评测

  • 提供完整的编程学习路径和竞赛体系

  • 实现企业级的技术评估解决方案

技术目标

  • 采用微服务架构,保证系统可扩展性

  • 实现前后端分离,提升开发效率

  • 集成现代化开发工具和流程

  • 确保系统安全性和高性能

2. 项目开发全流程详解

2.1 立项与需求分析阶段

2.1.1 需求收集方法论

多维度需求收集

  1. 用户访谈深度分析

// 用户需求分析模型
public class UserRequirement {private String userType;          // 用户类型private String usageScenario;     // 使用场景private String corePainPoint;     // 核心痛点private String expectedSolution;  // 期望解决方案private Integer priority;         // 需求优先级
}// 典型用户需求示例
List<UserRequirement> requirements = Arrays.asList(new UserRequirement("在校学生", "课程作业", "代码运行环境配置复杂", "在线代码执行", 1),new UserRequirement求职者", "面试准备", "缺乏真实编程环境", "模拟面试题库", 2),new UserRequirement("企业HR", "人才筛选", "技术评估效率低", "自动化技术测评", 1)
);
  1. 竞品功能矩阵分析

    功能模块LeetCode牛客网我们的优势
    题目数量2000+1500+渐进式更新,质量优先
    编程语言10+5+Java深度优化,扩展性强
    竞赛系统周赛/双周赛企业专场定制化竞赛支持
    学习路径付费课程社区驱动个性化推荐算法
  2. 技术可行性分析

2.1.2 需求规格说明书

功能需求详细分解

  1. 用户管理模块

UserManagement:Authentication:- 用户注册(邮箱/手机号)- 密码强度校验- 登录状态保持- 第三方登录(预留)Profile:- 个人信息维护- 学习数据统计- 成就系统- 消息中心

         题目管理模块

public class QuestionSpecification {// 题目属性private Long questionId;private String title;private String description;private String difficulty; // EASY, MEDIUM, HARDprivate List<String> tags;private QuestionContent content;// 测试用例private List<TestCase> testCases;private JudgeConfig judgeConfig;// 统计信息private QuestionStatistics statistics;
}public class JudgeConfig {private Integer timeLimit;     // 时间限制(ms)private Integer memoryLimit;   // 内存限制(MB)private Boolean specialJudge;  // 是否特殊判题private String judgeScript;    // 判题脚本
}

            竞赛系统模块

-- 竞赛数据模型
CREATE TABLE `tb_exam` (`exam_id` BIGINT PRIMARY KEY COMMENT '竞赛ID',`title` VARCHAR(100) NOT NULL COMMENT '竞赛标题',`description` TEXT COMMENT '竞赛描述',`start_time` DATETIME NOT NULL COMMENT '开始时间',`end_time` DATETIME NOT NULL COMMENT '结束时间',`duration` INT COMMENT '持续时间(分钟)',`type` TINYINT COMMENT '竞赛类型1-公开赛2-私有赛',`status` TINYINT DEFAULT 1 COMMENT '状态1-启用0-禁用',`max_participants` INT COMMENT '最大参与人数',`password` VARCHAR(50) COMMENT '访问密码',`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP
) COMMENT='竞赛表';

2.2 技术选型与架构设计

2.2.1 架构演进思考

从单体到微服务的演进路径

// 单体架构的问题示例
@Service
public class MonolithicOJService {// 用户管理、题目管理、竞赛管理、判题服务全部耦合在一起public SubmitResult submitCode(CodeSubmitRequest request) {// 1. 验证用户权限User user = userService.validateToken(request.getToken());// 2. 检查题目状态Question question = questionService.getById(request.getQuestionId());// 3. 保存提交记录SubmitRecord record = submitService.saveSubmitRecord(user, question, request.getCode());// 4. 执行代码判题(阻塞操作)JudgeResult result = judgeService.executeCode(record);// 5. 更新用户数据userService.updateUserStats(user, result);// 6. 发送通知notificationService.sendResultNotification(user, result);return convertToDTO(result);}
}

微服务拆分优势分析

  1. 技术异构性:不同服务可选择最适合的技术栈

  2. 独立部署:服务可独立发布,降低发布风险

  3. 故障隔离:单个服务故障不影响整体系统

  4. 团队自治:不同团队负责不同服务,提升开发效率

2.2.2 技术栈深度解析

后端技术决策矩阵

技术领域技术选型决策理由替代方案对比
微服务框架Spring Cloud Alibaba阿里云生态集成、中文文档丰富、国内社区活跃Spring Cloud Netflix(停止维护)、Dubbo(功能相对单一)
服务注册发现Nacos配置管理+服务发现二合一、AP架构保证高可用Eureka(2.x闭源)、Consul(运维复杂)
配置中心Nacos Config与注册中心统一、支持配置热更新Spring Cloud Config(需要配合Git)、Apollo(部署复杂)
流量控制Sentinel可视化控制台、多种流量控制策略Hystrix(停止维护)、Resilience4j(功能相对简单)
分布式事务SeataAT模式无侵入、支持多种事务模式2PC(性能较差)、TCC(实现复杂)

前端技术选型依据

// Vue3 Composition API vs Options API
// Composition API 优势分析
import { ref, reactive, computed, onMounted } from 'vue'export default {setup() {// 逻辑关注点分离,相关功能组织在一起const { user, loadUser } = useUser()const { questions, loadQuestions } = useQuestions()const { submissions, loadSubmissions } = useSubmissions()onMounted(() => {loadUser()loadQuestions()loadSubmissions()})return {user,questions,submissions}}
}// 对应的Options API(逻辑分散)
export default {data() {return {user: null,questions: [],submissions: []}},methods: {loadUser() { /* ... */ },loadQuestions() { /* ... */ },loadSubmissions() { /* ... */ }},mounted() {this.loadUser()this.loadQuestions()this.loadSubmissions()}
}

3. 微服务架构深度设计

3.1 服务拆分策略

3.1.1 领域驱动设计(DDD)实践

领域模型划分

// 核心领域模型定义
@Entity
@Table(name = "tb_question")
public class Question implements AggregateRoot {@Idprivate Long questionId;private String title;private String description;private QuestionDifficulty difficulty;@Embeddedprivate QuestionContent content;@OneToMany(mappedBy = "question")private List<TestCase> testCases;@Embeddedprivate JudgeConfig judgeConfig;// 领域方法public boolean validateCode(String code) {// 代码基础验证逻辑return code != null && !code.trim().isEmpty() && code.length() <= 10000;}public JudgeResult judgeSubmission(CodeSubmission submission) {// 判题领域逻辑if (!validateCode(submission.getCode())) {return JudgeResult.invalid("代码格式错误");}return doJudge(submission);}
}// 值对象
@Embeddable
public class QuestionContent {private String problemStatement;private String inputFormat;private String outputFormat;private List<String> constraints;private List<Example> examples;
}// 枚举类型
public enum QuestionDifficulty {EASY("简单", 1),MEDIUM("中等", 2),HARD("困难", 3);private final String description;private final int level;QuestionDifficulty(String description, int level) {this.description = description;this.level = level;}
}
3.1.2 服务边界定义

服务契约设计

// 服务接口定义 - 题目服务
@FeignClient(name = "oj-question", path = "/api/questions")
public interface QuestionServiceClient {@GetMapping("/{questionId}")ResponseEntity<QuestionDTO> getQuestionById(@PathVariable Long questionId);@PostMapping("/search")ResponseEntity<PageResult<QuestionDTO>> searchQuestions(@RequestBody QuestionQuery query);@GetMapping("/{questionId}/testcases")ResponseEntity<List<TestCaseDTO>> getTestCases(@PathVariable Long questionId);
}// 服务接口定义 - 用户服务
@FeignClient(name = "oj-user", path = "/api/users")
public interface UserServiceClient {@GetMapping("/{userId}")ResponseEntity<UserDTO> getUserById(@PathVariable Long userId);@PostMapping("/{userId}/submissions")ResponseEntity<Void> addUserSubmission(@PathVariable Long userId, @RequestBody SubmissionDTO submission);@GetMapping("/{userId}/statistics")ResponseEntity<UserStatistics> getUserStatistics(@PathVariable Long userId);
}

3.2 服务治理与通信

3.2.1 服务发现与负载均衡

Nacos服务注册配置

# application.yml
spring:application:name: oj-question-servicecloud:nacos:discovery:server-addr: ${NACOS_HOST:localhost}:8848namespace: ${NACOS_NAMESPACE:bitcoj}group: ${NACOS_GROUP:DEFAULT_GROUP}cluster-name: ${NACOS_CLUSTER:DEFAULT}# 服务元数据metadata:version: 1.0.0environment: ${spring.profiles.active}region: ${REGION:china-east}config:server-addr: ${spring.cloud.nacos.discovery.server-addr}namespace: ${spring.cloud.nacos.discovery.namespace}group: ${spring.cloud.nacos.discovery.group}file-extension: yaml# 配置自动刷新refresh-enabled: true# 服务健康检查配置
management:endpoints:web:exposure:include: health,info,metricsendpoint:health:show-details: always

负载均衡策略配置

@Configuration
public class LoadBalancerConfiguration {@Bean@LoadBalancerClient(name = "oj-judge-service", configuration = JudgeServiceConfiguration.class)public ReactorLoadBalancer<ServiceInstance> judgeServiceLoadBalancer(Environment environment, LoadBalancerClientFactory loadBalancerClientFactory) {String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);return new RoundRobinLoadBalancer(loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class),name);}
}// 自定义负载均衡策略
public class JudgeServiceConfiguration {@Beanpublic ServiceInstanceListSupplier judgeServiceInstanceListSupplier() {return ServiceInstanceListSupplier.builder().withBlockingDiscoveryClient().withSameInstancePreference() // 优先选择相同实例.withHealthChecks() // 健康检查.build();}
}
3.2.2 服务间通信模式

同步通信 - OpenFeign增强配置

@Configuration
public class FeignConfiguration {@Beanpublic Logger.Level feignLoggerLevel() {return Logger.Level.FULL;}@Beanpublic RequestInterceptor authRequestInterceptor() {return template -> {// 自动传递认证信息String token = SecurityContextHolder.getContext().getAuthentication().getCredentials().toString();template.header("Authorization", "Bearer " + token);};}@Beanpublic ErrorDecoder feignErrorDecoder() {return (methodKey, response) -> {if (response.status() == 401) {return new UnauthorizedException("认证失败");} else if (response.status() == 403) {return new ForbiddenException("权限不足");} else if (response.status() >= 500) {return new ServiceUnavailableException("服务暂时不可用");}return new FeignException(response.status(), response.reason());};}
}// 重试机制配置
@Bean
public Retryer feignRetryer() {return new Retryer.Default(100, 1000, 3); // 重试3次,间隔100ms开始,最大间隔1s
}

异步通信 - 消息队列深度设计

// 消息类型定义
public abstract class BaseMessage implements Serializable {protected String messageId;protected LocalDateTime timestamp;protected String sourceService;protected MessageType messageType;protected BaseMessage(MessageType messageType) {this.messageId = UUID.randomUUID().toString();this.timestamp = LocalDateTime.now();this.sourceService = getCurrentServiceName();this.messageType = messageType;}
}// 判题消息
public class JudgeMessage extends BaseMessage {private Long submissionId;private Long questionId;private String code;private String language;private JudgeConfig judgeConfig;public JudgeMessage() {super(MessageType.JUDGE_SUBMISSION);}// 消息验证public boolean validate() {return submissionId != null && questionId != null && StringUtils.hasText(code) && StringUtils.hasText(language);}
}// 消息生产者服务
@Service
@Slf4j
public class MessageProducerService {@Autowiredprivate RabbitTemplate rabbitTemplate;@Autowiredprivate ObjectMapper objectMapper;public <T extends BaseMessage> void sendMessage(String exchange, String routingKey, T message) {try {// 消息预处理message.preSend();// 发送消息rabbitTemplate.convertAndSend(exchange, routingKey, message, m -> {// 设置消息属性m.getMessageProperties().setMessageId(message.getMessageId());m.getMessageProperties().setTimestamp(new Date());m.getMessageProperties().setContentType("application/json");m.getMessageProperties().setContentEncoding("UTF-8");// 设置消息TTL(1小时)m.getMessageProperties().setExpiration("3600000");return m;});log.info("消息发送成功: messageId={}, type={}", message.getMessageId(), message.getMessageType());} catch (Exception e) {log.error("消息发送失败: messageId={}, error={}", message.getMessageId(), e.getMessage(), e);throw new MessageSendException("消息发送失败", e);}}// 延迟消息发送public <T extends BaseMessage> void sendDelayedMessage(String exchange, String routingKey, T message, long delayMillis) {rabbitTemplate.convertAndSend(exchange, routingKey, message, m -> {m.getMessageProperties().setDelay(Math.toIntExact(delayMillis));return m;});}
}

4. 核心业务模块实现

4.1 身份认证与安全体系

4.1.1 JWT + Redis混合认证方案

Token服务深度实现

@Service
@Slf4j
public class TokenService {@Autowiredprivate RedisService redisService;@Value("${jwt.secret}")private String secret;@Value("${jwt.expiration:720}")private Long expiration; // 默认12小时private static final String LOGIN_TOKEN_KEY = "login_tokens:";private static final String TOKEN_REFRESH_KEY = "token_refresh:";/*** 创建令牌 - 支持多端登录*/public TokenPair createToken(LoginUser loginUser, String clientType) {String accessToken = UUID.fastUUID().toString();String refreshToken = UUID.fastUUID().toString();// 设置客户端信息loginUser.setToken(accessToken);loginUser.setClientType(clientType);loginUser.setLoginTime(System.currentTimeMillis());// 存储登录信息storeLoginUser(loginUser, accessToken);// 生成JWTString jwtToken = generateJwtToken(loginUser, accessToken);// 存储刷新令牌storeRefreshToken(loginUser.getUserId(), clientType, refreshToken);return new TokenPair(jwtToken, refreshToken, expiration);}/*** 存储登录用户信息*/private void storeLoginUser(LoginUser loginUser, String accessToken) {String userKey = getTokenKey(accessToken);// 存储用户信息redisService.setCacheObject(userKey, loginUser, expiration, TimeUnit.MINUTES);// 存储用户-令牌映射(支持多端登录管理)String userTokenKey = getUserTokenKey(loginUser.getUserId(), loginUser.getClientType());redisService.setCacheObject(userTokenKey, accessToken, expiration, TimeUnit.MINUTES);}/*** 生成JWT令牌*/private String generateJwtToken(LoginUser loginUser, String accessToken) {Map<String, Object> claims = new HashMap<>();claims.put(SecurityConstants.USER_KEY, accessToken);claims.put(SecurityConstants.DETAILS_USER_ID, loginUser.getUserId());claims.put(SecurityConstants.DETAILS_USERNAME, loginUser.getUsername());claims.put(SecurityConstants.DETAILS_CLIENT_TYPE, loginUser.getClientType());claims.put(SecurityConstants.LOGIN_TIME, loginUser.getLoginTime());return JwtUtils.createToken(claims, secret);}/*** 刷新令牌*/public TokenPair refreshToken(String refreshToken, String clientType) {// 验证刷新令牌Long userId = validateRefreshToken(refreshToken, clientType);if (userId == null) {throw new AuthenticationException("刷新令牌无效");}// 获取用户信息LoginUser loginUser = loadUserById(userId);if (loginUser == null) {throw new AuthenticationException("用户不存在");}// 创建新令牌return createToken(loginUser, clientType);}/*** 令牌验证*/public LoginUser verifyToken(String token) {try {// 解析JWTClaims claims = JwtUtils.parseToken(token, secret);if (claims == null) {return null;}// 获取访问令牌String accessToken = JwtUtils.getUserKey(claims);if (StringUtils.isEmpty(accessToken)) {return null;}// 从Redis获取用户信息String userKey = getTokenKey(accessToken);LoginUser loginUser = redisService.getCacheObject(userKey, LoginUser.class);if (loginUser != null) {// 刷新令牌有效期refreshToken(loginUser);}return loginUser;} catch (ExpiredJwtException e) {log.warn("令牌已过期: {}", e.getMessage());throw new TokenExpiredException("令牌已过期");} catch (Exception e) {log.error("令牌验证失败: {}", e.getMessage());return null;}}/*** 强制下线*/public void forceLogout(Long userId, String clientType) {String userTokenKey = getUserTokenKey(userId, clientType);String accessToken = redisService.getCacheObject(userTokenKey, String.class);if (accessToken != null) {// 删除登录信息redisService.deleteObject(getTokenKey(accessToken));redisService.deleteObject(userTokenKey);// 删除刷新令牌deleteRefreshToken(userId, clientType);}}
}
4.1.2 密码安全策略

增强型密码安全服务

@Service
public class PasswordService {private final BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();private final Map<String, Integer> passwordAttempts = new ConcurrentHashMap<>();private static final int MAX_ATTEMPTS = 5;private static final long LOCK_DURATION = 15 * 60 * 1000; // 15分钟/*** 密码强度验证*/public PasswordStrength validatePasswordStrength(String password) {if (StringUtils.isEmpty(password)) {return PasswordStrength.EMPTY;}int score = 0;// 长度检查if (password.length() >= 8) score++;if (password.length() >= 12) score++;// 复杂度检查if (password.matches(".*[a-z].*")) score++; // 小写字母if (password.matches(".*[A-Z].*")) score++; // 大写字母  if (password.matches(".*\\d.*")) score++;    // 数字if (password.matches(".*[!@#$%^&*()_+\\-=\\[\\]{};':\"\\\\|,.<>\\/?].*")) score++; // 特殊字符// 常见密码检查if (isCommonPassword(password)) {score = Math.max(0, score - 2);}return PasswordStrength.fromScore(score);}/*** 加密密码*/public String encryptPassword(String password) {PasswordStrength strength = validatePasswordStrength(password);if (strength == PasswordStrength.WEAK) {throw new WeakPasswordException("密码强度不足,请使用更复杂的密码");}return passwordEncoder.encode(password);}/*** 密码验证(带尝试次数限制)*/public boolean matchesPassword(String rawPassword, String encodedPassword, String identifier) {// 检查是否被锁定if (isAccountLocked(identifier)) {throw new AccountLockedException("账户因多次密码错误已被临时锁定");}boolean matches = passwordEncoder.matches(rawPassword, encodedPassword);if (matches) {// 验证成功,重置尝试次数passwordAttempts.remove(identifier);} else {// 验证失败,记录尝试次数int attempts = passwordAttempts.getOrDefault(identifier, 0) + 1;passwordAttempts.put(identifier, attempts);if (attempts >= MAX_ATTEMPTS) {// 锁定账户lockAccount(identifier);throw new AccountLockedException("密码错误次数过多,账户已被锁定15分钟");}}return matches;}private boolean isAccountLocked(String identifier) {Integer attempts = passwordAttempts.get(identifier);return attempts != null && attempts >= MAX_ATTEMPTS;}private void lockAccount(String identifier) {// 可以在这里记录锁定日志或发送通知log.warn("账户被锁定: {}", identifier);// 15分钟后自动解锁Timer timer = new Timer();timer.schedule(new TimerTask() {@Overridepublic void run() {passwordAttempts.remove(identifier);log.info("账户自动解锁: {}", identifier);}}, LOCK_DURATION);}private boolean isCommonPassword(String password) {Set<String> commonPasswords = Set.of("123456", "password", "12345678", "qwerty", "abc123","1234567", "111111", "1234", "admin", "password1");return commonPasswords.contains(password.toLowerCase());}public enum PasswordStrength {EMPTY, WEAK, MEDIUM, STRONG, VERY_STRONG;public static PasswordStrength fromScore(int score) {if (score <= 2) return WEAK;if (score <= 4) return MEDIUM;if (score <= 6) return STRONG;return VERY_STRONG;}}
}

4.2 数据持久化设计

4.2.1 实体关系模型深度设计

完整的数据库架构

-- 用户表增强设计
CREATE TABLE `tb_user` (`user_id` BIGINT UNSIGNED NOT NULL COMMENT '用户ID-雪花算法',`username` VARCHAR(50) NOT NULL COMMENT '用户名',`email` VARCHAR(100) COMMENT '邮箱',`phone` VARCHAR(20) COMMENT '手机号',`password` VARCHAR(100) NOT NULL COMMENT '加密密码',`nick_name` VARCHAR(50) NOT NULL COMMENT '昵称',`avatar` VARCHAR(200) COMMENT '头像URL',`gender` TINYINT COMMENT '性别0-未知1-男2-女',`birthday` DATE COMMENT '生日',`introduction` TEXT COMMENT '个人简介',`school` VARCHAR(100) COMMENT '学校',`company` VARCHAR(100) COMMENT '公司',`position` VARCHAR(50) COMMENT '职位',`user_type` TINYINT DEFAULT 1 COMMENT '用户类型1-普通用户2-管理员',`status` TINYINT DEFAULT 1 COMMENT '状态1-正常0-禁用',`last_login_time` DATETIME COMMENT '最后登录时间',`last_login_ip` VARCHAR(50) COMMENT '最后登录IP',`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,`update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,PRIMARY KEY (`user_id`),UNIQUE KEY `uk_username` (`username`),UNIQUE KEY `uk_email` (`email`),UNIQUE KEY `uk_phone` (`phone`),KEY `idx_create_time` (`create_time`),KEY `idx_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户表';-- 题目表增强设计
CREATE TABLE `tb_question` (`question_id` BIGINT UNSIGNED NOT NULL COMMENT '题目ID',`title` VARCHAR(200) NOT NULL COMMENT '题目标题',`description` TEXT NOT NULL COMMENT '题目描述',`difficulty` TINYINT NOT NULL COMMENT '难度1-简单2-中等3-困难',`tags` JSON COMMENT '题目标签',`time_limit` INT NOT NULL DEFAULT 1000 COMMENT '时间限制(ms)',`memory_limit` INT NOT NULL DEFAULT 128 COMMENT '内存限制(MB)',`stack_limit` INT DEFAULT 128 COMMENT '堆栈限制(MB)',`input_description` TEXT COMMENT '输入描述',`output_description` TEXT COMMENT '输出描述',`sample_input` TEXT COMMENT '样例输入',`sample_output` TEXT COMMENT '样例输出',`hint` TEXT COMMENT '提示',`source` VARCHAR(100) COMMENT '题目来源',`author_id` BIGINT UNSIGNED COMMENT '作者ID',`visible` TINYINT DEFAULT 1 COMMENT '是否可见1-是0-否',`submit_count` INT DEFAULT 0 COMMENT '提交次数',`accept_count` INT DEFAULT 0 COMMENT '通过次数',`accept_rate` DECIMAL(5,2) DEFAULT 0.00 COMMENT '通过率',`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,`update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,PRIMARY KEY (`question_id`),KEY `idx_difficulty` (`difficulty`),KEY `idx_author` (`author_id`),KEY `idx_visible` (`visible`),KEY `idx_create_time` (`create_time`),KEY `idx_accept_rate` (`accept_rate`),FULLTEXT KEY `ft_title_desc` (`title`, `description`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='题目表';-- 提交记录表分区设计
CREATE TABLE `tb_submission` (`submission_id` BIGINT UNSIGNED NOT NULL COMMENT '提交ID',`user_id` BIGINT UNSIGNED NOT NULL COMMENT '用户ID',`question_id` BIGINT UNSIGNED NOT NULL COMMENT '题目ID',`exam_id` BIGINT UNSIGNED COMMENT '竞赛ID',`code` TEXT NOT NULL COMMENT '提交代码',`language` VARCHAR(20) NOT NULL COMMENT '编程语言',`status` TINYINT NOT NULL DEFAULT 0 COMMENT '状态0-待判题1-判题中2-成功3-失败',`execute_time` INT COMMENT '执行时间(ms)',`execute_memory` INT COMMENT '执行内存(KB)',`judge_result` JSON COMMENT '判题结果',`error_message` TEXT COMMENT '错误信息',`ip_address` VARCHAR(50) COMMENT '提交IP',`user_agent` TEXT COMMENT '用户代理',`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,`update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,PRIMARY KEY (`submission_id`, `create_time`),KEY `idx_user_question` (`user_id`, `question_id`),KEY `idx_user_exam` (`user_id`, `exam_id`),KEY `idx_question_status` (`question_id`, `status`),KEY `idx_create_time` (`create_time`),KEY `idx_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci 
PARTITION BY RANGE (YEAR(create_time)) (PARTITION p2023 VALUES LESS THAN (2024),PARTITION p2024 VALUES LESS THAN (2025),PARTITION p2025 VALUES LESS THAN (2026),PARTITION p_future VALUES LESS THAN MAXVALUE
) COMMENT='提交记录表';
4.2.2 MyBatis Plus深度集成

数据访问层增强设计

// 基础Mapper接口
public interface BaseMapper<T> extends com.baomidou.mybatisplus.core.mapper.BaseMapper<T> {/*** 批量插入(性能优化版本)*/Integer insertBatchSomeColumn(Collection<T> entityList);/*** 逻辑删除*/Integer deleteByIdWithFill(T entity);/*** 乐观锁更新*/Integer updateByIdWithVersion(T entity);
}// 题目Mapper定制化操作
@Mapper
public interface QuestionMapper extends BaseMapper<Question> {/*** 复杂查询:根据条件分页查询题目*/List<QuestionVO> selectQuestionList(@Param("query") QuestionQueryDTO query);/*** 更新题目统计信息*/@Update("UPDATE tb_question SET submit_count = submit_count + 1, " +"accept_count = accept_count + #{accepted}, " +"accept_rate = ROUND(accept_count * 100.0 / submit_count, 2) " +"WHERE question_id = #{questionId}")int updateQuestionStats(@Param("questionId") Long questionId, @Param("accepted") boolean accepted);/*** 获取题目详情(包含关联信息)*/@Select("SELECT q.*, u.nick_name as author_name " +"FROM tb_question q LEFT JOIN tb_user u ON q.author_id = u.user_id " +"WHERE q.question_id = #{questionId} AND q.visible = 1")@Results({@Result(property = "questionId", column = "question_id"),@Result(property = "testCases", column = "question_id", many = @Many(select = "selectTestCasesByQuestionId")),@Result(property = "tags", column = "tags", typeHandler = JsonTypeHandler.class)})QuestionDetailVO selectQuestionDetail(Long questionId);/*** 获取题目标签统计*/@Select("SELECT tag, COUNT(*) as count FROM tb_question, " +"JSON_TABLE(tags, '$[*]' COLUMNS(tag VARCHAR(50) PATH '$')) AS tags " +"WHERE visible = 1 GROUP BY tag ORDER BY count DESC")List<TagCountVO> selectTagStatistics();
}// 自定义类型处理器
@MappedTypes({List.class})
@MappedJdbcTypes(JdbcType.VARCHAR)
public class JsonTypeHandler extends BaseTypeHandler<List<String>> {private static final ObjectMapper objectMapper = new ObjectMapper();@Overridepublic void setNonNullParameter(PreparedStatement ps, int i, List<String> parameter, JdbcType jdbcType) throws SQLException {try {ps.setString(i, objectMapper.writeValueAsString(parameter));} catch (JsonProcessingException e) {throw new SQLException("JSON序列化失败", e);}}@Overridepublic List<String> getNullableResult(ResultSet rs, String columnName) throws SQLException {return parseJson(rs.getString(columnName));}private List<String> parseJson(String json) {if (StringUtils.isEmpty(json)) {return new ArrayList<>();}try {return objectMapper.readValue(json, objectMapper.getTypeFactory().constructCollectionType(List.class, String.class));} catch (Exception e) {return new ArrayList<>();}}
}

4.3 代码沙箱与判题引擎

4.3.1 Docker沙箱安全设计

安全的代码执行环境

@Service
@Slf4j
public class SecureCodeSandbox {@Autowiredprivate DockerClient dockerClient;@Value("${sandbox.timeout:10000}")private long timeout;@Value("${sandbox.memory-limit:256m}")private String memoryLimit;@Value("${sandbox.cpu-shares:512}")private int cpuShares;/*** 安全执行用户代码*/public ExecuteResult executeCodeSecurely(ExecuteRequest request) {// 1. 代码安全检查CodeSecurityCheckResult securityCheck = checkCodeSecurity(request.getCode(), request.getLanguage());if (!securityCheck.isSafe()) {return ExecuteResult.securityError(securityCheck.getRiskDescription());}// 2. 创建临时目录Path tempDir = createSecureTempDirectory();try {// 3. 准备执行环境ExecutionEnvironment env = prepareExecutionEnvironment(request, tempDir);// 4. 创建安全容器String containerId = createSecureContainer(env);// 5. 执行代码并监控ExecuteResult result = executeInContainerWithMonitoring(containerId, env);return result;} catch (Exception e) {log.error("代码执行失败: {}", e.getMessage(), e);return ExecuteResult.systemError("系统执行错误: " + e.getMessage());} finally {// 6. 清理资源cleanup(tempDir);}}/*** 代码安全检查*/private CodeSecurityCheckResult checkCodeSecurity(String code, String language) {CodeSecurityChecker checker = SecurityCheckerFactory.getChecker(language);return checker.check(code);}/*** 创建安全容器*/private String createSecureContainer(ExecutionEnvironment env) {// 容器配置HostConfig hostConfig = HostConfig.newHostConfig().withMemory(Long.parseLong(memoryLimit.replace("m", "")) * 1024 * 1024L).withMemorySwap(0L) // 禁用swap.withCpuShares(cpuShares).withNetworkMode("none") // 禁用网络.withCapDrop("ALL") // 删除所有权限.withSecurityOpts(List.of("no-new-privileges:true")).withBinds(Bind.parse(env.getTempDir().toString() + ":/app:ro"));// 创建容器return dockerClient.createContainerCmd(env.getImageName()).withHostConfig(hostConfig).withCmd(env.getExecutionCommand()).withTty(true).withAttachStdin(true).withAttachStdout(true).withAttachStderr(true).exec().getId();}/*** 带监控的代码执行*/private ExecuteResult executeInContainerWithMonitoring(String containerId, ExecutionEnvironment env) {// 启动容器dockerClient.startContainerCmd(containerId).exec();// 执行超时控制CompletableFuture<ExecuteResult> future = CompletableFuture.supplyAsync(() -> {try {return doExecute(containerId, env);} catch (Exception e) {return ExecuteResult.systemError("执行异常: " + e.getMessage());}});try {return future.get(timeout, TimeUnit.MILLISECONDS);} catch (TimeoutException e) {// 超时处理future.cancel(true);forceKillContainer(containerId);return ExecuteResult.timeoutError("执行超时");} catch (Exception e) {return ExecuteResult.systemError("执行失败: " + e.getMessage());}}/*** 强制终止容器*/private void forceKillContainer(String containerId) {try {dockerClient.stopContainerCmd(containerId).withTimeout(2).exec();dockerClient.removeContainerCmd(containerId).exec();} catch (Exception e) {log.warn("容器清理失败: {}", e.getMessage());}}
}// 代码安全检查器
public interface CodeSecurityChecker {CodeSecurityCheckResult check(String code);
}// Java代码安全检查器
@Component
public class JavaCodeSecurityChecker implements CodeSecurityChecker {private static final Set<String> DANGEROUS_IMPORTS = Set.of("java.lang.reflect", "java.lang.invoke", "java.lang.ProcessBuilder","java.lang.Runtime", "java.lang.System", "java.io.File","java.net", "java.nio", "java.sql");private static final Set<String> DANGEROUS_KEYWORDS = Set.of("Runtime.getRuntime()", "ProcessBuilder", "System.exit","File.", "Socket", "URLConnection", "Class.forName","Method.invoke", "Field.set", "Unsafe");@Overridepublic CodeSecurityCheckResult check(String code) {List<SecurityRisk> risks = new ArrayList<>();// 检查危险导入checkDangerousImports(code, risks);// 检查危险关键字checkDangerousKeywords(code, risks);// 检查代码长度if (code.length() > 10000) {risks.add(new SecurityRisk("CODE_TOO_LONG", "代码长度超过限制"));}// 检查递归深度checkRecursionDepth(code, risks);return new CodeSecurityCheckResult(risks.isEmpty(), risks);}private void checkDangerousImports(String code, List<SecurityRisk> risks) {for (String dangerousImport : DANGEROUS_IMPORTS) {if (code.contains("import " + dangerousImport)) {risks.add(new SecurityRisk("DANGEROUS_IMPORT", "禁止导入: " + dangerousImport));}}}private void checkDangerousKeywords(String code, List<SecurityRisk> risks) {for (String keyword : DANGEROUS_KEYWORDS) {if (code.contains(keyword)) {risks.add(new SecurityRisk("DANGEROUS_KEYWORD", "禁止使用: " + keyword));}}}private void checkRecursionDepth(String code, List<SecurityRisk> risks) {// 简单的递归深度检查long recursionCount = countOccurrences(code, "public static void main");if (recursionCount > 1) {risks.add(new SecurityRisk("MULTIPLE_MAIN", "检测到多个main方法"));}}private long countOccurrences(String text, String pattern) {return Pattern.compile(pattern).matcher(text).results().count();}
}
4.3.2 判题流程优化

异步判题流程设计

@Service
@Slf4j
public class AsyncJudgeService {@Autowiredprivate JudgeTaskDispatcher taskDispatcher;@Autowiredprivate SubmissionService submissionService;@Autowiredprivate QuestionService questionService;@Autowiredprivate UserService userService;/*** 提交代码判题*/@Async("judgeTaskExecutor")public CompletableFuture<JudgeResult> submitForJudge(CodeSubmission submission) {return CompletableFuture.supplyAsync(() -> {try {// 1. 更新提交状态为判题中submissionService.updateSubmissionStatus(submission.getSubmissionId(), SubmissionStatus.JUDGING);// 2. 获取题目信息和测试用例Question question = questionService.getQuestionById(submission.getQuestionId());List<TestCase> testCases = questionService.getTestCases(submission.getQuestionId());// 3. 执行判题JudgeResult result = executeJudgment(submission, question, testCases);// 4. 更新判题结果submissionService.updateJudgeResult(submission.getSubmissionId(), result);// 5. 更新用户和题目统计updateStatistics(submission, result, question);// 6. 发送结果通知sendResultNotification(submission, result);return result;} catch (Exception e) {log.error("判题过程异常: submissionId={}", submission.getSubmissionId(), e);submissionService.updateSubmissionStatus(submission.getSubmissionId(), SubmissionStatus.SYSTEM_ERROR);throw new JudgeException("判题系统异常", e);}});}/*** 执行判题流程*/private JudgeResult executeJudgment(CodeSubmission submission, Question question, List<TestCase> testCases) {JudgeResult result = new JudgeResult();result.setSubmissionId(submission.getSubmissionId());result.setQuestionId(submission.getQuestionId());List<TestCaseResult> caseResults = new ArrayList<>();boolean allPassed = true;for (int i = 0; i < testCases.size(); i++) {TestCase testCase = testCases.get(i);// 执行单个测试用例TestCaseResult caseResult = executeTestCase(submission, question, testCase, i + 1);caseResults.add(caseResult);if (!caseResult.isPassed()) {allPassed = false;// 如果第一个测试用例就失败,可以提前结束if (question.getJudgeConfig().isStopOnFirstFailure() && i == 0) {break;}}// 检查执行时间是否超限if (caseResult.getExecuteTime() > question.getTimeLimit()) {caseResult.setPassed(false);caseResult.setErrorMessage("时间超限");allPassed = false;break;}}result.setTestCaseResults(caseResults);result.setAllPassed(allPassed);result.setTotalCases(testCases.size());result.setPassedCases((int) caseResults.stream().filter(TestCaseResult::isPassed).count());return result;}/*** 执行单个测试用例*/private TestCaseResult executeTestCase(CodeSubmission submission, Question question, TestCase testCase, int caseIndex) {ExecuteRequest request = new ExecuteRequest();request.setCode(submission.getCode());request.setLanguage(submission.getLanguage());request.setInput(testCase.getInput());request.setTimeLimit(question.getTimeLimit());request.setMemoryLimit(question.getMemoryLimit());ExecuteResult executeResult = codeSandbox.executeCode(request);TestCaseResult caseResult = new TestCaseResult();caseResult.setCaseIndex(caseIndex);caseResult.setInput(testCase.getInput());caseResult.setExpectedOutput(testCase.getExpectedOutput());caseResult.setActualOutput(executeResult.getOutput());caseResult.setExecuteTime(executeResult.getExecuteTime());caseResult.setExecuteMemory(executeResult.getExecuteMemory());caseResult.setErrorMessage(executeResult.getErrorMessage());// 验证输出结果boolean passed = validateOutput(executeResult.getOutput(), testCase.getExpectedOutput(), question.getJudgeConfig());caseResult.setPassed(passed);return caseResult;}/*** 验证输出结果*/private boolean validateOutput(String actual, String expected, JudgeConfig judgeConfig) {if (actual == null || expected == null) {return false;}// 标准化输出(去除首尾空白字符)String normalizedActual = actual.trim().replaceAll("\\r\\n", "\n");String normalizedExpected = expected.trim().replaceAll("\\r\\n", "\n");if (judgeConfig.isSpecialJudge()) {// 特殊判题逻辑return specialJudge(normalizedActual, normalizedExpected, judgeConfig.getJudgeScript());} else {// 普通判题:精确匹配return normalizedActual.equals(normalizedExpected);}}
}

4.4 消息系统设计

4.4.1 消息类型与路由设计

完整的消息体系

// 消息类型枚举
public enum MessageType {// 系统消息SYSTEM_ANNOUNCEMENT("系统公告", "system", Priority.HIGH),SYSTEM_MAINTENANCE("系统维护", "system", Priority.HIGH),// 用户消息USER_WELCOME("欢迎消息", "user", Priority.LOW),USER_ACHIEVEMENT("成就解锁", "user", Priority.MEDIUM),// 提交相关SUBMISSION_RESULT("提交结果", "submission", Priority.HIGH),SUBMISSION_REVIEW("代码评审", "submission", Priority.MEDIUM),// 竞赛相关EXAM_INVITATION("竞赛邀请", "exam", Priority.MEDIUM),EXAM_REMINDER("竞赛提醒", "exam", Priority.MEDIUM),EXAM_RESULT("竞赛结果", "exam", Priority.HIGH),// 社交相关FOLLOW_NOTIFICATION("关注通知", "social", Priority.LOW),LIKE_NOTIFICATION("点赞通知", "social", Priority.LOW),COMMENT_NOTIFICATION("评论通知", "social", Priority.MEDIUM);private final String description;private final String category;private final Priority priority;MessageType(String description, String category, Priority priority) {this.description = description;this.category = category;this.priority = priority;}
}// 消息优先级
public enum Priority {LOW(1), MEDIUM(2), HIGH(3), URGENT(4);private final int level;Priority(int level) {this.level = level;}public int getLevel() {return level;}
}// 基础消息类
@Data
@Builder
public class BaseMessage implements Serializable {private String messageId;private MessageType messageType;private String title;private String content;private Map<String, Object> payload;private Long senderId;private List<Long> receiverIds;private LocalDateTime sendTime;private LocalDateTime expireTime;private Priority priority;private Map<String, String> attributes;// 消息验证public boolean validate() {return StringUtils.hasText(messageId) &&messageType != null &&StringUtils.hasText(title) &&sendTime != null &&(receiverIds != null && !receiverIds.isEmpty());}// 消息预处理public void preSend() {if (this.messageId == null) {this.messageId = UUID.randomUUID().toString();}if (this.sendTime == null) {this.sendTime = LocalDateTime.now();}if (this.priority == null) {this.priority = Priority.MEDIUM;}}
}
4.4.2 消息队列深度配置

RabbitMQ配置优化

@Configuration
@Slf4j
public class RabbitMQAdvancedConfig {@Beanpublic Jackson2JsonMessageConverter messageConverter() {ObjectMapper objectMapper = new ObjectMapper();objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);objectMapper.registerModule(new JavaTimeModule());objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);return new Jackson2JsonMessageConverter(objectMapper);}@Beanpublic SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory(ConnectionFactory connectionFactory, Jackson2JsonMessageConverter messageConverter) {SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();factory.setConnectionFactory(connectionFactory);factory.setMessageConverter(messageConverter);factory.setConcurrentConsumers(3); // 并发消费者数量factory.setMaxConcurrentConsumers(10); // 最大并发消费者factory.setPrefetchCount(10); // 每次预取消息数量factory.setDefaultRequeueRejected(false); // 拒绝的消息不重新入队// 错误处理factory.setErrorHandler(new ConditionalRejectingErrorHandler(new FatalExceptionStrategy()));// 确认模式factory.setAcknowledgeMode(AcknowledgeMode.AUTO);return factory;}// 判题消息队列配置@Beanpublic Queue judgeQueue() {return QueueBuilder.durable(RabbitMQConstants.JUDGE_QUEUE).withArgument("x-dead-letter-exchange", RabbitMQConstants.DLX_EXCHANGE).withArgument("x-dead-letter-routing-key", RabbitMQConstants.JUDGE_QUEUE + ".dlq").withArgument("x-message-ttl", 3600000) // 1小时TTL.withArgument("x-max-length", 10000) // 最大队列长度.build();}// 死信队列配置@Beanpublic Queue judgeDlq() {return QueueBuilder.durable(RabbitMQConstants.JUDGE_QUEUE + ".dlq").withArgument("x-message-ttl", 86400000) // 24小时TTL.withArgument("x-max-length", 1000).build();}@Beanpublic DirectExchange dlxExchange() {return new DirectExchange(RabbitMQConstants.DLX_EXCHANGE);}@Beanpublic Binding dlqBinding() {return BindingBuilder.bind(judgeDlq()).to(dlxExchange()).with(RabbitMQConstants.JUDGE_QUEUE + ".dlq");}// 消息监听器@Component@Slf4jpublic class JudgeMessageListener {@Autowiredprivate JudgeService judgeService;@RabbitListener(queues = RabbitMQConstants.JUDGE_QUEUE)@RabbitHandlerpublic void handleJudgeMessage(JudgeMessage message, Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag) {try {log.info("开始处理判题消息: messageId={}, submissionId={}", message.getMessageId(), message.getSubmissionId());// 执行判题逻辑judgeService.executeJudge(message);// 手动确认消息channel.basicAck(deliveryTag, false);log.info("判题消息处理完成: messageId={}", message.getMessageId());} catch (Exception e) {log.error("判题消息处理失败: messageId={}", message.getMessageId(), e);try {// 判断是否应该重试if (shouldRetry(message, e)) {// 拒绝消息并重新入队channel.basicNack(deliveryTag, false, true);} else {// 拒绝消息并不重新入队(进入死信队列)channel.basicNack(deliveryTag, false, false);}} catch (IOException ioException) {log.error("消息拒绝失败: messageId={}", message.getMessageId(), ioException);}}}private boolean shouldRetry(JudgeMessage message, Exception e) {// 根据异常类型决定是否重试if (e instanceof TemporaryFailureException) {return true;}if (e instanceof JudgeTimeoutException) {return false; // 超时错误不重试}// 默认重试3次return message.getRetryCount() < 3;}
}
}// 消息重试机制
@Component
public class MessageRetryService {
@Autowired
private RabbitTemplate rabbitTemplate;/*** 带重试的消息发送*/
public <T extends BaseMessage> void sendWithRetry(String exchange, String routingKey, T message, int maxRetries) {int attempt = 0;while (attempt <= maxRetries) {try {rabbitTemplate.convertAndSend(exchange, routingKey, message);log.info("消息发送成功: messageId={}, attempt={}", message.getMessageId(), attempt);return;} catch (Exception e) {attempt++;log.warn("消息发送失败: messageId={}, attempt={}, error={}", message.getMessageId(), attempt, e.getMessage());if (attempt > maxRetries) {log.error("消息发送最终失败: messageId={}, maxRetries={}", message.getMessageId(), maxRetries);throw new MessageSendException("消息发送失败,已达到最大重试次数", e);}// 指数退避try {long delay = (long) Math.pow(2, attempt) * 1000; // 2^attempt secondsThread.sleep(Math.min(delay, 30000)); // 最大延迟30秒} catch (InterruptedException ie) {Thread.currentThread().interrupt();throw new MessageSendException("消息发送被中断", ie);}}}
}

5. 前端架构与工程化

5.1 组件化架构设计

企业级Vue3项目结构深度设计

src/
├── apis/                    # API接口层
│   ├── modules/            # 模块化API
│   │   ├── question.js     # 题目相关API
│   │   ├── exam.js         # 竞赛相关API  
│   │   ├── user.js         # 用户相关API
│   │   ├── submission.js   # 提交记录API
│   │   └── message.js      # 消息系统API
│   ├── interceptors/       # 请求拦截器
│   │   ├── request.js      # 请求拦截
│   │   └── response.js     # 响应拦截
│   ├── constants/          # API常量
│   │   ├── endpoints.js    # 接口端点
│   │   └── error-codes.js  # 错误码
│   └── index.js            # API入口
├── assets/                 # 静态资源
│   ├── styles/            # 全局样式
│   │   ├── variables.scss # SCSS变量
│   │   ├── mixins.scss    # SCSS混入
│   │   ├── global.scss    # 全局样式
│   │   ├── theme/         # 主题样式
│   │   │   ├── light.scss
│   │   │   └── dark.scss
│   │   └── components/    # 组件样式
│   └── images/            # 图片资源
│       ├── icons/         # 图标
│       ├── backgrounds/   # 背景图
│       └── avatars/       # 头像
├── components/            # 组件库
│   ├── common/           # 通用组件
│   │   ├── OJButton/     # 按钮组件
│   │   │   ├── index.vue
│   │   │   └── props.ts
│   │   ├── OJInput/      # 输入框组件
│   │   ├── OJTable/      # 表格组件
│   │   ├── OJCodeEditor/ # 代码编辑器
│   │   ├── OJLoading/    # 加载组件
│   │   └── OJModal/      # 模态框组件
│   ├── business/         # 业务组件
│   │   ├── QuestionCard/ # 题目卡片
│   │   ├── SubmissionList/ # 提交列表
│   │   ├── ExamTimer/    # 竞赛计时器
│   │   ├── CodeResult/   # 代码执行结果
│   │   └── UserRank/     # 用户排名
│   └── layout/           # 布局组件
│       ├── AppHeader/    # 顶部导航
│       ├── AppSidebar/   # 侧边栏
│       ├── AppFooter/    # 底部
│       └── PageContainer/# 页面容器
├── composables/          # Vue3组合式函数
│   ├── usePagination.js  # 分页逻辑
│   ├── useCodeEditor.js  # 代码编辑器逻辑
│   ├── useUser.js        # 用户状态管理
│   ├── useWebSocket.js   # WebSocket连接
│   ├── useTheme.js       # 主题切换
│   ├── usePermission.js  # 权限管理
│   └── useLocalStorage.js # 本地存储
├── router/               # 路由配置
│   ├── index.js          # 路由入口
│   ├── routes/           # 路由模块
│   │   ├── question.js   # 题目路由
│   │   ├── exam.js       # 竞赛路由
│   │   ├── user.js       # 用户路由
│   │   └── admin.js      # 管理路由
│   └── guards/           # 路由守卫
│       ├── auth.js       # 认证守卫
│       ├── permission.js # 权限守卫
│       └── progress.js   # 进度条守卫
├── stores/               # 状态管理
│   ├── modules/         # Store模块
│   │   ├── user.js      # 用户状态
│   │   ├── question.js  # 题目状态
│   │   ├── exam.js      # 竞赛状态
│   │   └── app.js       # 应用状态
│   └── index.js         # Store入口
├── utils/               # 工具函数
│   ├── auth.js          # 认证工具
│   ├── request.js       # 请求工具
│   ├── validator.js     # 表单验证
│   ├── constants.js     # 常量定义
│   ├── formatter.js     # 格式化工具
│   ├── storage.js       # 存储工具
│   └── helper.js        # 辅助函数
├── views/               # 页面组件
│   ├── question/        # 题目相关页面
│   │   ├── List.vue     # 题目列表
│   │   ├── Detail.vue   # 题目详情
│   │   └── Solve.vue    # 解题页面
│   ├── exam/            # 竞赛相关页面
│   │   ├── List.vue     # 竞赛列表
│   │   ├── Detail.vue   # 竞赛详情
│   │   ├── Playing.vue  # 竞赛进行中
│   │   └── Result.vue   # 竞赛结果
│   ├── user/            # 用户相关页面
│   │   ├── Profile.vue  # 个人资料
│   │   ├── Submissions.vue # 提交记录
│   │   └── Messages.vue # 我的消息
│   └── admin/           # 管理后台
│       ├── Dashboard.vue # 仪表板
│       ├── QuestionManagement.vue # 题目管理
│       └── UserManagement.vue # 用户管理
└── main.js              # 应用入口

API层深度设计

// apis/modules/question.js
import request from '@/utils/request'// 题目相关API
export const questionApi = {// 获取题目列表getQuestionList(params) {return request({url: '/question/list',method: 'get',params})},// 获取题目详情getQuestionDetail(questionId) {return request({url: `/question/detail/${questionId}`,method: 'get'})},// 搜索题目searchQuestions(keyword, filters = {}) {return request({url: '/question/search',method: 'post',data: {keyword,...filters}})},// 创建题目(管理员)createQuestion(questionData) {return request({url: '/question/create',method: 'post',data: questionData})},// 更新题目(管理员)updateQuestion(questionId, questionData) {return request({url: `/question/update/${questionId}`,method: 'put',data: questionData})},// 获取题目统计getQuestionStats(questionId) {return request({url: `/question/stats/${questionId}`,method: 'get'})}
}// apis/interceptors/request.js
import { getToken } from '@/utils/auth'
import { ElMessage } from 'element-plus'let pendingRequests = new Map()// 生成请求key
function generateReqKey(config) {const { method, url, params, data } = configreturn [method, url, JSON.stringify(params), JSON.stringify(data)].join('&')
}// 添加请求到pending
function addPendingRequest(config) {const requestKey = generateReqKey(config)config.cancelToken = config.cancelToken || new axios.CancelToken(cancel => {if (!pendingRequests.has(requestKey)) {pendingRequests.set(requestKey, cancel)}})
}// 移除pending请求
function removePendingRequest(config) {const requestKey = generateReqKey(config)if (pendingRequests.has(requestKey)) {const cancel = pendingRequests.get(requestKey)cancel(requestKey)pendingRequests.delete(requestKey)}
}export const requestInterceptor = {onFulfilled: (config) => {// 移除重复请求removePendingRequest(config)// 添加当前请求addPendingRequest(config)// 添加认证tokenconst token = getToken()if (token) {config.headers['Authorization'] = `Bearer ${token}`}// 设置内容类型if (!config.headers['Content-Type']) {config.headers['Content-Type'] = 'application/json'}// 添加时间戳防止缓存if (config.method === 'get') {config.params = {...config.params,_t: Date.now()}}// 显示加载提示if (config.showLoading !== false) {config.loadingInstance = ElLoading.service({lock: true,text: '加载中...',background: 'rgba(0, 0, 0, 0.7)'})}return config},onRejected: (error) => {return Promise.reject(error)}
}// apis/interceptors/response.js
import { ElMessage, ElMessageBox } from 'element-plus'
import { removeToken } from '@/utils/auth'
import router from '@/router'export const responseInterceptor = {onFulfilled: (response) => {// 移除pending请求const config = response.configconst requestKey = generateReqKey(config)pendingRequests.delete(requestKey)// 关闭加载提示if (config.loadingInstance) {config.loadingInstance.close()}const { data } = responseconst { code, message } = data// 业务成功if (code === 1000) {return data}// 业务错误处理switch (code) {case 3001: // 未授权ElMessage.warning('登录已过期,请重新登录')removeToken()router.push('/login')breakcase 3002: // 参数错误ElMessage.warning(message || '参数错误')breakcase 3103: // 登录失败ElMessage.error(message || '用户名或密码错误')breakdefault:ElMessage.error(message || '操作失败')}return Promise.reject(new Error(message || 'Error'))},onRejected: (error) => {// 关闭加载提示if (error.config && error.config.loadingInstance) {error.config.loadingInstance.close()}// 移除pending请求if (error.config) {const requestKey = generateReqKey(error.config)pendingRequests.delete(requestKey)}// 错误处理if (axios.isCancel(error)) {console.log('请求被取消:', error.message)return Promise.reject(new Error('请求被取消'))}if (!error.response) {ElMessage.error('网络错误,请检查网络连接')return Promise.reject(error)}const { status, data } = error.responseswitch (status) {case 401:ElMessage.warning('未授权,请重新登录')removeToken()router.push('/login')breakcase 403:ElMessage.warning('没有权限访问该资源')breakcase 404:ElMessage.warning('请求的资源不存在')breakcase 500:ElMessage.error('服务器内部错误')breakcase 502:ElMessage.error('网关错误')breakcase 503:ElMessage.error('服务暂时不可用')breakcase 504:ElMessage.error('网关超时')breakdefault:ElMessage.error(data?.message || `请求错误: ${status}`)}return Promise.reject(error)}
}

5.2 高级代码编辑器实现

Monaco Editor深度集成与优化


### 5.2 高级代码编辑器实现**Monaco Editor深度集成**:```vue
<template><div class="code-editor-container"><div class="editor-header"><div class="language-selector"><el-select v-model="currentLanguage" @change="handleLanguageChange"><el-optionv-for="lang in supportedLanguages":key="lang.value":label="lang.label":value="lang.value"/></el-select></div><div class="editor-actions"><el-button @click="handleFormat" :disabled="!supportsFormatting"><i class="icon-format"></i>格式化</el-button><el-button @click="handleRun" type="primary"><i class="icon-run"></i>运行代码</el-button><el-button @click="handleSubmit" type="success"><i class="icon-submit"></i>提交代码</el-button></div></div><div class="editor-wrapper"><monaco-editorref="editorRef"v-model="code":language="currentLanguage":theme="editorTheme":options="editorOptions"@change="onCodeChange"@mount="onEditorMount"/></div><div class="editor-footer"><div class="status-bar"><span>行: {{ cursorPosition.lineNumber }}, 列: {{ cursorPosition.column }}</span><span>编码: UTF-8</span><span>语言: {{ currentLanguage.toUpperCase() }}</span></div></div></div>
</template><script setup>
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
import { useStore } from 'vuex'
import { ElMessage } from 'element-plus'
import { MonacoEditor } from '@/components/common'const props = defineProps({questionId: {type: String,required: true},initialCode: {type: String,default: ''},readOnly: {type: Boolean,default: false}
})const emit = defineEmits(['code-change', 'code-run', 'code-submit'])const store = useStore()
const editorRef = ref(null)
const monacoInstance = ref(null)// 响应式数据
const code = ref(props.initialCode || '')
const currentLanguage = ref('java')
const cursorPosition = ref({ lineNumber: 1, column: 1 })
const isEditorReady = ref(false)// 支持的语言列表
const supportedLanguages = computed(() => [{ value: 'java', label: 'Java', defaultCode: getJavaTemplate() },{ value: 'python', label: 'Python', defaultCode: getPythonTemplate() },{ value: 'cpp', label: 'C++', defaultCode: getCppTemplate() },{ value: 'javascript', label: 'JavaScript', defaultCode: getJavascriptTemplate() }
])// 编辑器配置
const editorOptions = computed(() => ({fontSize: 14,lineNumbers: 'on',roundedSelection: false,scrollBeyondLastLine: false,readOnly: props.readOnly,automaticLayout: true,minimap: { enabled: true },wordWrap: 'on',lineHeight: 20,letterSpacing: 0.5,cursorBlinking: 'blink',tabSize: 4,insertSpaces: true,detectIndentation: true,folding: true,foldingHighlight: true,showFoldingControls: 'mouseover',matchBrackets: 'always',scrollbar: {vertical: 'visible',horizontal: 'visible',useShadows: false},// 代码补全quickSuggestions: true,parameterHints: { enabled: true },// 错误检查glyphMargin: true,lightbulb: { enabled: true },// 主题相关colorDecorators: true
}))const editorTheme = ref('vs-dark')// 是否支持格式化
const supportsFormatting = computed(() => ['javascript', 'typescript', 'json', 'html', 'css'].includes(currentLanguage.value)
)// 编辑器事件处理
const onEditorMount = (editor, monaco) => {monacoInstance.value = monacoisEditorReady.value = true// 注册自定义主题monaco.editor.defineTheme('oj-theme', {base: 'vs-dark',inherit: true,rules: [{ token: 'comment', foreground: '6A9955', fontStyle: 'italic' },{ token: 'keyword', foreground: 'C586C0' },{ token: 'string', foreground: 'CE9178' },{ token: 'number', foreground: 'B5CEA8' }],colors: {'editor.background': '#1E1E1E','editor.foreground': '#D4D4D4'}})monaco.editor.setTheme('oj-theme')// 监听光标位置变化editor.onDidChangeCursorPosition((e) => {cursorPosition.value = e.position})// 注册代码补全registerCompletionProvider(monaco)
}const onCodeChange = (value) => {emit('code-change', {code: value,language: currentLanguage.value})// 自动保存到本地存储saveToLocalStorage()
}// 语言切换处理
const handleLanguageChange = (newLanguage) => {const languageConfig = supportedLanguages.value.find(lang => lang.value === newLanguage)if (languageConfig && code.value === getDefaultCode(currentLanguage.value)) {code.value = languageConfig.defaultCode}currentLanguage.value = newLanguage
}// 代码格式化
const handleFormat = async () => {if (!monacoInstance.value || !editorRef.value) returntry {const editor = editorRef.value.getEditor()const action = editor.getAction('editor.action.formatDocument')if (action) {await action.run()ElMessage.success('代码格式化完成')}} catch (error) {ElMessage.error('格式化失败: ' + error.message)}
}// 运行代码
const handleRun = () => {if (!validateCode()) returnemit('code-run', {code: code.value,language: currentLanguage.value,questionId: props.questionId})
}// 提交代码
const handleSubmit = () => {if (!validateCode()) returnemit('code-submit', {code: code.value,language: currentLanguage.value,questionId: props.questionId})
}// 代码验证
const validateCode = () => {if (!code.value.trim()) {ElMessage.warning('代码不能为空')return false}if (code.value.length > 10000) {ElMessage.warning('代码长度不能超过10000个字符')return false}return true
}// 代码模板
function getJavaTemplate() {return `import java.util.*;
import java.io.*;public class Main {public static void main(String[] args) {Scanner scanner = new Scanner(System.in);// 你的代码在这里scanner.close();}
}`
}function getPythonTemplate() {return `import sysdef main():# 你的代码在这里passif __name__ == "__main__":main()`
}// 本地存储管理
function saveToLocalStorage() {const key = `code_${props.questionId}_${currentLanguage.value}`localStorage.setItem(key, code.value)
}function loadFromLocalStorage() {const key = `code_${props.questionId}_${currentLanguage.value}`const savedCode = localStorage.getItem(key)if (savedCode) {code.value = savedCode}
}// 代码补全提供者
function registerCompletionProvider(monaco) {monaco.languages.registerCompletionItemProvider(currentLanguage.value, {provideCompletionItems: (model, position) => {const word = model.getWordUntilPosition(position)const range = {startLineNumber: position.lineNumber,endLineNumber: position.lineNumber,startColumn: word.startColumn,endColumn: word.endColumn}const suggestions = []// Java特定的代码补全if (currentLanguage.value === 'java') {suggestions.push({label: 'System.out.println',kind: monaco.languages.CompletionItemKind.Function,documentation: '输出文本到控制台',insertText: 'System.out.println(${1:""});',insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,range: range},{label: 'public static void main',kind: monaco.languages.CompletionItemKind.Snippet,documentation: '主方法模板',insertText: ['public static void main(String[] args) {','\t${1:// code here}','}'].join('\n'),insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,range: range})}return { suggestions }}})
}// 生命周期
onMounted(() => {loadFromLocalStorage()
})onUnmounted(() => {// 清理资源if (monacoInstance.value && editorRef.value) {const editor = editorRef.value.getEditor()editor.dispose()}
})// 监听props变化
watch(() => props.initialCode, (newCode) => {if (newCode && newCode !== code.value) {code.value = newCode}
})// 暴露方法给父组件
defineExpose({getCode: () => code.value,setCode: (newCode) => { code.value = newCode },getLanguage: () => currentLanguage.value,setLanguage: (lang) => { currentLanguage.value = lang },formatCode: handleFormat
})
</script><style lang="scss" scoped>
.code-editor-container {height: 100%;display: flex;flex-direction: column;border: 1px solid #dcdfe6;border-radius: 4px;overflow: hidden;.editor-header {display: flex;justify-content: space-between;align-items: center;padding: 8px 12px;background: #f5f7fa;border-bottom: 1px solid #dcdfe6;.language-selector {min-width: 120px;}.editor-actions {display: flex;gap: 8px;}}.editor-wrapper {flex: 1;min-height: 400px;}.editor-footer {background: #f5f7fa;border-top: 1px solid #dcdfe6;padding: 4px 12px;.status-bar {display: flex;justify-content: space-between;font-size: 12px;color: #909399;}}
}
</style>

6. 性能优化策略

6.1 多级缓存架构深度优化

Redis缓存策略优化

@Service
@Slf4j
public class AdvancedCacheService {@Autowiredprivate RedisTemplate<String, Object> redisTemplate;@Autowiredprivate RedissonClient redissonClient;// 缓存键前缀private static final String QUESTION_CACHE_PREFIX = "question:";private static final String USER_CACHE_PREFIX = "user:";private static final String EXAM_CACHE_PREFIX = "exam:";private static final String LEADERBOARD_PREFIX = "leaderboard:";/*** 多级缓存获取:本地缓存 + Redis*/public <T> T getWithMultiLevelCache(String key, Class<T> clazz, Supplier<T> loader, long expireTime) {// 第一级:本地缓存(Caffeine)T value = getFromLocalCache(key, clazz);if (value != null) {return value;}// 第二级:Redis分布式缓存value = getFromRedis(key, clazz);if (value != null) {// 回填本地缓存putToLocalCache(key, value);return value;}// 第三级:数据库加载return loadAndCache(key, clazz, loader, expireTime);}/*** 批量缓存操作*/public <T> Map<String, T> multiGet(List<String> keys, Class<T> clazz) {if (keys == null || keys.isEmpty()) {return Collections.emptyMap();}List<Object> values = redisTemplate.opsForValue().multiGet(keys);Map<String, T> result = new HashMap<>();for (int i = 0; i < keys.size(); i++) {if (values.get(i) != null) {result.put(keys.get(i), clazz.cast(values.get(i)));}}return result;}/*** 批量缓存设置*/public <T> void multiSet(Map<String, T> keyValueMap, long expireTime) {if (keyValueMap == null || keyValueMap.isEmpty()) {return;}Map<String, Object> redisMap = new HashMap<>();for (Map.Entry<String, T> entry : keyValueMap.entrySet()) {redisMap.put(entry.getKey(), entry.getValue());}redisTemplate.opsForValue().multiSet(redisMap);// 设置过期时间for (String key : keyValueMap.keySet()) {redisTemplate.expire(key, expireTime, TimeUnit.SECONDS);}}/*** 分布式锁保护缓存击穿*/public <T> T getWithLock(String key, Class<T> clazz, Supplier<T> loader, long expireTime) {// 先尝试从缓存获取T value = getFromRedis(key, clazz);if (value != null) {return value;}// 获取分布式锁RLock lock = redissonClient.getLock("lock:" + key);try {// 尝试加锁,最多等待5秒,锁持有30秒boolean locked = lock.tryLock(5, 30, TimeUnit.SECONDS);if (locked) {// 双重检查value = getFromRedis(key, clazz);if (value != null) {return value;}// 加载数据value = loader.get();if (value != null) {setToRedis(key, value, expireTime);}return value;} else {// 获取锁失败,返回空或默认值log.warn("获取分布式锁失败: {}", key);return null;}} catch (InterruptedException e) {Thread.currentThread().interrupt();throw new CacheException("缓存获取被中断", e);} finally {if (lock.isHeldByCurrentThread()) {lock.unlock();}}}/*** 缓存预热*/@Asyncpublic void preheatCache() {log.info("开始缓存预热...");// 预热热门题目preheatHotQuestions();// 预热排行榜preheatLeaderboards();// 预热系统配置preheatSystemConfig();log.info("缓存预热完成");}private void preheatHotQuestions() {List<Question> hotQuestions = questionService.getHotQuestions(100);Map<String, Object> cacheMap = new HashMap<>();for (Question question : hotQuestions) {String key = QUESTION_CACHE_PREFIX + question.getQuestionId();cacheMap.put(key, question);}multiSet(cacheMap, 30 * 60); // 30分钟}/*** 缓存统计和监控*/public CacheStats getCacheStats() {RMapCache<String, Object> statsMap = redissonClient.getMapCache("cache:stats");long hitCount = statsMap.get("hitCount", Long.class);long missCount = statsMap.get("missCount", Long.class);long totalCount = hitCount + missCount;double hitRate = totalCount > 0 ? (double) hitCount / totalCount : 0;return new CacheStats(hitCount, missCount, hitRate);}/*** 缓存清理策略*/@Scheduled(cron = "0 0 2 * * ?") // 每天凌晨2点执行public void cleanExpiredCache() {log.info("开始清理过期缓存...");// 扫描过期的缓存键Set<String> keys = redisTemplate.keys("*");int cleanedCount = 0;for (String key : keys) {Long ttl = redisTemplate.getExpire(key);if (ttl != null && ttl < 0) {// TTL为-1表示没有设置过期时间,-2表示键不存在redisTemplate.delete(key);cleanedCount++;}}log.info("缓存清理完成,共清理 {} 个键", cleanedCount);}// 本地缓存(Caffeine)配置@Beanpublic Cache<String, Object> localCache() {return Caffeine.newBuilder().maximumSize(1000).expireAfterWrite(10, TimeUnit.MINUTES).recordStats().build();}private <T> T getFromLocalCache(String key, Class<T> clazz) {try {Object value = localCache().getIfPresent(key);return clazz.cast(value);} catch (Exception e) {log.warn("本地缓存获取失败: {}", key, e);return null;}}private <T> void putToLocalCache(String key, T value) {try {localCache().put(key, value);} catch (Exception e) {log.warn("本地缓存写入失败: {}", key, e);}}private <T> T getFromRedis(String key, Class<T> clazz) {try {Object value = redisTemplate.opsForValue().get(key);return value != null ? clazz.cast(value) : null;} catch (Exception e) {log.warn("Redis缓存获取失败: {}", key, e);return null;}}private <T> void setToRedis(String key, T value, long expireTime) {try {redisTemplate.opsForValue().set(key, value, expireTime, TimeUnit.SECONDS);} catch (Exception e) {log.warn("Redis缓存写入失败: {}", key, e);}}private <T> T loadAndCache(String key, Class<T> clazz, Supplier<T> loader, long expireTime) {T value = loader.get();if (value != null) {setToRedis(key, value, expireTime);putToLocalCache(key, value);}return value;}
}

6.2 数据库深度优化

高级查询优化

-- 数据库性能优化配置
-- 1. 索引优化
CREATE INDEX idx_question_difficulty_status ON tb_question(difficulty, status);
CREATE INDEX idx_submission_user_question_status ON tb_submission(user_id, question_id, status);
CREATE INDEX idx_submission_create_time_desc ON tb_submission(create_time DESC);
CREATE INDEX idx_exam_status_times ON tb_exam(status, start_time, end_time);-- 2. 分区表管理
-- 提交记录表按时间分区(每月一个分区)
ALTER TABLE tb_submission PARTITION BY RANGE (TO_DAYS(create_time)) (PARTITION p202401 VALUES LESS THAN (TO_DAYS('2024-02-01')),PARTITION p202402 VALUES LESS THAN (TO_DAYS('2024-03-01')),PARTITION p202403 VALUES LESS THAN (TO_DAYS('2024-04-01')),PARTITION p_future VALUES LESS THAN MAXVALUE
);-- 3. 查询性能监控
-- 慢查询日志配置
SET GLOBAL slow_query_log = 1;
SET GLOBAL long_query_time = 2;
SET GLOBAL slow_query_log_file = '/var/log/mysql/slow.log';-- 4. 数据库连接池优化
-- application.yml 配置
spring:datasource:hikari:maximum-pool-size: 20minimum-idle: 5connection-timeout: 30000idle-timeout: 600000max-lifetime: 1800000connection-test-query: SELECT 1

MyBatis Plus查询优化

// 高级查询服务
@Service
@Slf4j
public class AdvancedQueryService {@Autowiredprivate QuestionMapper questionMapper;@Autowiredprivate SubmissionMapper submissionMapper;/*** 分页查询优化 - 避免深度分页*/public PageResult<QuestionVO> searchQuestionsOptimized(QuestionQueryDTO query) {// 使用游标分页替代传统分页if (query.getPageNum() > 100) {return searchQuestionsWithCursor(query);}// 传统分页Page<Question> page = new Page<>(query.getPageNum(), query.getPageSize());IPage<QuestionVO> result = questionMapper.selectQuestionPage(page, query);return PageResult.of(result);}/*** 游标分页查询*/private PageResult<QuestionVO> searchQuestionsWithCursor(QuestionQueryDTO query) {Long cursor = query.getCursor(); // 游标值(最后一条记录的ID)Integer size = query.getPageSize();List<QuestionVO> questions = questionMapper.selectQuestionsWithCursor(cursor, size);// 构建下一页游标Long nextCursor = null;if (questions.size() == size) {nextCursor = questions.get(questions.size() - 1).getQuestionId();}return PageResult.withCursor(questions, nextCursor);}/*** 批量插入优化*/@Transactionalpublic void batchInsertSubmissions(List<Submission> submissions) {if (submissions == null || submissions.isEmpty()) {return;}// 分批插入,避免单次插入数据量过大int batchSize = 1000;for (int i = 0; i < submissions.size(); i += batchSize) {int end = Math.min(i + batchSize, submissions.size());List<Submission> batch = submissions.subList(i, end);submissionMapper.insertBatchSomeColumn(batch);}}/*** 查询结果缓存*/@Cacheable(value = "question_search", key = "#query.hashCode()")public PageResult<QuestionVO> searchQuestionsCached(QuestionQueryDTO query) {return searchQuestionsOptimized(query);}/*** 统计查询优化*/public QuestionStatistics getQuestionStatistics(Long questionId) {// 使用单个查询获取所有统计信息return questionMapper.selectQuestionStatistics(questionId);}
}// 自定义SQL注入器
@Component
public class CustomSqlInjector extends DefaultSqlInjector {@Overridepublic List<AbstractMethod> getMethodList(Class<?> mapperClass) {List<AbstractMethod> methodList = super.getMethodList(mapperClass);// 添加自定义方法methodList.add(new InsertBatchSomeColumn());methodList.add(new SelectWithCursor());methodList.add(new UpdateOptimisticLock());return methodList;}
}// 乐观锁更新方法
public class UpdateOptimisticLock extends AbstractMethod {@Overridepublic MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {if (!tableInfo.isWithVersion()) {return null;}String sql = String.format("<script>UPDATE %s %s WHERE %s=#{%s} AND %s=#{%s}</script>",tableInfo.getTableName(),sqlSet(tableInfo.isWithLogicDelete(), false, tableInfo, false, "et", "et."),tableInfo.getKeyColumn(), tableInfo.getKeyProperty(),tableInfo.getVersionColumn(), tableInfo.getVersionProperty());SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, modelClass);return this.addUpdateMappedStatement(mapperClass, modelClass, "updateByIdWithVersion", sqlSource);}
}

7. 部署与运维体系

7.1 容器化部署深度设计

Docker多环境配置

# docker-compose.yml - 生产环境配置
version: '3.8'x-common-variables: &common-variablesSPRING_PROFILES_ACTIVE: prodNACOS_HOST: nacos-serverREDIS_HOST: redis-serverMYSQL_HOST: mysql-serverRABBITMQ_HOST: rabbitmq-serverservices:# 基础设施服务mysql-server:image: mysql:8.0container_name: oj-mysqlenvironment:MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}MYSQL_DATABASE: bitoj_prodMYSQL_USER: ${MYSQL_USER}MYSQL_PASSWORD: ${MYSQL_PASSWORD}ports:- "3306:3306"volumes:- mysql_data:/var/lib/mysql- ./config/mysql/conf.d:/etc/mysql/conf.d- ./backup/mysql:/backupcommand:- --character-set-server=utf8mb4- --collation-server=utf8mb4_unicode_ci- --innodb-buffer-pool-size=2G- --innodb-log-file-size=256M- --max-connections=1000restart: unless-stoppedhealthcheck:test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]timeout: 10sretries: 5redis-server:image: redis:6.2-alpinecontainer_name: oj-rediscommand: - redis-server- --requirepass ${REDIS_PASSWORD}- --maxmemory 1gb- --maxmemory-policy allkeys-lruports:- "6379:6379"volumes:- redis_data:/data- ./config/redis/redis.conf:/etc/redis/redis.confrestart: unless-stoppedhealthcheck:test: ["CMD", "redis-cli", "ping"]timeout: 5sretries: 5nacos-server:image: nacos/nacos-server:v2.2.3container_name: oj-nacosenvironment:- MODE=cluster- SPRING_DATASOURCE_PLATFORM=mysql- MYSQL_SERVICE_HOST=mysql-server- MYSQL_SERVICE_DB_NAME=bitoj_nacos- MYSQL_SERVICE_USER=${MYSQL_USER}- MYSQL_SERVICE_PASSWORD=${MYSQL_PASSWORD}- NACOS_SERVERS=nacos-server:8848- NACOS_APPLICATION_PORT=8848ports:- "8848:8848"volumes:- nacos_data:/home/nacos/data- nacos_logs:/home/nacos/logsrestart: unless-stoppeddepends_on:mysql-server:condition: service_healthyrabbitmq-server:image: rabbitmq:3.8-managementcontainer_name: oj-rabbitmqenvironment:- RABBITMQ_DEFAULT_USER=${RABBITMQ_USER}- RABBITMQ_DEFAULT_PASS=${RABBITMQ_PASSWORD}- RABBITMQ_DEFAULT_VHOST=/ports:- "5672:5672"- "15672:15672"volumes:- rabbitmq_data:/var/lib/rabbitmq- rabbitmq_logs:/var/log/rabbitmqrestart: unless-stoppedhealthcheck:test: ["CMD", "rabbitmqctl", "status"]timeout: 10sretries: 5# 业务微服务gateway-service:build:context: ./oj-gatewaydockerfile: Dockerfile.prodcontainer_name: oj-gatewayenvironment:<<: *common-variablesJAVA_OPTS: "-Xmx512m -Xms256m -XX:+UseG1GC -Djava.security.egd=file:/dev/./urandom"ports:- "19090:19090"depends_on:nacos-server:condition: service_startedredis-server:condition: service_healthyrestart: unless-stoppedhealthcheck:test: ["CMD", "curl", "-f", "http://localhost:19090/actuator/health"]interval: 30stimeout: 10sretries: 3user-service:build:context: ./oj-userdockerfile: Dockerfile.prodcontainer_name: oj-userenvironment:<<: *common-variablesJAVA_OPTS: "-Xmx1g -Xms512m -XX:+UseG1GC"deploy:replicas: 2depends_on:nacos-server:condition: service_startedrestart: unless-stoppedquestion-service:build:context: ./oj-questiondockerfile: Dockerfile.prodcontainer_name: oj-questionenvironment:<<: *common-variablesJAVA_OPTS: "-Xmx1g -Xms512m -XX:+UseG1GC"deploy:replicas: 2depends_on:nacos-server:condition: service_startedrestart: unless-stoppedjudge-service:build:context: ./oj-judgedockerfile: Dockerfile.prodcontainer_name: oj-judgeenvironment:<<: *common-variablesJAVA_OPTS: "-Xmx2g -Xms1g -XX:+UseG1GC"DOCKER_HOST: unix:///var/run/docker.sockvolumes:- /var/run/docker.sock:/var/run/docker.sockdeploy:replicas: 3depends_on:nacos-server:condition: service_startedrabbitmq-server:condition: service_healthyrestart: unless-stopped# 前端服务frontend-service:build:context: ./oj-frontenddockerfile: Dockerfile.prodcontainer_name: oj-frontendports:- "80:80"- "443:443"volumes:- ./config/nginx/nginx.conf:/etc/nginx/nginx.conf- ./config/nginx/conf.d:/etc/nginx/conf.d- ssl_certs:/etc/nginx/ssldepends_on:- gateway-servicerestart: unless-stoppedvolumes:mysql_data:redis_data:nacos_data:nacos_logs:rabbitmq_data:rabbitmq_logs:ssl_certs:networks:default:name: oj-networkdriver: bridge

Kubernetes生产部署

# k8s/namespace.yaml
apiVersion: v1
kind: Namespace
metadata:name: online-judgelabels:name: online-judge
---
# k8s/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:name: app-confignamespace: online-judge
data:application-prod.yml: |spring:datasource:url: jdbc:mysql://mysql-service:3306/bitoj_prod?useUnicode=true&characterEncoding=utf8&useSSL=falseusername: ${MYSQL_USER}password: ${MYSQL_PASSWORD}redis:host: redis-servicepassword: ${REDIS_PASSWORD}cloud:nacos:discovery:server-addr: nacos-service:8848config:server-addr: nacos-service:8848rabbitmq:host: rabbitmq-serviceusername: ${RABBITMQ_USER}password: ${RABBITMQ_PASSWORD}
---
# k8s/secret.yaml
apiVersion: v1
kind: Secret
metadata:name: app-secretsnamespace: online-judge
type: Opaque
data:mysql-user: <base64-encoded>mysql-password: <base64-encoded>redis-password: <base64-encoded>rabbitmq-user: <base64-encoded>rabbitmq-password: <base64-encoded>
---
# k8s/gateway-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:name: gateway-servicenamespace: online-judgelabels:app: gateway-service
spec:replicas: 2selector:matchLabels:app: gateway-servicetemplate:metadata:labels:app: gateway-serviceannotations:prometheus.io/scrape: "true"prometheus.io/port: "19090"prometheus.io/path: "/actuator/prometheus"spec:containers:- name: gateway-serviceimage: registry.example.com/oj-gateway:1.0.0ports:- containerPort: 19090env:- name: SPRING_PROFILES_ACTIVEvalue: "prod"- name: JAVA_OPTSvalue: "-Xmx512m -Xms256m -XX:+UseG1GC -Djava.security.egd=file:/dev/./urandom"resources:requests:memory: "512Mi"cpu: "250m"limits:memory: "1Gi"cpu: "500m"livenessProbe:httpGet:path: /actuator/healthport: 19090initialDelaySeconds: 60periodSeconds: 10readinessProbe:httpGet:path: /actuator/healthport: 19090initialDelaySeconds: 30periodSeconds: 5volumeMounts:- name: config-volumemountPath: /app/configvolumes:- name: config-volumeconfigMap:name: app-config
---
# k8s/gateway-service.yaml
apiVersion: v1
kind: Service
metadata:name: gateway-servicenamespace: online-judgelabels:app: gateway-service
spec:selector:app: gateway-serviceports:- port: 19090targetPort: 19090name: httptype: ClusterIP
---
# k8s/ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:name: oj-ingressnamespace: online-judgeannotations:nginx.ingress.kubernetes.io/rewrite-target: /nginx.ingress.kubernetes.io/ssl-redirect: "true"nginx.ingress.kubernetes.io/backend-protocol: "HTTP"
spec:tls:- hosts:- oj.example.comsecretName: oj-tls-secretrules:- host: oj.example.comhttp:paths:- path: /pathType: Prefixbackend:service:name: frontend-serviceport:number: 80- path: /apipathType: Prefixbackend:service:name: gateway-serviceport:number: 19090
---
# k8s/hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:name: gateway-hpanamespace: online-judge
spec:scaleTargetRef:apiVersion: apps/v1kind: Deploymentname: gateway-serviceminReplicas: 2maxReplicas: 10metrics:- type: Resourceresource:name: cputarget:type: UtilizationaverageUtilization: 70- type: Resourceresource:name: memorytarget:type: UtilizationaverageUtilization: 80

7.2 监控与日志体系

Prometheus + Grafana监控

# monitoring/prometheus.yml
global:scrape_interval: 15sevaluation_interval: 15srule_files:- "alert_rules.yml"scrape_configs:- job_name: 'online-judge'metrics_path: '/actuator/prometheus'static_configs:- targets: - 'gateway-service:19090'- 'user-service:8080'- 'question-service:8080'- 'judge-service:8080'relabel_configs:- source_labels: [__address__]target_label: instanceregex: '(.*):\d+'replacement: '${1}'- job_name: 'rabbitmq'static_configs:- targets: ['rabbitmq-service:15672']metrics_path: '/api/metrics'basic_auth:username: '${RABBITMQ_USER}'password: '${RABBITMQ_PASSWORD}'- job_name: 'mysql'static_configs:- targets: ['mysql-service:9104']metrics_path: '/metrics'- job_name: 'redis'static_configs:- targets: ['redis-service:9121']metrics_path: '/metrics'alerting:alertmanagers:- static_configs:- targets:- alertmanager:9093

Spring Boot Actuator深度配置

// 监控配置类
@Configuration
@EnableConfigurationProperties(value = {EndpointProperties.class, WebEndpointProperties.class})
public class MonitoringConfiguration {@Bean@ConfigurationProperties("management.endpoint.health")public HealthEndpointProperties healthEndpointProperties() {return new HealthEndpointProperties();}@Beanpublic HealthContributorRegistry healthContributorRegistry(ObjectProvider<HealthIndicator> healthIndicators,ObjectProvider<HealthContributor> healthContributors) {AutoConfiguredHealthContributorRegistry registry = new AutoConfiguredHealthContributorRegistry();healthIndicators.orderedStream().forEach(registry::registerContributor);healthContributors.orderedStream().forEach(registry::registerContributor);return registry;}@Beanpublic HealthEndpoint healthEndpoint(HealthContributorRegistry registry, HealthEndpointProperties properties) {return new HealthEndpoint(registry, properties.getShowDetails(), properties.getShowComponents());}// 自定义健康检查@Componentpublic class DatabaseHealthIndicator implements HealthIndicator {@Autowiredprivate DataSource dataSource;@Overridepublic Health health() {try (Connection connection = dataSource.getConnection()) {DatabaseMetaData metaData = connection.getMetaData();String databaseName = metaData.getDatabaseProductName();String databaseVersion = metaData.getDatabaseProductVersion();// 检查数据库连接池状态if (dataSource instanceof HikariDataSource) {HikariDataSource hikariDataSource = (HikariDataSource) dataSource;long activeConnections = hikariDataSource.getHikariPoolMXBean().getActiveConnections();long idleConnections = hikariDataSource.getHikariPoolMXBean().getIdleConnections();long totalConnections = hikariDataSource.getHikariPoolMXBean().getTotalConnections();return Health.up().withDetail("database", databaseName).withDetail("version", databaseVersion).withDetail("activeConnections", activeConnections).withDetail("idleConnections", idleConnections).withDetail("totalConnections", totalConnections).build();}return Health.up().withDetail("database", databaseName).withDetail("version", databaseVersion).build();} catch (Exception e) {return Health.down(e).build();}}}@Componentpublic class RedisHealthIndicator implements HealthIndicator {@Autowiredprivate RedisTemplate<String, Object> redisTemplate;@Overridepublic Health health() {try {// 测试Redis连接String testKey = "health:check:" + System.currentTimeMillis();redisTemplate.opsForValue().set(testKey, "test", Duration.ofSeconds(10));String value = (String) redisTemplate.opsForValue().get(testKey);redisTemplate.delete(testKey);if ("test".equals(value)) {return Health.up().withDetail("version", getRedisVersion()).withDetail("memory", getRedisMemoryInfo()).build();} else {return Health.down().withDetail("error", "Redis test failed").build();}} catch (Exception e) {return Health.down(e).build();}}private String getRedisVersion() {try {Properties info = redisTemplate.getRequiredConnectionFactory().getConnection().info("server");return info.getProperty("redis_version");} catch (Exception e) {return "unknown";}}private String getRedisMemoryInfo() {try {Properties info = redisTemplate.getRequiredConnectionFactory().getConnection().info("memory");return info.getProperty("used_memory_human") + " / " + info.getProperty("maxmemory_human");} catch (Exception e) {return "unknown";}}}// 自定义业务指标@Componentpublic class BusinessMetrics {private final Counter submissionCounter;private final Counter acceptedCounter;private final Gauge acceptanceRate;private final Timer judgeTimer;private final DistributionSummary codeSizeSummary;public BusinessMetrics(MeterRegistry registry) {this.submissionCounter = Counter.builder("oj.submission.total").description("Total number of code submissions").register(registry);this.acceptedCounter = Counter.builder("oj.submission.accepted").description("Number of accepted submissions").register(registry);this.acceptanceRate = Gauge.builder("oj.submission.acceptance.rate").description("Code acceptance rate").register(registry);this.judgeTimer = Timer.builder("oj.judge.duration").description("Time taken for code judgment").publishPercentiles(0.5, 0.95, 0.99).register(registry);this.codeSizeSummary = DistributionSummary.builder("oj.code.size").description("Size of submitted code").baseUnit("characters").register(registry);}public void recordSubmission(boolean accepted, long codeSize, long judgeTime) {submissionCounter.increment();if (accepted) {acceptedCounter.increment();}// 更新通过率double rate = (double) acceptedCounter.count() / submissionCounter.count();acceptanceRate.set(rate);// 记录执行时间judgeTimer.record(judgeTime, TimeUnit.MILLISECONDS);// 记录代码大小codeSizeSummary.record(codeSize);}}
}

ELK日志收集配置

# docker-compose.logging.yml
version: '3.8'services:elasticsearch:image: docker.elastic.co/elasticsearch/elasticsearch:7.17.0container_name: elasticsearchenvironment:- discovery.type=single-node- "ES_JAVA_OPTS=-Xms512m -Xmx512m"volumes:- elasticsearch_data:/usr/share/elasticsearch/dataports:- "9200:9200"networks:- logginglogstash:image: docker.elastic.co/logstash/logstash:7.17.0container_name: logstashvolumes:- ./config/logstash/logstash.conf:/usr/share/logstash/pipeline/logstash.conf- ./config/logstash/logstash.yml:/usr/share/logstash/config/logstash.ymlports:- "5044:5044"- "5000:5000/tcp"- "5000:5000/udp"- "9600:9600"environment:LS_JAVA_OPTS: "-Xmx256m -Xms256m"depends_on:- elasticsearchnetworks:- loggingkibana:image: docker.elastic.co/kibana/kibana:7.17.0container_name: kibanaports:- "5601:5601"environment:ELASTICSEARCH_HOSTS: http://elasticsearch:9200depends_on:- elasticsearchnetworks:- loggingfilebeat:image: docker.elastic.co/beats/filebeat:7.17.0container_name: filebeatvolumes:- ./config/filebeat/filebeat.yml:/usr/share/filebeat/filebeat.yml- /var/lib/docker/containers:/var/lib/docker/containers:ro- /var/run/docker.sock:/var/run/docker.sockdepends_on:- logstashnetworks:- loggingvolumes:elasticsearch_data:networks:logging:driver: bridge
# docker-compose.logging.yml
version: '3.8'services:elasticsearch:image: docker.elastic.co/elasticsearch/elasticsearch:7.17.0container_name: elasticsearchenvironment:- discovery.type=single-node- "ES_JAVA_OPTS=-Xms512m -Xmx512m"volumes:- elasticsearch_data:/usr/share/elasticsearch/dataports:- "9200:9200"networks:- logginglogstash:image: docker.elastic.co/logstash/logstash:7.17.0container_name: logstashvolumes:- ./config/logstash/logstash.conf:/usr/share/logstash/pipeline/logstash.conf- ./config/logstash/logstash.yml:/usr/share/logstash/config/logstash.ymlports:- "5044:5044"- "5000:5000/tcp"- "5000:5000/udp"- "9600:9600"environment:LS_JAVA_OPTS: "-Xmx256m -Xms256m"depends_on:- elasticsearchnetworks:- loggingkibana:image: docker.elastic.co/kibana/kibana:7.17.0container_name: kibanaports:- "5601:5601"environment:ELASTICSEARCH_HOSTS: http://elasticsearch:9200depends_on:- elasticsearchnetworks:- loggingfilebeat:image: docker.elastic.co/beats/filebeat:7.17.0container_name: filebeatvolumes:- ./config/filebeat/filebeat.yml:/usr/share/filebeat/filebeat.yml- /var/lib/docker/containers:/var/lib/docker/containers:ro- /var/run/docker.sock:/var/run/docker.sockdepends_on:- logstashnetworks:- loggingvolumes:elasticsearch_data:networks:logging:driver: bridge
# config/logstash/logstash.conf
input {beats {port => 5044}
}filter {# 解析Docker日志if [docker][container][labels][com_docker_compose_project] == "online-judge" {grok {match => { "message" => "\[%{TIMESTAMP_ISO8601:timestamp}\] \[%{DATA:thread}\] %{LOGLEVEL:loglevel} %{DATA:logger} - \[%{DATA:method},%{NUMBER:line}\] - %{GREEDYDATA:message}" }}# 解析JSON格式的日志if [message] =~ /^{.*}$/ {json {source => "message"target => "json_content"}}# 添加业务标签if [docker][container][labels][com_docker_compose_service] {mutate {add_field => { "service" => "%{[docker][container][labels][com_docker_compose_service]}""environment" => "production"}}}# 日期处理date {match => [ "timestamp", "ISO8601" ]target => "@timestamp"}}
}output {elasticsearch {hosts => ["elasticsearch:9200"]index => "online-judge-%{+YYYY.MM.dd}"}# 开发环境同时输出到控制台if [environment] == "development" {stdout { codec => rubydebug }}
}

8. 安全防护体系

8.1 全面的安全防护

Web安全深度配置

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {@Autowiredprivate TokenService tokenService;@Autowiredprivate UserDetailsService userDetailsService;@Overrideprotected void configure(HttpSecurity http) throws Exception {http// CSRF配置.csrf().disable()// 会话管理.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()// 异常处理.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint()).accessDeniedHandler(accessDeniedHandler()).and()// 权限配置.authorizeRequests()// 公开接口.antMatchers("/auth/login","/auth/register", "/auth/refresh","/public/**","/v3/api-docs/**","/swagger-ui/**","/swagger-resources/**","/webjars/**").permitAll()// 用户接口.antMatchers("/user/**").hasAnyRole("USER", "ADMIN")// 管理员接口.antMatchers("/admin/**").hasRole("ADMIN")// 需要认证的接口.anyRequest().authenticated().and()// JWT过滤器.addFilterBefore(jwtAuthenticationTokenFilter(), UsernamePasswordAuthenticationFilter.class)// 安全头配置.headers().contentSecurityPolicy("default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'").and().httpStrictTransportSecurity().includeSubDomains(true).maxAgeInSeconds(31536000).and().frameOptions().deny().xssProtection().block(true);}@Beanpublic JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter() {return new JwtAuthenticationTokenFilter(tokenService);}@Beanpublic AuthenticationEntryPoint authenticationEntryPoint() {return (request, response, authException) -> {response.setContentType(MediaType.APPLICATION_JSON_VALUE);response.setStatus(HttpStatus.UNAUTHORIZED.value());R<String> result = R.fail(ResultCode.FAILED_UNAUTHORIZED.getCode(), "认证失败: " + authException.getMessage());response.getWriter().write(JSON.toJSONString(result));};}@Beanpublic AccessDeniedHandler accessDeniedHandler() {return (request, response, accessDeniedException) -> {response.setContentType(MediaType.APPLICATION_JSON_VALUE);response.setStatus(HttpStatus.FORBIDDEN.value());R<String> result = R.fail(ResultCode.FAILED_FORBIDDEN.getCode(),"权限不足: " + accessDeniedException.getMessage());response.getWriter().write(JSON.toJSONString(result));};}@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());}@Bean@Overridepublic AuthenticationManager authenticationManagerBean() throws Exception {return super.authenticationManagerBean();}// 安全审计@Beanpublic AuditorAware<Long> auditorAware() {return new SecurityAuditorAware();}
}// 安全审计组件
@Component
public class SecurityAuditorAware implements AuditorAware<Long> {@Overridepublic Optional<Long> getCurrentAuditor() {Authentication authentication = SecurityContextHolder.getContext().getAuthentication();if (authentication == null || !authentication.isAuthenticated()) {return Optional.empty();}if (authentication.getPrincipal() instanceof LoginUser) {LoginUser loginUser = (LoginUser) authentication.getPrincipal();return Optional.of(loginUser.getUserId());}return Optional.empty();}
}// 请求限流配置
@Configuration
public class RateLimitConfiguration {@Beanpublic SentinelResourceAspect sentinelResourceAspect() {return new SentinelResourceAspect();}@Beanpublic FilterRegistrationBean<SentinelFilter> sentinelFilter() {FilterRegistrationBean<SentinelFilter> registration = new FilterRegistrationBean<>();registration.setFilter(new SentinelFilter());registration.addUrlPatterns("/*");registration.setName("sentinelFilter");registration.setOrder(1);return registration;}
}// 自定义Sentinel过滤器
public class SentinelFilter implements Filter {@Overridepublic void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)throws IOException, ServletException {HttpServletRequest httpRequest = (HttpServletRequest) request;String path = httpRequest.getRequestURI();String method = httpRequest.getMethod();// 限流规则检查if (!passRateLimit(path, method)) {((HttpServletResponse) response).setStatus(429);response.getWriter().write("{\"code\":429,\"message\":\"请求过于频繁,请稍后重试\"}");return;}chain.doFilter(request, response);}private boolean passRateLimit(String path, String method) {// 实现具体的限流逻辑// 可以根据路径、方法、用户等进行限流return true;}
}

代码安全沙箱增强

@Service
public class EnhancedCodeSandbox {private static final Set<String> FORBIDDEN_SYSTEM_PROPERTIES = Set.of("java.home", "user.dir", "user.home", "java.io.tmpdir");/*** 深度代码安全检查*/public SecurityCheckResult deepSecurityCheck(String code, String language) {SecurityCheckResult result = new SecurityCheckResult();// 1. 基础安全检查result.merge(basicSecurityCheck(code, language));// 2. 语言特定检查result.merge(languageSpecificCheck(code, language));// 3. 复杂度检查result.merge(complexityCheck(code));// 4. 资源使用预测result.merge(resourceUsagePrediction(code, language));return result;}private SecurityCheckResult basicSecurityCheck(String code, String language) {SecurityCheckResult result = new SecurityCheckResult();// 检查危险系统调用checkDangerousSystemCalls(code, result);// 检查文件操作checkFileOperations(code, result);// 检查网络操作checkNetworkOperations(code, result);// 检查反射操作checkReflectionOperations(code, result);// 检查线程操作checkThreadOperations(code, result);return result;}private void checkDangerousSystemCalls(String code, SecurityCheckResult result) {Pattern dangerousPatterns = Pattern.compile("Runtime\\.getRuntime\\(\\)\\.exec\\s*\\(|" +"System\\.exit\\s*\\(|" +"ProcessBuilder|" +"UNSAFE|" +"Unsafe\\.getUnsafe|" +"sun\\.misc|" +"jdk\\.internal",Pattern.CASE_INSENSITIVE);Matcher matcher = dangerousPatterns.matcher(code);while (matcher.find()) {result.addRisk(new SecurityRisk("DANGEROUS_SYSTEM_CALL","检测到危险系统调用: " + matcher.group(),SecurityLevel.HIGH));}}private void checkFileOperations(String code, SecurityCheckResult result) {Pattern filePatterns = Pattern.compile("File\\.|" +"FileInputStream|" +"FileOutputStream|" +"Files\\.|" +"Paths\\.get|" +"RandomAccessFile",Pattern.CASE_INSENSITIVE);Matcher matcher = filePatterns.matcher(code);while (matcher.find()) {result.addRisk(new SecurityRisk("FILE_OPERATION","检测到文件操作: " + matcher.group(),SecurityLevel.MEDIUM));}}/*** 代码复杂度分析*/private SecurityCheckResult complexityCheck(String code) {SecurityCheckResult result = new SecurityCheckResult();// 计算圈复杂度int cyclomaticComplexity = calculateCyclomaticComplexity(code);if (cyclomaticComplexity > 20) {result.addRisk(new SecurityRisk("HIGH_COMPLEXITY","代码圈复杂度过高: " + cyclomaticComplexity,SecurityLevel.MEDIUM));}// 检查嵌套深度int maxNestingDepth = calculateMaxNestingDepth(code);if (maxNestingDepth > 5) {result.addRisk(new SecurityRisk("DEEP_NESTING","代码嵌套深度过大: " + maxNestingDepth,SecurityLevel.MEDIUM));}// 检查递归调用if (hasDeepRecursion(code)) {result.addRisk(new SecurityRisk("DEEP_RECURSION","检测到深层递归调用",SecurityLevel.HIGH));}return result;}/*** 资源使用预测*/private SecurityCheckResult resourceUsagePrediction(String code, String language) {SecurityCheckResult result = new SecurityCheckResult();// 预测内存使用long estimatedMemory = estimateMemoryUsage(code, language);if (estimatedMemory > 100 * 1024 * 1024) { // 100MBresult.addRisk(new SecurityRisk("HIGH_MEMORY_USAGE","预测内存使用过高: " + (estimatedMemory / 1024 / 1024) + "MB",SecurityLevel.MEDIUM));}// 预测执行时间long estimatedTime = estimateExecutionTime(code, language);if (estimatedTime > 5000) { // 5秒result.addRisk(new SecurityRisk("LONG_EXECUTION_TIME", "预测执行时间过长: " + estimatedTime + "ms",SecurityLevel.MEDIUM));}return result;}// 安全执行环境public ExecuteResult executeInSecureEnvironment(ExecuteRequest request) {// 创建安全策略文件String policyFile = createSecurityPolicyFile(request.getLanguage());// 使用SecurityManager执行代码System.setSecurityManager(new OJSecurityManager());try {return doExecuteWithSecurityManager(request, policyFile);} finally {System.setSecurityManager(null);}}// 自定义SecurityManagerpublic static class OJSecurityManager extends SecurityManager {@Overridepublic void checkExec(String cmd) {throw new SecurityException("执行系统命令被禁止");}@Overridepublic void checkRead(String file) {// 只允许读取特定目录的文件if (!file.startsWith("/tmp/oj/")) {throw new SecurityException("文件读取被禁止: " + file);}}@Overridepublic void checkWrite(String file) {// 只允许写入特定目录的文件if (!file.startsWith("/tmp/oj/")) {throw new SecurityException("文件写入被禁止: " + file);}}@Overridepublic void checkConnect(String host, int port) {throw new SecurityException("网络连接被禁止");}@Overridepublic void checkCreateClassLoader() {throw new SecurityException("创建类加载器被禁止");}@Overridepublic void checkExit(int status) {throw new SecurityException("退出虚拟机被禁止");}}
}

9. 项目总结与展望

9.1 技术架构总结

架构演进历程

技术决策回顾

技术领域决策内容效果评估改进方向
微服务框架Spring Cloud Alibaba开发效率高,生态完善考虑Service Mesh
数据库MySQL + 分库分表满足当前性能需求引入时序数据库
缓存Redis集群性能提升明显增加本地缓存
消息队列RabbitMQ稳定可靠考虑Kafka
搜索Elasticsearch搜索性能优秀优化索引策略

9.2 性能指标达成

系统性能基准

// 性能测试报告
public class PerformanceReport {// 并发处理能力private int maxConcurrentUsers = 5000;private double throughput = 1200; // 请求/秒private double averageResponseTime = 85; // 毫秒// 代码判题性能private int judgeThroughput = 200; // 判题/分钟private double judgeSuccessRate = 99.8; // %// 系统可用性private double availability = 99.95; // %private double errorRate = 0.02; // %// 资源利用率private double cpuUtilization = 65; // %private double memoryUtilization = 70; // %private double diskUtilization = 45; // %
}

9.3 业务价值实现

教育价值体现

  1. 学习路径优化:基于用户行为数据的个性化推荐

  2. 技能评估:多维度的编程能力评估体系

  3. 竞赛体系:完整的竞赛生命周期管理

  4. 社区互动:代码评审、讨论区等社交功能

商业价值实现

  1. 技术面试:为企业提供技术人才评估解决方案

  2. 教育培训:为教育机构提供在线编程教学平台

  3. 技能认证:建立行业认可的编程技能认证体系

9.4 未来演进方向

技术演进规划

具体演进计划

  1. 短期(6个月)

    • 性能优化:缓存策略优化,数据库查询优化

    • 体验提升:前端性能优化,移动端适配

    • 监控完善:APM全链路监控,智能告警

  2. 中期(1-2年)

    • 云原生:全面转向Kubernetes,服务网格

    • AI集成:智能题目推荐,代码自动评分

    • 多租户:支持SaaS化部署,租户隔离

  3. 长期(2-3年)

    • 平台化:开放API,第三方应用集成

    • 国际化:多语言支持,全球部署

    • 生态建设:开发者社区,插件市场

10. 监控与告警系统

10.1 全链路监控

分布式追踪配置

// SkyWalking配置
@Configuration
public class TracingConfiguration {@Beanpublic Tracing tracing() {return Tracing.newBuilder().localServiceName("online-judge").spanReporter(spanReporter()).currentTraceContext(currentTraceContext()).build();}@Beanpublic SpanReporter spanReporter() {return new ZipkinReporter();}@Beanpublic CurrentTraceContext currentTraceContext() {return CurrentTraceContext.Default.create();}@Beanpublic TracingFilter tracingFilter() {return new TracingFilter(tracing());}
}// 自定义业务追踪
@Aspect
@Component
@Slf4j
public class BusinessTracingAspect {private final Tracer tracer;public BusinessTracingAspect(Tracer tracer) {this.tracer = tracer;}@Around("@annotation(BusinessTrace)")public Object traceBusinessMethod(ProceedingJoinPoint joinPoint) throws Throwable {String methodName = joinPoint.getSignature().getName();String className = joinPoint.getTarget().getClass().getSimpleName();ScopedSpan span = tracer.startScopedSpan(className + "." + methodName);try {span.tag("class", className);span.tag("method", methodName);Object result = joinPoint.proceed();span.finish();return result;} catch (Exception e) {span.error(e);span.finish();throw e;}}
}// 自定义注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface BusinessTrace {String value() default "";
}

10.2 智能告警系统

告警规则配置

# alert_rules.yml
groups:
- name: online-judge-alertsrules:- alert: HighErrorRateexpr: rate(http_requests_total{status=~"5.."}[5m]) > 0.1for: 2mlabels:severity: criticalservice: online-judgeannotations:summary: "高错误率告警"description: "错误率超过10%,当前值: {{ $value }}"- alert: HighResponseTimeexpr: histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m])) > 1for: 3mlabels:severity: warningservice: online-judgeannotations:summary: "高响应时间告警"description: "95%分位响应时间超过1秒,当前值: {{ $value }}s"- alert: ServiceDownexpr: up{job=~".*"} == 0for: 1mlabels:severity: criticalservice: online-judgeannotations:summary: "服务下线告警"description: "服务 {{ $labels.instance }} 已下线"- alert: HighMemoryUsageexpr: container_memory_usage_bytes{container!="POD"} / container_spec_memory_limit_bytes > 0.8for: 5mlabels:severity: warningservice: online-judgeannotations:summary: "高内存使用告警"description: "内存使用率超过80%,当前值: {{ $value }}"- alert: JudgeQueueBacklogexpr: rabbitmq_queue_messages_ready{queue="judge_queue"} > 1000for: 2mlabels:severity: warningservice: online-judgeannotations:summary: "判题队列积压告警"description: "判题队列积压超过1000,当前值: {{ $value }}"

告警通知配置

@Service
@Slf4j
public class AlertNotificationService {@Autowiredprivate MessageService messageService;@Autowiredprivate EmailService emailService;@Autowiredprivate SmsService smsService;@EventListenerpublic void handleAlertEvent(AlertEvent event) {log.info("处理告警事件: {}", event);// 根据告警级别发送不同通知switch (event.getSeverity()) {case CRITICAL:sendCriticalAlert(event);break;case WARNING:sendWarningAlert(event);break;case INFO:sendInfoAlert(event);break;}// 记录告警到数据库saveAlertRecord(event);}private void sendCriticalAlert(AlertEvent event) {// 发送短信通知smsService.sendCriticalAlert(event);// 发送邮件通知emailService.sendCriticalAlert(event);// 发送系统通知messageService.sendSystemAlert(event);}private void sendWarningAlert(AlertEvent event) {// 发送邮件通知emailService.sendWarningAlert(event);// 发送系统通知messageService.sendSystemAlert(event);}private void sendInfoAlert(AlertEvent event) {// 发送系统通知messageService.sendSystemAlert(event);}private void saveAlertRecord(AlertEvent event) {AlertRecord record = AlertRecord.builder().alertName(event.getAlertName()).severity(event.getSeverity()).description(event.getDescription()).startTime(event.getStartTime()).endTime(event.getEndTime()).status(AlertStatus.ACTIVE).build();alertRecordRepository.save(record);}// 告警自动恢复检测@Scheduled(fixedRate = 60000) // 每分钟检查一次public void checkAlertRecovery() {List<AlertRecord> activeAlerts = alertRecordRepository.findByStatus(AlertStatus.ACTIVE);for (AlertRecord alert : activeAlerts) {if (isAlertRecovered(alert)) {alert.setStatus(AlertStatus.RESOLVED);alert.setEndTime(LocalDateTime.now());alertRecordRepository.save(alert);// 发送恢复通知sendRecoveryNotification(alert);}}}
}

11. 持续集成与部署

11.1 GitLab CI/CD流水线

# .gitlab-ci.yml
stages:- test- build- security-scan- deployvariables:MAVEN_OPTS: "-Dmaven.repo.local=.m2/repository"cache:paths:- .m2/repository/- target/# 单元测试
unit-test:stage: testimage: maven:3.8-openjdk-17script:- mvn clean test- mvn jacoco:reportartifacts:paths:- target/site/jacoco/reports:junit:- target/surefire-reports/TEST-*.xmlonly:- merge_requests- main- develop# 集成测试
integration-test:stage: testimage: maven:3.8-openjdk-17services:- mysql:8.0- redis:6.2- rabbitmq:3.8-managementvariables:MYSQL_DATABASE: test_dbMYSQL_ROOT_PASSWORD: testREDIS_PASSWORD: testRABBITMQ_DEFAULT_USER: guestRABBITMQ_DEFAULT_PASS: guestscript:- mvn verify -P integration-testonly:- merge_requests- main# 代码质量检查
sonarqube-check:stage: testimage: maven:3.8-openjdk-17variables:SONAR_USER_HOME: "${CI_PROJECT_DIR}/.sonar"GIT_DEPTH: "0"cache:paths:- .sonar/cachescript:- mvn sonar:sonar -Dsonar.projectKey=online-judgeallow_failure: trueonly:- merge_requests- main# 安全扫描
security-scan:stage: security-scanimage: name: aquasec/trivy:latestentrypoint: [""]variables:TRIVY_NO_PROGRESS: "true"script:- trivy fs --severity HIGH,CRITICAL --exit-code 1 .- trivy config --severity HIGH,CRITICAL --exit-code 1 .allow_failure: falseonly:- merge_requests- main# Docker镜像构建
build-backend:stage: buildimage: docker:20.10services:- docker:20.10-dindvariables:DOCKER_HOST: tcp://docker:2376DOCKER_TLS_CERTDIR: "/certs"before_script:- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRYscript:- |for service in gateway user question judge; dodocker build -t $CI_REGISTRY_IMAGE/$service:${CI_COMMIT_SHA} -f $service/Dockerfile.prod $servicedocker push $CI_REGISTRY_IMAGE/$service:${CI_COMMIT_SHA}doneonly:- main# 部署到开发环境
deploy-dev:stage: deployimage: name: bitnami/kubectl:latestentrypoint: [""]script:- kubectl config use-context dev-cluster- |for service in gateway user question judge; dokubectl set image deployment/$service-service $service=$CI_REGISTRY_IMAGE/$service:${CI_COMMIT_SHA} -n online-judge-devdoneenvironment:name: developmenturl: https://dev-oj.example.comonly:- develop# 部署到生产环境
deploy-prod:stage: deployimage:name: bitnami/kubectl:latestentrypoint: [""]before_script:- echo $KUBECONFIG | base64 -d > kubeconfig- export KUBECONFIG=kubeconfigscript:- kubectl config use-context prod-cluster- |for service in gateway user question judge; dokubectl set image deployment/$service-service $service=$CI_REGISTRY_IMAGE/$service:${CI_COMMIT_SHA} -n online-judgedone- kubectl rollout status deployment/gateway-service -n online-judge --timeout=300senvironment:name: productionurl: https://oj.example.comwhen: manualonly:- main

11.2 自动化测试策略

测试金字塔实现

// 单元测试示例
@ExtendWith(MockitoExtension.class)
class QuestionServiceTest {@Mockprivate QuestionRepository questionRepository;@Mockprivate CacheService cacheService;@InjectMocksprivate QuestionService questionService;@Testvoid shouldReturnQuestionWhenExists() {// GivenLong questionId = 1L;Question question = createTestQuestion(questionId);when(questionRepository.findById(questionId)).thenReturn(Optional.of(question));when(cacheService.get(anyString(), eq(Question.class))).thenReturn(null);// WhenQuestion result = questionService.getQuestionById(questionId);// ThenassertThat(result).isNotNull();assertThat(result.getId()).isEqualTo(questionId);verify(questionRepository).findById(questionId);verify(cacheService).put(anyString(), eq(question), anyLong());}@Testvoid shouldThrowExceptionWhenQuestionNotFound() {// GivenLong questionId = 999L;when(questionRepository.findById(questionId)).thenReturn(Optional.empty());// When & ThenassertThatThrownBy(() -> questionService.getQuestionById(questionId)).isInstanceOf(QuestionNotFoundException.class).hasMessage("题目不存在: " + questionId);}
}// 集成测试示例
@SpringBootTest
@ActiveProfiles("test")
@Testcontainers
class QuestionServiceIntegrationTest {@Containerstatic MySQLContainer<?> mysql = new MySQLContainer<>("mysql:8.0").withDatabaseName("test_db").withUsername("test").withPassword("test");@Containerstatic GenericContainer<?> redis = new GenericContainer<>("redis:6.2").withExposedPorts(6379);@DynamicPropertySourcestatic void configureProperties(DynamicPropertyRegistry registry) {registry.add("spring.datasource.url", mysql::getJdbcUrl);registry.add("spring.datasource.username", mysql::getUsername);registry.add("spring.datasource.password", mysql::getPassword);registry.add("spring.redis.host", redis::getHost);registry.add("spring.redis.port", redis::getFirstMappedPort);}@Autowiredprivate QuestionService questionService;@Autowiredprivate QuestionRepository questionRepository;@Testvoid shouldCreateQuestionSuccessfully() {// GivenCreateQuestionRequest request = CreateQuestionRequest.builder().title("两数之和").description("给定一个整数数组...").difficulty("MEDIUM").tags(List.of("数组", "哈希表")).build();// WhenQuestion created = questionService.createQuestion(request);// ThenassertThat(created).isNotNull();assertThat(created.getId()).isNotNull();assertThat(created.getTitle()).isEqualTo("两数之和");// 验证数据库Optional<Question> saved = questionRepository.findById(created.getId());assertThat(saved).isPresent();}
}// 性能测试示例
@SpringBootTest
@ActiveProfiles("perf")
class QuestionServicePerformanceTest {@Autowiredprivate QuestionService questionService;@Testvoid shouldHandleConcurrentRequests() {// Givenint concurrentUsers = 100;int requestsPerUser = 10;// Whenlong startTime = System.currentTimeMillis();CompletableFuture<?>[] futures = new CompletableFuture[concurrentUsers];for (int i = 0; i < concurrentUsers; i++) {futures[i] = CompletableFuture.runAsync(() -> {for (int j = 0; j < requestsPerUser; j++) {questionService.getQuestionById((long) (j % 100 + 1));}});}CompletableFuture.allOf(futures).join();long endTime = System.currentTimeMillis();long totalTime = endTime - startTime;// ThenassertThat(totalTime).isLessThan(5000); // 5秒内完成double throughput = (concurrentUsers * requestsPerUser) / (totalTime / 1000.0);assertThat(throughput).isGreaterThan(100); // 每秒100请求}
}

总结

智码判官项目展示了现代Web应用的完整开发流程,从需求分析、架构设计、技术选型到具体实现,涵盖了微服务、前后端分离、容器化等主流技术实践。项目不仅提供了在线判题的核心功能,还具备了良好的扩展性、可维护性和性能表现。

这个项目为企业级应用开发提供了完整的参考实现,具有很高的学习价值和技术参考价值。无论是对于个人开发者学习现代Web开发技术,还是对于企业构建类似的在线教育平台,都具有重要的指导意义。

技术栈的深度整合 + 业务场景的完整覆盖 + 企业级的最佳实践 = 一个值得学习和参考的优秀项目案例

http://www.dtcms.com/a/609241.html

相关文章:

  • 基于 HT 技术的汽车制造车间数字孪生可视化系统
  • TOMCAT Docker 容器化部署指南
  • 自己做网站去哪买服务器天猫网站设计
  • React Native 自定义 ScrollView 滚动条:开箱即用的 IndicatorScrollView(附源码示例)
  • Java Web核心数据交互技术全解析
  • UML建模工具Enterprise Architect在DevOps中如何实现架构模型同步
  • 数据库-MYSQL作业五
  • 磁共振成像原理(理论)36:回波平面成像 (Echo-Planar Imaging)
  • mysql占用内存过大问题排查
  • 手游网站做cpc还是cpm广告号岳阳高端网站建设
  • 通过美剧学英语---学习笔记(2)
  • 【数据结构】排序详解:从快速排序分区逻辑,到携手冒泡排序的算法效率深度评测
  • 设计稿还原技巧:解决间距、阴影、字体适配的细节问题
  • 【034】Dubbo3从0到1系列之dubbo-remoting模块
  • 【数据结构】并查集(操作详解 + 模板 + 练习)
  • JS Map 函数的二度回炉
  • 网站建设类公司排名wordpress3.5.2
  • uniapp写H5授权登录及分享,返回到目标页面
  • 奥卡姆剃刀原理:机器学习中的简约哲学与实践指南
  • ASC学习笔记0007:用于与GameplayAbilities系统交互的核心ActorComponent
  • 福永附近做网站公司广州公共资源交易中心交易平台
  • 深入理解 Swift TaskGroup:从基础用法到性能优化的完整指南
  • csharp通过对象和模板字符串解析模板
  • MYSQL结构操作DDL指令1.数据库操作
  • 为什么会有免费制作网站wordpress建站腾讯云
  • 仓颉迁移实战:将 Node.js 微服务移植到 Cangjie 的工程化评测
  • Redis(六)——哨兵
  • 网站错敏词整改报告,如何整改后如何定期自查自检
  • 网站验收时项目建设总结报告网站建设与维护本科教材
  • 【Java】使用国密2,3,4.仿照https 统一请求响应加解密