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

源码链接: 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)
    );

  2. 竞品功能矩阵分析

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

2.1.2 需求规格说明书

功能需求详细分解

  1. 用户管理模块

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

题目管理模块

复制代码
public class QuestionSpecification {
    // 题目属性
    private Long questionId;
    private String title;
    private String description;
    private String difficulty; // EASY, MEDIUM, HARD
    private 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(功能相对简单)
分布式事务 Seata AT模式无侵入、支持多种事务模式 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 {
    @Id
    private Long questionId;
    
    private String title;
    private String description;
    private QuestionDifficulty difficulty;
    
    @Embedded
    private QuestionContent content;
    
    @OneToMany(mappedBy = "question")
    private List<TestCase> testCases;
    
    @Embedded
    private 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-service
  cloud:
    nacos:
      discovery:
        server-addr: ${NACOS_HOST:localhost}:8848
        namespace: ${NACOS_NAMESPACE:bitcoj}
        group: ${NACOS_GROUP:DEFAULT_GROUP}
        cluster-name: ${NACOS_CLUSTER:DEFAULT}
        # 服务元数据
        metadata:
          version: 1.0.0
          environment: ${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,metrics
  endpoint:
    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 {
    
    @Bean
    public ServiceInstanceListSupplier judgeServiceInstanceListSupplier() {
        return ServiceInstanceListSupplier.builder()
                .withBlockingDiscoveryClient()
                .withSameInstancePreference() // 优先选择相同实例
                .withHealthChecks() // 健康检查
                .build();
    }
}
3.2.2 服务间通信模式

同步通信 - OpenFeign增强配置

复制代码
@Configuration
public class FeignConfiguration {
    
    @Bean
    public Logger.Level feignLoggerLevel() {
        return Logger.Level.FULL;
    }
    
    @Bean
    public RequestInterceptor authRequestInterceptor() {
        return template -> {
            // 自动传递认证信息
            String token = SecurityContextHolder.getContext().getAuthentication().getCredentials().toString();
            template.header("Authorization", "Bearer " + token);
        };
    }
    
    @Bean
    public 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 {
    
    @Autowired
    private RabbitTemplate rabbitTemplate;
    
    @Autowired
    private 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 {
    
    @Autowired
    private 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);
        
        // 生成JWT
        String 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 {
            // 解析JWT
            Claims 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() {
            @Override
            public 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();
    
    @Override
    public 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);
        }
    }
    
    @Override
    public 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 {
    
    @Autowired
    private 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"
    );
    
    @Override
    public 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 {
    
    @Autowired
    private JudgeTaskDispatcher taskDispatcher;
    
    @Autowired
    private SubmissionService submissionService;
    
    @Autowired
    private QuestionService questionService;
    
    @Autowired
    private 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 {
    
    @Bean
    public 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);
    }
    
    @Bean
    public 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;
    }
    
    // 判题消息队列配置
    @Bean
    public 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();
    }
    
    // 死信队列配置
    @Bean
    public Queue judgeDlq() {
        return QueueBuilder.durable(RabbitMQConstants.JUDGE_QUEUE + ".dlq")
            .withArgument("x-message-ttl", 86400000) // 24小时TTL
            .withArgument("x-max-length", 1000)
            .build();
    }
    
    @Bean
    public DirectExchange dlxExchange() {
        return new DirectExchange(RabbitMQConstants.DLX_EXCHANGE);
    }
    
    @Bean
    public Binding dlqBinding() {
        return BindingBuilder.bind(judgeDlq())
            .to(dlxExchange())
            .with(RabbitMQConstants.JUDGE_QUEUE + ".dlq");
    }
    
    // 消息监听器
    @Component
    @Slf4j
    public class JudgeMessageListener {
        
        @Autowired
        private JudgeService judgeService;
        
        @RabbitListener(queues = RabbitMQConstants.JUDGE_QUEUE)
        @RabbitHandler
        public 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 seconds
                Thread.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 } = config
  return [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)
    
    // 添加认证token
    const 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.config
    const requestKey = generateReqKey(config)
    pendingRequests.delete(requestKey)
    
    // 关闭加载提示
    if (config.loadingInstance) {
      config.loadingInstance.close()
    }
    
    const { data } = response
    const { code, message } = data
    
    // 业务成功
    if (code === 1000) {
      return data
    }
    
    // 业务错误处理
    switch (code) {
      case 3001: // 未授权
        ElMessage.warning('登录已过期,请重新登录')
        removeToken()
        router.push('/login')
        break
        
      case 3002: // 参数错误
        ElMessage.warning(message || '参数错误')
        break
        
      case 3103: // 登录失败
        ElMessage.error(message || '用户名或密码错误')
        break
        
      default:
        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.response
    
    switch (status) {
      case 401:
        ElMessage.warning('未授权,请重新登录')
        removeToken()
        router.push('/login')
        break
        
      case 403:
        ElMessage.warning('没有权限访问该资源')
        break
        
      case 404:
        ElMessage.warning('请求的资源不存在')
        break
        
      case 500:
        ElMessage.error('服务器内部错误')
        break
        
      case 502:
        ElMessage.error('网关错误')
        break
        
      case 503:
        ElMessage.error('服务暂时不可用')
        break
        
      case 504:
        ElMessage.error('网关超时')
        break
        
      default:
        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-option
            v-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-editor
        ref="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 = monaco
  isEditorReady.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) return
  
  try {
    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()) return
  
  emit('code-run', {
    code: code.value,
    language: currentLanguage.value,
    questionId: props.questionId
  })
}

// 提交代码
const handleSubmit = () => {
  if (!validateCode()) return
  
  emit('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 sys

def main():
    # 你的代码在这里
    pass

if __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 {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    @Autowired
    private 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();
            }
        }
    }
    
    /**
     * 缓存预热
     */
    @Async
    public 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)配置
    @Bean
    public 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: 20
      minimum-idle: 5
      connection-timeout: 30000
      idle-timeout: 600000
      max-lifetime: 1800000
      connection-test-query: SELECT 1

MyBatis Plus查询优化

复制代码
// 高级查询服务
@Service
@Slf4j
public class AdvancedQueryService {
    
    @Autowired
    private QuestionMapper questionMapper;
    
    @Autowired
    private 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);
    }
    
    /**
     * 批量插入优化
     */
    @Transactional
    public 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 {
    
    @Override
    public 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 {
    
    @Override
    public 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-variables
  SPRING_PROFILES_ACTIVE: prod
  NACOS_HOST: nacos-server
  REDIS_HOST: redis-server
  MYSQL_HOST: mysql-server
  RABBITMQ_HOST: rabbitmq-server

services:
  # 基础设施服务
  mysql-server:
    image: mysql:8.0
    container_name: oj-mysql
    environment:
      MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
      MYSQL_DATABASE: bitoj_prod
      MYSQL_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:/backup
    command:
      - --character-set-server=utf8mb4
      - --collation-server=utf8mb4_unicode_ci
      - --innodb-buffer-pool-size=2G
      - --innodb-log-file-size=256M
      - --max-connections=1000
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      timeout: 10s
      retries: 5

  redis-server:
    image: redis:6.2-alpine
    container_name: oj-redis
    command: 
      - redis-server
      - --requirepass ${REDIS_PASSWORD}
      - --maxmemory 1gb
      - --maxmemory-policy allkeys-lru
    ports:
      - "6379:6379"
    volumes:
      - redis_data:/data
      - ./config/redis/redis.conf:/etc/redis/redis.conf
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      timeout: 5s
      retries: 5

  nacos-server:
    image: nacos/nacos-server:v2.2.3
    container_name: oj-nacos
    environment:
      - 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=8848
    ports:
      - "8848:8848"
    volumes:
      - nacos_data:/home/nacos/data
      - nacos_logs:/home/nacos/logs
    restart: unless-stopped
    depends_on:
      mysql-server:
        condition: service_healthy

  rabbitmq-server:
    image: rabbitmq:3.8-management
    container_name: oj-rabbitmq
    environment:
      - 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/rabbitmq
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "rabbitmqctl", "status"]
      timeout: 10s
      retries: 5

  # 业务微服务
  gateway-service:
    build:
      context: ./oj-gateway
      dockerfile: Dockerfile.prod
    container_name: oj-gateway
    environment:
      <<: *common-variables
      JAVA_OPTS: "-Xmx512m -Xms256m -XX:+UseG1GC -Djava.security.egd=file:/dev/./urandom"
    ports:
      - "19090:19090"
    depends_on:
      nacos-server:
        condition: service_started
      redis-server:
        condition: service_healthy
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:19090/actuator/health"]
      interval: 30s
      timeout: 10s
      retries: 3

  user-service:
    build:
      context: ./oj-user
      dockerfile: Dockerfile.prod
    container_name: oj-user
    environment:
      <<: *common-variables
      JAVA_OPTS: "-Xmx1g -Xms512m -XX:+UseG1GC"
    deploy:
      replicas: 2
    depends_on:
      nacos-server:
        condition: service_started
    restart: unless-stopped

  question-service:
    build:
      context: ./oj-question
      dockerfile: Dockerfile.prod
    container_name: oj-question
    environment:
      <<: *common-variables
      JAVA_OPTS: "-Xmx1g -Xms512m -XX:+UseG1GC"
    deploy:
      replicas: 2
    depends_on:
      nacos-server:
        condition: service_started
    restart: unless-stopped

  judge-service:
    build:
      context: ./oj-judge
      dockerfile: Dockerfile.prod
    container_name: oj-judge
    environment:
      <<: *common-variables
      JAVA_OPTS: "-Xmx2g -Xms1g -XX:+UseG1GC"
      DOCKER_HOST: unix:///var/run/docker.sock
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
    deploy:
      replicas: 3
    depends_on:
      nacos-server:
        condition: service_started
      rabbitmq-server:
        condition: service_healthy
    restart: unless-stopped

  # 前端服务
  frontend-service:
    build:
      context: ./oj-frontend
      dockerfile: Dockerfile.prod
    container_name: oj-frontend
    ports:
      - "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/ssl
    depends_on:
      - gateway-service
    restart: unless-stopped

volumes:
  mysql_data:
  redis_data:
  nacos_data:
  nacos_logs:
  rabbitmq_data:
  rabbitmq_logs:
  ssl_certs:

networks:
  default:
    name: oj-network
    driver: bridge

Kubernetes生产部署

复制代码
# k8s/namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
  name: online-judge
  labels:
    name: online-judge
---
# k8s/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config
  namespace: online-judge
data:
  application-prod.yml: |
    spring:
      datasource:
        url: jdbc:mysql://mysql-service:3306/bitoj_prod?useUnicode=true&characterEncoding=utf8&useSSL=false
        username: ${MYSQL_USER}
        password: ${MYSQL_PASSWORD}
      redis:
        host: redis-service
        password: ${REDIS_PASSWORD}
      cloud:
        nacos:
          discovery:
            server-addr: nacos-service:8848
          config:
            server-addr: nacos-service:8848
    rabbitmq:
      host: rabbitmq-service
      username: ${RABBITMQ_USER}
      password: ${RABBITMQ_PASSWORD}
---
# k8s/secret.yaml
apiVersion: v1
kind: Secret
metadata:
  name: app-secrets
  namespace: 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-service
  namespace: online-judge
  labels:
    app: gateway-service
spec:
  replicas: 2
  selector:
    matchLabels:
      app: gateway-service
  template:
    metadata:
      labels:
        app: gateway-service
      annotations:
        prometheus.io/scrape: "true"
        prometheus.io/port: "19090"
        prometheus.io/path: "/actuator/prometheus"
    spec:
      containers:
      - name: gateway-service
        image: registry.example.com/oj-gateway:1.0.0
        ports:
        - containerPort: 19090
        env:
        - name: SPRING_PROFILES_ACTIVE
          value: "prod"
        - name: JAVA_OPTS
          value: "-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/health
            port: 19090
          initialDelaySeconds: 60
          periodSeconds: 10
        readinessProbe:
          httpGet:
            path: /actuator/health
            port: 19090
          initialDelaySeconds: 30
          periodSeconds: 5
        volumeMounts:
        - name: config-volume
          mountPath: /app/config
      volumes:
      - name: config-volume
        configMap:
          name: app-config
---
# k8s/gateway-service.yaml
apiVersion: v1
kind: Service
metadata:
  name: gateway-service
  namespace: online-judge
  labels:
    app: gateway-service
spec:
  selector:
    app: gateway-service
  ports:
  - port: 19090
    targetPort: 19090
    name: http
  type: ClusterIP
---
# k8s/ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: oj-ingress
  namespace: online-judge
  annotations:
    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.com
    secretName: oj-tls-secret
  rules:
  - host: oj.example.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: frontend-service
            port:
              number: 80
      - path: /api
        pathType: Prefix
        backend:
          service:
            name: gateway-service
            port:
              number: 19090
---
# k8s/hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: gateway-hpa
  namespace: online-judge
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: gateway-service
  minReplicas: 2
  maxReplicas: 10
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 70
  - type: Resource
    resource:
      name: memory
      target:
        type: Utilization
        averageUtilization: 80

7.2 监控与日志体系

Prometheus + Grafana监控

复制代码
# monitoring/prometheus.yml
global:
  scrape_interval: 15s
  evaluation_interval: 15s

rule_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: instance
        regex: '(.*):\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();
    }
    
    @Bean
    public HealthContributorRegistry healthContributorRegistry(
            ObjectProvider<HealthIndicator> healthIndicators,
            ObjectProvider<HealthContributor> healthContributors) {
        
        AutoConfiguredHealthContributorRegistry registry = 
            new AutoConfiguredHealthContributorRegistry();
        
        healthIndicators.orderedStream().forEach(registry::registerContributor);
        healthContributors.orderedStream().forEach(registry::registerContributor);
        
        return registry;
    }
    
    @Bean
    public HealthEndpoint healthEndpoint(HealthContributorRegistry registry, 
                                       HealthEndpointProperties properties) {
        return new HealthEndpoint(registry, properties.getShowDetails(), 
            properties.getShowComponents());
    }
    
    // 自定义健康检查
    @Component
    public class DatabaseHealthIndicator implements HealthIndicator {
        
        @Autowired
        private DataSource dataSource;
        
        @Override
        public 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();
            }
        }
    }
    
    @Component
    public class RedisHealthIndicator implements HealthIndicator {
        
        @Autowired
        private RedisTemplate<String, Object> redisTemplate;
        
        @Override
        public 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";
            }
        }
    }
    
    // 自定义业务指标
    @Component
    public 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.0
    container_name: elasticsearch
    environment:
      - discovery.type=single-node
      - "ES_JAVA_OPTS=-Xms512m -Xmx512m"
    volumes:
      - elasticsearch_data:/usr/share/elasticsearch/data
    ports:
      - "9200:9200"
    networks:
      - logging

  logstash:
    image: docker.elastic.co/logstash/logstash:7.17.0
    container_name: logstash
    volumes:
      - ./config/logstash/logstash.conf:/usr/share/logstash/pipeline/logstash.conf
      - ./config/logstash/logstash.yml:/usr/share/logstash/config/logstash.yml
    ports:
      - "5044:5044"
      - "5000:5000/tcp"
      - "5000:5000/udp"
      - "9600:9600"
    environment:
      LS_JAVA_OPTS: "-Xmx256m -Xms256m"
    depends_on:
      - elasticsearch
    networks:
      - logging

  kibana:
    image: docker.elastic.co/kibana/kibana:7.17.0
    container_name: kibana
    ports:
      - "5601:5601"
    environment:
      ELASTICSEARCH_HOSTS: http://elasticsearch:9200
    depends_on:
      - elasticsearch
    networks:
      - logging

  filebeat:
    image: docker.elastic.co/beats/filebeat:7.17.0
    container_name: filebeat
    volumes:
      - ./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.sock
    depends_on:
      - logstash
    networks:
      - logging

volumes:
  elasticsearch_data:

networks:
  logging:
    driver: bridge

# docker-compose.logging.yml
version: '3.8'

services:
  elasticsearch:
    image: docker.elastic.co/elasticsearch/elasticsearch:7.17.0
    container_name: elasticsearch
    environment:
      - discovery.type=single-node
      - "ES_JAVA_OPTS=-Xms512m -Xmx512m"
    volumes:
      - elasticsearch_data:/usr/share/elasticsearch/data
    ports:
      - "9200:9200"
    networks:
      - logging

  logstash:
    image: docker.elastic.co/logstash/logstash:7.17.0
    container_name: logstash
    volumes:
      - ./config/logstash/logstash.conf:/usr/share/logstash/pipeline/logstash.conf
      - ./config/logstash/logstash.yml:/usr/share/logstash/config/logstash.yml
    ports:
      - "5044:5044"
      - "5000:5000/tcp"
      - "5000:5000/udp"
      - "9600:9600"
    environment:
      LS_JAVA_OPTS: "-Xmx256m -Xms256m"
    depends_on:
      - elasticsearch
    networks:
      - logging

  kibana:
    image: docker.elastic.co/kibana/kibana:7.17.0
    container_name: kibana
    ports:
      - "5601:5601"
    environment:
      ELASTICSEARCH_HOSTS: http://elasticsearch:9200
    depends_on:
      - elasticsearch
    networks:
      - logging

  filebeat:
    image: docker.elastic.co/beats/filebeat:7.17.0
    container_name: filebeat
    volumes:
      - ./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.sock
    depends_on:
      - logstash
    networks:
      - logging

volumes:
  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 {
    
    @Autowired
    private TokenService tokenService;
    
    @Autowired
    private UserDetailsService userDetailsService;
    
    @Override
    protected 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);
    }
    
    @Bean
    public JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter() {
        return new JwtAuthenticationTokenFilter(tokenService);
    }
    
    @Bean
    public 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));
        };
    }
    
    @Bean
    public 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));
        };
    }
    
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
    
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
    }
    
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
    
    // 安全审计
    @Bean
    public AuditorAware<Long> auditorAware() {
        return new SecurityAuditorAware();
    }
}

// 安全审计组件
@Component
public class SecurityAuditorAware implements AuditorAware<Long> {
    
    @Override
    public 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 {
    
    @Bean
    public SentinelResourceAspect sentinelResourceAspect() {
        return new SentinelResourceAspect();
    }
    
    @Bean
    public 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 {
    
    @Override
    public 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) { // 100MB
            result.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);
        }
    }
    
    // 自定义SecurityManager
    public static class OJSecurityManager extends SecurityManager {
        
        @Override
        public void checkExec(String cmd) {
            throw new SecurityException("执行系统命令被禁止");
        }
        
        @Override
        public void checkRead(String file) {
            // 只允许读取特定目录的文件
            if (!file.startsWith("/tmp/oj/")) {
                throw new SecurityException("文件读取被禁止: " + file);
            }
        }
        
        @Override
        public void checkWrite(String file) {
            // 只允许写入特定目录的文件
            if (!file.startsWith("/tmp/oj/")) {
                throw new SecurityException("文件写入被禁止: " + file);
            }
        }
        
        @Override
        public void checkConnect(String host, int port) {
            throw new SecurityException("网络连接被禁止");
        }
        
        @Override
        public void checkCreateClassLoader() {
            throw new SecurityException("创建类加载器被禁止");
        }
        
        @Override
        public 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 {
    
    @Bean
    public Tracing tracing() {
        return Tracing.newBuilder()
            .localServiceName("online-judge")
            .spanReporter(spanReporter())
            .currentTraceContext(currentTraceContext())
            .build();
    }
    
    @Bean
    public SpanReporter spanReporter() {
        return new ZipkinReporter();
    }
    
    @Bean
    public CurrentTraceContext currentTraceContext() {
        return CurrentTraceContext.Default.create();
    }
    
    @Bean
    public 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-alerts
  rules:
  - alert: HighErrorRate
    expr: rate(http_requests_total{status=~"5.."}[5m]) > 0.1
    for: 2m
    labels:
      severity: critical
      service: online-judge
    annotations:
      summary: "高错误率告警"
      description: "错误率超过10%,当前值: {{ $value }}"
      
  - alert: HighResponseTime
    expr: histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m])) > 1
    for: 3m
    labels:
      severity: warning
      service: online-judge
    annotations:
      summary: "高响应时间告警"
      description: "95%分位响应时间超过1秒,当前值: {{ $value }}s"
      
  - alert: ServiceDown
    expr: up{job=~".*"} == 0
    for: 1m
    labels:
      severity: critical
      service: online-judge
    annotations:
      summary: "服务下线告警"
      description: "服务 {{ $labels.instance }} 已下线"
      
  - alert: HighMemoryUsage
    expr: container_memory_usage_bytes{container!="POD"} / container_spec_memory_limit_bytes > 0.8
    for: 5m
    labels:
      severity: warning
      service: online-judge
    annotations:
      summary: "高内存使用告警"
      description: "内存使用率超过80%,当前值: {{ $value }}"
      
  - alert: JudgeQueueBacklog
    expr: rabbitmq_queue_messages_ready{queue="judge_queue"} > 1000
    for: 2m
    labels:
      severity: warning
      service: online-judge
    annotations:
      summary: "判题队列积压告警"
      description: "判题队列积压超过1000,当前值: {{ $value }}"

告警通知配置

复制代码
@Service
@Slf4j
public class AlertNotificationService {
    
    @Autowired
    private MessageService messageService;
    
    @Autowired
    private EmailService emailService;
    
    @Autowired
    private SmsService smsService;
    
    @EventListener
    public 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
  - deploy

variables:
  MAVEN_OPTS: "-Dmaven.repo.local=.m2/repository"

cache:
  paths:
    - .m2/repository/
    - target/

# 单元测试
unit-test:
  stage: test
  image: maven:3.8-openjdk-17
  script:
    - mvn clean test
    - mvn jacoco:report
  artifacts:
    paths:
      - target/site/jacoco/
    reports:
      junit:
        - target/surefire-reports/TEST-*.xml
  only:
    - merge_requests
    - main
    - develop

# 集成测试
integration-test:
  stage: test
  image: maven:3.8-openjdk-17
  services:
    - mysql:8.0
    - redis:6.2
    - rabbitmq:3.8-management
  variables:
    MYSQL_DATABASE: test_db
    MYSQL_ROOT_PASSWORD: test
    REDIS_PASSWORD: test
    RABBITMQ_DEFAULT_USER: guest
    RABBITMQ_DEFAULT_PASS: guest
  script:
    - mvn verify -P integration-test
  only:
    - merge_requests
    - main

# 代码质量检查
sonarqube-check:
  stage: test
  image: maven:3.8-openjdk-17
  variables:
    SONAR_USER_HOME: "${CI_PROJECT_DIR}/.sonar"
    GIT_DEPTH: "0"
  cache:
    paths:
      - .sonar/cache
  script:
    - mvn sonar:sonar -Dsonar.projectKey=online-judge
  allow_failure: true
  only:
    - merge_requests
    - main

# 安全扫描
security-scan:
  stage: security-scan
  image: 
    name: aquasec/trivy:latest
    entrypoint: [""]
  variables:
    TRIVY_NO_PROGRESS: "true"
  script:
    - trivy fs --severity HIGH,CRITICAL --exit-code 1 .
    - trivy config --severity HIGH,CRITICAL --exit-code 1 .
  allow_failure: false
  only:
    - merge_requests
    - main

# Docker镜像构建
build-backend:
  stage: build
  image: docker:20.10
  services:
    - docker:20.10-dind
  variables:
    DOCKER_HOST: tcp://docker:2376
    DOCKER_TLS_CERTDIR: "/certs"
  before_script:
    - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
  script:
    - |
      for service in gateway user question judge; do
        docker build -t $CI_REGISTRY_IMAGE/$service:${CI_COMMIT_SHA} -f $service/Dockerfile.prod $service
        docker push $CI_REGISTRY_IMAGE/$service:${CI_COMMIT_SHA}
      done
  only:
    - main

# 部署到开发环境
deploy-dev:
  stage: deploy
  image: 
    name: bitnami/kubectl:latest
    entrypoint: [""]
  script:
    - kubectl config use-context dev-cluster
    - |
      for service in gateway user question judge; do
        kubectl set image deployment/$service-service $service=$CI_REGISTRY_IMAGE/$service:${CI_COMMIT_SHA} -n online-judge-dev
      done
  environment:
    name: development
    url: https://dev-oj.example.com
  only:
    - develop

# 部署到生产环境
deploy-prod:
  stage: deploy
  image:
    name: bitnami/kubectl:latest
    entrypoint: [""]
  before_script:
    - echo $KUBECONFIG | base64 -d > kubeconfig
    - export KUBECONFIG=kubeconfig
  script:
    - kubectl config use-context prod-cluster
    - |
      for service in gateway user question judge; do
        kubectl set image deployment/$service-service $service=$CI_REGISTRY_IMAGE/$service:${CI_COMMIT_SHA} -n online-judge
      done
    - kubectl rollout status deployment/gateway-service -n online-judge --timeout=300s
  environment:
    name: production
    url: https://oj.example.com
  when: manual
  only:
    - main

11.2 自动化测试策略

测试金字塔实现

复制代码
// 单元测试示例
@ExtendWith(MockitoExtension.class)
class QuestionServiceTest {
    
    @Mock
    private QuestionRepository questionRepository;
    
    @Mock
    private CacheService cacheService;
    
    @InjectMocks
    private QuestionService questionService;
    
    @Test
    void shouldReturnQuestionWhenExists() {
        // Given
        Long questionId = 1L;
        Question question = createTestQuestion(questionId);
        when(questionRepository.findById(questionId)).thenReturn(Optional.of(question));
        when(cacheService.get(anyString(), eq(Question.class))).thenReturn(null);
        
        // When
        Question result = questionService.getQuestionById(questionId);
        
        // Then
        assertThat(result).isNotNull();
        assertThat(result.getId()).isEqualTo(questionId);
        verify(questionRepository).findById(questionId);
        verify(cacheService).put(anyString(), eq(question), anyLong());
    }
    
    @Test
    void shouldThrowExceptionWhenQuestionNotFound() {
        // Given
        Long questionId = 999L;
        when(questionRepository.findById(questionId)).thenReturn(Optional.empty());
        
        // When & Then
        assertThatThrownBy(() -> questionService.getQuestionById(questionId))
            .isInstanceOf(QuestionNotFoundException.class)
            .hasMessage("题目不存在: " + questionId);
    }
}

// 集成测试示例
@SpringBootTest
@ActiveProfiles("test")
@Testcontainers
class QuestionServiceIntegrationTest {
    
    @Container
    static MySQLContainer<?> mysql = new MySQLContainer<>("mysql:8.0")
        .withDatabaseName("test_db")
        .withUsername("test")
        .withPassword("test");
    
    @Container
    static GenericContainer<?> redis = new GenericContainer<>("redis:6.2")
        .withExposedPorts(6379);
    
    @DynamicPropertySource
    static 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);
    }
    
    @Autowired
    private QuestionService questionService;
    
    @Autowired
    private QuestionRepository questionRepository;
    
    @Test
    void shouldCreateQuestionSuccessfully() {
        // Given
        CreateQuestionRequest request = CreateQuestionRequest.builder()
            .title("两数之和")
            .description("给定一个整数数组...")
            .difficulty("MEDIUM")
            .tags(List.of("数组", "哈希表"))
            .build();
        
        // When
        Question created = questionService.createQuestion(request);
        
        // Then
        assertThat(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 {
    
    @Autowired
    private QuestionService questionService;
    
    @Test
    void shouldHandleConcurrentRequests() {
        // Given
        int concurrentUsers = 100;
        int requestsPerUser = 10;
        
        // When
        long 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;
        
        // Then
        assertThat(totalTime).isLessThan(5000); // 5秒内完成
        
        double throughput = (concurrentUsers * requestsPerUser) / (totalTime / 1000.0);
        assertThat(throughput).isGreaterThan(100); // 每秒100请求
    }
}

总结

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

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

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

相关推荐
user_admin_god2 小时前
企业级管理系统的站内信怎么轻量级优雅实现
java·大数据·数据库·spring boot
q***82912 小时前
Spring Boot 3.3.4 升级导致 Logback 之前回滚策略配置不兼容问题解决
java·spring boot·logback
百***99242 小时前
RabbitMQ高级特性----生产者确认机制
分布式·rabbitmq
小坏讲微服务2 小时前
SpringCloud零基础学全栈,实战企业级项目完整使用
后端·spring·spring cloud
长空任鸟飞_阿康2 小时前
AI 多模态全栈应用项目描述
前端·vue.js·人工智能·node.js·语音识别
码码哈哈0.03 小时前
Vue 3 + Vite 集成 Spring Boot 完整部署指南 - 前后端一体化打包方案
前端·vue.js·spring boot
百***84455 小时前
SpringBoot(整合MyBatis + MyBatis-Plus + MyBatisX插件使用)
spring boot·tomcat·mybatis
q***71855 小时前
Spring Boot 集成 MyBatis 全面讲解
spring boot·后端·mybatis
小坏讲微服务5 小时前
Spring Cloud Alibaba Gateway 集成 Redis 限流的完整配置
数据库·redis·分布式·后端·spring cloud·架构·gateway