[
  {
    "path": ".gitattributes",
    "content": "*.vue linguist-vendored\n"
  },
  {
    "path": ".github/workflows/ci-cd.yml",
    "content": "name: CI/CD for DOJ Backend\n\non:\n  push:\n    branches:\n      - main\n\njobs:\n  build-and-push:\n    runs-on: ubuntu-latest\n    strategy:\n      matrix:\n        service: ['user-service', 'problem-service', 'submission-service', 'sandbox-service', 'gateway-service']\n\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v3\n\n      - name: Set up JDK 17\n        uses: actions/setup-java@v3\n        with:\n          java-version: '17'\n          distribution: 'temurin'\n\n      - name: Login to Docker Hub\n        uses: docker/login-action@v2\n        with:\n          username: ${{ secrets.DOCKER_USERNAME }}\n          password: ${{ secrets.DOCKER_PASSWORD }}\n\n      - name: Build and push Docker image\n        run: |\n          SERVICE_NAME=${{ matrix.service }}\n          IMAGE_NAME=${{ secrets.DOCKER_USERNAME }}/doj-${SERVICE_NAME}\n          # Build the Docker image using the project root as context\n          docker build -t ${IMAGE_NAME}:latest -f DOJ-BE/${SERVICE_NAME}/Dockerfile .\n          # Push the Docker image\n          docker push ${IMAGE_NAME}:latest\n\n  # deploy:\n  #   needs: build-and-push\n  #   runs-on: ubuntu-latest\n\n  #   steps:\n  #     - name: Deploy to server\n  #       uses: appleboy/ssh-action@master\n  #       with:\n  #         host: ${{ secrets.SERVER_HOST }}\n  #         username: ${{ secrets.SERVER_USERNAME }}\n  #         key: ${{ secrets.SSH_PRIVATE_KEY }}\n  #         script: |\n  #           cd /path/to/your/project/D-OnlineJudge\n  #           git pull\n  #           docker-compose pull\n  #           docker-compose up -d --force-recreate\n"
  },
  {
    "path": ".gitignore",
    "content": ".idea/\n*.iml\n.DS_Store\n**/.DS_Store\n.vscode/\n/target/\n/DOJ-BE/target/\n/DOJ-BE/.idea/\n/DOJ-FE/node_modules/\n/DOJ-FE/dist/\n\n# Ignore local data and static files\nredis/\nnacos/\nmysql/\nstatic/\nelasticsearch/\nGEMINI.md\n\nskywalking-agent/\n\n.secrets"
  },
  {
    "path": "DOJ-BE/.gitignore",
    "content": "target/\n!.mvn/wrapper/maven-wrapper.jar\n!**/src/main/**/target/\n!**/src/test/**/target/\n\n./logs/**/*\n\n### IntelliJ IDEA ###\n.idea/modules.xml\n.idea/jarRepositories.xml\n.idea/compiler.xml\n.idea/libraries/\n*.iws\n*.iml\n*.ipr\n\n### Eclipse ###\n.apt_generated\n.classpath\n.factorypath\n.project\n.settings\n.springBeans\n.sts4-cache\n\n### NetBeans ###\n/nbproject/private/\n/nbbuild/\n/dist/\n/nbdist/\n/.nb-gradle/\nbuild/\n!**/src/main/**/build/\n!**/src/test/**/build/\n\n### VS Code ###\n.vscode/\n\n### Mac OS ###\n.DS_Store"
  },
  {
    "path": "DOJ-BE/common/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n         xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n    <parent>\n        <groupId>com.decade</groupId>\n        <artifactId>DOJ-BE</artifactId>\n        <version>1.0-SNAPSHOT</version>\n    </parent>\n\n    <artifactId>common</artifactId>\n\n    <properties>\n        <maven.compiler.source>17</maven.compiler.source>\n        <maven.compiler.target>17</maven.compiler.target>\n        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>\n    </properties>\n\n    <dependencies>\n        <!--openFeign-->\n        <dependency>\n            <groupId>org.springframework.cloud</groupId>\n            <artifactId>spring-cloud-starter-openfeign</artifactId>\n        </dependency>\n        <!--负载均衡器-->\n        <dependency>\n            <groupId>org.springframework.cloud</groupId>\n            <artifactId>spring-cloud-starter-loadbalancer</artifactId>\n        </dependency>\n        <!--OK http 的依赖 -->\n        <dependency>\n            <groupId>io.github.openfeign</groupId>\n            <artifactId>feign-okhttp</artifactId>\n        </dependency>\n        <!--nacos配置管理-->\n        <dependency>\n            <groupId>com.alibaba.cloud</groupId>\n            <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>\n        </dependency>\n        <!--读取bootstrap文件-->\n        <dependency>\n            <groupId>org.springframework.cloud</groupId>\n            <artifactId>spring-cloud-starter-bootstrap</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.springframework</groupId>\n            <artifactId>spring-web</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.springframework</groupId>\n            <artifactId>spring-webmvc</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.baomidou</groupId>\n            <artifactId>mybatis-plus-extension</artifactId>\n            <version>3.5.2</version>\n            <scope>compile</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.apache.tomcat.embed</groupId>\n            <artifactId>tomcat-embed-core</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.fasterxml.jackson.core</groupId>\n            <artifactId>jackson-databind</artifactId>\n        </dependency>\n    </dependencies>\n\n</project>\n"
  },
  {
    "path": "DOJ-BE/common/src/main/java/com/decade/doj/common/annotation/AdminRequired.java",
    "content": "package com.decade.doj.common.annotation;\n\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\n\n/**\n * 自定义注解，用于标记需要管理员权限的接口\n */\n@Target(ElementType.METHOD)\n@Retention(RetentionPolicy.RUNTIME)\npublic @interface AdminRequired {\n}\n"
  },
  {
    "path": "DOJ-BE/common/src/main/java/com/decade/doj/common/client/ProblemClient.java",
    "content": "package com.decade.doj.common.client;\n\nimport com.decade.doj.common.domain.R;\nimport com.decade.doj.common.domain.po.Problem;\nimport org.springframework.cloud.openfeign.FeignClient;\nimport org.springframework.web.bind.annotation.GetMapping;\nimport org.springframework.web.bind.annotation.PathVariable;\n\n@FeignClient(\"problem-service\")\npublic interface ProblemClient {\n\n    @GetMapping(\"/problem/{id}\")\n    R<Problem> getProblemById(@PathVariable Long id);\n\n    @GetMapping(\"/problem/count\")\n    R<Long> getCount();\n\n}\n"
  },
  {
    "path": "DOJ-BE/common/src/main/java/com/decade/doj/common/client/SubmissionClient.java",
    "content": "package com.decade.doj.common.client;\n\nimport com.decade.doj.common.domain.R;\nimport com.decade.doj.common.domain.po.Submission;\nimport com.decade.doj.common.domain.vo.SubmissionStatsVO;\nimport org.springframework.cloud.openfeign.FeignClient;\nimport org.springframework.web.bind.annotation.GetMapping;\nimport org.springframework.web.bind.annotation.PostMapping;\nimport org.springframework.web.bind.annotation.RequestBody;\n\n@FeignClient(\"submission-service\")\npublic interface SubmissionClient {\n\n    @PostMapping(\"/submission/submit\")\n    R<Long> submit(@RequestBody Submission submission);\n\n    @GetMapping(\"/submission/stats\")\n    R<SubmissionStatsVO> getStats();\n\n}\n"
  },
  {
    "path": "DOJ-BE/common/src/main/java/com/decade/doj/common/client/UserClient.java",
    "content": "package com.decade.doj.common.client;\n\nimport com.decade.doj.common.domain.R;\nimport com.decade.doj.common.domain.vo.InfoVO;\nimport org.springframework.cloud.openfeign.FeignClient;\nimport org.springframework.web.bind.annotation.GetMapping;\nimport org.springframework.web.bind.annotation.PathVariable;\n\nimport javax.validation.constraints.NotNull;\n\n@FeignClient(\"user-service\")\npublic interface UserClient {\n\n    @GetMapping(\"/user/{id}\")\n    R<InfoVO> getUser(@PathVariable(\"id\") @NotNull Long id);\n\n}\n"
  },
  {
    "path": "DOJ-BE/common/src/main/java/com/decade/doj/common/config/custom/DefaultFeignConfig.java",
    "content": "package com.decade.doj.common.config.custom;\n\nimport com.decade.doj.common.config.properties.JwtProperties;\nimport feign.Logger;\nimport feign.RequestInterceptor;\nimport lombok.RequiredArgsConstructor;\nimport org.springframework.boot.context.properties.EnableConfigurationProperties;\nimport org.springframework.context.annotation.Bean;\nimport com.decade.doj.common.utils.UserContext;\nimport org.springframework.context.annotation.Configuration;\n\n@Configuration\n@EnableConfigurationProperties(JwtProperties.class)\n@RequiredArgsConstructor\npublic class DefaultFeignConfig {\n\n    private final JwtProperties jwtProperties;\n\n    @Bean\n    public Logger.Level feignLogLevel(){\n        return Logger.Level.FULL;\n    }\n\n    @Bean\n    public RequestInterceptor userInfoRequestInterceptor(){\n        return template -> {\n            // 获取登录用户\n            Long userId = UserContext.getCurrentUser();\n            if(userId == null) {\n                // 如果为空则直接跳过\n                return;\n            }\n            // 如果不为空则放入请求头中，传递给下游微服务\n            template.header(jwtProperties.getSecretKey(), userId.toString());\n        };\n    }\n}\n\n"
  },
  {
    "path": "DOJ-BE/common/src/main/java/com/decade/doj/common/config/custom/JwtTool.java",
    "content": "package com.decade.doj.common.config.custom;\n\nimport cn.hutool.core.exceptions.ValidateException;\nimport cn.hutool.jwt.JWT;\nimport cn.hutool.jwt.JWTValidator;\nimport cn.hutool.jwt.signers.JWTSigner;\nimport cn.hutool.jwt.signers.JWTSignerUtil;\nimport com.decade.doj.common.exception.UnauthorizedException;\nimport org.springframework.context.annotation.Import;\nimport org.springframework.stereotype.Component;\n\nimport java.security.KeyPair;\nimport java.time.Duration;\nimport java.util.Date;\n\n@Component\n@Import(SecurityConfig.class)\npublic class JwtTool {\n\n    private final JWTSigner jwtSigner;\n\n    public JwtTool(KeyPair keyPair) {\n        this.jwtSigner = JWTSignerUtil.createSigner(\"rs256\", keyPair);\n    }\n\n    public String createToken(Long userId, Duration ttl) {\n        return JWT.create()\n                .setPayload(\"user\", userId)\n                .setExpiresAt(new Date(System.currentTimeMillis() + ttl.toMillis()))\n                .setSigner(jwtSigner)\n                .sign();\n    }\n\n    public Long parseToken(String token) {\n        // 1.校验token是否为空\n        if (token == null) {\n            throw new UnauthorizedException(\"未登录\");\n        }\n        // 2.校验并解析jwt\n        JWT jwt;\n        try {\n            jwt = JWT.of(token).setSigner(jwtSigner);\n        } catch (Exception e) {\n            throw new UnauthorizedException(\"无效的token\", e);\n        }\n        // 2.校验jwt是否有效\n        if (!jwt.verify()) {\n            // 验证失败\n            throw new UnauthorizedException(\"无效的token\");\n        }\n        // 3.校验是否过期\n        try {\n            JWTValidator.of(jwt).validateDate();\n        } catch (ValidateException e) {\n            throw new UnauthorizedException(\"token已经过期\");\n        }\n        // 4.数据格式校验\n        Object userPayload = jwt.getPayload(\"user\");\n        if (userPayload == null) {\n            // 数据为空\n            throw new UnauthorizedException(\"无效的token\");\n        }\n\n        // 5.数据解析\n        try {\n           return Long.valueOf(userPayload.toString());\n        } catch (RuntimeException e) {\n            // 数据格式有误\n            throw new UnauthorizedException(\"无效的token\");\n        }\n    }\n}"
  },
  {
    "path": "DOJ-BE/common/src/main/java/com/decade/doj/common/config/custom/MVCConfig.java",
    "content": "package com.decade.doj.common.config.custom;\n\nimport com.decade.doj.common.config.properties.ResourceProperties;\nimport com.decade.doj.common.interceptor.AdminCheckInterceptor;\nimport com.decade.doj.common.interceptor.IdentityInterceptor;\nimport lombok.RequiredArgsConstructor;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.boot.autoconfigure.condition.ConditionalOnClass;\nimport org.springframework.boot.context.properties.EnableConfigurationProperties;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.web.servlet.DispatcherServlet;\nimport org.springframework.web.servlet.config.annotation.InterceptorRegistry;\nimport org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;\nimport org.springframework.web.servlet.config.annotation.WebMvcConfigurer;\n\n@Configuration\n@ConditionalOnClass(DispatcherServlet.class)\n@EnableConfigurationProperties(ResourceProperties.class)\n@Slf4j\n@RequiredArgsConstructor\npublic class MVCConfig implements WebMvcConfigurer {\n\n    private final ResourceProperties resourceProperties;\n    private final IdentityInterceptor identityInterceptor;\n    private final AdminCheckInterceptor adminCheckInterceptor;\n\n    @Override\n    public void addResourceHandlers(ResourceHandlerRegistry registry) {\n        String request = resourceProperties.getRequest() + \"**\";\n        String location = \"file:\" + resourceProperties.getLocation();\n        log.info(\"request: {}, location: {}\", request, location);\n        registry.addResourceHandler(request).addResourceLocations(location);\n    }\n\n    @Override\n    public void addInterceptors(InterceptorRegistry registry) {\n        // 身份识别拦截器，必须先执行\n        registry.addInterceptor(identityInterceptor)\n                .addPathPatterns(\"/**\")\n                .excludePathPatterns(\"/swagger-resources/**\", \"/webjars/**\", \"/v2/**\", \"/swagger-ui.html/**\", \"/doc.html/**\", \"/actuator/**\");\n        \n        // 管理员权限拦截器，后执行\n        registry.addInterceptor(adminCheckInterceptor)\n                .addPathPatterns(\"/**\");\n    }\n}"
  },
  {
    "path": "DOJ-BE/common/src/main/java/com/decade/doj/common/config/custom/MybatisConfig.java",
    "content": "package com.decade.doj.common.config.custom;\n\nimport com.baomidou.mybatisplus.annotation.DbType;\nimport com.baomidou.mybatisplus.core.mapper.BaseMapper;\nimport com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;\nimport com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;\nimport org.springframework.boot.autoconfigure.condition.ConditionalOnClass;\nimport org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\n\n@Configuration\n@ConditionalOnClass(BaseMapper.class)\npublic class MybatisConfig {\n    @Bean\n    @ConditionalOnMissingBean\n    public MybatisPlusInterceptor mybatisPlusInterceptor() {\n        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();\n        PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor(DbType.MYSQL);\n        paginationInnerInterceptor.setMaxLimit(1000L);\n        interceptor.addInnerInterceptor(paginationInnerInterceptor);\n        return interceptor;\n    }\n}"
  },
  {
    "path": "DOJ-BE/common/src/main/java/com/decade/doj/common/config/custom/SecurityConfig.java",
    "content": "package com.decade.doj.common.config.custom;\n\nimport com.decade.doj.common.config.properties.JwtProperties;\nimport org.springframework.boot.context.properties.EnableConfigurationProperties;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\n\nimport java.io.InputStream;\nimport java.security.KeyPair;\nimport java.security.KeyStore;\nimport java.security.cert.Certificate;\nimport java.security.PrivateKey;\nimport java.security.PublicKey;\n\n@Configuration\n@EnableConfigurationProperties(JwtProperties.class)\npublic class SecurityConfig {\n\n    @Bean\n    public KeyPair keyPair(JwtProperties properties) throws Exception {\n        // 加载密钥库\n        KeyStore keyStore = KeyStore.getInstance(\"JKS\");\n        try (InputStream inputStream = properties.getLocation().getInputStream()) {\n            keyStore.load(inputStream, properties.getPassword().toCharArray());\n        }\n\n        // 读取私钥和证书\n        PrivateKey privateKey = (PrivateKey) keyStore.getKey(properties.getAlias(), properties.getPassword().toCharArray());\n        Certificate certificate = keyStore.getCertificate(properties.getAlias());\n        PublicKey publicKey = certificate.getPublicKey();\n\n        return new KeyPair(publicKey, privateKey);\n    }\n}"
  },
  {
    "path": "DOJ-BE/common/src/main/java/com/decade/doj/common/config/global/CommonExceptionConfig.java",
    "content": "package com.decade.doj.common.config.global;\n\nimport com.decade.doj.common.domain.R;\nimport com.decade.doj.common.exception.*;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.http.HttpStatus;\nimport org.springframework.http.ResponseEntity;\nimport org.springframework.validation.ObjectError;\nimport org.springframework.web.bind.MethodArgumentNotValidException;\nimport org.springframework.web.bind.annotation.ExceptionHandler;\nimport org.springframework.web.bind.annotation.ResponseStatus;\nimport org.springframework.web.bind.annotation.RestControllerAdvice;\nimport org.springframework.web.util.NestedServletException;\n\nimport java.net.BindException;\nimport java.util.stream.Collectors;\n\n@RestControllerAdvice\n@Slf4j\npublic class CommonExceptionConfig {\n\n    @ExceptionHandler(DbException.class)\n    public Object handleDbException(DbException e) {\n        log.error(\"mysql数据库操作异常 -> \", e);\n        return processResponse(e);\n    }\n\n    @ExceptionHandler(CommonException.class)\n    public Object handleBadRequestException(CommonException e) {\n        log.error(\"自定义异常 -> {} , 异常原因：{}  \",e.getClass().getName(), e.getMessage());\n        log.debug(\"\", e);\n        return processResponse(e);\n    }\n\n    @ExceptionHandler(MethodArgumentNotValidException.class)\n    public Object handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {\n        String msg = e.getBindingResult().getAllErrors()\n                .stream().map(ObjectError::getDefaultMessage)\n                .collect(Collectors.joining(\"|\"));\n        log.error(\"请求参数校验异常 -> {}\", msg);\n        log.debug(\"\", e);\n        return processResponse(new BadRequestException(msg));\n    }\n    @ExceptionHandler(BindException.class)\n    public Object handleBindException(BindException e) {\n        log.error(\"请求参数绑定异常 ->BindException， {}\", e.getMessage());\n        log.debug(\"\", e);\n        return processResponse(new BadRequestException(\"请求参数格式错误\"));\n    }\n\n    @ExceptionHandler(Exception.class)\n    public Object handleRuntimeException(Exception e) {\n        log.error(\"其他异常 uri : {} {}\", e.getMessage(), e.getStackTrace());\n        return processResponse(new CommonException(e.getMessage(), 500));\n    }\n\n    @ExceptionHandler(UnauthorizedException.class)\n    @ResponseStatus(HttpStatus.UNAUTHORIZED)\n    public R<Void> handleUnauthorizedException(UnauthorizedException e) {\n        log.error(\"<UNK> uri : {} {}\", e.getMessage(), e.getStackTrace());\n        return R.error(401, e.getMessage());\n    }\n\n    @ExceptionHandler(ForbiddenException.class)\n    @ResponseStatus(HttpStatus.FORBIDDEN)\n    public R<Void> handleForbiddenException(ForbiddenException e) {\n        log.error(\"403 uri : {} {}\", e.getMessage(), e.getStackTrace());\n        return R.error(403, e.getMessage());\n    }\n\n    private ResponseEntity<R<Void>> processResponse(CommonException e){\n        return ResponseEntity.status(e.getCode()).body(R.error(e));\n    }\n}\n"
  },
  {
    "path": "DOJ-BE/common/src/main/java/com/decade/doj/common/config/properties/AppNameProperties.java",
    "content": "package com.decade.doj.common.config.properties;\n\nimport lombok.Data;\nimport org.springframework.beans.factory.annotation.Value;\nimport org.springframework.stereotype.Component;\n\n@Data\n@Component\npublic class AppNameProperties {\n\n    @Value(\"${spring.application.name}\")\n    private String name;\n}\n"
  },
  {
    "path": "DOJ-BE/common/src/main/java/com/decade/doj/common/config/properties/JwtProperties.java",
    "content": "package com.decade.doj.common.config.properties;\n\nimport lombok.Data;\nimport org.springframework.boot.context.properties.ConfigurationProperties;\nimport org.springframework.core.io.Resource;\n\nimport java.time.Duration;\n\n@Data\n@ConfigurationProperties(prefix = \"doj.jwt\")\npublic class JwtProperties {\n    private Resource location;\n    private String password;\n    private String alias;\n    private Duration tokenTTL = Duration.ofMinutes(10);\n    private Duration refreshTokenTTL = Duration.ofDays(7);\n\n    private String authorization;\n    private String secretKey;\n}\n"
  },
  {
    "path": "DOJ-BE/common/src/main/java/com/decade/doj/common/config/properties/ResourceProperties.java",
    "content": "package com.decade.doj.common.config.properties;\n\nimport lombok.Data;\nimport org.springframework.boot.context.properties.ConfigurationProperties;\nimport org.springframework.core.io.Resource;\n\nimport java.time.Duration;\n\n@Data\n@ConfigurationProperties(prefix = \"doj.resource\")\npublic class ResourceProperties {\n\n    private String request;\n    private String location;\n    private String codePath;\n    private String problemCodePath;\n\n}\n"
  },
  {
    "path": "DOJ-BE/common/src/main/java/com/decade/doj/common/config/thread/ThreadPoolConfig.java",
    "content": "package com.decade.doj.common.config.thread;\n\nimport lombok.Data;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.boot.context.properties.ConfigurationProperties;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.scheduling.annotation.EnableAsync;\nimport org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;\n\nimport java.util.concurrent.ThreadPoolExecutor;\n\n/**\n * 线程池配置类\n * <p>\n * 支持通过 application.yml 配置参数：\n * <pre>\n * doj:\n *   thread-pool:\n *     run-code-core-size: 10\n *     run-code-max-size: 20\n *     run-code-queue-capacity: 50\n *     judging-core-size: 4\n *     judging-max-size: 8\n *     judging-queue-capacity: 100\n * </pre>\n */\n@Configuration\n@EnableAsync\n@ConfigurationProperties(prefix = \"doj.thread-pool\")\n@Data\n@Slf4j\npublic class ThreadPoolConfig {\n\n    // RunCodeThreadPool 配置\n    private int runCodeCoreSize = 10;\n    private int runCodeMaxSize = 20;\n    private int runCodeQueueCapacity = 50;\n\n    // JudgingThreadPool 配置\n    private int judgingCoreSize = 4;\n    private int judgingMaxSize = 8;\n    private int judgingQueueCapacity = 100;\n\n    /**\n     * 代码运行线程池\n     * 用于执行 /code 和 /problem 端点的代码运行任务\n     */\n    @Bean(\"RunCodeThreadPool\")\n    public ThreadPoolTaskExecutor runCodeThreadPoolExecutor() {\n        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();\n        executor.setCorePoolSize(runCodeCoreSize);\n        executor.setMaxPoolSize(runCodeMaxSize);\n        executor.setQueueCapacity(runCodeQueueCapacity);\n        executor.setThreadNamePrefix(\"RunCodeThread-\");\n        // 拒绝策略：调用者线程执行，防止任务丢失\n        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());\n        // 优雅关闭：等待任务完成\n        executor.setWaitForTasksToCompleteOnShutdown(true);\n        executor.setAwaitTerminationSeconds(60);\n        executor.initialize();\n        log.info(\"RunCodeThreadPool initialized: coreSize={}, maxSize={}, queueCapacity={}\",\n                runCodeCoreSize, runCodeMaxSize, runCodeQueueCapacity);\n        return executor;\n    }\n\n    /**\n     * 判题任务线程池\n     * 用于执行从 Redis 队列消费的判题任务\n     */\n    @Bean(\"JudgingThreadPool\")\n    public ThreadPoolTaskExecutor judgingThreadPoolExecutor() {\n        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();\n        executor.setCorePoolSize(judgingCoreSize);\n        executor.setMaxPoolSize(judgingMaxSize);\n        executor.setQueueCapacity(judgingQueueCapacity);\n        executor.setThreadNamePrefix(\"JudgingThread-\");\n        // 拒绝策略：调用者线程执行，确保任务不丢失\n        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());\n        // 优雅关闭：判题任务可能较长，给予更多等待时间\n        executor.setWaitForTasksToCompleteOnShutdown(true);\n        executor.setAwaitTerminationSeconds(120);\n        executor.initialize();\n        log.info(\"JudgingThreadPool initialized: coreSize={}, maxSize={}, queueCapacity={}\",\n                judgingCoreSize, judgingMaxSize, judgingQueueCapacity);\n        return executor;\n    }\n}\n\n"
  },
  {
    "path": "DOJ-BE/common/src/main/java/com/decade/doj/common/domain/PageDTO.java",
    "content": "package com.decade.doj.common.domain;\n\nimport cn.hutool.core.bean.BeanUtil;\nimport cn.hutool.core.collection.CollUtil;\nimport com.baomidou.mybatisplus.extension.plugins.pagination.Page;\nimport lombok.AllArgsConstructor;\nimport lombok.Data;\nimport lombok.NoArgsConstructor;\n\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.function.Function;\nimport java.util.stream.Collectors;\n\n@Data\n@NoArgsConstructor\n@AllArgsConstructor\npublic class PageDTO<T> {\n    protected Long total;\n    protected Long pages;\n    protected List<T> list;\n\n    public static <T> PageDTO<T> empty(Long total, Long pages) {\n        return new PageDTO<>(total, pages, Collections.emptyList());\n    }\n\n    public static <T> PageDTO<T> fullPage(Long total, Long pages, List<T> list) {\n        return new PageDTO<>(total, pages, list);\n    }\n\n    public static <T> PageDTO<T> empty(Page<?> page) {\n        return new PageDTO<>(page.getTotal(), page.getPages(), Collections.emptyList());\n    }\n\n    public static <T> PageDTO<T> of(Page<T> page) {\n        if(page == null){\n            return new PageDTO<>();\n        }\n        if (CollUtil.isEmpty(page.getRecords())) {\n            return empty(page);\n        }\n        return new PageDTO<>(page.getTotal(), page.getPages(), page.getRecords());\n    }\n    public static <T,R> PageDTO<T> of(Page<R> page, Function<R, T> mapper) {\n        if(page == null){\n            return new PageDTO<>();\n        }\n        if (CollUtil.isEmpty(page.getRecords())) {\n            return empty(page);\n        }\n        return new PageDTO<>(page.getTotal(), page.getPages(),\n                page.getRecords().stream().map(mapper).collect(Collectors.toList()));\n    }\n    public static <T> PageDTO<T> of(Page<?> page, List<T> list) {\n        return new PageDTO<>(page.getTotal(), page.getPages(), list);\n    }\n\n    public static <T, R> PageDTO<T> of(Page<R> page, Class<T> clazz) {\n        return new PageDTO<>(page.getTotal(), page.getPages(), BeanUtil.copyToList(page.getRecords(), clazz));\n    }\n}\n\n"
  },
  {
    "path": "DOJ-BE/common/src/main/java/com/decade/doj/common/domain/PageQueryDTO.java",
    "content": "package com.decade.doj.common.domain;\n\nimport cn.hutool.core.util.StrUtil;\nimport com.baomidou.mybatisplus.core.metadata.OrderItem;\nimport com.baomidou.mybatisplus.core.toolkit.StringUtils;\nimport com.baomidou.mybatisplus.extension.plugins.pagination.Page;\nimport io.swagger.v3.oas.annotations.media.Schema;\nimport lombok.Data;\nimport lombok.experimental.Accessors;\n\nimport javax.validation.constraints.Min;\n\n@Data\n@Schema(description = \"分页查询条件\")\n@Accessors(chain = true)\npublic class PageQueryDTO {\n\n    public static final Integer DEFAULT_PAGE_SIZE = 20;\n    public static final Integer DEFAULT_PAGE_NUM = 1;\n    @Schema(description = \"页码\")\n    @Min(value = 1, message = \"页码不能小于1\")\n    private Integer pageNo = DEFAULT_PAGE_NUM;\n    @Schema(description = \"每页数量\")\n    @Min(value = 1, message = \"每页查询数量不能小于1\")\n    private Integer pageSize = DEFAULT_PAGE_SIZE;\n    @Schema(description = \"是否升序\")\n    private Boolean isAsc = true;\n    @Schema(description = \"排序字段\")\n    private String sortBy;\n\n    public int from() {\n        return (pageNo - 1) * pageSize;\n    }\n\n    public <T> Page<T> toMpPage(OrderItem... orderItems) {\n        Page<T> page = new Page<>(pageNo, pageSize);\n        // 是否手动指定排序方式\n        if (orderItems != null && orderItems.length > 0) {\n            for (OrderItem orderItem : orderItems) {\n                page.addOrder(orderItem);\n            }\n            return page;\n        }\n        // 前端是否有排序字段\n        if (StrUtil.isNotEmpty(sortBy)) {\n            OrderItem orderItem = new OrderItem();\n            orderItem.setAsc(isAsc);\n            orderItem.setColumn(sortBy);\n            page.addOrder(orderItem);\n        }\n        return page;\n    }\n\n    public <T> Page<T> toMpPage(String defaultSortBy, boolean isAsc) {\n        if (StringUtils.isBlank(sortBy)) {\n            sortBy = defaultSortBy;\n            this.isAsc = isAsc;\n        }\n        Page<T> page = new Page<>(pageNo, pageSize);\n        OrderItem orderItem = new OrderItem();\n        orderItem.setAsc(this.isAsc);\n        orderItem.setColumn(sortBy);\n        page.addOrder(orderItem);\n        return page;\n    }\n\n    public <T> Page<T> toMpPageDefaultSortByCreateTimeDesc() {\n        return toMpPage(\"create_time\", false);\n    }\n\n\n}\n"
  },
  {
    "path": "DOJ-BE/common/src/main/java/com/decade/doj/common/domain/R.java",
    "content": "package com.decade.doj.common.domain;\n\nimport com.decade.doj.common.exception.CommonException;\nimport lombok.Data;\n\n\n@Data\npublic class R<T> {\n    private int code;\n    private String msg;\n    private T data;\n\n    public static R<Void> ok() {\n        return ok(null);\n    }\n\n    public static <T> R<T> ok(T data) {\n        return new R<>(200, \"OK\", data);\n    }\n\n    public static <T> R<T> error(String msg) {\n        return new R<>(500, msg, null);\n    }\n\n    public static <T> R<T> error(int code, String msg) {\n        return new R<>(code, msg, null);\n    }\n\n    public static <T> R<T> error(CommonException e) {\n        return new R<>(e.getCode(), e.getMessage(), null);\n    }\n\n    public R() {\n    }\n\n    public R(int code, String msg, T data) {\n        this.code = code;\n        this.msg = msg;\n        this.data = data;\n    }\n\n    public boolean success(){\n        return code == 200;\n    }\n}\n"
  },
  {
    "path": "DOJ-BE/common/src/main/java/com/decade/doj/common/domain/json/StringListDeserializer.java",
    "content": "package com.decade.doj.common.domain.json;\n\nimport com.fasterxml.jackson.core.JsonParser;\nimport com.fasterxml.jackson.core.JsonToken;\nimport com.fasterxml.jackson.databind.DeserializationContext;\nimport com.fasterxml.jackson.databind.JsonDeserializer;\n\nimport java.io.IOException;\nimport java.util.ArrayList;\nimport java.util.List;\n\npublic class StringListDeserializer extends JsonDeserializer<List<String>> {\n\n    @Override\n    public List<String> deserialize(JsonParser parser, DeserializationContext context) throws IOException {\n        JsonToken token = parser.currentToken();\n        if (token == JsonToken.START_ARRAY) {\n            List<String> values = new ArrayList<>();\n            while (parser.nextToken() != JsonToken.END_ARRAY) {\n                values.add(parser.getValueAsString());\n            }\n            return values;\n        }\n\n        if (token == JsonToken.VALUE_STRING) {\n            String value = parser.getValueAsString();\n            if (value == null || value.isBlank()) {\n                return List.of();\n            }\n            return List.of(value);\n        }\n\n        return context.readValue(parser, context.getTypeFactory().constructCollectionType(List.class, String.class));\n    }\n}\n"
  },
  {
    "path": "DOJ-BE/common/src/main/java/com/decade/doj/common/domain/po/Problem.java",
    "content": "package com.decade.doj.common.domain.po;\n\nimport com.decade.doj.common.domain.json.StringListDeserializer;\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize;\nimport io.swagger.v3.oas.annotations.media.Schema;\nimport java.io.Serializable;\nimport java.util.List;\nimport lombok.Data;\nimport lombok.EqualsAndHashCode;\nimport lombok.experimental.Accessors;\n\n/**\n * <p>\n *\n * </p>\n *\n * @author\n * @since 2024-09-17\n */\n@Data\n@Accessors(chain = true)\n@Schema(description=\"Problem对象\")\npublic class Problem {\n\n    private Long id;\n\n    private String name;\n\n    private String description;\n\n    private String inputStyle;\n\n    private String outputStyle;\n\n    @JsonDeserialize(using = StringListDeserializer.class)\n    private List<String> inputSample;\n\n    @JsonDeserialize(using = StringListDeserializer.class)\n    private List<String> outputSample;\n\n    private String hint;\n\n    private String difficulty;\n\n    private Integer timeLimit;\n\n    private Integer memoryLimit;\n\n    private Integer totalPass;\n\n    private Integer totalAttempt;\n\n    private List<String> tags;\n\n    private String testData;\n\n    private String testAns;\n}\n"
  },
  {
    "path": "DOJ-BE/common/src/main/java/com/decade/doj/common/domain/po/Submission.java",
    "content": "package com.decade.doj.common.domain.po;\n\nimport lombok.Data;\nimport lombok.experimental.Accessors;\n\nimport java.util.Date;\n\n@Data\n@Accessors(chain = true)\npublic class Submission {\n    private Long id;\n    private Long userId;\n    private String userName;\n    private Long problemId;\n    private String problemName;\n    private String language;\n    private String code;\n    private Integer exitValue;\n    private String status;\n    private String message;\n    private Double time;\n    private Long memory;\n    private Date submitTime;\n}"
  },
  {
    "path": "DOJ-BE/common/src/main/java/com/decade/doj/common/domain/po/User.java",
    "content": "package com.decade.doj.common.domain.po;\n\nimport com.baomidou.mybatisplus.annotation.IdType;\nimport com.baomidou.mybatisplus.annotation.TableId;\nimport com.baomidou.mybatisplus.annotation.TableName;\nimport lombok.Data;\nimport lombok.EqualsAndHashCode;\nimport lombok.experimental.Accessors;\n\n/**\n * <p>\n * \n * </p>\n *\n * @author \n * @since 2024-08-26\n */\n@Data\n@EqualsAndHashCode(callSuper = false)\n@Accessors(chain = true)\n@TableName(\"user\")\npublic class User {\n\n    @TableId(value = \"id\", type = IdType.AUTO)\n    private Long id;\n\n    private String username;\n\n    private String avatar = \"\";\n\n    private String email;\n\n    private String password;\n\n    private Integer score = 0;\n\n    private Integer ranks = 0;\n\n    private String school = \"\";\n\n    private Boolean gender = true;\n\n    private Integer easySolve = 0;\n\n    private Integer middleSolve = 0;\n\n    private Integer hardSolve = 0;\n\n    private Boolean role = true;\n\n    private String url = \"\";\n\n    private String sign = \"这个人很懒，什么都没留下\";\n\n    private Long fans = 0L;\n\n    private Long subscribe = 0L;\n\n    private Boolean ban = false;\n\n\n}\n"
  },
  {
    "path": "DOJ-BE/common/src/main/java/com/decade/doj/common/domain/vo/ExecuteMessage.java",
    "content": "package com.decade.doj.common.domain.vo;\n\nimport lombok.Data;\nimport lombok.experimental.Accessors;\n\nimport java.util.HashMap;\nimport java.util.HashSet;\nimport java.util.Map;\nimport java.util.Set;\n\n@Data\n@Accessors(chain = true)\npublic class ExecuteMessage {\n\n    private Integer exitValue;\n    private String status;\n    private String message;\n    private double time;\n    private Long memory;\n\n    private static final Map<Integer, String> exitStatusMap = new HashMap<>();\n    private static final Set<Integer> InfoStatus = new HashSet<>();\n\n    static {\n        exitStatusMap.put(0, \"Finished\");\n        exitStatusMap.put(1, \"Runtime Error\");\n        exitStatusMap.put(2, \"Compile Error\");\n        exitStatusMap.put(124, \"Time Limit Exceeded\");\n        exitStatusMap.put(137, \"Memory Limit Exceeded\");\n        exitStatusMap.put(10, \"Accepted\");\n        exitStatusMap.put(11, \"Wrong Answer\");\n\n        InfoStatus.add(0);\n        InfoStatus.add(1);\n        InfoStatus.add(2);\n        InfoStatus.add(10);\n        InfoStatus.add(11);\n    }\n\n    public static String getStatus(Integer exitValue) {\n        if (exitValue == null) {\n            return \"Unknown Error\";\n        }\n        return exitStatusMap.getOrDefault(exitValue, \"Unknown exitValue\");\n    }\n\n    public static boolean show(Integer exitValue) {\n        return InfoStatus.contains(exitValue);\n    }\n}"
  },
  {
    "path": "DOJ-BE/common/src/main/java/com/decade/doj/common/domain/vo/InfoVO.java",
    "content": "package com.decade.doj.common.domain.vo;\n\nimport com.decade.doj.common.domain.po.User;\nimport lombok.Data;\nimport lombok.EqualsAndHashCode;\nimport lombok.experimental.Accessors;\n\n@EqualsAndHashCode(callSuper = true)\n@Data\n@Accessors(chain = true)\npublic class InfoVO extends User {\n\n}\n"
  },
  {
    "path": "DOJ-BE/common/src/main/java/com/decade/doj/common/domain/vo/StatsVO.java",
    "content": "package com.decade.doj.common.domain.vo;\n\nimport lombok.AllArgsConstructor;\nimport lombok.Builder;\nimport lombok.Data;\nimport lombok.NoArgsConstructor;\n\n/**\n * 首页统计数据 VO\n */\n@Data\n@Builder\n@NoArgsConstructor\n@AllArgsConstructor\npublic class StatsVO {\n    /**\n     * 累计提交数\n     */\n    private Long totalSubmissions;\n\n    /**\n     * 今日提交数\n     */\n    private Long todaySubmissions;\n\n    /**\n     * 题目总数\n     */\n    private Long totalProblems;\n\n    /**\n     * 活跃用户数（有提交记录的用户）\n     */\n    private Long activeUsers;\n}\n"
  },
  {
    "path": "DOJ-BE/common/src/main/java/com/decade/doj/common/domain/vo/SubmissionStatsVO.java",
    "content": "package com.decade.doj.common.domain.vo;\n\nimport lombok.AllArgsConstructor;\nimport lombok.Data;\nimport lombok.NoArgsConstructor;\n\n/**\n * 提交统计 VO\n */\n@Data\n@NoArgsConstructor\n@AllArgsConstructor\npublic class SubmissionStatsVO {\n    /**\n     * 累计提交数\n     */\n    private Long totalSubmissions;\n\n    /**\n     * 今日提交数\n     */\n    private Long todaySubmissions;\n}\n"
  },
  {
    "path": "DOJ-BE/common/src/main/java/com/decade/doj/common/exception/BadRequestException.java",
    "content": "package com.decade.doj.common.exception;\n\npublic class BadRequestException extends CommonException{\n\n    public BadRequestException(String message) {\n        super(message, 400);\n    }\n\n    public BadRequestException(String message, Throwable cause) {\n        super(message, cause, 400);\n    }\n\n    public BadRequestException(Throwable cause) {\n        super(cause, 400);\n    }\n}\n"
  },
  {
    "path": "DOJ-BE/common/src/main/java/com/decade/doj/common/exception/BizIllegalException.java",
    "content": "package com.decade.doj.common.exception;\n\npublic class BizIllegalException extends CommonException{\n\n    public BizIllegalException(String message) {\n        super(message, 500);\n    }\n\n    public BizIllegalException(String message, Throwable cause) {\n        super(message, cause, 500);\n    }\n\n    public BizIllegalException(Throwable cause) {\n        super(cause, 500);\n    }\n}\n"
  },
  {
    "path": "DOJ-BE/common/src/main/java/com/decade/doj/common/exception/CommonException.java",
    "content": "package com.decade.doj.common.exception;\n\nimport lombok.Getter;\n\n@Getter\npublic class CommonException extends RuntimeException{\n    private int code;\n\n    public CommonException(String message, int code) {\n        super(message);\n        this.code = code;\n    }\n\n    public CommonException(String message, Throwable cause, int code) {\n        super(message, cause);\n        this.code = code;\n    }\n\n    public CommonException(Throwable cause, int code) {\n        super(cause);\n        this.code = code;\n    }\n}\n"
  },
  {
    "path": "DOJ-BE/common/src/main/java/com/decade/doj/common/exception/DbException.java",
    "content": "package com.decade.doj.common.exception;\n\npublic class DbException extends CommonException{\n\n    public DbException(String message) {\n        super(message, 500);\n    }\n\n    public DbException(String message, Throwable cause) {\n        super(message, cause, 500);\n    }\n\n    public DbException(Throwable cause) {\n        super(cause, 500);\n    }\n}\n"
  },
  {
    "path": "DOJ-BE/common/src/main/java/com/decade/doj/common/exception/ForbiddenException.java",
    "content": "package com.decade.doj.common.exception;\n\npublic class ForbiddenException extends CommonException{\n\n    public ForbiddenException(String message) {\n        super(message, 403);\n    }\n\n    public ForbiddenException(String message, Throwable cause) {\n        super(message, cause, 403);\n    }\n\n    public ForbiddenException(Throwable cause) {\n        super(cause, 403);\n    }\n}\n"
  },
  {
    "path": "DOJ-BE/common/src/main/java/com/decade/doj/common/exception/UnauthorizedException.java",
    "content": "package com.decade.doj.common.exception;\n\npublic class UnauthorizedException extends CommonException{\n\n    public UnauthorizedException(String message) {\n        super(message, 401);\n    }\n\n    public UnauthorizedException(String message, Throwable cause) {\n        super(message, cause, 401);\n    }\n\n    public UnauthorizedException(Throwable cause) {\n        super(cause, 401);\n    }\n}\n"
  },
  {
    "path": "DOJ-BE/common/src/main/java/com/decade/doj/common/interceptor/AdminCheckInterceptor.java",
    "content": "package com.decade.doj.common.interceptor;\n\nimport com.decade.doj.common.annotation.AdminRequired;\nimport com.decade.doj.common.client.UserClient;\nimport com.decade.doj.common.domain.R;\nimport com.decade.doj.common.domain.vo.InfoVO;\nimport com.decade.doj.common.exception.ForbiddenException;\nimport com.decade.doj.common.exception.UnauthorizedException;\nimport com.decade.doj.common.utils.UserContext;\nimport org.springframework.context.ApplicationContext;\nimport org.springframework.context.ApplicationContextAware;\nimport org.springframework.stereotype.Component;\nimport org.springframework.web.method.HandlerMethod;\nimport org.springframework.web.servlet.HandlerInterceptor;\n\nimport javax.servlet.http.HttpServletRequest;\nimport javax.servlet.http.HttpServletResponse;\n\n@Component\npublic class AdminCheckInterceptor implements HandlerInterceptor, ApplicationContextAware {\n\n    private ApplicationContext applicationContext;\n\n    private UserClient userClient;\n\n    @Override\n    public void setApplicationContext(ApplicationContext applicationContext) {\n        this.applicationContext = applicationContext;\n    }\n\n    @Override\n    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {\n        if (!(handler instanceof HandlerMethod handlerMethod)) {\n            return true;\n        }\n\n        AdminRequired adminRequired = handlerMethod.getMethodAnnotation(AdminRequired.class);\n\n        if (adminRequired == null) {\n            return true;\n        }\n\n        Long userId = UserContext.getCurrentUser();\n        if (userId == null) {\n            throw new UnauthorizedException(\"用户未登录\");\n        }\n\n        // Lazily get the UserClient bean from the context\n        if (userClient == null) {\n            this.userClient = applicationContext.getBean(UserClient.class);\n        }\n\n        R<InfoVO> userResult = userClient.getUser(userId);\n        if (!userResult.success() || userResult.getData() == null) {\n            throw new ForbiddenException(\"无法获取用户信息\");\n        }\n\n        InfoVO userInfo = userResult.getData();\n        if (userInfo.getRole()) {\n            throw new ForbiddenException(\"无权访问，需要管理员权限\");\n        }\n\n        return true;\n    }\n}\n"
  },
  {
    "path": "DOJ-BE/common/src/main/java/com/decade/doj/common/interceptor/IdentityInterceptor.java",
    "content": "package com.decade.doj.common.interceptor;\nimport cn.hutool.core.util.StrUtil;\nimport com.decade.doj.common.config.properties.JwtProperties;\nimport com.decade.doj.common.exception.UnauthorizedException;\nimport com.decade.doj.common.utils.UserContext;\nimport lombok.RequiredArgsConstructor;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.boot.context.properties.EnableConfigurationProperties;\nimport org.springframework.stereotype.Component;\nimport org.springframework.web.servlet.HandlerInterceptor;\n\nimport javax.servlet.http.HttpServletRequest;\nimport javax.servlet.http.HttpServletResponse;\n\n@Slf4j\n@Component\n@EnableConfigurationProperties(JwtProperties.class)\n@RequiredArgsConstructor\npublic class IdentityInterceptor implements HandlerInterceptor {\n\n    private final JwtProperties jwtProperties;\n\n    @Override\n    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {\n        log.debug(\"path: {}\", request.getRequestURI());\n        String userId = request.getHeader(jwtProperties.getSecretKey());\n        if (userId == null) {\n            // throw new UnauthorizedException(\"请访问网关服务!\");\n            userId = String.valueOf(3);\n        }\n        log.debug(\"当前用户ID: {}\", userId);\n        if (StrUtil.isNotBlank(userId)) {\n            UserContext.setCurrentUser(Long.parseLong(userId));\n        }\n        return true;\n    }\n\n    @Override\n    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {\n        UserContext.clear();\n    }\n\n}\n"
  },
  {
    "path": "DOJ-BE/common/src/main/java/com/decade/doj/common/utils/LocalResource.java",
    "content": "package com.decade.doj.common.utils;\n\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.core.io.ClassPathResource;\n\nimport java.io.File;\n\npublic class LocalResource {\n\n    public static String getLocalFilePath(String resourcePath) {\n        try {\n            ClassPathResource resource = new ClassPathResource(resourcePath);\n            File file = resource.getFile();  // 获取文件对象\n            return file.getAbsolutePath().replace(\"\\\\\", \"/\");   // 返回文件的绝对路径\n        } catch (Exception e) {\n            System.out.println(\"获取文件失败\"+e);\n            return null;\n        }\n    }\n\n}\n"
  },
  {
    "path": "DOJ-BE/common/src/main/java/com/decade/doj/common/utils/UserContext.java",
    "content": "package com.decade.doj.common.utils;\n\npublic class UserContext {\n\n    private static final ThreadLocal<Long> userHolder = new ThreadLocal<>();\n\n    public static Long getCurrentUser() {\n        return userHolder.get();\n    }\n\n    public static void setCurrentUser(Long id) {\n        userHolder.set(id);\n    }\n\n    public static void clear() {\n        userHolder.remove();\n    }\n\n}\n"
  },
  {
    "path": "DOJ-BE/common/src/main/resources/META-INF/spring-configuration-metadata.json",
    "content": "{\n  \"groups\": [\n    {\n      \"name\": \"doj.db\"\n    },\n    {\n      \"name\": \"doj.nacos\"\n    },\n    {\n      \"name\": \"doj.swagger\"\n    }\n  ],\n  \"properties\": [\n    {\n      \"name\": \"doj.db.host\",\n      \"type\": \"java.lang.String\",\n      \"description\": \"rabbitmq的地址\",\n      \"defaultValue\": \"192.168.150.101\"\n    }\n  ],\n  \"hints\": []\n}"
  },
  {
    "path": "DOJ-BE/common/src/main/resources/META-INF/spring.factories",
    "content": "org.springframework.boot.autoconfigure.EnableAutoConfiguration=\\\n  com.decade.doj.common.config.global.CommonExceptionConfig"
  },
  {
    "path": "DOJ-BE/common/src/main/resources/application-common.yaml",
    "content": "doj:\n    port:\n        gateway-service: ${DOJ_PORT_GATEWAY:8080}\n        user-service: ${DOJ_PORT_USER:8081}\n        sandbox-service: ${DOJ_PORT_SANDBOX:8082}\n        problem-service: ${DOJ_PORT_PROBLEM:8083}\n        submission-service: ${DOJ_PORT_SUBMISSION:8084}\n\n    resource:\n        location: ${DOJ_RESOURCE_LOCATION:/Users/qzj/Desktop/Development/D-OnlineJudge/static/files/}\n        request: ${DOJ_RESOURCE_REQUEST:/static/}\n\n        code-path: ${DOJ_CODE_PATH:/Users/qzj/Desktop/Development/D-OnlineJudge/static/codes/}\n        problem-code-path: ${DOJ_PROBLEM_CODE_PATH:/Users/qzj/Desktop/Development/D-OnlineJudge/static/problem-codes/}"
  },
  {
    "path": "DOJ-BE/common/src/test/java/com/decade/doj/common/domain/po/ProblemJsonTest.java",
    "content": "package com.decade.doj.common.domain.po;\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.List;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\npublic class ProblemJsonTest {\n\n    @Test\n    void shouldDeserializeSampleArrays() throws Exception {\n        String json = \"{\\\"id\\\":1,\\\"name\\\":\\\"A+B\\\",\\\"inputSample\\\":[\\\"1 2\\\"],\\\"outputSample\\\":[\\\"3\\\"]}\";\n        ObjectMapper mapper = new ObjectMapper();\n        Problem problem = mapper.readValue(json, Problem.class);\n\n        assertEquals(List.of(\"1 2\"), problem.getInputSample());\n        assertEquals(List.of(\"3\"), problem.getOutputSample());\n    }\n}\n"
  },
  {
    "path": "DOJ-BE/gateway-service/Dockerfile",
    "content": "# Stage 1: Build the application\nFROM maven:3.8.4-openjdk-17 AS build\n\nWORKDIR /app\n\n# Copy all pom.xml files first to leverage Docker cache\nCOPY DOJ-BE/pom.xml .\nCOPY DOJ-BE/common/pom.xml ./common/\nCOPY DOJ-BE/gateway-service/pom.xml ./gateway-service/\nCOPY DOJ-BE/problem-service/pom.xml ./problem-service/\nCOPY DOJ-BE/sandbox-service/pom.xml ./sandbox-service/\nCOPY DOJ-BE/submission-service/pom.xml ./submission-service/\nCOPY DOJ-BE/user-service/pom.xml ./user-service/\n\n# Download all dependencies\nRUN mvn dependency:go-offline\n\n# Copy all source code\nCOPY DOJ-BE/common/src ./common/src\nCOPY DOJ-BE/gateway-service/src ./gateway-service/src\n\n# Build the specific service\nRUN mvn -f ./pom.xml -pl gateway-service -am clean package -DskipTests\nRUN ls -al ./gateway-service/target\n\n# Stage 2: Create the runtime image\nFROM eclipse-temurin:17-jre\n\nWORKDIR /app\n\n# Copy the executable jar from the build stage\nCOPY --from=build /app/gateway-service/target/gateway-service.jar ./app.jar\n\nEXPOSE 8080\n\nENTRYPOINT [\"java\", \"-jar\", \"./app.jar\"]"
  },
  {
    "path": "DOJ-BE/gateway-service/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n         xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n    <parent>\n        <groupId>com.decade</groupId>\n        <artifactId>DOJ-BE</artifactId>\n        <version>1.0-SNAPSHOT</version>\n    </parent>\n\n    <artifactId>gateway-service</artifactId>\n\n    <properties>\n        <maven.compiler.source>17</maven.compiler.source>\n        <maven.compiler.target>17</maven.compiler.target>\n        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>\n    </properties>\n\n    <dependencies>\n        <!--common-->\n        <dependency>\n            <groupId>com.decade</groupId>\n            <artifactId>common</artifactId>\n            <version>1.0-SNAPSHOT</version>\n        </dependency>\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-autoconfigure</artifactId>\n        </dependency>\n\n        <!--网关-->\n        <dependency>\n            <groupId>org.springframework.cloud</groupId>\n            <artifactId>spring-cloud-starter-gateway</artifactId>\n        </dependency>\n        <!--nacos discovery-->\n        <dependency>\n            <groupId>com.alibaba.cloud</groupId>\n            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>\n        </dependency>\n        <!--负载均衡-->\n        <dependency>\n            <groupId>org.springframework.cloud</groupId>\n            <artifactId>spring-cloud-starter-loadbalancer</artifactId>\n        </dependency>\n        <!-- sentinel -->\n        <dependency>\n            <groupId>com.alibaba.cloud</groupId>\n            <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>\n        </dependency>\n        <!-- sentinel持久化到nacos -->\n        <dependency>\n            <groupId>com.alibaba.csp</groupId>\n            <artifactId>sentinel-datasource-nacos</artifactId>\n        </dependency>\n    </dependencies>\n\n    <build>\n        <finalName>${project.artifactId}</finalName>\n        <plugins>\n            <plugin>\n                <groupId>org.springframework.boot</groupId>\n                <artifactId>spring-boot-maven-plugin</artifactId>\n            </plugin>\n        </plugins>\n    </build>\n\n</project>"
  },
  {
    "path": "DOJ-BE/gateway-service/src/main/java/com/decade/doj/gateway/GatewayApplication.java",
    "content": "package com.decade.doj.gateway;\n\nimport com.decade.doj.common.config.custom.JwtTool;\nimport org.springframework.boot.SpringApplication;\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\nimport org.springframework.context.annotation.Import;\n\n@SpringBootApplication\n@Import({JwtTool.class})\npublic class GatewayApplication {\n    public static void main(String[] args) {\n        SpringApplication.run(GatewayApplication.class, args);\n    }\n}\n"
  },
  {
    "path": "DOJ-BE/gateway-service/src/main/java/com/decade/doj/gateway/config/properties/AuthProperties.java",
    "content": "package com.decade.doj.gateway.config.properties;\n\nimport lombok.Data;\nimport org.springframework.boot.context.properties.ConfigurationProperties;\nimport org.springframework.stereotype.Component;\n\nimport java.util.List;\n\n@Data\n@ConfigurationProperties(prefix = \"doj.auth\")\npublic class AuthProperties {\n    private List<String> includePaths;\n    private List<String> excludePaths;\n}\n"
  },
  {
    "path": "DOJ-BE/gateway-service/src/main/java/com/decade/doj/gateway/filters/AuthGlobalFilter.java",
    "content": "package com.decade.doj.gateway.filters;\n\nimport cn.hutool.core.text.AntPathMatcher;\nimport com.decade.doj.common.config.properties.JwtProperties;\nimport com.decade.doj.common.domain.R;\nimport com.decade.doj.common.exception.UnauthorizedException;\nimport com.decade.doj.common.config.custom.JwtTool;\nimport com.decade.doj.gateway.config.properties.AuthProperties;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport lombok.RequiredArgsConstructor;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.boot.context.properties.EnableConfigurationProperties;\nimport org.springframework.cloud.gateway.filter.GatewayFilterChain;\nimport org.springframework.cloud.gateway.filter.GlobalFilter;\nimport org.springframework.core.Ordered;\nimport org.springframework.core.io.buffer.DataBuffer;\nimport org.springframework.core.io.buffer.DataBufferFactory;\nimport org.springframework.http.HttpStatus;\nimport org.springframework.http.MediaType;\nimport org.springframework.stereotype.Component;\nimport org.springframework.web.server.ServerWebExchange;\nimport reactor.core.publisher.Mono;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\n@Component\n@RequiredArgsConstructor\n@EnableConfigurationProperties({AuthProperties.class, JwtProperties.class})\n@Slf4j\npublic class AuthGlobalFilter implements GlobalFilter, Ordered {\n\n    private final JwtTool jwtTool;\n    private final AuthProperties authProperties;\n    private final JwtProperties jwtProperties;\n\n    private AntPathMatcher antPathMatcher = new AntPathMatcher();\n\n    @Override\n    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {\n        log.info(jwtProperties.getSecretKey());\n        String path = exchange.getRequest().getURI().getPath();\n        if (isExcludedPath(path)) {\n            ServerWebExchange build = exchange.mutate()\n                    .request(builder -> builder.header(jwtProperties.getSecretKey(), String.valueOf(0)))\n                    .build();\n            return chain.filter(build);\n        }\n        log.info(\"path:{}\", path);\n        log.info(\"headers:{}\", exchange.getRequest().getHeaders().entrySet());\n        String token = exchange.getRequest().getHeaders().getFirst(jwtProperties.getAuthorization());\n        Long userId;\n        try {\n            userId = jwtTool.parseToken(token);\n        } catch (UnauthorizedException e) {\n            return handleUnauthorizedResponse(exchange);\n        }\n        // 校验成功，将userId放入请求头\n        ServerWebExchange build = exchange.mutate()\n                .request(builder -> builder.header(jwtProperties.getSecretKey(), String.valueOf(userId)))\n                .build();\n        return chain.filter(build);\n    }\n\n    public Mono<Void> handleUnauthorizedResponse(ServerWebExchange exchange) {\n        exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);\n        exchange.getResponse().getHeaders().setContentType(MediaType.APPLICATION_JSON);\n\n        R<String> response = R.error(401, \"登陆过期，请重新登陆！\");\n\n        DataBufferFactory bufferFactory = exchange.getResponse().bufferFactory();\n\n        try {\n            ObjectMapper objectMapper = new ObjectMapper();\n            byte[] bytes = objectMapper.writeValueAsBytes(response);\n            DataBuffer dataBuffer = bufferFactory.wrap(bytes);\n            return exchange.getResponse().writeWith(Mono.just(dataBuffer));\n        } catch (Exception e) {\n            return exchange.getResponse().setComplete();\n        }\n    }\n\n    private boolean isExcludedPath(String path) {\n        for (String ignorePath : authProperties.getExcludePaths()) {\n            if (antPathMatcher.match(ignorePath, path)) {\n                return true;\n            }\n        }\n        return false;\n    }\n\n    @Override\n    public int getOrder() {\n        return 0;\n    }\n}\n"
  },
  {
    "path": "DOJ-BE/gateway-service/src/main/java/com/decade/doj/gateway/filters/CrossFilter.java",
    "content": "package com.decade.doj.gateway.filters;\n\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.web.cors.CorsConfiguration;\nimport org.springframework.web.cors.reactive.CorsWebFilter;\nimport org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;\n\n@Configuration\npublic class CrossFilter {\n    /**\n     * 添加跨域过滤器\n     * @return\n     */\n    @Bean\n    public CorsWebFilter corsWebFilter(){\n        //基于url跨域，选择reactive包下的\n        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();\n        // 跨域配置信息\n        CorsConfiguration configuration = new CorsConfiguration();\n        // 允许跨域的头\n        configuration.addAllowedHeader(\"*\");\n        // 允许跨域的请求方式\n        configuration.addAllowedMethod(\"*\");\n        // 允许跨域的请求来源\n        configuration.addAllowedOrigin(\"http://localhost:5174\");\n        configuration.addAllowedOrigin(\"http://localhost:5173\");\n        configuration.addAllowedOrigin(\"http://localhost:8088\");\n        // 预检请求的有效期，单位为秒\n        configuration.setMaxAge(3600L);\n        // 是否允许携带cookie跨域\n        configuration.setAllowCredentials(true);\n        // 任意url都要进行跨域配置\n        source.registerCorsConfiguration(\"/**\", configuration);\n        return new CorsWebFilter(source);\n    }\n}"
  },
  {
    "path": "DOJ-BE/gateway-service/src/main/resources/application.yaml",
    "content": "server:\n    port: ${doj.port.gateway-service}\n\nmanagement:\n    endpoints:\n        web:\n            exposure:\n                include: health,prometheus\n\nspring:\n  cloud:\n    sentinel:\n      transport:\n        dashboard: ${SENTINEL_DASHBOARD_ADDR:localhost:8858}\n      datasource:\n        ds1:\n          nacos:\n            server-addr: ${spring.cloud.nacos.server-addr}\n            data-id: gateway-sentinel-rules\n            group-id: DEFAULT_GROUP\n            data-type: json\n            rule-type: flow\n        degrade:\n          nacos:\n            server-addr: ${spring.cloud.nacos.server-addr}\n            data-id: gateway-sentinel-degrade-rules\n            group-id: DEFAULT_GROUP\n            data-type: json\n            rule-type: degrade\n\ndoj:\n    auth:\n        excludePaths:\n            - /\n            - /static/**\n            - /user/login\n            - /user/register\n            - /user/refresh"
  },
  {
    "path": "DOJ-BE/gateway-service/src/main/resources/bootstrap.yaml",
    "content": "spring:\n    application:\n        name: gateway-service\n    profiles:\n        active: dev, common\n    cloud:\n        nacos:\n            server-addr: ${NACOS_SERVER_ADDR:127.0.0.1:8848}\n            config:\n                file-extension: yaml\n                shared-configs:\n                    -   dataId: shared-jwt.yaml\n        gateway:\n            routes:\n                -   id: user\n                    uri: lb://user-service\n                    predicates:\n                        - Path=/user/**,/static/**\n                -   id: problem\n                    uri: lb://problem-service\n                    predicates:\n                        - Path=/problem/**\n                -   id: sandbox\n                    uri: lb://sandbox-service\n                    predicates:\n                        - Path=/sandbox/**\n                -   id: submission\n                    uri: lb://submission-service\n                    predicates:\n                        - Path=/submission/**"
  },
  {
    "path": "DOJ-BE/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n         xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n\n    <groupId>com.decade</groupId>\n    <artifactId>DOJ-BE</artifactId>\n    <version>1.0-SNAPSHOT</version>\n    <packaging>pom</packaging>\n    <modules>\n        <module>user-service</module>\n        <module>gateway-service</module>\n        <module>common</module>\n        <module>sandbox-service</module>\n        <module>problem-service</module>\n        <module>submission-service</module>\n    </modules>\n\n    <parent>\n        <groupId>org.springframework.boot</groupId>\n        <artifactId>spring-boot-starter-parent</artifactId>\n        <version>2.7.12</version>\n        <relativePath/>\n    </parent>\n\n    <properties>\n        <maven.compiler.source>17</maven.compiler.source>\n        <maven.compiler.target>17</maven.compiler.target>\n        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>\n        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>\n        <spring-cloud.version>2021.0.3</spring-cloud.version>\n        <spring-cloud-alibaba.version>2021.0.4.0</spring-cloud-alibaba.version>\n        <org.projectlombok.version>1.18.32</org.projectlombok.version>\n        <mybatis-plus.version>3.5.2</mybatis-plus.version>\n        <mysql.version>8.0.23</mysql.version>\n        <knife4j.version>4.5.0</knife4j.version>\n        <hutool.version>5.8.21</hutool.version>\n    </properties>\n\n    <dependencyManagement>\n        <dependencies>\n            <dependency>\n                <groupId>org.springframework.cloud</groupId>\n                <artifactId>spring-cloud-dependencies</artifactId>\n                <version>${spring-cloud.version}</version>\n                <type>pom</type>\n                <scope>import</scope>\n            </dependency>\n            <dependency>\n                <groupId>com.alibaba.cloud</groupId>\n                <artifactId>spring-cloud-alibaba-dependencies</artifactId>\n                <version>${spring-cloud-alibaba.version}</version>\n                <type>pom</type>\n                <scope>import</scope>\n            </dependency>\n            <dependency>\n                <groupId>com.baomidou</groupId>\n                <artifactId>mybatis-plus-boot-starter</artifactId>\n                <version>${mybatis-plus.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>mysql</groupId>\n                <artifactId>mysql-connector-java</artifactId>\n                <version>${mysql.version}</version>\n            </dependency>\n        </dependencies>\n    </dependencyManagement>\n\n    <dependencies>\n        <dependency>\n            <groupId>org.projectlombok</groupId>\n            <artifactId>lombok</artifactId>\n            <version>${org.projectlombok.version}</version>\n        </dependency>\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-test</artifactId>\n            <scope>test</scope>\n        </dependency>\n<!--        <dependency>-->\n<!--            <groupId>com.github.xiaoymin</groupId>-->\n<!--            <artifactId>knife4j-openapi2-spring-boot-starter</artifactId>-->\n<!--            <version>4.1.0</version>-->\n<!--        </dependency>-->\n        <dependency>\n            <groupId>com.github.xiaoymin</groupId>\n            <artifactId>knife4j-openapi3-spring-boot-starter</artifactId>\n            <version>4.5.0</version>\n        </dependency>\n        <dependency>\n            <groupId>cn.hutool</groupId>\n            <artifactId>hutool-all</artifactId>\n            <version>${hutool.version}</version>\n        </dependency>\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-validation</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-data-redis</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-amqp</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.alibaba</groupId>\n            <artifactId>fastjson</artifactId>\n            <version>2.0.58</version>\n        </dependency>\n        <!-- actuator -->\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-actuator</artifactId>\n        </dependency>\n        <!-- micrometer-prometheus -->\n        <dependency>\n            <groupId>io.micrometer</groupId>\n            <artifactId>micrometer-registry-prometheus</artifactId>\n        </dependency>\n    </dependencies>\n\n    <build>\n        <pluginManagement>\n            <plugins>\n                <plugin>\n                    <groupId>org.apache.maven.plugins</groupId>\n                    <artifactId>maven-compiler-plugin</artifactId>\n                    <version>3.8.1</version>\n                    <configuration>\n                        <source>17</source> <!-- depending on your project -->\n                        <target>17</target> <!-- depending on your project -->\n                    </configuration>\n                </plugin>\n            </plugins>\n        </pluginManagement>\n    </build>\n\n</project>"
  },
  {
    "path": "DOJ-BE/problem-service/Dockerfile",
    "content": "# Stage 1: Build the application\nFROM maven:3.8.4-openjdk-17 AS build\n\nWORKDIR /app\n\n# Copy all pom.xml files first to leverage Docker cache\nCOPY DOJ-BE/pom.xml .\nCOPY DOJ-BE/common/pom.xml ./common/\nCOPY DOJ-BE/gateway-service/pom.xml ./gateway-service/\nCOPY DOJ-BE/problem-service/pom.xml ./problem-service/\nCOPY DOJ-BE/sandbox-service/pom.xml ./sandbox-service/\nCOPY DOJ-BE/submission-service/pom.xml ./submission-service/\nCOPY DOJ-BE/user-service/pom.xml ./user-service/\n\n# Download all dependencies\nRUN mvn dependency:go-offline\n\n# Copy all source code\nCOPY DOJ-BE/common/src ./common/src\nCOPY DOJ-BE/problem-service/src ./problem-service/src\n\n# Build the specific service\nRUN mvn -f ./pom.xml -pl problem-service -am clean package -DskipTests\nRUN ls -al ./problem-service/target\n\n# Stage 2: Create the runtime image\nFROM eclipse-temurin:17-jre\n\nWORKDIR /app\n\n# Copy the executable jar from the build stage\nCOPY --from=build /app/problem-service/target/problem-service.jar ./app.jar\n\nEXPOSE 8083\n\nENTRYPOINT [\"java\", \"-jar\", \"./app.jar\"]"
  },
  {
    "path": "DOJ-BE/problem-service/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n         xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n    <parent>\n        <groupId>com.decade</groupId>\n        <artifactId>DOJ-BE</artifactId>\n        <version>1.0-SNAPSHOT</version>\n    </parent>\n\n    <artifactId>problem-service</artifactId>\n\n    <properties>\n        <maven.compiler.source>17</maven.compiler.source>\n        <maven.compiler.target>17</maven.compiler.target>\n        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>\n    </properties>\n\n\n    <dependencies>\n        <!--common-->\n        <dependency>\n            <groupId>com.decade</groupId>\n            <artifactId>common</artifactId>\n            <version>1.0-SNAPSHOT</version>\n        </dependency>\n        <!--web-->\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-web</artifactId>\n        </dependency>\n        <!--数据库-->\n        <dependency>\n            <groupId>mysql</groupId>\n            <artifactId>mysql-connector-java</artifactId>\n        </dependency>\n        <!--mybatis-->\n        <dependency>\n            <groupId>com.baomidou</groupId>\n            <artifactId>mybatis-plus-boot-starter</artifactId>\n        </dependency>\n        <!--nacos 服务注册发现-->\n        <dependency>\n            <groupId>com.alibaba.cloud</groupId>\n            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>\n        </dependency>\n        <!-- redis -->\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-data-redis</artifactId>\n        </dependency>\n        <!-- caffeine -->\n        <dependency>\n            <groupId>com.github.ben-manes.caffeine</groupId>\n            <artifactId>caffeine</artifactId>\n        </dependency>\n        <!-- elasticsearch -->\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-data-elasticsearch</artifactId>\n        </dependency>\n    </dependencies>\n    <build>\n        <finalName>${project.artifactId}</finalName>\n        <plugins>\n            <plugin>\n                <groupId>org.springframework.boot</groupId>\n                <artifactId>spring-boot-maven-plugin</artifactId>\n            </plugin>\n        </plugins>\n    </build>\n\n</project>"
  },
  {
    "path": "DOJ-BE/problem-service/src/main/java/com/decade/doj/problem/ProblemApplication.java",
    "content": "package com.decade.doj.problem;\n\nimport com.decade.doj.common.config.custom.DefaultFeignConfig;\nimport com.decade.doj.common.config.custom.JwtTool;\nimport com.decade.doj.common.config.custom.MVCConfig;\nimport com.decade.doj.common.config.custom.MybatisConfig;\nimport com.decade.doj.common.interceptor.AdminCheckInterceptor;\nimport com.decade.doj.common.interceptor.IdentityInterceptor;\nimport org.mybatis.spring.annotation.MapperScan;\nimport org.springframework.boot.SpringApplication;\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\nimport org.springframework.cache.annotation.EnableCaching;\nimport org.springframework.cloud.openfeign.EnableFeignClients;\nimport org.springframework.context.annotation.Import;\n\n@SpringBootApplication\n@MapperScan(\"com.decade.doj.problem.mapper\")\n@EnableCaching\n@EnableFeignClients(basePackages = \"com.decade.doj.common.client\", defaultConfiguration = DefaultFeignConfig.class)\n@Import({MVCConfig.class, MybatisConfig.class, IdentityInterceptor.class, AdminCheckInterceptor.class})\npublic class ProblemApplication {\n    public static void main(String[] args) {\n        SpringApplication.run(ProblemApplication.class, args);\n    }\n}\n"
  },
  {
    "path": "DOJ-BE/problem-service/src/main/java/com/decade/doj/problem/config/CacheConfig.java",
    "content": "package com.decade.doj.problem.config;\n\nimport com.github.benmanes.caffeine.cache.Caffeine;\nimport org.springframework.cache.CacheManager;\nimport org.springframework.cache.caffeine.CaffeineCacheManager;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.context.annotation.Primary;\nimport org.springframework.data.redis.cache.RedisCacheConfiguration;\nimport org.springframework.data.redis.cache.RedisCacheManager;\nimport org.springframework.data.redis.connection.RedisConnectionFactory;\nimport org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;\nimport org.springframework.data.redis.serializer.RedisSerializationContext;\n\nimport java.time.Duration;\n\n@Configuration\npublic class CacheConfig {\n\n    // L1 本地缓存 (Caffeine)\n    @Bean\n    public CacheManager caffeineCacheManager() {\n        CaffeineCacheManager cacheManager = new CaffeineCacheManager();\n        cacheManager.setCaffeine(Caffeine.newBuilder()\n                .initialCapacity(100) // 初始大小\n                .maximumSize(500)     // 最大数量\n                .expireAfterWrite(Duration.ofMinutes(5))); // 5分钟后过期\n        return cacheManager;\n    }\n\n    // L2 分布式缓存 (Redis)\n    @Primary // 将 Redis 缓存作为默认和主要的缓存管理器\n    @Bean\n    public CacheManager redisCacheManager(RedisConnectionFactory redisConnectionFactory) {\n        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()\n                .entryTtl(Duration.ofMinutes(30)) // L2 缓存时间更长\n                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));\n\n        return RedisCacheManager.builder(redisConnectionFactory)\n                .cacheDefaults(config)\n                .build();\n    }\n}"
  },
  {
    "path": "DOJ-BE/problem-service/src/main/java/com/decade/doj/problem/config/ProblemIndexInitializer.java",
    "content": "package com.decade.doj.problem.config;\n\nimport com.decade.doj.problem.domain.document.ProblemDocument;\nimport lombok.RequiredArgsConstructor;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.data.elasticsearch.core.ElasticsearchRestTemplate;\nimport org.springframework.data.elasticsearch.core.IndexOperations;\nimport org.springframework.stereotype.Component;\n\nimport javax.annotation.PostConstruct;\n\n@Slf4j\n@Component\n@RequiredArgsConstructor\npublic class ProblemIndexInitializer {\n\n    private final ElasticsearchRestTemplate elasticsearchTemplate;\n\n    @PostConstruct\n    public void initIndex() {\n        IndexOperations indexOps = elasticsearchTemplate.indexOps(ProblemDocument.class);\n        if (!indexOps.exists()) {\n            indexOps.create();\n            indexOps.putMapping(indexOps.createMapping());\n            log.info(\"Created Elasticsearch index: {}\", indexOps.getIndexCoordinates().getIndexName());\n        }\n    }\n}\n"
  },
  {
    "path": "DOJ-BE/problem-service/src/main/java/com/decade/doj/problem/controller/ProblemController.java",
    "content": "package com.decade.doj.problem.controller;\n\n\nimport com.decade.doj.common.annotation.AdminRequired;\nimport com.decade.doj.common.domain.PageDTO;\nimport com.decade.doj.common.domain.R;\nimport com.decade.doj.problem.domain.dto.ProblemPageQueryDTO;\nimport com.decade.doj.problem.domain.po.Problem;\nimport com.decade.doj.problem.service.IProblemService;\nimport io.swagger.v3.oas.annotations.Operation;\nimport io.swagger.v3.oas.annotations.tags.Tag;\nimport lombok.RequiredArgsConstructor;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.web.bind.annotation.*;\n\nimport java.util.List;\n\n/**\n * <p>\n *  前端控制器\n * </p>\n *\n * @author \n * @since 2024-09-17\n */\n@RestController\n@RequestMapping(\"/problem\")\n@Tag(name = \"题目相关接口\")\n@Slf4j\n@RequiredArgsConstructor\npublic class ProblemController {\n\n    private final IProblemService problemService;\n\n    @PostMapping(\"/admin/sync-es\")\n    @AdminRequired\n    @Operation(summary = \"同步题目数据到elasticsearch\")\n    public R<Integer> syncProblemToEs() {\n        Integer res = problemService.syncAllToElasticsearch();\n        return R.ok(res);\n    }\n\n    @PostMapping(\"/admin/reindex\")\n    @AdminRequired\n    @Operation(summary = \"重建索引并同步题目\")\n    public R<Integer> reindexProblems() {\n        Integer res = problemService.reindexAll();\n        return R.ok(res);\n    }\n\n    @PostMapping(\"/admin/reset\")\n    @AdminRequired\n    @Operation(summary = \"清空 problem 数据与 ES 索引\")\n    public R<Void> resetProblems() {\n        problemService.resetProblems();\n        return R.ok();\n    }\n\n    @PostMapping\n    @AdminRequired\n    @Operation(summary = \"新增题目\")\n    public R<Void> createProblem(@RequestBody Problem problem) {\n        problemService.save(problem);\n        return R.ok();\n    }\n\n    @PutMapping\n    @AdminRequired\n    @Operation(summary = \"修改题目\")\n    public R<Void> updateProblem(@RequestBody Problem problem) {\n        problemService.updateById(problem);\n        return R.ok();\n    }\n\n    @DeleteMapping(\"/{id}\")\n    @AdminRequired\n    @Operation(summary = \"删除题目\")\n    public R<Void> deleteProblem(@PathVariable Long id) {\n        problemService.removeById(id);\n        return R.ok();\n    }\n\n    @GetMapping(\"/list\")\n    @Operation(summary = \"获取题目列表\")\n    public R<List<Problem>> list() {\n        return R.ok(problemService.list());\n    }\n\n    @GetMapping(\"/count\")\n    @Operation(summary = \"获取题目总数\")\n    public R<Long> getCount() {\n        return R.ok(problemService.count());\n    }\n\n    @GetMapping(\"/page\")\n    @Operation(summary = \"分页获取题目列表\")\n    public R<PageDTO<Problem>> page(ProblemPageQueryDTO problemPageQueryDTO) {\n        PageDTO<Problem> res = problemService.pageQuery(problemPageQueryDTO);\n        return R.ok(res);\n    }\n\n    @GetMapping(\"/{id}\")\n    @Operation(summary = \"获取题目详情\")\n    public R<Problem> getProblemById(@PathVariable Long id) {\n        return R.ok(problemService.getById(id));\n    }\n\n}\n"
  },
  {
    "path": "DOJ-BE/problem-service/src/main/java/com/decade/doj/problem/domain/document/ProblemDocument.java",
    "content": "package com.decade.doj.problem.domain.document;\n\nimport lombok.AllArgsConstructor;\nimport lombok.Data;\nimport lombok.NoArgsConstructor;\nimport org.springframework.data.annotation.Id;\nimport org.springframework.data.elasticsearch.annotations.Document;\nimport org.springframework.data.elasticsearch.annotations.Field;\nimport org.springframework.data.elasticsearch.annotations.FieldType;\nimport org.springframework.data.elasticsearch.annotations.Setting;\n\n@Data\n@NoArgsConstructor\n@AllArgsConstructor\n@Document(indexName = \"doj_problem\")\n@Setting(settingPath = \"es/problem-settings.json\")\npublic class ProblemDocument {\n\n    @Id\n    private Long id;\n\n    // 题目名称，全文检索（大小写不敏感）\n    @Field(type = FieldType.Text, analyzer = \"ik_max_lowercase\", searchAnalyzer = \"ik_smart_lowercase\")\n    private String name;\n\n    // 题目描述，全文检索\n    @Field(type = FieldType.Text, analyzer = \"ik_max_lowercase\", searchAnalyzer = \"ik_smart_lowercase\")\n    private String description;\n}\n"
  },
  {
    "path": "DOJ-BE/problem-service/src/main/java/com/decade/doj/problem/domain/dto/ProblemPageQueryDTO.java",
    "content": "package com.decade.doj.problem.domain.dto;\n\nimport com.decade.doj.common.domain.PageQueryDTO;\nimport io.swagger.v3.oas.annotations.media.Schema;\nimport lombok.Data;\nimport lombok.EqualsAndHashCode;\nimport lombok.experimental.Accessors;\nimport java.util.List;\n\n@EqualsAndHashCode(callSuper = true)\n@Data\n@Accessors(chain = true)\npublic class ProblemPageQueryDTO extends PageQueryDTO {\n\n    @Schema(description = \"题目名称\")\n    private String name;\n\n    @Schema(description = \"题目描述\")\n    private String description;\n\n    @Schema(description = \"难度\")\n    private String difficulty;\n\n    @Schema(description = \"标签\")\n    private List<String> tags;\n\n    @Schema(description = \"状态: 已解决/尝试中/未开始\")\n    private String status;\n}\n"
  },
  {
    "path": "DOJ-BE/problem-service/src/main/java/com/decade/doj/problem/domain/dto/ProblemTagView.java",
    "content": "package com.decade.doj.problem.domain.dto;\n\nimport lombok.Data;\n\n@Data\npublic class ProblemTagView {\n\n    private Long problemId;\n    private String tagName;\n}\n"
  },
  {
    "path": "DOJ-BE/problem-service/src/main/java/com/decade/doj/problem/domain/po/Problem.java",
    "content": "package com.decade.doj.problem.domain.po;\n\nimport com.baomidou.mybatisplus.annotation.TableField;\nimport com.baomidou.mybatisplus.annotation.TableName;\nimport com.baomidou.mybatisplus.annotation.IdType;\nimport com.baomidou.mybatisplus.annotation.TableId;\nimport com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;\nimport java.io.Serializable;\nimport java.util.List;\n\nimport io.swagger.v3.oas.annotations.media.Schema;\nimport lombok.Data;\nimport lombok.EqualsAndHashCode;\nimport lombok.experimental.Accessors;\n\n/**\n * <p>\n * \n * </p>\n *\n * @author \n * @since 2024-09-17\n */\n@Data\n@EqualsAndHashCode(callSuper = false)\n@Accessors(chain = true)\n@TableName(value = \"problem\", autoResultMap = true)\n@Schema(description=\"Problem对象\")\npublic class Problem implements Serializable {\n\n    private static final long serialVersionUID = 1L;\n\n    @TableId(value = \"id\", type = IdType.AUTO)\n    private Long id;\n\n    @TableField(\"name\")\n    private String name;\n\n    @TableField(\"description\")\n    private String description;\n\n    @TableField(\"input_style\")\n    private String inputStyle;\n\n    @TableField(\"output_style\")\n    private String outputStyle;\n\n    @TableField(value = \"input_sample\", typeHandler = JacksonTypeHandler.class)\n    private java.util.List<String> inputSample;\n\n    @TableField(value = \"output_sample\", typeHandler = JacksonTypeHandler.class)\n    private java.util.List<String> outputSample;\n\n    @TableField(\"hint\")\n    private String hint;\n\n    @TableField(\"difficulty\")\n    private String difficulty;\n\n    @TableField(\"time_limit\")\n    private Integer timeLimit;\n\n    @TableField(\"memory_limit\")\n    private Integer memoryLimit;\n\n    @TableField(\"total_pass\")\n    private Integer totalPass;\n\n    @TableField(\"total_attempt\")\n    private Integer totalAttempt;\n\n    @TableField(exist = false)\n    private java.util.List<String> tags;\n\n    @TableField(exist = false)\n    private String status;\n\n    @TableField(\"test_data\")\n    private String testData;\n\n    @TableField(\"test_ans\")\n    private String testAns;\n}\n"
  },
  {
    "path": "DOJ-BE/problem-service/src/main/java/com/decade/doj/problem/domain/po/ProblemTag.java",
    "content": "package com.decade.doj.problem.domain.po;\n\nimport com.baomidou.mybatisplus.annotation.TableField;\nimport com.baomidou.mybatisplus.annotation.TableName;\nimport io.swagger.v3.oas.annotations.media.Schema;\nimport lombok.AllArgsConstructor;\nimport lombok.Data;\nimport lombok.NoArgsConstructor;\n\nimport java.io.Serializable;\n\n@Data\n@NoArgsConstructor\n@AllArgsConstructor\n@TableName(\"problem_tag\")\n@Schema(description = \"题目与标签关联对象\")\npublic class ProblemTag implements Serializable {\n\n    private static final long serialVersionUID = 1L;\n\n    @TableField(\"problem_id\")\n    private Long problemId;\n\n    @TableField(\"tag_id\")\n    private Long tagId;\n}\n"
  },
  {
    "path": "DOJ-BE/problem-service/src/main/java/com/decade/doj/problem/domain/po/Tag.java",
    "content": "package com.decade.doj.problem.domain.po;\n\nimport com.baomidou.mybatisplus.annotation.IdType;\nimport com.baomidou.mybatisplus.annotation.TableField;\nimport com.baomidou.mybatisplus.annotation.TableId;\nimport com.baomidou.mybatisplus.annotation.TableName;\nimport io.swagger.v3.oas.annotations.media.Schema;\nimport lombok.Data;\nimport lombok.experimental.Accessors;\n\nimport java.io.Serializable;\n\n@Data\n@Accessors(chain = true)\n@TableName(\"tag\")\n@Schema(description = \"标签对象\")\npublic class Tag implements Serializable {\n\n    private static final long serialVersionUID = 1L;\n\n    @TableId(value = \"id\", type = IdType.AUTO)\n    private Long id;\n\n    @TableField(\"name\")\n    private String name;\n\n}\n"
  },
  {
    "path": "DOJ-BE/problem-service/src/main/java/com/decade/doj/problem/mapper/ProblemMapper.java",
    "content": "package com.decade.doj.problem.mapper;\n\nimport com.baomidou.mybatisplus.core.mapper.BaseMapper;\nimport com.baomidou.mybatisplus.core.metadata.IPage;\nimport com.baomidou.mybatisplus.extension.plugins.pagination.Page;\nimport com.decade.doj.problem.domain.po.Problem;\nimport org.apache.ibatis.annotations.Param;\n\nimport java.util.List;\n\n/**\n * <p>\n *  Mapper 接口\n * </p>\n *\n * @author \n * @since 2024-09-17\n */\npublic interface ProblemMapper extends BaseMapper<Problem> {\n\n    IPage<Problem> selectPageWithFilters(Page<Problem> page,\n                                         @Param(\"ids\") List<Long> ids,\n                                         @Param(\"difficulty\") String difficulty,\n                                         @Param(\"tagNames\") List<String> tagNames,\n                                         @Param(\"status\") String status,\n                                         @Param(\"userId\") Long userId);\n\n}\n"
  },
  {
    "path": "DOJ-BE/problem-service/src/main/java/com/decade/doj/problem/mapper/ProblemTagMapper.java",
    "content": "package com.decade.doj.problem.mapper;\n\nimport com.baomidou.mybatisplus.core.mapper.BaseMapper;\nimport com.decade.doj.problem.domain.dto.ProblemTagView;\nimport com.decade.doj.problem.domain.po.ProblemTag;\nimport org.apache.ibatis.annotations.Mapper;\nimport org.apache.ibatis.annotations.Param;\nimport org.apache.ibatis.annotations.Select;\n\nimport java.util.List;\n\n@Mapper\npublic interface ProblemTagMapper extends BaseMapper<ProblemTag> {\n\n    @Select({\n            \"<script>\",\n            \"SELECT pt.problem_id AS problemId, t.name AS tagName\",\n            \"FROM problem_tag pt\",\n            \"JOIN tag t ON pt.tag_id = t.id\",\n            \"WHERE pt.problem_id IN\",\n            \"<foreach collection='problemIds' item='id' open='(' separator=',' close=')'>\",\n            \"#{id}\",\n            \"</foreach>\",\n            \"</script>\"\n    })\n    List<ProblemTagView> selectTagsByProblemIds(@Param(\"problemIds\") List<Long> problemIds);\n}\n"
  },
  {
    "path": "DOJ-BE/problem-service/src/main/java/com/decade/doj/problem/mapper/TagMapper.java",
    "content": "package com.decade.doj.problem.mapper;\n\nimport com.baomidou.mybatisplus.core.mapper.BaseMapper;\nimport com.decade.doj.problem.domain.po.Tag;\nimport org.apache.ibatis.annotations.Mapper;\n\n@Mapper\npublic interface TagMapper extends BaseMapper<Tag> {\n}\n"
  },
  {
    "path": "DOJ-BE/problem-service/src/main/java/com/decade/doj/problem/mq/MqConfig.java",
    "content": "package com.decade.doj.problem.mq;\n\nimport org.springframework.amqp.core.*;\nimport org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;\nimport org.springframework.amqp.support.converter.MessageConverter;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\n\n@Configuration\npublic class MqConfig {\n\n    // --- 用于题目统计更新的队列 ---\n    @Bean\n    public TopicExchange topicExchange() {\n        return new TopicExchange(\"doj.topic\");\n    }\n\n    @Bean\n    public Queue problemStatsUpdateQueue() {\n        return new Queue(\"problem.stats.update.queue\");\n    }\n\n    @Bean\n    public Binding statsBinding() {\n        return BindingBuilder.bind(problemStatsUpdateQueue()).to(topicExchange()).with(\"submission.created\");\n    }\n\n    // --- 用于多级缓存同步的广播队列 ---\n    @Bean\n    public FanoutExchange cacheUpdateExchange() {\n        return new FanoutExchange(\"cache.update.exchange\");\n    }\n\n    @Bean\n    public Queue problemCacheUpdateQueue() {\n        // 匿名队列：每个消费者实例都有自己的、临时的、独占的队列\n        return new AnonymousQueue();\n    }\n\n    @Bean\n    public Binding cacheBinding() {\n        return BindingBuilder.bind(problemCacheUpdateQueue()).to(cacheUpdateExchange());\n    }\n\n    @Bean\n    public MessageConverter jsonMessageConverter(){\n        return new Jackson2JsonMessageConverter();\n    }\n}\n"
  },
  {
    "path": "DOJ-BE/problem-service/src/main/java/com/decade/doj/problem/mq/StatsUpdateListener.java",
    "content": "package com.decade.doj.problem.mq;\n\nimport com.decade.doj.problem.service.IProblemService;\nimport lombok.RequiredArgsConstructor;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.amqp.rabbit.annotation.RabbitListener;\nimport org.springframework.stereotype.Component;\n\nimport java.util.Map;\n\n@Slf4j\n@Component\n@RequiredArgsConstructor\npublic class StatsUpdateListener {\n\n    private final IProblemService problemService;\n\n    @RabbitListener(queues = \"problem.stats.update.queue\")\n    public void listenStatsUpdateQueue(Map<String, Object> message) {\n        if (message == null) {\n            return;\n        }\n        Object problemIdObj = message.get(\"problemId\");\n        Long problemId = null;\n        if (problemIdObj instanceof Integer) {\n            problemId = ((Integer) problemIdObj).longValue();\n        } else if (problemIdObj instanceof Long) {\n            problemId = (Long) problemIdObj;\n        }\n        Boolean isAccepted = (Boolean) message.get(\"isAccepted\");\n\n        if (problemId == null || isAccepted == null) {\n            log.error(\"接收到无效的消息：{}\", message);\n            return;\n        }\n        \n        log.info(\"接收到题目提交消息，开始更新题目统计数据。problemId: {}, isAccepted: {}\", problemId, isAccepted);\n        problemService.updateProblemStats(problemId, isAccepted);\n    }\n}\n"
  },
  {
    "path": "DOJ-BE/problem-service/src/main/java/com/decade/doj/problem/repository/ProblemRepository.java",
    "content": "package com.decade.doj.problem.repository;\n\nimport com.decade.doj.problem.domain.document.ProblemDocument;\nimport org.springframework.data.elasticsearch.repository.ElasticsearchRepository;\n\npublic interface ProblemRepository extends ElasticsearchRepository<ProblemDocument, Long> {\n}\n"
  },
  {
    "path": "DOJ-BE/problem-service/src/main/java/com/decade/doj/problem/service/IProblemService.java",
    "content": "package com.decade.doj.problem.service;\n\nimport com.decade.doj.common.domain.PageDTO;\nimport com.decade.doj.common.domain.PageQueryDTO;\nimport com.decade.doj.problem.domain.dto.ProblemPageQueryDTO;\nimport com.decade.doj.problem.domain.po.Problem;\nimport com.baomidou.mybatisplus.extension.service.IService;\n\n/**\n * <p>\n *  服务类\n * </p>\n *\n * @author \n * @since 2024-09-17\n */\npublic interface IProblemService extends IService<Problem> {\n\n    PageDTO<Problem> pageQuery(ProblemPageQueryDTO problemPageQueryDTO);\n\n    void updateProblemStats(Long problemId, boolean isAccepted);\n\n    int syncAllToElasticsearch();\n\n    int reindexAll();\n\n    void resetProblems();\n}\n"
  },
  {
    "path": "DOJ-BE/problem-service/src/main/java/com/decade/doj/problem/service/impl/ProblemServiceImpl.java",
    "content": "package com.decade.doj.problem.service.impl;\n\nimport com.baomidou.mybatisplus.extension.plugins.pagination.Page;\nimport com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;\nimport com.decade.doj.common.domain.PageDTO;\nimport com.decade.doj.problem.domain.document.ProblemDocument;\nimport com.decade.doj.common.utils.UserContext;\nimport com.decade.doj.problem.domain.dto.ProblemPageQueryDTO;\nimport com.decade.doj.problem.domain.dto.ProblemTagView;\nimport com.decade.doj.problem.domain.po.Problem;\nimport com.decade.doj.problem.domain.po.ProblemTag;\nimport com.decade.doj.problem.domain.po.Tag;\nimport com.decade.doj.problem.mapper.ProblemMapper;\nimport com.decade.doj.problem.mapper.ProblemTagMapper;\nimport com.decade.doj.problem.mapper.TagMapper;\nimport com.decade.doj.problem.repository.ProblemRepository;\nimport com.decade.doj.problem.service.IProblemService;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.amqp.rabbit.annotation.RabbitListener;\nimport org.springframework.amqp.rabbit.core.RabbitTemplate;\nimport org.springframework.beans.BeanUtils;\nimport org.springframework.beans.factory.annotation.Qualifier;\nimport org.springframework.cache.Cache;\nimport org.springframework.cache.CacheManager;\nimport org.springframework.data.domain.PageRequest;\nimport org.springframework.data.elasticsearch.core.ElasticsearchRestTemplate;\nimport org.springframework.data.elasticsearch.core.IndexOperations;\nimport org.springframework.data.elasticsearch.core.SearchHits;\nimport org.springframework.data.elasticsearch.core.query.NativeSearchQuery;\nimport org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder;\nimport org.springframework.stereotype.Service;\nimport org.springframework.util.StringUtils;\nimport org.elasticsearch.common.unit.Fuzziness;\nimport org.elasticsearch.index.query.BoolQueryBuilder;\nimport org.elasticsearch.index.query.QueryBuilders;\n\nimport java.io.Serializable;\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.LinkedHashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.stream.Collectors;\n\n@Service\n@Slf4j\npublic class ProblemServiceImpl extends ServiceImpl<ProblemMapper, Problem> implements IProblemService {\n\n    private static final int MAX_ES_RESULTS = 10000;\n\n    private final CacheManager redisCacheManager;\n    private final CacheManager caffeineCacheManager;\n    private final RabbitTemplate rabbitTemplate;\n    private final ProblemRepository problemRepository;\n    private final ElasticsearchRestTemplate elasticsearchTemplate;\n    private final TagMapper tagMapper;\n    private final ProblemTagMapper problemTagMapper;\n\n    public ProblemServiceImpl(@Qualifier(\"redisCacheManager\") CacheManager redisCacheManager,\n                              @Qualifier(\"caffeineCacheManager\") CacheManager caffeineCacheManager,\n                              RabbitTemplate rabbitTemplate,\n                              ProblemRepository problemRepository,\n                              ElasticsearchRestTemplate elasticsearchTemplate,\n                              TagMapper tagMapper,\n                              ProblemTagMapper problemTagMapper) {\n        this.redisCacheManager = redisCacheManager;\n        this.caffeineCacheManager = caffeineCacheManager;\n        this.rabbitTemplate = rabbitTemplate;\n        this.problemRepository = problemRepository;\n        this.elasticsearchTemplate = elasticsearchTemplate;\n        this.tagMapper = tagMapper;\n        this.problemTagMapper = problemTagMapper;\n    }\n\n    @Override\n    public Problem getById(Serializable id) {\n        // 1. 查 L1 本地缓存\n        Cache caffeineCache = caffeineCacheManager.getCache(\"problem\");\n        if (caffeineCache != null && caffeineCache.get(id) != null) {\n            log.info(\"L1 缓存命中, id: {}\", id);\n            return caffeineCache.get(id, Problem.class);\n        }\n\n        // 2. 查 L2 Redis 缓存\n        Cache redisCache = redisCacheManager.getCache(\"problem\");\n        if (redisCache != null && redisCache.get(id) != null) {\n            log.info(\"L2 缓存命中, id: {}\", id);\n            Problem problem = redisCache.get(id, Problem.class);\n            // 将数据写回 L1\n            if (caffeineCache != null && problem != null) {\n                caffeineCache.put(id, problem);\n            }\n            return problem;\n        }\n\n        // 3. 查 L3 数据库\n        log.info(\"缓存未命中，从数据库查询, id: {}\", id);\n        Problem problem = super.getById(id);\n        if (problem != null) {\n            attachTags(List.of(problem));\n        }\n\n        // 4. 依次写回 L2 和 L1\n        if (problem != null) {\n            if (redisCache != null) {\n                redisCache.put(id, problem);\n            }\n            if (caffeineCache != null) {\n                caffeineCache.put(id, problem);\n            }\n        }\n        return problem;\n    }\n\n    @Override\n    public boolean save(Problem entity) {\n        // 1. 保存到数据库\n        boolean success = super.save(entity);\n        if (success) {\n            // 2. 处理标签\n            syncTags(entity.getId(), entity.getTags());\n            // 3. 同步到 Elasticsearch\n            syncToElasticsearch(entity);\n        }\n        return success;\n    }\n\n    @Override\n    public boolean updateById(Problem entity) {\n        // 1. 更新数据库\n        boolean success = super.updateById(entity);\n        if (success) {\n            // 2. 同步标签\n            syncTags(entity.getId(), entity.getTags());\n            // 3. 清理缓存\n            clearCache(entity.getId());\n            // 4. 同步到 Elasticsearch\n            syncToElasticsearch(entity);\n        }\n        return success;\n    }\n\n    @Override\n    public boolean removeById(Serializable id) {\n        // 1. 删除数据库\n        boolean success = super.removeById(id);\n        if (success) {\n            // 2. 删除标签关联\n            problemTagMapper.delete(new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<ProblemTag>()\n                    .eq(ProblemTag::getProblemId, id));\n            // 3. 清理缓存\n            clearCache(id);\n            // 4. 从 Elasticsearch 删除\n            deleteFromElasticsearch((Long) id);\n        }\n        return success;\n    }\n\n    /**\n     * 同步标签到数据库\n     */\n    private void syncTags(Long problemId, List<String> tags) {\n        // 1. 先删除原有关联\n        problemTagMapper.delete(new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<ProblemTag>()\n                .eq(ProblemTag::getProblemId, problemId));\n\n        if (tags == null || tags.isEmpty()) {\n            return;\n        }\n\n        Set<String> normalizedTags = new LinkedHashSet<>();\n        for (String tagName : tags) {\n            if (!StringUtils.hasText(tagName)) {\n                continue;\n            }\n            normalizedTags.add(tagName.trim());\n        }\n\n        // 2. 处理每个标签\n        for (String tagName : normalizedTags) {\n            // 查找标签，不存在则创建\n            Tag tag = tagMapper.selectOne(new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<Tag>()\n                    .eq(Tag::getName, tagName));\n            if (tag == null) {\n                tag = new Tag().setName(tagName);\n                tagMapper.insert(tag);\n            }\n            // 创建关联\n            problemTagMapper.insert(new ProblemTag(problemId, tag.getId()));\n        }\n    }\n\n    /**\n     * 同步数据到 Elasticsearch\n     */\n    private void syncToElasticsearch(Problem problem) {\n        try {\n            ProblemDocument document = new ProblemDocument();\n            BeanUtils.copyProperties(problem, document);\n            problemRepository.save(document);\n            log.info(\"同步到 ES 成功, id: {}\", problem.getId());\n        } catch (Exception e) {\n            log.error(\"同步到 ES 失败, id: {}, error: {}\", problem.getId(), e.getMessage(), e);\n        }\n    }\n\n    /**\n     * 从 Elasticsearch 删除\n     */\n    private void deleteFromElasticsearch(Long id) {\n        try {\n            problemRepository.deleteById(id);\n            log.info(\"从 ES 删除成功, id: {}\", id);\n        } catch (Exception e) {\n            log.error(\"从 ES 删除失败, id: {}, error: {}\", id, e.getMessage(), e);\n        }\n    }\n\n    private void clearCache(Serializable id) {\n        // 1. 删除 L2 Redis 缓存\n        Cache redisCache = redisCacheManager.getCache(\"problem\");\n        if (redisCache != null) {\n            redisCache.evict(id);\n            log.info(\"L2 缓存已清除, id: {}\", id);\n        }\n        // 2. 发送 MQ 消息，通知其他实例删除 L1 缓存\n        rabbitTemplate.convertAndSend(\"cache.update.exchange\", \"\", id.toString());\n        log.info(\"发送缓存更新广播, id: {}\", id);\n    }\n\n    // 监听 MQ 消息，删除本地 L1 缓存\n    @RabbitListener(queues = \"#{@problemCacheUpdateQueue.name}\")\n    public void onCacheUpdate(String id) {\n        Cache caffeineCache = caffeineCacheManager.getCache(\"problem\");\n        if (caffeineCache != null) {\n            caffeineCache.evict(Long.valueOf(id));\n            log.info(\"收到广播，L1 缓存已清除, id: {}\", id);\n        }\n    }\n\n    @Override\n    public PageDTO<Problem> pageQuery(ProblemPageQueryDTO problemPageQueryDTO) {\n        log.info(\"分页查询题目列表: {}\", problemPageQueryDTO);\n\n        List<Long> ids = null;\n        if (needElasticsearchSearch(problemPageQueryDTO)) {\n            ids = searchIdsByElasticsearch(problemPageQueryDTO);\n            if (ids.isEmpty()) {\n                long esCount = safeEsCount();\n                long dbCount = super.count();\n                if (shouldResyncEs(esCount, dbCount)) {\n                    syncAllToElasticsearch();\n                    ids = searchIdsByElasticsearch(problemPageQueryDTO);\n                }\n            }\n            if (ids.isEmpty()) {\n                return PageDTO.empty(0L, 0L);\n            }\n        }\n\n        List<String> tagNames = normalizeTags(problemPageQueryDTO.getTags());\n\n        Page<Problem> page = (Page<Problem>) baseMapper.selectPageWithFilters(\n                problemPageQueryDTO.toMpPage(\"id\", true),\n                ids,\n                problemPageQueryDTO.getDifficulty(),\n                tagNames,\n                problemPageQueryDTO.getStatus(),\n                UserContext.getCurrentUser()\n        );\n        List<Problem> records = page.getRecords();\n        attachTags(records);\n        return PageDTO.fullPage(page.getTotal(), page.getPages(), records);\n    }\n\n    /**\n     * 判断是否需要使用 ES 搜索\n     * 当有文本搜索需求时使用 ES,简单的精确查询使用 MySQL\n     */\n    private boolean needElasticsearchSearch(ProblemPageQueryDTO dto) {\n        return StringUtils.hasText(dto.getName()) || StringUtils.hasText(dto.getDescription());\n    }\n\n    /**\n     * 使用 Elasticsearch 搜索\n     */\n    private List<Long> searchIdsByElasticsearch(ProblemPageQueryDTO dto) {\n        log.info(\"使用 ES 搜索并结合 MySQL 过滤\");\n\n        BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();\n        boolean hasQuery = false;\n        if (StringUtils.hasText(dto.getName())) {\n            boolQuery.should(QueryBuilders.matchQuery(\"name\", dto.getName()).fuzziness(Fuzziness.AUTO));\n            boolQuery.should(QueryBuilders.matchQuery(\"description\", dto.getName()).fuzziness(Fuzziness.AUTO));\n            hasQuery = true;\n        }\n        if (StringUtils.hasText(dto.getDescription())) {\n            boolQuery.should(QueryBuilders.matchQuery(\"description\", dto.getDescription()).fuzziness(Fuzziness.AUTO));\n            hasQuery = true;\n        }\n\n        if (!hasQuery) {\n            return List.of();\n        }\n\n        boolQuery.minimumShouldMatch(1);\n        NativeSearchQuery query = new NativeSearchQueryBuilder()\n                .withQuery(boolQuery)\n                .withPageable(PageRequest.of(0, MAX_ES_RESULTS))\n                .build();\n        SearchHits<ProblemDocument> searchHits = elasticsearchTemplate.search(query, ProblemDocument.class);\n        return searchHits.getSearchHits().stream()\n                .map(hit -> hit.getContent().getId())\n                .collect(Collectors.toList());\n    }\n\n    static List<String> normalizeTags(List<String> tags) {\n        if (tags == null || tags.isEmpty()) {\n            return List.of();\n        }\n        Set<String> normalized = new LinkedHashSet<>();\n        for (String tag : tags) {\n            if (!StringUtils.hasText(tag)) {\n                continue;\n            }\n            normalized.add(tag.trim());\n        }\n        return new ArrayList<>(normalized);\n    }\n\n    static boolean shouldResyncEs(long esCount, long dbCount) {\n        return esCount == 0 && dbCount > 0;\n    }\n\n    private long safeEsCount() {\n        try {\n            return problemRepository.count();\n        } catch (Exception e) {\n            log.warn(\"查询 ES count 失败，尝试重新创建索引\", e);\n            recreateIndex();\n            return 0L;\n        }\n    }\n\n    private void attachTags(List<Problem> problems) {\n        if (problems == null || problems.isEmpty()) {\n            return;\n        }\n\n        List<Long> problemIds = problems.stream()\n                .map(Problem::getId)\n                .filter(id -> id != null)\n                .collect(Collectors.toList());\n        if (problemIds.isEmpty()) {\n            return;\n        }\n\n        List<ProblemTagView> tagViews = problemTagMapper.selectTagsByProblemIds(problemIds);\n        Map<Long, List<String>> tagMap = new HashMap<>();\n        for (ProblemTagView view : tagViews) {\n            tagMap.computeIfAbsent(view.getProblemId(), key -> new ArrayList<>()).add(view.getTagName());\n        }\n\n        for (Problem problem : problems) {\n            problem.setTags(tagMap.getOrDefault(problem.getId(), List.of()));\n        }\n    }\n\n    @Override\n    public void updateProblemStats(Long problemId, boolean isAccepted) {\n        lambdaUpdate()\n                .eq(Problem::getId, problemId)\n                .setSql(\"total_attempt = total_attempt + 1\")\n                .setSql(isAccepted, \"total_pass = total_pass + 1\")\n                .update();\n\n        // 更新后同步到 ES\n        Problem problem = super.getById(problemId);\n        if (problem != null) {\n            syncToElasticsearch(problem);\n        }\n    }\n\n    /**\n     * 全量同步数据到 ES (用于初始化或数据修复)\n     */\n    @Override\n    public int syncAllToElasticsearch() {\n        log.info(\"开始全量同步数据到 ES\");\n        List<Problem> allProblems = super.list();\n\n        List<ProblemDocument> documents = allProblems.stream()\n                .map(problem -> new ProblemDocument(problem.getId(), problem.getName(), problem.getDescription()))\n                .collect(Collectors.toList());\n        log.info(\"全量同步完成,共同步 {} 条数据\", documents.size());\n        problemRepository.saveAll(documents);\n        return documents.size();\n    }\n\n    @Override\n    public int reindexAll() {\n        recreateIndex();\n        return syncAllToElasticsearch();\n    }\n\n    @Override\n    public void resetProblems() {\n        log.info(\"开始清空 problem 模块数据...\");\n        problemTagMapper.delete(new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<>());\n        tagMapper.delete(new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<>());\n        super.remove(new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<>());\n        recreateIndex();\n        log.info(\"problem 数据与 ES 索引已清空\");\n    }\n\n    private void recreateIndex() {\n        IndexOperations indexOps = elasticsearchTemplate.indexOps(ProblemDocument.class);\n        if (indexOps.exists()) {\n            indexOps.delete();\n        }\n        indexOps.create();\n        indexOps.putMapping(indexOps.createMapping());\n    }\n}\n"
  },
  {
    "path": "DOJ-BE/problem-service/src/main/resources/application.yaml",
    "content": "server:\n    port: ${doj.port.problem-service}\n\nmanagement:\n    endpoints:\n        web:\n            exposure:\n                include: health,prometheus\n    metrics:\n        export:\n            prometheus:\n                enabled: true\n\ndoj:\n    db:\n        host: ${DOJ_DB_HOST:127.0.0.1}\n        name: doj_problem\n        user: ${DOJ_DB_USER:root}\n        pwd: ${DOJ_DB_PWD:123}\n    mq:\n        host: ${DOJ_MQ_HOST:127.0.0.1}\n    redis:\n        host: ${DOJ_REDIS_HOST:127.0.0.1}\n    swagger:\n        title: 题目接口文档\n        scan: com.decade.doj.problem.controller\n\nspring:\n  elasticsearch:\n    uris: ${ELASTICSEARCH_URIS:http://127.0.0.1:9200}"
  },
  {
    "path": "DOJ-BE/problem-service/src/main/resources/bootstrap.yaml",
    "content": "spring:\n    application:\n        name: problem-service\n    profiles:\n        active: dev, common\n    cloud:\n        nacos:\n            server-addr: ${NACOS_SERVER_ADDR:127.0.0.1:8848}\n            config:\n                file-extension: yaml\n                shared-configs:\n                    - dataId: shared-jdbc.yaml\n                    - dataId: shared-swagger.yaml\n                    - dataId: shared-jwt.yaml\n                    - dataId: shared-rabbitmq.yaml\n\n# shared-jdbc.yaml\n#spring:\n#    datasource:\n#        driver-class-name: com.mysql.cj.jdbc.Driver\n#        url: jdbc:mysql://${doj.db.host:192.168.74.131}:3306/${doj.db.name}\n#        username: ${doj.db.user:root}\n#        password: ${doj.db.pwd:123}\n#        hikari:\n#            auto-commit: true\n#            maximum-pool-size: 10\n#            minimum-idle: 10\n#            connection-test-query: select 1\n#            connection-timeout: 20000\n#mybatis-plus:\n#    global-config:\n#        db-config:\n#            update-strategy: not_null\n#            id-type: auto\n\n# shared-swagger.yaml\n#logging:\n#    level:\n#        com.decade: debug\n#    pattern:\n#        dateformat: HH:mm:ss:SSS\n#knife4j:\n#    enable: true\n#    openapi:\n#        title: ${doj.swagger.title}\n#        description: \"Duck Online Judge API\"\n#        email: \"decade-qzj@foxmail.com\"\n#        version: v1.0.0\n#        group:\n#            default:\n#                group-name: default\n#                api-rule: package\n#                api-rule-resources:\n#                    - ${doj.swagger.scan}\n\n# shared-jwt.yaml\n#doj:\n#    jwt:\n#        location: classpath:doj.jks\n#        alias: decade\n#        password: doj123\n#        tokenTTL: 30m\n#        authorization: \"authorization\"\n#        secret-key: \"uid\""
  },
  {
    "path": "DOJ-BE/problem-service/src/main/resources/es/problem-settings.json",
    "content": "{\n  \"analysis\": {\n    \"analyzer\": {\n      \"ik_max_lowercase\": {\n        \"tokenizer\": \"ik_max_word\",\n        \"filter\": [\"lowercase\"]\n      },\n      \"ik_smart_lowercase\": {\n        \"tokenizer\": \"ik_smart\",\n        \"filter\": [\"lowercase\"]\n      }\n    },\n    \"normalizer\": {\n      \"lowercase_normalizer\": {\n        \"type\": \"custom\",\n        \"filter\": [\"lowercase\"]\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "DOJ-BE/problem-service/src/main/resources/mapper/ProblemMapper.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE mapper PUBLIC \"-//mybatis.org//DTD Mapper 3.0//EN\" \"http://mybatis.org/dtd/mybatis-3-mapper.dtd\">\n<mapper namespace=\"com.decade.doj.problem.mapper.ProblemMapper\">\n\n    <resultMap id=\"ProblemResultMap\" type=\"com.decade.doj.problem.domain.po.Problem\">\n        <id column=\"id\" property=\"id\" />\n        <result column=\"name\" property=\"name\" />\n        <result column=\"description\" property=\"description\" />\n        <result column=\"input_style\" property=\"inputStyle\" />\n        <result column=\"output_style\" property=\"outputStyle\" />\n        <result column=\"hint\" property=\"hint\" />\n        <result column=\"input_sample\" property=\"inputSample\" typeHandler=\"com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler\" />\n        <result column=\"output_sample\" property=\"outputSample\" typeHandler=\"com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler\" />\n        <result column=\"difficulty\" property=\"difficulty\" />\n        <result column=\"time_limit\" property=\"timeLimit\" />\n        <result column=\"memory_limit\" property=\"memoryLimit\" />\n        <result column=\"total_pass\" property=\"totalPass\" />\n        <result column=\"total_attempt\" property=\"totalAttempt\" />\n        <result column=\"test_data\" property=\"testData\" />\n        <result column=\"test_ans\" property=\"testAns\" />\n        <result column=\"status\" property=\"status\" />\n    </resultMap>\n\n    <select id=\"selectPageWithFilters\" resultMap=\"ProblemResultMap\">\n        SELECT\n            p.*,\n            CASE\n                WHEN EXISTS (\n                    SELECT 1 FROM doj_submission.submission s\n                    WHERE s.problem_id = p.id\n                      AND s.user_id = #{userId}\n                      AND s.status = 'Accepted'\n                ) THEN '已解决'\n                WHEN EXISTS (\n                    SELECT 1 FROM doj_submission.submission s\n                    WHERE s.problem_id = p.id\n                      AND s.user_id = #{userId}\n                ) THEN '尝试中'\n                ELSE '未开始'\n            END AS status\n        FROM problem p\n        WHERE 1 = 1\n        <if test=\"ids != null and ids.size > 0\">\n            AND p.id IN\n            <foreach collection=\"ids\" item=\"id\" open=\"(\" separator=\",\" close=\")\">\n                #{id}\n            </foreach>\n        </if>\n        <if test=\"difficulty != null and difficulty != ''\">\n            AND p.difficulty = #{difficulty}\n        </if>\n        <if test=\"tagNames != null and tagNames.size > 0\">\n            AND EXISTS (\n                SELECT 1\n                FROM problem_tag pt\n                JOIN tag t ON pt.tag_id = t.id\n                WHERE pt.problem_id = p.id\n                  AND t.name IN\n                <foreach collection=\"tagNames\" item=\"tag\" open=\"(\" separator=\",\" close=\")\">\n                    #{tag}\n                </foreach>\n            )\n        </if>\n        <if test=\"status != null and status != ''\">\n            <choose>\n                <when test=\"status == '已解决'\">\n                    AND EXISTS (\n                        SELECT 1 FROM doj_submission.submission s\n                        WHERE s.problem_id = p.id\n                          AND s.user_id = #{userId}\n                          AND s.status = 'Accepted'\n                    )\n                </when>\n                <when test=\"status == '尝试中'\">\n                    AND EXISTS (\n                        SELECT 1 FROM doj_submission.submission s\n                        WHERE s.problem_id = p.id\n                          AND s.user_id = #{userId}\n                    )\n                    AND NOT EXISTS (\n                        SELECT 1 FROM doj_submission.submission s\n                        WHERE s.problem_id = p.id\n                          AND s.user_id = #{userId}\n                          AND s.status = 'Accepted'\n                    )\n                </when>\n                <when test=\"status == '未开始'\">\n                    AND NOT EXISTS (\n                        SELECT 1 FROM doj_submission.submission s\n                        WHERE s.problem_id = p.id\n                          AND s.user_id = #{userId}\n                    )\n                </when>\n                <otherwise>\n                    AND 1 = 1\n                </otherwise>\n            </choose>\n        </if>\n        ORDER BY p.id ASC\n    </select>\n\n</mapper>\n"
  },
  {
    "path": "DOJ-BE/problem-service/src/main/resources/sql/problem_rebuild.sql",
    "content": "SET NAMES utf8mb4;\n\nDROP TABLE IF EXISTS problem_tag;\nDROP TABLE IF EXISTS tag;\nDROP TABLE IF EXISTS problem;\n\nCREATE TABLE problem (\n  id BIGINT AUTO_INCREMENT PRIMARY KEY,\n  name VARCHAR(128) NOT NULL,\n  description MEDIUMTEXT NOT NULL,\n  input_style TEXT NOT NULL,\n  output_style TEXT NOT NULL,\n  input_sample JSON NOT NULL,\n  output_sample JSON NOT NULL,\n  difficulty VARCHAR(10) NOT NULL,\n  time_limit INT NOT NULL,\n  memory_limit INT NOT NULL,\n  hint TEXT,\n  total_pass INT NOT NULL DEFAULT 0,\n  total_attempt INT NOT NULL DEFAULT 0,\n  test_data LONGTEXT NOT NULL,\n  test_ans LONGTEXT NOT NULL,\n  INDEX idx_problem_difficulty (difficulty)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;\n\nCREATE TABLE tag (\n  id BIGINT AUTO_INCREMENT PRIMARY KEY,\n  name VARCHAR(64) NOT NULL UNIQUE\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;\n\nCREATE TABLE problem_tag (\n  problem_id BIGINT NOT NULL,\n  tag_id BIGINT NOT NULL,\n  PRIMARY KEY (problem_id, tag_id),\n  INDEX idx_tag_id (tag_id)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;\n\nINSERT INTO tag (id, name) VALUES\n(1, '入门'),\n(2, '模拟'),\n(3, '动态规划'),\n(4, '数组'),\n(5, '栈'),\n(6, '字符串'),\n(7, '矩阵'),\n(8, '二分'),\n(9, '有序'),\n(10, '排序'),\n(11, '区间'),\n(12, '网格'),\n(13, '图论'),\n(14, '拓扑排序'),\n(15, '最小生成树'),\n(16, '最短路径'),\n(17, '数学'),\n(18, '基础'),\n(19, '双指针'),\n(20, '分治'),\n(21, '归并排序'),\n(22, '前缀和'),\n(23, 'DFS'),\n(24, 'BFS'),\n(25, '数论');\n\nINSERT INTO problem (\n  id, name, description, input_style, output_style, input_sample, output_sample,\n  difficulty, time_limit, memory_limit, hint, total_pass, total_attempt, test_data, test_ans\n) VALUES\n(1, 'A+B Problem',\n '给定两个整数 A 和 B，输出它们的和。\\n\\n**任务**\\n- 读取两个整数并计算 A+B。\\n- 结果可能为负数。',\n '- 一行包含两个整数 A 和 B，用空格分隔。',\n '- 输出一个整数，即 A+B。',\n '[\"1 2\"]',\n '[\"3\"]',\n '简单', 1000, 256,\n '- 范围：-1e9 <= A,B <= 1e9\\n- 使用 64 位整数存储结果',\n 0, 0, '1 2', '3'),\n\n(2, '最大子数组和',\n '给定长度为 n 的整数数组，找到和最大的连续子数组，输出该最大和。\\n\\n**说明**\\n- 子数组必须连续。\\n- 至少选择一个元素。',\n '- 第一行一个整数 n。\\n- 第二行 n 个整数，表示数组元素。',\n '- 输出最大子数组和。',\n '[\"9\\\\n-2 1 -3 4 -1 2 1 -5 4\"]',\n '[\"6\"]',\n '中等', 1000, 256,\n '- 1 <= n <= 200000\\n- 元素范围 -1e9 到 1e9\\n- 可使用 Kadane 线性算法',\n 0, 0, '9\\n-2 1 -3 4 -1 2 1 -5 4', '6'),\n\n(3, '有效括号',\n '给定仅由 ()[]{} 组成的字符串，判断括号是否成对且顺序正确。\\n\\n**有效条件**\\n- 左括号必须由同类型右括号闭合。\\n- 括号必须按正确顺序闭合。',\n '- 一行字符串 s。',\n '- 若有效输出 true，否则输出 false。',\n '[\"([])\"]',\n '[\"true\"]',\n '简单', 1000, 256,\n '- 1 <= |s| <= 100000\\n- 使用栈进行匹配',\n 0, 0, '([])', 'true'),\n\n(4, '矩阵转置',\n '给定一个 m x n 矩阵，输出其转置矩阵（n x m）。\\n\\n转置后，第 i 行第 j 列元素变为原矩阵的第 j 行第 i 列。',\n '- 第一行两个整数 m n。\\n- 接下来 m 行，每行 n 个整数。',\n '- 输出 n 行，每行 m 个整数，为转置后的矩阵。',\n '[\"2 3\\\\n1 2 3\\\\n4 5 6\"]',\n '[\"1 4\\\\n2 5\\\\n3 6\"]',\n '简单', 1000, 256,\n '- 1 <= m,n <= 500\\n- 注意空格与换行格式',\n 0, 0, '2 3\\n1 2 3\\n4 5 6', '1 4\\n2 5\\n3 6'),\n\n(5, '二分查找',\n '在一个升序数组中查找目标值 target。\\n\\n若找到，输出其 0-based 索引；否则输出 -1。',\n '- 第一行两个整数 n 和 target。\\n- 第二行 n 个整数，按升序排列。',\n '- 输出目标值的索引或 -1。',\n '[\"5 7\\\\n1 3 5 7 9\"]',\n '[\"3\"]',\n '简单', 1000, 256,\n '- 1 <= n <= 1000000\\n- 数组已升序\\n- 使用二分查找 O(log n)',\n 0, 0, '5 7\\n1 3 5 7 9', '3'),\n\n(6, '合并区间',\n '给定 n 个闭区间 [l, r]，将所有重叠或相邻的区间合并，并按 l 升序输出。',\n '- 第一行一个整数 n。\\n- 接下来 n 行，每行两个整数 l r。',\n '- 输出合并后的区间，每行一个 l r。',\n '[\"4\\\\n1 3\\\\n2 6\\\\n8 10\\\\n15 18\"]',\n '[\"1 6\\\\n8 10\\\\n15 18\"]',\n '中等', 1000, 256,\n '- 1 <= n <= 200000\\n- 先按 l 排序\\n- 若前一个区间右端点 >= 下一个区间左端点，则合并',\n 0, 0, '4\\n1 3\\n2 6\\n8 10\\n15 18', '1 6\\n8 10\\n15 18'),\n\n(7, '最小路径和',\n '给定一个 m x n 的非负整数网格，从左上角到右下角，只能向右或向下移动，求路径上的最小和。',\n '- 第一行 m n。\\n- 接下来 m 行，每行 n 个非负整数。',\n '- 输出最小路径和。',\n '[\"3 3\\\\n1 3 1\\\\n1 5 1\\\\n4 2 1\"]',\n '[\"7\"]',\n '中等', 1000, 256,\n '- 1 <= m,n <= 200\\n- 0 <= grid[i][j] <= 100\\n- 使用动态规划',\n 0, 0, '3 3\\n1 3 1\\n1 5 1\\n4 2 1', '7'),\n\n(8, '编辑距离',\n '给定两个字符串 s 和 t，你可以对 s 执行插入、删除、替换操作，使其变为 t。\\n\\n求最少操作数。',\n '- 第一行字符串 s。\\n- 第二行字符串 t。',\n '- 输出最小编辑距离。',\n '[\"kitten\\\\nsitting\"]',\n '[\"3\"]',\n '困难', 1000, 256,\n '- 1 <= |s|, |t| <= 2000\\n- dp[i][j] 表示前 i 和前 j 的最小操作数',\n 0, 0, 'kitten\\nsitting', '3'),\n\n(9, '拓扑排序',\n '给定一个有向无环图 (DAG)，输出**字典序最小**的拓扑序列。\\n\\n如果多个节点入度为 0，优先选择编号更小的节点。',\n '- 第一行 n m，表示节点数和边数（节点编号从 1 到 n）。\\n- 接下来 m 行，每行一条有向边 u v。',\n '- 输出 n 个整数，为字典序最小的拓扑序。',\n '[\"4 3\\\\n1 2\\\\n2 3\\\\n3 4\"]',\n '[\"1 2 3 4\"]',\n '中等', 1000, 256,\n '- 1 <= n <= 200000, 1 <= m <= 300000\\n- 使用小根堆维护入度为 0 的节点',\n 0, 0, '4 3\\n1 2\\n2 3\\n3 4', '1 2 3 4'),\n\n(10, '最小生成树',\n '给定一个连通无向带权图，求其最小生成树的权值之和。',\n '- 第一行 n m。\\n- 接下来 m 行，每行 u v w，表示边及权值。',\n '- 输出最小生成树权值之和。',\n '[\"4 5\\\\n1 2 1\\\\n1 3 4\\\\n2 3 2\\\\n2 4 5\\\\n3 4 3\"]',\n '[\"6\"]',\n '中等', 1000, 256,\n '- 1 <= n <= 200000, 1 <= m <= 300000\\n- 使用 Kruskal 或 Prim',\n 0, 0, '4 5\\n1 2 1\\n1 3 4\\n2 3 2\\n2 4 5\\n3 4 3', '6'),\n\n(11, '最短路径',\n '给定一个正权图，求从 s 到 t 的最短路径长度。\\n\\n若不可达，输出 -1。',\n '- 第一行 n m s t。\\n- 接下来 m 行，每行 u v w，表示从 u 到 v 的权值为 w 的边。',\n '- 输出最短距离，若不可达输出 -1。',\n '[\"5 6 1 5\\\\n1 2 2\\\\n1 3 4\\\\n2 3 1\\\\n2 4 7\\\\n3 5 3\\\\n4 5 1\"]',\n '[\"6\"]',\n '中等', 1000, 256,\n '- 1 <= n <= 200000, 1 <= m <= 300000\\n- 权值均为正\\n- 使用 Dijkstra',\n 0, 0, '5 6 1 5\\n1 2 2\\n1 3 4\\n2 3 1\\n2 4 7\\n3 5 3\\n4 5 1', '6'),\n\n(12, '最大公约数',\n '给定两个非负整数 a 和 b，输出它们的最大公约数。',\n '- 一行两个整数 a 和 b。',\n '- 输出 gcd(a,b)。',\n '[\"48 18\"]',\n '[\"6\"]',\n '简单', 1000, 256,\n '- 1 <= a,b <= 1e18\\n- 使用欧几里得算法',\n 0, 0, '48 18', '6'),\n\n(13, '判断回文',\n '判断给定字符串是否为回文串。\\n\\n回文串从左到右和从右到左完全一致。',\n '- 一行字符串 s（仅包含字母或数字，无空格）。',\n '- 若为回文输出 true，否则输出 false。',\n '[\"level\"]',\n '[\"true\"]',\n '简单', 1000, 256,\n '- 1 <= |s| <= 1000000\\n- 双指针从两端向中间',\n 0, 0, 'level', 'true'),\n\n(14, '数组旋转',\n '给定数组，将其向右旋转 k 步。\\n\\n例如 [1,2,3,4,5] 向右旋转 2 步得到 [4,5,1,2,3]。',\n '- 第一行 n k。\\n- 第二行 n 个整数。',\n '- 输出旋转后的数组。',\n '[\"5 2\\\\n1 2 3 4 5\"]',\n '[\"4 5 1 2 3\"]',\n '简单', 1000, 256,\n '- 1 <= n <= 200000\\n- k 可能很大，先取 k % n',\n 0, 0, '5 2\\n1 2 3 4 5', '4 5 1 2 3'),\n\n(15, '逆序对计数',\n '给定一个数组，统计其中逆序对的数量。\\n\\n若 i < j 且 a[i] > a[j]，则 (i,j) 构成一对逆序对。',\n '- 第一行 n。\\n- 第二行 n 个整数。',\n '- 输出逆序对数量。',\n '[\"5\\\\n2 3 8 6 1\"]',\n '[\"5\"]',\n '困难', 1000, 256,\n '- 1 <= n <= 200000\\n- 使用归并排序或树状数组',\n 0, 0, '5\\n2 3 8 6 1', '5'),\n\n(16, '数位和',\n '给定一个非负整数 N，计算其十进制表示中所有数字之和。',\n '- 一行一个整数 N。',\n '- 输出数字之和。',\n '[\"12345\"]',\n '[\"15\"]',\n '简单', 1000, 256,\n '- 0 <= N <= 1e18\\n- 逐位取模累加',\n 0, 0, '12345', '15'),\n\n(17, '区间和查询',\n '给定长度为 n 的数组，回答 q 次区间求和查询。\\n\\n每个查询给出 l r（1-based），输出 a[l..r] 的和。',\n '- 第一行 n q。\\n- 第二行 n 个整数。\\n- 接下来 q 行，每行 l r。',\n '- 对每个查询输出一行区间和。',\n '[\"5 3\\\\n1 2 3 4 5\\\\n1 3\\\\n2 5\\\\n4 4\"]',\n '[\"6\\\\n14\\\\n4\"]',\n '简单', 1000, 256,\n '- 1 <= n,q <= 200000\\n- 使用前缀和',\n 0, 0, '5 3\\n1 2 3 4 5\\n1 3\\n2 5\\n4 4', '6\\n14\\n4'),\n\n(18, '岛屿数量',\n '给定由 0 和 1 组成的网格，1 表示陆地，0 表示水域。\\n\\n相邻（上下左右）陆地连成一个岛屿，求岛屿数量。',\n '- 第一行 m n。\\n- 接下来 m 行，每行 n 个数字（0 或 1）。',\n '- 输出岛屿数量。',\n '[\"3 4\\\\n1 1 0 0\\\\n0 1 0 1\\\\n1 0 0 1\"]',\n '[\"3\"]',\n '中等', 1000, 256,\n '- 1 <= m,n <= 500\\n- 使用 DFS/BFS 标记访问',\n 0, 0, '3 4\\n1 1 0 0\\n0 1 0 1\\n1 0 0 1', '3'),\n\n(19, '最长公共子序列',\n '给定两个字符串 s 和 t，求它们的最长公共子序列长度。\\n\\n子序列不要求连续，但相对顺序必须一致。',\n '- 第一行字符串 s。\\n- 第二行字符串 t。',\n '- 输出最长公共子序列长度。',\n '[\"abcde\\\\nace\"]',\n '[\"3\"]',\n '中等', 1000, 256,\n '- 1 <= |s|, |t| <= 2000\\n- 使用二维 DP',\n 0, 0, 'abcde\\nace', '3'),\n\n(20, '阶乘末尾零',\n '给定整数 N，计算 N! 末尾连续 0 的个数。\\n\\nN! 中 0 的数量取决于因子 2 和 5 的数量，且 2 的数量充足。',\n '- 一行一个整数 N。',\n '- 输出末尾 0 的个数。',\n '[\"25\"]',\n '[\"6\"]',\n '中等', 1000, 256,\n '- 0 <= N <= 1e18\\n- 统计 N/5 + N/25 + N/125 + ...',\n 0, 0, '25', '6');\n\nINSERT INTO problem_tag (problem_id, tag_id) VALUES\n(1, 1), (1, 2),\n(2, 3), (2, 4),\n(3, 5), (3, 6),\n(4, 7), (4, 2),\n(5, 8), (5, 4), (5, 9),\n(6, 10), (6, 11),\n(7, 3), (7, 12),\n(8, 3), (8, 6),\n(9, 13), (9, 14),\n(10, 13), (10, 15),\n(11, 13), (11, 16),\n(12, 17), (12, 18),\n(13, 6), (13, 19),\n(14, 4), (14, 2),\n(15, 20), (15, 21),\n(16, 17), (16, 18),\n(17, 22), (17, 4),\n(18, 23), (18, 24), (18, 13),\n(19, 3), (19, 6),\n(20, 17), (20, 25);\n"
  },
  {
    "path": "DOJ-BE/problem-service/src/test/java/com/decade/doj/problem/TestProblem.java",
    "content": "package com.decade.doj.problem;\n\nimport com.decade.doj.problem.domain.po.Problem;\nimport com.decade.doj.problem.service.IProblemService;\nimport com.decade.doj.problem.service.impl.ProblemServiceImpl;\nimport lombok.RequiredArgsConstructor;\nimport org.junit.jupiter.api.Test;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.boot.test.context.SpringBootTest;\n\n@SpringBootTest\npublic class TestProblem {\n\n    @Autowired\n    private IProblemService problemService;\n\n    @Test\n    public void test() {\n        Problem problem = problemService.getById(1);\n        System.out.println(problem);\n        System.out.println(\"fin\");\n    }\n\n}\n"
  },
  {
    "path": "DOJ-BE/sandbox-service/Dockerfile",
    "content": "# Stage 1: Build the application\nFROM maven:3.8.4-openjdk-17 AS build\n\nWORKDIR /app\n\n# Copy all pom.xml files first to leverage Docker cache\nCOPY DOJ-BE/pom.xml .\nCOPY DOJ-BE/common/pom.xml ./common/\nCOPY DOJ-BE/gateway-service/pom.xml ./gateway-service/\nCOPY DOJ-BE/problem-service/pom.xml ./problem-service/\nCOPY DOJ-BE/sandbox-service/pom.xml ./sandbox-service/\nCOPY DOJ-BE/submission-service/pom.xml ./submission-service/\nCOPY DOJ-BE/user-service/pom.xml ./user-service/\n\n# Download all dependencies\nRUN mvn dependency:go-offline\n\n# Copy all source code\nCOPY DOJ-BE/common/src ./common/src\nCOPY DOJ-BE/sandbox-service/src ./sandbox-service/src\n\n# Build the specific service\nRUN mvn -f ./pom.xml -pl sandbox-service -am clean package -DskipTests\nRUN ls -al ./sandbox-service/target\n\n# Stage 2: Create the runtime image\nFROM eclipse-temurin:17-jre\n\n# Install Docker \nUSER root\nRUN apt-get update && apt-get install -y docker.io\n\nWORKDIR /app\n\n# Copy the executable jar from the build stage\nCOPY --from=build /app/sandbox-service/target/sandbox-service.jar ./app.jar\n\nEXPOSE 8082\n\nENTRYPOINT [\"java\", \"-jar\", \"./app.jar\"]"
  },
  {
    "path": "DOJ-BE/sandbox-service/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n         xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n    <parent>\n        <groupId>com.decade</groupId>\n        <artifactId>DOJ-BE</artifactId>\n        <version>1.0-SNAPSHOT</version>\n    </parent>\n\n    <artifactId>sandbox-service</artifactId>\n\n    <properties>\n        <maven.compiler.source>17</maven.compiler.source>\n        <maven.compiler.target>17</maven.compiler.target>\n        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>\n    </properties>\n\n    <dependencies>\n<!--        <dependency>-->\n<!--            <groupId>com.github.docker-java</groupId>-->\n<!--            <artifactId>docker-java</artifactId>-->\n<!--            <version>3.3.0</version>-->\n<!--        </dependency>-->\n<!--        <dependency>-->\n<!--            <groupId>com.github.docker-java</groupId>-->\n<!--            <artifactId>docker-java-transport-httpclient5</artifactId>-->\n<!--            <version>3.3.0</version>-->\n<!--        </dependency>-->\n        <!--common-->\n        <dependency>\n            <groupId>com.decade</groupId>\n            <artifactId>common</artifactId>\n            <version>1.0-SNAPSHOT</version>\n        </dependency>\n        <dependency>\n            <groupId>com.alibaba.cloud</groupId>\n            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>\n        </dependency>\n        <!--web-->\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-web</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.jcraft</groupId>\n            <artifactId>jsch</artifactId>\n            <version>0.1.55</version>\n        </dependency>\n    </dependencies>\n\n    <build>\n        <finalName>${project.artifactId}</finalName>\n        <plugins>\n            <plugin>\n                <groupId>org.springframework.boot</groupId>\n                <artifactId>spring-boot-maven-plugin</artifactId>\n            </plugin>\n        </plugins>\n    </build>\n\n</project>"
  },
  {
    "path": "DOJ-BE/sandbox-service/src/main/java/com/decade/doj/sandbox/SandboxApplication.java",
    "content": "package com.decade.doj.sandbox;\n\nimport com.decade.doj.common.config.custom.DefaultFeignConfig;\nimport com.decade.doj.common.config.custom.MVCConfig;\nimport com.decade.doj.common.config.custom.MybatisConfig;\nimport com.decade.doj.common.config.thread.ThreadPoolConfig;\nimport com.decade.doj.common.interceptor.AdminCheckInterceptor;\nimport com.decade.doj.common.interceptor.IdentityInterceptor;\nimport org.springframework.boot.SpringApplication;\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\nimport org.springframework.cloud.openfeign.EnableFeignClients;\nimport org.springframework.context.annotation.Import;\n\n@SpringBootApplication\n@EnableFeignClients(basePackages = \"com.decade.doj.common.client\", defaultConfiguration = DefaultFeignConfig.class)\n@Import({ThreadPoolConfig.class, MVCConfig.class, MybatisConfig.class, IdentityInterceptor.class, AdminCheckInterceptor.class})\npublic class SandboxApplication {\n    public static void main(String[] args) {\n        SpringApplication.run(SandboxApplication.class, args);\n    }\n}\n"
  },
  {
    "path": "DOJ-BE/sandbox-service/src/main/java/com/decade/doj/sandbox/config/MqConfig.java",
    "content": "package com.decade.doj.sandbox.config;\n\nimport org.springframework.amqp.core.Binding;\nimport org.springframework.amqp.core.BindingBuilder;\nimport org.springframework.amqp.core.Queue;\nimport org.springframework.amqp.core.TopicExchange;\nimport org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;\nimport org.springframework.amqp.support.converter.MessageConverter;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\n\n@Configuration\npublic class MqConfig {\n    @Bean\n    public MessageConverter jsonMessageConverter(){\n        return new Jackson2JsonMessageConverter();\n    }\n}"
  },
  {
    "path": "DOJ-BE/sandbox-service/src/main/java/com/decade/doj/sandbox/controller/SandboxController.java",
    "content": "package com.decade.doj.sandbox.controller;\n\nimport com.alibaba.fastjson.JSON;\nimport com.decade.doj.common.client.ProblemClient;\nimport com.decade.doj.common.client.SubmissionClient;\nimport com.decade.doj.common.config.properties.ResourceProperties;\nimport com.decade.doj.common.domain.R;\nimport com.decade.doj.common.domain.po.Problem;\nimport com.decade.doj.common.domain.po.Submission;\nimport com.decade.doj.common.utils.UserContext;\nimport com.decade.doj.sandbox.domain.vo.ExecuteMessage;\nimport com.decade.doj.sandbox.domain.vo.JudgingTask;\nimport com.decade.doj.sandbox.enums.LanguageEnum;\nimport com.decade.doj.sandbox.service.ISandboxService;\nimport io.swagger.v3.oas.annotations.Operation;\nimport io.swagger.v3.oas.annotations.tags.Tag;\nimport lombok.RequiredArgsConstructor;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.boot.context.properties.EnableConfigurationProperties;\nimport org.springframework.data.redis.core.StringRedisTemplate;\nimport org.springframework.web.bind.annotation.*;\nimport org.springframework.web.multipart.MultipartFile;\n\nimport javax.validation.constraints.NotBlank;\nimport java.io.IOException;\nimport java.nio.charset.StandardCharsets;\nimport java.nio.file.FileSystems;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.util.UUID;\nimport java.util.concurrent.CompletableFuture;\n\n@RestController\n@RequestMapping(\"/sandbox\")\n@Tag(name = \"沙箱相关接口\")\n@Slf4j\n@RequiredArgsConstructor\n@EnableConfigurationProperties(ResourceProperties.class)\npublic class SandboxController {\n\n    private final ResourceProperties resourceProperties;\n\n    private final ISandboxService sandboxService;\n    private final SubmissionClient submissionClient;\n    private final ProblemClient problemClient;\n    private final StringRedisTemplate redisTemplate;\n\n    @PostMapping(\"/code\")\n    @Operation(summary = \"运行代码文件(异步执行)\")\n    public CompletableFuture<R<ExecuteMessage>> runCode(@RequestParam(\"file\") MultipartFile file, @RequestParam(\"language\") @NotBlank String lang) {\n        if (file.isEmpty()) {\n            return CompletableFuture.completedFuture(R.error(\"上传的文件不能为空!\"));\n        }\n\n        if (LanguageEnum.isInValidLanguage(lang)) {\n            return CompletableFuture.completedFuture(R.error(\"不支持的编程语言: \" + lang));\n        }\n\n        try {\n            String path = saveFile(file, resourceProperties.getCodePath(), null)[0];\n            return sandboxService\n                    .runCodeInSandbox(path, file.getOriginalFilename(), lang)\n                    .thenApply(R::ok);\n        } catch (IOException e) {\n            log.error(\"保存代码文件失败\", e);\n            return CompletableFuture.completedFuture(R.error(\"文件保存失败: \" + e.getMessage()));\n        }\n    }\n\n    @PostMapping(\"/problem\")\n    @Operation(summary = \"运行题目代码(异步执行)\")\n    public CompletableFuture<R<ExecuteMessage>> runProblem(@RequestParam(\"file\") MultipartFile file, @RequestParam(\"input\") MultipartFile input, @RequestParam(\"language\") @NotBlank String lang, @RequestParam(\"pid\") Long pid) {\n        if (file.isEmpty()) {\n            return CompletableFuture.completedFuture(R.error(\"上传的文件不能为空!\"));\n        }\n\n        if (LanguageEnum.isInValidLanguage(lang)) {\n            return CompletableFuture.completedFuture(R.error(\"不支持的编程语言: \" + lang));\n        }\n\n        try {\n            String[] Paths = saveFile(file, resourceProperties.getCodePath(), null);\n            String codePath = Paths[0];\n            saveFile(input, resourceProperties.getCodePath(), Paths[1]);\n\n            return sandboxService\n                    .runCodeInSandboxWI(codePath, input.getOriginalFilename(), file.getOriginalFilename(), lang)\n                    .thenApply(R::ok);\n        } catch (IOException e) {\n            log.error(\"保存代码/输入文件失败\", e);\n            return CompletableFuture.completedFuture(R.error(\"文件保存失败: \" + e.getMessage()));\n        }\n    }\n\n    @PostMapping(\"/validate\")\n    @Operation(summary = \"验证题目代码(异步提交)\")\n    public R<Long> runProblemValidate(@RequestParam(\"file\") MultipartFile file, @RequestParam(\"language\") @NotBlank String lang, @RequestParam(\"pid\") Long pid) throws IOException {\n        if (file.isEmpty()) {\n            return R.error(400, \"上传的文件不能为空!\");\n        }\n\n        if (LanguageEnum.isInValidLanguage(lang)) {\n            return R.error(400, \"不支持的编程语言: \" + lang);\n        }\n\n        String[] Paths = saveFile(file, resourceProperties.getCodePath(), null);\n        String codePath = Paths[0];\n        String[] data = saveText2File(pid, resourceProperties.getCodePath(), Paths[1]);\n\n        // 1. 调用 submission-service 创建一个 PENDING 状态的提交记录\n        Submission submissionDTO = new Submission();\n        submissionDTO.setProblemId(pid);\n        submissionDTO.setUserId(UserContext.getCurrentUser());\n        submissionDTO.setLanguage(lang);\n        submissionDTO.setCode(Paths[2]);\n        R<Long> response = submissionClient.submit(submissionDTO);\n        if (!response.success()) {\n            return R.error(500, \"创建提交记录失败: \" + response.getMsg());\n        }\n        Long submissionId = response.getData();\n\n        // 2. 创建判题任务\n        JudgingTask task = new JudgingTask()\n            .setSubmissionId(submissionId)\n            .setLocalPath(codePath)\n            .setInput(data[0])\n            .setOutput(data[1])\n            .setFilename(file.getOriginalFilename())\n            .setLang(lang)\n            .setProblemId(pid)\n            .setUid(UserContext.getCurrentUser());\n\n        // 3. 将任务推入 Redis 队列\n        redisTemplate.opsForList().leftPush(\"judging:queue\", JSON.toJSONString(task));\n        log.info(\"新的[验证]任务已推入队列, submissionId: {}\", submissionId);\n\n        // 4. 立即返回 submissionId\n        return R.ok(submissionId);\n    }\n\n    private String[] saveText2File(Long pid, String basePath, String folderName) throws IOException {\n        if (folderName == null) {\n            folderName = UUID.randomUUID().toString();\n        }\n\n        String subFolderPathStr = basePath + folderName + FileSystems.getDefault().getSeparator();\n\n        Path subFolderPath = Paths.get(subFolderPathStr);\n        Files.createDirectories(subFolderPath);\n\n        String inputFileName = pid + \"_p_input.txt\";\n        // 从其他微服务读取input和output数据\n        Problem problem = problemClient.getProblemById(pid).getData();\n        String inputdata = problem.getTestData();\n        String outputdata = problem.getTestAns();\n\n        Path inputFilePath = subFolderPath.resolve(inputFileName);\n        Files.writeString(inputFilePath, inputdata);\n\n        return new String[]{inputFileName, outputdata};\n    }\n\n    private String[] saveFile(MultipartFile file, String basePath, String folderName) throws IOException {\n        if (folderName == null) {\n            folderName = UUID.randomUUID().toString();\n        }\n\n        String subFolderPathStr = basePath + folderName + FileSystems.getDefault().getSeparator();\n\n        Path subFolderPath = Paths.get(subFolderPathStr);\n        Files.createDirectories(subFolderPath);\n\n        String origFilename = file.getOriginalFilename();\n        if (origFilename == null || origFilename.isBlank()) {\n            throw new IOException(\"上传文件原始文件名为空，无法保存\");\n        }\n\n        Path destinationFilePath = subFolderPath.resolve(origFilename);\n\n        byte[] bytes = file.getBytes();\n        String content = new String(bytes, StandardCharsets.UTF_8);\n        Files.write(destinationFilePath, bytes);\n        // file.transferTo(destinationFilePath.toFile());\n\n        return new String[]{destinationFilePath.toUri().getPath(), folderName, content};\n    }\n\n}\n"
  },
  {
    "path": "DOJ-BE/sandbox-service/src/main/java/com/decade/doj/sandbox/domain/vo/ExecuteMessage.java",
    "content": "package com.decade.doj.sandbox.domain.vo;\n\nimport lombok.Data;\nimport lombok.experimental.Accessors;\n\nimport java.util.HashMap;\nimport java.util.HashSet;\nimport java.util.Map;\nimport java.util.Set;\n\n@Data\n@Accessors(chain = true)\npublic class ExecuteMessage {\n\n    private Integer exitValue;\n    private String status;\n    private String message;\n    private double time;\n    private Long memory;\n\n    private static final Map<Integer, String> exitStatusMap = new HashMap<>();\n    private static final Set<Integer> InfoStatus = new HashSet<>();\n\n    static {\n        exitStatusMap.put(0, \"Finished\");\n        exitStatusMap.put(1, \"Runtime Error\");\n        exitStatusMap.put(2, \"Compile Error\");\n        exitStatusMap.put(124, \"Time Limit Exceeded\");\n        exitStatusMap.put(137, \"Memory Limit Exceeded\");\n        exitStatusMap.put(10, \"Accepted\");\n        exitStatusMap.put(11, \"Wrong Answer\");\n\n        InfoStatus.add(0);\n        InfoStatus.add(1);\n        InfoStatus.add(2);\n        InfoStatus.add(10);\n        InfoStatus.add(11);\n    }\n\n    public static String getStatus(Integer exitValue) {\n        if (exitValue == null) {\n            return \"Unknown Error\";\n        }\n        return exitStatusMap.getOrDefault(exitValue, \"Unknown exitValue\");\n    }\n\n    public static boolean show(Integer exitValue) {\n        return InfoStatus.contains(exitValue);\n    }\n}"
  },
  {
    "path": "DOJ-BE/sandbox-service/src/main/java/com/decade/doj/sandbox/domain/vo/JudgingTask.java",
    "content": "package com.decade.doj.sandbox.domain.vo;\n\nimport lombok.Data;\nimport lombok.experimental.Accessors;\n\n@Data\n@Accessors(chain = true)\npublic class JudgingTask {\n    Long submissionId;\n    String localPath;\n    String input;\n    String output;\n    String filename;\n    String lang;\n    Long problemId;\n    String code;\n    Long uid;\n}\n"
  },
  {
    "path": "DOJ-BE/sandbox-service/src/main/java/com/decade/doj/sandbox/enums/LanguageEnum.java",
    "content": "package com.decade.doj.sandbox.enums;\n\nimport lombok.Getter;\nimport lombok.Setter;\n\nimport java.util.*;\n\n@Getter\npublic enum LanguageEnum {\n\n    PYTHON(\"python3 %s.py\", \"python\", \"128m\", 5, \"code-runner-python\"),\n    JAVA(\"sh -c 'javac %s.java && java -Xmx1024m %s'\", \"java\", \"128m\", 2, \"code-runner-java\"),\n    CPP(\"sh -c 'g++ -std=c++17 %s.cpp -o %s.out && ./%s.out'\", \"cpp\", \"128m\", 1, \"code-runner-cpp\");\n\n    private final String runCmd;\n    private final String language;\n    private final String imageName;\n\n    @Setter\n    private String memoryLimit;\n\n    @Setter\n    private int timeLimit;\n\n    // 语言名称（小写）到枚举实例的映射\n    private static final Map<String, LanguageEnum> NAME_MAP;\n\n    static {\n        Map<String, LanguageEnum> map = new HashMap<>();\n        for (LanguageEnum le : LanguageEnum.values()) {\n            map.put(le.language.toLowerCase(Locale.ROOT), le);\n        }\n        NAME_MAP = Collections.unmodifiableMap(map);\n    }\n\n    LanguageEnum(String runCmd,\n                 String language,\n                 String memoryLimit,\n                 int timeLimit,\n                 String imageName) {\n        this.runCmd = runCmd;\n        this.language = language;\n        this.memoryLimit = memoryLimit;\n        this.timeLimit = timeLimit;\n        this.imageName = imageName;\n    }\n\n    /**\n     * 检查给定语言字符串是否不在支持列表中。\n     *\n     * @param lang 待校验的语言，比如 \"java\"、\"python\"、\"cpp\"\n     * @return 如果 lang 不在 NAME_MAP 中，返回 true；否则返回 false\n     */\n    public static boolean isInValidLanguage(String lang) {\n        if (lang == null) {\n            return true;\n        }\n        return !NAME_MAP.containsKey(lang.trim().toLowerCase(Locale.ROOT));\n    }\n\n    /**\n     * 根据语言字符串获取对应的枚举实例，大小写不敏感。\n     *\n     * @param lang 语言名称，比如 \"java\"、\"python\"、\"cpp\"\n     * @return 对应的 LanguageEnum 实例；若找不到则返回 null\n     */\n    public static LanguageEnum getLanguageEnum(String lang) {\n        if (lang == null) {\n            return null;\n        }\n        return NAME_MAP.get(lang.trim().toLowerCase(Locale.ROOT));\n    }\n}\n"
  },
  {
    "path": "DOJ-BE/sandbox-service/src/main/java/com/decade/doj/sandbox/service/ISandboxService.java",
    "content": "package com.decade.doj.sandbox.service;\n\nimport com.decade.doj.sandbox.domain.vo.ExecuteMessage;\nimport com.decade.doj.sandbox.domain.vo.JudgingTask;\n\nimport java.util.concurrent.CompletableFuture;\n\npublic interface ISandboxService {\n\n    CompletableFuture<ExecuteMessage> runCodeInSandbox(String localPath, String filename, String lang);\n\n    CompletableFuture<ExecuteMessage> runCodeInSandboxWI(String localPath, String inputname, String filename, String lang);\n\n    void execute(JudgingTask task);\n}\n"
  },
  {
    "path": "DOJ-BE/sandbox-service/src/main/java/com/decade/doj/sandbox/service/impl/SandboxService.java",
    "content": "package com.decade.doj.sandbox.service.impl;\n\nimport cn.hutool.core.date.DateTime;\nimport com.decade.doj.common.client.ProblemClient;\nimport com.decade.doj.common.client.SubmissionClient;\nimport com.decade.doj.common.client.UserClient;\nimport com.decade.doj.common.domain.po.Problem;\nimport com.decade.doj.common.domain.po.Submission;\nimport com.decade.doj.common.domain.po.User;\nimport com.decade.doj.common.utils.UserContext;\nimport com.decade.doj.sandbox.domain.vo.ExecuteMessage;\nimport com.decade.doj.sandbox.domain.vo.JudgingTask;\nimport com.decade.doj.sandbox.enums.LanguageEnum;\nimport com.decade.doj.sandbox.service.ISandboxService;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.amqp.rabbit.core.RabbitTemplate;\nimport org.springframework.beans.factory.annotation.Qualifier;\nimport org.springframework.beans.factory.annotation.Value;\nimport org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;\nimport org.springframework.stereotype.Service;\n\nimport java.io.BufferedReader;\nimport java.io.File;\nimport java.io.InputStreamReader;\nimport java.util.Arrays;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\n@Service\n@Slf4j\npublic class SandboxService implements ISandboxService {\n\n    private final RabbitTemplate rabbitTemplate;\n    private final ThreadPoolTaskExecutor runCodeExecutor;\n\n    public SandboxService(\n            RabbitTemplate rabbitTemplate,\n            @Qualifier(\"RunCodeThreadPool\") ThreadPoolTaskExecutor runCodeExecutor\n    ) {\n        this.rabbitTemplate = rabbitTemplate;\n        this.runCodeExecutor = runCodeExecutor;\n    }\n\n    /**\n     * 通过占位符数量构造最终执行命令，例如：\n     *   rawCmd = \"python3 %s.py\"，baseName = \"Main\" -> \"python3 Main.py\"\n     *   rawCmd = \"g++ %s.cpp -o %s.out && ./%s.out\" -> 3 个占位符，将 baseName 填充三次\n     *\n     * @param rawCmd  带有若干 \"%s\" 占位符的 Shell 命令模板\n     * @param baseName 去掉后缀后的文件名\n     * @return 填充好占位符的最终命令字符串\n     */\n    public static String buildRunCommand(String rawCmd, String baseName) {\n        // 计算 \"%s\" 出现的次数\n        int placeholderCount = rawCmd.split(\"%s\", -1).length - 1;\n        return switch (placeholderCount) {\n            case 1 -> String.format(rawCmd, baseName);\n            case 2 -> String.format(rawCmd, baseName, baseName);\n            case 3 -> String.format(rawCmd, baseName, baseName, baseName);\n            default -> throw new IllegalArgumentException(\"Unsupported placeholder count in runCmd: \" + placeholderCount);\n        };\n    }\n\n    // 正则编译为静态常量，避免每次重复编译\n    private static final Pattern MEM_PATTERN = Pattern.compile(\n            \"Maximum resident set size \\\\(kbytes\\\\): (\\\\d+)\"\n    );\n    private static final Pattern TIME_PATTERN = Pattern.compile(\n            \"Elapsed \\\\(wall clock\\\\) time \\\\(h:mm:ss or m:ss\\\\): \\\\d+:(\\\\d+\\\\.\\\\d+)\"\n    );\n    // 构造 /usr/bin/time 与 timeout 的命令模板\n    private static final String TIMEOUT_TEMPLATE = \"/usr/bin/time -v timeout %ds %s\";\n    // 挂载目录\n    private static final String MOUNT_PATH = \"/app\";\n\n\n    @Value(\"${DOJ_CODE_PATH:/app/static/codes/}\")\n    private String containerCodePath;\n    \n    @Value(\"${HOST_CODE_PATH:}\")\n    private String hostCodePath;\n\n    private String convertToHostPath(String containerPath) {\n        if (hostCodePath == null || hostCodePath.isEmpty()) {\n            log.warn(\"HOST_CODE_PATH 未配置，使用容器路径: {}\", containerPath);\n            return containerPath;\n        }\n        \n        String normalizedContainerPath = containerCodePath.replaceAll(\"/$\", \"\");\n        String normalizedHostPath = hostCodePath.replaceAll(\"/$\", \"\");\n        \n        if (containerPath.startsWith(normalizedContainerPath)) {\n            String converted = containerPath.replace(normalizedContainerPath, normalizedHostPath);\n            log.debug(\"路径转换: {} -> {}\", containerPath, converted);\n            return converted;\n        }\n        \n        return containerPath;\n    }\n\n    @Override\n    public CompletableFuture<ExecuteMessage> runCodeInSandbox(String filePath, String filename, String lang) {\n        return CompletableFuture.supplyAsync(\n                () -> _runCodeInSandbox(filePath, filename, lang),\n                runCodeExecutor\n        );\n    }\n\n    private ExecuteMessage _runCodeInSandbox(String filePath, String filename, String lang) {\n\n        LanguageEnum languageEnum = LanguageEnum.getLanguageEnum(lang);\n\n        // 镜像名称\n        String imageName = languageEnum.getImageName();\n        // 运行时挂载目录（宿主与容器）\n        // String fileDir = new File(filePath).getParent();\n        String containerFileDir = new File(filePath).getParent();\n        String hostFileDir = convertToHostPath(containerFileDir);\n        String mountPath = MOUNT_PATH;\n\n        int dotIndex = filename.lastIndexOf(\".\");\n        String baseName = (dotIndex >= 0)\n                ? filename.substring(0, dotIndex)\n                : filename;\n\n        try {\n            // 构造真实要执行的命令\n            String runCmd = buildRunCommand(languageEnum.getRunCmd(), baseName);\n            String execCmd = String.format(\n                    TIMEOUT_TEMPLATE,\n                    languageEnum.getTimeLimit(),\n                    runCmd\n            );\n\n            // 准备 Docker 运行命令列表\n            List<String> command = Arrays.asList(\n                    \"docker\", \"run\", \"--rm\",\n                    \"-v\", hostFileDir + \":\" + mountPath,\n                    \"--memory\", languageEnum.getMemoryLimit(),\n                    imageName,\n                    execCmd\n            );\n\n            ProcessBuilder builder = new ProcessBuilder(command);\n            // 合并标准输出与标准错误\n            builder.redirectErrorStream(true);\n\n            long startTimeMillis = System.currentTimeMillis();\n\n            Process process = builder.start();\n\n            // 读取进程输出\n            StringBuilder fullOutputBuilder = new StringBuilder();\n            try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {\n                String line;\n                while ((line = reader.readLine()) != null) {\n                    fullOutputBuilder.append(line).append(\"\\n\");\n                }\n            }\n\n            int exitCode = process.waitFor();\n            long endTimeMillis = System.currentTimeMillis();\n\n            String fullOutput = fullOutputBuilder.toString().trim();\n\n            // 匹配内存、耗时信息\n            Matcher memMatcher = MEM_PATTERN.matcher(fullOutput);\n            Matcher timeMatcher = TIME_PATTERN.matcher(fullOutput);\n\n            String memoryUsage = memMatcher.find() ? memMatcher.group(1) : \"-1\";\n            String timeUsed = timeMatcher.find()\n                    ? timeMatcher.group(1)\n                    : String.valueOf((endTimeMillis - startTimeMillis) / 1000.0);\n\n            int statIndex = fullOutput.indexOf(\"Command being timed:\");\n            String trimmedOutput = (statIndex != -1)\n                    ? fullOutput.substring(0, statIndex).trim()\n                    : fullOutput;\n\n            log.info(\"Exit code: {}\", exitCode);\n            log.info(\"Raw output:\\n{}\", fullOutput);\n\n            return new ExecuteMessage()\n                    .setExitValue(exitCode)\n                    .setStatus(ExecuteMessage.getStatus(exitCode))\n                    .setMessage(ExecuteMessage.show(exitCode) ? trimmedOutput : \"\")\n                    .setTime(Double.parseDouble(timeUsed))\n                    .setMemory(Long.parseLong(memoryUsage));\n        } catch (Exception e) {\n            log.error(\"运行沙箱代码出错\", e);\n            return new ExecuteMessage()\n                    .setExitValue(1)\n                    .setStatus(\"Runtime Error\")\n                    .setMessage(\"执行代码时发生错误: \" + e.getMessage());\n        }\n    }\n\n    @Override\n    public CompletableFuture<ExecuteMessage> runCodeInSandboxWI(String localPath, String inputname, String filename, String lang) {\n        return CompletableFuture.supplyAsync(\n                () -> _runCodeInSandboxWI(localPath, inputname, filename, lang, null),\n                runCodeExecutor\n        );\n    }\n\n    @Override\n    public void execute(JudgingTask task) {\n        ExecuteMessage result = _runCodeInSandboxWI(task.getLocalPath(), task.getInput(), task.getFilename(), task.getLang(), task.getOutput());\n        log.info(\"User: {}, Problem: {}, 判题结果: {}\", task.getUid(), task.getProblemId(), result);\n\n        Map<String, Object> resultMessage = Map.of(\n                \"submissionId\", task.getSubmissionId(),\n                \"executeMessage\", result\n        );\n        rabbitTemplate.convertAndSend(\"doj.topic\", \"judging.result\", resultMessage);\n        log.info(\"判题结果已发布到MQ, submissionId: {}\", task.getSubmissionId());\n    }\n\n    private ExecuteMessage _runCodeInSandboxWI(String filePath, String inputname, String filename, String lang, String answer) {\n\n        LanguageEnum languageEnum = LanguageEnum.getLanguageEnum(lang);\n\n        // 镜像名称\n        String imageName = languageEnum.getImageName();\n        // 运行时挂载目录（宿主与容器）\n        // String fileDir = new File(filePath).getParent();\n        String containerFileDir = new File(filePath).getParent();\n        String hostFileDir = convertToHostPath(containerFileDir);\n        String mountPath = MOUNT_PATH;\n\n        int dotIndex = filename.lastIndexOf(\".\");\n        String baseName = (dotIndex >= 0)\n                ? filename.substring(0, dotIndex)\n                : filename;\n\n        try {\n            // 构造真实要执行的命令\n            String runCmd = buildRunCommand(languageEnum.getRunCmd(), baseName);\n            runCmd += \" < \" + inputname; // 添加输入重定向\n            String execCmd = String.format(\n                    TIMEOUT_TEMPLATE,\n                    languageEnum.getTimeLimit(),\n                    runCmd\n            );\n\n            // 准备 Docker 运行命令列表\n            List<String> command = Arrays.asList(\n                    \"docker\", \"run\", \"--rm\",\n                    \"-v\", hostFileDir + \":\" + mountPath,\n                    \"--memory\", languageEnum.getMemoryLimit(),\n                    imageName,\n                    execCmd\n            );\n\n            ProcessBuilder builder = new ProcessBuilder(command);\n            // 合并标准输出与标准错误\n            builder.redirectErrorStream(true);\n\n            long startTimeMillis = System.currentTimeMillis();\n\n            Process process = builder.start();\n\n            // 读取进程输出\n            StringBuilder fullOutputBuilder = new StringBuilder();\n            try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {\n                String line;\n                while ((line = reader.readLine()) != null) {\n                    fullOutputBuilder.append(line).append(\"\\n\");\n                }\n            }\n\n            int exitCode = process.waitFor();\n            long endTimeMillis = System.currentTimeMillis();\n\n            String fullOutput = fullOutputBuilder.toString().trim();\n\n            // 匹配内存、耗时信息\n            Matcher memMatcher = MEM_PATTERN.matcher(fullOutput);\n            Matcher timeMatcher = TIME_PATTERN.matcher(fullOutput);\n\n            String memoryUsage = memMatcher.find() ? memMatcher.group(1) : \"-1\";\n            String timeUsed = timeMatcher.find()\n                    ? timeMatcher.group(1)\n                    : String.valueOf((endTimeMillis - startTimeMillis) / 1000.0);\n\n            int statIndex = fullOutput.indexOf(\"Command being timed:\");\n            String trimmedOutput = (statIndex != -1)\n                    ? fullOutput.substring(0, statIndex).trim()\n                    : fullOutput;\n\n            log.info(\"Exit code: {}\", exitCode);\n            log.info(\"Raw output:\\n{}\", fullOutput);\n\n            String status = ExecuteMessage.getStatus(exitCode);\n            if (answer != null && ExecuteMessage.getStatus(exitCode).equals(\"Finished\")) {\n                // 比较输出与答案\n                boolean isCorrect = trimmedOutput.equals(answer.trim());\n                if (isCorrect) {\n                    status = \"Accepted\";\n                    exitCode = 10;\n                } else {\n                    status = \"Wrong Answer\";\n                    exitCode = 11;\n                    trimmedOutput += \"\\n```\\nExpected: \" + answer.trim();\n                }\n            }\n\n            return new ExecuteMessage()\n                    .setExitValue(exitCode)\n                    .setStatus(status)\n                    .setMessage(ExecuteMessage.show(exitCode) ? trimmedOutput : \"\")\n                    .setTime(Double.parseDouble(timeUsed))\n                    .setMemory(Long.parseLong(memoryUsage));\n        } catch (Exception e) {\n            log.error(\"运行沙箱代码出错\", e);\n            return new ExecuteMessage()\n                    .setExitValue(1)\n                    .setStatus(\"Runtime Error\")\n                    .setMessage(\"执行代码时发生错误: \" + e.getMessage());\n        }\n    }\n}\n"
  },
  {
    "path": "DOJ-BE/sandbox-service/src/main/java/com/decade/doj/sandbox/worker/JudgingWorker.java",
    "content": "package com.decade.doj.sandbox.worker;\n\nimport com.alibaba.fastjson.JSON;\nimport com.decade.doj.sandbox.domain.vo.JudgingTask;\nimport com.decade.doj.sandbox.service.ISandboxService;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.beans.factory.DisposableBean;\nimport org.springframework.beans.factory.annotation.Qualifier;\nimport org.springframework.boot.ApplicationArguments;\nimport org.springframework.boot.ApplicationRunner;\nimport org.springframework.data.redis.core.StringRedisTemplate;\nimport org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;\nimport org.springframework.stereotype.Component;\n\nimport java.util.concurrent.TimeUnit;\nimport java.util.concurrent.atomic.AtomicBoolean;\n\n/**\n * 判题任务消费者\n * <p>\n * 使用单一调度线程从 Redis 队列轮询任务，并将任务分发到 JudgingThreadPool 执行。\n * 实现 DisposableBean 接口支持优雅关闭。\n */\n@Slf4j\n@Component\npublic class JudgingWorker implements ApplicationRunner, DisposableBean {\n\n    private final StringRedisTemplate redisTemplate;\n    private final ISandboxService sandboxService;\n    private final ThreadPoolTaskExecutor judgingExecutor;\n\n    private static final String JUDGING_QUEUE_KEY = \"judging:queue\";\n    private static final int POLL_TIMEOUT_SECONDS = 5;\n\n    private final AtomicBoolean running = new AtomicBoolean(true);\n    private Thread schedulerThread;\n\n    public JudgingWorker(\n            StringRedisTemplate redisTemplate,\n            ISandboxService sandboxService,\n            @Qualifier(\"JudgingThreadPool\") ThreadPoolTaskExecutor judgingExecutor\n    ) {\n        this.redisTemplate = redisTemplate;\n        this.sandboxService = sandboxService;\n        this.judgingExecutor = judgingExecutor;\n    }\n\n    @Override\n    public void run(ApplicationArguments args) {\n        schedulerThread = new Thread(this::pollAndDispatch, \"JudgingScheduler\");\n        schedulerThread.start();\n        log.info(\"判题调度器已启动，任务将分发到 JudgingThreadPool 执行\");\n    }\n\n    /**\n     * 轮询 Redis 队列并分发任务到线程池\n     */\n    private void pollAndDispatch() {\n        while (running.get()) {\n            try {\n                String taskJson = redisTemplate.opsForList()\n                        .rightPop(JUDGING_QUEUE_KEY, POLL_TIMEOUT_SECONDS, TimeUnit.SECONDS);\n\n                if (taskJson != null) {\n                    JudgingTask task = JSON.parseObject(taskJson, JudgingTask.class);\n                    log.info(\"收到判题任务: submissionId={}, problemId={}\", \n                            task.getSubmissionId(), task.getProblemId());\n\n                    // 分发到线程池异步执行\n                    judgingExecutor.execute(() -> executeTask(task));\n                }\n            } catch (Exception e) {\n                if (running.get()) {\n                    log.error(\"轮询判题队列出现异常\", e);\n                    // 短暂休眠避免错误风暴\n                    try {\n                        Thread.sleep(1000);\n                    } catch (InterruptedException ie) {\n                        Thread.currentThread().interrupt();\n                        break;\n                    }\n                }\n            }\n        }\n        log.info(\"判题调度器已停止轮询\");\n    }\n\n    /**\n     * 执行单个判题任务\n     */\n    private void executeTask(JudgingTask task) {\n        try {\n            log.debug(\"开始执行判题任务: submissionId={}\", task.getSubmissionId());\n            sandboxService.execute(task);\n            log.debug(\"判题任务执行完成: submissionId={}\", task.getSubmissionId());\n        } catch (Exception e) {\n            log.error(\"执行判题任务异常: submissionId={}, problemId={}\", \n                    task.getSubmissionId(), task.getProblemId(), e);\n        }\n    }\n\n    @Override\n    public void destroy() {\n        log.info(\"正在关闭判题调度器...\");\n        running.set(false);\n\n        if (schedulerThread != null) {\n            schedulerThread.interrupt();\n            try {\n                // 等待调度线程终止\n                schedulerThread.join(5000);\n            } catch (InterruptedException e) {\n                Thread.currentThread().interrupt();\n            }\n        }\n\n        log.info(\"判题调度器已关闭，线程池将由 Spring 容器管理关闭\");\n    }\n}\n\n"
  },
  {
    "path": "DOJ-BE/sandbox-service/src/main/resources/application.yaml",
    "content": "server:\n    port: ${doj.port.sandbox-service}\n\nmanagement:\n  endpoints:\n    web:\n      exposure:\n        include: health,prometheus\n\ndoj:\n    mq:\n        host: ${DOJ_MQ_HOST:127.0.0.1}\n    redis:\n        host: ${DOJ_REDIS_HOST:127.0.0.1}\n    swagger:\n        title: 沙箱接口文档\n        scan: com.decade.doj.sandbox.controller"
  },
  {
    "path": "DOJ-BE/sandbox-service/src/main/resources/bootstrap.yaml",
    "content": "spring:\n    application:\n        name: sandbox-service\n    profiles:\n        active: dev, common\n    cloud:\n        nacos:\n            server-addr: ${NACOS_SERVER_ADDR:127.0.0.1:8848}\n            config:\n                file-extension: yaml\n                shared-configs:\n                    - dataId: shared-jdbc.yaml\n                    - dataId: shared-swagger.yaml\n                    - dataId: shared-jwt.yaml\n                    - dataId: shared-rabbitmq.yaml"
  },
  {
    "path": "DOJ-BE/sandbox-service/src/main/resources/test/20240906.py",
    "content": "\nans = []\nres = 0\nfor i in range(10000000):\n    ans.append(i)\n    res += i\n\nprint(f\"{res} test success nopt failed!\")"
  },
  {
    "path": "DOJ-BE/sandbox-service/src/main/resources/test/main.cpp",
    "content": "#include <iostream>\n#include <vector>\n\nusing namespace std;\n\nint main() {\n    long long ans = 0;\n    vector<int> v;\n    for (int i = 0; i < 10000000; i++) {\n        ans += i;\n        v.push_back(i);\n    }\n    cout << ans << \" test success!\" << endl;\n    return 0;\n}"
  },
  {
    "path": "DOJ-BE/sandbox-service/src/main/resources/test/main.java",
    "content": "import java.util.List;\nimport java.util.ArrayList;\n\npublic class Main {\n    public static void main(String[] args) {\n        long ans = 0;\n        List<Integer> res = new ArrayList<>();\n        for (int i = 0;i < 10000000;i++){\n            ans += i;\n            res.add(i);\n        }\n        System.out.println(ans+\" test success!\");\n    }\n}"
  },
  {
    "path": "DOJ-BE/sandbox-service/src/test/java/com/decade/doj/sandbox/SandboxTest.java",
    "content": "package com.decade.doj.sandbox;\n\nimport com.decade.doj.sandbox.domain.vo.ExecuteMessage;\nimport com.decade.doj.sandbox.service.ISandboxService;\nimport org.junit.jupiter.api.Test;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.boot.test.context.SpringBootTest;\n\n@SpringBootTest\npublic class SandboxTest {\n\n    @Autowired\n    private ISandboxService sandboxService;\n\n    @Test\n    public void testRunCode() {\n        String filePath = \"/Users/qzj/Desktop/Development/D-OnlineJudge/static/docker-sandbox/python/Main.py\"; // 替换为实际的文件路径\n        String filename = \"Main.py\"; // 替换为实际的文件名\n        String lang = \"python\";\n\n        // try {\n        //     ExecuteMessage res = sandboxService.runCodeInSandbox(filePath, filename, lang);\n        //     System.out.println(res);\n        // } catch (Exception e) {\n        //     e.printStackTrace();\n        // }\n    }\n\n}\n"
  },
  {
    "path": "DOJ-BE/submission-service/Dockerfile",
    "content": "# Stage 1: Build the application\nFROM maven:3.8.4-openjdk-17 AS build\n\nWORKDIR /app\n\n# Copy all pom.xml files first to leverage Docker cache\nCOPY DOJ-BE/pom.xml .\nCOPY DOJ-BE/common/pom.xml ./common/\nCOPY DOJ-BE/gateway-service/pom.xml ./gateway-service/\nCOPY DOJ-BE/problem-service/pom.xml ./problem-service/\nCOPY DOJ-BE/sandbox-service/pom.xml ./sandbox-service/\nCOPY DOJ-BE/submission-service/pom.xml ./submission-service/\nCOPY DOJ-BE/user-service/pom.xml ./user-service/\n\n# Download all dependencies\nRUN mvn dependency:go-offline\n\n# Copy all source code\nCOPY DOJ-BE/common/src ./common/src\nCOPY DOJ-BE/submission-service/src ./submission-service/src\n\n# Build the specific service\nRUN mvn -f ./pom.xml -pl submission-service -am clean package -DskipTests\nRUN ls -al ./submission-service/target\n\n# Stage 2: Create the runtime image\nFROM eclipse-temurin:17-jre\n\nWORKDIR /app\n\n# Copy the executable jar from the build stage\nCOPY --from=build /app/submission-service/target/submission-service.jar ./app.jar\n\nEXPOSE 8084\n\nENTRYPOINT [\"java\", \"-jar\", \"./app.jar\"]"
  },
  {
    "path": "DOJ-BE/submission-service/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n         xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n    <parent>\n        <groupId>com.decade</groupId>\n        <artifactId>DOJ-BE</artifactId>\n        <version>1.0-SNAPSHOT</version>\n    </parent>\n\n    <groupId>com.decade.doj</groupId>\n    <artifactId>submission-service</artifactId>\n\n    <properties>\n        <maven.compiler.source>17</maven.compiler.source>\n        <maven.compiler.target>17</maven.compiler.target>\n        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>\n    </properties>\n\n    <dependencies>\n        <!--common-->\n        <dependency>\n            <groupId>com.decade</groupId>\n            <artifactId>common</artifactId>\n            <version>1.0-SNAPSHOT</version>\n        </dependency>\n        <!--web-->\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-web</artifactId>\n        </dependency>\n        <!--数据库-->\n        <dependency>\n            <groupId>mysql</groupId>\n            <artifactId>mysql-connector-java</artifactId>\n        </dependency>\n        <!--mybatis-->\n        <dependency>\n            <groupId>com.baomidou</groupId>\n            <artifactId>mybatis-plus-boot-starter</artifactId>\n        </dependency>\n        <!--nacos 服务注册发现-->\n        <dependency>\n            <groupId>com.alibaba.cloud</groupId>\n            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>\n        </dependency>\n        <!--openfeign-->\n        <dependency>\n            <groupId>org.springframework.cloud</groupId>\n            <artifactId>spring-cloud-starter-openfeign</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-websocket</artifactId>\n        </dependency>\n    </dependencies>\n\n    <build>\n        <finalName>${project.artifactId}</finalName>\n        <plugins>\n            <plugin>\n                <groupId>org.springframework.boot</groupId>\n                <artifactId>spring-boot-maven-plugin</artifactId>\n            </plugin>\n        </plugins>\n    </build>\n</project>"
  },
  {
    "path": "DOJ-BE/submission-service/src/main/java/com/decade/doj/submission/SubmissionApplication.java",
    "content": "package com.decade.doj.submission;\n\nimport com.decade.doj.common.config.custom.DefaultFeignConfig;\nimport com.decade.doj.common.config.custom.JwtTool;\nimport com.decade.doj.common.config.custom.MVCConfig;\nimport com.decade.doj.common.config.custom.MybatisConfig;\nimport com.decade.doj.common.interceptor.AdminCheckInterceptor;\nimport com.decade.doj.common.interceptor.IdentityInterceptor;\nimport org.mybatis.spring.annotation.MapperScan;\nimport org.springframework.boot.SpringApplication;\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\nimport org.springframework.cloud.openfeign.EnableFeignClients;\nimport org.springframework.context.annotation.Import;\n\n@SpringBootApplication\n@MapperScan(\"com.decade.doj.submission.mapper\")\n@EnableFeignClients(basePackages = \"com.decade.doj.common.client\", defaultConfiguration = DefaultFeignConfig.class)\n@Import({JwtTool.class, MVCConfig.class, MybatisConfig.class, IdentityInterceptor.class, AdminCheckInterceptor.class})\npublic class SubmissionApplication {\n    public static void main(String[] args) {\n        SpringApplication.run(SubmissionApplication.class, args);\n    }\n}\n"
  },
  {
    "path": "DOJ-BE/submission-service/src/main/java/com/decade/doj/submission/config/MqConfig.java",
    "content": "package com.decade.doj.submission.config;\n\nimport org.springframework.amqp.core.Binding;\nimport org.springframework.amqp.core.BindingBuilder;\nimport org.springframework.amqp.core.Queue;\nimport org.springframework.amqp.core.TopicExchange;\nimport org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;\nimport org.springframework.amqp.support.converter.MessageConverter;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\n\n@Configuration\npublic class MqConfig {\n\n    @Bean\n    public TopicExchange topicExchange() {\n        return new TopicExchange(\"doj.topic\");\n    }\n\n    @Bean\n    public Queue judgingResultQueue() {\n        return new Queue(\"judging.result.queue\");\n    }\n\n    @Bean\n    public Binding binding() {\n        return BindingBuilder.bind(judgingResultQueue()).to(topicExchange()).with(\"judging.result\");\n    }\n\n    @Bean\n    public MessageConverter jsonMessageConverter(){\n        return new Jackson2JsonMessageConverter();\n    }\n}\n"
  },
  {
    "path": "DOJ-BE/submission-service/src/main/java/com/decade/doj/submission/config/WebSocketConfig.java",
    "content": "package com.decade.doj.submission.config;\n\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.web.socket.server.standard.ServerEndpointExporter;\n\n@Configuration\npublic class WebSocketConfig {\n    @Bean\n    public ServerEndpointExporter serverEndpointExporter() {\n        // 让 @ServerEndpoint 生效\n        return new ServerEndpointExporter();\n    }\n}"
  },
  {
    "path": "DOJ-BE/submission-service/src/main/java/com/decade/doj/submission/controller/SubmissionController.java",
    "content": "package com.decade.doj.submission.controller;\n\nimport com.decade.doj.common.domain.PageDTO;\nimport com.decade.doj.common.domain.R;\nimport com.decade.doj.common.domain.vo.ExecuteMessage;\nimport com.decade.doj.common.domain.vo.SubmissionStatsVO;\nimport com.decade.doj.common.utils.UserContext;\nimport com.decade.doj.submission.domain.dto.SubmissionPageQueryDTO;\nimport com.decade.doj.submission.domain.po.Submission;\nimport com.decade.doj.submission.service.ISubmissionService;\nimport io.swagger.v3.oas.annotations.Operation;\nimport io.swagger.v3.oas.annotations.tags.Tag;\nimport lombok.RequiredArgsConstructor;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.web.bind.annotation.*;\n\nimport java.util.List;\n\n@RestController\n@RequestMapping(\"/submission\")\n@Tag(name = \"提交相关接口\")\n@Slf4j\n@RequiredArgsConstructor\npublic class SubmissionController {\n\n    private final ISubmissionService submissionService;\n\n    @PostMapping(\"/submit\")\n    @Operation(summary = \"提交记录\")\n    public R<Long> submit(@RequestBody Submission submission) {\n        submissionService.save(submission);\n        return R.ok(submission.getId());\n    }\n\n    @GetMapping(\"/page\")\n    @Operation(summary = \"分页获取提交列表\")\n    public R<PageDTO<Submission>> page(SubmissionPageQueryDTO problemPageQueryDTO) {\n        PageDTO<Submission> res = submissionService.pageQuery(problemPageQueryDTO);\n        return R.ok(res);\n    }\n\n    @GetMapping(\"/stats\")\n    @Operation(summary = \"获取提交统计\")\n    public R<SubmissionStatsVO> getStats() {\n        return R.ok(submissionService.getStats());\n    }\n\n    @GetMapping(\"/match/{id}\")\n    @Operation(summary = \"获取当前用户指定问题的提交详情\")\n    public R<Integer> getById(@PathVariable String id) {\n        List<Submission> submissions = submissionService.lambdaQuery()\n                .eq(Submission::getProblemId, id)\n                .list();\n        int f = 0;\n        for (Submission submission : submissions) {\n            if (submission.getUserId() != null && submission.getUserId().equals(UserContext.getCurrentUser())) {\n                f = 1;\n                if (ExecuteMessage.getStatus(submission.getExitValue()).equals(\"Accepted\")) {\n                    return R.ok(1); // 已经提交过且通过\n                }\n            }\n        }\n        if (f == 1) {\n            return R.ok(2); // 已经提交过但未通过\n        }\n        return R.ok(0);\n    }\n}\n"
  },
  {
    "path": "DOJ-BE/submission-service/src/main/java/com/decade/doj/submission/domain/dto/SubmissionPageQueryDTO.java",
    "content": "package com.decade.doj.submission.domain.dto;\n\nimport com.decade.doj.common.domain.PageQueryDTO;\nimport io.swagger.v3.oas.annotations.media.Schema;\nimport lombok.Data;\nimport lombok.EqualsAndHashCode;\nimport lombok.experimental.Accessors;\n\n@EqualsAndHashCode(callSuper = true)\n@Data\n@Accessors(chain = true)\npublic class SubmissionPageQueryDTO extends PageQueryDTO {\n\n    @Schema(description = \"提交ID\")\n    private Long submissionId;\n\n    @Schema(description = \"用户ID\")\n    private String userId;\n\n    @Schema(description = \"题目ID\")\n    private String problemId;\n\n    @Schema(description = \"语言\")\n    private String language;\n\n    @Schema(description = \"判题状态\")\n    private String status;\n}\n"
  },
  {
    "path": "DOJ-BE/submission-service/src/main/java/com/decade/doj/submission/domain/po/Submission.java",
    "content": "package com.decade.doj.submission.domain.po;\n\nimport com.baomidou.mybatisplus.annotation.IdType;\nimport com.baomidou.mybatisplus.annotation.TableField;\nimport com.baomidou.mybatisplus.annotation.TableId;\nimport com.baomidou.mybatisplus.annotation.TableName;\nimport java.util.Date;\nimport lombok.Data;\n\n/**\n * 代码提交记录表\n * @TableName submission\n */\n@TableName(value =\"submission\")\n@Data\npublic class Submission {\n    /**\n     * 提交记录主键\n     */\n    @TableId(type = IdType.AUTO)\n    private Long id;\n\n    /**\n     * 用户ID（来源于 doj_user.user）\n     */\n    private Long userId;\n\n    /**\n     * 题目ID（来源于 doj_problem.problem）\n     */\n    private Long problemId;\n\n    private String userName;\n\n    private String problemName;\n\n    /**\n     * 编程语言\n     */\n    private String language;\n\n    /**\n     * 提交的代码文本内容\n     */\n    private String code;\n\n    /**\n     * 程序退出码\n     */\n    private Integer exitValue;\n\n    /**\n     * 判题状态\n     */\n    private String status;\n\n    /**\n     * 判题详细信息\n     */\n    private String message;\n\n    /**\n     * 运行时间（单位：秒）\n     */\n    private Double time;\n\n    /**\n     * 内存使用（单位：KB）\n     */\n    private Long memory;\n\n    /**\n     * 提交时间\n     */\n    private Date submitTime;\n}"
  },
  {
    "path": "DOJ-BE/submission-service/src/main/java/com/decade/doj/submission/mapper/SubmissionMapper.java",
    "content": "package com.decade.doj.submission.mapper;\r\n\r\nimport com.decade.doj.submission.domain.po.Submission;\r\nimport com.baomidou.mybatisplus.core.mapper.BaseMapper;\r\n\r\n/**\r\n* @author qzj\r\n* @description 针对表【submission(代码提交记录表)】的数据库操作Mapper\r\n* @createDate 2025-06-05 13:28:52\r\n* @Entity com.decade.doj.submission.domain.po.Submission\r\n*/\r\npublic interface SubmissionMapper extends BaseMapper<Submission> {\r\n\r\n}\r\n\r\n\r\n\r\n\r\n"
  },
  {
    "path": "DOJ-BE/submission-service/src/main/java/com/decade/doj/submission/mq/ResultListener.java",
    "content": "package com.decade.doj.submission.mq;\n\nimport com.alibaba.fastjson.JSON;\nimport com.alibaba.fastjson.JSONObject;\nimport com.decade.doj.common.client.ProblemClient;\nimport com.decade.doj.common.client.UserClient;\nimport com.decade.doj.common.domain.po.Problem;\nimport com.decade.doj.common.domain.po.User;\nimport com.decade.doj.common.utils.UserContext;\nimport com.decade.doj.submission.domain.po.Submission;\nimport com.decade.doj.submission.service.ISubmissionService;\nimport com.decade.doj.submission.websocket.SubmissionWSServer;\nimport lombok.RequiredArgsConstructor;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.amqp.rabbit.annotation.RabbitListener;\nimport org.springframework.amqp.rabbit.core.RabbitTemplate;\nimport org.springframework.stereotype.Component;\n\nimport java.util.Map;\n\n@Slf4j\n@Component\n@RequiredArgsConstructor\npublic class ResultListener {\n\n    private final ISubmissionService submissionService;\n    private final ProblemClient problemClient;\n    private final UserClient userClient;\n    private final RabbitTemplate rabbitTemplate;\n\n    @RabbitListener(queues = \"judging.result.queue\")\n    public void onMessage(Map<String, Object> message) {\n        log.info(\"从 RabbitMQ 收到判题结果消息: {}\", message);\n        try {\n            Long submissionId = Long.valueOf(message.get(\"submissionId\").toString());\n            Map<String, Object> executeMessage = (Map<String, Object>) message.get(\"executeMessage\");\n\n            if (executeMessage == null) {\n                log.error(\"消息格式错误，缺少 submissionId 或 executeMessage\");\n                return;\n            }\n\n            // 1. 更新数据库\n            Submission submission = submissionService.getById(submissionId);\n            UserContext.setCurrentUser(submission.getUserId());\n            User user = userClient.getUser(submission.getUserId()).getData();\n            if (user == null) {\n                log.error(\"用户 {} 不存在，无法更新提交记录\", submission.getUserId());\n                return;\n            }\n            submission.setUserName(user.getUsername());\n            Problem problem = problemClient.getProblemById(submission.getProblemId()).getData();\n            if (problem == null) {\n                log.error(\"题目 {} 不存在，无法更新提交记录\", submission.getProblemId());\n                return;\n            }\n            submission.setProblemName(problem.getName());\n            submission.setId(submissionId);\n            submission.setStatus((String) executeMessage.get(\"status\"));\n            submission.setExitValue((Integer) executeMessage.get(\"exitValue\"));\n            submission.setMessage((String) executeMessage.get(\"message\"));\n            submission.setTime((Double) executeMessage.get(\"time\"));\n            submission.setMemory(((Number) executeMessage.get(\"memory\")).longValue());\n            submissionService.updateById(submission);\n            log.info(\"提交记录 {} 已更新\", submissionId);\n\n            // 2. 通过 WebSocket 推送给前端\n            SubmissionWSServer.sendMessage(submissionId, JSON.toJSONString(submission));\n\n            // 3. 发送 RabbitMQ 事件，用于更新用户和题目统计\n            Map<String, Object> submissionMessage = Map.of(\n                \"problemId\", submission.getProblemId(),\n                \"isAccepted\", \"Accepted\".equals(submission.getStatus())\n            );\n            rabbitTemplate.convertAndSend(\"doj.topic\", \"submission.created\", submissionMessage);\n\n            if (\"Accepted\".equals(submission.getStatus())) {\n                long acCount = submissionService.lambdaQuery()\n                    .eq(Submission::getUserId, submission.getUserId())\n                    .eq(Submission::getProblemId, submission.getProblemId())\n                    .eq(Submission::getStatus, \"Accepted\")\n                    .count();\n                if (acCount == 1) {\n                    Map<String, Long> solvedMessage = Map.of(\n                        \"userId\", submission.getUserId(),\n                        \"problemId\", submission.getProblemId()\n                    );\n                    rabbitTemplate.convertAndSend(\"doj.topic\", \"problem.solved\", solvedMessage);\n                    log.info(\"用户 {} 首次 AC 题目 {}，已发送 problem.solved 事件\", submission.getUserId(), submission.getProblemId());\n                }\n            }\n\n        } catch (Exception e) {\n            log.error(\"处理判题结果消息时发生异常\", e);\n        }\n    }\n}\n"
  },
  {
    "path": "DOJ-BE/submission-service/src/main/java/com/decade/doj/submission/service/ISubmissionService.java",
    "content": "package com.decade.doj.submission.service;\r\n\r\nimport com.decade.doj.common.domain.PageDTO;\r\nimport com.decade.doj.common.domain.vo.SubmissionStatsVO;\r\nimport com.decade.doj.submission.domain.dto.SubmissionPageQueryDTO;\r\nimport com.decade.doj.submission.domain.po.Submission;\r\nimport com.baomidou.mybatisplus.extension.service.IService;\r\n\r\n/**\r\n* @author qzj\r\n* @description 针对表【submission(代码提交记录表)】的数据库操作Service\r\n* @createDate 2025-06-05 13:28:52\r\n*/\r\npublic interface ISubmissionService extends IService<Submission> {\r\n    PageDTO<Submission> pageQuery(SubmissionPageQueryDTO submissionPageQueryDTO);\r\n\r\n    SubmissionStatsVO getStats();\r\n}\r\n"
  },
  {
    "path": "DOJ-BE/submission-service/src/main/java/com/decade/doj/submission/service/impl/ISubmissionServiceImpl.java",
    "content": "package com.decade.doj.submission.service.impl;\r\n\r\nimport com.baomidou.mybatisplus.extension.plugins.pagination.Page;\r\nimport com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;\r\nimport com.decade.doj.common.domain.PageDTO;\r\nimport com.decade.doj.common.domain.vo.SubmissionStatsVO;\r\nimport com.decade.doj.submission.domain.dto.SubmissionPageQueryDTO;\r\nimport com.decade.doj.submission.domain.po.Submission;\r\nimport com.decade.doj.submission.service.ISubmissionService;\r\nimport com.decade.doj.submission.mapper.SubmissionMapper;\r\nimport lombok.RequiredArgsConstructor;\r\nimport lombok.extern.slf4j.Slf4j;\r\nimport org.springframework.amqp.rabbit.core.RabbitTemplate;\r\nimport org.springframework.stereotype.Service;\r\n\r\nimport java.time.LocalDate;\r\nimport java.time.ZoneId;\r\nimport java.util.Date;\r\nimport java.util.List;\r\nimport java.util.Map;\r\n\r\n/**\r\n* @author qzj\r\n* @description 针对表【submission(代码提交记录表)】的数据库操作Service实现\r\n* @createDate 2025-06-05 13:28:52\r\n*/\r\n@Service\r\n@Slf4j\r\n@RequiredArgsConstructor\r\npublic class ISubmissionServiceImpl extends ServiceImpl<SubmissionMapper, Submission>\r\n    implements ISubmissionService {\r\n\r\n    public PageDTO<Submission> pageQuery(SubmissionPageQueryDTO submissionPageQueryDTO) {\r\n        // 如果 submissionId 存在，则执行精确查询\r\n        if (submissionPageQueryDTO.getSubmissionId() != null) {\r\n            Submission submission = this.getById(submissionPageQueryDTO.getSubmissionId());\r\n            if (submission == null) {\r\n                return PageDTO.empty(0L, 0L);\r\n            }\r\n            return PageDTO.fullPage(1L, 1L, List.of(submission));\r\n        }\r\n\r\n        log.info(\"分页查询提交列表: {}\", submissionPageQueryDTO);\r\n        log.info(\"userId={}, problemId={}, status={}, language={}\",\r\n                submissionPageQueryDTO.getUserId(),\r\n                submissionPageQueryDTO.getProblemId(),\r\n                submissionPageQueryDTO.getStatus(),\r\n                submissionPageQueryDTO.getLanguage());\r\n\r\n        String user = submissionPageQueryDTO.getUserId();\r\n        String problem = submissionPageQueryDTO.getProblemId();\r\n\r\n        Page<Submission> submissionList = lambdaQuery()\r\n                .like(user != null && !user.isBlank(), Submission::getUserName, submissionPageQueryDTO.getUserId())\r\n                .like(problem != null && !problem.isBlank(), Submission::getProblemName, submissionPageQueryDTO.getProblemId())\r\n                .eq(submissionPageQueryDTO.getStatus() != null, Submission::getStatus, submissionPageQueryDTO.getStatus())\r\n                .eq(submissionPageQueryDTO.getLanguage() != null, Submission::getLanguage, submissionPageQueryDTO.getLanguage())\r\n                .page(submissionPageQueryDTO.toMpPage(\"submit_time\", false));\r\n\r\n        return PageDTO.fullPage(submissionList.getTotal(), submissionList.getPages(), submissionList.getRecords());\r\n    }\r\n\r\n    @Override\r\n    public SubmissionStatsVO getStats() {\r\n        // 获取总提交数\r\n        long totalSubmissions = this.count();\r\n\r\n        // 获取今日提交数\r\n        LocalDate today = LocalDate.now();\r\n        Date startOfDay = Date.from(today.atStartOfDay(ZoneId.systemDefault()).toInstant());\r\n        long todaySubmissions = this.lambdaQuery()\r\n                .ge(Submission::getSubmitTime, startOfDay)\r\n                .count();\r\n\r\n        return new SubmissionStatsVO(totalSubmissions, todaySubmissions);\r\n    }\r\n\r\n}\r\n"
  },
  {
    "path": "DOJ-BE/submission-service/src/main/java/com/decade/doj/submission/websocket/SubmissionWSServer.java",
    "content": "package com.decade.doj.submission.websocket;\n\nimport com.alibaba.fastjson.JSON;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.stereotype.Component;\n\nimport javax.websocket.OnClose;\nimport javax.websocket.OnMessage;\nimport javax.websocket.OnOpen;\nimport javax.websocket.Session;\nimport javax.websocket.server.ServerEndpoint;\nimport java.io.IOException;\nimport java.util.Map;\nimport java.util.concurrent.ConcurrentHashMap;\n\n@Slf4j\n@Component\n@ServerEndpoint(\"/ws/submission\")\npublic class SubmissionWSServer {\n\n    private static final Map<Long, Session> ONLINE_SESSIONS = new ConcurrentHashMap<>();\n\n    @OnOpen\n    public void onOpen(Session session) {\n        log.info(\"WebSocket 连接已建立: {}\", session.getId());\n    }\n\n    @OnMessage\n    public void onMessage(String message, Session session) {\n        try {\n            Map<String, Object> data = JSON.parseObject(message);\n            Long submissionId = Long.valueOf(data.get(\"submissionId\").toString());\n            ONLINE_SESSIONS.put(submissionId, session);\n            log.info(\"submissionId: {} 已订阅 WebSocket 通知\", submissionId);\n        } catch (Exception e) {\n            log.error(\"处理 WebSocket 消息时出错: {}\", message, e);\n        }\n    }\n\n    @OnClose\n    public void onClose(Session session) {\n        // 清理无效的 session\n        ONLINE_SESSIONS.values().removeIf(s -> !s.isOpen());\n        log.info(\"WebSocket 连接已关闭: {}\", session.getId());\n    }\n\n    public static void sendMessage(Long submissionId, String message) {\n        Session session = ONLINE_SESSIONS.get(submissionId);\n        if (session != null && session.isOpen()) {\n            try {\n                session.getBasicRemote().sendText(message);\n                log.info(\"成功向 submissionId: {} 推送消息\", submissionId);\n                // 推送成功后可以移除，避免重复推送\n                ONLINE_SESSIONS.remove(submissionId);\n            } catch (IOException e) {\n                log.error(\"向 submissionId: {} 推送消息失败\", submissionId, e);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "DOJ-BE/submission-service/src/main/resources/application.yaml",
    "content": "server:\n    port: ${doj.port.submission-service}\n\nmanagement:\n  endpoints:\n    web:\n      exposure:\n        include: health,prometheus\n\ndoj:\n    db:\n        host: ${DOJ_DB_HOST:127.0.0.1}\n        name: doj_submission\n        user: ${DOJ_DB_USER:root}\n        pwd: ${DOJ_DB_PWD:123}\n    mq:\n        host: ${DOJ_MQ_HOST:127.0.0.1}\n    swagger:\n        title: 沙箱接口文档\n        scan: com.decade.doj.submission.controller"
  },
  {
    "path": "DOJ-BE/submission-service/src/main/resources/bootstrap.yaml",
    "content": "spring:\n    application:\n        name: submission-service\n    profiles:\n        active: dev, common\n    cloud:\n        nacos:\n            server-addr: ${NACOS_SERVER_ADDR:127.0.0.1:8848}\n            config:\n                file-extension: yaml\n                shared-configs:\n                    - dataId: shared-jdbc.yaml\n                    - dataId: shared-swagger.yaml\n                    - dataId: shared-jwt.yaml\n                    - dataId: shared-rabbitmq.yaml"
  },
  {
    "path": "DOJ-BE/submission-service/src/main/resources/com/decade/doj/submission/mapper/SubmissionMapper.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n<!DOCTYPE mapper\r\n        PUBLIC \"-//mybatis.org//DTD Mapper 3.0//EN\"\r\n        \"http://mybatis.org/dtd/mybatis-3-mapper.dtd\">\r\n<mapper namespace=\"com.decade.doj.submission.mapper.SubmissionMapper\">\r\n\r\n    <resultMap id=\"BaseResultMap\" type=\"com.decade.doj.submission.domain.po.Submission\">\r\n            <id property=\"id\" column=\"id\" />\r\n            <result property=\"userId\" column=\"user_id\" />\r\n            <result property=\"problemId\" column=\"problem_id\" />\r\n            <result property=\"language\" column=\"language\" />\r\n            <result property=\"code\" column=\"code\" />\r\n            <result property=\"exitValue\" column=\"exit_value\" />\r\n            <result property=\"status\" column=\"status\" />\r\n            <result property=\"message\" column=\"message\" />\r\n            <result property=\"time\" column=\"time\" />\r\n            <result property=\"memory\" column=\"memory\" />\r\n            <result property=\"submitTime\" column=\"submit_time\" />\r\n    </resultMap>\r\n\r\n    <sql id=\"Base_Column_List\">\r\n        id,user_id,problem_id,language,code,exit_value,\n        status,message,time,memory,submit_time\r\n    </sql>\r\n</mapper>\r\n"
  },
  {
    "path": "DOJ-BE/submission-service/src/test/java/com/decade/doj/submission/SubmissionTest.java",
    "content": "package com.decade.doj.submission;\n\nimport com.decade.doj.submission.domain.po.Submission;\nimport com.decade.doj.submission.service.ISubmissionService;\nimport org.junit.jupiter.api.Test;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.boot.test.context.SpringBootTest;\n\nimport java.util.Date;\n\n@SpringBootTest\npublic class SubmissionTest {\n\n    @Autowired\n    private ISubmissionService submissionService;\n\n    @Test\n    public void testSubmissionService() {\n        Submission submission = new Submission();\n        submission.setUserId(3L);\n        submission.setProblemId(1L);\n        submission.setLanguage(\"Java\");\n        submission.setCode(\"public class Solution { public int add(int a, int b) { return a + b; } }\");\n        submission.setStatus(\"Wrong\");\n        submission.setMessage(\"Compilation Error\");\n        submission.setMemory(12L);\n        submission.setTime(100.0);\n        submission.setSubmitTime(new Date());\n\n        boolean result = submissionService.save(submission);\n        assert result : \"Submission should be saved successfully\";\n    }\n}\n"
  },
  {
    "path": "DOJ-BE/user-service/Dockerfile",
    "content": "# Stage 1: Build the application\nFROM maven:3.8.4-openjdk-17 AS build\n\nWORKDIR /app\n\n# Copy all pom.xml files first to leverage Docker cache\nCOPY DOJ-BE/pom.xml .\nCOPY DOJ-BE/common/pom.xml ./common/\nCOPY DOJ-BE/gateway-service/pom.xml ./gateway-service/\nCOPY DOJ-BE/problem-service/pom.xml ./problem-service/\nCOPY DOJ-BE/sandbox-service/pom.xml ./sandbox-service/\nCOPY DOJ-BE/submission-service/pom.xml ./submission-service/\nCOPY DOJ-BE/user-service/pom.xml ./user-service/\n\n# Download all dependencies\nRUN mvn dependency:go-offline\n\n# Copy all source code\nCOPY DOJ-BE/common/src ./common/src\nCOPY DOJ-BE/user-service/src ./user-service/src\n\n# Build the specific service\nRUN mvn -f ./pom.xml -pl user-service -am clean package -DskipTests\nRUN ls -al ./user-service/target\n\n# Stage 2: Create the runtime image\nFROM eclipse-temurin:17-jre\n\nWORKDIR /app\n\n# Copy the executable jar from the build stage\nCOPY --from=build /app/user-service/target/user-service.jar ./app.jar\n\nEXPOSE 8081\n\nENTRYPOINT [\"java\", \"-jar\", \"./app.jar\"]"
  },
  {
    "path": "DOJ-BE/user-service/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n         xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n    <parent>\n        <groupId>com.decade</groupId>\n        <artifactId>DOJ-BE</artifactId>\n        <version>1.0-SNAPSHOT</version>\n    </parent>\n\n    <artifactId>user-service</artifactId>\n\n    <properties>\n        <maven.compiler.source>17</maven.compiler.source>\n        <maven.compiler.target>17</maven.compiler.target>\n        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>\n    </properties>\n\n    <dependencies>\n        <!--common-->\n        <dependency>\n            <groupId>com.decade</groupId>\n            <artifactId>common</artifactId>\n            <version>1.0-SNAPSHOT</version>\n        </dependency>\n        <!--web-->\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-web</artifactId>\n        </dependency>\n        <!--数据库-->\n        <dependency>\n            <groupId>mysql</groupId>\n            <artifactId>mysql-connector-java</artifactId>\n        </dependency>\n        <!--mybatis-->\n        <dependency>\n            <groupId>com.baomidou</groupId>\n            <artifactId>mybatis-plus-boot-starter</artifactId>\n        </dependency>\n        <!--单元测试-->\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-test</artifactId>\n        </dependency>\n<!--        &lt;!&ndash;redis&ndash;&gt;-->\n<!--        <dependency>-->\n<!--            <groupId>org.springframework.boot</groupId>-->\n<!--            <artifactId>spring-boot-starter-data-redis</artifactId>-->\n<!--        </dependency>-->\n        <!--nacos 服务注册发现-->\n        <dependency>\n            <groupId>com.alibaba.cloud</groupId>\n            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>\n        </dependency>\n<!--        <dependency>-->\n<!--            <groupId>org.mybatis</groupId>-->\n<!--            <artifactId>mybatis-spring</artifactId>-->\n<!--            <version>3.0.2</version>-->\n<!--        </dependency>-->\n    </dependencies>\n    <build>\n        <finalName>${project.artifactId}</finalName>\n        <plugins>\n            <plugin>\n                <groupId>org.springframework.boot</groupId>\n                <artifactId>spring-boot-maven-plugin</artifactId>\n            </plugin>\n        </plugins>\n    </build>\n\n</project>"
  },
  {
    "path": "DOJ-BE/user-service/src/main/java/com/decade/doj/user/UserApplication.java",
    "content": "package com.decade.doj.user;\n\nimport com.decade.doj.common.config.custom.DefaultFeignConfig;\nimport com.decade.doj.common.config.custom.JwtTool;\nimport com.decade.doj.common.config.custom.MVCConfig;\nimport com.decade.doj.common.config.custom.MybatisConfig;\nimport com.decade.doj.common.config.properties.AppNameProperties;\nimport com.decade.doj.common.interceptor.AdminCheckInterceptor;\nimport com.decade.doj.common.interceptor.IdentityInterceptor;\nimport org.mybatis.spring.annotation.MapperScan;\nimport org.springframework.boot.SpringApplication;\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\nimport org.springframework.cloud.openfeign.EnableFeignClients;\nimport org.springframework.context.annotation.Import;\n\n@SpringBootApplication\n@MapperScan(\"com.decade.doj.user.mapper\")\n@EnableFeignClients(basePackages = \"com.decade.doj.common.client\", defaultConfiguration = DefaultFeignConfig.class)\n@Import({JwtTool.class, MVCConfig.class, MybatisConfig.class, IdentityInterceptor.class, AppNameProperties.class, AdminCheckInterceptor.class})\npublic class UserApplication {\n    public static void main(String[] args) {\n        SpringApplication.run(UserApplication.class, args);\n    }\n}\n"
  },
  {
    "path": "DOJ-BE/user-service/src/main/java/com/decade/doj/user/controller/AnnouncementController.java",
    "content": "package com.decade.doj.user.controller;\n\nimport com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;\nimport com.decade.doj.common.annotation.AdminRequired;\nimport com.decade.doj.common.domain.R;\nimport com.decade.doj.user.domain.po.Announcement;\nimport com.decade.doj.user.service.IAnnouncementService;\nimport io.swagger.v3.oas.annotations.Operation;\nimport io.swagger.v3.oas.annotations.tags.Tag;\nimport lombok.RequiredArgsConstructor;\nimport org.springframework.web.bind.annotation.*;\n\nimport java.time.LocalDateTime;\nimport java.util.List;\n\n@RestController\n@RequestMapping(\"/user/announcement\")\n@Tag(name = \"公告接口\")\n@RequiredArgsConstructor\npublic class AnnouncementController {\n\n    private final IAnnouncementService announcementService;\n\n    @GetMapping(\"/list\")\n    @Operation(summary = \"获取公告列表\")\n    public R<List<Announcement>> list() {\n        return R.ok(announcementService.list(new LambdaQueryWrapper<Announcement>()\n                .eq(Announcement::getDeleted, false)\n                .orderByDesc(Announcement::getCreateTime)));\n    }\n\n    // Admin endpoints (simplified, assuming standard auth/admin check or internal use if gateway handles it)\n    // For now, I'll allow creation to support the requirement\n    \n    @PostMapping\n    @AdminRequired\n    @Operation(summary = \"新增公告\")\n    public R<Void> create(@RequestBody Announcement announcement) {\n        announcement.setCreateTime(LocalDateTime.now());\n        announcement.setUpdateTime(LocalDateTime.now());\n        announcementService.save(announcement);\n        return R.ok();\n    }\n}\n"
  },
  {
    "path": "DOJ-BE/user-service/src/main/java/com/decade/doj/user/controller/UserController.java",
    "content": "package com.decade.doj.user.controller;\n\n\nimport cn.hutool.core.bean.BeanUtil;\nimport com.decade.doj.common.config.properties.ResourceProperties;\nimport com.decade.doj.common.domain.PageQueryDTO;\nimport com.decade.doj.common.domain.PageDTO;\nimport com.decade.doj.common.domain.R;\nimport com.decade.doj.common.domain.vo.StatsVO;\nimport com.decade.doj.user.domain.dto.LoginDTO;\nimport com.decade.doj.user.domain.dto.RegisterDTO;\nimport com.decade.doj.user.domain.dto.UpdPwdDTO;\nimport com.decade.doj.user.domain.po.User;\nimport com.decade.doj.user.domain.vo.InfoVO;\nimport com.decade.doj.user.domain.vo.LoginVO;\nimport com.decade.doj.user.domain.vo.RankVO;\nimport com.decade.doj.user.service.IUserService;\nimport io.swagger.v3.oas.annotations.Operation;\nimport io.swagger.v3.oas.annotations.tags.Tag;\nimport lombok.RequiredArgsConstructor;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.validation.annotation.Validated;\nimport org.springframework.web.bind.annotation.*;\nimport org.springframework.web.multipart.MultipartFile;\n\nimport javax.validation.constraints.NotNull;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.util.UUID;\n\n/**\n * <p>\n *  前端控制器\n * </p>\n *\n * @author \n * @since 2024-08-26\n */\n@RestController\n@RequestMapping(\"/user\")\n@Tag(name = \"用户相关接口\")\n@Slf4j\n@RequiredArgsConstructor\npublic class UserController {\n\n    private final IUserService userService;\n\n    private final ResourceProperties resourceProperties;\n\n    @PutMapping(\"/pwd\")\n    @Operation(summary = \"修改密码接口\")\n    public R updatePwd(@RequestBody @Validated UpdPwdDTO updPwdDTO) {\n        return userService.updatePwd(updPwdDTO);\n    }\n\n    @PostMapping(\"/avatar\")\n    @Operation(summary = \"上传头像接口\")\n    public R<String> uploadAvatar(@RequestParam(\"file\") MultipartFile file) {\n        if (file.isEmpty()) {\n            return R.error(\"文件为空!\");\n        }\n        String filename = UUID.randomUUID() + file.getOriginalFilename();\n        Path path = Paths.get(resourceProperties.getLocation() + filename);\n\n        try {\n            file.transferTo(path);\n        } catch (Exception e) {\n            return R.error(\"文件上传失败!\");\n        }\n\n        return R.ok(resourceProperties.getRequest()+filename);\n    }\n\n    @PostMapping(\"/register\")\n    @Operation(summary = \"用户注册接口\")\n    public R register(@RequestBody @Validated RegisterDTO registerDTO) {\n        return userService.register(registerDTO);\n    }\n\n    @PostMapping(\"/login\")\n    @Operation(summary = \"用户登录接口\")\n    public R<LoginVO> login(@RequestBody @Validated LoginDTO loginDTO) {\n        return userService.login(loginDTO);\n    }\n\n    @PostMapping(\"/refresh\")\n    @Operation(summary = \"刷新令牌接口\")\n    public R<String> refreshToken(@RequestHeader(\"Authorization\") String refreshToken) {\n        return userService.refreshToken(refreshToken);\n    }\n\n    @GetMapping(\"/rankings\")\n    @Operation(summary = \"获取排行榜\")\n    public R<PageDTO<RankVO>> getRankings(PageQueryDTO pageQueryDTO) {\n        return userService.getRankings(pageQueryDTO);\n    }\n\n    @GetMapping(\"/stats\")\n    @Operation(summary = \"获取首页统计数据\")\n    public R<StatsVO> getStats() {\n        return R.ok(userService.getStats());\n    }\n\n    @GetMapping(\"/{id}\")\n    @Operation(summary = \"查询用户接口\")\n    public R<InfoVO> getUser(@PathVariable(\"id\") @NotNull Long id) {\n        User user = userService.getById(id);\n        if (user == null) {\n            return R.error(\"用户不存在!\");\n        }\n        InfoVO infoVO = BeanUtil.copyProperties(user, InfoVO.class);\n        return R.ok(infoVO);\n    }\n\n    @PutMapping()\n    @Operation(summary = \"修改用户接口\")\n    public R updateUser(@RequestBody User user) {\n        return userService.updateUser(user);\n    }\n\n}\n\n"
  },
  {
    "path": "DOJ-BE/user-service/src/main/java/com/decade/doj/user/domain/dto/LoginDTO.java",
    "content": "package com.decade.doj.user.domain.dto;\n\nimport io.swagger.v3.oas.annotations.media.Schema;\nimport lombok.Data;\nimport lombok.experimental.Accessors;\n\nimport javax.validation.constraints.NotBlank;\n\n@Data\n@Accessors(chain = true)\n@Schema(description = \"登录信息\")\npublic class LoginDTO {\n\n    @Schema(description = \"用户名\", required = true)\n    @NotBlank(message = \"用户名不能为空\")\n    private String username;\n\n    @Schema(description = \"密码\", required = true)\n    @NotBlank(message = \"密码不能为空\")\n    private String password;\n\n}\n"
  },
  {
    "path": "DOJ-BE/user-service/src/main/java/com/decade/doj/user/domain/dto/RegisterDTO.java",
    "content": "package com.decade.doj.user.domain.dto;\n\nimport io.swagger.v3.oas.annotations.media.Schema;\nimport lombok.Data;\nimport lombok.experimental.Accessors;\n\nimport javax.validation.constraints.Email;\nimport javax.validation.constraints.NotBlank;\n\n@Data\n@Accessors(chain = true)\n@Schema(description = \"注册信息\")\npublic class RegisterDTO {\n\n    @Schema(description = \"用户名\", required = true)\n    @NotBlank(message = \"用户名不能为空\")\n    private String username;\n\n    @Schema(description = \"密码\", required = true)\n    @NotBlank(message = \"密码不能为空\")\n    private String password;\n\n    @Schema(description = \"邮箱\", required = true)\n    @Email(message = \"邮箱格式不正确\")\n    private String email;\n\n    @Schema(description = \"签名\")\n    private String sign;\n\n}\n"
  },
  {
    "path": "DOJ-BE/user-service/src/main/java/com/decade/doj/user/domain/dto/UpdPwdDTO.java",
    "content": "package com.decade.doj.user.domain.dto;\n\nimport io.swagger.v3.oas.annotations.media.Schema;\nimport lombok.Data;\nimport lombok.experimental.Accessors;\n\nimport javax.validation.constraints.NotBlank;\n\n@Data\n@Accessors(chain = true)\n@Schema(description = \"修改密码DTO\")\npublic class UpdPwdDTO {\n\n    @Schema(description = \"旧密码\", required = true)\n    @NotBlank(message = \"旧密码不能为空\")\n    private String oldPassword;\n\n    @Schema(description = \"新密码\", required = true)\n    @NotBlank(message = \"新密码不能为空\")\n    private String newPassword;\n\n}\n"
  },
  {
    "path": "DOJ-BE/user-service/src/main/java/com/decade/doj/user/domain/po/Announcement.java",
    "content": "package com.decade.doj.user.domain.po;\n\nimport com.baomidou.mybatisplus.annotation.IdType;\nimport com.baomidou.mybatisplus.annotation.TableField;\nimport com.baomidou.mybatisplus.annotation.TableId;\nimport com.baomidou.mybatisplus.annotation.TableName;\nimport lombok.Data;\nimport lombok.EqualsAndHashCode;\nimport lombok.experimental.Accessors;\n\nimport java.time.LocalDateTime;\n\n/**\n * <p>\n * 公告表\n * </p>\n *\n * @author Antigravity\n * @since 2026-01-24\n */\n@Data\n@EqualsAndHashCode(callSuper = false)\n@Accessors(chain = true)\n@TableName(\"announcement\")\npublic class Announcement {\n\n    @TableId(value = \"id\", type = IdType.AUTO)\n    private Long id;\n\n    private String title;\n\n    private String content;\n\n    @TableField(\"create_time\")\n    private LocalDateTime createTime;\n\n    @TableField(\"update_time\")\n    private LocalDateTime updateTime;\n\n    @TableField(\"creator_id\")\n    private Long creatorId;\n\n    private Boolean deleted = false;\n}\n"
  },
  {
    "path": "DOJ-BE/user-service/src/main/java/com/decade/doj/user/domain/po/User.java",
    "content": "package com.decade.doj.user.domain.po;\n\nimport com.baomidou.mybatisplus.annotation.TableName;\nimport com.baomidou.mybatisplus.annotation.IdType;\nimport com.baomidou.mybatisplus.annotation.TableId;\n\nimport lombok.Data;\nimport lombok.EqualsAndHashCode;\nimport lombok.experimental.Accessors;\n\n/**\n * <p>\n * \n * </p>\n *\n * @author \n * @since 2024-08-26\n */\n@Data\n@EqualsAndHashCode(callSuper = false)\n@Accessors(chain = true)\n@TableName(\"user\")\npublic class User {\n\n    @TableId(value = \"id\", type = IdType.AUTO)\n    private Long id;\n\n    private String username;\n\n    private String avatar = \"\";\n\n    private String email;\n\n    private String password;\n\n    private Integer score = 0;\n\n    private Integer ranks = 0;\n\n    private String school = \"\";\n\n    private Boolean gender = true;\n\n    private Integer easySolve = 0;\n\n    private Integer middleSolve = 0;\n\n    private Integer hardSolve = 0;\n\n    private Boolean role = true;\n\n    private String url = \"\";\n\n    private String sign = \"这个人很懒，什么都没留下\";\n\n    private Long fans = 0L;\n\n    private Long subscribe = 0L;\n\n    private Boolean ban = false;\n\n\n}\n"
  },
  {
    "path": "DOJ-BE/user-service/src/main/java/com/decade/doj/user/domain/vo/InfoVO.java",
    "content": "package com.decade.doj.user.domain.vo;\n\nimport com.decade.doj.user.domain.po.User;\nimport lombok.Data;\nimport lombok.EqualsAndHashCode;\nimport lombok.experimental.Accessors;\n\n@EqualsAndHashCode(callSuper = true)\n@Data\n@Accessors(chain = true)\npublic class InfoVO extends User {\n\n}\n"
  },
  {
    "path": "DOJ-BE/user-service/src/main/java/com/decade/doj/user/domain/vo/LoginVO.java",
    "content": "package com.decade.doj.user.domain.vo;\n\nimport lombok.Data;\nimport lombok.experimental.Accessors;\n\n@Data\n@Accessors(chain = true)\npublic class LoginVO {\n\n    private String accessToken;\n    private String refreshToken;\n    private Long userId;\n    private String username;\n\n}"
  },
  {
    "path": "DOJ-BE/user-service/src/main/java/com/decade/doj/user/domain/vo/RankVO.java",
    "content": "package com.decade.doj.user.domain.vo;\n\nimport lombok.Data;\n\n@Data\npublic class RankVO {\n    private Long rank;\n    private Long userId;\n    private String username;\n    private String avatar;\n    private Integer score;\n    private Integer easySolve;\n    private Integer middleSolve;\n    private Integer hardSolve;\n    private String mostUsedLanguage;\n}\n"
  },
  {
    "path": "DOJ-BE/user-service/src/main/java/com/decade/doj/user/mapper/AnnouncementMapper.java",
    "content": "package com.decade.doj.user.mapper;\n\nimport com.baomidou.mybatisplus.core.mapper.BaseMapper;\nimport com.decade.doj.user.domain.po.Announcement;\n\n/**\n * <p>\n * 公告表 Mapper 接口\n * </p>\n *\n * @author Antigravity\n * @since 2026-01-24\n */\npublic interface AnnouncementMapper extends BaseMapper<Announcement> {\n\n}\n"
  },
  {
    "path": "DOJ-BE/user-service/src/main/java/com/decade/doj/user/mapper/UserMapper.java",
    "content": "package com.decade.doj.user.mapper;\n\nimport com.decade.doj.user.domain.po.User;\nimport com.baomidou.mybatisplus.core.mapper.BaseMapper;\nimport org.apache.ibatis.annotations.Select;\n\n/**\n * <p>\n *  Mapper 接口\n * </p>\n *\n * @author \n * @since 2024-08-26\n */\npublic interface UserMapper extends BaseMapper<User> {\n\n    @Select(\"select id, username, sign, easy_solve from user where id = #{id}\")\n    User chooseById(Integer id);\n\n}\n"
  },
  {
    "path": "DOJ-BE/user-service/src/main/java/com/decade/doj/user/mq/MqConfig.java",
    "content": "package com.decade.doj.user.mq;\n\nimport org.springframework.amqp.core.Binding;\nimport org.springframework.amqp.core.BindingBuilder;\nimport org.springframework.amqp.core.Queue;\nimport org.springframework.amqp.core.TopicExchange;\nimport org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;\nimport org.springframework.amqp.support.converter.MessageConverter;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\n\n@Configuration\npublic class MqConfig {\n\n    @Bean\n    public TopicExchange topicExchange() {\n        return new TopicExchange(\"doj.topic\");\n    }\n\n    @Bean\n    public Queue statsUpdateQueue() {\n        return new Queue(\"stats.update.queue\");\n    }\n\n    @Bean\n    public Binding binding() {\n        return BindingBuilder.bind(statsUpdateQueue()).to(topicExchange()).with(\"problem.solved\");\n    }\n\n    @Bean\n    public MessageConverter jsonMessageConverter(){\n        return new Jackson2JsonMessageConverter();\n    }\n}\n"
  },
  {
    "path": "DOJ-BE/user-service/src/main/java/com/decade/doj/user/mq/StatsUpdateListener.java",
    "content": "package com.decade.doj.user.mq;\n\nimport com.decade.doj.common.utils.UserContext;\nimport com.decade.doj.user.service.IUserService;\nimport lombok.RequiredArgsConstructor;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.amqp.rabbit.annotation.RabbitListener;\nimport org.springframework.stereotype.Component;\n\nimport java.util.Map;\n\n@Slf4j\n@Component\n@RequiredArgsConstructor\npublic class StatsUpdateListener {\n\n    private final IUserService userService;\n\n    @RabbitListener(queues = \"stats.update.queue\")\n    public void listenStatsUpdateQueue(Map<String, Long> message) {\n        if (message == null) {\n            return;\n        }\n        Long userId = message.get(\"userId\");\n        Long problemId = message.get(\"problemId\");\n        if (userId == null || problemId == null) {\n            log.error(\"接收到无效的消息：{}\", message);\n            return;\n        }\n        log.info(\"接收到用户解题消息，开始更新用户统计数据。userId: {}, problemId: {}\", userId, problemId);\n        // 调用业务方法处理\n        userService.handleProblemSolved(userId, problemId);\n    }\n}\n"
  },
  {
    "path": "DOJ-BE/user-service/src/main/java/com/decade/doj/user/service/IAnnouncementService.java",
    "content": "package com.decade.doj.user.service;\n\nimport com.baomidou.mybatisplus.extension.service.IService;\nimport com.decade.doj.user.domain.po.Announcement;\n\n/**\n * <p>\n * 公告表 服务类\n * </p>\n *\n * @author Antigravity\n * @since 2026-01-24\n */\npublic interface IAnnouncementService extends IService<Announcement> {\n\n}\n"
  },
  {
    "path": "DOJ-BE/user-service/src/main/java/com/decade/doj/user/service/IUserService.java",
    "content": "package com.decade.doj.user.service;\n\nimport com.decade.doj.common.domain.PageDTO;\nimport com.decade.doj.common.domain.PageQueryDTO;\nimport com.decade.doj.common.domain.R;\nimport com.decade.doj.common.domain.vo.StatsVO;\nimport com.decade.doj.user.domain.dto.LoginDTO;\nimport com.decade.doj.user.domain.dto.RegisterDTO;\nimport com.decade.doj.user.domain.dto.UpdPwdDTO;\nimport com.decade.doj.user.domain.po.User;\nimport com.baomidou.mybatisplus.extension.service.IService;\nimport com.decade.doj.user.domain.vo.LoginVO;\nimport com.decade.doj.user.domain.vo.RankVO;\n\n/**\n * <p>\n *  服务类\n * </p>\n *\n * @author \n * @since 2024-08-26\n */\npublic interface IUserService extends IService<User> {\n\n    R<LoginVO> login(LoginDTO loginDTO);\n\n    R<String> refreshToken(String refreshToken);\n\n    R register(RegisterDTO registerDTO);\n\n    R updateUser(User user);\n\n    R updatePwd(UpdPwdDTO updPwdDTO);\n\n    R<PageDTO<RankVO>> getRankings(PageQueryDTO pageQueryDTO);\n\n    void handleProblemSolved(Long userId, Long problemId);\n\n    StatsVO getStats();\n}\n"
  },
  {
    "path": "DOJ-BE/user-service/src/main/java/com/decade/doj/user/service/impl/AnnouncementServiceImpl.java",
    "content": "package com.decade.doj.user.service.impl;\n\nimport com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;\nimport com.decade.doj.user.domain.po.Announcement;\nimport com.decade.doj.user.mapper.AnnouncementMapper;\nimport com.decade.doj.user.service.IAnnouncementService;\nimport org.springframework.stereotype.Service;\n\n/**\n * <p>\n * 公告表 服务实现类\n * </p>\n *\n * @author Antigravity\n * @since 2026-01-24\n */\n@Service\npublic class AnnouncementServiceImpl extends ServiceImpl<AnnouncementMapper, Announcement> implements IAnnouncementService {\n\n}\n"
  },
  {
    "path": "DOJ-BE/user-service/src/main/java/com/decade/doj/user/service/impl/UserServiceImpl.java",
    "content": "package com.decade.doj.user.service.impl;\n\nimport com.decade.doj.common.client.ProblemClient;\nimport com.decade.doj.common.client.SubmissionClient;\nimport com.decade.doj.common.config.properties.JwtProperties;\nimport com.decade.doj.common.domain.PageDTO;\nimport com.decade.doj.common.domain.PageQueryDTO;\nimport com.decade.doj.common.domain.R;\nimport com.decade.doj.common.domain.po.Problem;\nimport com.decade.doj.common.domain.vo.StatsVO;\nimport com.decade.doj.common.domain.vo.SubmissionStatsVO;\nimport com.decade.doj.common.exception.BadRequestException;\nimport com.decade.doj.common.exception.ForbiddenException;\nimport com.decade.doj.common.config.custom.JwtTool;\nimport com.decade.doj.common.exception.UnauthorizedException;\nimport com.decade.doj.common.utils.UserContext;\nimport com.decade.doj.user.domain.dto.LoginDTO;\nimport com.decade.doj.user.domain.dto.RegisterDTO;\nimport com.decade.doj.user.domain.dto.UpdPwdDTO;\nimport com.decade.doj.user.domain.vo.LoginVO;\nimport com.decade.doj.user.domain.vo.RankVO;\nimport com.decade.doj.user.mapper.UserMapper;\nimport com.decade.doj.user.domain.po.User;\nimport com.decade.doj.user.service.IUserService;\nimport com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;\nimport com.decade.doj.common.config.properties.AppNameProperties;\nimport com.decade.doj.user.utils.AESTool;\nimport lombok.RequiredArgsConstructor;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.data.redis.core.StringRedisTemplate;\nimport org.springframework.data.redis.core.ZSetOperations;\nimport org.springframework.stereotype.Service;\n\nimport javax.annotation.PostConstruct;\nimport java.util.*;\nimport java.util.concurrent.TimeUnit;\nimport java.util.stream.Collectors;\nimport java.util.Random;\n\n/**\n * <p>\n *  服务实现类\n * </p>\n *\n * @author \n * @since 2024-08-26\n */\n@Service\n@RequiredArgsConstructor\n@Slf4j\npublic class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {\n\n    private final AESTool aesTool;\n    private final JwtTool jwtTool;\n    private final JwtProperties jwtProperties;\n    private final StringRedisTemplate redisTemplate;\n    private final AppNameProperties appNameProperties;\n    private final ProblemClient problemClient;\n    private final SubmissionClient submissionClient;\n\n    @PostConstruct\n    public void initRankings() {\n        // 服务启动时，将所有用户数据同步到 Redis 排行榜\n        List<User> users = this.list();\n        if (users.isEmpty()) {\n            return;\n        }\n        Set<ZSetOperations.TypedTuple<String>> tuples = users.stream()\n                .map(user -> ZSetOperations.TypedTuple.of(user.getId().toString(), user.getScore().doubleValue()))\n                .collect(Collectors.toSet());\n        redisTemplate.opsForZSet().add(getRankingKey(), tuples);\n        log.info(\"用户排行榜数据已同步到 Redis。\");\n    }\n\n    private String getRedisKeyPrefix() {\n        return appNameProperties.getName() + \":refresh_token:\";\n    }\n\n    private String getRankingKey() {\n        return appNameProperties.getName() + \":ranks\";\n    }\n\n    @Override\n    public R<LoginVO> login(LoginDTO loginDTO) {\n        String username = loginDTO.getUsername();\n        String password = loginDTO.getPassword();\n        User user = lambdaQuery().eq(User::getUsername, username).one();\n        if (user == null) {\n            throw new BadRequestException(\"用户名错误\");\n        }\n        if (!aesTool.match(password, user.getPassword())) {\n            throw new BadRequestException(\"密码错误\");\n        }\n        if (user.getBan()) {\n            throw new ForbiddenException(\"用户被冻结\");\n        }\n        // 1. 生成访问令牌 (Access Token)\n        String accessToken = jwtTool.createToken(user.getId(), jwtProperties.getTokenTTL());\n\n        // 2. 生成刷新令牌 (Refresh Token)\n        String refreshToken = UUID.randomUUID().toString();\n\n        // 3. 将 Refresh Token 存入 Redis\n        String redisKey = getRedisKeyPrefix() + refreshToken;\n        redisTemplate.opsForValue().set(redisKey, user.getId().toString(), jwtProperties.getRefreshTokenTTL());\n\n        // 4. 封装并返回双令牌\n        LoginVO loginVO = new LoginVO()\n                .setAccessToken(accessToken)\n                .setRefreshToken(refreshToken)\n                .setUserId(user.getId())\n                .setUsername(user.getUsername());\n        return R.ok(loginVO);\n    }\n\n    @Override\n    public R<String> refreshToken(String refreshToken) {\n        String redisKey = getRedisKeyPrefix() + refreshToken;\n        String userIdStr = redisTemplate.opsForValue().get(redisKey);\n\n        if (userIdStr == null) {\n            throw new UnauthorizedException(\"无效的刷新令牌\");\n        }\n\n        Long userId = Long.valueOf(userIdStr);\n        String accessToken = jwtTool.createToken(userId, jwtProperties.getTokenTTL());\n        return R.ok(accessToken);\n    }\n\n    @Override\n    public R<PageDTO<RankVO>> getRankings(PageQueryDTO pageQueryDTO) {\n        long start = (long) (pageQueryDTO.getPageNo() - 1) * pageQueryDTO.getPageSize();\n        long end = start + pageQueryDTO.getPageSize() - 1;\n\n        // 1. 从 Redis 的 Sorted Set 中获取排名和分数\n        Set<ZSetOperations.TypedTuple<String>> tuples = redisTemplate.opsForZSet().reverseRangeWithScores(getRankingKey(), start, end);\n        if (tuples == null || tuples.isEmpty()) {\n            return R.ok(PageDTO.empty(0L, 0L));\n        }\n\n        // 2. 提取用户ID并批量查询用户信息\n        List<Long> userIds = tuples.stream().map(tuple -> Long.valueOf(Objects.requireNonNull(tuple.getValue()))).toList();\n        Map<Long, User> userMap = this.listByIds(userIds).stream().collect(Collectors.toMap(User::getId, user -> user));\n\n        // 3. 组装VO\n        List<RankVO> rankVOs = new ArrayList<>();\n        long currentRank = start + 1;\n        List<String> languages = List.of(\"cpp\", \"java\", \"python\");\n        Random random = new Random();\n\n        for (ZSetOperations.TypedTuple<String> tuple : tuples) {\n            Long userId = Long.valueOf(Objects.requireNonNull(tuple.getValue()));\n            User user = userMap.get(userId);\n            if (user != null) {\n                RankVO rankVO = new RankVO();\n                rankVO.setRank(currentRank++);\n                rankVO.setUserId(user.getId());\n                rankVO.setUsername(user.getUsername());\n                rankVO.setAvatar(user.getAvatar());\n                rankVO.setScore(user.getScore());\n                rankVO.setEasySolve(user.getEasySolve());\n                rankVO.setMiddleSolve(user.getMiddleSolve());\n                rankVO.setHardSolve(user.getHardSolve());\n                // 随机设置常用语言用于前端展示\n                rankVO.setMostUsedLanguage(languages.get(random.nextInt(languages.size())));\n                rankVOs.add(rankVO);\n            }\n        }\n\n        Long total = redisTemplate.opsForZSet().zCard(getRankingKey());\n        long pages = (total + pageQueryDTO.getPageSize() - 1) / pageQueryDTO.getPageSize();\n\n        return R.ok(PageDTO.fullPage(total, pages, rankVOs));\n    }\n\n    @Override\n    public void handleProblemSolved(Long userId, Long problemId) {\n        UserContext.setCurrentUser(userId);\n        Problem problem = problemClient.getProblemById(problemId).getData();\n        if (problem == null) return;\n\n        User user = this.getById(userId);\n        if (user == null) return;\n\n        int scoreChange = switch (problem.getDifficulty()) {\n            case \"简单\" -> {\n                user.setEasySolve(user.getEasySolve() + 1);\n                yield 500;\n            }\n            case \"中等\" -> {\n                user.setMiddleSolve(user.getMiddleSolve() + 1);\n                yield 1000;\n            }\n            case \"困难\" -> {\n                user.setHardSolve(user.getHardSolve() + 1);\n                yield 2000;\n            }\n            default -> 0;\n        };\n\n        user.setScore(user.getScore() + scoreChange);\n        this.updateById(user);\n        redisTemplate.opsForZSet().add(getRankingKey(), user.getId().toString(), user.getScore());\n        log.info(\"用户 {} 分数及统计数据更新成功\", userId);\n    }\n\n    @Override\n    public R register(RegisterDTO registerDTO) {\n        String username = registerDTO.getUsername();\n        String password = registerDTO.getPassword();\n        String email = registerDTO.getEmail();\n        String signature = registerDTO.getSign();\n        boolean exist = lambdaQuery().eq(User::getUsername, username).exists();\n        if (exist) {\n            throw new BadRequestException(\"用户名已存在\");\n        }\n        User user = new User()\n                .setUsername(username)\n                .setPassword(aesTool.encode(password, aesTool.fnv1aHash(password)))\n                .setEmail(email)\n                .setSign(signature);\n        save(user);\n        // 将新用户同步到排行榜\n        redisTemplate.opsForZSet().add(getRankingKey(), user.getId().toString(), 0.0);\n        return R.ok();\n    }\n\n    @Override\n    public R updatePwd(UpdPwdDTO updPwdDTO) {\n        String oldPassword = updPwdDTO.getOldPassword();\n        String newPassword = updPwdDTO.getNewPassword();\n        User user = getById(UserContext.getCurrentUser());\n        if (user == null) {\n            return R.error(\"用户不存在!\");\n        }\n        if (!aesTool.match(oldPassword, user.getPassword())) {\n            return R.error(\"原密码错误!\");\n        }\n        user.setPassword(aesTool.encode(newPassword, aesTool.fnv1aHash(newPassword)));\n        updateById(user);\n        return R.ok();\n    }\n\n    @Override\n    public R updateUser(User user) {\n        user.setId(UserContext.getCurrentUser());\n        User col = lambdaQuery().eq(User::getUsername, user.getUsername()).one();\n        if (col != null && !col.getId().equals(user.getId())) {\n            return R.error(\"用户名已存在!\");\n        }\n        user.setBan(null)\n                .setPassword(null)\n                .setScore(null)\n                .setRanks(null)\n                .setEasySolve(null)\n                .setMiddleSolve(null)\n                .setHardSolve(null)\n                .setRole(null)\n                .setFans(null)\n                .setSubscribe(null);\n        updateById(user);\n        return R.ok();\n    }\n\n    @Override\n    public StatsVO getStats() {\n        // 获取提交统计\n        R<SubmissionStatsVO> submissionStatsR = submissionClient.getStats();\n        SubmissionStatsVO submissionStats = submissionStatsR.success() ? submissionStatsR.getData() : new SubmissionStatsVO(0L, 0L);\n\n        // 获取题目总数\n        R<Long> problemCountR = problemClient.getCount();\n        Long problemCount = problemCountR.success() ? problemCountR.getData() : 0L;\n\n        // 获取用户总数 (活跃用户)\n        long activeUsers = this.count();\n\n        return StatsVO.builder()\n                .totalSubmissions(submissionStats.getTotalSubmissions())\n                .todaySubmissions(submissionStats.getTodaySubmissions())\n                .totalProblems(problemCount)\n                .activeUsers(activeUsers)\n                .build();\n    }\n}\n"
  },
  {
    "path": "DOJ-BE/user-service/src/main/java/com/decade/doj/user/utils/AESTool.java",
    "content": "package com.decade.doj.user.utils;\n\nimport com.decade.doj.common.exception.UnauthorizedException;\nimport org.springframework.stereotype.Component;\n\nimport javax.annotation.PostConstruct;\nimport javax.crypto.Cipher;\nimport javax.crypto.KeyGenerator;\nimport javax.crypto.NoSuchPaddingException;\nimport javax.crypto.SecretKey;\nimport javax.crypto.spec.SecretKeySpec;\nimport java.nio.charset.StandardCharsets;\nimport java.security.NoSuchAlgorithmException;\nimport java.security.SecureRandom;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.Base64;\nimport java.util.List;\n\n\n@Component\npublic class AESTool {\n\n    private final String AES_ECB = \"AES/ECB/PKCS5Padding\";\n\n    private final int KEY_SIZE = 128; // 128-bit key size\n    private final int NUM_KEYS = 8; // Number of random keys\n\n    private List<byte[]> candidateKeys = new ArrayList<>();\n\n    @PostConstruct\n    public void init() {\n        String[] keys = {\n                \"6A2B5E1D9F6C2A7E\",\n                \"3D5B6F4E9C7A1B2C\",\n                \"4E8A3D7B2F9C1D5A\",\n                \"7C2B4D1E6F9A5E8C\",\n                \"9F1A4B7E3C5D6B2A\",\n                \"2D7C9B4E1A5F6E3B\",\n                \"5A1C7E2B4F3D9C8A\",\n                \"8C3D1E7B6F5A9B2E\"\n        };\n        for (String key : keys) {\n            candidateKeys.add(key.getBytes(StandardCharsets.UTF_8));\n        }\n    }\n\n    public int fnv1aHash(String str) {\n        final int FNV_prime = 0x01000193;\n        final int FNV_offset_basis = 0x811c9dc5;\n        int hash = FNV_offset_basis;\n        for (int i = 0; i < str.length(); i++) {\n            hash ^= str.charAt(i);\n            hash *= FNV_prime;\n        }\n        return Math.abs(hash) % NUM_KEYS;\n    }\n\n    private String _encode(String data) {\n        int index = fnv1aHash(data);\n        byte[] key = candidateKeys.get(index);\n        SecretKeySpec secretKeySpec = new SecretKeySpec(key, \"AES\");\n        try {\n            Cipher cipher = Cipher.getInstance(AES_ECB);\n            cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec);\n            byte[] ret = cipher.doFinal(data.getBytes(StandardCharsets.UTF_8));\n            return Base64.getEncoder().encodeToString(ret);\n        } catch (Exception e) {\n            throw new UnauthorizedException(\"Failed to initialize AES cipher\", e);\n        }\n    }\n\n    public boolean match(String raw, String encoded) {\n        String ret = _encode(raw);\n        return ret.equals(encoded);\n    }\n\n    public String encode(String data, int index) {\n        byte[] key = candidateKeys.get(index);\n        SecretKeySpec secretKeySpec = new SecretKeySpec(key, \"AES\");\n        try {\n            Cipher cipher = Cipher.getInstance(AES_ECB);\n            cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec);\n            byte[] ret = cipher.doFinal(data.getBytes(StandardCharsets.UTF_8));\n            return Base64.getEncoder().encodeToString(ret);\n        } catch (Exception e) {\n            throw new UnauthorizedException(\"Failed to initialize AES cipher\", e);\n        }\n    }\n\n    public String decode(String data) {\n        int index = NUM_KEYS - 1;\n        byte[] key = candidateKeys.get(index);\n        SecretKeySpec secretKeySpec = new SecretKeySpec(key, \"AES\");\n        try {\n            Cipher cipher = Cipher.getInstance(AES_ECB);\n            cipher.init(Cipher.DECRYPT_MODE, secretKeySpec);\n            byte[] ret = cipher.doFinal(Base64.getDecoder().decode(data));\n            return new String(ret, StandardCharsets.UTF_8);\n        } catch (Exception e) {\n            throw new UnauthorizedException(\"Failed to initialize AES cipher\", e);\n        }\n    }\n\n}\n"
  },
  {
    "path": "DOJ-BE/user-service/src/main/resources/application.yaml",
    "content": "server:\n    port: ${doj.port.user-service}\n\nmanagement:\n  endpoints:\n    web:\n      exposure:\n        include: health,prometheus\n\ndoj:\n    db:\n        host: ${DOJ_DB_HOST:127.0.0.1}\n        name: doj_user\n        user: ${DOJ_DB_USER:root}\n        pwd: ${DOJ_DB_PWD:123}\n    mq:\n        host: ${DOJ_MQ_HOST:127.0.0.1}\n    redis:\n        host: ${DOJ_REDIS_HOST:127.0.0.1}\n    swagger:\n        title: 用户服务接口文档\n        scan: com.decade.doj.user.controller"
  },
  {
    "path": "DOJ-BE/user-service/src/main/resources/bootstrap.yaml",
    "content": "spring:\n    application:\n        name: user-service\n    profiles:\n        active: dev, common\n    cloud:\n        nacos:\n            server-addr: ${NACOS_SERVER_ADDR:127.0.0.1:8848}\n            config:\n                file-extension: yaml\n                shared-configs:\n                    - dataId: shared-jdbc.yaml\n                    - dataId: shared-swagger.yaml\n                    - dataId: shared-jwt.yaml\n                    - dataId: shared-rabbitmq.yaml\n\n# shared-jdbc.yaml\n#s\n\n# shared-swagger.yaml\n#logging:\n#    level:\n#        com.decade: debug\n#    pattern:\n#        dateformat: HH:mm:ss:SSS\n#knife4j:\n#    enable: true\n#    openapi:\n#        title: ${doj.swagger.title}\n#        description: \"Duck Online Judge API\"\n#        email: \"decade-qzj@foxmail.com\"\n#        version: v1.0.0\n#        group:\n#            default:\n#                group-name: default\n#                api-rule: package\n#                api-rule-resources:\n#                    - ${doj.swagger.scan}\n\n# shared-jwt.yaml\n#doj:\n#    jwt:\n#        location: classpath:doj.jks\n#        alias: decade\n#        password: doj123\n#        tokenTTL: 30m\n#        authorization: \"authorization\"\n#        secret-key: \"uid\""
  },
  {
    "path": "DOJ-BE/user-service/src/test/java/com/decade/doj/user/UserTest.java",
    "content": "package com.decade.doj.user;\n\nimport com.decade.doj.user.domain.po.User;\nimport com.decade.doj.user.mapper.UserMapper;\nimport com.decade.doj.user.service.IUserService;\nimport com.decade.doj.user.utils.AESTool;\nimport org.junit.jupiter.api.Test;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.boot.test.context.SpringBootTest;\n\nimport java.sql.SQLOutput;\n\n@SpringBootTest\npublic class UserTest {\n\n    @Autowired\n    private IUserService userService;\n\n    @Autowired\n    private UserMapper userMapper;\n\n    @Autowired\n    private AESTool aesTool;\n\n    @Test\n    public void testAes() {\n        String pods = \"123\";\n        System.out.println(aesTool.encode(pods, aesTool.fnv1aHash(pods)));\n        System.out.println(aesTool.match(pods, \"1okxVZ0I1b0jrfTp7wyGpg==\"));\n    }\n\n    @Test\n    public void testSaveUser() {\n        User user = userService.getById(1);\n        System.out.println(user);\n    }\n\n}\n"
  },
  {
    "path": "DOJ-FE/.eslintignore",
    "content": "dist\nnode_modules"
  },
  {
    "path": "DOJ-FE/.eslintrc.cjs",
    "content": "module.exports = {\n    env: {\n        browser: true,\n        es2021: true,\n        node: true,\n        jest: true,\n    },\n    /* 指定如何解析语法 */\n    parser: \"vue-eslint-parser\",\n    /** 优先级低于 parse 的语法解析配置 */\n    parserOptions: {\n        ecmaVersion: \"latest\",\n        sourceType: \"module\",\n        parser: \"@typescript-eslint/parser\",\n        jsxPragma: \"React\",\n        ecmaFeatures: {\n            jsx: true,\n        },\n    },\n    /* 继承已有的规则 */\n    extends: [\n        \"plugin:vue/vue3-essential\",\n        // \"eslint:recommended\",\n        // \"plugin:@typescript-eslint/recommended\",\n    ],\n    plugins: [\"vue\", \"@typescript-eslint\"],\n    /*\n     * \"off\" 或 0    ==>  关闭规则\n     * \"warn\" 或 1   ==>  打开的规则作为警告（不影响代码执行）\n     * \"error\" 或 2  ==>  规则作为一个错误（代码不能执行，界面报错）\n     */\n    rules: {\n        // eslint（https://eslint.bootcss.com/docs/rules/）\n        \"no-var\": \"error\", // 要求使用 let 或 const 而不是 var\n        \"no-multiple-empty-lines\": \"off\", // 不允许多个空行\n        \"no-console\": process.env.NODE_ENV === \"production\" ? \"error\" : \"off\",\n        \"no-debugger\": process.env.NODE_ENV === \"production\" ? \"error\" : \"off\",\n        \"no-unexpected-multiline\": \"error\", // 禁止空余的多行\n        \"no-useless-escape\": \"off\", // 禁止不必要的转义字符\n\n        // typeScript (https://typescript-eslint.io/rules)\n        \"@typescript-eslint/no-unused-vars\": \"off\", // 禁止定义未使用的变量\n        \"@typescript-eslint/prefer-ts-expect-error\": \"off\", // 禁止使用 @ts-ignore\n        \"@typescript-eslint/no-explicit-any\": \"off\", // 禁止使用 any 类型\n        \"@typescript-eslint/no-non-null-assertion\": \"off\",\n        \"@typescript-eslint/no-namespace\": \"off\", // 禁止使用自定义 TypeScript 模块和命名空间。\n        \"@typescript-eslint/semi\": \"off\",\n\n        // eslint-plugin-vue (https://eslint.vuejs.org/rules/)\n        \"vue/multi-word-component-names\": \"off\", // 要求组件名称始终为 “-” 链接的单词\n        \"vue/script-setup-uses-vars\": \"error\", // 防止<script setup>使用的变量<template>被标记为未使用\n        \"vue/no-mutating-props\": \"off\", // 不允许组件 prop的改变\n        \"vue/attribute-hyphenation\": \"off\", // 对模板中的自定义组件强制执行属性命名样式\n    },\n};\n"
  },
  {
    "path": "DOJ-FE/.gitignore",
    "content": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\n\nnode_modules\ndist\ndist-ssr\n*.local\n\n# Editor directories and files\n.vscode/*\n!.vscode/extensions.json\n.idea\n.DS_Store\n*.suo\n*.ntvs*\n*.njsproj\n*.sln\n*.sw?\n"
  },
  {
    "path": "DOJ-FE/Dockerfile",
    "content": "FROM nginx:alpine\n\nCOPY dist/ /usr/share/nginx/html/\n\nCOPY nginx.conf /etc/nginx/conf.d/default.conf\n\nEXPOSE 80\n"
  },
  {
    "path": "DOJ-FE/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n\n<head>\n    <meta charset=\"UTF-8\">\n    <link rel=\"icon\" href=\"/vite.svg\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>Vite + Vue + TS</title>\n    <link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css\" />\n    <link rel=\"stylesheet\" href=\"//at.alicdn.com/t/font_2143783_iq6z4ey5vu.css\">\n    <link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css\">\n</head>\n\n<body>\n    <div id=\"app\"></div>\n    \n    <script type=\"module\" src=\"/src/main.ts\"></script>\n</body>\n\n</html>\n"
  },
  {
    "path": "DOJ-FE/nginx.conf",
    "content": "upstream gateway_backend {\n    server gateway-service:8080;\n}\n\nserver {\n    resolver 127.0.0.11 valid=5s;  \n\n    listen       80;\n    server_name  localhost;\n\n    location / {\n        root   /usr/share/nginx/html;\n        index  index.html index.htm;\n        try_files $uri $uri/ /index.html;\n    }\n\n    location /api/ {\n        rewrite ^/api/(.*)$ /$1 break;\n        proxy_pass http://gateway_backend;\n\n        proxy_set_header Host $host;\n        proxy_set_header X-Real-IP $remote_addr;\n        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n        proxy_set_header X-Forwarded-Proto $scheme;\n    }\n\n    error_page   500 502 503 504  /50x.html;\n    location = /50x.html {\n        root   /usr/share/nginx/html;\n    }\n}\n"
  },
  {
    "path": "DOJ-FE/package.json",
    "content": "{\n  \"name\": \"duck_online_judge\",\n  \"private\": true,\n  \"version\": \"1.0.0\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"lint\": \"eslint src\",\n    \"fix\": \"eslint src --fix\",\n    \"dev\": \"NODE_OPTIONS='--no-warnings' vite --open\",\n    \"build\": \"vue-tsc && vite build\",\n    \"build:test\": \"vue-tsc && vite build --mode test\",\n    \"build:pro\": \"vue-tsc && vite build --mode production\",\n    \"preview\": \"vite preview\",\n    \"preinstall\": \"node ./scripts/preinstall.js\"\n  },\n  \"dependencies\": {\n    \"@codemirror/commands\": \"^6.6.1\",\n    \"@codemirror/lang-cpp\": \"^6.0.2\",\n    \"@codemirror/lang-java\": \"^6.0.1\",\n    \"@codemirror/lang-javascript\": \"^6.2.2\",\n    \"@codemirror/lang-python\": \"^6.1.6\",\n    \"@codemirror/theme-one-dark\": \"^6.1.2\",\n    \"@codemirror/view\": \"^6.33.0\",\n    \"@element-plus/icons-vue\": \"^2.3.1\",\n    \"axios\": \"^1.6.5\",\n    \"cm6-theme-material-dark\": \"^0.2.0\",\n    \"cm6-theme-nord\": \"^0.2.0\",\n    \"codemirror\": \"^6.0.1\",\n    \"cropperjs\": \"^1.6.2\",\n    \"echarts\": \"^5.5.1\",\n    \"element-plus\": \"^2.5.1\",\n    \"katex\": \"^0.16.22\",\n    \"marked\": \"^17.0.1\",\n    \"nprogress\": \"^0.2.0\",\n    \"pinia\": \"^2.2.2\",\n    \"pinia-plugin-persistedstate\": \"^3.2.1\",\n    \"sass\": \"1.69.7\",\n    \"thememirror\": \"^2.0.1\",\n    \"vue\": \"^3.3.11\",\n    \"vue-codemirror\": \"^6.1.1\",\n    \"vue-router\": \"^4.4.3\"\n  },\n  \"devDependencies\": {\n    \"@babel/eslint-parser\": \"^7.23.3\",\n    \"@types/codemirror\": \"^5.60.15\",\n    \"@types/marked\": \"^6.0.0\",\n    \"@types/nprogress\": \"^0.2.3\",\n    \"@typescript-eslint/eslint-plugin\": \"^6.19.0\",\n    \"@typescript-eslint/parser\": \"^6.19.0\",\n    \"@vitejs/plugin-vue\": \"^4.5.2\",\n    \"eslint\": \"^8.56.0\",\n    \"eslint-plugin-import\": \"^2.29.1\",\n    \"eslint-plugin-node\": \"^11.1.0\",\n    \"eslint-plugin-vue\": \"^9.20.1\",\n    \"typescript\": \"^5.2.2\",\n    \"vite\": \"^5.4.21\",\n    \"vite-plugin-svg-icons\": \"^2.0.1\",\n    \"vue-tsc\": \"^3.1.0\"\n  }\n}\n"
  },
  {
    "path": "DOJ-FE/run-docker.sh",
    "content": "#!/bin/bash\n\n# 脚本执行时，如果任何命令失败，则立即退出\nset -e\n\npnpm build\n\n# 定义镜像和容器的名称\nIMAGE_NAME=\"doj-fe\"\nCONTAINER_NAME=\"doj-fe-container\"\n\n# 1. 构建最新的前端镜像\necho \"Building Docker image: $IMAGE_NAME...\"\ndocker build -t $IMAGE_NAME .\n\n# 2. 检查是否存在同名容器，如果存在则停止并删除\nif [ $(docker ps -a -q -f name=^/${CONTAINER_NAME}$) ]; then\n    echo \"Stopping and removing existing container: $CONTAINER_NAME...\"\n    docker stop $CONTAINER_NAME\n    docker rm $CONTAINER_NAME\nfi\n\n# 3. 运行新的容器\necho \"Running new container: $CONTAINER_NAME on http://localhost:8088\"\ndocker run -d --name $CONTAINER_NAME -p 8088:80 --memory=\"64m\" --network doj $IMAGE_NAME\n\necho \"Deployment successful!\"\n"
  },
  {
    "path": "DOJ-FE/scripts/preinstall.js",
    "content": "console.log(\"Check install tools!\");\nif (!/pnpm/.test(process.env.npm_execpath || \"\")) {\n  console.warn(\n    `\\u001b[33mThis repository must using pnpm as the package manager ` +\n      ` for scripts to work properly.\\u001b[39m\\n`,\n  );\n  process.exit(1);\n}\n"
  },
  {
    "path": "DOJ-FE/src/App.vue",
    "content": "<script setup lang=\"ts\">\nimport { onMounted } from 'vue';\nimport { useWebSocket } from '@/utils/websocket';\n\nonMounted(() => {\n  // 在应用根组件挂载时，初始化 WebSocket 连接\n  useWebSocket();\n});\n</script>\n\n<template>\n    <!-- 一级路由出口组件 -->\n    <RouterView />\n</template>\n\n<style scoped lang=\"scss\"></style>\n\n"
  },
  {
    "path": "DOJ-FE/src/api/announcement/index.ts",
    "content": "import request from '@/utils/request';\nimport type { BaseResponseData } from '@/api/base';\nimport { AxiosResponse } from 'axios';\n\n// Types\nexport interface Announcement {\n    id: number;\n    title: string;\n    content: string;\n    createTime: string;\n    updateTime: string;\n}\n\nexport type AnnouncementListResponse = BaseResponseData & {\n    data: Announcement[];\n};\n\nenum API {\n    ANNOUNCEMENT_LIST = '/user/announcement/list',\n}\n\nexport const reqAnnouncementList = () => request.get<AnnouncementListResponse, AxiosResponse<AnnouncementListResponse>>(API.ANNOUNCEMENT_LIST);\n"
  },
  {
    "path": "DOJ-FE/src/api/base/index.ts",
    "content": "// types.ts\nexport type BaseResponse = {\n    code: number;\n    message: string;\n};\n\nexport type BaseResponseData = BaseResponse & {\n    data: string\n};\n\nexport type BasePageQueryForm = {\n    pageNo: number,\n    pageSize: number,\n    isAsc: boolean,\n    sortBy: string,\n};"
  },
  {
    "path": "DOJ-FE/src/api/problem/index.ts",
    "content": "// 统一管理项目接口\nimport request from '@/utils/request';\nimport type { ProblemsResponseData, ProblemPageQueryForm, ProblemsPageResponseData, ProblemsDetailResponseData } from './type';\nimport type { BaseResponseData } from '@/api/base';\nimport { AxiosResponse } from 'axios';\n\nenum API {\n    PROBLEMS_URL = '/problem/list',\n    PROBLEMS_PAGE_URL = '/problem/page',\n    PROBLEMS_detail = '/problem/',\n    PROBLEMS_SYNC_ES = '/problem/admin/sync-es',\n    PROBLEMS_REINDEX = '/problem/admin/reindex',\n    PROBLEMS_RESET = '/problem/admin/reset',\n};\n\n// 暴露请求函数\nexport const reqProblemList = () => request.get<ProblemsResponseData, AxiosResponse<ProblemsResponseData>>(API.PROBLEMS_URL);\nexport const reqProblemPageList = (data: ProblemPageQueryForm) => request.get<ProblemsPageResponseData, AxiosResponse<ProblemsPageResponseData>>(API.PROBLEMS_PAGE_URL, {\n    params: data\n});\nexport const reqProblemDetail = (data: string) => request.get<ProblemsDetailResponseData, AxiosResponse<ProblemsDetailResponseData>>(API.PROBLEMS_detail + data);\n\n// Admin APIs\nexport const reqCreateProblem = (data: any) => request.post<BaseResponseData, AxiosResponse<BaseResponseData>>(API.PROBLEMS_detail, data);\nexport const reqUpdateProblem = (data: any) => request.put<BaseResponseData, AxiosResponse<BaseResponseData>>(API.PROBLEMS_detail, data);\nexport const reqDeleteProblem = (id: number) => request.delete<BaseResponseData, AxiosResponse<BaseResponseData>>(API.PROBLEMS_detail + id);\nexport const reqSyncProblemToEs = () => request.post<BaseResponseData, AxiosResponse<BaseResponseData>>(API.PROBLEMS_SYNC_ES);\nexport const reqReindexProblemEs = () => request.post<BaseResponseData, AxiosResponse<BaseResponseData>>(API.PROBLEMS_REINDEX);\nexport const reqResetProblems = () => request.post<BaseResponseData, AxiosResponse<BaseResponseData>>(API.PROBLEMS_RESET);\n"
  },
  {
    "path": "DOJ-FE/src/api/problem/type.ts",
    "content": "import { BaseResponse, BasePageQueryForm } from '@/api/base';\n\nexport type ProblemPageQueryForm = BasePageQueryForm & {\n    name?: string,\n    description?: string,\n    difficulty?: string,\n    tags?: string[],\n    status?: string,\n};\n\nexport type ProblemType = {\n    id: number;\n    name: string;\n    description: string;\n    inputStyle: string;\n    outputStyle: string;\n    hint: string;\n    inputSample: string[];\n    outputSample: string[];\n    difficulty: string;\n    timeLimit: number;\n    memoryLimit: number;\n    totalPass: number;\n    totalAttempt: number;\n    tags: string[];\n    status?: string;\n    testData?: string;\n    testAns?: string;\n};\n\nexport type ProblemsPageResponseData = BaseResponse & {\n    data: {\n        total: number,\n        pages: number,\n        list: ProblemType[],\n    },\n};\n\nexport type ProblemsResponseData = BaseResponse & {\n    data: ProblemType[],\n};\n\nexport type ProblemsDetailResponseData = BaseResponse & {\n    data: ProblemType,\n};\n"
  },
  {
    "path": "DOJ-FE/src/api/stats/index.ts",
    "content": "import request from '@/utils/request';\nimport type { BaseResponseData } from '@/api/base';\nimport { AxiosResponse } from 'axios';\n\n// Types\nexport interface Stats {\n    totalSubmissions: number;\n    todaySubmissions: number;\n    totalProblems: number;\n    activeUsers: number;\n}\n\nexport type StatsResponse = BaseResponseData & {\n    data: Stats;\n};\n\nenum API {\n    STATS = '/user/stats',\n}\n\nexport const reqStats = () => request.get<StatsResponse, AxiosResponse<StatsResponse>>(API.STATS);\n"
  },
  {
    "path": "DOJ-FE/src/api/submission/index.ts",
    "content": "// 统一管理项目接口\nimport request from '@/utils/request';\nimport type { Submission, SubmissionPageQueryForm, SubmissionsPageResponseData, SubmissionUserMatch } from './type';\nimport type { BaseResponseData } from '@/api/base';\nimport { AxiosResponse } from 'axios';\n\nenum API {\n    SUBMISSIONS_LIST = '/submission/list',\n    SUBMISSIONS_PAGE = '/submission/page',\n    SUBMISSION_DETAIL = '/submission/',\n    SUBMISSION_USER_PROBLEM = '/submission/match/',\n};\n\n// 请求提交列表（非分页）\nexport const reqSubmissionList = () =>\n    request.get<SubmissionsPageResponseData, AxiosResponse<SubmissionsPageResponseData>>(API.SUBMISSIONS_LIST);\n\n// 请求提交分页列表\nexport const reqSubmissionPageList = (data: SubmissionPageQueryForm) =>\n    request.get<SubmissionsPageResponseData, AxiosResponse<SubmissionsPageResponseData>>(API.SUBMISSIONS_PAGE, {\n        params: data\n    });\n\n// 请求用户提交的题目\nexport const reqSubmissionUserProblem = (id: number) =>\n    request.get<SubmissionUserMatch, AxiosResponse<SubmissionUserMatch>>(API.SUBMISSION_USER_PROBLEM + id);"
  },
  {
    "path": "DOJ-FE/src/api/submission/type.ts",
    "content": "import { BaseResponse, BasePageQueryForm } from '@/api/base';\n\nexport type Submission = {\n    id: number;            // 提交记录主键\n    userId: number;        // 用户ID（来源于 doj_user.user）\n    problemId: number;     // 题目ID（来源于 doj_problem.problem）\n    language: string;      // 编程语言\n    code: string;          // 提交的代码文本内容\n    exitValue?: number;    // 程序退出码\n    status?: string;       // 判题状态\n    message?: string;      // 判题详细信息\n    time?: number;         // 运行时间（单位：秒）\n    memory?: number;       // 内存使用（单位：KB）\n    submitTime: string;    // 提交时间\n};\n\nexport type SubmissionPageQueryForm = BasePageQueryForm & {\n    userId?: number | string;\n    problemId?: number;\n    language?: string;\n    status?: string;\n};\n\nexport type SubmissionsPageResponseData = BaseResponse & {\n    data: {\n        total: number;\n        pages: number;\n        list: Submission[];\n    },\n};\n\nexport type SubmissionUserMatch = BaseResponse & {\n    data: number;\n};"
  },
  {
    "path": "DOJ-FE/src/api/submit/index.ts",
    "content": "// 统一管理项目接口\nimport request from '@/utils/request';\nimport type { submitForm, executeResponseData, problemSubmitForm, sidResponseData } from './type';\nimport type { BaseResponseData } from '@/api/base';\nimport { AxiosResponse } from 'axios';\n\nenum API {\n    SUBMIT_URL = '/sandbox/code',\n    PROBLEMS_SUBMIT_URL = '/sandbox/problem',\n    PROBLEMS_VALIDATE_URL = '/sandbox/validate'\n};\n\n// 暴露请求函数\nexport const reqSubmit = (data: FormData) => request.post< executeResponseData, AxiosResponse<executeResponseData>>(API.SUBMIT_URL, data);\nexport const reqProblemSubmit = (data: FormData) => request.post< executeResponseData, AxiosResponse<executeResponseData>>(API.PROBLEMS_SUBMIT_URL, data);\nexport const reqProblemValidate = (data: FormData) => request.post< executeResponseData, AxiosResponse<sidResponseData>>(API.PROBLEMS_VALIDATE_URL, data);\n"
  },
  {
    "path": "DOJ-FE/src/api/submit/type.ts",
    "content": "import { BaseResponse } from '@/api/base'\n\nexport type submitForm = {\n    file: File,\n    language: string,\n};\n\nexport type problemSubmitForm = {\n    pid: string,\n    file: File,\n    input: File,\n    language: string,\n};\n\nexport type executeMessage = {\n    exitValue: number,\n    status: string,\n    message: string,\n    time: number,\n    memory: number,\n};\n\nexport type executeResponseData = BaseResponse & {\n    data: executeMessage,\n};\n\nexport type sidResponseData = BaseResponse & {\n    data: number,\n};"
  },
  {
    "path": "DOJ-FE/src/api/user/index.ts",
    "content": "// 统一管理项目接口\nimport request from '@/utils/request';\nimport type { loginForm, loginResponseData, registerForm, userInfoResponseData, userType, updPwdForm } from './type';\nimport type { BaseResponseData } from '@/api/base';\n\nenum API {\n    LOGIN_URL = '/user/login',\n    REGISTER_URL = '/user/register',\n    USERINFO_URL = '/user/',\n    USERPWD_URL = '/user/pwd',\n    AVATAR_URL = '/user/avatar',\n    RANKINGS_URL = '/user/rankings',\n};\n\n// 暴露请求函数\nexport const reqLogin = (data: loginForm) => request.post< loginResponseData>(API.LOGIN_URL, data);\n\nexport const reqRegister = (data: registerForm) => request.post<userInfoResponseData>(API.REGISTER_URL, data);\n\nexport const reqUserInfo = (id: number) => request.get<userInfoResponseData>(API.USERINFO_URL + id);\n\nexport const updUserInfo = (data: userType) => request.put<BaseResponseData>(API.USERINFO_URL, data);\n\nexport const updUserPwd = (data: updPwdForm) => request.put<BaseResponseData>(API.USERPWD_URL, data);\n\nexport const updUserAvatar = (data: FormData) => request.post<BaseResponseData>(API.AVATAR_URL, data);\n\nexport const reqRankings = (params: { pageNo: number, pageSize: number }) => {\n    return request.get<any, any>(API.RANKINGS_URL, { params });\n}"
  },
  {
    "path": "DOJ-FE/src/api/user/type.ts",
    "content": "import { BaseResponse } from '@/api/base'\n\n// 登陆接口请求数据类型\nexport type loginForm = {\n    username: string,\n    password: string\n};\n\n// 登陆接口返回数据类型\ntype dataType = {\n    accessToken: string,\n    refreshToken: string,\n    userId: number,\n    username: string,\n    avatar?: string,\n    role: boolean,\n};\nexport type loginResponseData = BaseResponse & {\n    data: dataType\n};\n\n// 注册接口请求数据类型\nexport type registerForm = {\n    username: string,\n    password: string,\n    email: string\n    sign: string\n};\n// 注册接口返回数据类型\nexport type registerResponseData = BaseResponse & {\n    data: string\n};\n\n// 头像上传接口返回数据类型\nexport type uploadAvatarResponseData = BaseResponse & {\n    data: string\n};\n\n// 用户信息接口返回数据类型\nexport type userType = {\n    id: number,\n    username: string,\n    avatar: string,\n    email: string,\n    password: string,\n    score: number,\n    ranks: number,\n    school: string,\n    gender: boolean,\n    easySolve: number,\n    middleSolve: number,\n    hardSolve: number,\n    role: string,\n    url: string,\n    sign: string,\n    fans: number,\n    subscribe: number,\n    ban: boolean,\n};\n\nexport type userInfoResponseData = BaseResponse & {\n    data: userType\n};\n\n// 修改密码接口请求数据类型\nexport type updPwdForm = {\n    id: number,\n    oldPassword: string,\n    newPassword: string\n};"
  },
  {
    "path": "DOJ-FE/src/components/CodeEditor/codeEditor.vue",
    "content": "<template>\n    <div class=\"editor\">\n        <div class=\"main\">\n            <codemirror v-model=\"code\" :style=\"{\n                width: config.width,\n                height: config.height,\n                backgroundColor: 'var(--bg-primary)',\n                color: 'var(--text-primary)',\n                fontSize: fontSizeStr,\n            }\" placeholder=\"Please enter the code.\" :extensions=\"extensions\" :disabled=\"config.disabled\"\n                :indent-with-tab=\"true\" :tab-size=\"config.tabSize\" @update=\"handleStateUpdate\" @ready=\"handleReady\" />\n        </div>\n        <div class=\"divider\"></div>\n        <div class=\"footer\">\n            <div class=\"buttons\">\n                <el-button :icon=\"VideoPlay\" round @click=\"handleSubmit\">run</el-button>\n            </div>\n            <div class=\"infos\">\n                <span class=\"item\">Spaces: {{ config.tabSize }}</span>\n                <span class=\"item\">Length: {{ state.length }}</span>\n                <span class=\"item\">Lines: {{ state.lines }}</span>\n                <span class=\"item\">Cursor: {{ state.cursor }}</span>\n                <span class=\"item\">Selected: {{ state.selected }}</span>\n            </div>\n        </div>\n\n        <!-- 仅在 outputVisible 为 true 时显示 output 区域 -->\n        <div class=\"output\" v-if=\"outputVisible\">\n            <div class=\"output-header\">\n                <span>代码运行状态：</span>\n                <span :class=\"output?.exitValue !== 0 && output?.exitValue !== 10 ? 'error-text' : 'success-text'\">\n                    {{ output?.status }}\n                    <el-icon v-if=\"output?.status === 'Running'\" v-loading=\"loading\" :element-loading-svg=\"svg\"\n                        class=\"custom-loading-svg\" element-loading-svg-view-box=\"-10, -10, 50, 50\">\n                    </el-icon>\n                </span>\n                <div class=\"running-status\" v-show=\"output?.exitValue === 0\">\n                    <span>{{ timeInfo }}</span>\n                </div>\n                <div class=\"running-status\" v-show=\"output?.exitValue === 0\">\n                    <span>{{ memoryInfo }}</span>\n                </div>\n                <span class=\"closeoutput\" @click=\"outputVisible = false\">\n                    <el-icon>\n                        <Close />\n                    </el-icon>\n                </span>\n            </div>\n\n            <!-- 输入输出部分 -->\n            <div class=\"input-output-section\" v-show=\"outputTextVis\">\n                <div class=\"input-field\" v-if=\"inputModel\">\n                    <label>输入</label>\n                    <!-- <div class=\"input-content\">{{ input }}</div> -->\n                </div>\n                <div class=\"output-field\">\n                    <label>输出</label>\n                    <pre class=\"output-content\">{{ output?.message }}</pre>\n                </div>\n            </div>\n        </div>\n    </div>\n</template>\n\n<script lang=\"ts\" setup>\nimport { reactive, shallowRef, computed, watch, onMounted, ref } from 'vue'\nimport { EditorView, ViewUpdate } from '@codemirror/view'\nimport { Codemirror } from 'vue-codemirror'\nimport { ElButton, ElMessage } from 'element-plus'\nimport { Close, VideoPlay } from '@element-plus/icons-vue'\nimport { configType } from './index.vue'\nimport { reqSubmit } from '@/api/submit'\nimport { executeMessage } from '@/api/submit/type'\n\nconst props = defineProps<{\n    config: configType,\n    code: string,\n    theme: Object | Array<string>,\n    language: Function,\n    languageName: string\n}>();\n\n// 组件暴露\ndefineExpose({\n    Codemirror\n});\n\n// 响应式状态\nconst code = shallowRef(props.code);\nconst cmView = shallowRef<EditorView>();\nconst inputModel = shallowRef(false);  // 控制输入区域显示与否\nconst output = shallowRef<executeMessage>();  // 保存输出结果\nconst outputVisible = shallowRef(false);  // 控制 output 区域显示与否\nconst outputTextVis = shallowRef(false);  // 控制输入输出区域显示与否\n\nconst fontSizeStr = computed(() => `${props.config.fontSize}px`);\nconst timeInfo = computed(() => {\n    return output.value?.time ? `${(output.value.time * 1000).toFixed(2)} ms` : '0.00 ms';\n});\nconst memoryInfo = computed(() => {\n    return output.value?.memory ? `${(output.value.memory).toFixed(2)} KB` : '0.00 KB';\n});\n\nconst loading = ref(true);\nconst svg = `\n        <path class=\"path\" d=\"\n          M 30 15\n          L 28 17\n          M 25.61 25.61\n          A 15 15, 0, 0, 1, 15 30\n          A 15 15, 0, 1, 1, 27.99 7.5\n          L 15 15\n        \" style=\"stroke-width: 4px; fill: #F9F9F9\"/>\n      `\n\n// 计算属性\nconst extensions = computed(() => {\n    const result = [];\n    if (props.language) {\n        result.push(props.language());\n    }\n    if (props.theme) {\n        result.push(props.theme);\n    }\n    return result;\n});\n\nconst handleReady = ({ view }: any) => {\n    cmView.value = view;\n};\n\nconst state = reactive({\n    lines: null as null | number,\n    cursor: null as null | number,\n    selected: null as null | number,\n    length: null as null | number\n});\n\nconst handleStateUpdate = (viewUpdate: ViewUpdate) => {\n    const ranges = viewUpdate.state.selection.ranges\n    state.selected = ranges.reduce((plus, range) => plus + range.to - range.from, 0)\n    state.cursor = ranges[0].anchor\n    state.length = viewUpdate.state.doc.length\n    state.lines = viewUpdate.state.doc.lines\n};\n\nconst handleSubmit = async () => {\n\n    outputTextVis.value = false;\n\n    output.value = {\n        exitValue: -1, // 设置为 null 表示还没有结果\n        status: 'Running',\n        message: '',\n        time: 0,\n        memory: 0,\n    };\n\n    // 将代码字符串转换为 Blob 文件\n    const codeBlob = new Blob([code.value], { type: 'text/plain' });\n\n    // 获取语言扩展名\n    const languageExtension = getLanguageExtension(props.languageName);\n\n    // 创建 FormData\n    const formData = new FormData();\n    formData.append('file', codeBlob, `Main.${languageExtension}`);\n    formData.append('language', props.languageName);\n\n    ElMessage.success('提交成功');\n\n    // 发送请求\n    const response = (await reqSubmit(formData)).data;\n\n    // 成功时处理\n    if (response.code === 200) {\n        const data = response.data;\n        if (data.exitValue === 0 || data.exitValue >= 10) {\n            output.value = data;\n        } else {\n            output.value = data;\n        }\n        if (data.message.trim() !== '') {\n            outputTextVis.value = true;\n        }else{\n            outputTextVis.value = false;\n        }\n    }\n    // 显示 output 区域\n    outputVisible.value = true;\n};\n\n// 获取语言扩展名的辅助函数\nconst getLanguageExtension = (languageName: string) => {\n    if (!languageName) return 'txt'; // 默认 txt 后缀\n\n    // 你可以根据不同语言函数推断其扩展名\n    const language = languageName.toLowerCase();\n\n    switch (language) {\n        case 'javascript': return 'js';\n        case 'python': return 'py';\n        case 'cpp': return 'cpp';\n        case 'java': return 'java';\n        // 添加更多语言映射\n        default: return 'txt';\n    }\n};\n\n// 监听 props 变化\nonMounted(() => {\n    watch(\n        () => props.code,\n        (_code) => {\n            code.value = _code\n        }\n    );\n    const handleKeyDown = (event: KeyboardEvent) => {\n        const isSaveShortcut = \n            (event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 's';\n\n        if (isSaveShortcut) {\n            event.preventDefault(); // 阻止默认保存行为\n            console.log('Save shortcut (Ctrl+S / Cmd+S) is disabled in the editor.');\n            // 可选：在这里执行其他逻辑，比如保存代码等\n        }\n    };\n\n    window.addEventListener('keydown', handleKeyDown);\n});\n\n// log 方法直接使用 console\nconst log = console.log\n</script>\n\n<style lang=\"scss\" scoped>\n@import '@/styles/variable.scss';\n\n.editor {\n    .divider {\n        height: 1px;\n        background-color: $border-color;\n    }\n\n    .main {\n\n        .code {\n            width: 30%;\n            height: 100px;\n            margin: 0;\n            padding: 0.4em;\n            overflow: scroll;\n            border-left: 1px solid $border-color;\n            font-family: monospace;\n        }\n    }\n\n    .footer {\n        height: 3rem;\n        padding: 0 1em;\n        display: flex;\n        justify-content: space-between;\n        align-items: center;\n        font-size: 90%;\n        background-color: var(--bg-elevated);\n        border-top: 1px solid var(--border-color);\n\n        .buttons {\n            .item {\n                margin-right: 1em;\n                display: inline-flex;\n                justify-content: center;\n                align-items: center;\n                background-color: transparent;\n                border: 1px dashed var(--border-color);\n                font-size: $font-size-small;\n                color: var(--text-secondary);\n                cursor: pointer;\n\n                .iconfont {\n                    margin-left: $xs-gap;\n                }\n\n                &:hover {\n                    color: var(--text-primary);\n                    border-color: var(--text-primary);\n                }\n            }\n\n            :deep(.el-button) {\n                background: var(--bg-primary);\n                border-color: var(--border-color);\n                color: var(--text-primary);\n                \n                &:hover, &:focus {\n                    background: var(--primary-start);\n                    border-color: var(--primary-start);\n                    color: #fff;\n                }\n                \n                &.el-button--primary {\n                    background: var(--primary-gradient);\n                    border: none;\n                    color: #fff;\n                    \n                    &:hover {\n                        opacity: 0.9;\n                    }\n                }\n            }\n        }\n\n        .infos {\n            color: var(--text-secondary);\n            .item {\n                margin-left: 2em;\n                display: inline-block;\n                font-feature-settings: 'tnum';\n            }\n        }\n    }\n\n    .output {\n        margin-top: 20px;\n        border: 1px solid var(--border-color);\n        border-radius: 4px;\n        padding: 20px;\n        background-color: var(--bg-elevated);\n        box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2);\n\n        .output-header {\n            display: flex;\n            justify-content: flex-start;\n            //margin-bottom: 10px;\n            color: var(--text-primary);\n\n            .closeoutput {\n                margin-left: auto;\n                cursor: pointer;\n            }\n\n            .running-status {\n                background-color: var(--bg-primary);\n                border: 1px solid var(--border-color);\n                border-radius: 4px;\n                margin-left: 10px;\n                padding: 0 10px;\n\n                span {\n                    color: var(--danger);\n                    font-size: 16px;\n                }\n            }\n\n            .custom-loading-svg {\n                background-color: transparent;\n                /* 设置背景为透明 */\n                border: none;\n                /* 清除边框 */\n                padding: 0;\n                /* 去掉内边距 */\n            }\n\n            span {\n                font-size: 18px;\n                font-weight: bold;\n            }\n\n            /* 根据状态动态改变颜色 */\n            .error-text {\n                color: var(--danger);\n                /* 红色表示错误 */\n            }\n\n            .success-text {\n                color: var(--success);\n                /* 绿色表示成功 */\n            }\n        }\n\n        .input-output-section {\n            display: flex;\n            flex-direction: column;\n            gap: 10px;\n            margin-top: 15px;\n\n            .input-field,\n            .output-field {\n                display: flex;\n                flex-direction: column;\n            }\n\n            label {\n                font-size: 14px;\n                margin-bottom: 5px;\n                font-weight: bold;\n                color: var(--text-secondary);\n            }\n\n            .input-content {\n                background-color: var(--bg-primary);\n                border: 1px solid var(--border-color);\n                border-radius: 4px;\n                padding: 10px;\n                font-size: 14px;\n                color: var(--text-primary);\n                line-height: 1.5;\n            }\n\n            .output-content {\n                background-color: var(--bg-primary);\n                border: 1px solid var(--border-color);\n                border-radius: 4px;\n                padding: 10px;\n                font-size: 14px;\n                color: var(--text-primary);\n                line-height: 1.5;\n                white-space: pre-wrap;\n                word-wrap: break-word;\n            }\n        }\n    }\n\n}\n</style>"
  },
  {
    "path": "DOJ-FE/src/components/CodeEditor/index.vue",
    "content": "<template>\n    <div :class=\"['online-coding', { 'full-page': isPageFullscreen }]\">\n        <toolbar v-if=\"config.editorType !== 'show'\"\n            :config=\"config\"\n            :themes=\"Object.keys(themes)\"\n            :languages=\"Object.keys(languages)\"\n            @language=\"ensureLanguageCode\"\n            @fullscreen=\"togglePageFullscreen\"\n        />\n        <div class=\"divider\"></div>\n        <div class=\"loading-box\" v-if=\"loading\">\n            <loading />\n        </div>\n        <component\n            v-else=\"currentLangCode\"\n            :is=\"editorComponent\"\n            :config=\"config\"\n            :theme=\"currentTheme\"\n            :language=\"currentLangCode.language\"\n            :language-name=\"currentLangCode.languageName\"\n            :code=\"currentLangCode.code\"\n        />\n    </div>\n</template>\n\n<script lang=\"ts\" setup>\nimport { reactive, computed, shallowRef, onBeforeMount, PropType, toRefs, ref } from 'vue';\nimport languages from './languages';\nimport * as themes from './themes';\nimport Toolbar from './toolbar.vue';\nimport CodeEditor from './codeEditor.vue';\nimport ProblemEditor from './problemEditor.vue';\nimport ShowEditor from './showEditor.vue';\n\nexport type configType = {\n    tabSize: number;\n    fontSize: number;\n    disabled: boolean;\n    height: string;\n    width: string;\n    language: string;\n    theme: string;\n    editorType: string; // 'code' | 'problem'\n    code?: string; // 可选属性，适用于 'show' 编辑器\n};\n\nconst props = defineProps({\n    config: {\n        type: Object as PropType<configType>,\n        required: true\n    }\n});\n\nconst { config } = toRefs(props);\n\nconst editorComponent = computed(() => {\n    if (config.value.editorType === 'problem') {\n        return ProblemEditor;\n    } else if (config.value.editorType === 'show') {\n        return ShowEditor;\n    }\n    return CodeEditor;\n});\n\nconst loading = shallowRef(false)\nconst langCodeMap = reactive(new Map<string, { code: string; language: () => any; languageName: string; }>())\nconst currentLangCode = computed(() => langCodeMap.get(config.value.language)!)\nconst currentTheme = computed(() => {\n    return config.value.theme !== 'default' ? (themes as any)[config.value.theme] : void 0\n});\n\n// 定义异步函数，用于确保加载语言代码\nconst ensureLanguageCode = async (targetLanguage: string) => {\n    config.value.language = targetLanguage;\n    loading.value = true;\n    const delayPromise = () => new Promise((resolve) => window.setTimeout(resolve, 260));\n    if (langCodeMap.has(targetLanguage)) {\n        await delayPromise();\n    } else {\n        const [result] = await Promise.all([languages[targetLanguage](), delayPromise()]);\n        if (config.value.code) {\n            result.default.code = config.value.code;\n        }\n        result.default.languageName = targetLanguage;\n        langCodeMap.set(targetLanguage, result.default);\n    }\n    loading.value = false;\n};\n\n// 设置加载状态为 true，并确保在组件挂载前初始化语言代码\nloading.value = true;\nonBeforeMount(() => {\n    ensureLanguageCode(config.value.language)\n});\n\n// 页面全屏相关状态\nconst isPageFullscreen = ref(false);\nconst togglePageFullscreen = () => {\n    isPageFullscreen.value = !isPageFullscreen.value;\n    config.value.height = isPageFullscreen.value ? '88vh' : '62vh';\n};\n\n\n</script>\n\n<style lang=\"scss\" scoped>\n@import '@/styles/variable.scss';\n\n.online-coding {\n\n    .divider {\n        height: 1px;\n        background-color: #e8e8e8;\n    }\n\n    .loading-box {\n        width: 80%;\n        min-height: 20rem;\n        max-height: 60rem;\n    }\n\n    &.full-page {\n        position: fixed;\n        top: 0;\n        left: 0;\n        width: 100vw;\n        z-index: 9999;\n        background-color: #fff;\n    }\n}\n</style>\n"
  },
  {
    "path": "DOJ-FE/src/components/CodeEditor/lang-code/cpp/cpp.cpp",
    "content": "// template\n#include <iostream>\n#include <vector>\n\nusing namespace std;\n\nint main() {\n    int a, b, n;\n    cin >> a >> b;\n    n = a + b;\n    cout << n << endl;\n    return 0;\n}"
  },
  {
    "path": "DOJ-FE/src/components/CodeEditor/lang-code/cpp/index.ts",
    "content": "import { cpp } from '@codemirror/lang-cpp'\nimport code from './cpp.cpp?raw'\n\nexport default {\n  language: cpp,\n  code\n}\n"
  },
  {
    "path": "DOJ-FE/src/components/CodeEditor/lang-code/java/index.ts",
    "content": "import { java } from '@codemirror/lang-java'\nimport code from './java.java?raw'\n\nexport default {\n  language: java,\n  code\n}\n"
  },
  {
    "path": "DOJ-FE/src/components/CodeEditor/lang-code/java/java.java",
    "content": "// template\n\npublic class Main {\n    public static void main(String[] args) {\n        int mbSize = 256 * 1024 * 1024; // 256MB in bytes\n        \n        byte[] largeArray = new byte[mbSize];\n    \n        System.out.println(\"Successfully allocated 256MB of memory.\");\n        System.out.println(\"Hello, World!\");\n    }\n}"
  },
  {
    "path": "DOJ-FE/src/components/CodeEditor/lang-code/python/index.ts",
    "content": "import { python } from '@codemirror/lang-python'\nimport code from './python.py?raw'\n\nexport default {\n  language: python,\n  code\n}\n"
  },
  {
    "path": "DOJ-FE/src/components/CodeEditor/lang-code/python/python.py",
    "content": "# template\n\na = []\n\nfor i in range(10000000):\n    a.append(i)\n\nprint(sum(a))\n\nprint(\"Hello, World!\")"
  },
  {
    "path": "DOJ-FE/src/components/CodeEditor/languages.ts",
    "content": "const importers = import.meta.glob<string>('./lang-code/*/index.ts')\nconst languages: { [key in string]: () => any } = {}\nObject.keys(importers).forEach((fileName) => {\n  const language = fileName.replace('./lang-code/', '').replace('/index.ts', '')\n  languages[language] = importers[fileName]\n})\n\nexport default languages\n"
  },
  {
    "path": "DOJ-FE/src/components/CodeEditor/problemEditor.vue",
    "content": "<template>\n    <div class=\"editor\">\n        <div class=\"main\">\n            <codemirror v-model=\"code\" :style=\"{\n                width: config.width,\n                height: config.height,\n                backgroundColor: 'var(--bg-primary)',\n                color: 'var(--text-primary)',\n                fontSize: fontSizeStr,\n            }\" placeholder=\"Please enter the code.\" :extensions=\"extensions\" :disabled=\"config.disabled\"\n                :indent-with-tab=\"true\" :tab-size=\"config.tabSize\" @update=\"handleStateUpdate\" @ready=\"handleReady\" />\n        </div>\n        <div class=\"divider\"></div>\n        <div class=\"footer\">\n            <div class=\"buttons\">\n                <el-button :icon=\"VideoPlay\" round @click=\"handleSubmit\">run</el-button>\n                <el-button :icon=\"VideoPlay\" type=\"primary\" round @click=\"handleSubmit1\">submit</el-button>\n            </div>\n            <div class=\"infos\">\n                <span class=\"item\">Spaces: {{ config.tabSize }}</span>\n                <span class=\"item\">Length: {{ state.length }}</span>\n                <span class=\"item\">Lines: {{ state.lines }}</span>\n                <span class=\"item\">Cursor: {{ state.cursor }}</span>\n                <span class=\"item\">Selected: {{ state.selected }}</span>\n            </div>\n        </div>\n\n        <!-- 仅在 outputVisible 为 true 时显示 output 区域 -->\n        <div class=\"input-section\">\n            <div class=\"input-field\">\n                <label>输入</label>\n                <el-input v-model=\"inputContent\" :autosize=\"{ minRows: 1, maxRows: 10 }\" type=\"textarea\" />\n            </div>\n        </div>\n        <div class=\"output\" v-if=\"outputVisible\">\n            <div class=\"output-header\">\n                <span>代码运行状态：</span>\n                <span :class=\"output?.exitValue !== 0 && output?.exitValue !== 10 ? 'error-text' : 'success-text'\">\n                    {{ output?.status }}\n                    <el-icon v-if=\"output?.status === 'Running'\" v-loading=\"loading\" :element-loading-svg=\"svg\"\n                        class=\"custom-loading-svg\" element-loading-svg-view-box=\"-10, -10, 50, 50\">\n                    </el-icon>\n                </span>\n                <div class=\"running-status\" v-show=\"output?.exitValue === 0\">\n                    <span>{{ timeInfo }}</span>\n                </div>\n                <div class=\"running-status\" v-show=\"output?.exitValue === 0\">\n                    <span>{{ memoryInfo }}</span>\n                </div>\n                <span class=\"closeoutput\" @click=\"outputVisible = false\">\n                    <el-icon>\n                        <Close />\n                    </el-icon>\n                </span>\n            </div>\n            <!-- 输入输出部分 -->\n            <div class=\"input-output-section\" v-show=\"outputTextVis\">\n                <div class=\"output-field\">\n                    <label>输出</label>\n                    <pre class=\"output-content\">{{ output?.message }}</pre>\n                </div>\n            </div>\n        </div>\n    </div>\n</template>\n\n<script lang=\"ts\" setup>\nimport { reactive, shallowRef, computed, watch, onMounted, ref } from 'vue'\nimport { EditorView, ViewUpdate } from '@codemirror/view'\nimport { Codemirror } from 'vue-codemirror'\nimport { ElButton, ElMessage, ElMessageBox } from 'element-plus'\nimport { Close, VideoPlay } from '@element-plus/icons-vue'\nimport { configType } from './index.vue'\nimport { useRoute } from 'vue-router'\nimport { reqProblemSubmit, reqProblemValidate } from '@/api/submit'\nimport { executeMessage } from '@/api/submit/type'\nimport { useWebSocket } from '@/utils/websocket'\n\nconst props = defineProps<{\n    config: configType,\n    code: string,\n    theme: Object | Array<string>,\n    language: Function,\n    languageName: string\n}>();\n\nconst route = useRoute();\n\n// 组件暴露\ndefineExpose({\n    Codemirror\n});\n\n// 响应式状态\nconst code = shallowRef(props.code);\nconst cmView = shallowRef<EditorView>();\nconst inputModel = shallowRef(true);  // 控制输入区域显示与否\nconst output = shallowRef<executeMessage>();  // 保存输出结果\nconst outputVisible = shallowRef(false);  // 控制 output 区域显示与否\nconst outputTextVis = shallowRef(false);  // 控制输入输出区域显示与否\nconst inputContent = ref(''); // 用于存储输入内容\n\nconst fontSizeStr = computed(() => `${props.config.fontSize}px`);\nconst timeInfo = computed(() => {\n    return output.value?.time ? `${(output.value.time * 1000).toFixed(2)} ms` : '0.00 ms';\n});\nconst memoryInfo = computed(() => {\n    return output.value?.memory ? `${(output.value.memory).toFixed(2)} KB` : '0.00 KB';\n});\n\nconst loading = ref(true);\nconst svg = `\n        <path class=\"path\" d=\"\n          M 30 15\n          L 28 17\n          M 25.61 25.61\n          A 15 15, 0, 0, 1, 15 30\n          A 15 15, 0, 1, 1, 27.99 7.5\n          L 15 15\n        \" style=\"stroke-width: 4px; fill: #F9F9F9\"/>\n      `\n\n// 计算属性\nconst extensions = computed(() => {\n    const result = [];\n    if (props.language) {\n        result.push(props.language());\n    }\n    if (props.theme) {\n        result.push(props.theme);\n    }\n    return result;\n});\n\nconst handleReady = ({ view }: any) => {\n    cmView.value = view;\n};\n\nconst state = reactive({\n    lines: null as null | number,\n    cursor: null as null | number,\n    selected: null as null | number,\n    length: null as null | number\n});\n\nconst handleStateUpdate = (viewUpdate: ViewUpdate) => {\n    const ranges = viewUpdate.state.selection.ranges\n    state.selected = ranges.reduce((plus, range) => plus + range.to - range.from, 0)\n    state.cursor = ranges[0].anchor\n    state.length = viewUpdate.state.doc.length\n    state.lines = viewUpdate.state.doc.lines\n};\n\nconst handleSubmit1 = async () => {\n\n    // 将代码字符串转换为 Blob 文件\n    const codeBlob = new Blob([code.value], { type: 'text/plain' });\n\n    // 获取语言扩展名\n    const languageExtension = getLanguageExtension(props.languageName);\n\n    // 创建 FormData\n    const formData = new FormData();\n    const pid = route.params.id as string;\n    formData.append('pid', pid);\n    formData.append('file', codeBlob, `Main.${languageExtension}`);\n    formData.append('language', props.languageName);\n\n    try {\n        // 发送异步验证请求\n        const response = (await reqProblemValidate(formData)).data;\n        if (response.code === 200) {\n            ElMessage.info('提交成功，判题中...');\n            const submissionId = response.data;\n            // 订阅 WebSocket 结果\n            console.log(submissionId);\n            useWebSocket().subscribeToSubmission(submissionId);\n        } else {\n            ElMessage.error(`提交失败: ${response.message}`);\n        }\n    } catch (err) {\n        ElMessage.error('提交请求失败');\n    }\n};\n\nconst handleSubmit = async () => {\n\n    outputTextVis.value = false;\n\n    output.value = {\n        exitValue: -1, // 设置为 null 表示还没有结果\n        status: 'Running',\n        message: '',\n        time: 0,\n        memory: 0,\n    };\n\n    // 将代码字符串转换为 Blob 文件\n    const codeBlob = new Blob([code.value], { type: 'text/plain' });\n    const inputBlob = new Blob([inputContent.value], { type: 'text/plain' });\n\n    // 获取语言扩展名\n    const languageExtension = getLanguageExtension(props.languageName);\n    const formData = new FormData();\n    const pid = route.params.id as string;\n    formData.append('pid', pid);\n    formData.append('file', codeBlob, `Main.${languageExtension}`);\n    formData.append('input', inputBlob, `${pid}_input.txt`);\n    formData.append('language', props.languageName);\n\n    ElMessage.success('提交成功');\n\n    // 发送请求\n    const response = (await reqProblemSubmit(formData)).data;\n\n    // 成功时处理\n    if (response.code === 200) {\n        const data = response.data;\n        if (data.exitValue === 0 || data.exitValue >= 10) {\n            output.value = data;\n        } else {\n            output.value = data;\n        }\n        if (data.message.trim() !== '') {\n            outputTextVis.value = true;\n        } else {\n            outputTextVis.value = false;\n        }\n    }\n    // 显示 output 区域\n    outputVisible.value = true;\n};\n\n// 获取语言扩展名的辅助函数\nconst getLanguageExtension = (languageName: string) => {\n    if (!languageName) return 'txt'; // 默认 txt 后缀\n\n    // 你可以根据不同语言函数推断其扩展名\n    const language = languageName.toLowerCase();\n\n    switch (language) {\n        case 'javascript': return 'js';\n        case 'python': return 'py';\n        case 'cpp': return 'cpp';\n        case 'java': return 'java';\n        // 添加更多语言映射\n        default: return 'txt';\n    }\n};\n\n// 监听 props 变化\nonMounted(() => {\n    watch(\n        () => props.code,\n        (_code) => {\n            code.value = _code\n        }\n    );\n    const handleKeyDown = (event: KeyboardEvent) => {\n        const isSaveShortcut =\n            (event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 's';\n\n        if (isSaveShortcut) {\n            event.preventDefault(); // 阻止默认保存行为\n            console.log('Save shortcut (Ctrl+S / Cmd+S) is disabled in the editor.');\n            // 可选：在这里执行其他逻辑，比如保存代码等\n        }\n    };\n\n    window.addEventListener('keydown', handleKeyDown);\n});\n\n// log 方法直接使用 console\nconst log = console.log\n</script>\n\n<style lang=\"scss\" scoped>\n@import '@/styles/variable.scss';\n\n.editor {\n    .divider {\n        height: 1px;\n        background-color: $border-color;\n    }\n\n    .main {\n\n        .code {\n            width: 30%;\n            height: 100px;\n            margin: 0;\n            padding: 0.4em;\n            overflow: scroll;\n            border-left: 1px solid $border-color;\n            font-family: monospace;\n        }\n    }\n\n    .footer {\n        height: 3rem;\n        padding: 0 1em;\n        display: flex;\n        justify-content: space-between;\n        align-items: center;\n        font-size: 90%;\n        background-color: var(--bg-elevated);\n        border-top: 1px solid var(--border-color);\n\n        .buttons {\n            .item {\n                margin-right: 1em;\n                display: inline-flex;\n                justify-content: center;\n                align-items: center;\n                background-color: transparent;\n                border: 1px dashed var(--border-color);\n                font-size: $font-size-small;\n                color: var(--text-secondary);\n                cursor: pointer;\n\n                .iconfont {\n                    margin-left: $xs-gap;\n                }\n\n                &:hover {\n                    color: var(--text-primary);\n                    border-color: var(--text-primary);\n                }\n            }\n\n            :deep(.el-button) {\n                background: var(--bg-primary);\n                border-color: var(--border-color);\n                color: var(--text-primary);\n                \n                &:hover, &:focus {\n                    background: var(--primary-start);\n                    border-color: var(--primary-start);\n                    color: #fff;\n                }\n                \n                &.el-button--primary {\n                    background: var(--primary-gradient);\n                    border: none;\n                    color: #fff;\n                    \n                    &:hover {\n                        opacity: 0.9;\n                    }\n                }\n            }\n        }\n\n        .infos {\n            color: var(--text-secondary);\n            .item {\n                margin-left: 2em;\n                display: inline-block;\n                font-feature-settings: 'tnum';\n            }\n        }\n    }\n\n    .output {\n        margin-top: 20px;\n        border: 1px solid var(--border-color);\n        border-radius: 4px;\n        padding: 20px;\n        background-color: var(--bg-elevated);\n        box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2);\n\n        .output-header {\n            display: flex;\n            justify-content: flex-start;\n            //margin-bottom: 10px;\n            color: var(--text-primary);\n\n            .closeoutput {\n                margin-left: auto;\n                cursor: pointer;\n            }\n\n            .running-status {\n                background-color: var(--bg-primary);\n                border: 1px solid var(--border-color);\n                border-radius: 4px;\n                margin-left: 10px;\n                padding: 0 10px;\n\n                span {\n                    color: var(--danger);\n                    font-size: 16px;\n                }\n            }\n\n            .custom-loading-svg {\n                background-color: transparent;\n                /* 设置背景为透明 */\n                border: none;\n                /* 清除边框 */\n                padding: 0;\n                /* 去掉内边距 */\n            }\n\n            span {\n                font-size: 18px;\n                font-weight: bold;\n            }\n\n            /* 根据状态动态改变颜色 */\n            .error-text {\n                color: var(--danger);\n                /* 红色表示错误 */\n            }\n\n            .success-text {\n                color: var(--success);\n                /* 绿色表示成功 */\n            }\n        }\n\n        .input-output-section {\n            display: flex;\n            flex-direction: column;\n            gap: 10px;\n            margin-top: 15px;\n\n            .input-field,\n            .output-field {\n                display: flex;\n                flex-direction: column;\n            }\n\n            label {\n                font-size: 14px;\n                margin-bottom: 5px;\n                font-weight: bold;\n                color: var(--text-secondary);\n            }\n\n            .input-content {\n                background-color: var(--bg-primary);\n                border: 1px solid var(--border-color);\n                border-radius: 4px;\n                padding: 10px;\n                font-size: 14px;\n                color: var(--text-primary);\n                line-height: 1.5;\n            }\n\n            .output-content {\n                background-color: var(--bg-primary);\n                border: 1px solid var(--border-color);\n                border-radius: 4px;\n                padding: 10px;\n                font-size: 14px;\n                color: var(--text-primary);\n                line-height: 1.5;\n                white-space: pre-wrap;\n                word-wrap: break-word;\n            }\n        }\n    }\n\n    .input-section {\n        margin-top: 20px;\n        border: 1px solid var(--border-color);\n        border-radius: 4px;\n        padding: 20px;\n        background-color: var(--bg-elevated);\n        box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2);\n\n        .input-field {\n            display: flex;\n            flex-direction: column;\n        }\n\n        label {\n            font-size: 14px;\n            margin-bottom: 5px;\n            font-weight: bold;\n            color: var(--text-secondary);\n        }\n\n        .input-content {\n            background-color: var(--bg-primary);\n            border: 1px solid var(--border-color);\n            border-radius: 4px;\n            padding: 10px;\n            font-size: 14px;\n            color: var(--text-primary);\n            line-height: 1.5;\n            min-height: 50px;\n            white-space: pre-wrap;\n            word-wrap: break-word;\n            outline: none;\n\n            &:focus {\n                border-color: var(--primary-start);\n                box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.2);\n            }\n        }\n\n        :deep(.el-textarea__inner) {\n            background-color: var(--bg-primary);\n            color: var(--text-primary);\n            border-color: var(--border-color);\n            box-shadow: none;\n            \n            &:focus {\n                border-color: var(--primary-start);\n                box-shadow: 0 0 0 1px var(--primary-start);\n            }\n        }\n    }\n}\n</style>"
  },
  {
    "path": "DOJ-FE/src/components/CodeEditor/showEditor.vue",
    "content": "<template>\n    <div class=\"editor\">\n        <div class=\"main\">\n            <codemirror v-model=\"code\" :style=\"{\n                width: config.width,\n                height: config.height,\n                backgroundColor: 'var(--bg-primary)',\n                color: 'var(--text-primary)',\n                fontSize: fontSizeStr,\n            }\" placeholder=\"Please enter the code.\" :extensions=\"extensions\" :disabled=\"true\"\n                :indent-with-tab=\"true\" :tab-size=\"config.tabSize\" @update=\"handleStateUpdate\" @ready=\"handleReady\" />\n        </div>\n        <div class=\"divider\"></div>\n        <div class=\"footer\">\n            <div class=\"infos\">\n                <span class=\"item\">Spaces: {{ config.tabSize }}</span>\n                <span class=\"item\">Length: {{ state.length }}</span>\n                <span class=\"item\">Lines: {{ state.lines }}</span>\n                <span class=\"item\">Cursor: {{ state.cursor }}</span>\n                <span class=\"item\">Selected: {{ state.selected }}</span>\n            </div>\n        </div>\n    </div>\n</template>\n\n<script lang=\"ts\" setup>\nimport { reactive, shallowRef, computed, watch, onMounted, ref } from 'vue'\nimport { EditorView, ViewUpdate } from '@codemirror/view'\nimport { Codemirror } from 'vue-codemirror'\nimport { ElButton, ElMessage } from 'element-plus'\nimport { Close, VideoPlay } from '@element-plus/icons-vue'\nimport { configType } from './index.vue'\nimport { reqSubmit } from '@/api/submit'\nimport { executeMessage } from '@/api/submit/type'\n\nconst props = defineProps<{\n    config: configType,\n    code: string,\n    theme: Object | Array<string>,\n    language: Function,\n    languageName: string\n}>();\n\n// 组件暴露\ndefineExpose({\n    Codemirror\n});\n\n// 响应式状态\nconst code = shallowRef(props.code);\nconst cmView = shallowRef<EditorView>();\nconst inputModel = shallowRef(false);  // 控制输入区域显示与否\nconst output = shallowRef<executeMessage>();  // 保存输出结果\nconst outputVisible = shallowRef(false);  // 控制 output 区域显示与否\nconst outputTextVis = shallowRef(false);  // 控制输入输出区域显示与否\n\nconst fontSizeStr = computed(() => `${props.config.fontSize}px`);\nconst timeInfo = computed(() => {\n    return output.value?.time ? `${(output.value.time * 1000).toFixed(2)} ms` : '0.00 ms';\n});\nconst memoryInfo = computed(() => {\n    return output.value?.memory ? `${(output.value.memory).toFixed(2)} KB` : '0.00 KB';\n});\n\nconst loading = ref(true);\nconst svg = `\n        <path class=\"path\" d=\"\n          M 30 15\n          L 28 17\n          M 25.61 25.61\n          A 15 15, 0, 0, 1, 15 30\n          A 15 15, 0, 1, 1, 27.99 7.5\n          L 15 15\n        \" style=\"stroke-width: 4px; fill: #F9F9F9\"/>\n      `\n\n// 计算属性\nconst extensions = computed(() => {\n    const result = [];\n    if (props.language) {\n        result.push(props.language());\n    }\n    if (props.theme) {\n        result.push(props.theme);\n    }\n    return result;\n});\n\nconst handleReady = ({ view }: any) => {\n    cmView.value = view;\n};\n\nconst state = reactive({\n    lines: null as null | number,\n    cursor: null as null | number,\n    selected: null as null | number,\n    length: null as null | number\n});\n\nconst handleStateUpdate = (viewUpdate: ViewUpdate) => {\n    const ranges = viewUpdate.state.selection.ranges\n    state.selected = ranges.reduce((plus, range) => plus + range.to - range.from, 0)\n    state.cursor = ranges[0].anchor\n    state.length = viewUpdate.state.doc.length\n    state.lines = viewUpdate.state.doc.lines\n};\n\nconst handleSubmit = async () => {\n\n    outputTextVis.value = false;\n\n    output.value = {\n        exitValue: -1, // 设置为 null 表示还没有结果\n        status: 'Running',\n        message: '',\n        time: 0,\n        memory: 0,\n    };\n\n    // 将代码字符串转换为 Blob 文件\n    const codeBlob = new Blob([code.value], { type: 'text/plain' });\n\n    // 获取语言扩展名\n    const languageExtension = getLanguageExtension(props.languageName);\n\n    // 创建 FormData\n    const formData = new FormData();\n    formData.append('file', codeBlob, `Main.${languageExtension}`);\n    formData.append('language', props.languageName);\n\n    ElMessage.success('提交成功');\n\n    // 发送请求\n    const response = (await reqSubmit(formData)).data;\n\n    // 成功时处理\n    if (response.code === 200) {\n        const data = response.data;\n        if (data.exitValue === 0 || data.exitValue >= 10) {\n            output.value = data;\n        } else {\n            output.value = data;\n        }\n        if (data.message.trim() !== '') {\n            outputTextVis.value = true;\n        }else{\n            outputTextVis.value = false;\n        }\n    }\n    // 显示 output 区域\n    outputVisible.value = true;\n};\n\n// 获取语言扩展名的辅助函数\nconst getLanguageExtension = (languageName: string) => {\n    if (!languageName) return 'txt'; // 默认 txt 后缀\n\n    // 你可以根据不同语言函数推断其扩展名\n    const language = languageName.toLowerCase();\n\n    switch (language) {\n        case 'javascript': return 'js';\n        case 'python': return 'py';\n        case 'cpp': return 'cpp';\n        case 'java': return 'java';\n        // 添加更多语言映射\n        default: return 'txt';\n    }\n};\n\n// 监听 props 变化\nonMounted(() => {\n    watch(\n        () => props.code,\n        (_code) => {\n            code.value = _code\n        }\n    );\n    const handleKeyDown = (event: KeyboardEvent) => {\n        const isSaveShortcut = \n            (event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 's';\n\n        if (isSaveShortcut) {\n            event.preventDefault(); // 阻止默认保存行为\n            console.log('Save shortcut (Ctrl+S / Cmd+S) is disabled in the editor.');\n            // 可选：在这里执行其他逻辑，比如保存代码等\n        }\n    };\n\n    window.addEventListener('keydown', handleKeyDown);\n});\n\n// log 方法直接使用 console\nconst log = console.log\n</script>\n\n<style lang=\"scss\" scoped>\n@import '@/styles/variable.scss';\n\n.editor {\n    .divider {\n        height: 1px;\n        background-color: $border-color;\n    }\n\n    .main {\n\n        .code {\n            width: 30%;\n            height: 100px;\n            margin: 0;\n            padding: 0.4em;\n            overflow: scroll;\n            border-left: 1px solid $border-color;\n            font-family: monospace;\n        }\n    }\n\n    .footer {\n        height: 3rem;\n        padding: 0 1em;\n        display: flex;\n        justify-content: space-between;\n        align-items: center;\n        font-size: 90%;\n        background-color: var(--bg-elevated);\n        border-top: 1px solid var(--border-color);\n\n        .buttons {\n            .item {\n                margin-right: 1em;\n                display: inline-flex;\n                justify-content: center;\n                align-items: center;\n                background-color: transparent;\n                border: 1px dashed var(--border-color);\n                font-size: $font-size-small;\n                color: var(--text-secondary);\n                cursor: pointer;\n\n                .iconfont {\n                    margin-left: $xs-gap;\n                }\n\n                &:hover {\n                    color: var(--text-primary);\n                    border-color: var(--text-primary);\n                }\n            }\n        }\n\n        .infos {\n            color: var(--text-secondary);\n            .item {\n                margin-left: 2em;\n                display: inline-block;\n                font-feature-settings: 'tnum';\n            }\n        }\n    }\n\n    .output {\n        margin-top: 20px;\n        border: 1px solid var(--border-color);\n        border-radius: 4px;\n        padding: 20px;\n        background-color: var(--bg-elevated);\n        box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2);\n\n        .output-header {\n            display: flex;\n            justify-content: flex-start;\n            //margin-bottom: 10px;\n            color: var(--text-primary);\n\n            .closeoutput {\n                margin-left: auto;\n                cursor: pointer;\n            }\n\n            .running-status {\n                background-color: var(--bg-primary);\n                border: 1px solid var(--border-color);\n                border-radius: 4px;\n                margin-left: 10px;\n                padding: 0 10px;\n\n                span {\n                    color: var(--danger);\n                    font-size: 16px;\n                }\n            }\n\n            .custom-loading-svg {\n                background-color: transparent;\n                /* 设置背景为透明 */\n                border: none;\n                /* 清除边框 */\n                padding: 0;\n                /* 去掉内边距 */\n            }\n\n            span {\n                font-size: 18px;\n                font-weight: bold;\n            }\n\n            /* 根据状态动态改变颜色 */\n            .error-text {\n                color: var(--danger);\n                /* 红色表示错误 */\n            }\n\n            .success-text {\n                color: var(--success);\n                /* 绿色表示成功 */\n            }\n        }\n\n        .input-output-section {\n            display: flex;\n            flex-direction: column;\n            gap: 10px;\n            margin-top: 15px;\n\n            .input-field,\n            .output-field {\n                display: flex;\n                flex-direction: column;\n            }\n\n            label {\n                font-size: 14px;\n                margin-bottom: 5px;\n                font-weight: bold;\n                color: var(--text-secondary);\n            }\n\n            .input-content {\n                background-color: var(--bg-primary);\n                border: 1px solid var(--border-color);\n                border-radius: 4px;\n                padding: 10px;\n                font-size: 14px;\n                color: var(--text-primary);\n                line-height: 1.5;\n            }\n\n            .output-content {\n                background-color: var(--bg-primary);\n                border: 1px solid var(--border-color);\n                border-radius: 4px;\n                padding: 10px;\n                font-size: 14px;\n                color: var(--text-primary);\n                line-height: 1.5;\n                white-space: pre-wrap;\n                word-wrap: break-word;\n            }\n        }\n    }\n\n}\n</style>"
  },
  {
    "path": "DOJ-FE/src/components/CodeEditor/themes.ts",
    "content": "export { oneDark } from '@codemirror/theme-one-dark'\nexport { materialDark } from 'cm6-theme-material-dark'\nexport { nord } from 'cm6-theme-nord'\nexport {\n  amy,\n  ayuLight,\n  barf,\n  bespin,\n  birdsOfParadise,\n  boysAndGirls,\n  clouds,\n  cobalt,\n  coolGlow,\n  dracula,\n  espresso,\n  noctisLilac,\n  rosePineDawn,\n  smoothy,\n  solarizedLight,\n  tomorrow\n} from 'thememirror' \n"
  },
  {
    "path": "DOJ-FE/src/components/CodeEditor/toolbar.vue",
    "content": "<template>\n    <div class=\"toolbar\">\n        <div class=\"item\">\n            <label for=\"language\">language:</label>\n            <select name=\"language\" id=\"language\" :value=\"config.language\" @change=\"handleSelectLanguage\">\n                <option :value=\"option\" :key=\"option\" v-for=\"option in languages\">\n                    {{ option }}\n                </option>\n            </select>\n        </div>\n        <div class=\"item\">\n            <label for=\"theme\">theme:</label>\n            <select name=\"theme\" id=\"theme\" v-model=\"config.theme\">\n                <option :value=\"option\" :key=\"option\" v-for=\"option in ['default', ...themes]\">\n                    {{ option }}\n                </option>\n            </select>\n        </div>\n        <div class=\"item\">\n            <label for=\"fontSize\">tabSize:</label>\n            <select name=\"fontSize\" id=\"fontSize\" v-model.number=\"config.fontSize\">\n                <option :value=\"option\" :key=\"option\" v-for=\"option in [16, 18, 20, 22, 24, 26]\">\n                    {{ option }}\n                </option>\n            </select>\n        </div>\n        <div class=\"item\">\n            <label for=\"tabSize\">tabSize:</label>\n            <select name=\"tabSize\" id=\"tabSize\" v-model.number=\"config.tabSize\">\n                <option :value=\"option\" :key=\"option\" v-for=\"option in [2, 4, 6, 8]\">\n                    {{ option }}\n                </option>\n            </select>\n        </div>\n        <div class=\"item\">\n            <button class=\"full-button\" @click=\"handleFullScreen\">\n                <el-icon class=\"full-icon\">\n                    <FullScreen />\n                </el-icon>\n            </button>\n        </div>\n    </div>\n</template>\n  \n<script lang=\"ts\" setup>\nimport { toRefs } from 'vue';\nimport { defineProps, defineEmits } from 'vue';\nimport { FullScreen } from '@element-plus/icons-vue';\nimport { configType } from './index.vue';\n\nconst props = defineProps<{\n    config: configType,\n    languages: Array<string>,\n    themes: Array<string>,\n}>();\n\nconst emit = defineEmits(['language', 'fullscreen']);\nconst { config, languages, themes } = toRefs(props);\n\nconst handleSelectLanguage = (event: Event) => {\n    const target = event.target as HTMLSelectElement;\n    emit('language', target.value);\n};\n\nconst handleFullScreen = () => {\n    emit('fullscreen');\n};\n</script>\n  \n<style lang=\"scss\" scoped>\n.toolbar {\n    display: flex;\n    justify-content: flex-start;\n    align-items: center;\n    gap: 50px;\n    height: 3rem;\n    padding: 0 1em;\n    background-color: var(--bg-elevated);\n    border-bottom: 1px solid var(--border-color);\n    color: var(--text-primary);\n\n    .item {\n        display: inline-flex;\n        align-items: center;\n\n        label {\n            display: inline-block;\n            margin-right: 0.2em;\n            color: var(--text-secondary);\n        }\n\n        .full-button {\n            border: none;\n            background: none;\n            color: var(--text-primary);\n            cursor: pointer;\n\n            .full-icon {\n                font-size: 1.5rem;\n            }\n            \n            &:hover {\n                color: var(--primary-start);\n            }\n        }\n\n        &:last-child {\n            margin-left: auto;\n        }\n    }\n\n    input,\n    button,\n    select {\n        margin: 0;\n        background-color: var(--bg-primary);\n        color: var(--text-primary);\n        border: 1px solid var(--border-color);\n        padding: 4px 8px;\n        border-radius: 4px;\n        \n        &:focus {\n            outline: none;\n            border-color: var(--primary-start);\n        }\n    }\n\n    select {\n        max-width: 8em;\n    }\n}\n</style>\n  "
  },
  {
    "path": "DOJ-FE/src/components/SvgIcon/index.vue",
    "content": "<template>\n    <!-- svg:图标外层容器节点,内部需要与use标签结合使用 -->\n    <svg :style=\"{ width, height }\">\n        <!-- xlink:href执行用哪一个图标,属性值务必#icon-图标名字 -->\n        <!-- use标签fill属性可以设置图标的颜色 -->\n        <use :xlink:href=\"prefix + name\" :fill=\"color\"></use>\n    </svg>\n</template>\n\n<script setup lang=\"ts\">\n//接受父组件传递过来的参数\ndefineProps({\n    //xlink:href属性值前缀\n    prefix: {\n        type: String,\n        default: '#icon-'\n    },\n    //提供使用的图标名字\n    name: String,\n    //接受父组件传递颜色\n    color: {\n        type: String,\n        default: ''\n    },\n    //接受父组件传递过来的图标的宽度\n    width: {\n        type: String,\n        default: '16px'\n    },\n    //接受父组件传递过来的图标的高度\n    height: {\n        type: String,\n        default: '16px'\n    }\n})\n</script>\n\n<style scoped></style>"
  },
  {
    "path": "DOJ-FE/src/components/index.ts",
    "content": "import { App, Component } from 'vue';\nimport svgIcon from './SvgIcon/index.vue';\nimport CodeEditor from './CodeEditor/index.vue';\n\ntype GlobalComponents = {\n    [key: string]: Component\n}\nconst allGlobalComponents: GlobalComponents = {svgIcon, CodeEditor};\n// 对外暴露插件对象\nexport default {\n    install(app: App){\n        Object.keys(allGlobalComponents).forEach(key => {\n            app.component(key, allGlobalComponents[key]);\n        });\n    }\n}"
  },
  {
    "path": "DOJ-FE/src/main.ts",
    "content": "import { createApp } from \"vue\";\nimport App from \"@/App.vue\";\n// elm组件\nimport ElementPlus from 'element-plus';\nimport 'element-plus/dist/index.css';\n//@ts-ignore\nimport zhCn from 'element-plus/dist/locale/zh-cn.mjs';\n// svg图标服务\nimport 'virtual:svg-icons-register';\n// 常用的组件 进行全局注册\nimport globalComponents from \"@/components\";\n// 引入全局样式\nimport \"@/styles/index.scss\";\n// 引入路由\nimport router from './router';\n// pinia\nimport { createPinia } from \"pinia\";\nimport piniaPluginPersistedstate from 'pinia-plugin-persistedstate';\nconst pinia = createPinia();\npinia.use(piniaPluginPersistedstate)\n\nconst app = createApp(App);\n\n// use里面的组件 必须包含install方法\napp.use(pinia);\napp.use(router);\napp.use(globalComponents);\napp.use(ElementPlus, {\n    locale: zhCn\n})\napp.mount('#app');\n"
  },
  {
    "path": "DOJ-FE/src/router/index.ts",
    "content": "// createRouter：创建router实例对象\n// createWebHistory：创建history模式的路由\n\nimport { createRouter, createWebHistory } from 'vue-router'\n\nimport NProgress from 'nprogress';\nimport 'nprogress/nprogress.css';\n\nimport Layout from '@/views/Layout/index.vue'\nimport Home from '@/views/Home/index.vue'\nimport Login from '@/views/User/login/index.vue'\nimport Register from '@/views/User/register/index.vue'\nimport Info from '@/views/User/info/index.vue'\nimport UserHome from '@/views/User/home/index.vue'\nimport OnlineEditor from '@/views/OnlineEditor/index.vue'\nimport Problem from '@/views/Problem/index.vue'\nimport ProblemDetail from '@/views/Problem/detail/index.vue'\nimport Status from '@/views/Status/index.vue'\n\nconst router = createRouter({\n    history: createWebHistory(import.meta.env.BASE_URL),\n    // path和component对应关系的位置\n    routes: [\n        {\n            path: '/',\n            component: Layout,\n            children: [\n                {\n                    path: '',\n                    component: Home\n                }, {\n                    path: '/login',\n                    component: Login\n                }, {\n                    path: '/register',\n                    component: Register\n                }, {\n                    path: '/info',\n                    component: Info\n                }, {\n                    path: '/home',\n                    component: UserHome\n                }, {\n                    path: '/online',\n                    component: OnlineEditor\n                }, {\n                    path: '/problem',\n                    component: Problem\n                }, {\n                    path: '/problem/:id',\n                    component: ProblemDetail\n                }, {\n                    path: '/status',\n                    component: Status\n                }, {\n                    path: '/rankings',\n                    component: () => import('@/views/Rankings/index.vue')\n                }, {\n                    path: '/admin',\n                    component: () => import('@/views/Admin/index.vue'),\n                    // You typically want admin guard here, but keeping simple for now\n                }\n            ]\n        }\n    ],\n    // 路由滚动行为定制\n    scrollBehavior() {\n        return {\n            top: 0\n        }\n    }\n});\n\n// 在路由导航前，启动进度条\nrouter.beforeEach((to, from, next) => {\n    NProgress.start();\n    if (to.path === '/login') {\n        sessionStorage.setItem('redirect', from.path);\n    }\n    next();\n});\n\n// 在路由导航结束后，关闭进度条\nrouter.afterEach(() => {\n    NProgress.done();\n});\n\nexport default router\n"
  },
  {
    "path": "DOJ-FE/src/stores/userStore/index.ts",
    "content": "// 管理用户数据相关\n\nimport { defineStore } from 'pinia';\nimport { ref } from 'vue';\nimport { reqLogin } from '@/api/user';\nimport { loginForm, loginResponseData } from '@/api/user/type';\n\n// 定义用户信息的接口类型\ninterface UserInfo {\n    userId: number;\n    username: string;\n    accessToken: string;\n    refreshToken: string;\n    avatar?: string;\n    role: boolean; // false = admin, true = user (based on backend logic)\n}\n\nexport const useUserStore = defineStore('user', () => {\n    const userInfo = ref<UserInfo | undefined>();\n\n    const getUserInfo = async (form: loginForm) => {\n        const res = await reqLogin(form);\n        userInfo.value = res.data.data;\n    };\n\n    const clearUserInfo = () => {\n        userInfo.value = undefined;\n    };\n\n    // 新增一个 action，只用于更新 accessToken\n    const setAccessToken = (token: string) => {\n        if (userInfo.value) {\n            userInfo.value.accessToken = token;\n        }\n    };\n\n    // 以对象的格式把state和action return\n    return {\n        userInfo,\n        getUserInfo,\n        clearUserInfo,\n        setAccessToken, // 暴露新的 action\n    };\n}, {\n    persist: true,\n});"
  },
  {
    "path": "DOJ-FE/src/styles/index.scss",
    "content": "// ================================\n// D-OnlineJudge Design System\n// Component Styles & Utilities\n// ================================\n\n@import './theme.scss';\n\n// ========== Base Reset ==========\n* {\n  box-sizing: border-box;\n  margin: 0;\n  padding: 0;\n}\n\nhtml {\n  font-size: 16px;\n  scroll-behavior: smooth;\n}\n\nbody {\n  font-family: $font-family;\n  font-size: $font-size-base;\n  line-height: $line-height-normal;\n  color: var(--text-primary);\n  background: var(--bg-primary);\n  min-height: 100vh;\n  overflow-x: hidden;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n}\n\n// ========== Scrollbar ==========\n::-webkit-scrollbar {\n  width: 8px;\n  height: 8px;\n}\n\n::-webkit-scrollbar-track {\n  background: var(--bg-secondary);\n}\n\n::-webkit-scrollbar-thumb {\n  background: rgba(0, 0, 0, 0.1); \n  border-radius: $radius-full;\n  \n  &:hover {\n    background: rgba(0, 0, 0, 0.2); \n  }\n}\n\n// Dark mode override if needed (handled by variables usually, but simple rgba works for basic gray)\n[data-theme='dark'] ::-webkit-scrollbar-thumb {\n    background: rgba(255, 255, 255, 0.2);\n    &:hover {\n        background: rgba(255, 255, 255, 0.3);\n    }\n}\n\n// ========== Links ==========\na {\n  color: var(--primary-start);\n  text-decoration: none;\n  transition: color $transition-fast;\n  \n  &:hover {\n    color: var(--primary-end);\n  }\n}\n\n// ========== Typography ==========\nh1, h2, h3, h4, h5, h6 {\n  font-weight: $font-weight-semibold;\n  line-height: $line-height-tight;\n  color: var(--text-primary);\n}\n\nh1 { font-size: $font-size-4xl; }\nh2 { font-size: $font-size-3xl; }\nh3 { font-size: $font-size-2xl; }\nh4 { font-size: $font-size-xl; }\nh5 { font-size: $font-size-lg; }\nh6 { font-size: $font-size-base; }\n\n.text-gradient {\n  @include gradient-text;\n}\n\n.text-muted {\n  color: var(--text-muted);\n}\n\n.text-secondary {\n  color: var(--text-secondary);\n}\n\n// ========== Glass Card ==========\n.card-glass {\n  @include glass;\n  @include card;\n  padding: $space-lg;\n  transition: all $transition-normal;\n  \n  &:hover {\n    border-color: var(--border-light);\n    box-shadow: var(--shadow-lg);\n  }\n}\n\n.card-solid {\n  @include card;\n  padding: $space-lg;\n}\n\n// ========== Buttons ==========\n.btn {\n  display: inline-flex;\n  align-items: center;\n  justify-content: center;\n  gap: $space-sm;\n  padding: $space-sm $space-lg;\n  font-size: $font-size-sm;\n  font-weight: $font-weight-medium;\n  border-radius: $radius-md;\n  border: none;\n  cursor: pointer;\n  transition: all $transition-normal;\n  text-decoration: none;\n  \n  &:disabled {\n    opacity: 0.5;\n    cursor: not-allowed;\n  }\n}\n\n.btn-primary {\n  @extend .btn;\n  background: var(--primary-gradient);\n  color: white;\n  box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);\n  \n  &:hover:not(:disabled) {\n    transform: translateY(-2px);\n    box-shadow: 0 6px 20px rgba(102, 126, 234, 0.5);\n  }\n  \n  &:active {\n    transform: translateY(0);\n  }\n}\n\n.btn-secondary {\n  @extend .btn;\n  background: transparent;\n  color: var(--text-primary);\n  border: 1px solid var(--border-light);\n  \n  &:hover:not(:disabled) {\n    background: var(--glass-bg);\n    border-color: var(--primary-start);\n    color: var(--primary-start);\n  }\n}\n\n.btn-ghost {\n  @extend .btn;\n  background: transparent;\n  color: var(--text-secondary);\n  \n  &:hover:not(:disabled) {\n    background: var(--glass-bg);\n    color: var(--text-primary);\n  }\n}\n\n.btn-success {\n  @extend .btn;\n  background: $success;\n  color: white;\n  \n  &:hover:not(:disabled) {\n    background: darken($success, 5%);\n    transform: translateY(-2px);\n  }\n}\n\n.btn-danger {\n  @extend .btn;\n  background: $danger;\n  color: white;\n  \n  &:hover:not(:disabled) {\n    background: darken($danger, 5%);\n    transform: translateY(-2px);\n  }\n}\n\n// ========== Input ==========\n.input {\n  width: 100%;\n  padding: $space-sm $space-md;\n  font-size: $font-size-base;\n  color: var(--text-primary);\n  background: var(--bg-elevated);\n  border: 1px solid var(--border-color);\n  border-radius: $radius-md;\n  outline: none;\n  transition: all $transition-fast;\n  \n  &::placeholder {\n    color: var(--text-muted);\n  }\n  \n  &:focus {\n    border-color: var(--primary-start);\n    box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.2);\n  }\n  \n  &:disabled {\n    opacity: 0.5;\n    cursor: not-allowed;\n  }\n}\n\n// ========== Tags / Badges ==========\n.tag {\n  display: inline-flex;\n  align-items: center;\n  padding: $space-xs $space-sm;\n  font-size: $font-size-xs;\n  font-weight: $font-weight-medium;\n  border-radius: $radius-sm;\n  \n  &.tag-success {\n    background: $success-light;\n    color: $success;\n  }\n  \n  &.tag-warning {\n    background: $warning-light;\n    color: $warning;\n  }\n  \n  &.tag-danger {\n    background: $danger-light;\n    color: $danger;\n  }\n  \n  &.tag-info {\n    background: $info-light;\n    color: $info;\n  }\n  \n  &.tag-easy {\n    background: rgba($difficulty-easy, 0.15);\n    color: $difficulty-easy;\n  }\n  \n  &.tag-medium {\n    background: rgba($difficulty-medium, 0.15);\n    color: $difficulty-medium;\n  }\n  \n  &.tag-hard {\n    background: rgba($difficulty-hard, 0.15);\n    color: $difficulty-hard;\n  }\n}\n\n// ========== Modern Table ==========\n.table-modern {\n  width: 100%;\n  border-collapse: separate;\n  border-spacing: 0;\n  \n  th, td {\n    padding: $space-md;\n    text-align: left;\n    border-bottom: 1px solid var(--border-color);\n  }\n  \n  th {\n    font-size: $font-size-sm;\n    font-weight: $font-weight-semibold;\n    color: var(--text-secondary);\n    text-transform: uppercase;\n    letter-spacing: 0.05em;\n    background: var(--bg-elevated);\n  }\n  \n  tbody tr {\n    transition: background $transition-fast;\n    \n    &:hover {\n      background: var(--glass-bg);\n    }\n    \n    &:last-child td {\n      border-bottom: none;\n    }\n  }\n}\n\n// ========== Animations ==========\n@keyframes fadeIn {\n  from {\n    opacity: 0;\n    transform: translateY(10px);\n  }\n  to {\n    opacity: 1;\n    transform: translateY(0);\n  }\n}\n\n@keyframes fadeInUp {\n  from {\n    opacity: 0;\n    transform: translateY(20px);\n  }\n  to {\n    opacity: 1;\n    transform: translateY(0);\n  }\n}\n\n@keyframes pulse {\n  0%, 100% {\n    opacity: 1;\n  }\n  50% {\n    opacity: 0.5;\n  }\n}\n\n@keyframes shimmer {\n  0% {\n    background-position: -200% 0;\n  }\n  100% {\n    background-position: 200% 0;\n  }\n}\n\n@keyframes glow {\n  0%, 100% {\n    box-shadow: 0 0 5px rgba(102, 126, 234, 0.5);\n  }\n  50% {\n    box-shadow: 0 0 20px rgba(102, 126, 234, 0.8);\n  }\n}\n\n.animate-fade-in {\n  animation: fadeIn $transition-normal ease-out;\n}\n\n.animate-fade-in-up {\n  animation: fadeInUp $transition-slow ease-out;\n}\n\n.animate-pulse {\n  animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;\n}\n\n// ========== Layout Utilities ==========\n.container {\n  width: 100%;\n  max-width: $container-max;\n  margin: 0 auto;\n  padding: 0 $container-padding;\n}\n\n.flex {\n  display: flex;\n}\n\n.flex-col {\n  flex-direction: column;\n}\n\n.items-center {\n  align-items: center;\n}\n\n.justify-center {\n  justify-content: center;\n}\n\n.justify-between {\n  justify-content: space-between;\n}\n\n.gap-sm {\n  gap: $space-sm;\n}\n\n.gap-md {\n  gap: $space-md;\n}\n\n.gap-lg {\n  gap: $space-lg;\n}\n\n// ========== Spacing Utilities ==========\n.mt-sm { margin-top: $space-sm; }\n.mt-md { margin-top: $space-md; }\n.mt-lg { margin-top: $space-lg; }\n.mt-xl { margin-top: $space-xl; }\n\n.mb-sm { margin-bottom: $space-sm; }\n.mb-md { margin-bottom: $space-md; }\n.mb-lg { margin-bottom: $space-lg; }\n.mb-xl { margin-bottom: $space-xl; }\n\n.p-sm { padding: $space-sm; }\n.p-md { padding: $space-md; }\n.p-lg { padding: $space-lg; }\n.p-xl { padding: $space-xl; }\n\n// ========== Element Plus Overrides ==========\n.el-table {\n  --el-table-bg-color: transparent !important;\n  --el-table-tr-bg-color: transparent !important;\n  --el-table-header-bg-color: var(--bg-elevated) !important;\n  --el-table-border-color: var(--border-color) !important;\n  --el-table-text-color: var(--text-primary) !important;\n  --el-table-header-text-color: var(--text-secondary) !important;\n  \n  background: transparent !important;\n  \n  .el-table__row:hover > td {\n    background: var(--glass-bg) !important;\n  }\n}\n\n.el-card {\n  --el-card-bg-color: var(--bg-card) !important;\n  --el-card-border-color: var(--border-color) !important;\n  border-radius: $radius-lg !important;\n}\n\n.el-input__wrapper {\n  background: var(--bg-elevated) !important;\n  border: 1px solid var(--border-color) !important;\n  box-shadow: none !important;\n  border-radius: $radius-md !important;\n  \n  &:hover {\n    border-color: var(--border-light) !important;\n  }\n  \n  &.is-focus {\n    border-color: var(--primary-start) !important;\n    box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.2) !important;\n  }\n}\n\n.el-input__inner {\n  color: var(--text-primary) !important;\n  \n  &::placeholder {\n    color: var(--text-muted) !important;\n  }\n}\n\n.el-select__wrapper {\n  background: var(--bg-elevated) !important;\n  border: 1px solid var(--border-color) !important;\n  box-shadow: none !important;\n  border-radius: $radius-md !important;\n}\n\n.el-button--primary {\n  background: var(--primary-gradient) !important;\n  border: none !important;\n  \n  &:hover {\n    opacity: 0.9;\n  }\n}\n\n.el-pagination {\n  --el-pagination-bg-color: transparent !important;\n  --el-pagination-button-bg-color: var(--bg-elevated) !important;\n  --el-pagination-button-color: var(--text-secondary) !important;\n  --el-pagination-hover-color: var(--primary-start) !important;\n}\n\n.el-tag {\n  border-radius: $radius-sm !important;\n}\n\n.el-dropdown-menu {\n  background: var(--bg-card) !important;\n  border: 1px solid var(--border-color) !important;\n  border-radius: $radius-md !important;\n  backdrop-filter: blur(var(--glass-blur));\n}\n\n.el-dropdown-menu__item {\n  color: var(--text-primary) !important;\n  \n  &:hover {\n    background: var(--glass-bg) !important;\n    color: var(--primary-start) !important;\n  }\n}"
  },
  {
    "path": "DOJ-FE/src/styles/theme.scss",
    "content": "\n\n// ========== CSS Variables (Theme Support) ==========\n:root {\n  // Colors\n  --primary-start: #{$primary-start};\n  --primary-end: #{$primary-end};\n  --primary-gradient: #{$primary-gradient};\n  \n  --accent-cyan: #{$accent-cyan};\n  --accent-green: #{$accent-green};\n  --accent-orange: #{$accent-orange};\n  --accent-pink: #{$accent-pink};\n  \n  --success: #{$success};\n  --success-light: #{$success-light};\n  --warning: #{$warning};\n  --warning-light: #{$warning-light};\n  --danger: #{$danger};\n  --danger-light: #{$danger-light};\n  --info: #{$info};\n  --info-light: #{$info-light};\n  \n  // Dark theme as default\n  --bg-primary: #{$dark-bg-primary};\n  --bg-secondary: #{$dark-bg-secondary};\n  --bg-tertiary: #{$dark-bg-tertiary};\n  --bg-card: #{$dark-bg-card};\n  --bg-elevated: #{$dark-bg-elevated};\n  \n  --border-color: #{$dark-border};\n  --border-light: #{$dark-border-light};\n  \n  --text-primary: #{$dark-text-primary};\n  --text-secondary: #{$dark-text-secondary};\n  --text-muted: #{$dark-text-muted};\n  --text-disabled: #{$dark-text-disabled};\n  \n  // Shadows\n  --shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.3);\n  --shadow-md: 0 4px 16px rgba(0, 0, 0, 0.4);\n  --shadow-lg: 0 8px 32px rgba(0, 0, 0, 0.5);\n  --shadow-glow: 0 0 20px rgba(102, 126, 234, 0.3);\n  \n  // Glass effect\n  --glass-bg: rgba(255, 255, 255, 0.05);\n  --glass-border: rgba(255, 255, 255, 0.1);\n  --glass-blur: 12px;\n}\n\n// Light theme override\n[data-theme=\"light\"] {\n  --bg-primary: #{$light-bg-primary};\n  --bg-secondary: #{$light-bg-secondary};\n  --bg-tertiary: #{$light-bg-tertiary};\n  --bg-card: #{$light-bg-card};\n  --bg-elevated: rgba(255, 255, 255, 0.8);\n  \n  --border-color: #{$light-border};\n  --border-light: #{$light-border-light};\n  \n  --text-primary: #{$light-text-primary};\n  --text-secondary: #{$light-text-secondary};\n  --text-muted: #{$light-text-muted};\n  --text-disabled: #cbd5e1;\n  \n  --shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.08);\n  --shadow-md: 0 4px 16px rgba(0, 0, 0, 0.12);\n  --shadow-lg: 0 8px 32px rgba(0, 0, 0, 0.16);\n  --shadow-glow: 0 0 20px rgba(102, 126, 234, 0.15);\n  \n  --glass-bg: rgba(255, 255, 255, 0.7);\n  --glass-border: rgba(0, 0, 0, 0.08);\n}\n"
  },
  {
    "path": "DOJ-FE/src/styles/variable.scss",
    "content": "@use 'sass:math';\n\n// ================================\n// D-OnlineJudge Design System\n// Modern Dark Theme Variables\n// ================================\n\n// ========== Color Palette ==========\n\n// Primary Gradient Colors\n$primary-start: #667eea;\n$primary-end: #764ba2;\n$primary-gradient: linear-gradient(135deg, $primary-start 0%, $primary-end 100%);\n\n// Accent Colors\n$accent-cyan: #00d4ff;\n$accent-green: #00f5a0;\n$accent-orange: #ff9a3c;\n$accent-pink: #ff6b9d;\n$accent-purple: #a855f7;\n\n// Status Colors\n$success: #10b981;\n$success-light: rgba(16, 185, 129, 0.15);\n$warning: #f59e0b;\n$warning-light: rgba(245, 158, 11, 0.15);\n$danger: #ef4444;\n$danger-light: rgba(239, 68, 68, 0.15);\n$info: #3b82f6;\n$info-light: rgba(59, 130, 246, 0.15);\n\n// Difficulty Colors\n$difficulty-easy: #10b981;\n$difficulty-medium: #f59e0b;\n$difficulty-hard: #ef4444;\n\n// ========== Dark Theme ==========\n$dark-bg-primary: #0f0f23;\n$dark-bg-secondary: #1a1a2e;\n$dark-bg-tertiary: #16213e;\n$dark-bg-card: rgba(26, 26, 46, 0.8);\n$dark-bg-elevated: rgba(30, 30, 60, 0.6);\n\n$dark-border: rgba(255, 255, 255, 0.08);\n$dark-border-light: rgba(255, 255, 255, 0.12);\n\n$dark-text-primary: #ffffff;\n$dark-text-secondary: rgba(255, 255, 255, 0.7);\n$dark-text-muted: rgba(255, 255, 255, 0.5);\n$dark-text-disabled: rgba(255, 255, 255, 0.3);\n\n// ========== Light Theme ==========\n$light-bg-primary: #f8fafc;\n$light-bg-secondary: #ffffff;\n$light-bg-tertiary: #f1f5f9;\n$light-bg-card: rgba(255, 255, 255, 0.9);\n\n$light-border: rgba(0, 0, 0, 0.08);\n$light-border-light: rgba(0, 0, 0, 0.12);\n\n$light-text-primary: #1e293b;\n$light-text-secondary: #64748b;\n$light-text-muted: #94a3b8;\n\n\n\n// ========== Layout ==========\n$navbar-height: 64px;\n$footer-height: 60px;\n$container-max: 1400px;\n$container-padding: 24px;\n\n// ========== Spacing ==========\n$space-xs: 4px;\n$space-sm: 8px;\n$space-md: 16px;\n$space-lg: 24px;\n$space-xl: 32px;\n$space-2xl: 48px;\n$space-3xl: 64px;\n\n// ========== Border Radius ==========\n$radius-sm: 6px;\n$radius-md: 12px;\n$radius-lg: 16px;\n$radius-xl: 24px;\n$radius-full: 9999px;\n\n// ========== Typography ==========\n$font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n$font-mono: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace;\n\n$font-size-xs: 12px;\n$font-size-sm: 14px;\n$font-size-base: 16px;\n$font-size-lg: 18px;\n$font-size-xl: 20px;\n$font-size-2xl: 24px;\n$font-size-3xl: 30px;\n$font-size-4xl: 36px;\n$font-size-5xl: 48px;\n\n$font-weight-normal: 400;\n$font-weight-medium: 500;\n$font-weight-semibold: 600;\n$font-weight-bold: 700;\n\n$line-height-tight: 1.25;\n$line-height-normal: 1.5;\n$line-height-relaxed: 1.75;\n\n// ========== Transitions ==========\n$transition-fast: 150ms ease;\n$transition-normal: 300ms ease;\n$transition-slow: 500ms ease;\n\n// ========== Z-Index ==========\n$z-dropdown: 100;\n$z-sticky: 200;\n$z-fixed: 300;\n$z-modal-backdrop: 400;\n$z-modal: 500;\n$z-tooltip: 600;\n\n// ========== Breakpoints ==========\n$breakpoint-sm: 640px;\n$breakpoint-md: 768px;\n$breakpoint-lg: 1024px;\n$breakpoint-xl: 1280px;\n$breakpoint-2xl: 1536px;\n\n// ========== Mixins ==========\n\n// Responsive\n@mixin mobile {\n  @media (max-width: #{$breakpoint-md - 1}) {\n    @content;\n  }\n}\n\n@mixin tablet {\n  @media (min-width: $breakpoint-md) and (max-width: #{$breakpoint-lg - 1}) {\n    @content;\n  }\n}\n\n@mixin desktop {\n  @media (min-width: $breakpoint-lg) {\n    @content;\n  }\n}\n\n// Glass Effect\n@mixin glass {\n  background: var(--glass-bg);\n  backdrop-filter: blur(var(--glass-blur));\n  -webkit-backdrop-filter: blur(var(--glass-blur));\n  border: 1px solid var(--glass-border);\n}\n\n// Gradient Text\n@mixin gradient-text {\n  background: var(--primary-gradient);\n  -webkit-background-clip: text;\n  -webkit-text-fill-color: transparent;\n  background-clip: text;\n}\n\n// Card Style\n@mixin card {\n  background: var(--bg-card);\n  border-radius: $radius-lg;\n  border: 1px solid var(--border-color);\n  box-shadow: var(--shadow-md);\n}\n\n// Hover Glow\n@mixin hover-glow {\n  transition: box-shadow $transition-normal, transform $transition-normal;\n  \n  &:hover {\n    box-shadow: var(--shadow-glow);\n    transform: translateY(-2px);\n  }\n}\n\n// ========== Compatibility ==========\n$border-color: var(--border-color);\n$text-color: var(--text-primary);\n$text-secondary: var(--text-secondary);\n$text-disabled: var(--text-disabled);\n$link-color: var(--primary-start);\n$bg-color: var(--bg-primary);\n$header-bg: var(--bg-elevated);\n$body-bg: var(--bg-primary);\n\n// More Compatibility\n$font-size-small: $font-size-xs;\n$font-size-large: $font-size-lg;\n$font-size-huge: $font-size-xl;\n\n$gap: $space-md;\n$sm-gap: $space-sm;\n$xs-gap: $space-xs;\n$lg-gap: $space-lg;\n\n$white: #ffffff;\n$black: #000000;\n"
  },
  {
    "path": "DOJ-FE/src/utils/markdown.ts",
    "content": "import { marked } from 'marked';\nimport katex from 'katex';\n\n// Configure marked options if needed\nmarked.setOptions({\n    breaks: true, // Enable line breaks\n    gfm: true,    // Enable GitHub Flavored Markdown\n});\n\nexport const renderMarkdown = (text: string | undefined): string => {\n    if (!text) return '';\n\n    // 1. Protect Math delimiters\n    // We replace $$...$$ and $...$ with placeholders to prevent marked from messing them up.\n    // Using a random string or specific pattern as placeholder.\n    const mathBlocks: string[] = [];\n    const protectedText = text.replace(/(\\$\\$.*?\\$\\$|\\$.*?\\$)/gs, (match) => {\n        mathBlocks.push(match);\n        return `%%%MATH_BLOCK_${mathBlocks.length - 1}%%%`;\n    });\n\n    // 2. Render Markdown\n    let html = marked.parse(protectedText) as string;\n\n    // 3. Restore Math and Render KaTeX\n    html = html.replace(/%%%MATH_BLOCK_(\\d+)%%%/g, (_match, index) => {\n        const math = mathBlocks[parseInt(index, 10)];\n        if (!math) return '';\n\n        if (math.startsWith('$$')) {\n            // Display math\n            try {\n                return katex.renderToString(math.slice(2, -2), {\n                    displayMode: true,\n                    throwOnError: false\n                });\n            } catch (e) {\n                return math;\n            }\n        } else {\n            // Inline math\n            try {\n                return katex.renderToString(math.slice(1, -1), {\n                    displayMode: false,\n                    throwOnError: false\n                });\n            } catch (e) {\n                return math;\n            }\n        }\n    });\n\n    return html;\n};\n"
  },
  {
    "path": "DOJ-FE/src/utils/request.ts",
    "content": "// 二次封装axios：使用请求与响应拦截器\nimport axios, { type InternalAxiosRequestConfig } from \"axios\";\nimport { ElMessage } from \"element-plus\";\nimport { useUserStore } from \"@/stores/userStore\";\nimport router from \"@/router\";\n// 创建一个专门用于刷新 token 的、不带任何拦截器的 axios 实例\nconst refreshTokenRequest = axios.create({\n    baseURL: import.meta.env.VITE_APP_URL,\n    timeout: 50000,\n});\n// 创建主要的axios实例\nconst request = axios.create({\n    baseURL: import.meta.env.VITE_APP_URL,\n    timeout: 50000,\n});\n// 请求拦截器\nrequest.interceptors.request.use((config) => {\n    const userStore = useUserStore();\n    const accessToken = userStore.userInfo?.accessToken;\n    if (accessToken) {\n        config.headers.Authorization = accessToken;\n    }\n    return config;\n});\n\n// --- 静默刷新逻辑 ---\nlet isRefreshing = false;\n// 存储因 token 过期而被挂起的请求\nlet requests: ((token: string) => void)[] = [];\n\n// 响应拦截器\nrequest.interceptors.response.use(\n    (response) => {\n        return response;\n    },\n    async (error) => {\n        const { response, config } = error;\n        const userStore = useUserStore();\n\n        // 只有当响应是 401 时才执行刷新逻辑\n        if (response && response.status === 401) {\n            // 如果正在刷新中，则将当前失败的请求挂起\n            if (isRefreshing) {\n                return new Promise((resolve) => {\n                    requests.push((token: string) => {\n                        // 当刷新成功后，用新的 token 重新发起请求\n                        (config as InternalAxiosRequestConfig).headers.Authorization = token;\n                        resolve(request(config));\n                    });\n                });\n            }\n\n            isRefreshing = true;\n\n            try {\n                // 使用独立的 axios 实例来刷新 token，避免循环拦截\n                const res = await refreshTokenRequest.post('/user/refresh', {}, {\n                    headers: {\n                        Authorization: userStore.userInfo?.refreshToken || ''\n                    }\n                });\n\n                const newAccessToken = res.data.data;\n                // 1. 更新 Pinia 和当前请求的 accessToken\n                userStore.setAccessToken(newAccessToken);\n                (config as InternalAxiosRequestConfig).headers.Authorization = newAccessToken;\n\n                // 2. 重试队列中所有挂起的请求\n                requests.forEach((cb) => cb(newAccessToken));\n                requests = []; // 清空队列\n\n                // 3. 重试本次失败的请求\n                return request(config);\n\n            } catch (refreshError) {\n                // 如果刷新令牌也失败了（例如 refresh_token 也过期了），则清除所有信息并跳转登录页\n                userStore.clearUserInfo();\n                router.push('/login');\n                ElMessage.error('登录已过期，请重新登录');\n                return Promise.reject(refreshError);\n            } finally {\n                isRefreshing = false;\n            }\n        }\n\n        // 处理其他网络错误\n        const error_info = response?.data || {};\n        const msg = error_info.msg || '网络错误，请稍后再试';\n        ElMessage({\n            type: 'error',\n            message: msg,\n        });\n\n        return Promise.reject(error);\n    }\n);\n\nexport default request;"
  },
  {
    "path": "DOJ-FE/src/utils/websocket.ts",
    "content": "import { ElNotification } from 'element-plus';\nimport router from '@/router';\nimport { h } from 'vue';\n\nlet socket: WebSocket | null = null;\n\n// 辅助函数：创建丰富的通知内容\nconst renderResultMessage = (result: any) => {\n    const time = result.time ? `${(result.time * 1000).toFixed(2)} ms` : 'N/A';\n    const memory = result.memory ? `${result.memory.toFixed(2)} KB` : 'N/A';\n\n    return h('div', null, [\n        h('p', { style: { margin: '0 0 5px 0' } }, [\n            h('strong', null, '状态: '),\n            h('span', { style: { fontWeight: 'bold', color: result.status === 'Accepted' ? '#67c23a' : '#f56c6c' } }, ` ${result.status}`)\n        ]),\n        h('p', { style: { margin: '0 0 5px 0' } }, [\n            h('strong', null, '耗时: '),\n            h('span', null, time)\n        ]),\n        h('p', { style: { margin: '0 0 5px 0' } }, [\n            h('strong', null, '内存: '),\n            h('span', null, memory)\n        ]),\n        result.message && result.status !== 'Accepted' ? h('div', null, [\n            h('strong', null, '信息: '),\n            h('pre', { style: { marginTop: '5px', padding: '5px', background: '#f5f5f5', border: '1px solid #e3e3e3', borderRadius: '4px', whiteSpace: 'pre-wrap', wordBreak: 'break-all' } }, result.message)\n        ]) : null\n    ]);\n};\n\nconst connectWebSocket = () => {\n    if (socket && socket.readyState === WebSocket.OPEN) {\n        return;\n    }\n    if (socket && socket.readyState === WebSocket.CONNECTING) {\n        return;\n    }\n\n    const wsUrl = 'ws://127.0.0.1:8084/ws/submission'; // 直接连接后端 WebSocket 地址\n\n    socket = new WebSocket(wsUrl);\n\n    socket.onopen = () => {\n        console.log('WebSocket 连接已建立。');\n    };\n\n    socket.onmessage = (event) => {\n        try {\n            const result = JSON.parse(event.data);\n            console.log('收到判题结果:', result);\n\n            ElNotification({\n                title: `提交 #${result.id} 已完成`,\n                message: renderResultMessage(result),\n                type: result.status === 'Accepted' ? 'success' : 'error',\n                duration: 15000, // 延长显示时间\n                onClick: () => {\n                    router.push(`/status?submissionId=${result.id}`);\n                },\n                position: 'bottom-right',\n            });\n\n        } catch (e) {\n            console.error('处理 WebSocket 消息失败:', e);\n        }\n    };\n\n    socket.onclose = (event) => {\n        console.log('WebSocket 连接已关闭:', event);\n        socket = null;\n        // 可选：实现一个延迟重连机制\n        // setTimeout(() => {\n        //     console.log('尝试重新连接 WebSocket...');\n        //     connectWebSocket();\n        // }, 5000);\n    };\n\n    socket.onerror = (error) => {\n        console.error('WebSocket 发生错误:', error);\n        // 可以在这里添加一些错误提示\n    };\n};\n\nconst subscribeToSubmission = (submissionId: number) => {\n    const trySubscribe = () => {\n        if (socket && socket.readyState === WebSocket.OPEN) {\n            console.log(`订阅提交 ID: ${submissionId}`);\n            socket.send(JSON.stringify({ submissionId }));\n        } else if (socket && socket.readyState === WebSocket.CONNECTING) {\n            // 如果正在连接，则等待一会再试\n            console.log('WebSocket 正在连接，稍后重试订阅...');\n            setTimeout(trySubscribe, 500);\n        } else {\n            // 如果连接已关闭或不存在，则尝试重连并订阅\n            console.log('WebSocket 未连接，正在尝试重新连接...');\n            connectWebSocket();\n            setTimeout(trySubscribe, 500);\n        }\n    };\n    trySubscribe();\n};\n\nexport const useWebSocket = () => {\n    // 确保只有一个 WebSocket 连接实例\n    if (!socket) {\n        connectWebSocket();\n    }\n\n    return {\n        subscribeToSubmission\n    };\n};"
  },
  {
    "path": "DOJ-FE/src/views/Admin/Problem/components/ProblemDialog.vue",
    "content": "<template>\n    <el-dialog \n        :title=\"isEdit ? '编辑题目' : '新增题目'\" \n        v-model=\"visible\" \n        width=\"800px\" \n        :close-on-click-modal=\"false\"\n    >\n        <el-form :model=\"form\" label-width=\"100px\">\n            <el-form-item label=\"标题\">\n                <el-input v-model=\"form.name\" />\n            </el-form-item>\n            <el-form-item label=\"难度\">\n                <el-select v-model=\"form.difficulty\">\n                    <el-option label=\"简单\" value=\"简单\" />\n                    <el-option label=\"中等\" value=\"中等\" />\n                    <el-option label=\"困难\" value=\"困难\" />\n                </el-select>\n            </el-form-item>\n            <el-form-item label=\"时间限制\">\n                <el-input v-model=\"form.timeLimit\" placeholder=\"例如: 1000\">\n                    <template #append>ms</template>\n                </el-input>\n            </el-form-item>\n            <el-form-item label=\"内存限制\">\n                <el-input v-model=\"form.memoryLimit\" placeholder=\"例如: 256\">\n                    <template #append>MB</template>\n                </el-input>\n            </el-form-item>\n            <el-form-item label=\"标签\">\n                <el-input v-model=\"tagsStr\" placeholder=\"逗号分隔，如: dp,math\" />\n            </el-form-item>\n            \n            <div class=\"editor-tabs\">\n                <el-tabs type=\"card\" v-model=\"activeEditor\">\n                    <el-tab-pane label=\"题目描述\" name=\"desc\">\n                        <el-input type=\"textarea\" :rows=\"10\" v-model=\"form.description\" placeholder=\"支持 Markdown\" />\n                    </el-tab-pane>\n                    <el-tab-pane label=\"输入格式\" name=\"input\">\n                        <el-input type=\"textarea\" :rows=\"5\" v-model=\"form.inputStyle\" placeholder=\"支持 Markdown\" />\n                    </el-tab-pane>\n                    <el-tab-pane label=\"输出格式\" name=\"output\">\n                        <el-input type=\"textarea\" :rows=\"5\" v-model=\"form.outputStyle\" placeholder=\"支持 Markdown\" />\n                    </el-tab-pane>\n                    <el-tab-pane label=\"样例输入\" name=\"sampleIn\">\n                        <el-input type=\"textarea\" :rows=\"5\" v-model=\"samplesInStr\" placeholder=\"样例之间使用 '---' 分隔\" />\n                        <div class=\"tip\">样例之间使用 '---' (单独一行) 分隔</div>\n                    </el-tab-pane>\n                    <el-tab-pane label=\"样例输出\" name=\"sampleOut\">\n                        <el-input type=\"textarea\" :rows=\"5\" v-model=\"samplesOutStr\" placeholder=\"样例之间使用 '---' 分隔\" />\n                        <div class=\"tip\">样例之间使用 '---' (单独一行) 分隔</div>\n                    </el-tab-pane>\n                    <el-tab-pane label=\"提示\" name=\"range\">\n                        <el-input type=\"textarea\" :rows=\"5\" v-model=\"form.hint\" placeholder=\"支持 Markdown\" />\n                    </el-tab-pane>\n                    <el-tab-pane label=\"测试数据\" name=\"testData\">\n                        <el-input type=\"textarea\" :rows=\"5\" v-model=\"form.testData\" placeholder=\"评测输入（原样写入文件）\" />\n                    </el-tab-pane>\n                    <el-tab-pane label=\"标准答案\" name=\"testAns\">\n                        <el-input type=\"textarea\" :rows=\"5\" v-model=\"form.testAns\" placeholder=\"评测输出（与程序输出精确匹配）\" />\n                    </el-tab-pane>\n                </el-tabs>\n            </div>\n        </el-form>\n        <template #footer>\n            <el-button @click=\"visible = false\">取消</el-button>\n            <el-button type=\"primary\" @click=\"submit\" :loading=\"loading\">确定</el-button>\n        </template>\n    </el-dialog>\n</template>\n\n<script lang=\"ts\" setup>\nimport { ref, reactive, watch } from 'vue';\nimport { ElMessage } from 'element-plus';\nimport { reqCreateProblem, reqUpdateProblem, reqProblemDetail } from '@/api/problem';\n\nconst emit = defineEmits(['refresh']);\n\nconst visible = ref(false);\nconst isEdit = ref(false);\nconst loading = ref(false);\nconst activeEditor = ref('desc');\nconst tagsStr = ref('');\n\nconst form = reactive({\n    id: undefined,\n    name: '',\n    difficulty: '简单',\n    timeLimit: '',\n    memoryLimit: '',\n    tags: [] as string[],\n    description: '',\n    inputStyle: '',\n    outputStyle: '',\n    inputSample: [] as string[],\n    outputSample: [] as string[],\n    hint: '',\n    testData: '',\n    testAns: ''\n});\n\nconst samplesInStr = ref('');\nconst samplesOutStr = ref('');\n\nconst open = async (row?: any) => {\n    visible.value = true;\n    if (row) {\n        isEdit.value = true;\n        try {\n            const res = await reqProblemDetail(String(row.id));\n            const data = res.data.data;\n            Object.assign(form, data);\n            \n            tagsStr.value = (data.tags || []).join(',');\n            samplesInStr.value = (data.inputSample || []).join('\\n---\\n');\n            samplesOutStr.value = (data.outputSample || []).join('\\n---\\n');\n        } catch (e) {\n            ElMessage.error('加载详情失败');\n        }\n    } else {\n        isEdit.value = false;\n        resetForm();\n    }\n};\n\nconst resetForm = () => {\n    form.id = undefined;\n    form.name = '';\n    form.difficulty = '简单';\n    form.timeLimit = '1000';\n    form.memoryLimit = '256';\n    tagsStr.value = '';\n    samplesInStr.value = '';\n    samplesOutStr.value = '';\n    form.description = '';\n    form.inputStyle = '';\n    form.outputStyle = '';\n    form.inputSample = [];\n    form.outputSample = [];\n    form.hint = '';\n    form.testData = '';\n    form.testAns = '';\n};\n\nconst submit = async () => {\n    loading.value = true;\n    try {\n        // Process tags\n        form.tags = tagsStr.value ? tagsStr.value.split(',').map(t => t.trim()).filter(t => t) : [];\n        \n        // Process samples (using --- as separator for multi-sample input)\n        form.inputSample = samplesInStr.value ? samplesInStr.value.split('\\n---\\n').map(s => s.trim()) : [];\n        form.outputSample = samplesOutStr.value ? samplesOutStr.value.split('\\n---\\n').map(s => s.trim()) : [];\n        \n        // Basic validation\n        if (form.inputSample.length !== form.outputSample.length) {\n            ElMessage.warning('输入样例和输出样例数量不匹配');\n            return;\n        }\n\n        const payload = {\n            ...form,\n            timeLimit: Number(form.timeLimit),\n            memoryLimit: Number(form.memoryLimit)\n        };\n\n        let res;\n        if (isEdit.value) {\n            res = await reqUpdateProblem(payload as any);\n        } else {\n            res = await reqCreateProblem(payload as any);\n        }\n        \n        if (res.data.code === 200) {\n            ElMessage.success(isEdit.value ? '修改成功' : '新增成功');\n            visible.value = false;\n            emit('refresh');\n        } else {\n            ElMessage.error(res.data.message || '操作失败');\n        }\n    } catch (e) {\n        ElMessage.error('请求出错');\n    } finally {\n        loading.value = false;\n    }\n};\n\ndefineExpose({ open });\n</script>\n\n<style scoped>\n.tip {\n    font-size: 12px;\n    color: #999;\n    margin-top: 5px;\n}\n</style>\n"
  },
  {
    "path": "DOJ-FE/src/views/Admin/Problem/index.vue",
    "content": "<template>\n    <div class=\"problem-manager\">\n        <div class=\"actions-bar\">\n            <el-button type=\"primary\" @click=\"handleAdd\">新增题目</el-button>\n            <el-button type=\"warning\" @click=\"handleSync\" :loading=\"syncLoading\">同步到ES</el-button>\n        </div>\n\n        <el-table :data=\"tableData\" v-loading=\"loading\" style=\"width: 100%\">\n            <el-table-column prop=\"id\" label=\"ID\" width=\"80\" />\n            <el-table-column prop=\"name\" label=\"题目名称\" />\n            <el-table-column prop=\"difficulty\" label=\"难度\" width=\"100\">\n                <template #default=\"{ row }\">\n                    <el-tag :type=\"getDifficultyType(row.difficulty)\">{{ row.difficulty }}</el-tag>\n                </template>\n            </el-table-column>\n            <el-table-column prop=\"tags\" label=\"标签\">\n                <template #default=\"{ row }\">\n                    <el-tag v-for=\"tag in parseTags(row.tags)\" :key=\"tag\" size=\"small\" class=\"mr-1\">{{ tag }}</el-tag>\n                </template>\n            </el-table-column>\n            <el-table-column label=\"操作\" width=\"180\">\n                <template #default=\"{ row }\">\n                    <el-button size=\"small\" @click=\"handleEdit(row)\">编辑</el-button>\n                    <el-button size=\"small\" type=\"danger\" @click=\"handleDelete(row)\">删除</el-button>\n                </template>\n            </el-table-column>\n        </el-table>\n\n        <div class=\"pagination-container\">\n            <el-pagination\n                v-model:current-page=\"pageParams.pageNo\"\n                v-model:page-size=\"pageParams.pageSize\"\n                :total=\"total\"\n                layout=\"prev, pager, next\"\n                @current-change=\"getData\"\n            />\n        </div>\n\n        <ProblemDialog \n            ref=\"dialogRef\" \n            @refresh=\"getData\"\n        />\n    </div>\n</template>\n\n<script lang=\"ts\" setup>\nimport { ref, onMounted, reactive } from 'vue';\nimport { ElMessage, ElMessageBox } from 'element-plus';\nimport { reqProblemPageList, reqDeleteProblem, reqSyncProblemToEs } from '@/api/problem';\nimport type { ProblemType } from '@/api/problem/type';\nimport ProblemDialog from './components/ProblemDialog.vue';\n\nconst tableData = ref<ProblemType[]>([]);\nconst loading = ref(false);\nconst syncLoading = ref(false);\nconst total = ref(0);\nconst pageParams = reactive({\n    pageNo: 1,\n    pageSize: 10,\n    keyword: '',\n    isAsc: false,\n    sortBy: 'id',\n    name: '',\n    difficulty: '',\n    tags: [] as string[],\n    status: ''\n});\n\nconst dialogRef = ref();\n\nconst getDifficultyType = (diff: string) => {\n    if (diff === '简单') return 'success';\n    if (diff === '中等') return 'warning';\n    if (diff === '困难') return 'danger';\n    return 'info';\n};\n\nconst parseTags = (tags: any) => {\n    if (!tags) return [];\n    if (Array.isArray(tags)) return tags;\n    try {\n        if (typeof tags === 'string' && tags.startsWith('[')) {\n            return JSON.parse(tags.replace(/'/g, '\"'));\n        }\n        return String(tags).split(',');\n    } catch {\n        return [tags];\n    }\n};\n\nconst getData = async () => {\n    loading.value = true;\n    try {\n        const res = await reqProblemPageList(pageParams as any);\n        if (res.data.code === 200) {\n            tableData.value = res.data.data.list;\n            total.value = res.data.data.total;\n        }\n    } finally {\n        loading.value = false;\n    }\n};\n\nconst handleAdd = () => {\n    dialogRef.value?.open();\n};\n\nconst handleEdit = (row: any) => {\n    dialogRef.value?.open(row);\n};\n\nconst handleDelete = (row: any) => {\n    ElMessageBox.confirm('确定要删除该题目吗？', '提示', {\n        type: 'warning'\n    }).then(async () => {\n        const res = await reqDeleteProblem(row.id);\n        if (res.data.code === 200) {\n            ElMessage.success('删除成功');\n            getData();\n        } else {\n            ElMessage.error(res.data.message || '删除失败');\n        }\n    });\n};\n\nconst handleSync = async () => {\n    syncLoading.value = true;\n    try {\n        const res = await reqSyncProblemToEs();\n        if (res.data.code === 200) {\n            ElMessage.success(`同步成功，共同步 ${res.data.data} 条数据`);\n        } else {\n            ElMessage.error(res.data.message || '同步失败');\n        }\n    } catch (e) {\n        ElMessage.error('同步请求失败');\n    } finally {\n        syncLoading.value = false;\n    }\n};\n\nonMounted(() => {\n    getData();\n});\n</script>\n\n<style scoped lang=\"scss\">\n\n\n.problem-manager {\n    .actions-bar {\n        margin-bottom: $space-lg;\n        display: flex;\n        gap: $space-md;\n    }\n    \n    .mr-1 {\n        margin-right: 4px;\n    }\n    \n    .pagination-container {\n        margin-top: $space-lg;\n        display: flex;\n        justify-content: center;\n    }\n}\n</style>\n"
  },
  {
    "path": "DOJ-FE/src/views/Admin/index.vue",
    "content": "<template>\n    <div class=\"admin-page\">\n        <div class=\"admin-header\">\n            <h1>后台管理</h1>\n            <div class=\"header-actions\">\n                <el-button @click=\"router.push('/')\">回到首页</el-button>\n            </div>\n        </div>\n        \n        <div class=\"admin-content card-glass\">\n            <el-tabs v-model=\"activeTab\">\n                <el-tab-pane label=\"题目管理\" name=\"problem\">\n                    <ProblemManager />\n                </el-tab-pane>\n                <!-- Add more tabs later: Announcement, User, etc. -->\n            </el-tabs>\n        </div>\n    </div>\n</template>\n\n<script lang=\"ts\" setup>\nimport { ref } from 'vue';\nimport { useRouter } from 'vue-router';\nimport ProblemManager from './Problem/index.vue';\n\nconst router = useRouter();\nconst activeTab = ref('problem');\n</script>\n\n<style scoped lang=\"scss\">\n\n\n.admin-page {\n    max-width: 1400px;\n    margin: 0 auto;\n    padding: $space-lg;\n    display: flex;\n    flex-direction: column;\n    gap: $space-lg;\n}\n\n.admin-header {\n    display: flex;\n    align-items: center;\n    justify-content: space-between;\n    \n    h1 {\n        font-size: $font-size-2xl;\n        font-weight: bold;\n        @include gradient-text;\n    }\n}\n\n.admin-content {\n    background: var(--bg-elevated);\n    padding: $space-lg;\n    border-radius: $radius-lg;\n    min-height: 500px;\n}\n</style>\n"
  },
  {
    "path": "DOJ-FE/src/views/Home/index.vue",
    "content": "<script lang=\"ts\" setup>\nimport { ref, onMounted } from 'vue';\nimport { useRouter } from 'vue-router';\n\nconst router = useRouter();\n\n// Stats data\nconst stats = ref([\n    { label: '累计提交', value: '0', icon: 'upload' },\n    { label: '今日提交', value: '0', icon: 'today' },\n    { label: '题目总数', value: '0', icon: 'book' },\n    { label: '活跃用户', value: '0', icon: 'users' },\n]);\n\n// Fetch stats from API\nimport { reqStats } from '@/api/stats';\n\nconst formatNumber = (num: number): string => {\n    return num.toLocaleString('en-US');\n};\n\nconst fetchStats = async () => {\n    try {\n        const res = await reqStats();\n        if (res.data.code === 200) {\n            const data = res.data.data;\n            stats.value = [\n                { label: '累计提交', value: formatNumber(data.totalSubmissions), icon: 'upload' },\n                { label: '今日提交', value: formatNumber(data.todaySubmissions), icon: 'today' },\n                { label: '题目总数', value: formatNumber(data.totalProblems), icon: 'book' },\n                { label: '活跃用户', value: formatNumber(data.activeUsers), icon: 'users' },\n            ];\n        }\n    } catch (e) {\n        console.error('Failed to fetch stats', e);\n    }\n};\n\n// Announcements\nimport { reqAnnouncementList } from '@/api/announcement';\nimport { computed } from 'vue';\n\ninterface AnnouncementItem {\n    id: number;\n    title: string;\n    content: string;\n    date: string;\n    isNew: boolean;\n}\n\nconst announcements = ref<AnnouncementItem[]>([]);\nconst currentPage = ref(1);\nconst pageSize = 6; // Adjust based on height match\nconst dialogVisible = ref(false);\nconst currentAnnouncement = ref<AnnouncementItem | null>(null);\n\nconst totalPages = computed(() => Math.ceil(announcements.value.length / pageSize));\nconst displayedAnnouncements = computed(() => {\n    const start = (currentPage.value - 1) * pageSize;\n    return announcements.value.slice(start, start + pageSize);\n});\n\nconst showAnnouncement = (item: AnnouncementItem) => {\n    currentAnnouncement.value = item;\n    dialogVisible.value = true;\n};\n\nconst formatDate = (dateStr: string) => {\n    const d = new Date(dateStr);\n    const year = d.getFullYear();\n    const month = String(d.getMonth() + 1).padStart(2, '0');\n    const day = String(d.getDate()).padStart(2, '0');\n    return `${year}/${month}/${day}`;\n};\n\nconst fetchAnnouncements = async () => {\n    try {\n        const res = await reqAnnouncementList();\n        if (res.data.code === 200) {\n            announcements.value = res.data.data.map((item: any) => ({\n                id: item.id,\n                title: item.title,\n                content: item.content,\n                date: formatDate(item.createTime),\n                isNew: (Date.now() - new Date(item.createTime).getTime()) < 7 * 24 * 60 * 60 * 1000 // New if within 7 days\n            }));\n        }\n    } catch (e) {\n        console.error('Failed to fetch announcements', e);\n    }\n};\n\n// Features (Updated content)\nconst features = ref([\n    { \n        icon: 'trophy', \n        title: '排行榜', \n        desc: '查看顶尖选手排名，追逐榜单荣耀',\n        link: '/rankings'\n    },\n    { \n        icon: 'fire', \n        title: '热门题目', \n        desc: '挑战最受关注的经典算法题目',\n        link: '/problem'\n    },\n    { \n        icon: 'calendar', \n        title: '近期比赛', \n        desc: '参与在线竞赛，实时检验实力',\n        link: '/status' // Placeholder\n    },\n]);\n\nonMounted(() => {\n    fetchStats();\n    fetchAnnouncements();\n});\n\nconst goToProblems = () => {\n    router.push('/problem');\n};\n\nconst goToEditor = () => {\n    router.push('/online');\n};\n</script>\n\n<template>\n    <div class=\"home-page\">\n        <!-- Hero Section -->\n        <section class=\"hero animate-fade-in-up\">\n            <div class=\"hero-content\">\n                <h1 class=\"hero-title\">\n                    <span class=\"text-gradient\">Duck Online Judge</span>\n                </h1>\n                <p class=\"hero-subtitle\">\n                    现代化的在线编程判题平台，助你提升算法能力\n                </p>\n                <div class=\"hero-actions\">\n                    <button class=\"btn-primary btn-lg\" @click=\"goToProblems\">\n                        <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n                            <path d=\"M4 19.5A2.5 2.5 0 016.5 17H20\"/>\n                            <path d=\"M6.5 2H20v20H6.5A2.5 2.5 0 014 19.5v-15A2.5 2.5 0 016.5 2z\"/>\n                        </svg>\n                        开始刷题\n                    </button>\n                    <button class=\"btn-secondary btn-lg\" @click=\"goToEditor\">\n                        <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n                            <polyline points=\"16 18 22 12 16 6\"/>\n                            <polyline points=\"8 6 2 12 8 18\"/>\n                        </svg>\n                        在线编程\n                    </button>\n                </div>\n            </div>\n            \n            <!-- Floating decoration -->\n            <div class=\"hero-decoration\">\n                <div class=\"floating-card card-1\">\n                    <span class=\"status accepted\">AC</span>\n                </div>\n                <div class=\"floating-card card-2\">\n                    <span class=\"code-snippet\">&lt;/&gt;</span>\n                </div>\n                <div class=\"floating-card card-3\">\n                    <span class=\"status runtime\">100ms</span>\n                </div>\n            </div>\n        </section>\n\n        <!-- Stats Section -->\n        <section class=\"stats-section\">\n            <div class=\"stats-grid\">\n                <div v-for=\"stat in stats\" :key=\"stat.label\" class=\"stat-card card-glass\">\n                    <div class=\"stat-icon\">\n                        <!-- Upload icon -->\n                        <svg v-if=\"stat.icon === 'upload'\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n                            <path d=\"M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4\"/>\n                            <polyline points=\"17 8 12 3 7 8\"/>\n                            <line x1=\"12\" y1=\"3\" x2=\"12\" y2=\"15\"/>\n                        </svg>\n                        <!-- Today icon -->\n                        <svg v-if=\"stat.icon === 'today'\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n                            <rect x=\"3\" y=\"4\" width=\"18\" height=\"18\" rx=\"2\" ry=\"2\"/>\n                            <line x1=\"16\" y1=\"2\" x2=\"16\" y2=\"6\"/>\n                            <line x1=\"8\" y1=\"2\" x2=\"8\" y2=\"6\"/>\n                            <line x1=\"3\" y1=\"10\" x2=\"21\" y2=\"10\"/>\n                        </svg>\n                        <!-- Book icon -->\n                        <svg v-if=\"stat.icon === 'book'\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n                            <path d=\"M4 19.5A2.5 2.5 0 016.5 17H20\"/>\n                            <path d=\"M6.5 2H20v20H6.5A2.5 2.5 0 014 19.5v-15A2.5 2.5 0 016.5 2z\"/>\n                        </svg>\n                        <!-- Users icon -->\n                        <svg v-if=\"stat.icon === 'users'\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n                            <path d=\"M17 21v-2a4 4 0 00-4-4H5a4 4 0 00-4 4v2\"/>\n                            <circle cx=\"9\" cy=\"7\" r=\"4\"/>\n                            <path d=\"M23 21v-2a4 4 0 00-3-3.87\"/>\n                            <path d=\"M16 3.13a4 4 0 010 7.75\"/>\n                        </svg>\n                    </div>\n                    <div class=\"stat-info\">\n                        <span class=\"stat-value\">{{ stat.value }}</span>\n                        <span class=\"stat-label\">{{ stat.label }}</span>\n                    </div>\n                </div>\n            </div>\n        </section>\n\n        <!-- Main Content -->\n        <section class=\"main-content\">\n            <div class=\"content-grid\">\n                <!-- Announcements -->\n                <div class=\"announcements-card card-glass\">\n                    <div class=\"card-header\">\n                        <h2>\n                            <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n                                <path d=\"M22 17H2a3 3 0 003 3h14a3 3 0 003-3zM6 5a3 3 0 00-3 3v9h18V8a3 3 0 00-3-3H6z\"/>\n                            </svg>\n                            公告\n                        </h2>\n                    </div>\n                    <div class=\"card-body\">\n                        <ul class=\"announcement-list\">\n                            <li v-for=\"item in displayedAnnouncements\" :key=\"item.id\" class=\"announcement-item\">\n                                <a href=\"#\" class=\"announcement-link\" @click.prevent=\"showAnnouncement(item)\">\n                                    <span class=\"announcement-title\">\n                                        {{ item.title }}\n                                        <span v-if=\"item.isNew\" class=\"badge-new\">NEW</span>\n                                    </span>\n                                    <span class=\"announcement-date\">{{ item.date }}</span>\n                                </a>\n                            </li>\n                        </ul>\n                        <div class=\"pagination-mini\" v-if=\"totalPages > 1\">\n                            <button class=\"page-btn\" :disabled=\"currentPage === 1\" @click=\"currentPage--\">\n                                <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"><polyline points=\"15 18 9 12 15 6\"/></svg>\n                            </button>\n                            <span class=\"page-info\">{{ currentPage }} / {{ totalPages }}</span>\n                            <button class=\"page-btn\" :disabled=\"currentPage === totalPages\" @click=\"currentPage++\">\n                                <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"><polyline points=\"9 18 15 12 9 6\"/></svg>\n                            </button>\n                        </div>\n                    </div>\n                </div>\n\n                <!-- Features -->\n                <div class=\"features-section\">\n                    <div v-for=\"feature in features\" :key=\"feature.title\" class=\"feature-card card-glass\" @click=\"router.push(feature.link)\" style=\"cursor: pointer;\">\n                        <div class=\"feature-icon\">\n                            <!-- Trophy icon -->\n                            <svg v-if=\"feature.icon === 'trophy'\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n                                <path d=\"M6 9H4.5a2.5 2.5 0 010-5H6\"/>\n                                <path d=\"M18 9h1.5a2.5 2.5 0 000-5H18\"/>\n                                <path d=\"M4 22h16\"/>\n                                <path d=\"M10 14.66V17c0 .55-.47.98-.97 1.21C7.85 18.75 7 20.24 7 22\"/>\n                                <path d=\"M14 14.66V17c0 .55.47.98.97 1.21C16.15 18.75 17 20.24 17 22\"/>\n                                <path d=\"M18 2H6v7a6 6 0 0012 0V2z\"/>\n                            </svg>\n                            <!-- Fire icon -->\n                            <svg v-if=\"feature.icon === 'fire'\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n                                <path d=\"M8.5 14.5A2.5 2.5 0 0 0 11 12c0-1.38-.5-2-1-3-1.072-2.143-.224-4.054 2-6 .5 2.5 2 4.9 4 6.5 2 1.6 3 3.5 3 5.5a7 7 0 1 1-14 0c0-1.1.2-2.2.5-3.3a7 7 0 0 0 12.1 7.1\"/>\n                            </svg>\n                            <!-- Calendar icon -->\n                            <svg v-if=\"feature.icon === 'calendar'\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n                                <rect x=\"3\" y=\"4\" width=\"18\" height=\"18\" rx=\"2\" ry=\"2\"/>\n                                <line x1=\"16\" y1=\"2\" x2=\"16\" y2=\"6\"/>\n                                <line x1=\"8\" y1=\"2\" x2=\"8\" y2=\"6\"/>\n                                <line x1=\"3\" y1=\"10\" x2=\"21\" y2=\"10\"/>\n                            </svg>\n                        </div>\n                        <h3>{{ feature.title }}</h3>\n                        <p>{{ feature.desc }}</p>\n                    </div>\n                </div>\n            </div>\n        </section>\n\n        <!-- Announcement Dialog -->\n        <el-dialog\n            v-model=\"dialogVisible\"\n            :title=\"currentAnnouncement?.title\"\n            width=\"600px\"\n            class=\"announcement-dialog\"\n        >\n            <div class=\"announcement-content\">\n                <div class=\"announcement-meta\">\n                    <span>发布时间：{{ currentAnnouncement?.date }}</span>\n                </div>\n                <div class=\"announcement-body\">\n                    {{ currentAnnouncement?.content }}\n                </div>\n            </div>\n        </el-dialog>\n    </div>\n</template>\n\n<style scoped lang=\"scss\">\n\n\n.home-page {\n    display: flex;\n    flex-direction: column;\n    gap: $space-2xl;\n}\n\n// Hero Section\n.hero {\n    position: relative;\n    text-align: center;\n    padding: $space-3xl 0;\n    \n    .hero-content {\n        position: relative;\n        z-index: 1;\n    }\n    \n    .hero-title {\n        font-size: clamp($font-size-4xl, 8vw, 72px);\n        font-weight: $font-weight-bold;\n        margin-bottom: $space-md;\n        line-height: 1.1;\n    }\n    \n    .hero-subtitle {\n        font-size: $font-size-xl;\n        color: var(--text-secondary);\n        margin-bottom: $space-xl;\n        max-width: 500px;\n        margin-left: auto;\n        margin-right: auto;\n    }\n    \n    .hero-actions {\n        display: flex;\n        gap: $space-md;\n        justify-content: center;\n        flex-wrap: wrap;\n        \n        .btn-lg {\n            padding: $space-md $space-xl;\n            font-size: $font-size-base;\n            display: flex;\n            align-items: center;\n            gap: $space-sm;\n            \n            svg {\n                width: 20px;\n                height: 20px;\n            }\n        }\n    }\n}\n\n// Hero floating cards\n.hero-decoration {\n    position: absolute;\n    top: 0;\n    left: 0;\n    right: 0;\n    bottom: 0;\n    pointer-events: none;\n    overflow: hidden;\n}\n\n.floating-card {\n    position: absolute;\n    padding: $space-sm $space-md;\n    background: var(--glass-bg);\n    backdrop-filter: blur(8px);\n    border: 1px solid var(--glass-border);\n    border-radius: $radius-md;\n    font-weight: $font-weight-semibold;\n    animation: float 6s ease-in-out infinite;\n    \n    &.card-1 {\n        top: 20%;\n        left: 10%;\n        animation-delay: 0s;\n        \n        .accepted {\n            color: $success;\n        }\n    }\n    \n    &.card-2 {\n        top: 30%;\n        right: 15%;\n        animation-delay: 2s;\n        \n        .code-snippet {\n            @include gradient-text;\n            font-family: $font-mono;\n        }\n    }\n    \n    &.card-3 {\n        bottom: 25%;\n        left: 20%;\n        animation-delay: 4s;\n        \n        .runtime {\n            color: $info;\n        }\n    }\n}\n\n@keyframes float {\n    0%, 100% { transform: translateY(0px); }\n    50% { transform: translateY(-20px); }\n}\n\n// Stats Section\n.stats-section {\n    .stats-grid {\n        display: grid;\n        grid-template-columns: repeat(4, 1fr);\n        gap: $space-lg;\n        \n        @include mobile {\n            grid-template-columns: repeat(2, 1fr);\n        }\n    }\n}\n\n.stat-card {\n    display: flex;\n    align-items: center;\n    gap: $space-md;\n    padding: $space-lg;\n    @include hover-glow;\n    \n    .stat-icon {\n        width: 48px;\n        height: 48px;\n        display: flex;\n        align-items: center;\n        justify-content: center;\n        background: var(--primary-gradient);\n        border-radius: $radius-md;\n        \n        svg {\n            width: 24px;\n            height: 24px;\n            color: white;\n        }\n    }\n    \n    .stat-info {\n        display: flex;\n        flex-direction: column;\n    }\n    \n    .stat-value {\n        font-size: $font-size-2xl;\n        font-weight: $font-weight-bold;\n        color: var(--text-primary);\n    }\n    \n    .stat-label {\n        font-size: $font-size-sm;\n        color: var(--text-muted);\n    }\n}\n\n// Main Content\n.content-grid {\n    display: grid;\n    grid-template-columns: 2fr 1fr;\n    gap: $space-xl;\n    \n    @include mobile {\n        grid-template-columns: 1fr;\n    }\n}\n\n// Announcements\n.announcements-card {\n    .card-header {\n        padding: $space-lg;\n        border-bottom: 1px solid var(--border-color);\n        \n        h2 {\n            display: flex;\n            align-items: center;\n            gap: $space-sm;\n            font-size: $font-size-lg;\n            font-weight: $font-weight-semibold;\n            margin: 0;\n            \n            svg {\n                width: 20px;\n                height: 20px;\n                color: var(--primary-start);\n            }\n        }\n    }\n    \n    .card-body {\n        padding: 0;\n    }\n\n    padding-bottom: 0;\n}\n\n.announcement-list {\n    list-style: none;\n    margin: 0;\n    padding: 0;\n}\n\n.announcement-item {\n    border-bottom: 1px solid var(--border-color);\n    \n    &:last-child {\n        border-bottom: none;\n    }\n}\n\n.announcement-link {\n    display: flex;\n    justify-content: space-between;\n    align-items: center;\n    padding: $space-md $space-lg;\n    color: var(--text-primary);\n    text-decoration: none;\n    transition: background $transition-fast;\n    \n    &:hover {\n        background: var(--glass-bg);\n    }\n    \n    .announcement-title {\n        display: flex;\n        align-items: center;\n        gap: $space-sm;\n        font-size: $font-size-sm;\n    }\n    \n    .badge-new {\n        padding: 2px 6px;\n        font-size: 10px;\n        font-weight: $font-weight-bold;\n        background: var(--primary-gradient);\n        color: white;\n        border-radius: $radius-sm;\n    }\n    \n    .announcement-date {\n        font-size: $font-size-xs;\n        color: var(--text-muted);\n    }\n}\n\n// Features\n.features-section {\n    display: flex;\n    flex-direction: column;\n    gap: $space-md;\n}\n\n.feature-card {\n    padding: $space-lg;\n    @include hover-glow;\n    \n    .feature-icon {\n        width: 40px;\n        height: 40px;\n        display: flex;\n        align-items: center;\n        justify-content: center;\n        background: rgba(102, 126, 234, 0.1);\n        border-radius: $radius-md;\n        margin-bottom: $space-md;\n        \n        svg {\n            width: 20px;\n            height: 20px;\n            color: var(--primary-start);\n        }\n    }\n    \n    h3 {\n        font-size: $font-size-base;\n        font-weight: $font-weight-semibold;\n        margin-bottom: $space-xs;\n    }\n    \n    p {\n        font-size: $font-size-sm;\n        color: var(--text-secondary);\n        margin: 0;\n    }\n}\n// Pagination\n.pagination-mini {\n    display: flex;\n    justify-content: center;\n    align-items: center;\n    gap: $space-md;\n    padding: $space-md;\n    border-top: 1px solid var(--border-color);\n    \n    .page-btn {\n        background: transparent;\n        border: none;\n        color: var(--text-secondary);\n        cursor: pointer;\n        padding: 4px;\n        border-radius: $radius-sm;\n        display: flex;\n        align-items: center;\n        \n        &:disabled {\n            opacity: 0.3;\n            cursor: not-allowed;\n        }\n        \n        &:hover:not(:disabled) {\n            background: var(--bg-hover);\n            color: var(--primary-start);\n        }\n        \n        svg {\n            width: 16px;\n            height: 16px;\n        }\n    }\n    \n    .page-info {\n        font-size: $font-size-xs;\n        color: var(--text-muted);\n    }\n}\n\n// Dialog Styles\n.announcement-content {\n    .announcement-meta {\n        margin-bottom: $space-md;\n        color: var(--text-muted);\n        font-size: $font-size-sm;\n        border-bottom: 1px solid var(--border-color);\n        padding-bottom: $space-sm;\n    }\n    \n    .announcement-body {\n        line-height: $line-height-relaxed;\n        color: var(--text-primary);\n        white-space: pre-wrap;\n    }\n}\n</style>"
  },
  {
    "path": "DOJ-FE/src/views/Layout/components/LayoutFixed.vue",
    "content": "<script lang=\"ts\" setup>\nimport { useUserStore } from '@/stores/userStore'\nimport { ElDropdown, ElDropdownMenu, ElDropdownItem, ElAvatar, ElIcon } from 'element-plus';\nimport { Moon, Sunny } from '@element-plus/icons-vue';\nimport { DropdownInstance } from 'element-plus';\nimport { ref, computed } from 'vue';\nimport { useRouter } from 'vue-router';\n\nconst router = useRouter();\nconst userStore = useUserStore();\nconst dropdown = ref<DropdownInstance>();\n\nconst closeDropdown = (command: string) => {\n    dropdown.value?.handleClose();\n    router.push(command);\n};\n\nconst logout = () => {\n    userStore.clearUserInfo();\n    window.location.href = '/login';\n};\n\nconst navItems = [\n    { path: '/', icon: 'home', text: '首页' },\n    { path: '/problem', icon: 'book', text: '题库' },\n    { path: '/online', icon: 'code', text: '编码' },\n    { path: '/status', icon: 'activity', text: '状态' },\n    { path: '/rankings', icon: 'trophy', text: '排名' },\n];\n\nconst isActive = (path: string) => {\n    if (path === '/') {\n        return router.currentRoute.value.path === '/';\n    }\n    return router.currentRoute.value.path.startsWith(path);\n};\n\nconst userInitials = computed(() => {\n    if (userStore.userInfo?.username) {\n        return userStore.userInfo.username.charAt(0).toUpperCase();\n    }\n    return 'U';\n});\n\n// Theme Management\nconst isDark = ref(localStorage.getItem('theme') !== 'light');\n\nconst toggleTheme = () => {\n    isDark.value = !isDark.value;\n    const theme = isDark.value ? 'dark' : 'light';\n    document.documentElement.setAttribute('data-theme', theme);\n    localStorage.setItem('theme', theme);\n};\n\n// Initialize theme\nif (!isDark.value) {\n    document.documentElement.setAttribute('data-theme', 'light');\n}\n\n</script>\n\n<template>\n    <header class=\"navbar\">\n        <nav class=\"navbar-inner\">\n            <!-- Logo -->\n            <router-link to=\"/\" class=\"logo\">\n                <div class=\"logo-icon\">\n                    <svg viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n                        <path d=\"M12 2L2 7L12 12L22 7L12 2Z\" stroke=\"url(#gradient)\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n                        <path d=\"M2 17L12 22L22 17\" stroke=\"url(#gradient)\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n                        <path d=\"M2 12L12 17L22 12\" stroke=\"url(#gradient)\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n                        <defs>\n                            <linearGradient id=\"gradient\" x1=\"2\" y1=\"2\" x2=\"22\" y2=\"22\">\n                                <stop stop-color=\"#667eea\"/>\n                                <stop offset=\"1\" stop-color=\"#764ba2\"/>\n                            </linearGradient>\n                        </defs>\n                    </svg>\n                </div>\n                <span class=\"logo-text\">DOJ</span>\n            </router-link>\n\n            <!-- Navigation Items -->\n            <div class=\"nav-items\">\n                <router-link\n                    v-for=\"item in navItems\"\n                    :key=\"item.path\"\n                    class=\"nav-item\"\n                    :to=\"item.path\"\n                    :class=\"{ active: isActive(item.path) }\"\n                >\n                    <span class=\"nav-icon\">\n                        <!-- Home icon -->\n                        <svg v-if=\"item.icon === 'home'\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n                            <path d=\"M3 9l9-7 9 7v11a2 2 0 01-2 2H5a2 2 0 01-2-2z\"/>\n                            <polyline points=\"9 22 9 12 15 12 15 22\"/>\n                        </svg>\n                        <!-- Book icon -->\n                        <svg v-if=\"item.icon === 'book'\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n                            <path d=\"M4 19.5A2.5 2.5 0 016.5 17H20\"/>\n                            <path d=\"M6.5 2H20v20H6.5A2.5 2.5 0 014 19.5v-15A2.5 2.5 0 016.5 2z\"/>\n                        </svg>\n                        <!-- Code icon -->\n                        <svg v-if=\"item.icon === 'code'\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n                            <polyline points=\"16 18 22 12 16 6\"/>\n                            <polyline points=\"8 6 2 12 8 18\"/>\n                        </svg>\n                        <!-- Activity icon -->\n                        <svg v-if=\"item.icon === 'activity'\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n                            <polyline points=\"22 12 18 12 15 21 9 3 6 12 2 12\"/>\n                        </svg>\n                        <!-- Trophy icon -->\n                        <svg v-if=\"item.icon === 'trophy'\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n                            <path d=\"M6 9H4.5a2.5 2.5 0 010-5H6\"/>\n                            <path d=\"M18 9h1.5a2.5 2.5 0 000-5H18\"/>\n                            <path d=\"M4 22h16\"/>\n                            <path d=\"M10 14.66V17c0 .55-.47.98-.97 1.21C7.85 18.75 7 20.24 7 22\"/>\n                            <path d=\"M14 14.66V17c0 .55.47.98.97 1.21C16.15 18.75 17 20.24 17 22\"/>\n                            <path d=\"M18 2H6v7a6 6 0 0012 0V2z\"/>\n                        </svg>\n                    </span>\n                    <span class=\"nav-text\">{{ item.text }}</span>\n                </router-link>\n            </div>\n\n            <!-- User Area -->\n            <div class=\"user-area\">\n                <template v-if=\"userStore.userInfo\">\n                    <el-dropdown trigger=\"click\" @command=\"closeDropdown\" ref=\"dropdown\" placement=\"bottom-end\" popper-class=\"user-dropdown-popper\">\n                        <div class=\"user-button\">\n                            <el-avatar \n                                :size=\"36\" \n                                :src=\"userStore.userInfo.avatar ? '/api' + userStore.userInfo.avatar : ''\"\n                                class=\"user-avatar\"\n                            >\n                                {{ userInitials }}\n                            </el-avatar>\n                            <span class=\"user-name\">{{ userStore.userInfo.username }}</span>\n                            <svg class=\"dropdown-arrow\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n                                <polyline points=\"6 9 12 15 18 9\"/>\n                            </svg>\n                        </div>\n                        <template #dropdown>\n                            <el-dropdown-menu class=\"user-dropdown\">\n                                <!-- Admin Entry -->\n                                <el-dropdown-item command=\"/admin\" v-if=\"!userStore.userInfo.role\">\n                                    <div class=\"dropdown-item-content\">\n                                        <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n                                            <path d=\"M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5\"/>\n                                        </svg>\n                                        <span>后台管理</span>\n                                    </div>\n                                </el-dropdown-item>\n                                <el-dropdown-item divided command=\"/home\">\n                                    <div class=\"dropdown-item-content\">\n                                        <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n                                            <path d=\"M20 21v-2a4 4 0 00-4-4H8a4 4 0 00-4 4v2\"/>\n                                            <circle cx=\"12\" cy=\"7\" r=\"4\"/>\n                                        </svg>\n                                        <span>个人主页</span>\n                                    </div>\n                                </el-dropdown-item>\n                                <el-dropdown-item command=\"/info\">\n                                    <div class=\"dropdown-item-content\">\n                                        <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n                                            <circle cx=\"12\" cy=\"12\" r=\"3\"/>\n                                            <path d=\"M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 010 2.83 2 2 0 01-2.83 0l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-2 2 2 2 0 01-2-2v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 01-2.83 0 2 2 0 010-2.83l.06-.06a1.65 1.65 0 00.33-1.82 1.65 1.65 0 00-1.51-1H3a2 2 0 01-2-2 2 2 0 012-2h.09A1.65 1.65 0 004.6 9a1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 010-2.83 2 2 0 012.83 0l.06.06a1.65 1.65 0 001.82.33H9a1.65 1.65 0 001-1.51V3a2 2 0 012-2 2 2 0 012 2v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 012.83 0 2 2 0 010 2.83l-.06.06a1.65 1.65 0 00-.33 1.82V9a1.65 1.65 0 001.51 1H21a2 2 0 012 2 2 2 0 01-2 2h-.09a1.65 1.65 0 00-1.51 1z\"/>\n                                        </svg>\n                                        <span>设置</span>\n                                    </div>\n                                </el-dropdown-item>\n                                <el-dropdown-item>\n                                    <div class=\"dropdown-item-content theme-switch\" @click.stop=\"toggleTheme\">\n                                        <svg v-if=\"isDark\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n                                            <path d=\"M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z\"></path>\n                                        </svg>\n                                        <svg v-else viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n                                            <circle cx=\"12\" cy=\"12\" r=\"5\"></circle>\n                                            <line x1=\"12\" y1=\"1\" x2=\"12\" y2=\"3\"></line>\n                                            <line x1=\"12\" y1=\"21\" x2=\"12\" y2=\"23\"></line>\n                                            <line x1=\"4.22\" y1=\"4.22\" x2=\"5.64\" y2=\"5.64\"></line>\n                                            <line x1=\"18.36\" y1=\"18.36\" x2=\"19.78\" y2=\"19.78\"></line>\n                                            <line x1=\"1\" y1=\"12\" x2=\"3\" y2=\"12\"></line>\n                                            <line x1=\"21\" y1=\"12\" x2=\"23\" y2=\"12\"></line>\n                                            <line x1=\"4.22\" y1=\"19.78\" x2=\"5.64\" y2=\"18.36\"></line>\n                                            <line x1=\"18.36\" y1=\"5.64\" x2=\"19.78\" y2=\"4.22\"></line>\n                                        </svg>\n                                        <span>{{ isDark ? '暗色模式' : '亮色模式' }}</span>\n                                    </div>\n                                </el-dropdown-item>\n                                <el-dropdown-item divided @click=\"logout\" class=\"logout-item\">\n                                    <div class=\"dropdown-item-content\">\n                                        <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n                                            <path d=\"M9 21H5a2 2 0 01-2-2V5a2 2 0 012-2h4\"/>\n                                            <polyline points=\"16 17 21 12 16 7\"/>\n                                            <line x1=\"21\" y1=\"12\" x2=\"9\" y2=\"12\"/>\n                                        </svg>\n                                        <span>退出登录</span>\n                                    </div>\n                                </el-dropdown-item>\n                            </el-dropdown-menu>\n                        </template>\n                    </el-dropdown>\n                </template>\n                <template v-else>\n                    <router-link to=\"/login\" class=\"auth-btn login-btn\">\n                        登录\n                    </router-link>\n                    <router-link to=\"/register\" class=\"auth-btn register-btn\">\n                        注册\n                    </router-link>\n                </template>\n            </div>\n        </nav>\n    </header>\n</template>\n\n<style scoped lang=\"scss\">\n\n\n.navbar {\n    position: fixed;\n    top: 0;\n    left: 0;\n    right: 0;\n    height: $navbar-height;\n    z-index: $z-fixed;\n    @include glass;\n    border-bottom: 1px solid var(--border-color);\n}\n\n.navbar-inner {\n    max-width: $container-max;\n    height: 100%;\n    margin: 0 auto;\n    padding: 0 $container-padding;\n    display: flex;\n    align-items: center;\n    justify-content: space-between;\n}\n\n// Logo\n.logo {\n    display: flex;\n    align-items: center;\n    gap: $space-sm;\n    text-decoration: none;\n    \n    .logo-icon {\n        width: 32px;\n        height: 32px;\n        \n        svg {\n            width: 100%;\n            height: 100%;\n        }\n    }\n    \n    .logo-text {\n        font-size: $font-size-xl;\n        font-weight: $font-weight-bold;\n        @include gradient-text;\n    }\n}\n\n// Navigation\n.nav-items {\n    display: flex;\n    align-items: center;\n    gap: $space-xs;\n}\n\n.nav-item {\n    display: flex;\n    align-items: center;\n    gap: $space-sm;\n    padding: $space-sm $space-md;\n    color: var(--text-secondary);\n    text-decoration: none;\n    border-radius: $radius-md;\n    transition: all $transition-fast;\n    position: relative;\n    \n    .nav-icon {\n        width: 18px;\n        height: 18px;\n        display: flex;\n        align-items: center;\n        justify-content: center;\n        \n        svg {\n            width: 100%;\n            height: 100%;\n        }\n    }\n    \n    .nav-text {\n        font-size: $font-size-sm;\n        font-weight: $font-weight-medium;\n    }\n    \n    &:hover {\n        color: var(--text-primary);\n        background: var(--glass-bg);\n    }\n    \n    &.active {\n        color: var(--primary-start);\n        background: rgba(102, 126, 234, 0.1);\n        \n        &::after {\n            content: '';\n            position: absolute;\n            bottom: -1px;\n            left: 50%;\n            transform: translateX(-50%);\n            width: 24px;\n            height: 2px;\n            background: var(--primary-gradient);\n            border-radius: $radius-full;\n        }\n    }\n}\n\n// User Area\n.user-area {\n    display: flex;\n    align-items: center;\n    gap: $space-md;\n}\n\n.user-button {\n    display: flex;\n    align-items: center;\n    gap: $space-sm;\n    padding: $space-xs $space-sm;\n    border-radius: $radius-full;\n    cursor: pointer;\n    transition: all $transition-fast;\n    \n    &:hover {\n        background: var(--glass-bg);\n    }\n    \n    .user-avatar {\n        border: 2px solid var(--border-light);\n        background: var(--primary-gradient);\n        color: white;\n        font-weight: $font-weight-semibold;\n    }\n    \n    .user-name {\n        font-size: $font-size-sm;\n        font-weight: $font-weight-medium;\n        color: var(--text-primary);\n        max-width: 100px;\n        overflow: hidden;\n        text-overflow: ellipsis;\n        white-space: nowrap;\n    }\n    \n    .dropdown-arrow {\n        width: 16px;\n        height: 16px;\n        color: var(--text-muted);\n        transition: transform $transition-fast;\n    }\n}\n\n// Auth Buttons\n.auth-btn {\n    padding: $space-sm $space-lg;\n    font-size: $font-size-sm;\n    font-weight: $font-weight-medium;\n    border-radius: $radius-full;\n    text-decoration: none;\n    transition: all $transition-fast;\n}\n\n.login-btn {\n    color: var(--text-primary);\n    \n    &:hover {\n        background: var(--glass-bg);\n    }\n}\n\n.register-btn {\n    background: var(--primary-gradient);\n    color: white;\n    \n    &:hover {\n        box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);\n        transform: translateY(-1px);\n    }\n}\n\n\n\n// Dropdown Menu Styles\n:deep(.user-dropdown) {\n    min-width: 180px;\n    padding: $space-xs;\n    background: var(--bg-elevated);\n    border: 1px solid var(--border-color);\n    box-shadow: var(--shadow-lg);\n    border-radius: $radius-md;\n    \n    .el-dropdown-menu__item {\n        padding: $space-sm;\n        border-radius: $radius-sm;\n        color: var(--text-primary);\n        \n        &:hover, &:focus {\n            background-color: var(--bg-primary);\n            color: var(--primary-start);\n        }\n        \n        &.logout-item {\n            .dropdown-item-content {\n                color: $danger;\n            }\n            \n            &:hover {\n                background: $danger-light !important;\n            }\n        }\n        \n        &.is-divided {\n            border-top: 1px solid var(--border-color);\n            margin-top: $space-xs;\n        }\n    }\n}\n\n.dropdown-item-content {\n    display: flex;\n    align-items: center;\n    gap: $space-sm;\n    width: 100%;\n    \n    svg {\n        width: 16px;\n        height: 16px;\n    }\n    \n    &.theme-switch {\n        cursor: pointer;\n        justify-content: flex-start;\n    }\n}\n\n// Mobile Responsive\n@include mobile {\n    .nav-text {\n        display: none;\n    }\n    \n    .nav-item {\n        padding: $space-sm;\n    }\n    \n    .user-name {\n        display: none;\n    }\n    \n    .logo-text {\n        display: none;\n    }\n}\n</style>\n\n<style lang=\"scss\">\n// Global styles for dropdown popper\n.user-dropdown-popper {\n    &.el-popper {\n        border: 1px solid var(--border-color) !important;\n        background: var(--bg-elevated) !important;\n    }\n\n    .el-popper__arrow::before {\n        background: var(--bg-elevated) !important;\n        border: 1px solid var(--border-color) !important;\n    }\n}\n</style>"
  },
  {
    "path": "DOJ-FE/src/views/Layout/components/LayoutFooter.vue",
    "content": "<script lang=\"ts\" setup>\nconst currentYear = new Date().getFullYear();\n</script>\n\n<template>\n    <footer class=\"footer\">\n        <div class=\"footer-inner\">\n            <div class=\"footer-content\">\n                <!-- Logo and Description -->\n                <div class=\"footer-brand\">\n                    <div class=\"brand-logo\">\n                        <svg viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n                            <path d=\"M12 2L2 7L12 12L22 7L12 2Z\" stroke=\"url(#footerGradient)\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n                            <path d=\"M2 17L12 22L22 17\" stroke=\"url(#footerGradient)\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n                            <path d=\"M2 12L12 17L22 12\" stroke=\"url(#footerGradient)\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n                            <defs>\n                                <linearGradient id=\"footerGradient\" x1=\"2\" y1=\"2\" x2=\"22\" y2=\"22\">\n                                    <stop stop-color=\"#667eea\"/>\n                                    <stop offset=\"1\" stop-color=\"#764ba2\"/>\n                                </linearGradient>\n                            </defs>\n                        </svg>\n                        <span>DOJ</span>\n                    </div>\n                    <p class=\"brand-desc\">Duck Online Judge - 现代化在线编程判题平台</p>\n                </div>\n\n                <!-- Links -->\n                <div class=\"footer-links\">\n                    <a href=\"https://github.com\" target=\"_blank\" class=\"footer-link\">\n                        <svg viewBox=\"0 0 24 24\" fill=\"currentColor\">\n                            <path d=\"M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z\"/>\n                        </svg>\n                    </a>\n                </div>\n            </div>\n\n            <!-- Copyright -->\n            <div class=\"footer-bottom\">\n                <p class=\"copyright\">\n                    © {{ currentYear }} Duck Online Judge. Powered by <span class=\"highlight\">Decade</span>\n                </p>\n                <p class=\"icp\">苏ICP备3102128号</p>\n            </div>\n        </div>\n    </footer>\n</template>\n\n<style scoped lang=\"scss\">\n\n\n.footer {\n    margin-top: auto;\n    border-top: 1px solid var(--border-color);\n    background: var(--bg-secondary);\n}\n\n.footer-inner {\n    max-width: $container-max;\n    margin: 0 auto;\n    padding: $space-xl $container-padding;\n}\n\n.footer-content {\n    display: flex;\n    justify-content: space-between;\n    align-items: center;\n    margin-bottom: $space-lg;\n    \n    @include mobile {\n        flex-direction: column;\n        gap: $space-lg;\n        text-align: center;\n    }\n}\n\n.footer-brand {\n    .brand-logo {\n        display: flex;\n        align-items: center;\n        gap: $space-sm;\n        margin-bottom: $space-sm;\n        \n        svg {\n            width: 24px;\n            height: 24px;\n        }\n        \n        span {\n            font-size: $font-size-lg;\n            font-weight: $font-weight-bold;\n            @include gradient-text;\n        }\n    }\n    \n    .brand-desc {\n        color: var(--text-muted);\n        font-size: $font-size-sm;\n    }\n}\n\n.footer-links {\n    display: flex;\n    gap: $space-md;\n}\n\n.footer-link {\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    width: 40px;\n    height: 40px;\n    border-radius: $radius-full;\n    background: var(--glass-bg);\n    color: var(--text-secondary);\n    transition: all $transition-fast;\n    \n    svg {\n        width: 20px;\n        height: 20px;\n    }\n    \n    &:hover {\n        background: var(--primary-gradient);\n        color: white;\n        transform: translateY(-2px);\n    }\n}\n\n.footer-bottom {\n    padding-top: $space-lg;\n    border-top: 1px solid var(--border-color);\n    text-align: center;\n    \n    .copyright {\n        color: var(--text-muted);\n        font-size: $font-size-sm;\n        margin-bottom: $space-xs;\n        \n        .highlight {\n            @include gradient-text;\n            font-weight: $font-weight-medium;\n        }\n    }\n    \n    .icp {\n        color: var(--text-disabled);\n        font-size: $font-size-xs;\n    }\n}\n</style>"
  },
  {
    "path": "DOJ-FE/src/views/Layout/index.vue",
    "content": "<script lang=\"ts\" setup>\nimport LayoutFooter from '@/views/Layout/components/LayoutFooter.vue'\nimport LayoutFixed from '@/views/Layout/components/LayoutFixed.vue'\nimport { provide, ref } from 'vue';\n\nconst refresh = ref(0);\nconst triggerRefresh = () => {\n    refresh.value += 1;\n};\nprovide('triggerRefresh', triggerRefresh);\n</script>\n\n<template>\n    <div class=\"app-layout\">\n        <!-- Animated Background -->\n        <div class=\"bg-gradient\"></div>\n        <div class=\"bg-grid\"></div>\n        \n        <!-- Header -->\n        <LayoutFixed />\n        \n        <!-- Main Content -->\n        <main class=\"main-content\">\n            <div class=\"content-wrapper\">\n                <router-view v-slot=\"{ Component }\" :key=\"$route.fullPath\">\n                    <transition name=\"page\" mode=\"out-in\">\n                        <component :is=\"Component\" />\n                    </transition>\n                </router-view>\n            </div>\n        </main>\n        \n        <!-- Footer -->\n        <LayoutFooter />\n    </div>\n</template>\n\n<style scoped lang=\"scss\">\n\n\n.app-layout {\n    min-height: 100vh;\n    display: flex;\n    flex-direction: column;\n    position: relative;\n    overflow-x: hidden;\n}\n\n// Animated gradient background\n.bg-gradient {\n    position: fixed;\n    top: 0;\n    left: 0;\n    right: 0;\n    bottom: 0;\n    background: \n        radial-gradient(ellipse at 20% 20%, rgba(102, 126, 234, 0.15) 0%, transparent 50%),\n        radial-gradient(ellipse at 80% 80%, rgba(118, 75, 162, 0.15) 0%, transparent 50%),\n        radial-gradient(ellipse at 50% 50%, rgba(0, 212, 255, 0.05) 0%, transparent 70%),\n        var(--bg-primary);\n    z-index: -2;\n}\n\n// Subtle grid pattern\n.bg-grid {\n    position: fixed;\n    top: 0;\n    left: 0;\n    right: 0;\n    bottom: 0;\n    background-image: \n        linear-gradient(rgba(255, 255, 255, 0.02) 1px, transparent 1px),\n        linear-gradient(90deg, rgba(255, 255, 255, 0.02) 1px, transparent 1px);\n    background-size: 50px 50px;\n    z-index: -1;\n}\n\n.main-content {\n    flex: 1;\n    padding-top: calc($navbar-height + $space-lg);\n    padding-bottom: $space-xl;\n    min-height: calc(100vh - $navbar-height - $footer-height);\n}\n\n.content-wrapper {\n    max-width: $container-max;\n    margin: 0 auto;\n    padding: 0 $container-padding;\n    \n    @include mobile {\n        padding: 0 $space-md;\n    }\n}\n\n// Page transition animations\n.page-enter-active,\n.page-leave-active {\n    transition: all 0.3s ease;\n}\n\n.page-enter-from {\n    opacity: 0;\n    transform: translateY(20px);\n}\n\n.page-leave-to {\n    opacity: 0;\n    transform: translateY(-10px);\n}\n</style>"
  },
  {
    "path": "DOJ-FE/src/views/OnlineEditor/index.vue",
    "content": "<template>\n    <div class=\"online-editor-page\">\n        <!-- Page Header -->\n        <div class=\"page-header\">\n            <h1 class=\"page-title\">\n                <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n                    <polyline points=\"16 18 22 12 16 6\"/>\n                    <polyline points=\"8 6 2 12 8 18\"/>\n                </svg>\n                在线编程\n            </h1>\n            <p class=\"page-desc\">随时随地编写并运行代码</p>\n        </div>\n\n        <div class=\"editor-container card-glass\">\n            <Editor :config=\"config\" />\n        </div>\n    </div>\n</template>\n\n<script setup lang='ts'>\nimport { ref } from 'vue';\nimport { configType } from '@/components/CodeEditor/index.vue';\nimport Editor from '@/components/CodeEditor/index.vue';\n\nconst config = ref<configType>({\n    tabSize: 4,\n    disabled: false,\n    height: '65vh',\n    width: '100%',\n    language: 'cpp',\n    theme: 'default',\n    fontSize: 16,\n    editorType: 'code'\n})\n</script>\n\n<style scoped lang=\"scss\">\n\n\n.online-editor-page {\n    display: flex;\n    flex-direction: column;\n    gap: $space-lg;\n    max-width: 1400px;\n    margin: 0 auto;\n    width: 100%;\n}\n\n.page-header {\n    .page-title {\n        display: flex;\n        align-items: center;\n        gap: $space-sm;\n        font-size: $font-size-2xl;\n        font-weight: $font-weight-bold;\n        margin-bottom: $space-xs;\n        \n        svg {\n            width: 28px;\n            height: 28px;\n            color: var(--primary-start);\n        }\n    }\n    \n    .page-desc {\n        color: var(--text-secondary);\n        font-size: $font-size-sm;\n    }\n}\n\n.editor-container {\n    padding: $space-md;\n    border-radius: $radius-lg;\n    overflow: hidden;\n}\n</style>"
  },
  {
    "path": "DOJ-FE/src/views/Problem/detail/index.vue",
    "content": "<template>\n    <div class=\"problem-detail-page\">\n        <section class=\"hero card-glass\">\n            <div class=\"hero-main\">\n                <div class=\"title-row\">\n                    <span class=\"problem-id\">#{{ problem?.id }}</span>\n                    <h1 class=\"problem-title\">{{ problem?.name }}</h1>\n                </div>\n            </div>\n            <div class=\"hero-actions\">\n                <button class=\"btn-primary\" @click=\"scrollToEditor\">\n                    <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"10\">\n                        <polyline points=\"16 18 22 12 16 6\"/>\n                        <polyline points=\"8 6 2 12 8 18\"/>\n                    </svg>\n                    提交代码\n                </button>\n                <button class=\"btn-secondary\" @click=\"personSubmission\">我的提交</button>\n            </div>\n        </section>\n\n        <div class=\"problem-layout\">\n            <main class=\"problem-main\">\n                <div class=\"problem-content-wrapper\">\n                    <section class=\"content-card card-glass\">\n                        <div class=\"content-section\">\n                            <h2 class=\"section-title\">题目描述</h2>\n                            <div class=\"markdown-body\" v-html=\"renderMarkdown(problem?.description)\"></div>\n                        </div>\n\n                        <div class=\"content-section\">\n                            <h2 class=\"section-title\">输入格式</h2>\n                            <div class=\"markdown-body\" v-html=\"renderMarkdown(problem?.inputStyle)\"></div>\n                        </div>\n\n                        <div class=\"content-section\">\n                            <h2 class=\"section-title\">输出格式</h2>\n                            <div class=\"markdown-body\" v-html=\"renderMarkdown(problem?.outputStyle)\"></div>\n                        </div>\n\n                        <div class=\"content-section\">\n                            <h2 class=\"section-title\">样例</h2>\n                            <div v-if=\"problem?.inputSample?.length\" class=\"samples\">\n                                <div v-for=\"(input, idx) in problem?.inputSample\" :key=\"idx\" class=\"sample-pair\">\n                                    <div class=\"sample-card\">\n                                        <div class=\"sample-header\">\n                                            <span>输入样例 #{{ idx + 1 }}</span>\n                                            <button class=\"copy-tiny\" @click=\"copyText(input)\">复制</button>\n                                        </div>\n                                        <pre class=\"sample-code\">{{ input }}</pre>\n                                    </div>\n                                    <div class=\"sample-card\">\n                                        <div class=\"sample-header\">\n                                            <span>输出样例 #{{ idx + 1 }}</span>\n                                            <button class=\"copy-tiny\" @click=\"copyText(problem?.outputSample?.[idx])\">复制</button>\n                                        </div>\n                                        <pre class=\"sample-code\">{{ problem?.outputSample?.[idx] }}</pre>\n                                    </div>\n                                </div>\n                            </div>\n                            <div v-else class=\"empty-block\">暂无样例</div>\n                        </div>\n\n                        <div v-if=\"problem?.hint\" class=\"content-section\">\n                            <h2 class=\"section-title\">提示</h2>\n                            <div class=\"markdown-body\" v-html=\"renderMarkdown(problem?.hint)\"></div>\n                        </div>\n                    </section>\n                </div>\n            </main>\n\n            <aside class=\"problem-sidebar\">\n                <div class=\"sidebar-card card-glass\">\n                    <h3 class=\"sidebar-title\">问题信息</h3>\n                    <div class=\"meta-list\">\n                        <div class=\"meta-item-inline\">\n                            <span class=\"meta-label\">难度</span>\n                            <span class=\"difficulty-tag\" :class=\"difficultyClass\">{{ problem?.difficulty }}</span>\n                        </div>\n                        <div class=\"meta-item-inline\">\n                            <span class=\"meta-label\">时间限制</span>\n                            <span class=\"meta-value\">{{ problem?.timeLimit }} ms</span>\n                        </div>\n                        <div class=\"meta-item-inline\">\n                            <span class=\"meta-label\">内存限制</span>\n                            <span class=\"meta-value\">{{ problem?.memoryLimit }} MB</span>\n                        </div>\n                    </div>\n                </div>\n\n                <div class=\"sidebar-card card-glass\">\n                    <h3 class=\"sidebar-title\">标签</h3>\n                    <div class=\"tag-row\">\n                        <span v-for=\"tag in problem?.tags\" :key=\"tag\" class=\"tag-chip\" @click=\"searchByTag(tag)\">{{ tag }}</span>\n                        <span v-if=\"!problem?.tags?.length\" class=\"tag-empty\">暂无标签</span>\n                    </div>\n                </div>\n\n                <div class=\"sidebar-card card-glass\">\n                    <h3 class=\"sidebar-title\">提交统计</h3>\n                    <div class=\"stats-grid\">\n                        <div class=\"stat-item\">\n                            <span class=\"stat-label\">提交次数</span>\n                            <span class=\"stat-value\">{{ problem?.totalAttempt }}</span>\n                        </div>\n                        <div class=\"stat-item\">\n                            <span class=\"stat-label\">通过次数</span>\n                            <span class=\"stat-value\">{{ problem?.totalPass }}</span>\n                        </div>\n                        <div class=\"stat-item\">\n                            <span class=\"stat-label\">通过率</span>\n                            <span class=\"stat-value\">{{ passRate }}%</span>\n                        </div>\n                    </div>\n                </div>\n            </aside>\n        </div>\n\n        <div class=\"editor-container\" ref=\"editorRef\">\n            <div class=\"editor-card card-glass\">\n                <div class=\"editor-header\">\n                    <h2>代码编辑器</h2>\n                </div>\n                <Editor :config=\"config\" />\n            </div>\n        </div>\n    </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { onMounted, ref, computed } from 'vue';\nimport { useRoute, useRouter } from 'vue-router';\nimport { reqProblemDetail } from '@/api/problem';\nimport type { ProblemType } from '@/api/problem/type';\nimport 'katex/dist/katex.min.css';\nimport { configType } from '@/components/CodeEditor/index.vue';\nimport Editor from '@/components/CodeEditor/index.vue';\nimport { useUserStore } from '@/stores/userStore';\nimport { renderMarkdown } from '@/utils/markdown';\n\nconst config = ref<configType>({\n    tabSize: 4,\n    disabled: false,\n    height: '60vh',\n    width: '100%',\n    language: 'cpp',\n    theme: 'default',\n    fontSize: 16,\n    editorType: 'problem'\n});\n\nconst route = useRoute();\nconst router = useRouter();\nconst problem = ref<ProblemType>();\nconst userStore = useUserStore();\nconst editorRef = ref<HTMLElement | null>(null);\n\nconst difficultyClass = computed(() => {\n    if (!problem.value) return '';\n    const diff = problem.value.difficulty;\n    return {\n        easy: diff === '简单',\n        medium: diff === '中等',\n        hard: diff === '困难'\n    };\n});\n\nconst passRate = computed(() => {\n    if (!problem.value || problem.value.totalAttempt === 0) return '0.0';\n    return (problem.value.totalPass / problem.value.totalAttempt * 100).toFixed(1);\n});\n\nconst scrollToEditor = () => {\n    editorRef.value?.scrollIntoView({ behavior: 'smooth' });\n};\n\nconst copyText = (text: string | undefined) => {\n    if (text) {\n        navigator.clipboard.writeText(text).catch(err => {\n            console.error('复制失败：', err);\n        });\n    }\n};\n\nconst personSubmission = () => {\n    router.push({\n        path: '/status',\n        query: {\n            problemName: problem.value?.name || '',\n            userName: userStore.userInfo?.username || ''\n        }\n    });\n};\n\nconst searchByTag = (tag: string) => {\n    router.push({\n        path: '/problem',\n        query: { tag }\n    });\n};\n\nonMounted(async () => {\n    const pid = route.params.id as string;\n    problem.value = (await reqProblemDetail(pid)).data.data;\n});\n</script>\n\n<style scoped lang=\"scss\">\n.problem-detail-page {\n    padding: $space-lg;\n    max-width: 1400px;\n    margin: 0 auto;\n    display: flex;\n    flex-direction: column;\n    gap: $space-xl;\n}\n\n.hero {\n    display: grid;\n    grid-template-columns: 1fr auto;\n    gap: $space-lg;\n    align-items: center;\n    background: var(--bg-elevated);\n    border-radius: $radius-lg;\n\n    @include tablet {\n        grid-template-columns: 1fr;\n    }\n}\n\n.hero-main {\n    display: flex;\n    flex-direction: column;\n    gap: $space-md;\n}\n\n.title-row {\n    display: flex;\n    align-items: baseline;\n    gap: $space-sm;\n}\n\n.problem-id {\n    padding: 2px 10px;\n    border-radius: $radius-full;\n    background: var(--bg-primary);\n    border: 1px solid var(--border-color);\n    color: var(--text-muted);\n    font-size: $font-size-xs;\n}\n\n.problem-title {\n    margin: 0;\n    font-size: $font-size-2xl;\n    color: var(--text-primary);\n}\n\n.hero-meta {\n    display: flex;\n    flex-wrap: wrap;\n    gap: $space-sm;\n}\n\n.meta-chip {\n    padding: 6px 12px;\n    border-radius: $radius-full;\n    background: var(--bg-primary);\n    border: 1px solid var(--border-color);\n    color: var(--text-secondary);\n    font-size: $font-size-xs;\n}\n\n.tag-row {\n    display: flex;\n    flex-wrap: wrap;\n    gap: $space-xs;\n}\n\n.tag-chip {\n    padding: 4px 12px;\n    border-radius: $radius-full;\n    background: $info-light;\n    border: 1px solid rgba($info, 0.2);\n    color: var(--text-secondary);\n    font-size: $font-size-xs;\n    transition: all $transition-fast;\n    cursor: pointer;\n\n    &:hover {\n        background: rgba($info, 0.25);\n        border-color: rgba($info, 0.4);\n    }\n}\n\n.tag-empty {\n    color: var(--text-muted);\n    font-size: $font-size-xs;\n}\n\n.hero-actions {\n    display: flex;\n    gap: $space-md;\n    align-items: center;\n    flex-shrink: 0;\n    flex-wrap: wrap;\n    justify-content: flex-end;\n\n    button {\n        padding: 0 $space-lg;\n        height: 40px;\n        display: flex;\n        align-items: center;\n        gap: $space-sm;\n        white-space: nowrap;\n        min-width: 120px;\n    }\n}\n\n.problem-layout {\n    display: grid;\n    grid-template-columns: 1fr 320px;\n    gap: $space-xl;\n\n    @include tablet {\n        grid-template-columns: 1fr;\n    }\n}\n\n.problem-main {\n    display: flex;\n    flex-direction: column;\n    gap: $space-lg;\n}\n\n.content-card {\n    padding: $space-lg;\n    background: var(--bg-elevated);\n    border-radius: $radius-lg;\n}\n\n.content-section + .content-section {\n    margin-top: $space-lg;\n    padding-top: $space-lg;\n    border-top: 1px solid var(--border-color);\n}\n\n.section-title {\n    margin: 0 0 $space-md;\n    font-size: $font-size-lg;\n    color: var(--primary-start);\n    display: flex;\n    align-items: center;\n    gap: $space-sm;\n    border-bottom: 1px solid var(--border-color);\n    padding-bottom: $space-xs;\n}\n\n.samples {\n    display: flex;\n    flex-direction: column;\n    gap: $space-lg;\n}\n\n.sample-pair {\n    display: grid;\n    grid-template-columns: 1fr 1fr;\n    gap: $space-lg;\n\n    @include tablet {\n        grid-template-columns: 1fr;\n    }\n}\n\n.sample-card {\n    background: var(--bg-primary);\n    border-radius: $radius-md;\n    padding: $space-md;\n}\n\n.sample-header {\n    display: flex;\n    justify-content: space-between;\n    align-items: center;\n    font-size: $font-size-xs;\n    color: var(--text-secondary);\n    margin-bottom: $space-xs;\n}\n\n.sample-code {\n    margin: 0;\n    white-space: pre-wrap;\n    font-family: $font-mono;\n    font-size: $font-size-sm;\n    color: var(--text-primary);\n}\n\n.copy-tiny {\n    background: transparent;\n    border: 1px solid var(--border-color);\n    border-radius: $radius-sm;\n    padding: 2px 8px;\n    font-size: 10px;\n    cursor: pointer;\n\n    &:hover {\n        background: var(--glass-bg);\n    }\n}\n\n.empty-block {\n    color: var(--text-muted);\n    font-size: $font-size-sm;\n}\n\n.problem-sidebar {\n    display: flex;\n    flex-direction: column;\n    gap: $space-lg;\n}\n\n.sidebar-card {\n    padding: $space-lg;\n    background: var(--bg-elevated);\n    border-radius: $radius-lg;\n}\n\n.sidebar-title {\n    font-size: $font-size-base;\n    font-weight: 600;\n    margin-bottom: $space-md;\n}\n\n.stats-grid {\n    display: grid;\n    gap: $space-sm;\n}\n\n.stat-item {\n    display: flex;\n    justify-content: space-between;\n    font-size: $font-size-sm;\n    color: var(--text-secondary);\n}\n\n.stat-value {\n    color: var(--text-primary);\n    font-weight: 500;\n}\n\n.meta-item-inline {\n    display: flex;\n    justify-content: space-between;\n    align-items: center;\n    font-size: $font-size-sm;\n    color: var(--text-secondary);\n    padding: $space-xs 0;\n\n    &:not(:last-child) {\n        border-bottom: 1px solid var(--border-color);\n    }\n}\n\n.difficulty-tag {\n    padding: 2px 10px;\n    border-radius: $radius-full;\n    font-size: 12px;\n    &.easy { background: rgba($success, 0.12); color: $success; }\n    &.medium { background: rgba($warning, 0.12); color: $warning; }\n    &.hard { background: rgba($danger, 0.12); color: $danger; }\n}\n\n.markdown-body {\n    line-height: 1.7;\n    color: var(--text-secondary);\n\n    :deep(h1),\n    :deep(h2),\n    :deep(h3) {\n        color: var(--text-primary);\n        margin: $space-md 0 $space-sm;\n        font-weight: 600;\n    }\n\n    :deep(p) {\n        margin: 0 0 $space-sm;\n    }\n\n    :deep(ul),\n    :deep(ol) {\n        padding-left: 1.5em;\n        margin: 0 0 $space-sm;\n    }\n\n    :deep(code) {\n        background: var(--bg-primary);\n        padding: 2px 6px;\n        border-radius: 6px;\n        font-family: $font-mono;\n        font-size: $font-size-sm;\n    }\n\n    :deep(pre) {\n        background: var(--bg-primary);\n        padding: $space-md;\n        border-radius: $radius-md;\n        overflow: auto;\n    }\n\n    :deep(blockquote) {\n        margin: $space-md 0;\n        padding: $space-sm $space-md;\n        border-left: 3px solid var(--primary-start);\n        background: var(--glass-bg);\n    }\n}\n\n.editor-container {\n    .editor-card {\n        padding: 0;\n        overflow: hidden;\n    }\n\n    .editor-header {\n        padding: $space-md $space-lg;\n        border-bottom: 1px solid var(--border-color);\n    }\n}\n</style>\n"
  },
  {
    "path": "DOJ-FE/src/views/Problem/index.vue",
    "content": "<template>\n    <div class=\"problem-page\">\n        <!-- Page Header -->\n        <div class=\"page-header\">\n            <h1 class=\"page-title\">\n                <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n                    <path d=\"M4 19.5A2.5 2.5 0 016.5 17H20\"/>\n                    <path d=\"M6.5 2H20v20H6.5A2.5 2.5 0 014 19.5v-15A2.5 2.5 0 016.5 2z\"/>\n                </svg>\n                题目列表\n            </h1>\n            <p class=\"page-desc\">共 {{ total }} 道题目，开始你的编程之旅</p>\n        </div>\n\n        <!-- Filters -->\n        <div class=\"filters-section card-glass\">\n            <div class=\"filter-group\">\n                <el-select v-model=\"searchDifficulty\" placeholder=\"难度\" @clear=\"searchDifficulty = null\" clearable class=\"filter-select\">\n                    <el-option label=\"简单\" value=\"简单\">\n                        <span class=\"difficulty-option easy\">简单</span>\n                    </el-option>\n                    <el-option label=\"中等\" value=\"中等\">\n                        <span class=\"difficulty-option medium\">中等</span>\n                    </el-option>\n                    <el-option label=\"困难\" value=\"困难\">\n                        <span class=\"difficulty-option hard\">困难</span>\n                    </el-option>\n                </el-select>\n                \n                <el-select v-model=\"searchStatus\" placeholder=\"状态\" clearable @clear=\"searchStatus = null\" class=\"filter-select\">\n                    <el-option label=\"已解决\" value=\"已解决\">\n                        <span class=\"status-option passed\">✓ 已解决</span>\n                    </el-option>\n                    <el-option label=\"未开始\" value=\"未开始\">\n                        <span class=\"status-option\">未开始</span>\n                    </el-option>\n                    <el-option label=\"尝试中\" value=\"尝试中\">\n                        <span class=\"status-option trying\">尝试中</span>\n                    </el-option>\n                </el-select>\n                \n                <el-input \n                    v-model=\"searchTag\" \n                    placeholder=\"标签 (用逗号分隔)\" \n                    clearable \n                    class=\"filter-input\"\n                    :prefix-icon=\"Tag\"\n                />\n                \n                <el-input \n                    v-model=\"searchName\" \n                    placeholder=\"搜索题目名称或内容...\" \n                    clearable \n                    class=\"filter-input search-input\"\n                    :prefix-icon=\"Search\"\n                />\n            </div>\n        </div>\n\n        <!-- Problem Table -->\n        <div class=\"table-container card-glass\">\n            <el-table \n                :data=\"tableData\" \n                :default-sort=\"{ prop: 'id', order: 'ascending' }\"\n                class=\"problem-table\"\n                v-loading=\"loading\"\n            >\n                <el-table-column prop=\"status\" label=\"状态\" align=\"center\" width=\"80\">\n                    <template v-slot=\"scope\">\n                        <div class=\"status-cell\">\n                            <span v-if=\"!scope.row.status || scope.row.status === '未开始'\" class=\"status-icon empty\"></span>\n                            <span v-else-if=\"scope.row.status === '已解决'\" class=\"status-icon passed\">\n                                <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"3\">\n                                    <polyline points=\"20 6 9 17 4 12\"/>\n                                </svg>\n                            </span>\n                            <span v-else-if=\"scope.row.status === '尝试中'\" class=\"status-icon trying\">\n                                <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n                                    <circle cx=\"12\" cy=\"12\" r=\"10\"/>\n                                    <line x1=\"12\" y1=\"8\" x2=\"12\" y2=\"12\"/>\n                                    <line x1=\"12\" y1=\"16\" x2=\"12.01\" y2=\"16\"/>\n                                </svg>\n                            </span>\n                        </div>\n                    </template>\n                </el-table-column>\n                \n                <el-table-column prop=\"id\" label=\"#\" sortable align=\"center\" width=\"80\" />\n                \n                <el-table-column prop=\"name\" label=\"题目\" min-width=\"200\">\n                    <template v-slot=\"scope\">\n                        <span class=\"problem-name\" @click=\"toProblem(scope.row.id)\">\n                            {{ scope.row.name }}\n                        </span>\n                    </template>\n                </el-table-column>\n                \n                <el-table-column prop=\"difficulty\" label=\"难度\" align=\"center\" width=\"100\">\n                    <template v-slot=\"scope\">\n                        <span \n                            class=\"difficulty-tag\"\n                            :class=\"{\n                                'easy': scope.row.difficulty === '简单',\n                                'medium': scope.row.difficulty === '中等',\n                                'hard': scope.row.difficulty === '困难'\n                            }\"\n                        >\n                            {{ scope.row.difficulty }}\n                        </span>\n                    </template>\n                </el-table-column>\n                \n                <el-table-column prop=\"totalAttempt\" label=\"提交\" align=\"center\" width=\"100\">\n                    <template v-slot=\"scope\">\n                        <span class=\"attempt-count\">{{ scope.row.totalAttempt }}</span>\n                    </template>\n                </el-table-column>\n                \n                <el-table-column prop=\"passRate\" label=\"通过率\" sortable width=\"120\" align=\"center\">\n                    <template v-slot=\"scope\">\n                        <div class=\"pass-rate\">\n                            <div class=\"rate-bar\">\n                                <div \n                                    class=\"rate-fill\" \n                                    :style=\"{ width: getPassRate(scope.row) + '%' }\"\n                                    :class=\"{ high: getPassRate(scope.row) >= 60, medium: getPassRate(scope.row) >= 30 && getPassRate(scope.row) < 60 }\"\n                                ></div>\n                            </div>\n                            <span class=\"rate-text\">{{ formatPassRate(scope.row) }}</span>\n                        </div>\n                    </template>\n                </el-table-column>\n            </el-table>\n\n            <!-- Pagination -->\n            <div class=\"pagination-container\">\n                <el-pagination \n                    background \n                    layout=\"prev, pager, next, sizes, total\"\n                    :total=\"total\" \n                    :page-count=\"pages\" \n                    :page-sizes=\"pageSizes\" \n                    v-model:page-size=\"pageQueryFrom.pageSize\" \n                    v-model:current-page=\"pageQueryFrom.pageNo\"\n                    @change=\"getProblemList\"\n                />\n            </div>\n        </div>\n    </div>\n</template>\n\n<script setup lang='ts'>\nimport { ElTable, ElTableColumn, ElSelect, ElOption, ElInput, ElPagination } from 'element-plus';\nimport { Search, PriceTag as Tag } from '@element-plus/icons-vue';\nimport { ref, onMounted, watch } from 'vue';\nimport { useRouter, useRoute } from 'vue-router';\nimport { reqProblemPageList } from '@/api/problem';\nimport type { ProblemType } from '@/api/problem/type';\nimport type { BasePageQueryForm } from '@/api/base';\n\nconst router = useRouter();\nconst route = useRoute();\n\nconst searchDifficulty = ref();\nconst searchStatus = ref<string | null>(null);\nconst searchTag = ref();\nconst searchName = ref();\nconst loading = ref(false);\n\nwatch([searchDifficulty, searchStatus, searchTag, searchName], () => {\n    getProblemList();\n});\n\nconst getPassRate = (row: any) => {\n    if (!row.totalAttempt) return 0;\n    return (row.totalPass / row.totalAttempt * 100);\n};\n\nconst formatPassRate = (row: any) => {\n    return `${getPassRate(row).toFixed(1)}%`;\n};\n\nconst tableData = ref<ProblemType[]>([]);\nconst pageQueryFrom = ref<BasePageQueryForm>({\n    pageNo: 1,\n    pageSize: 20,\n    isAsc: true,\n    sortBy: ''\n});\nconst total = ref(0);\nconst pages = ref(0);\nconst pageSizes = ref([10, 20, 50, 100]);\n\nconst toProblem = (id: number) => {\n    router.push(\"/problem/\" + id);\n};\n\nconst getProblemList = async () => {\n    loading.value = true;\n    try {\n        const tags = searchTag.value ? searchTag.value.split(/[,，]/).map((t: string) => t.trim()).filter((t: string) => t !== '') : [];\n        const params: any = {\n            ...pageQueryFrom.value,\n            name: searchName.value,\n            difficulty: searchDifficulty.value,\n            status: searchStatus.value\n        };\n        if (tags.length > 0) {\n            params.tags = tags;\n        }\n        const res = await reqProblemPageList(params);\n        const { total: totalV, pages: pagesV, list } = res.data.data;\n        total.value = totalV;\n        pages.value = pagesV;\n        tableData.value = list;\n    } finally {\n        loading.value = false;\n    }\n};\n\nonMounted(() => {\n    if (route.query.tag) {\n        searchTag.value = route.query.tag as string;\n    }\n    getProblemList();\n});\n</script>\n\n<style scoped lang=\"scss\">\n\n\n.problem-page {\n    display: flex;\n    flex-direction: column;\n    gap: $space-lg;\n}\n\n// Page Header\n.page-header {\n    .page-title {\n        display: flex;\n        align-items: center;\n        gap: $space-sm;\n        font-size: $font-size-2xl;\n        font-weight: $font-weight-bold;\n        margin-bottom: $space-xs;\n        \n        svg {\n            width: 28px;\n            height: 28px;\n            color: var(--primary-start);\n        }\n    }\n    \n    .page-desc {\n        color: var(--text-secondary);\n        font-size: $font-size-sm;\n    }\n}\n\n// Filters\n.filters-section {\n    padding: $space-md $space-lg;\n}\n\n.filter-group {\n    display: flex;\n    align-items: center;\n    gap: $space-md;\n    flex-wrap: wrap;\n    \n    .filter-select {\n        width: 120px;\n    }\n    \n    .filter-input {\n        width: 180px;\n    }\n    \n    .search-input {\n        width: 250px;\n        margin-left: auto;\n    }\n}\n\n.difficulty-option {\n    &.easy { color: $difficulty-easy; }\n    &.medium { color: $difficulty-medium; }\n    &.hard { color: $difficulty-hard; }\n}\n\n.status-option {\n    &.passed { color: $success; }\n    &.trying { color: $warning; }\n}\n\n// Table Container\n.table-container {\n    padding: 0;\n    overflow: hidden;\n}\n\n.problem-table {\n    --el-table-row-hover-bg-color: var(--glass-bg);\n    \n    :deep(.el-table__header-wrapper) {\n        th {\n            background: var(--bg-elevated) !important;\n            border-bottom: 1px solid var(--border-color) !important;\n        }\n    }\n    \n    :deep(.el-table__body-wrapper) {\n        td {\n            border-bottom: 1px solid var(--border-color) !important;\n        }\n    }\n}\n\n// Status Cell\n.status-cell {\n    display: flex;\n    justify-content: center;\n    \n    .status-icon {\n        width: 20px;\n        height: 20px;\n        display: flex;\n        align-items: center;\n        justify-content: center;\n        \n        svg {\n            width: 16px;\n            height: 16px;\n        }\n        \n        &.passed {\n            color: $success;\n        }\n        \n        &.trying {\n            color: $warning;\n        }\n    }\n}\n\n// Problem Name\n.problem-name {\n    color: var(--text-primary);\n    font-weight: $font-weight-medium;\n    cursor: pointer;\n    transition: color $transition-fast;\n    \n    &:hover {\n        color: var(--primary-start);\n    }\n}\n\n// Difficulty Tag\n.difficulty-tag {\n    padding: $space-xs $space-sm;\n    border-radius: $radius-sm;\n    font-size: $font-size-xs;\n    font-weight: $font-weight-medium;\n    \n    &.easy {\n        background: rgba($difficulty-easy, 0.15);\n        color: $difficulty-easy;\n    }\n    \n    &.medium {\n        background: rgba($difficulty-medium, 0.15);\n        color: $difficulty-medium;\n    }\n    \n    &.hard {\n        background: rgba($difficulty-hard, 0.15);\n        color: $difficulty-hard;\n    }\n}\n\n// Attempt Count\n.attempt-count {\n    color: var(--text-secondary);\n    font-size: $font-size-sm;\n}\n\n// Pass Rate\n.pass-rate {\n    display: flex;\n    align-items: center;\n    gap: $space-sm;\n    \n    .rate-bar {\n        flex: 1;\n        height: 4px;\n        background: var(--border-color);\n        border-radius: $radius-full;\n        overflow: hidden;\n        \n        .rate-fill {\n            height: 100%;\n            background: $danger;\n            border-radius: $radius-full;\n            transition: width $transition-normal;\n            \n            &.medium {\n                background: $warning;\n            }\n            \n            &.high {\n                background: $success;\n            }\n        }\n    }\n    \n    .rate-text {\n        font-size: $font-size-xs;\n        color: var(--text-muted);\n        min-width: 40px;\n        text-align: right;\n    }\n}\n\n// Pagination\n.pagination-container {\n    padding: $space-lg;\n    display: flex;\n    justify-content: center;\n    border-top: 1px solid var(--border-color);\n}\n</style>\n"
  },
  {
    "path": "DOJ-FE/src/views/Rankings/index.vue",
    "content": "<template>\n    <div class=\"rankings-page\">\n        <!-- Page Header -->\n        <div class=\"page-header\">\n            <h1 class=\"page-title\">\n                <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n                    <path d=\"M6 9H4.5a2.5 2.5 0 010-5H6\"/>\n                    <path d=\"M18 9h1.5a2.5 2.5 0 000-5H18\"/>\n                    <path d=\"M4 22h16\"/>\n                    <path d=\"M10 14.66V17c0 .55-.47.98-.97 1.21C7.85 18.75 7 20.24 7 22\"/>\n                    <path d=\"M14 14.66V17c0 .55.47.98.97 1.21C16.15 18.75 17 20.24 17 22\"/>\n                    <path d=\"M18 2H6v7a6 6 0 0012 0V2z\"/>\n                </svg>\n                排行榜\n            </h1>\n            <p class=\"page-desc\">展示最优秀的编程高手</p>\n        </div>\n\n        <!-- Top 3 Podium removed as requested -->\n        \n\n\n        <!-- Rankings Table -->\n        <div class=\"table-container card-glass\">\n            <el-table \n                :data=\"rankings\" \n                class=\"rankings-table\"\n                v-loading=\"loading\"\n            >\n                <el-table-column label=\"排名\" width=\"100\" align=\"center\">\n                    <template #default=\"{ row }\">\n                        <div class=\"rank-cell\">\n                            <span v-if=\"row.rank <= 3\" class=\"rank-medal\" :class=\"`rank-${row.rank}`\">\n                                <svg viewBox=\"0 0 24 24\" fill=\"currentColor\">\n                                    <path d=\"M6 9H4.5a2.5 2.5 0 010-5H6M18 9h1.5a2.5 2.5 0 000-5H18M4 22h16M10 14.66V17c0 .55-.47.98-.97 1.21C7.85 18.75 7 20.24 7 22M14 14.66V17c0 .55.47.98.97 1.21C16.15 18.75 17 20.24 17 22M18 2H6v7a6 6 0 0012 0V2z\"/>\n                                </svg>\n                            </span>\n                            <span v-else class=\"rank-number\">{{ row.rank }}</span>\n                        </div>\n                    </template>\n                </el-table-column>\n\n                <el-table-column label=\"用户\" min-width=\"200\">\n                    <template #default=\"{ row }\">\n                        <div class=\"user-cell\">\n                            <el-avatar :size=\"40\" :src=\"row.avatar ? '/api' + row.avatar : ''\" class=\"user-avatar\">\n                                {{ row.username?.charAt(0) }}\n                            </el-avatar>\n                            <span class=\"username\">{{ row.username }}</span>\n                        </div>\n                    </template>\n                </el-table-column>\n\n                <el-table-column label=\"积分\" width=\"150\" align=\"center\" sortable>\n                    <template #default=\"{ row }\">\n                        <span class=\"score\" :class=\"getScoreClass(row.score)\">{{ row.score }}</span>\n                    </template>\n                </el-table-column>\n\n                <el-table-column label=\"常用语言\" width=\"130\" align=\"center\">\n                    <template #default=\"{ row }\">\n                        <span class=\"lang-tag\" :class=\"row.mostUsedLanguage\">\n                            {{ formatLanguage(row.mostUsedLanguage) }}\n                        </span>\n                    </template>\n                </el-table-column>\n\n                <el-table-column label=\"解题数\" width=\"200\" align=\"center\">\n                    <template #default=\"{ row }\">\n                        <div class=\"solved-cell\">\n                            <span class=\"solved-tag easy\">{{ row.easySolve }}</span>\n                            <span class=\"solved-tag medium\">{{ row.middleSolve }}</span>\n                            <span class=\"solved-tag hard\">{{ row.hardSolve }}</span>\n                        </div>\n                    </template>\n                </el-table-column>\n            </el-table>\n\n            <div class=\"pagination-container\">\n                <el-pagination \n                    background \n                    layout=\"prev, pager, next\" \n                    :total=\"total\" \n                    :page-size=\"pageSize\"\n                    @current-change=\"handlePageChange\" \n                />\n            </div>\n        </div>\n    </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { ref, onMounted } from 'vue';\nimport { ElTable, ElTableColumn, ElPagination, ElAvatar } from 'element-plus';\nimport { reqRankings } from '@/api/user';\n\ninterface RankItem {\n    rank: number;\n    userId: number;\n    username: string;\n    avatar: string;\n    score: number;\n    mostUsedLanguage: 'java' | 'python' | 'cpp';\n    easySolve: number;\n    middleSolve: number;\n    hardSolve: number;\n}\n\nconst loading = ref(true);\nconst rankings = ref<RankItem[]>([]);\nconst total = ref(0);\nconst currentPage = ref(1);\nconst pageSize = ref(20);\n\nconst fetchRankings = async (page: number) => {\n    loading.value = true;\n    const res = await reqRankings({ pageNo: page, pageSize: pageSize.value });\n    if (res.data.code === 200) {\n        rankings.value = res.data.data.list;\n        total.value = res.data.data.total;\n    }\n    loading.value = false;\n};\n\nconst getScoreClass = (score: number) => {\n    if (score >= 1700) return 'legendary';\n    if (score >= 1500) return 'epic';\n    if (score >= 1300) return 'rare';\n    if (score >= 1100) return 'good';\n    return 'normal';\n};\n\nconst formatLanguage = (lang: string) => {\n    const map: Record<string, string> = { 'cpp': 'C++', 'java': 'Java', 'python': 'Python' };\n    return map[lang] || lang?.toUpperCase();\n};\n\nconst handlePageChange = (page: number) => {\n    currentPage.value = page;\n    fetchRankings(page);\n};\n\nonMounted(() => fetchRankings(currentPage.value));\n</script>\n\n<style lang=\"scss\" scoped>\n\n\n.rankings-page {\n    display: flex;\n    flex-direction: column;\n    gap: $space-lg;\n}\n\n.page-header {\n    .page-title {\n        display: flex;\n        align-items: center;\n        gap: $space-sm;\n        font-size: $font-size-2xl;\n        font-weight: $font-weight-bold;\n        margin-bottom: $space-xs;\n        \n        svg {\n            width: 28px;\n            height: 28px;\n            color: #ffd700;\n        }\n    }\n    \n    .page-desc {\n        color: var(--text-secondary);\n        font-size: $font-size-sm;\n    }\n}\n\n// Podium\n.podium-section {\n    display: flex;\n    justify-content: center;\n    align-items: flex-end;\n    gap: $space-lg;\n    padding: $space-xl 0;\n}\n\n.podium-card {\n    display: flex;\n    flex-direction: column;\n    align-items: center;\n    padding: $space-lg;\n    background: var(--bg-card);\n    border: 1px solid var(--border-color);\n    border-radius: $radius-lg;\n    position: relative;\n    transition: all $transition-normal;\n    \n    &:hover {\n        transform: translateY(-5px);\n        box-shadow: var(--shadow-lg);\n    }\n    \n    .rank-badge {\n        position: absolute;\n        top: -12px;\n        width: 24px;\n        height: 24px;\n        display: flex;\n        align-items: center;\n        justify-content: center;\n        border-radius: $radius-full;\n        font-size: $font-size-sm;\n        font-weight: $font-weight-bold;\n        color: white;\n    }\n    \n    &.first {\n        padding-bottom: $space-2xl;\n        .rank-badge { background: #ffd700; }\n        .crown {\n            position: absolute;\n            top: -35px;\n            color: #ffd700;\n            svg { width: 32px; height: 32px; }\n        }\n    }\n    &.second .rank-badge { background: #c0c0c0; }\n    &.third .rank-badge { background: #cd7f32; }\n    \n    .podium-avatar {\n        margin-bottom: $space-sm;\n        border: 3px solid var(--border-light);\n        background: var(--primary-gradient);\n        color: white;\n        font-weight: $font-weight-bold;\n    }\n    \n    .podium-name {\n        font-weight: $font-weight-semibold;\n        color: var(--text-primary);\n        margin-bottom: $space-xs;\n    }\n    \n    .podium-score {\n        font-size: $font-size-sm;\n        color: var(--text-muted);\n    }\n}\n\n// Table\n.table-container { padding: 0; overflow: hidden; }\n\n.rank-cell {\n    display: flex;\n    justify-content: center;\n    \n    .rank-medal {\n        svg { width: 24px; height: 24px; }\n        &.rank-1 { color: #ffd700; }\n        &.rank-2 { color: #c0c0c0; }\n        &.rank-3 { color: #cd7f32; }\n    }\n    \n    .rank-number {\n        font-weight: $font-weight-bold;\n        color: var(--text-secondary);\n    }\n}\n\n.user-cell {\n    display: flex;\n    align-items: center;\n    gap: $space-md;\n    \n    .user-avatar {\n        background: var(--primary-gradient);\n        color: white;\n        font-weight: $font-weight-semibold;\n    }\n    \n    .username { font-weight: $font-weight-medium; }\n}\n\n.score {\n    font-weight: $font-weight-bold;\n    &.legendary { color: #a855f7; }\n    &.epic { color: #f97316; }\n    &.rare { color: #3b82f6; }\n    &.good { color: #22c55e; }\n    &.normal { color: var(--text-muted); }\n}\n\n.lang-tag {\n    padding: $space-xs $space-sm;\n    border-radius: $radius-sm;\n    font-size: $font-size-xs;\n    font-weight: $font-weight-medium;\n    &.cpp { background: rgba(#00599c, 0.15); color: #00599c; }\n    &.java { background: rgba(#f8981d, 0.15); color: #f8981d; }\n    &.python { background: rgba(#3776ab, 0.15); color: #3776ab; }\n}\n\n.solved-cell {\n    display: flex;\n    justify-content: center;\n    gap: $space-sm;\n}\n\n.solved-tag {\n    padding: $space-xs $space-sm;\n    border-radius: $radius-sm;\n    font-size: $font-size-xs;\n    font-weight: $font-weight-medium;\n    &.easy { background: rgba($success, 0.15); color: $success; }\n    &.medium { background: rgba($warning, 0.15); color: $warning; }\n    &.hard { background: rgba($danger, 0.15); color: $danger; }\n}\n\n.pagination-container {\n    padding: $space-lg;\n    display: flex;\n    justify-content: center;\n    border-top: 1px solid var(--border-color);\n}\n</style>"
  },
  {
    "path": "DOJ-FE/src/views/Status/index.vue",
    "content": "<template>\n    <div class=\"status-page\">\n        <!-- Page Header -->\n        <div class=\"page-header\">\n            <h1 class=\"page-title\">\n                <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n                    <polyline points=\"22 12 18 12 15 21 9 3 6 12 2 12\"/>\n                </svg>\n                提交状态\n            </h1>\n            <p class=\"page-desc\">查看所有代码提交记录</p>\n        </div>\n\n        <!-- Filters -->\n        <div class=\"filters-section card-glass\">\n            <div class=\"filter-group\">\n                <el-input v-model=\"searchProblemName\" placeholder=\"题目名称\" clearable class=\"filter-input\" :prefix-icon=\"Search\" />\n                <el-input v-model=\"searchUserId\" placeholder=\"用户名称\" clearable class=\"filter-input\" />\n                <el-select v-model=\"searchLanguage\" placeholder=\"语言\" clearable class=\"filter-select\">\n                    <el-option label=\"C++\" value=\"cpp\" />\n                    <el-option label=\"Java\" value=\"java\" />\n                    <el-option label=\"Python\" value=\"python\" />\n                </el-select>\n                <el-select v-model=\"searchStatus\" placeholder=\"评测状态\" clearable class=\"filter-select status-select\">\n                    <el-option label=\"Accepted\" value=\"Accepted\">\n                        <span class=\"status-option accepted\">✓ Accepted</span>\n                    </el-option>\n                    <el-option label=\"Wrong Answer\" value=\"Wrong Answer\">\n                        <span class=\"status-option wrong\">✗ Wrong Answer</span>\n                    </el-option>\n                    <el-option label=\"Runtime Error\" value=\"Runtime Error\">\n                        <span class=\"status-option error\">Runtime Error</span>\n                    </el-option>\n                    <el-option label=\"Compile Error\" value=\"Compile Error\">\n                        <span class=\"status-option error\">Compile Error</span>\n                    </el-option>\n                    <el-option label=\"Time Limit Exceeded\" value=\"Time Limit Exceeded\">\n                        <span class=\"status-option tle\">Time Limit Exceeded</span>\n                    </el-option>\n                    <el-option label=\"Memory Limit Exceeded\" value=\"Memory Limit Exceeded\">\n                        <span class=\"status-option tle\">Memory Limit Exceeded</span>\n                    </el-option>\n                </el-select>\n            </div>\n        </div>\n\n        <!-- Status Table -->\n        <div class=\"table-container card-glass\">\n            <el-table \n                :data=\"tableData\" \n                :default-sort=\"{ prop: 'id', order: 'descending' }\"\n                class=\"status-table\"\n                v-loading=\"loading\"\n            >\n                <el-table-column type=\"expand\" width=\"30\">\n                    <template v-slot=\"props\">\n                        <div class=\"code-expand\">\n                            <Editor :config=\"{\n                                tabSize: 4,\n                                disabled: true,\n                                height: '400px',\n                                width: '100%',\n                                language: props.row.language,\n                                theme: 'default',\n                                fontSize: 14,\n                                editorType: 'show',\n                                code: props.row.code\n                            }\" />\n                        </div>\n                    </template>\n                </el-table-column>\n\n                <el-table-column prop=\"submitTime\" label=\"提交时间\" sortable align=\"center\" width=\"160\">\n                    <template v-slot=\"scope\">\n                        <span class=\"time-cell\">{{ scope.row.submitTime }}</span>\n                    </template>\n                </el-table-column>\n\n                <el-table-column prop=\"id\" label=\"ID\" sortable align=\"center\" width=\"80\">\n                    <template v-slot=\"scope\">\n                        <span class=\"id-cell\">#{{ scope.row.id }}</span>\n                    </template>\n                </el-table-column>\n\n                <el-table-column prop=\"problemName\" label=\"题目\" min-width=\"150\">\n                    <template v-slot=\"scope\">\n                        <span class=\"problem-link\" @click=\"toProblem(scope.row.problemId)\">\n                            {{ scope.row.problemName }}\n                        </span>\n                    </template>\n                </el-table-column>\n\n                <el-table-column prop=\"userName\" label=\"用户\" align=\"center\" width=\"120\">\n                    <template v-slot=\"scope\">\n                        <span class=\"user-cell\">{{ scope.row.userName }}</span>\n                    </template>\n                </el-table-column>\n\n                <el-table-column prop=\"language\" label=\"语言\" align=\"center\" width=\"100\">\n                    <template v-slot=\"scope\">\n                        <span class=\"lang-tag\" :class=\"scope.row.language\">\n                            {{ formatLanguage(scope.row.language) }}\n                        </span>\n                    </template>\n                </el-table-column>\n\n                <el-table-column prop=\"status\" label=\"状态\" align=\"center\" min-width=\"140\">\n                    <template v-slot=\"scope\">\n                        <span class=\"status-tag\" :class=\"getStatusClass(scope.row.status)\">\n                            {{ scope.row.status }}\n                        </span>\n                    </template>\n                </el-table-column>\n\n                <el-table-column prop=\"time\" label=\"耗时\" align=\"center\" width=\"100\">\n                    <template v-slot=\"scope\">\n                        <span class=\"metric-cell\">{{ scope.row.time }}ms</span>\n                    </template>\n                </el-table-column>\n\n                <el-table-column prop=\"memory\" label=\"内存\" align=\"center\" width=\"100\">\n                    <template v-slot=\"scope\">\n                        <span class=\"metric-cell\">{{ scope.row.memory }}KB</span>\n                    </template>\n                </el-table-column>\n            </el-table>\n\n            <!-- Pagination -->\n            <div class=\"pagination-container\">\n                <el-pagination \n                    background \n                    layout=\"prev, pager, next, sizes, total\" \n                    :total=\"total\"\n                    :page-count=\"pages\" \n                    :page-sizes=\"pageSizes\" \n                    v-model:page-size=\"pageQueryForm.pageSize\"\n                    v-model:current-page=\"pageQueryForm.pageNo\" \n                    @change=\"getStatusList\" \n                />\n            </div>\n        </div>\n    </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { ref, onMounted, watch } from 'vue';\nimport { useRoute, useRouter } from 'vue-router';\nimport { ElTable, ElTableColumn, ElSelect, ElOption, ElInput, ElPagination } from 'element-plus';\nimport { Search } from '@element-plus/icons-vue';\nimport { reqSubmissionPageList } from '@/api/submission';\nimport { reqUserInfo } from '@/api/user';\nimport { reqProblemDetail } from '@/api/problem';\nimport type { BasePageQueryForm } from '@/api/base';\nimport { Submission } from '@/api/submission/type';\nimport Editor from '@/components/CodeEditor/index.vue';\n\ninterface StatusItem {\n    id: number;\n    submitTime: string;\n    problemId: number;\n    problemName: string;\n    userId: number;\n    userName: string;\n    language: string;\n    code: string;\n    status?: string;\n    time?: number;\n    memory?: number;\n}\n\nconst router = useRouter();\nconst route = useRoute();\n\nconst searchProblemName = ref();\nconst searchUserId = ref();\nconst searchLanguage = ref();\nconst searchStatus = ref();\nconst loading = ref(false);\n\nwatch([searchProblemName, searchUserId, searchLanguage, searchStatus], () => {\n    getStatusList();\n});\n\nconst tableData = ref<StatusItem[]>([]);\nconst pageQueryForm = ref<BasePageQueryForm>({\n    pageNo: 1,\n    pageSize: 20,\n    isAsc: false,\n    sortBy: ''\n});\nconst total = ref(0);\nconst pages = ref(0);\nconst pageSizes = ref([10, 20, 50, 100]);\n\nconst toProblem = (id: number) => {\n    router.push(\"/problem/\" + id);\n};\n\nconst formatLanguage = (lang: string) => {\n    const map: Record<string, string> = {\n        'cpp': 'C++',\n        'java': 'Java',\n        'python': 'Python'\n    };\n    return map[lang] || lang;\n};\n\nconst getStatusClass = (status: string) => {\n    if (status === 'Accepted') return 'accepted';\n    if (status === 'Wrong Answer') return 'wrong';\n    if (status?.includes('Error')) return 'error';\n    if (status?.includes('Exceeded')) return 'tle';\n    return 'pending';\n};\n\nconst getStatusList = async () => {\n    loading.value = true;\n    try {\n        const params = {\n            ...pageQueryForm.value,\n            problemId: searchProblemName.value,\n            userId: searchUserId.value,\n            language: searchLanguage.value,\n            status: searchStatus.value\n        };\n        const res = await reqSubmissionPageList(params);\n        const { total: totalV, pages: pagesV, list } = res.data.data;\n        total.value = totalV;\n        pages.value = pagesV;\n        tableData.value = await Promise.all(list.map(async (item: Submission) => {\n            const problemRes = await reqProblemDetail(String(item.problemId));\n            const problemName = problemRes.data?.data?.name || String(item.problemId);\n            const userInfo = await reqUserInfo(item.userId);\n            const userName = userInfo.data?.data?.username || '未知用户';\n            const timedate = new Date(item.submitTime).toLocaleString();\n            return {\n                id: item.id,\n                submitTime: timedate,\n                problemName,\n                problemId: item.problemId,\n                userId: item.userId,\n                userName,\n                language: item.language,\n                code: item.code,\n                status: item.status,\n                time: item.time,\n                memory: item.memory,\n            };\n        }));\n    } finally {\n        loading.value = false;\n    }\n};\n\nonMounted(() => {\n    const { problemName, userName } = route.query;\n    if (problemName) searchProblemName.value = problemName as string;\n    if (userName) searchUserId.value = userName as string;\n    getStatusList();\n});\n</script>\n\n<style scoped lang=\"scss\">\n\n\n.status-page {\n    display: flex;\n    flex-direction: column;\n    gap: $space-lg;\n}\n\n.page-header {\n    .page-title {\n        display: flex;\n        align-items: center;\n        gap: $space-sm;\n        font-size: $font-size-2xl;\n        font-weight: $font-weight-bold;\n        margin-bottom: $space-xs;\n        \n        svg {\n            width: 28px;\n            height: 28px;\n            color: var(--primary-start);\n        }\n    }\n    \n    .page-desc {\n        color: var(--text-secondary);\n        font-size: $font-size-sm;\n    }\n}\n\n.filters-section {\n    padding: $space-md $space-lg;\n}\n\n.filter-group {\n    display: flex;\n    gap: $space-md;\n    flex-wrap: wrap;\n    \n    .filter-input { width: 180px; }\n    .filter-select { width: 140px; }\n    .status-select { width: 180px; }\n}\n\n.status-option {\n    &.accepted { color: $success; }\n    &.wrong, &.error { color: $danger; }\n    &.tle { color: $warning; }\n}\n\n.table-container {\n    padding: 0;\n    overflow: hidden;\n}\n\n.code-expand {\n    padding: $space-md;\n    background: var(--bg-primary);\n    border-radius: $radius-md;\n    margin: $space-sm;\n    border: 1px solid var(--border-color);\n}\n\n.time-cell {\n    font-size: $font-size-xs;\n    color: var(--text-muted);\n}\n\n.id-cell {\n    font-size: $font-size-sm;\n    color: var(--text-secondary);\n    font-family: $font-mono;\n}\n\n.problem-link {\n    color: var(--text-primary);\n    font-weight: $font-weight-medium;\n    cursor: pointer;\n    transition: color $transition-fast;\n    \n    &:hover {\n        color: var(--primary-start);\n    }\n}\n\n.user-cell {\n    color: var(--text-secondary);\n    font-size: $font-size-sm;\n}\n\n.lang-tag {\n    padding: $space-xs $space-sm;\n    border-radius: $radius-sm;\n    font-size: $font-size-xs;\n    font-weight: $font-weight-medium;\n    \n    &.cpp {\n        background: rgba(#00599c, 0.15);\n        color: #00599c;\n    }\n    &.java {\n        background: rgba(#f8981d, 0.15);\n        color: #f8981d;\n    }\n    &.python {\n        background: rgba(#3776ab, 0.15);\n        color: #3776ab;\n    }\n}\n\n.status-tag {\n    padding: $space-xs $space-sm;\n    border-radius: $radius-sm;\n    font-size: $font-size-xs;\n    font-weight: $font-weight-semibold;\n    \n    &.accepted {\n        background: $success-light;\n        color: $success;\n    }\n    &.wrong, &.error {\n        background: $danger-light;\n        color: $danger;\n    }\n    &.tle {\n        background: $warning-light;\n        color: $warning;\n    }\n    &.pending {\n        background: var(--glass-bg);\n        color: var(--text-muted);\n    }\n}\n\n.metric-cell {\n    font-size: $font-size-xs;\n    color: var(--text-muted);\n    font-family: $font-mono;\n}\n\n.pagination-container {\n    padding: $space-lg;\n    display: flex;\n    justify-content: center;\n    border-top: 1px solid var(--border-color);\n}\n</style>"
  },
  {
    "path": "DOJ-FE/src/views/User/home/index.vue",
    "content": "<template>\n    <div class=\"profile-page\">\n        <!-- Profile Header -->\n        <div class=\"profile-header card-glass\">\n            <div class=\"user-intro\">\n                <el-avatar :size=\"100\" :src=\"form.avatar ? '/api' + form.avatar : ''\" class=\"profile-avatar\">\n                    {{ form.username?.charAt(0).toUpperCase() }}\n                </el-avatar>\n                <div class=\"user-details\">\n                    <div class=\"name-row\">\n                        <h1 class=\"username\">{{ form.username }}</h1>\n                        <span class=\"gender-icon\" v-if=\"form.gender !== undefined\">\n                            <svg v-if=\"form.gender\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"#3b82f6\" stroke-width=\"2\">\n                                <circle cx=\"10.5\" cy=\"8.5\" r=\"5.5\"/>\n                                <path d=\"M16 3l5 0\"/>\n                                <path d=\"M21 3l0 5\"/>\n                                <path d=\"M14.5 9.5l6.5-6.5\"/>\n                            </svg>\n                            <svg v-else viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"#ec4899\" stroke-width=\"2\">\n                                <circle cx=\"12\" cy=\"8\" r=\"5\"/>\n                                <path d=\"M12 13v8\"/>\n                                <path d=\"M9 18h6\"/>\n                            </svg>\n                        </span>\n                        <el-tag size=\"small\" effect=\"dark\" class=\"role-tag\" v-if=\"form.role\">{{ form.role }}</el-tag>\n                    </div>\n                    <p class=\"signature\">{{ form.sign || '这个人很懒，什么都没写~' }}</p>\n                    <div class=\"meta-row\">\n                        <div class=\"meta-item\" v-if=\"form.school\">\n                            <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n                                <path d=\"M22 10v6M2 10l10-5 10 5-10 5z\"/>\n                                <path d=\"M6 12v5c0 1 2 3 6 3s6-2 6-3v-5\"/>\n                            </svg>\n                            {{ form.school }}\n                        </div>\n                        <div class=\"meta-item\" v-if=\"form.email\">\n                            <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n                                <path d=\"M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z\"/>\n                                <polyline points=\"22,6 12,13 2,6\"/>\n                            </svg>\n                            {{ form.email }}\n                        </div>\n                        <div class=\"meta-item link\" v-if=\"form.url\">\n                            <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n                                <path d=\"M10 13a5 5 0 007.54.54l3-3a5 5 0 00-7.07-7.07l-1.72 1.71\"/>\n                                <path d=\"M14 11a5 5 0 00-7.54-.54l-3 3a5 5 0 007.07 7.07l1.71-1.71\"/>\n                            </svg>\n                            <a :href=\"form.url\" target=\"_blank\">{{ form.url }}</a>\n                        </div>\n                    </div>\n                </div>\n            </div>\n            <div class=\"stats-row\">\n                <div class=\"stat-box\">\n                    <span class=\"value\">{{ form.easySolve + form.middleSolve + form.hardSolve }}</span>\n                    <span class=\"label\">解决</span>\n                </div>\n                <div class=\"divider\"></div>\n                <div class=\"stat-box\">\n                    <span class=\"value\">{{ form.score }}</span>\n                    <span class=\"label\">积分</span>\n                </div>\n                <div class=\"divider\"></div>\n                <div class=\"stat-box\">\n                    <span class=\"value\">{{ form.ranks > 0 ? form.ranks : '-' }}</span>\n                    <span class=\"label\">排名</span>\n                </div>\n                <!-- <div class=\"divider\"></div>\n                <div class=\"stat-box\">\n                    <span class=\"value\">{{ form.fans }}</span>\n                    <span class=\"label\">粉丝</span>\n                </div> -->\n            </div>\n        </div>\n\n        <div class=\"content-grid\">\n            <!-- Left Column: Charts & StatsDetail -->\n            <div class=\"grid-col left\">\n                <div class=\"chart-card card-glass\">\n                    <h3 class=\"card-title\">数据统计</h3>\n                    <div ref=\"chartRef\" class=\"chart-container\"></div>\n                </div>\n                \n                <div class=\"solved-card card-glass\">\n                    <h3 class=\"card-title\">解题分布</h3>\n                    <div class=\"distribution-bars\">\n                        <div class=\"dist-item\">\n                            <div class=\"dist-label\">简单</div>\n                            <div class=\"dist-bar-bg\">\n                                <div class=\"dist-bar easy\" :style=\"{ width: getPercentage(form.easySolve) }\"></div>\n                            </div>\n                            <div class=\"dist-val\">{{ form.easySolve }}</div>\n                        </div>\n                        <div class=\"dist-item\">\n                            <div class=\"dist-label\">中等</div>\n                            <div class=\"dist-bar-bg\">\n                                <div class=\"dist-bar medium\" :style=\"{ width: getPercentage(form.middleSolve) }\"></div>\n                            </div>\n                            <div class=\"dist-val\">{{ form.middleSolve }}</div>\n                        </div>\n                        <div class=\"dist-item\">\n                            <div class=\"dist-label\">困难</div>\n                            <div class=\"dist-bar-bg\">\n                                <div class=\"dist-bar hard\" :style=\"{ width: getPercentage(form.hardSolve) }\"></div>\n                            </div>\n                            <div class=\"dist-val\">{{ form.hardSolve }}</div>\n                        </div>\n                    </div>\n                </div>\n            </div>\n\n            <!-- Right Column: Recent Activity & Solved Problems -->\n            <div class=\"grid-col right\">\n                <!-- Recent Activity -->\n                <div class=\"activity-card card-glass\">\n                    <h3 class=\"card-title\">\n                        最近提交\n                        <span class=\"view-all\" @click=\"router.push(`/status?userName=${form.username}`)\">查看全部</span>\n                    </h3>\n                    <div class=\"activity-list\" v-loading=\"loadingActivity\">\n                        <div v-for=\"item in recentSubmissions\" :key=\"item.id\" class=\"activity-item\">\n                            <div class=\"activity-status\" :class=\"getStatusClass(item.status)\">\n                                <svg v-if=\"item.status === 'Accepted'\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"><polyline points=\"20 6 9 17 4 12\"/></svg>\n                                <svg v-else viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"><line x1=\"18\" y1=\"6\" x2=\"6\" y2=\"18\"/><line x1=\"6\" y1=\"6\" x2=\"18\" y2=\"18\"/></svg>\n                            </div>\n                            <div class=\"activity-info\">\n                                <div class=\"activity-main\">\n                                    <span class=\"prob-name\" @click=\"router.push(`/problem/${item.problemId}`)\">{{ item.problemName }}</span>\n                                    <span class=\"time-ago\">{{ item.submitTime }}</span>\n                                </div>\n                                <div class=\"activity-meta\">\n                                    <span class=\"lang-badge\">{{ item.language }}</span>\n                                    <span class=\"status-text\" :class=\"getStatusClass(item.status)\">{{ item.status }}</span>\n                                </div>\n                            </div>\n                        </div>\n                        <div class=\"empty-tip\" v-if=\"!recentSubmissions.length\">暂无提交记录</div>\n                    </div>\n                </div>\n\n                <!-- Solved Problems -->\n                <div class=\"solved-list-card card-glass\">\n                    <h3 class=\"card-title\">已解决题目</h3>\n                    <div class=\"solved-tags\" v-loading=\"loadingSolved\">\n                        <div \n                            v-for=\"pid in solvedProblems\" \n                            :key=\"pid\" \n                            class=\"solved-tag\" \n                            @click=\"router.push(`/problem/${pid}`)\"\n                        >\n                            {{ pid }}\n                        </div>\n                        <div class=\"empty-tip\" v-if=\"!solvedProblems.length\">暂无数据</div>\n                    </div>\n                </div>\n            </div>\n        </div>\n    </div>\n</template>\n\n<script lang=\"ts\" setup>\nimport { ref, onMounted, nextTick } from 'vue';\nimport { useRouter } from 'vue-router';\nimport { ElAvatar, ElTag } from 'element-plus';\nimport { reqUserInfo } from '@/api/user';\nimport { reqSubmissionPageList } from '@/api/submission';\nimport { reqProblemDetail } from '@/api/problem';\nimport { useUserStore } from '@/stores/userStore';\nimport * as echarts from \"echarts\";\n\nconst router = useRouter();\nconst userStore = useUserStore();\nconst chartRef = ref();\n\nconst form = ref({\n    id: 0,\n    username: '',\n    avatar: '',\n    email: '',\n    score: 0,\n    ranks: 0,\n    school: '',\n    gender: true,\n    easySolve: 0,\n    middleSolve: 0,\n    hardSolve: 0,\n    url: '',\n    sign: '',\n    fans: 0,\n    subscribe: 0,\n    role: '',\n});\n\nconst recentSubmissions = ref<any[]>([]);\nconst solvedProblems = ref<number[]>([]);\nconst loadingActivity = ref(false);\nconst loadingSolved = ref(false);\n\nconst getStatusClass = (status: string) => {\n    if (status === 'Accepted') return 'accepted';\n    if (status === 'Wrong Answer') return 'wrong';\n    if (status?.includes('Error')) return 'error';\n    if (status?.includes('Exceeded')) return 'tle';\n    return 'pending';\n};\n\nconst getPercentage = (val: number) => {\n    const total = (form.value.easySolve + form.value.middleSolve + form.value.hardSolve) || 1;\n    return Math.min(100, Math.max(5, (val / total) * 100)) + '%';\n}\n\nconst generateChart = () => {\n    if (!chartRef.value) return;\n    const chart = echarts.init(chartRef.value);\n    const data = [\n        { name: '简单', value: form.value.easySolve },\n        { name: '中等', value: form.value.middleSolve },\n        { name: '困难', value: form.value.hardSolve },\n    ].filter(item => item.value > 0);\n\n    if (data.length === 0) {\n        data.push({ name: '暂无数据', value: 1 });\n    }\n\n    const option = {\n        tooltip: {\n            trigger: 'item',\n            backgroundColor: 'rgba(15, 15, 35, 0.9)',\n            borderColor: 'rgba(255,255,255,0.1)',\n            textStyle: { color: '#fff' }\n        },\n        legend: {\n            bottom: '0%',\n            left: 'center',\n            textStyle: { color: 'var(--text-secondary)' } // Dynamic color handled by CSS vars usually, but echarts needs direct value. \n            // In dark mode this might need adjustment, but text-secondary is usually ok.\n        },\n        series: [{\n            type: 'pie',\n            radius: ['40%', '70%'],\n            center: ['50%', '45%'],\n            avoidLabelOverlap: false,\n            itemStyle: { \n                borderRadius: 6, \n                borderColor: 'var(--bg-card)', \n                borderWidth: 2 \n            },\n            label: { show: false },\n            data: data,\n            color: data[0].name === '暂无数据' ? ['#333'] : ['#10b981', '#f59e0b', '#ef4444']\n        }]\n    };\n    chart.setOption(option);\n    \n    // Resize chart on window resize\n    window.addEventListener('resize', () => {\n        chart.resize();\n    });\n};\n\nconst fetchRecentActivity = async (username: string) => {\n    loadingActivity.value = true;\n    try {\n        const res = await reqSubmissionPageList({\n            pageNo: 1,\n            pageSize: 5,\n            userId: username,\n            isAsc: false,\n            sortBy: 'id'\n        });\n        const list = res.data.data.list;\n        \n        recentSubmissions.value = await Promise.all(list.map(async (item: any) => {\n            // Optimistically try to get problem name, though api might return it immediately if joined\n            let problemName = item.problemName || String(item.problemId);\n            if (!item.problemName) {\n                 const pRes = await reqProblemDetail(String(item.problemId));\n                 problemName = pRes.data?.data?.name || problemName;\n            }\n            return {\n                id: item.id,\n                problemId: item.problemId,\n                problemName: problemName,\n                status: item.status,\n                language: item.language,\n                submitTime: new Date(item.submitTime).toLocaleDateString()\n            };\n        }));\n    } finally {\n        loadingActivity.value = false;\n    }\n};\n\nconst fetchSolvedProblems = async (username: string) => {\n    loadingSolved.value = true;\n    try {\n        const res = await reqSubmissionPageList({\n            pageNo: 1,\n            pageSize: 50,\n            userId: username,\n            status: 'Accepted',\n            isAsc: false,\n            sortBy: 'id'\n        });\n        const list = res.data.data.list;\n        const ids = new Set(list.map((item: any) => item.problemId));\n        solvedProblems.value = Array.from(ids).sort((a: any, b: any) => a - b);\n    } finally {\n        loadingSolved.value = false;\n    }\n}\n\nonMounted(async () => {\n    const uid = userStore.userInfo!.userId;\n    const res = await reqUserInfo(uid);\n    form.value = res.data.data;\n    \n    fetchRecentActivity(form.value.username);\n    fetchSolvedProblems(form.value.username);\n    \n    nextTick(() => generateChart());\n});\n</script>\n\n<style scoped lang=\"scss\">\n\n\n.profile-page {\n    display: flex;\n    flex-direction: column;\n    gap: $space-lg;\n    max-width: 1200px;\n    margin: 0 auto;\n    width: 100%;\n}\n\n// Profile Header\n.profile-header {\n    display: flex;\n    align-items: center;\n    justify-content: space-between;\n    padding: $space-xl;\n    gap: $space-xl;\n    \n    @include mobile {\n        flex-direction: column;\n        align-items: flex-start;\n    }\n}\n\n.user-intro {\n    display: flex;\n    align-items: center;\n    gap: $space-lg;\n    \n    .profile-avatar {\n        background: var(--primary-gradient);\n        font-size: 40px;\n        font-weight: bold;\n        color: white;\n        border: 4px solid var(--bg-elevated);\n        box-shadow: var(--shadow-md);\n    }\n    \n    @include mobile {\n        flex-direction: column;\n        align-items: flex-start;\n        \n        .profile-avatar {\n            margin-bottom: $space-md;\n        }\n    }\n}\n\n.user-details {\n    .name-row {\n        display: flex;\n        align-items: center;\n        gap: $space-sm;\n        margin-bottom: $space-xs;\n        \n        .username {\n            font-size: $font-size-3xl;\n            font-weight: 700;\n            color: var(--text-primary);\n            line-height: 1.2;\n        }\n        \n        .gender-icon svg {\n            width: 20px;\n            height: 20px;\n        }\n    }\n    \n    .signature {\n        color: var(--text-secondary);\n        margin-bottom: $space-md;\n        font-size: $font-size-sm;\n    }\n    \n    .meta-row {\n        display: flex;\n        flex-wrap: wrap;\n        gap: $space-lg;\n        \n        .meta-item {\n            display: flex;\n            align-items: center;\n            gap: $space-xs;\n            color: var(--text-muted);\n            font-size: $font-size-sm;\n            \n            svg { width: 16px; height: 16px; }\n            \n            &.link a {\n                color: var(--primary-start);\n                &:hover { text-decoration: underline; }\n            }\n        }\n    }\n}\n\n.stats-row {\n    display: flex;\n    align-items: center;\n    gap: $space-xl;\n    \n    .divider {\n        width: 1px;\n        height: 40px;\n        background: var(--border-color);\n    }\n    \n    .stat-box {\n        display: flex;\n        flex-direction: column;\n        align-items: center;\n        min-width: 60px;\n        \n        .value {\n            font-size: $font-size-2xl;\n            font-weight: 800;\n            color: var(--text-primary);\n        }\n        \n        .label {\n            font-size: $font-size-xs;\n            color: var(--text-muted);\n            text-transform: uppercase;\n            letter-spacing: 0.5px;\n        }\n    }\n    \n    @include mobile {\n        width: 100%;\n        justify-content: space-around;\n        padding-top: $space-md;\n        border-top: 1px solid var(--border-color);\n    }\n}\n\n// Content Grid\n.content-grid {\n    display: grid;\n    grid-template-columns: 350px 1fr;\n    gap: $space-lg;\n    \n    @include mobile {\n        grid-template-columns: 1fr;\n    }\n}\n\n.grid-col {\n    display: flex;\n    flex-direction: column;\n    gap: $space-lg;\n}\n\n.card-title {\n    font-size: $font-size-lg;\n    font-weight: 600;\n    margin-bottom: $space-lg;\n    padding-bottom: $space-sm;\n    border-bottom: 1px solid var(--border-color);\n    display: flex;\n    justify-content: space-between;\n    align-items: center;\n    \n    .view-all {\n        font-size: $font-size-xs;\n        color: var(--primary-start);\n        cursor: pointer;\n        font-weight: normal;\n        \n        &:hover { text-decoration: underline; }\n    }\n}\n\n.chart-card, .solved-card, .activity-card, .solved-list-card {\n    padding: $space-lg;\n}\n\n.chart-container {\n    height: 250px;\n}\n\n// Distribution Bars\n.distribution-bars {\n    display: flex;\n    flex-direction: column;\n    gap: $space-md;\n    \n    .dist-item {\n        display: flex;\n        align-items: center;\n        gap: $space-md;\n        font-size: $font-size-sm;\n        \n        .dist-label {\n            width: 40px;\n            color: var(--text-secondary);\n        }\n        \n        .dist-bar-bg {\n            flex: 1;\n            height: 8px;\n            background: var(--bg-primary);\n            border-radius: $radius-full;\n            overflow: hidden;\n            \n            .dist-bar {\n                height: 100%;\n                border-radius: $radius-full;\n                \n                &.easy { background: $success; }\n                &.medium { background: $warning; }\n                &.hard { background: $danger; }\n            }\n        }\n        \n        .dist-val {\n            width: 30px;\n            text-align: right;\n            font-weight: 600;\n        }\n    }\n}\n\n// Activity List\n.activity-list {\n    display: flex;\n    flex-direction: column;\n    gap: $space-md;\n}\n\n.activity-item {\n    display: flex;\n    align-items: flex-start;\n    gap: $space-md;\n    padding-bottom: $space-md;\n    border-bottom: 1px solid var(--border-color);\n    \n    &:last-child {\n        border-bottom: none;\n        padding-bottom: 0;\n    }\n    \n    .activity-status {\n        width: 36px;\n        height: 36px;\n        border-radius: 50%;\n        display: flex;\n        align-items: center;\n        justify-content: center;\n        flex-shrink: 0;\n        \n        svg { width: 18px; height: 18px; }\n        \n        &.accepted { background: $success-light; color: $success; }\n        &.wrong { background: $danger-light; color: $danger; }\n        &.error { background: $danger-light; color: $danger; }\n        &.tle { background: $warning-light; color: $warning; }\n        &.pending { background: var(--bg-primary); color: var(--text-muted); }\n    }\n    \n    .activity-info {\n        flex: 1;\n        \n        .activity-main {\n            display: flex;\n            justify-content: space-between;\n            margin-bottom: $space-xs;\n            \n            .prob-name {\n                font-weight: 600;\n                color: var(--text-primary);\n                cursor: pointer;\n                &:hover { color: var(--primary-start); }\n            }\n            \n            .time-ago {\n                font-size: $font-size-xs;\n                color: var(--text-muted);\n            }\n        }\n        \n        .activity-meta {\n            display: flex;\n            gap: $space-md;\n            font-size: $font-size-xs;\n            \n            .lang-badge {\n                background: var(--bg-primary);\n                padding: 2px 6px;\n                border-radius: 4px;\n                color: var(--text-secondary);\n            }\n            \n            .status-text {\n                &.accepted { color: $success; }\n                &.wrong { color: $danger; }\n            }\n        }\n    }\n}\n\n// Solved Tags\n.solved-tags {\n    display: flex;\n    flex-wrap: wrap;\n    gap: $space-sm;\n    \n    .solved-tag {\n        padding: 4px 10px;\n        background: var(--bg-primary);\n        border: 1px solid var(--border-color);\n        border-radius: $radius-sm;\n        font-size: $font-size-sm;\n        color: var(--text-primary);\n        cursor: pointer;\n        transition: all $transition-fast;\n        font-family: $font-mono;\n        \n        &:hover {\n            border-color: $success;\n            color: $success;\n            background: $success-light;\n        }\n    }\n}\n\n.empty-tip {\n    color: var(--text-muted);\n    font-size: $font-size-sm;\n    text-align: center;\n    padding: $space-lg;\n    width: 100%;\n}\n</style>"
  },
  {
    "path": "DOJ-FE/src/views/User/info/index.vue",
    "content": "<template>\n    <div class=\"settings-page\">\n        <div class=\"settings-container card-glass\">\n            <!-- Sidebar -->\n            <div class=\"settings-sidebar\">\n                <div class=\"user-preview\">\n                    <!-- <el-avatar :size=\"80\" :src=\"form.avatar ? '/api' + form.avatar : ''\" class=\"preview-avatar\">\n                        {{ form.username?.charAt(0).toUpperCase() }}\n                    </el-avatar> -->\n                    <el-upload ref=\"upload\" :action=\"avatarURL\" :show-file-list=\"false\" :on-success=\"handleSuccess\"\n                        :before-upload=\"beforeUpload\" :headers=\"uploadHeaders\" class=\"avatar-uploader\">\n                        <img v-if=\"imageUrl\" :src=\"imageUrl\" class=\"preview-avatar\" />\n                        <el-avatar v-else :size=\"80\" class=\"preview-avatar\">\n                            {{ form.username?.charAt(0).toUpperCase() }}\n                        </el-avatar>\n                        <div class=\"upload-mask\">\n                            <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n                                <path d=\"M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z\"></path>\n                                <circle cx=\"12\" cy=\"13\" r=\"4\"></circle>\n                            </svg>\n                        </div>\n                    </el-upload>\n                    <span class=\"preview-name\">{{ form.username }}</span>\n                </div>\n                <nav class=\"nav-menu\">\n                    <button :class=\"['nav-item', { active: activeTab === 0 }]\" @click=\"activeTab = 0\">\n                        <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n                            <path d=\"M20 21v-2a4 4 0 00-4-4H8a4 4 0 00-4 4v2\" />\n                            <circle cx=\"12\" cy=\"7\" r=\"4\" />\n                        </svg>\n                        个人资料\n                    </button>\n                    <button :class=\"['nav-item', { active: activeTab === 1 }]\" @click=\"activeTab = 1\">\n                        <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n                            <rect x=\"3\" y=\"11\" width=\"18\" height=\"11\" rx=\"2\" ry=\"2\" />\n                            <path d=\"M7 11V7a5 5 0 0110 0v4\" />\n                        </svg>\n                        修改密码\n                    </button>\n                </nav>\n            </div>\n\n            <!-- Content -->\n            <div class=\"settings-content\">\n                <!-- Profile Tab -->\n                <transition name=\"fade\" mode=\"out-in\">\n                    <div v-if=\"activeTab === 0\" key=\"profile\" class=\"tab-content\">\n                        <div class=\"tab-header\">\n                            <h2 class=\"tab-title\">个人资料</h2>\n                            <p class=\"tab-desc\">更新你的个人信息</p>\n                        </div>\n\n                        <el-form :model=\"form\" :rules=\"rules\" ref=\"formRef\" class=\"settings-form\" label-position=\"top\">\n                            <div class=\"form-grid\">\n                                <el-form-item prop=\"username\" label=\"用户名\">\n                                    <div class=\"input-wrapper\">\n                                        <el-input v-model=\"form.username\" placeholder=\"输入用户名\" />\n                                    </div>\n                                </el-form-item>\n                                <el-form-item prop=\"gender\" label=\"性别\">\n                                    <el-select v-model=\"form.gender\" placeholder=\"选择性别\" class=\"full-width\">\n                                        <el-option label=\"男\" :value=\"true\" />\n                                        <el-option label=\"女\" :value=\"false\" />\n                                    </el-select>\n                                </el-form-item>\n                                <el-form-item prop=\"school\" label=\"学校\">\n                                    <el-input v-model=\"form.school\" placeholder=\"输入学校名称\" />\n                                </el-form-item>\n                                <el-form-item prop=\"email\" label=\"邮箱\">\n                                    <el-input v-model=\"form.email\" placeholder=\"email@example.com\" disabled />\n                                </el-form-item>\n                                <el-form-item prop=\"url\" label=\"个人主页\" class=\"full-span\">\n                                    <el-input v-model=\"form.url\" placeholder=\"https://your-website.com\" />\n                                </el-form-item>\n                                <el-form-item prop=\"sign\" label=\"个性签名\" class=\"full-span\">\n                                    <el-input v-model=\"form.sign\" type=\"textarea\" :rows=\"3\" placeholder=\"写点什么介绍自己...\" />\n                                </el-form-item>\n                            </div>\n\n                            <div class=\"form-actions\">\n                                <button type=\"button\" class=\"submit-btn\" @click=\"handleSubmit\">\n                                    保存更改\n                                </button>\n                            </div>\n                        </el-form>\n                    </div>\n\n                    <!-- Password Tab -->\n                    <div v-else key=\"password\" class=\"tab-content\">\n                        <div class=\"tab-header\">\n                            <h2 class=\"tab-title\">修改密码</h2>\n                            <p class=\"tab-desc\">定期修改密码以保护账户安全</p>\n                        </div>\n\n                        <el-form :model=\"pwdForm\" :rules=\"pwdRules\" ref=\"pwdFormRef\" class=\"settings-form narrow\"\n                            label-position=\"top\">\n                            <el-form-item prop=\"oldPassword\" label=\"当前密码\">\n                                <el-input v-model=\"pwdForm.oldPassword\" type=\"password\" placeholder=\"输入当前密码\" show-password />\n                            </el-form-item>\n\n                            <el-form-item prop=\"newPassword1\" label=\"新密码\">\n                                <el-input v-model=\"pwdForm.newPassword1\" type=\"password\" placeholder=\"输入新密码\" show-password />\n                            </el-form-item>\n\n                            <el-form-item prop=\"newPassword2\" label=\"确认新密码\">\n                                <el-input v-model=\"pwdForm.newPassword2\" type=\"password\" placeholder=\"再次输入新密码\"\n                                    show-password />\n                            </el-form-item>\n\n                            <div class=\"form-actions\">\n                                <button type=\"button\" class=\"submit-btn\" @click=\"handlePwdSubmit\">\n                                    更新密码\n                                </button>\n                            </div>\n                        </el-form>\n                    </div>\n                </transition>\n            </div>\n        </div>\n    </div>\n</template>\n\n<script lang=\"ts\" setup>\nimport { ref, onMounted } from 'vue';\nimport { ElForm, ElFormItem, ElInput, ElSelect, ElOption, ElUpload, ElAvatar, ElMessage, UploadProps, UploadRawFile } from 'element-plus';\nimport { reqUserInfo, updUserInfo, updUserPwd } from '@/api/user';\nimport { useUserStore } from '@/stores/userStore';\n\nconst userStore = useUserStore();\nconst activeTab = ref(0);\nconst upload = ref<InstanceType<typeof ElUpload>>();\nconst imageUrl = ref<string>();\nconst avatarURL = '/api/user/avatar'; // Proxy handles /api\n\ntype FormRef = InstanceType<typeof ElForm>;\nconst formRef = ref<FormRef>();\nconst pwdFormRef = ref<FormRef>();\n\nconst form = ref({\n    id: 0,\n    username: '',\n    avatar: '',\n    email: '',\n    score: 0,\n    ranks: 0,\n    school: '',\n    gender: true,\n    easySolve: 0,\n    middleSolve: 0,\n    hardSolve: 0,\n    url: '',\n    sign: '',\n    fans: 0,\n    subscribe: 0,\n    password: '',\n    role: '',\n    ban: false,\n});\n\nconst pwdForm = ref({\n    oldPassword: '',\n    newPassword1: '',\n    newPassword2: '',\n});\n\nconst uploadHeaders = { token: userStore.userInfo?.accessToken };\n\nconst beforeUpload = (file: File) => {\n    const isValid = file.type === 'image/jpeg' || file.type === 'image/png';\n    const isLt2M = file.size / 1024 / 1024 < 2;\n    if (!isValid) ElMessage.error('Avatar picture must be JPG format!');\n    if (!isLt2M) ElMessage.error('Avatar picture size can not exceed 2MB!');\n    return isValid && isLt2M;\n};\n\nconst handleSuccess: UploadProps['onSuccess'] = (response: any) => {\n    if (response.code === 200) {\n        imageUrl.value = '/api' + response.data;\n        form.value.avatar = response.data;\n        // User store update if needed\n        if (userStore.userInfo) {\n            userStore.userInfo.avatar = response.data;\n        }\n        ElMessage.success('头像上传成功');\n    } else {\n        ElMessage.error(response.message);\n    }\n};\n\nconst rules = ref({\n    username: [{ required: true, message: '请输入用户名', trigger: 'blur' }]\n});\n\nconst validatePasswordConfirmation = (rule: any, value: string, callback: Function) => {\n    if (value !== pwdForm.value.newPassword1) {\n        callback(new Error('两次输入的密码不一致'));\n    } else {\n        callback();\n    }\n};\n\nconst pwdRules = {\n    oldPassword: [{ required: true, message: '请输入当前密码', trigger: 'blur' }],\n    newPassword1: [\n        { required: true, message: '请输入新密码', trigger: 'blur' },\n        { min: 6, max: 20, message: '密码长度在 6 到 20 个字符之间', trigger: 'blur' }\n    ],\n    newPassword2: [\n        { required: true, message: '请确认新密码', trigger: 'blur' },\n        { validator: validatePasswordConfirmation, trigger: 'blur' }\n    ]\n};\n\nconst handleSubmit = () => {\n    formRef.value!.validate(async (valid) => {\n        if (valid) {\n            await updUserInfo(form.value);\n            ElMessage.success('保存成功');\n        }\n    });\n};\n\nconst handlePwdSubmit = () => {\n    pwdFormRef.value!.validate(async (valid) => {\n        if (valid) {\n            await updUserPwd({\n                id: 0,\n                oldPassword: pwdForm.value.oldPassword,\n                newPassword: pwdForm.value.newPassword1\n            });\n            ElMessage.success('密码修改成功');\n            pwdForm.value = { oldPassword: '', newPassword1: '', newPassword2: '' };\n        }\n    });\n};\n\nonMounted(async () => {\n    const res = await reqUserInfo(userStore.userInfo?.userId || 0);\n    form.value = res.data.data;\n    if (form.value.avatar) {\n        imageUrl.value = '/api' + form.value.avatar;\n    }\n});\n</script>\n\n<style scoped lang=\"scss\">\n\n\n.settings-page {\n    padding: $space-lg 0;\n    max-width: 1000px;\n    margin: 0 auto;\n}\n\n.settings-container {\n    display: flex;\n    min-height: 500px;\n    overflow: hidden;\n    border-radius: $radius-lg;\n    \n    @include mobile {\n        flex-direction: column;\n    }\n}\n\n// Sidebar\n.settings-sidebar {\n    width: 240px;\n    padding: $space-xl;\n    border-right: 1px solid var(--border-color);\n    background: var(--bg-elevated);\n    flex-shrink: 0;\n    display: flex;\n    flex-direction: column;\n    align-items: center;\n    \n    @include mobile {\n        width: 100%;\n        padding: $space-md;\n        border-right: none;\n        border-bottom: 1px solid var(--border-color);\n        flex-direction: row;\n        justify-content: space-between;\n    }\n}\n\n.user-preview {\n    display: flex;\n    flex-direction: column;\n    align-items: center;\n    margin-bottom: $space-xl;\n    position: relative;\n    \n    @include mobile {\n        flex-direction: row;\n        gap: $space-md;\n        margin-bottom: 0;\n    }\n    \n    .avatar-uploader {\n        position: relative;\n        cursor: pointer;\n        \n        &:hover .upload-mask {\n            opacity: 1;\n        }\n    }\n    \n    .preview-avatar {\n        width: 80px;\n        height: 80px;\n        border-radius: 50%;\n        background: var(--primary-gradient);\n        color: white;\n        font-weight: $font-weight-bold;\n        font-size: 32px;\n        display: flex;\n        align-items: center;\n        justify-content: center;\n        object-fit: cover;\n        border: 3px solid var(--bg-elevated);\n        box-shadow: var(--shadow-md);\n    }\n    \n    .upload-mask {\n        position: absolute;\n        top: 0;\n        left: 0;\n        width: 100%;\n        height: 100%;\n        border-radius: 50%;\n        background: rgba(0, 0, 0, 0.5);\n        display: flex;\n        align-items: center;\n        justify-content: center;\n        opacity: 0;\n        transition: opacity $transition-fast;\n        color: white;\n        \n        svg {\n            width: 24px;\n            height: 24px;\n        }\n    }\n    \n    .preview-name {\n        margin-top: $space-md;\n        font-weight: $font-weight-semibold;\n        color: var(--text-primary);\n        font-size: $font-size-lg;\n        \n        @include mobile {\n            margin-top: 0;\n        }\n    }\n}\n\n.nav-menu {\n    display: flex;\n    flex-direction: column;\n    gap: $space-xs;\n    width: 100%;\n    \n    @include mobile {\n        flex-direction: row;\n        width: auto;\n    }\n}\n\n.nav-item {\n    display: flex;\n    align-items: center;\n    gap: $space-sm;\n    padding: $space-sm $space-md;\n    background: transparent;\n    border: none;\n    border-radius: $radius-md;\n    color: var(--text-secondary);\n    font-size: $font-size-sm;\n    cursor: pointer;\n    transition: all $transition-fast;\n    text-align: left;\n    width: 100%;\n    \n    svg {\n        width: 18px;\n        height: 18px;\n    }\n    \n    &:hover {\n        background: var(--glass-bg);\n        color: var(--text-primary);\n    }\n    \n    &.active {\n        background: rgba(102, 126, 234, 0.1);\n        color: var(--primary-start);\n        font-weight: $font-weight-medium;\n    }\n}\n\n// Content\n.settings-content {\n    flex: 1;\n    padding: $space-xl $space-2xl;\n    position: relative;\n    \n    @include mobile {\n        padding: $space-lg;\n    }\n}\n\n.tab-header {\n    margin-bottom: $space-xl;\n    \n    .tab-title {\n        font-size: $font-size-xl;\n        font-weight: $font-weight-bold;\n        margin-bottom: $space-xs;\n    }\n    \n    .tab-desc {\n        color: var(--text-secondary);\n        font-size: $font-size-sm;\n    }\n}\n\n// Form\n.form-grid {\n    display: grid;\n    grid-template-columns: 1fr 1fr;\n    gap: $space-lg;\n    \n    @include mobile {\n        grid-template-columns: 1fr;\n    }\n}\n\n.settings-form {\n    &.narrow {\n        max-width: 400px;\n    }\n    \n    .full-span {\n        grid-column: 1 / -1;\n    }\n    \n    .full-width {\n        width: 100%;\n    }\n}\n\n.form-actions {\n    margin-top: $space-xl;\n    display: flex;\n    justify-content: flex-end;\n}\n\n.submit-btn {\n    padding: $space-sm $space-xl;\n    background: var(--primary-gradient);\n    color: white;\n    border: none;\n    border-radius: $radius-md;\n    font-size: $font-size-sm;\n    font-weight: $font-weight-semibold;\n    cursor: pointer;\n    transition: all $transition-normal;\n    box-shadow: var(--shadow-glow);\n    \n    &:hover {\n        transform: translateY(-2px);\n        box-shadow: 0 4px 20px rgba(102, 126, 234, 0.5);\n    }\n}\n\n// Transition\n.fade-enter-active,\n.fade-leave-active {\n    transition: opacity 0.3s ease;\n}\n\n.fade-enter-from,\n.fade-leave-to {\n    opacity: 0;\n}\n</style>"
  },
  {
    "path": "DOJ-FE/src/views/User/login/index.vue",
    "content": "<template>\n    <div class=\"login-page\">\n        <div class=\"login-container\">\n            <!-- Left Side - Branding -->\n            <div class=\"branding-section\">\n                <div class=\"brand-content\">\n                    <div class=\"logo\">\n                        <svg viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n                            <path d=\"M12 2L2 7L12 12L22 7L12 2Z\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n                            <path d=\"M2 17L12 22L22 17\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n                            <path d=\"M2 12L12 17L22 12\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n                        </svg>\n                    </div>\n                    <h1>Duck Online Judge</h1>\n                    <p>现代化的在线编程判题平台</p>\n                    <div class=\"features\">\n                        <div class=\"feature\">\n                            <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n                                <path d=\"M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z\"/>\n                            </svg>\n                            安全沙箱执行\n                        </div>\n                        <div class=\"feature\">\n                            <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n                                <polygon points=\"13 2 3 14 12 14 11 22 21 10 12 10 13 2\"/>\n                            </svg>\n                            实时评测反馈\n                        </div>\n                        <div class=\"feature\">\n                            <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n                                <polyline points=\"16 18 22 12 16 6\"/>\n                                <polyline points=\"8 6 2 12 8 18\"/>\n                            </svg>\n                            多语言支持\n                        </div>\n                    </div>\n                </div>\n            </div>\n\n            <!-- Right Side - Form -->\n            <div class=\"form-section\">\n                <div class=\"form-card\">\n                    <h2>欢迎回来</h2>\n                    <p class=\"subtitle\">登录你的账户继续编程之旅</p>\n\n                    <el-form :model=\"form\" :rules=\"rules\" ref=\"formRef\" class=\"login-form\">\n                        <el-form-item prop=\"username\">\n                            <el-input \n                                v-model=\"form.username\" \n                                placeholder=\"用户名\"\n                                size=\"large\"\n                            >\n                                <template #prefix>\n                                    <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" class=\"input-icon\">\n                                        <path d=\"M20 21v-2a4 4 0 00-4-4H8a4 4 0 00-4 4v2\"/>\n                                        <circle cx=\"12\" cy=\"7\" r=\"4\"/>\n                                    </svg>\n                                </template>\n                            </el-input>\n                        </el-form-item>\n\n                        <el-form-item prop=\"password\">\n                            <el-input \n                                v-model=\"form.password\" \n                                type=\"password\" \n                                placeholder=\"密码\"\n                                size=\"large\"\n                                show-password\n                            >\n                                <template #prefix>\n                                    <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" class=\"input-icon\">\n                                        <rect x=\"3\" y=\"11\" width=\"18\" height=\"11\" rx=\"2\" ry=\"2\"/>\n                                        <path d=\"M7 11V7a5 5 0 0110 0v4\"/>\n                                    </svg>\n                                </template>\n                            </el-input>\n                        </el-form-item>\n\n                        <div class=\"form-options\">\n                            <label class=\"remember-me\">\n                                <input type=\"checkbox\" v-model=\"rememberMe\" />\n                                <span>记住我</span>\n                            </label>\n                            <a href=\"#\" class=\"forgot-link\">忘记密码？</a>\n                        </div>\n\n                        <el-form-item>\n                            <button type=\"button\" class=\"submit-btn\" @click=\"handleSubmit\" :disabled=\"loading\">\n                                <span v-if=\"loading\" class=\"loading-spinner\"></span>\n                                <span v-else>登录</span>\n                            </button>\n                        </el-form-item>\n                    </el-form>\n\n                    <div class=\"form-footer\">\n                        <span>还没有账户？</span>\n                        <router-link to=\"/register\" class=\"register-link\">立即注册</router-link>\n                    </div>\n                </div>\n            </div>\n        </div>\n    </div>\n</template>\n\n<script lang=\"ts\" setup>\nimport { ref } from 'vue';\nimport { ElForm, ElFormItem, ElInput, ElMessage } from 'element-plus';\nimport { useUserStore } from '@/stores/userStore';\nimport { useRouter } from 'vue-router';\n\nconst userStore = useUserStore();\nconst router = useRouter();\n\nconst form = ref({ username: '', password: '' });\nconst rememberMe = ref(false);\nconst loading = ref(false);\n\ntype FormRef = InstanceType<typeof ElForm>;\nconst formRef = ref<FormRef>();\n\nconst rules = ref({\n    username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],\n    password: [{ required: true, message: '请输入密码', trigger: 'blur' }]\n});\n\nconst handleSubmit = async () => {\n    try {\n        await formRef.value!.validate();\n        loading.value = true;\n        await userStore.getUserInfo({\n            username: form.value.username,\n            password: form.value.password\n        });\n        ElMessage.success(\"登录成功!\");\n        const redirect = sessionStorage.getItem(\"redirect\");\n        router.push(redirect || \"/\");\n        sessionStorage.removeItem(\"redirect\");\n    } catch (error) {\n        ElMessage.error(\"登录失败，请检查用户名或密码\");\n    } finally {\n        loading.value = false;\n    }\n};\n</script>\n\n<style scoped lang=\"scss\">\n\n\n.login-page {\n    min-height: 100vh;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    padding: $space-lg;\n}\n\n.login-container {\n    display: flex;\n    max-width: 900px;\n    width: 100%;\n    background: var(--bg-card);\n    border: 1px solid var(--border-color);\n    border-radius: $radius-xl;\n    overflow: hidden;\n    box-shadow: var(--shadow-lg);\n    \n    @include mobile {\n        flex-direction: column;\n    }\n}\n\n// Branding Section\n.branding-section {\n    flex: 1;\n    background: var(--primary-gradient);\n    padding: $space-2xl;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    \n    @include mobile {\n        padding: $space-xl;\n    }\n}\n\n.brand-content {\n    color: white;\n    text-align: center;\n    \n    .logo {\n        width: 60px;\n        height: 60px;\n        margin: 0 auto $space-lg;\n        \n        svg {\n            width: 100%;\n            height: 100%;\n        }\n    }\n    \n    h1 {\n        font-size: $font-size-2xl;\n        font-weight: $font-weight-bold;\n        margin-bottom: $space-sm;\n    }\n    \n    p {\n        opacity: 0.9;\n        margin-bottom: $space-xl;\n    }\n}\n\n.features {\n    display: flex;\n    flex-direction: column;\n    gap: $space-md;\n    text-align: left;\n    \n    .feature {\n        display: flex;\n        align-items: center;\n        gap: $space-sm;\n        font-size: $font-size-sm;\n        opacity: 0.9;\n        \n        svg {\n            width: 18px;\n            height: 18px;\n        }\n    }\n}\n\n// Form Section\n.form-section {\n    flex: 1;\n    padding: $space-2xl;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n}\n\n.form-card {\n    width: 100%;\n    max-width: 360px;\n    \n    h2 {\n        font-size: $font-size-2xl;\n        font-weight: $font-weight-bold;\n        margin-bottom: $space-xs;\n    }\n    \n    .subtitle {\n        color: var(--text-secondary);\n        margin-bottom: $space-xl;\n    }\n}\n\n.login-form {\n    .el-form-item {\n        margin-bottom: $space-lg;\n    }\n    \n    .input-icon {\n        width: 18px;\n        height: 18px;\n        color: var(--text-muted);\n    }\n}\n\n.form-options {\n    display: flex;\n    justify-content: space-between;\n    align-items: center;\n    margin-bottom: $space-lg;\n    \n    .remember-me {\n        display: flex;\n        align-items: center;\n        gap: $space-xs;\n        font-size: $font-size-sm;\n        color: var(--text-secondary);\n        cursor: pointer;\n        \n        input {\n            accent-color: var(--primary-start);\n        }\n    }\n    \n    .forgot-link {\n        font-size: $font-size-sm;\n        color: var(--primary-start);\n        \n        &:hover {\n            text-decoration: underline;\n        }\n    }\n}\n\n.submit-btn {\n    width: 100%;\n    padding: $space-md;\n    background: var(--primary-gradient);\n    color: white;\n    border: none;\n    border-radius: $radius-md;\n    font-size: $font-size-base;\n    font-weight: $font-weight-semibold;\n    cursor: pointer;\n    transition: all $transition-normal;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    gap: $space-sm;\n    \n    &:hover:not(:disabled) {\n        box-shadow: 0 6px 20px rgba(102, 126, 234, 0.5);\n        transform: translateY(-2px);\n    }\n    \n    &:disabled {\n        opacity: 0.7;\n        cursor: not-allowed;\n    }\n    \n    .loading-spinner {\n        width: 20px;\n        height: 20px;\n        border: 2px solid rgba(255,255,255,0.3);\n        border-top-color: white;\n        border-radius: 50%;\n        animation: spin 0.8s linear infinite;\n    }\n}\n\n@keyframes spin {\n    to { transform: rotate(360deg); }\n}\n\n.form-footer {\n    text-align: center;\n    margin-top: $space-xl;\n    color: var(--text-secondary);\n    font-size: $font-size-sm;\n    \n    .register-link {\n        color: var(--primary-start);\n        font-weight: $font-weight-medium;\n        margin-left: $space-xs;\n        \n        &:hover {\n            text-decoration: underline;\n        }\n    }\n}\n</style>"
  },
  {
    "path": "DOJ-FE/src/views/User/register/index.vue",
    "content": "<template>\n    <div class=\"register-page\">\n        <div class=\"register-container\">\n            <!-- Left Side - Branding -->\n            <div class=\"branding-section\">\n                <div class=\"brand-content\">\n                    <div class=\"logo\">\n                        <svg viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n                            <path d=\"M12 2L2 7L12 12L22 7L12 2Z\" stroke=\"currentColor\" stroke-width=\"2\"/>\n                            <path d=\"M2 17L12 22L22 17\" stroke=\"currentColor\" stroke-width=\"2\"/>\n                            <path d=\"M2 12L12 17L22 12\" stroke=\"currentColor\" stroke-width=\"2\"/>\n                        </svg>\n                    </div>\n                    <h1>Duck Online Judge</h1>\n                    <p>加入我们，开启你的编程之旅</p>\n                    <div class=\"stats\">\n                        <div class=\"stat\">\n                            <span class=\"value\">1000+</span>\n                            <span class=\"label\">题目</span>\n                        </div>\n                        <div class=\"stat\">\n                            <span class=\"value\">5000+</span>\n                            <span class=\"label\">用户</span>\n                        </div>\n                        <div class=\"stat\">\n                            <span class=\"value\">50万+</span>\n                            <span class=\"label\">提交</span>\n                        </div>\n                    </div>\n                </div>\n            </div>\n\n            <!-- Right Side - Form -->\n            <div class=\"form-section\">\n                <div class=\"form-card\">\n                    <h2>创建账户</h2>\n                    <p class=\"subtitle\">填写以下信息完成注册</p>\n\n                    <el-form :model=\"form\" :rules=\"rules\" ref=\"formRef\" class=\"register-form\">\n                        <el-form-item prop=\"username\">\n                            <el-input v-model=\"form.username\" placeholder=\"用户名\" size=\"large\">\n                                <template #prefix>\n                                    <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" class=\"input-icon\">\n                                        <path d=\"M20 21v-2a4 4 0 00-4-4H8a4 4 0 00-4 4v2\"/>\n                                        <circle cx=\"12\" cy=\"7\" r=\"4\"/>\n                                    </svg>\n                                </template>\n                            </el-input>\n                        </el-form-item>\n\n                        <el-form-item prop=\"email\">\n                            <el-input v-model=\"form.email\" placeholder=\"邮箱\" size=\"large\">\n                                <template #prefix>\n                                    <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" class=\"input-icon\">\n                                        <path d=\"M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z\"/>\n                                        <polyline points=\"22,6 12,13 2,6\"/>\n                                    </svg>\n                                </template>\n                            </el-input>\n                        </el-form-item>\n\n                        <el-form-item prop=\"password\">\n                            <el-input v-model=\"form.password\" type=\"password\" placeholder=\"密码\" size=\"large\" show-password>\n                                <template #prefix>\n                                    <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" class=\"input-icon\">\n                                        <rect x=\"3\" y=\"11\" width=\"18\" height=\"11\" rx=\"2\" ry=\"2\"/>\n                                        <path d=\"M7 11V7a5 5 0 0110 0v4\"/>\n                                    </svg>\n                                </template>\n                            </el-input>\n                        </el-form-item>\n\n                        <el-form-item prop=\"sign\">\n                            <el-input v-model=\"form.sign\" placeholder=\"个性签名（可选）\" size=\"large\">\n                                <template #prefix>\n                                    <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" class=\"input-icon\">\n                                        <path d=\"M12 20h9\"/>\n                                        <path d=\"M16.5 3.5a2.121 2.121 0 013 3L7 19l-4 1 1-4L16.5 3.5z\"/>\n                                    </svg>\n                                </template>\n                            </el-input>\n                        </el-form-item>\n\n                        <el-form-item>\n                            <button type=\"button\" class=\"submit-btn\" @click=\"handleSubmit\" :disabled=\"loading\">\n                                <span v-if=\"loading\" class=\"loading-spinner\"></span>\n                                <span v-else>注册</span>\n                            </button>\n                        </el-form-item>\n                    </el-form>\n\n                    <div class=\"form-footer\">\n                        <span>已有账户？</span>\n                        <router-link to=\"/login\" class=\"login-link\">立即登录</router-link>\n                    </div>\n                </div>\n            </div>\n        </div>\n    </div>\n</template>\n\n<script lang=\"ts\" setup>\nimport { ref } from 'vue';\nimport { ElForm, ElFormItem, ElInput, ElMessage, FormRules } from 'element-plus';\nimport { reqRegister } from '@/api/user';\nimport { useRouter } from 'vue-router';\n\nconst router = useRouter();\nconst loading = ref(false);\n\ntype ElFormInstance = InstanceType<typeof ElForm>;\nconst formRef = ref<ElFormInstance>();\n\ninterface FormType {\n    username: string;\n    password: string;\n    email: string;\n    sign: string;\n}\n\nconst form = ref<FormType>({\n    username: '',\n    password: '',\n    email: '',\n    sign: ''\n});\n\nconst rules = ref<FormRules<FormType>>({\n    username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],\n    password: [\n        { required: true, message: '请输入密码', trigger: 'blur' },\n        { min: 6, max: 20, message: '密码长度在 6 到 20 个字符之间', trigger: 'blur' }\n    ],\n    email: [\n        { required: true, message: '请输入邮箱', trigger: 'blur' },\n        { type: 'email', message: '请输入正确的邮箱地址', trigger: 'blur' }\n    ]\n});\n\nconst handleSubmit = async () => {\n    try {\n        await formRef.value!.validate();\n        loading.value = true;\n        await reqRegister(form.value);\n        ElMessage.success(\"注册成功!\");\n        router.push('/login');\n    } catch (error) {\n        ElMessage.error(\"注册失败，请检查输入\");\n    } finally {\n        loading.value = false;\n    }\n};\n</script>\n\n<style scoped lang=\"scss\">\n\n\n.register-page {\n    min-height: 100vh;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    padding: $space-lg;\n}\n\n.register-container {\n    display: flex;\n    max-width: 900px;\n    width: 100%;\n    background: var(--bg-card);\n    border: 1px solid var(--border-color);\n    border-radius: $radius-xl;\n    overflow: hidden;\n    box-shadow: var(--shadow-lg);\n    \n    @include mobile {\n        flex-direction: column;\n    }\n}\n\n.branding-section {\n    flex: 1;\n    background: var(--primary-gradient);\n    padding: $space-2xl;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n}\n\n.brand-content {\n    color: white;\n    text-align: center;\n    \n    .logo {\n        width: 60px;\n        height: 60px;\n        margin: 0 auto $space-lg;\n        svg { width: 100%; height: 100%; }\n    }\n    \n    h1 {\n        font-size: $font-size-2xl;\n        font-weight: $font-weight-bold;\n        margin-bottom: $space-sm;\n    }\n    \n    p {\n        opacity: 0.9;\n        margin-bottom: $space-xl;\n    }\n}\n\n.stats {\n    display: flex;\n    justify-content: center;\n    gap: $space-xl;\n    \n    .stat {\n        display: flex;\n        flex-direction: column;\n        \n        .value {\n            font-size: $font-size-xl;\n            font-weight: $font-weight-bold;\n        }\n        \n        .label {\n            font-size: $font-size-xs;\n            opacity: 0.8;\n        }\n    }\n}\n\n.form-section {\n    flex: 1;\n    padding: $space-2xl;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n}\n\n.form-card {\n    width: 100%;\n    max-width: 360px;\n    \n    h2 {\n        font-size: $font-size-2xl;\n        font-weight: $font-weight-bold;\n        margin-bottom: $space-xs;\n    }\n    \n    .subtitle {\n        color: var(--text-secondary);\n        margin-bottom: $space-xl;\n    }\n}\n\n.register-form {\n    .el-form-item { margin-bottom: $space-md; }\n    .input-icon { width: 18px; height: 18px; color: var(--text-muted); }\n}\n\n.submit-btn {\n    width: 100%;\n    padding: $space-md;\n    background: var(--primary-gradient);\n    color: white;\n    border: none;\n    border-radius: $radius-md;\n    font-size: $font-size-base;\n    font-weight: $font-weight-semibold;\n    cursor: pointer;\n    transition: all $transition-normal;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    \n    &:hover:not(:disabled) {\n        box-shadow: 0 6px 20px rgba(102, 126, 234, 0.5);\n        transform: translateY(-2px);\n    }\n    \n    &:disabled { opacity: 0.7; cursor: not-allowed; }\n    \n    .loading-spinner {\n        width: 20px;\n        height: 20px;\n        border: 2px solid rgba(255,255,255,0.3);\n        border-top-color: white;\n        border-radius: 50%;\n        animation: spin 0.8s linear infinite;\n    }\n}\n\n@keyframes spin {\n    to { transform: rotate(360deg); }\n}\n\n.form-footer {\n    text-align: center;\n    margin-top: $space-xl;\n    color: var(--text-secondary);\n    font-size: $font-size-sm;\n    \n    .login-link {\n        color: var(--primary-start);\n        font-weight: $font-weight-medium;\n        margin-left: $space-xs;\n        &:hover { text-decoration: underline; }\n    }\n}\n</style>\n"
  },
  {
    "path": "DOJ-FE/src/vite-env.d.ts",
    "content": "/// <reference types=\"vite/client\" />\n\n// 识别.vue文件\ndeclare module \"*.vue\" {\n    import { DefineComponent } from \"vue\";\n    const component:DefineComponent<{},{},any>\n    export default component\n \n}\n\n// 定义import.meta.env类型\ninterface ImportMetaEnv{\n    readonly VITE_APP_URL:string,\n    readonly VITE_APP_TITLE:string,\n    readonly NODE_ENV:string,\n}\n\n\n// 自定义组件类型\nimport Editor from '@/components/CodeEditor/index.vue';\ndeclare module 'vue' {\n  export interface GlobalComponents {\n    Editor: typeof Editor\n  }\n}"
  },
  {
    "path": "DOJ-FE/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2020\",\n    \"useDefineForClassFields\": true,\n    \"module\": \"ESNext\",\n    \"lib\": [\"ES2020\", \"DOM\", \"DOM.Iterable\"],\n    \"skipLibCheck\": true,\n\n    /* Bundler mode */\n    \"moduleResolution\": \"bundler\",\n    \"allowImportingTsExtensions\": true,\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"noEmit\": true,\n    \"jsx\": \"preserve\",\n\n    /* Linting */\n    \"strict\": true,\n\n    // 全局组件声明\n    // \"types\": [\"element-plus/global\"],\n\n    // 路径别名\n    \"baseUrl\": \"./\", \n    \"paths\": {\n      \"@/*\": [\"src/*\"] \n    }\n  },\n  \"include\": [\"src/**/*.ts\", \"src/**/*.tsx\", \"src/**/*.vue\"],\n  \"references\": [{ \"path\": \"./tsconfig.node.json\" }]\n}\n"
  },
  {
    "path": "DOJ-FE/tsconfig.node.json",
    "content": "{\n  \"compilerOptions\": {\n    \"composite\": true,\n    \"skipLibCheck\": true,\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"bundler\",\n    \"allowSyntheticDefaultImports\": true\n  },\n  \"include\": [\"vite.config.ts\"]\n}\n"
  },
  {
    "path": "DOJ-FE/vite.config.ts",
    "content": "import { defineConfig, UserConfig, ConfigEnv } from \"vite\";\nimport vue from \"@vitejs/plugin-vue\";\nimport path from \"path\";\nimport { createSvgIconsPlugin } from 'vite-plugin-svg-icons'\n//  https://vitejs.dev/config/\nexport default defineConfig(({ command }: ConfigEnv): UserConfig => {\n    return {\n        plugins: [\n            vue(),\n            createSvgIconsPlugin({\n                // Specify the icon folder to be cached\n                iconDirs: [path.resolve(process.cwd(), 'src/assets/icons')],\n                // Specify symbolId format\n                symbolId: 'icon-[dir]-[name]',\n            }),\n        ],\n        resolve: {\n            alias: {\n                \"@\": path.resolve(\"./src\")\n            }\n        },\n        css: {\n            preprocessorOptions: {\n                scss: {\n                    javascriptEnabled: true,\n                    additionalData: '@import \"./src/styles/variable.scss\";',\n                },\n            },\n        },\n        server: {\n            proxy: {\n                '/api': {\n                    target: 'http://localhost:8080',\n                    changeOrigin: true,\n                    rewrite: (path) => path.replace(/^\\/api/, '')\n                }\n            }\n        }\n    }\n});\n"
  },
  {
    "path": "LICENSE",
    "content": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "README.md",
    "content": "# D-OnlineJudge\n\n[![Java](https://img.shields.io/badge/java-17-blue)](https://www.oracle.com/java/technologies/javase-jdk17-downloads.html) [![Spring Boot](https://img.shields.io/badge/spring--boot-2.7.12-green)](https://spring.io/projects/spring-boot) [![Spring Cloud](https://img.shields.io/badge/Spring%20Cloud-2021.0.3-green)](https://spring.io/projects/spring-cloud) [![Spring Cloud Gateway](https://img.shields.io/badge/Spring%20Cloud%20Gateway-3.1.5-green)](https://spring.io/projects/spring-cloud-gateway) [![OpenFeign](https://img.shields.io/badge/OpenFeign-3.1.5-green)](https://github.com/OpenFeign/feign) [![Alibaba Nacos](https://img.shields.io/badge/Alibaba%20Nacos-2.5.1-green)](https://nacos.io/) [![Redis](https://img.shields.io/badge/Redis-7.0-red)](https://redis.io/) [![RabbitMQ](https://img.shields.io/badge/RabbitMQ-3--management-orange)](https://www.rabbitmq.com/) [![Elasticsearch](https://img.shields.io/badge/Elasticsearch-7.17.6-yellow)](https://www.elastic.co/) [![Sentinel](https://img.shields.io/badge/Sentinel-1.8.6-green)](https://github.com/alibaba/Sentinel) [![MySQL](https://img.shields.io/badge/MySQL-8.0-blue)](https://www.mysql.com/) [![MyBatis-Plus](https://img.shields.io/badge/MyBatis--Plus-3.5.2-green)](https://baomidou.com/) [![Vue.js](https://img.shields.io/badge/vue.js-3.3%2B-green)](https://vuejs.org/) [![TypeScript](https://img.shields.io/badge/TypeScript-5.0%2B-blue)](https://www.typescriptlang.org/) [![Element Plus](https://img.shields.io/badge/Element%20Plus-2.5%2B-green)](https://element-plus.org/) [![Docker](https://img.shields.io/badge/docker-20.10%2B-blue)](https://www.docker.com/) [![SkyWalking](https://img.shields.io/badge/SkyWalking-8.0%2B-purple)](https://skywalking.apache.org/) [![Prometheus](https://img.shields.io/badge/Prometheus-2.40%2B-blue)](https://prometheus.io/) [![License](https://img.shields.io/badge/license-Apache-blue.svg)](LICENSE)\n\n<div align=\"center\">\n<p align=\"center\">\n  <img src=\"./docs/welcome.jpg\" width=\"48%\">\n  <img src=\"./docs/code.jpg\" width=\"48%\"/>\n</p>\n</div>\n\n## 📖 Overview\n\n**D-OnlineJudge** is a full-stack online coding and competitive programming platform built with **Java Spring Cloud microservices architecture** and **Vue 3 frontend framework**.\n\nIt provides a secure, scalable, and feature-rich environment for programming competitions, code submissions, and real-time verdict feedback.\n\n### ✨ Key Features\n\n- **🏗️ Microservices Architecture**: Spring Cloud-based modular design with independent deployable services, high cohesion, and loose coupling\n- **🔒 Secure Sandbox Environment**: Docker containerization provides isolated execution environments to prevent malicious code attacks\n- **⚡ Asynchronous Judging**: Redis task queues + RabbitMQ event-driven architecture for high-performance, non-blocking code execution\n- **🔐 Enterprise-Grade Authentication**: Long-lived and short-lived token mechanism (Access Token + Refresh Token) with Redis-backed session management\n- **📊 Full-Stack Observability**: SkyWalking distributed tracing, Prometheus metrics monitoring, and Loki log aggregation for complete system visibility\n- **🚀 Automated CI/CD Pipeline**: GitHub Actions workflow for continuous integration, building, and deployment\n- **🔍 Intelligent Full-Text Search**: Elasticsearch with IK Chinese tokenizer for millisecond-level problem search\n- **💾 Three-Tier Cache Architecture**: Local cache (Caffeine) + distributed cache (Redis) + database, dramatically reducing database load\n- **🌐 Real-Time WebSocket Notifications**: Global push system for real-time verdict feedback without page blocking\n- **🛡️ Traffic Management**: Alibaba Sentinel integration for rate limiting, circuit breaking, and system protection\n\n---\n\n## 🚀 Quick Start\n\nThis guide will help you set up and run D-OnlineJudge locally. Follow the steps below.\n\n### Prerequisites\n\nEnsure you have the following software installed:\n\n| Software | Version | Purpose |\n| :--- | :--- | :--- |\n| **JDK** | 17+ | Backend compilation and runtime |\n| **Maven** | 3.6+ | Project build tool |\n| **Docker** | 20.10+ | Containerization |\n| **Docker Compose** | 1.29+ | Container orchestration |\n| **Git** | 2.0+ | Version control |\n| **Node.js** | 16+ (optional) | Frontend development |\n| **Pnpm** | 8+ (optional) | Frontend package manager |\n\n### Step 1: Clone the Repository\n\n```bash\ngit clone https://github.com/yourusername/D-OnlineJudge.git\ncd D-OnlineJudge\n```\n\n### Step 2: Backend Setup (`DOJ-BE`)\n\n#### 2.1 Start Core Dependency Services\n\nWe provide complete Docker Compose configurations to start all dependencies with a single command:\n\n```bash\n# Create Docker network\ndocker network create doj\n\n# Start all core services (MySQL, Redis, RabbitMQ, Nacos, Elasticsearch, etc.)\n# For detailed commands, see docs/0.build.md section \"2. Deploying Core Dependency Services\"\n```\n\n#### 2.2 Configure Nacos\n\n1. Access Nacos console: `http://localhost:8848/nacos` (default: `nacos`/`nacos`)\n2. Navigate to \"Configuration Management\" → \"Configuration List\"\n3. Create the following shared configuration files:\n\n| Data ID | Description |\n| :--- | :--- |\n| `shared-jdbc.yaml` | Database and cache connection settings |\n| `shared-swagger.yaml` | API documentation and logging config |\n| `shared-jwt.yaml` | JWT authentication key configuration |\n| `shared-rabbitmq.yaml` | RabbitMQ message queue settings |\n\n> 💡 **Configuration templates** are available in `docs/0.build.md` section \"2.8 Adding Shared Configurations to Nacos\".\n\n#### 2.3 Generate JWT Keystore\n\nExecute the following command in `DOJ-BE/common/src/main/resources/`:\n\n```bash\nkeytool -genkeypair -alias decade -keyalg RSA -keysize 2048 \\\n  -validity 365 -keypass doj123 -keystore doj.jks -storepass doj123\n```\n\n#### 2.4 Build Backend Project\n\n```bash\ncd DOJ-BE\nmvn clean install\n```\n\n#### 2.5 Start Microservices\n\nOpen a separate terminal for each microservice and start them in order:\n\n```bash\n# 1. Start Gateway Service\njava -Dhttp.proxySet=false -Dhttps.proxySet=false \\\n  -jar gateway-service/target/gateway-service-1.0-SNAPSHOT.jar\n\n# 2. Start User Service\njava -Dhttp.proxySet=false -Dhttps.proxySet=false \\\n  -jar user-service/target/user-service-1.0-SNAPSHOT.jar\n\n# 3. Start Problem Service\njava -Dhttp.proxySet=false -Dhttps.proxySet=false \\\n  -jar problem-service/target/problem-service-1.0-SNAPSHOT.jar\n\n# 4. Start Submission Service\njava -Dhttp.proxySet=false -Dhttps.proxySet=false \\\n  -jar submission-service/target/submission-service-1.0-SNAPSHOT.jar\n\n# 5. Start Sandbox Service\njava -Dhttp.proxySet=false -Dhttps.proxySet=false \\\n  -jar sandbox-service/target/sandbox-service-1.0-SNAPSHOT.jar\n```\n\n> 🎯 **Verify Backend Startup**:\n> - Check Nacos service list: `http://localhost:8848/nacos` should show all services in healthy status\n> - Access API documentation: `http://localhost:8080/doc.html` should display all service endpoints\n\n### Step 3: Frontend Deployment (`DOJ-FE`)\n\n#### Option A: Docker Container Deployment (Recommended for Production)\n\n```bash\ncd DOJ-FE\n\n# One-click build, package, and start (grant execute permission once)\nchmod +x run-docker.sh\n./run-docker.sh\n\n# Application will start at http://localhost:8088\n```\n\nThis script automatically:\n- ✅ Executes `pnpm build` to generate static files\n- ✅ Builds Nginx image based on `Dockerfile`\n- ✅ Stops old container and starts new one\n- ✅ Maps port `8088 → 80`\n\n#### Option B: Local Development Mode\n\n```bash\ncd DOJ-FE\n\n# Install dependencies\npnpm install\n\n# Start development server with hot reload\npnpm dev\n\n# Access at http://localhost:5173\n```\n\n### Step 4: Access the Application\n\nOnce all services are running successfully, access them via:\n\n| Component | URL | Description |\n| :--- | :--- | :--- |\n| **Frontend Application** | `http://localhost:8088` | Online coding competition platform |\n| **API Documentation** | `http://localhost:8080/doc.html` | Knife4j Swagger documentation |\n| **Nacos Console** | `http://localhost:8848/nacos` | Service registry and configuration |\n| **SkyWalking UI** | `http://localhost:9999` | Distributed tracing (optional) |\n| **Grafana Dashboard** | `http://localhost:3000` | Metrics monitoring and alerting (optional) |\n| **Kibana Logs** | `http://localhost:5601` | Log aggregation and analysis (optional) |\n\n---\n\n## 📊 Observability\n\n### Comprehensive Monitoring System\n\n| Pillar | Technology | Purpose |\n| :--- | :--- | :--- |\n| **Traces** | Apache SkyWalking | \"What did the request experience?\" - Link visualization, performance analysis |\n| **Metrics** | Prometheus + Grafana | \"How is the system performing?\" - Real-time monitoring and alerting |\n| **Logs** | Loki + Promtail | \"What happened?\" - Log aggregation and troubleshooting |\n\n### Quick Start Observability Stack\n\n```bash\n# Start SkyWalking\ndocker-compose -f docker-compose-skywalking.yml up -d\n# Access at http://localhost:9999\n\n# Start Prometheus + Grafana + Loki\ndocker-compose -f docker-compose-monitoring.yml up -d\n# Access Grafana at http://localhost:3000 (admin/admin)\n```\n\nFor detailed setup, see `docs/0.build.md` section \"4. Building the Observability Platform\".\n\n---\n\n## 🐳 Docker and Container Orchestration\n\n### Start All Services\n\n```bash\n# Start all backend microservices (dependencies must be started first)\ndocker-compose -f docker-compose-service.yml up -d\n\n# Start frontend application\ncd DOJ-FE && ./run-docker.sh\n\n# View running containers\ndocker ps\n```\n\n### Build Sandbox Environment Images\n\n```bash\n# Build multi-language code execution environments\ndocker build -t code-runner-cpp -f docs/Dockerfile.cpp .\ndocker build -t code-runner-java -f docs/Dockerfile.java .\ndocker build -t code-runner-python -f docs/Dockerfile.python .\n```\n\n---\n\n## 🤝 Contributing\n\nWe welcome all forms of contributions!\n\n1. **Fork** this repository\n2. Create a feature branch (`git checkout -b feature/AmazingFeature`)\n3. Commit your changes (`git commit -m 'Add some AmazingFeature'`)\n4. Push to the branch (`git push origin feature/AmazingFeature`)\n5. Open a Pull Request\n\n---\n\n## 📝 License\n\nThis project is licensed under the **MIT License**. See the [LICENSE](LICENSE) file for details.\n\n---\n\n## 📞 Contact and Feedback\n\nFor questions, suggestions, or feedback, please reach out through:\n\n- **GitHub Issues**: [Submit an Issue](https://github.com/yourusername/D-OnlineJudge/issues)\n- **Discussions**: [GitHub Discussions](https://github.com/yourusername/D-OnlineJudge/discussions)\n\n---\n\n## 🙏 Acknowledgments\n\nSpecial thanks to the following open-source projects for their inspiration and support\n\n---\n\n**⭐ If this project helps you, please give it a Star! ⭐**\n"
  },
  {
    "path": "docker-compose-monitoring.yml",
    "content": "version: '3.8'\nservices:\n  prometheus:\n    image: prom/prometheus:v2.45.0\n    container_name: prometheus\n    ports:\n      - \"9090:9090\"\n    volumes:\n      - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml\n    command: --config.file=/etc/prometheus/prometheus.yml\n    networks:\n      - doj\n\n  grafana:\n    image: grafana/grafana:9.5.3\n    container_name: grafana\n    ports:\n      - \"3000:3000\"\n    environment:\n      - GF_SECURITY_ADMIN_USER=admin\n      - GF_SECURITY_ADMIN_PASSWORD=admin\n    networks:\n      - doj\n\n  loki:\n    image: grafana/loki:2.8.0\n    container_name: loki\n    ports:\n      - \"3100:3100\"\n    command: -config.file=/etc/loki/local-config.yaml\n    networks:\n      - doj\n\n  promtail:\n    image: grafana/promtail:2.8.0\n    container_name: promtail\n    volumes:\n      - /var/run/docker.sock:/var/run/docker.sock\n      - ./prometheus/promtail-config.yml:/etc/promtail/config.yml\n    command: -config.file=/etc/promtail/config.yml\n    networks:\n      - doj\n\nnetworks:\n  doj:\n    external: true\n"
  },
  {
    "path": "docker-compose-service.yml",
    "content": "version: '3.8'\n\nservices:\n  #-------------------- Application Services --------------------#\n  gateway-service:\n    image: decadeqzj/doj-gateway-service:latest\n    container_name: gateway-service\n    ports:\n      - \"8080:8080\"\n    environment:\n      - NACOS_SERVER_ADDR=nacos:8848\n      - SENTINEL_DASHBOARD_ADDR=sentinel:8080\n      - JAVA_OPTS=-Xms128m -Xmx256m\n    mem_limit: 512m\n    networks:\n      - doj\n\n  user-service:\n    image: decadeqzj/doj-user-service:latest\n    container_name: user-service\n    ports:\n      - \"8081:8081\"\n    environment:\n      - NACOS_SERVER_ADDR=nacos:8848\n      - DOJ_DB_HOST=mysql\n      - DOJ_MQ_HOST=rabbitmq\n      - DOJ_REDIS_HOST=redis\n      - JAVA_OPTS=-Xms128m -Xmx256m\n    mem_limit: 512m\n    networks:\n      - doj\n\n  problem-service:\n    image: decadeqzj/doj-problem-service:latest\n    container_name: problem-service\n    ports:\n      - \"8083:8083\"\n    environment:\n      - NACOS_SERVER_ADDR=nacos:8848\n      - DOJ_DB_HOST=mysql\n      - DOJ_MQ_HOST=rabbitmq\n      - DOJ_REDIS_HOST=redis\n      - ELASTICSEARCH_URIS=http://elasticsearch:9200\n      - JAVA_OPTS=-Xms128m -Xmx256m\n    mem_limit: 512m\n    networks:\n      - doj\n\n  submission-service:\n    image: decadeqzj/doj-submission-service:latest\n    container_name: submission-service\n    ports:\n      - \"8084:8084\"\n    environment:\n      - NACOS_SERVER_ADDR=nacos:8848\n      - DOJ_DB_HOST=mysql\n      - DOJ_MQ_HOST=rabbitmq\n      - JAVA_OPTS=-Xms128m -Xmx256m\n    mem_limit: 512m\n    networks:\n      - doj\n\n  sandbox-service:\n    image: decadeqzj/doj-sandbox-service:latest\n    container_name: sandbox-service\n    ports:\n      - \"8082:8082\"\n    environment:\n      - NACOS_SERVER_ADDR=nacos:8848\n      - DOJ_REDIS_HOST=redis\n      - DOJ_MQ_HOST=rabbitmq\n      - DOJ_RESOURCE_LOCATION=/app/static/files/\n      - DOJ_CODE_PATH=/app/static/codes/\n      - DOJ_PROBLEM_CODE_PATH=/app/static/problem-codes/\n      - HOST_CODE_PATH=${PWD}/static/codes\n      - HOST_PROBLEM_CODE_PATH=${PWD}/static/problem-codes\n      - JAVA_OPTS=-Xms128m -Xmx256m\n    volumes:\n      - /var/run/docker.sock:/var/run/docker.sock\n      - ${PWD}/static/files:/app/static/files\n      - ${PWD}/static/codes:/app/static/codes\n      - ${PWD}/static/problem-codes:/app/static/problem-codes\n    mem_limit: 512m\n    networks:\n      - doj\n\nnetworks:\n  doj:\n    external: true"
  },
  {
    "path": "docs/0.build.md",
    "content": "# D-OnlineJudge: 构建与开发指南\n\n本文档为开发者提供一份清晰的指南，说明如何配置开发环境、构建和运行 D-OnlineJudge 项目的后端服务。\n\n---\n\n## 1. 环境准备 (Prerequisites)\n\n在开始之前，请确保您的开发环境中已安装以下软件：\n\n- **JDK**: 版本 `17` 或更高。请通过 `java --version` 命令确认。\n- **Maven**: 版本 `3.6` 或更高，用于构建项目。请通过 `mvn --version` 确认。\n- **Docker**: 用于运行 MySQL、Nacos 等依赖服务，以及作为代码沙箱的执行环境。\n\n---\n\n## 2. 部署核心依赖服务\n\n见 [docker部署](1.docker部署.md) 文档\n\n---\n\n## 3. 构建和运行后端微服务\n\n### 3.1. 配置微服务\n\n见以下文档：\n- [Common项目配置](2.Common项目配置.md)\n- [Gateway项目配置](3.Gateway项目配置.md)\n- [User-Service项目配置](4.User-Service项目配置.md)\n- [Problem-Service项目配置](5.Problem-Service项目配置.md)\n- [Submission-Service项目配置](6.Submission-Service项目配置.md)\n- [Sandbox-Service项目配置](7.Sandbox-service项目配置.md)\n\n### 3.2. 构建所有模块\n\n在后端父项目 `DOJ-BE` 的根目录下，使用 Maven 进行编译和打包。\n\n```sh\ncd /Users/qzj/Desktop/Development/D-OnlineJudge/DOJ-BE\nmvn clean install\n```\n\n### 3.3. 运行微服务\n\n为每个微服务打开一个单独的终端，并从 `DOJ-BE` 根目录启动它们。建议按照以下顺序启动：\n\n1.  **Gateway-Service**\n    ```sh\n    java -Dhttp.proxySet=false -Dhttps.proxySet=false -jar gateway-service/target/gateway-service-1.0-SNAPSHOT.jar\n    ```\n2.  **User-Service**\n    ```sh\n    java -Dhttp.proxySet=false -Dhttps.proxySet=false -jar user-service/target/user-service-1.0-SNAPSHOT.jar\n    ```\n3.  **Problem-Service**\n    ```sh\n    java -Dhttp.proxySet=false -Dhttps.proxySet=false -jar problem-service/target/problem-service-1.0-SNAPSHOT.jar\n    ```\n4.  **Submission-Service**\n    ```sh\n    java -Dhttp.proxySet=false -Dhttps.proxySet=false -jar submission-service/target/submission-service-1.0-SNAPSHOT.jar\n    ```\n5.  **Sandbox-Service**\n    ```sh\n    java -Dhttp.proxySet=false -Dhttps.proxySet=false -jar sandbox-service/target/sandbox-service-1.0-SNAPSHOT.jar\n    ```\n\n### 3.4. 验证服务状态\n\n- **检查 Nacos**: 再次访问 Nacos 控制台 `http://localhost:8848/nacos`，进入 “服务管理” -> “服务列表”，您应该能看到所有已启动的微服务（`gateway-service`, `user-service` 等）都处于健康状态。\n- **访问 API 文档**: 访问网关提供的聚合 API 文档 `http://localhost:8080/doc.html`，您应该能看到所有微服务的接口列表。\n\n## 4. 可观测性平台搭建\n\n### 4.1. 部署 SkyWalking (分布式追踪)\n\n项目使用 SkyWalking 进行服务间的链路追踪。我们通过一个独立的 Docker Compose 文件来部署它及其依赖的 Elasticsearch。\n\n1.  **启动 SkyWalking 服务**:\n    在项目根目录下，执行以下命令：\n    ```sh\n    docker-compose -f docker-compose-skywalking.yml up -d\n    ```\n\n2.  **访问 SkyWalking UI**:\n    等待一到两分钟，让所有服务完全启动。然后，通过浏览器访问 `http://localhost:9999`。\n\n### 4.2. 部署 Prometheus + Grafana (指标监控)\n\n我们使用 Prometheus 抓取指标，Grafana 进行可视化展示。\n\n1.  **启动监控服务**:\n    在项目根目录下，执行以下命令：\n    ```sh\n    docker-compose -f docker-compose-monitoring.yml up -d\n    ```\n\n2.  **访问 Grafana UI**:\n    通过浏览器访问 `http://localhost:3000` (默认用户名/密码: `admin`/`admin`)。\n\n3.  **配置数据源与仪表盘**:\n    在 Grafana 中，添加 Prometheus (`http://prometheus:9090`) 作为数据源，然后即可创建或导入仪表盘来展示微服务指标。\n\n4.  **导入预置仪表盘 (推荐)**:\n    为了快速开始，项目根目录下提供了一个预置的仪表盘文件 `grafana-dashboard.json`。\n    - 在 Grafana 左侧菜单，点击 “+” -> “Import dashboard”。\n    - 点击 “Upload JSON file”，选择项目中的 `grafana-dashboard.json` 文件。\n    - 在下方的 “Prometheus” 选项中，选择您刚刚配置好的 Prometheus 数据源。\n    - 点击 “Import”，即可拥有一个包含所有核心指标的监控大盘。\n\n### 4.3. 部署 Loki (日志聚合)\n\n我们使用 Loki 聚合所有微服务的日志，并由 Promtail 进行采集。\n\n1.  **启动日志服务**:\n    Loki 和 Promtail 的配置已包含在 `docker-compose-monitoring.yml` 中。执行以下命令即可一并启动：\n    ```sh\n    docker-compose -f docker-compose-monitoring.yml up -d\n    ```\n\n2.  **在 Grafana 中添加 Loki 数据源**:\n    - 访问 Grafana (`http://localhost:3000`)。\n    - 进入 \"Configuration\" -> \"Data sources\"，点击 \"Add data source\"。\n    - 选择 \"Loki\"。\n    - 在 URL 字段中，输入 `http://loki:3100`。\n    - 点击 \"Save & test\"。\n\n3.  **查询日志**:\n    添加成功后，即可在 Grafana 的 \"Explore\" 页面选择 Loki 数据源，并通过容器名称等标签来查询日志。\n\n### 4.4. 为微服务挂载 Agent (IntelliJ IDEA)\n\n为了让微服务能被 SkyWalking 追踪，需要在启动时为其挂载 Agent 探针。\n\n1.  **修改运行配置**: 在 IntelliJ IDEA 中，为每一个后端微服务（`user-service`, `gateway-service` 等）的“运行/调试配置” (Run/Debug Configurations) 进行修改。\n2.  **添加 VM 选项**: 找到 \"VM options\" 输入框，并添加以下两行：\n    ```\n    -javaagent:/path/to/your/skywalking-agent/skywalking-agent.jar\n    -Dskywalking.agent.service_name=user-service\n    ```\n    - **注意**: \n        - 将 `/path/to/your/skywalking-agent/` 替换为您本地 `skywalking-agent` 文件夹的**绝对路径**。\n        - `-Dskywalking.agent.service_name` 的值需要为每个服务单独设置（如 `problem-service`, `gateway-service` 等）。\n\n3.  **重启所有服务**后，即可在 SkyWalking UI 上看到服务拓扑和调用链路。\n\n---\n\n## 5. 代码沙箱 Docker 环境\n\n`sandbox-service` 依赖于预先构建好的 Docker 镜像来执行代码。请确保您已经构建了项目所需的多语言执行环境镜像。\n\n进入 `docs/7.Sandbox-service项目配置.md` 文件所在的目录，您会找到 `Dockerfile.cpp`, `Dockerfile.java`, `Dockerfile.python` 等文件。\n\n使用以下命令构建镜像：\n\n```sh\n# 构建 C++ 环境镜像\ndocker build -t code-runner-cpp -f Dockerfile.cpp .\n\n# 构建 Java 环境镜像\ndocker build -t code-runner-java -f Dockerfile.java .\n\n# 构建 Python 环境镜像\ndocker build -t code-runner-python -f Dockerfile.python .\n```\n\n确保这些镜像在 `sandbox-service` 运行的 Docker 环境中是可用的。\n\n---\n\n## 5. 自动化部署 (CI/CD)\n\n除了手动部署，本项目已配置了一套基于 GitHub Actions 的自动化 CI/CD 流水线。当代码被推送到 `main` 分支时，会自动触发以下流程：\n\n1.  **并行构建**: 自动编译所有微服务，并使用各自的 `Dockerfile` 构建 Docker 镜像。\n2.  **推送镜像**: 将构建好的镜像推送到 Docker Hub。\n3.  **远程部署**: 通过 SSH 连接到服务器，拉取最新镜像并使用 `docker-compose` 重启服务。\n\n### 准备工作\n\n### 核心环境变量说明\n\n本项目的容器化部署高度依赖环境变量来实现配置的灵活性。以下是在 `docker-compose-service.yml` 中使用的核心变量及其作用：\n\n-   **`NACOS_SERVER_ADDR`**: Nacos 服务器地址，容器环境应设为 `nacos:8848`。\n-   **`SENTINEL_DASHBOARD_ADDR`**: Sentinel Dashboard 地址，容器环境应设为 `sentinel:8080`。\n-   **`DOJ_DB_HOST`**: 数据库主机地址，容器环境应设为 `mysql`。\n-   **`DOJ_MQ_HOST`**: RabbitMQ 主机地址，容器环境应设为 `rabbitmq`。\n-   **`DOJ_REDIS_HOST`**: Redis 主机地址，容器环境应设为 `redis`。\n-   **`HOST_CODE_PATH`**: (仅用于 `sandbox-service`) 宿主机上用于存放临时代码的目录的绝对路径。通过 `${PWD}/static/codes` 动态获取，用于解决 Docker-out-of-Docker 的路径挂载问题。\n-   **`JAVA_OPTS`**: 用于设置 JVM 启动参数，如 `-Xms128m -Xmx256m`，以控制服务内存占用。\n\n核心配置文件位于 `.github/workflows/ci-cd.yml`。\n\n## 6. 前端部署 (Docker + Nginx)\n\n前端项目 (`DOJ-FE`) 采用业界标准的“打包 + Nginx 托管”的方式进行容器化部署。\n\n### 6.1. 环境与配置\n\n在部署前，请确保已完成以下配置，以保证前后端通信正常：\n\n1.  **环境变量**: 项目通过 `.env.development` 和 `.env.production` 文件来管理不同环境的 API 地址。我们已将其统一为相对路径 `/api`。\n\n2.  **Vite 代理 (仅开发环境)**: 在 `vite.config.ts` 中配置了 `server.proxy`，使得在开发模式下，所有对 `/api` 的请求都会被代理到本地运行的后端网关 (`http://localhost:8080`)。\n\n3.  **Nginx 代理 (生产环境)**: 在 `DOJ-FE/nginx.conf` 文件中，配置了反向代理规则。所有 `/api` 请求会被转发到 `http://host.docker.internal:8080/`，即宿主机上运行的后端网关服务。这使得容器化的前端可以与本地运行的后端进行通信。\n\n### 6.2. 一键部署脚本\n\n为了简化部署流程，项目在 `DOJ-FE` 目录下提供了一个一键部署脚本 `run-docker.sh`。\n\n**使用方法:**\n\n1.  **进入前端项目目录**:\n    ```sh\n    cd /Users/qzj/Desktop/Development/D-OnlineJudge/DOJ-FE\n    ```\n\n2.  **为脚本添加执行权限 (仅需一次)**:\n    ```sh\n    chmod +x run-docker.sh\n    ```\n\n3.  **执行一键部署**:\n    ```sh\n    ./run-docker.sh\n    ```\n\n该脚本会自动完成以下所有工作：\n- **打包应用**: 执行 `pnpm build` 生成最新的静态文件到 `dist` 目录。\n- **构建镜像**: 执行 `docker build`，根据 `Dockerfile` 将 `dist` 目录和 `nginx.conf` 打包成一个名为 `doj-frontend` 的 Nginx 镜像。\n- **清理旧容器**: 自动停止并删除任何已存在的同名旧容器。\n- **启动新容器**: 启动一个新的容器，并将主机的 `8088` 端口映射到容器的 `80` 端口。\n\n部署成功后，即可通过浏览器访问 `http://localhost:8088` 来使用 D-OnlineJudge 系统。\n\n"
  },
  {
    "path": "docs/0.re.md",
    "content": "### **项目：D-OnlineJudge 分布式在线判题系统**\n\n### **第一部分：需求规格说明书 (Requirements Specification Document)**\n\n#### **1. 项目概述**\n\nD-OnlineJudge 是一个基于 Java Spring Cloud 微服务架构和 Vue 3 前端构建的全栈在线编程和判题平台。项目旨在为编程学习者、竞赛参与者和教育工作者提供一个稳定、安全、功能丰富的在线编程环境。\n\n**核心价值**:\n\n*   **微服务架构**: 系统采用模块化设计，各服务职责单一、可独立部署和扩展，保证了系统的高内聚和低耦合。\n*   **安全沙箱**: 通过 Docker 容器化技术，为用户代码提供一个资源隔离、安全的执行环境，有效防止恶意代码攻击。\n*   **前后端分离**: 现代化的前后端分离架构，使得前端（UI/UX）和后端（业务逻辑）可以并行开发、独立演进。\n*   **统一的开发体验**: 提供完整的文档和部署指南，支持通过 Docker Compose 一键启动整个后端环境，简化了开发和部署流程。\n\n#### **2. 功能性需求 (Functional Requirements)**\n\n**2.1. 用户模块 (`user-service`)**\n\n*   **FR-1**: 支持用户通过邮箱和密码进行注册和登录。\n*   **FR-2**: 支持用户查看和编辑个人资料（如用户名、头像、学校、个性签名等）。\n*   **FR-3**: 支持用户密码的加密存储和安全修改。\n*   **FR-4**: 系统应包含角色管理，至少分为普通用户和管理员两种角色。\n\n**2.2. 题目模块 (`problem-service`)**\n\n*   **FR-5**: 管理员能够创建、编辑、查看和删除编程题目。\n*   **FR-6**: 题目应包含丰富的元数据，如：题面描述、输入/输出格式、样例、时间限制、内存限制、难度、标签等。\n*   **FR-7**: 普通用户能够按列表或ID查看题目详情。\n*   **FR-8**: 支持根据题号、难度、标签等条件对题目进行筛选和搜索。\n\n**2.3. 提交与判题模块 (`submission-service` & `sandbox-service`)**\n\n*   **FR-9**: 用户能够针对特定题目，选择编程语言（如 C++, Java, Python）并提交代码。\n*   **FR-10**: 系统必须记录每一次提交，包括提交人、题目、代码、提交时间、语言等。\n*   **FR-11**: 代码必须在安全隔离的沙箱环境中执行。\n*   **FR-12**: 沙箱必须对代码的**运行时间**和**内存使用**进行严格限制，并能处理超时（Time Limit Exceeded）和超内存（Memory Limit Exceeded）等情况。\n*   **FR-13**: 系统能够自动将用户代码的输出与预设的标准答案进行比对，并返回判题结果，至少包括：\n    *   Accepted (AC)\n    *   Wrong Answer (WA)\n    *   Compile Error (CE)\n    *   Time Limit Exceeded (TLE)\n    *   Memory Limit Exceeded (MLE)\n    *   Runtime Error (RE)\n*   **FR-14**: 用户可以查看自己的提交历史、状态和详细的判题结果。\n\n**2.4. 网关与认证模块 (`gateway-service`)**\n\n*   **FR-15**: 所有对后端微服务的请求都必须通过 API 网关进行路由。\n*   **FR-16**: 网关必须对除白名单（如登录、注册）外的所有请求进行 JWT 身份认证。\n*   **FR-17**: 网关需要处理跨域请求，以支持前后端分离部署。\n\n#### **3. 非功能性需求 (Non-Functional Requirements)**\n\n*   **NFR-1 (安全性)**:\n    *   用户密码等敏感信息必须加密存储。\n    *   必须通过 Docker 沙箱有效防止用户提交的恶意代码对服务器造成破坏。\n    *   API 接口需进行有效的认证和授权。\n*   **NFR-2 (可靠性)**:\n    *   核心业务服务应设计为无状态，支持多实例部署以实现高可用。\n    *   使用 Nacos 作为服务注册中心，确保服务的动态发现和故障转移。\n*   **NFR-3 (可扩展性)**:\n    *   微服务架构允许各个模块（用户、题目、提交）独立进行水平扩展。\n    *   代码沙箱应易于扩展，以支持更多的编程语言。\n*   **NFR-4 (可维护性)**:\n    *   提供详细的 API 文档（通过 Knife4j/Swagger）。\n    *   提供基于 Docker 的一键部署方案，简化开发和运维流程。\n    *   代码结构清晰，遵循 Spring Boot 社区的最佳实践。\n\n-----\n\n### **第二部分：项目计划与里程碑 (Development Plan)**\n\n**里程碑 1: 核心架构与基础服务搭建 (MVP) - ✅ 已完成**\n\n*   **目标**: 验证微服务架构的可行性，搭建核心基础服务。\n*   **核心交付物**:\n    *   **项目结构**: 搭建了基于 Maven 的多模块项目 `DOJ-BE`，包含 `user-service`, `problem-service`, `submission-service`, `gateway-service`, `sandbox-service`, 和 `common` 模块。\n    *   **服务治理**: 集成 Nacos 作为服务注册中心和统一配置中心。\n    *   **API 网关**: `gateway-service` 搭建完成，实现了基本的路由功能。\n    *   **用户服务**: `user-service` 完成，提供用户注册和登录的基础功能。\n    *   **题目服务**: `problem-service` 完成，支持题目的增删改查。\n\n**里程碑 2: 认证与安全沙箱实现 (Alpha) - ✅ 已完成**\n\n*   **目标**: 打通认证流程，并实现核心的、安全的代码判题功能。\n*   **核心交付物**:\n    *   **统一认证**: `gateway-service` 集成 JWT，实现了全局的、无状态的身份认证和鉴权。\n    *   **服务间通信**: 微服务之间通过 OpenFeign 进行调用，并实现了调用链路中的用户身份传递。\n    *   **Docker 沙箱**: `sandbox-service` 搭建完成，能够接收代码和语言，启动隔离的 Docker 容器执行代码，并对时间和内存进行限制。\n    *   **异步判题**: `sandbox-service` 采用异步线程池处理判题任务，提高了系统的响应能力和吞吐量。\n    *   **提交服务**: `submission-service` 能够接收提交请求，调用沙箱服务，并记录判题结果。\n\n**里程碑 3: 业务功能完善与部署 (Beta) - ✅ 已完成**\n\n*   **目标**: 完善核心业务流程，并提供完整的部署和文档支持。\n*   **核心交付物**:\n    *   **数据库设计**: 完成了 `user`, `problem`, `submission` 三个核心业务的数据库表结构设计和初始化。\n    *   **API 文档**: 所有微服务均集成了 Knife4j，提供清晰、可交互的 API 文档。\n    *   **Docker Compose 部署**: 提供了完整的 `docker-compose` 部署方案，能够一键启动 MySQL, Nacos 以及所有后端微服务，极大地简化了开发和测试环境的搭建。\n    *   **详细文档**: 编写了包括 Docker 部署、各服务配置、远程调用在内的多份详细开发文档。\n\n-----\n\n### **第三部分：后端项目结构 (Backend Project Structure)**\n\n后端项目 `DOJ-BE` 遵循标准的 Maven 多模块项目布局。\n\n```\nDOJ-BE/\n├── pom.xml               # 父项目 POM，管理所有依赖和模块\n├── common/                 # 公共模块 (工具、配置、Feign 客户端)\n│   ├── pom.xml\n│   └── src/\n├── gateway-service/        # API 网关服务\n│   ├── pom.xml\n│   └── src/\n├── user-service/           # 用户服务\n│   ├── pom.xml\n│   └── src/\n├── problem-service/        # 题目服务\n│   ├── pom.xml\n│   └── src/\n├── submission-service/     # 提交服务\n│   ├── pom.xml\n│   └── src/\n└── sandbox-service/        # 代码沙箱服务\n    ├── pom.xml\n    └── src/\n```\n\n**结构说明**:\n\n*   **`DOJ-BE/pom.xml`**: 这是父项目的 `pom.xml`，它通过 `<packaging>pom</packaging>` 定义自己为聚合项目。它在 `<modules>` 部分列出了所有子模块，并在 `<dependencyManagement>` 中统一管理了整个项目的依赖版本（如 Spring Boot, Spring Cloud, MyBatis-Plus 等），确保了版本的一致性。\n*   **`common/`**: 这是一个公共工具模块，被所有其他业务微服务依赖。它通常包含：\n    *   **工具类**: 如 `JwtTool`, `AesTool` 等。\n    *   **配置类**: 全局生效的配置，如 `MybatisConfig`, `MVCConfig`，以及通过 `spring.factories` 自动装配的全局异常处理器。\n    *   **Feign 客户端**: 定义了所有跨服务调用的 Feign 接口（如 `UserClient`）。\n    *   **共享模型**: 如 DTOs, VOs, 和通用的响应结果类 `R`。\n*   **`gateway-service/`**: Spring Cloud Gateway 应用，负责路由和过滤。\n*   **`user-service/`, `problem-service/`, `submission-service/`**: 标准的 Spring Boot 应用，分别对应一个核心业务领域。每个服务都有自己的 `Controller`, `Service`, `Mapper` 和 `Entity` 层。\n    **`sandbox-service/`**: 核心的判题服务，同样是一个 Spring Boot 应用，但其内部逻辑更侧重于与 Docker 引擎的交互和异步任务处理。\n\n---\n\n## 第四部分：架构升级与演进计划\n\n为实现一个更健壮、可扩展的系统，我们制定了清晰的演进计划。\n\n- **[ ] 中间件集成**\n    - [x] **集成 Redis**: 用于实现长短令牌存储、分布式缓存和高性能任务队列。\n    - [x] **集成 Message Queue (RabbitMQ)**: 用于实现服务间的事件驱动和异步解耦。\n    - [x] **集成 Sentinel**: 在网关层实现流量控制与熔断降级。\n\n- **[ ] 实时通信**\n    - [x] **实现 WebSocket 服务**: 用于向前端实时推送判题结果。\n\n- **[ ] 可观测性建设**\n    - [x] **集成 SkyWalking**: 搭建分布式链路追踪系统。\n    - [x] **部署 Prometheus & Grafana**: 建立统一的指标监控与告警平台。\n    - [x] **部署 ELK 或 Loki**: 建立集中式的日志管理系统。\n\n- **[ ] 自动化运维**\n    - [x] **建立 CI/CD 流水线**: 使用 Jenkins 或 GitHub Actions 自动化项目的测试、构建和部署流程。\n"
  },
  {
    "path": "docs/0.technology.md",
    "content": "# D-OnlineJudge: 技术选型与架构设计 (v2.0)\n\n本文档阐述 D-OnlineJudge 项目为实现高性能、高可用、高扩展性目标而设计的核心技术体系与架构亮点。\n\n---\n\n## 1. 升级版架构蓝图\n\n为应对复杂的业务场景和未来的技术挑战，D-OnlineJudge 的后端架构演进为一套集流量治理、异步通信、全链路可观测性于一体的现代化微服务体系。\n\n```mermaid\ngraph TD\n    subgraph \"用户接入层\"\n        A[Browser/Client] --> B(API Gateway);\n    end\n\n    subgraph \"核心业务层\"\n        direction LR\n        B --> D[User Service];\n        B --> E[Problem Service];\n        B --> F[Submission Service];\n    end\n\n    subgraph \"核心引擎层\"\n        K(Sandbox Service)\n    end\n\n    subgraph \"数据与消息层\"\n        direction TB\n        subgraph \"服务治理\"\n            G[Nacos]\n        end\n        subgraph \"消息与队列\"\n            I[Redis Task Queue]\n            J[RabbitMQ Event Bus]\n        end\n        subgraph \"数据存储\"\n            H[Redis Cache/Token]\n            DB[(MySQL Databases)]\n        end\n    end\n    \n    subgraph \"可观测性层\"\n        L[SkyWalking]\n        M[Prometheus + Grafana]\n        N[ELK/Loki]\n    end\n\n    %% 定义服务间的核心交互\n    F -- 1. 投递任务 --> I;\n    K -- 2. 消费任务 --> I;\n    K -- 3. 发布原始结果 --> J;\n    F -- 4. 消费结果 & 发布业务事件 --> J;\n    D -- 5. 消费用户统计事件 --> J;\n    E -- 5. 消费题目统计事件 --> J;\n    \n    F -- WebSocket 实时推送 --> A;\n\n    %% 定义与中间件的通用交互\n    D & E & F & K -- 注册/配置 --> G;\n    D & E & F -- 读/写 --> DB;\n    D & F -- 读/写 --> H;\n\n    %% 定义与可观测性的交互\n    D & E & F & K & B -- 上报数据 --> L;\n    D & E & F & K & B -- 上报数据 --> M;\n    D & E & F & K & B -- 上报数据 --> N;\n```\n\n---\n\n## 2. 核心架构亮点\n\n### 亮点一：统一API网关与智能治理中心\n\nAPI网关 (`gateway-service`) 不再仅仅是请求的“中转站”，而是通过深度集成 **Alibaba Sentinel**，升级为系统的“**流量治理与安全中枢**”，为整个后端服务集群提供了强大的保护能力。\n\n- **统一入口与路由**: 所有外部请求的唯一入口，基于路径将流量精确分发至下游服务。\n- **安全屏障**: \n    - **企业级认证**: 采用**长短令牌（Refresh/Access Token）机制**，通过网关层的全局过滤器（GlobalFilter）高效校验短令牌 `access_token`。\n    - **服务访问控制**: 在各微服务中配置拦截器，强制要求所有请求必须包含源自网关的特定标识，杜绝了内网环境下的服务横向穿透风险。\n\n- **流量智能治理 (Sentinel)**: \n    - **流量控制 (Flow Control)**: 我们为核心接口配置了精细化的 QPS（每秒查询率）限流规则。例如，为防止高频“刷题”，对“获取题目详情”接口 (`/problem/{id}`) 设置了“10次/分钟”的访问限制。这是通过将 QPS 设置为1，并配合“排队等待”的流控效果（等待超时6秒）来实现的，既能精确控制频率，又能缓冲用户的正常突发请求，体验更平滑。\n    - **熔断降级 (Circuit Breaking)**: 我们为所有跨服务的调用配置了基于“慢调用比例”的熔断规则。例如，当对 `user-service` 的调用响应时间超过预设阈值（如200ms）的比例过高时，网关会自动“熔断”对此服务的新请求，在一段时间内直接返回友好错误，而不是让大量请求堆积、超时。这能有效防止单个下游服务的故障或性能下降，通过“雪崩效应”拖垮整个系统，极大地提升了系统的健壮性。\n    - **动态与持久化**: 所有 Sentinel 规则都存储在 **Nacos** 中，网关服务在启动时会自动拉取，并能动态监听其变更。这使得运维人员可以在不重启服务的情况下，通过 Nacos 控制台实时调整流控和熔断策略。\n\n### 亮点二：混合消息模式：Redis任务队列与RabbitMQ事件总线\n\n为了同时满足判题任务的“高性能分发”和业务事件的“可靠广播”这两种不同需求，我们采用了一套优雅的混合消息架构，充分利用了 Redis 和 RabbitMQ 各自的优势。\n\n- **Redis List 作为任务队列**: 对于判题任务的分发，我们追求的是极致的低延迟和高吞吐。Redis 基于内存的 `LPUSH` / `BRPOP` 命令提供了完美的轻量级任务队列实现，使得 `sandbox-service` 的提交接口可以瞬时响应。\n- **RabbitMQ 作为事件总线**: 对于判题完成后的状态变更，这属于业务领域的事件。我们使用 RabbitMQ 强大的 Topic 交换机来广播这些事件（如 `submission.created`, `problem.solved`），未来任何对这些事件感兴趣的下游服务（如统计服务、徽章服务等）都可以灵活地订阅，实现了真正的事件驱动和微服务解耦。\n\n```mermaid\nsequenceDiagram\n    autonumber\n    participant FE as 前端\n    participant Submission as 提交服务\n    participant RedisQueue as Redis任务队列\n    participant Sandbox as 沙箱服务 (Worker)\n    participant RabbitMQ as RabbitMQ事件总线\n    participant User as 用户服务\n    participant Problem as 题目服务\n\n    FE->>Submission: 1. 提交代码 (POST /submission/create)\n    activate Submission\n    Submission->>Submission: 2. 创建 Submission 记录 (状态: PENDING)\n    Submission->>RedisQueue: 3. LPUSH judging:queue (发布判题任务)\n    Submission-->>FE: 4. 立即返回 submissionId\n    deactivate Submission\n\n    FE->>Submission: 5. 建立 WebSocket 连接\n\n    activate Sandbox\n    Sandbox->>RedisQueue: 6. BRPOP judging:queue (阻塞等待任务)\n    Sandbox->>Sandbox: 7. 执行 Docker 判题\n    Sandbox->>RabbitMQ: 8. 发送 `judging.result` 消息\n    deactivate Sandbox\n\n    activate Submission\n    Submission->>RabbitMQ: 9. 监听并消费 `judging.result` 消息\n    Submission->>Submission: 10. 更新数据库记录状态\n    Submission->>FE: 11. 通过 WebSocket 推送最终结果\n    Submission->>RabbitMQ: 12. 发送 `submission.created` & `problem.solved` 业务事件\n    deactivate Submission\n\n    activate User\n    User->>RabbitMQ: 13. 消费 `problem.solved` 事件并更新用户统计\n    deactivate User\n\n    activate Problem\n    Problem->>RabbitMQ: 14. 消费 `submission.created` 事件并更新题目统计\n    deactivate Problem\n```\n\n### 亮点三：企业级认证与分布式缓存中心\n\n我们引入 **Redis** 作为核心的内存数据中间件，并升级了认证体系，实现了高性能与高安全性的统一。\n\n#### 认证与刷新流程\n\n```mermaid\nsequenceDiagram\n    autonumber\n    participant User as 用户\n    participant Frontend as 前端\n    participant Gateway as 网关\n    participant UserService as 用户服务\n    participant Redis as Redis\n\n    User->>Frontend: 输入用户名/密码登录\n    Frontend->>Gateway: POST /user/login\n    Gateway->>UserService: 转发登录请求\n\n    activate UserService\n    UserService->>UserService: 校验用户名/密码\n    UserService->>UserService: 生成 access_token (JWT)\n    UserService->>UserService: 生成 refresh_token (UUID)\n    UserService->>Redis: 将 {refresh_token: userId} 存入 Redis (设置过期时间)\n    UserService-->>Frontend: 返回 access_token 和 refresh_token\n    deactivate UserService\n\n    Note over Frontend: 前端将两个 token 存入持久化存储 (Pinia)\n\n    loop 日常API请求\n        Frontend->>Gateway: GET /problem/list (Header携带 access_token)\n        activate Gateway\n        Gateway->>Gateway: 校验 access_token (JWT)\n        alt access_token 有效\n            Gateway->>ProblemService: 转发请求 (注入 userId)\n        else access_token 过期\n            Gateway-->>Frontend: 返回 401 Unauthorized\n        end\n        deactivate Gateway\n    end\n\n    Note over Frontend: 收到 401 错误，触发静默刷新逻辑\n\n    Frontend->>Gateway: POST /user/refresh (Header携带 refresh_token)\n    Gateway->>UserService: 转发刷新请求\n\n    activate UserService\n    UserService->>Redis: 在 Redis 中查找 refresh_token 是否存在\n    alt refresh_token 有效\n        UserService->>Redis: 从 Redis 获取 userId\n        UserService->>UserService: 生成新的 access_token\n        UserService-->>Frontend: 返回新的 access_token\n    else refresh_token 无效或过期\n        UserService-->>Frontend: 返回 401/403，提示需要重新登录\n    end\n    deactivate UserService\n\n    Note over Frontend: 收到新 access_token，更新本地存储，并重试刚才失败的API请求\n```\n\n- **长短令牌机制**:\n    - **`access_token` (JWT)**: 短时效、无状态，用于API请求，包含用户的角色权限信息，由网关进行快速验证。\n    - **`refresh_token`**: 长时效、有状态，存储在 **Redis** 中。用于在`access_token`过期后，安全地换取新的令牌，实现了用户的无感续期和强制下线等高级功能。\n- **分布式高速缓存**: \n    - **热点数据缓存**: 将频繁访问且不常变化的数据，如用户信息、题目详情、热门排行榜等，缓存在Redis中，大幅降低数据库的读取压力。\n    - **分布式锁**: 利用Redis实现分布式锁，解决在集群环境下更新题目通过数、用户积分等场景下的并发安全问题。\n\n### 亮点四：优雅的认证与用户上下文传递\n\n在微服务架构中，如何安全、无感地将用户的身份信息（如 User ID）在服务调用链中传递，是一个核心问题。本项目采用了一套“网关注入 -> 线程内缓存 -> Feign自动填充 -> 权限校验”的优雅方案。\n\n```mermaid\nsequenceDiagram\n    participant Gateway as 网关\n    participant ServiceA as 微服务A (e.g., Sandbox)\n    participant ServiceB as 微服务B (e.g., Problem)\n\n    Gateway->>ServiceA: 1. 转发请求 (Header 中已包含 `uid`)\n    \n    activate ServiceA\n    ServiceA->>ServiceA: 2. `IdentityInterceptor` 拦截请求\n    ServiceA->>ServiceA: 3. 从 Header 读取 `uid` 并存入 `UserContext` (ThreadLocal)\n    ServiceA->>ServiceB: 4. 通过 FeignClient 发起内部调用\n    deactivate ServiceA\n\n    Note over ServiceA, ServiceB: Feign `RequestInterceptor` 被触发\n    ServiceA->>ServiceA: 5. 从 `UserContext` 取出 `uid`\n    ServiceA->>ServiceB: 6. 将 `uid` 自动填入新请求的 Header 中\n\n    activate ServiceB\n    ServiceB->>ServiceB: 7. `IdentityInterceptor` 再次拦截\n    ServiceB->>ServiceB: 8. 验证 Header 中存在 `uid`，放行\n    deactivate ServiceB\n```\n\n```mermaid\nsequenceDiagram\n    participant Gateway as 网关\n    participant ProblemService as 题目服务\n    participant UserService as 用户服务\n\n    Gateway->>ProblemService: 1. 转发请求 POST /problem (Header 中已包含 `uid`)\n    \n    activate ProblemService\n    ProblemService->>ProblemService: 2. `IdentityInterceptor` 拦截请求, 将 `uid` 存入 `UserContext`\n    ProblemService->>ProblemService: 3. `AdminCheckInterceptor` 拦截请求, 发现 `@AdminRequired` 注解\n    ProblemService->>UserService: 4. 通过 Feign `UserClient` 调用 `getUserById(uid)`\n    activate UserService\n    UserService-->>ProblemService: 5. 返回用户信息 (包含 `role`)\n    deactivate UserService\n    ProblemService->>ProblemService: 6. 校验 `role` 是否为 'admin', 校验通过\n    ProblemService->>ProblemService: 7. 执行 Controller 业务逻辑\n    ProblemService-->>Gateway: 8. 返回响应\n    deactivate ProblemService\n```\n\n- **网关层统一注入**: 所有外部请求经网关认证后，用户的 `userId` 被解析并放入请求头，作为身份标识的唯一来源。\n- **线程内上下文 (`ThreadLocal`)**: 每个业务微服务都配置了一个 `IdentityInterceptor`。它在处理每个请求的开始，从请求头中读取 `userId` 并存入一个 `ThreadLocal` 变量 (`UserContext`) 中。这使得在当前请求的处理线程中，任何地方的业务代码都可以方便地通过 `UserContext.getCurrentUser()` 获取当前用户ID，而无需在方法参数中层层传递。\n- **Feign 自动填充**: 为了解决服务间调用时 `ThreadLocal` 信息丢失的问题，我们配置了一个全局的 Feign `RequestInterceptor`。它会在每一次 Feign 调用发起前，自动从 `UserContext` 中获取 `userId`，并将其添加到新的 HTTP 请求头中。这确保了用户身份在整个微服务调用链中能够无缝、安全地传递下去。\n\n#### 扩展：管理员权限校验\n\n基于以上用户上下文传递机制，我们构建了一套非侵入式的管理员权限校验方案。\n\n- **`@AdminRequired` 注解**: 我们创建了一个自定义注解，只需将其标记在任何需要管理员权限的 Controller 方法上即可。\n- **`AdminCheckInterceptor`**: 我们创建了第二个拦截器，它会在 `IdentityInterceptor` 之后执行。如果检测到方法上的 `@AdminRequired` 注解，它就会通过 `UserContext` 获取用户ID，然后通过 Feign 客户端 `UserClient` 回调 `user-service` 获取该用户的角色。如果角色不是 `admin`，则直接抛出 `ForbiddenException`，请求被终止并返回 403 错误。\n- **依赖注入与启动**: 通过在每个微服务的主启动类上使用 `@Import({..., AdminCheckInterceptor.class})`，我们确保了该拦截器作为一个 Bean 被 Spring 容器管理，从而解决了因 Feign 客户端初始化而可能导致的循环依赖问题。\n\n### 亮点五：全局 WebSocket 实时通知\n\n为了在异步判题架构下，向用户提供优雅、无干扰的实时结果反馈，我们构建了一套全局的 WebSocket 通知系统，取代了需要用户停留在当前页面的传统方案。\n\n```mermaid\nsequenceDiagram\n    autonumber\n    participant FE as 前端\n    participant Submission as 提交服务\n    participant Redis as Redis Pub/Sub (或 RabbitMQ)\n\n    FE->>Submission: 1. 提交代码后，拿到 submissionId\n    FE->>Submission: 2. 建立 WebSocket 连接\n    activate Submission\n    Submission->>Submission: 3. 存储 Session，并与 submissionId 关联\n    deactivate Submission\n\n    Note right of Submission: ... 判题流程在后台异步进行 ...\n\n    activate Submission\n    Submission->>Redis: 4. 监听器收到判题结果消息\n    Submission->>Submission: 5. 根据 submissionId 查找对应的 Session\n    Submission-->>FE: 6. 通过 WebSocket 主动推送结果\n    deactivate Submission\n\n    FE->>FE: 7. 收到消息，更新UI（弹出通知）\n```\n\n- **全局服务化**: 我们将 WebSocket 的所有连接管理、消息监听和处理逻辑，都封装在一个独立的、全局可用的服务 (`src/utils/websocket.ts`) 中。该服务在应用根组件 (`App.vue`) 挂载时被初始化，确保了其生命周期与整个应用保持一致。\n\n- **按需订阅**: 用户在提交代码后，前端会调用 `useWebSocket().subscribeToSubmission(submissionId)` 方法。该方法会通过已建立的全局 WebSocket 连接，向后端发送一条“订阅”消息，将当前会话与本次提交的 `submissionId` 关联起来。\n\n- **非阻塞式富文本通知**: 当后端判题完成，并通过 WebSocket 推送结果时，前端的全局监听器会捕获到该消息。它并不会粗暴地打断用户的当前操作，而是调用 Element Plus 的 `ElNotification` 组件，在屏幕的右下角弹出一个内容丰富的通知卡片。我们利用 Vue 的 `h` 函数（渲染函数）来动态构建通知的 `message`，使其可以包含状态、耗时、内存、错误信息等复杂的 HTML 结构。\n\n- **可交互与自动关闭**: 该通知卡片在显示一段时间后会自动消失，用户也可以点击它，页面会自动跳转到对应的提交详情页，实现了从“收到结果”到“查看详情”的无缝体验。\n\n### 亮点六：高性能三级缓存架构 (L1+L2+DB)\n\n对于“题目详情”这类访问频率极高、但内容不常变更的“热点数据”，我们设计并实现了一套“本地缓存 + 分布式缓存 + 数据库”的三级缓存架构，最大限度地降低了数据库压力，将查询性能提升至极致。\n\n```mermaid\ngraph TD\n    subgraph \"读请求 (Read Flow)\"\n        direction LR\n        Read_A[API Request: GET /problem/1] --> Read_B{Problem Service 1};\n        Read_B -- 1. 查 L1 --> Read_C[L1: Caffeine];\n        Read_C -- Miss --> Read_D{2. 查 L2};\n        Read_D --> Read_E[L2: Redis];\n        Read_E -- Miss --> Read_F{3. 查 L3};\n        Read_F --> Read_G[L3: MySQL];\n        Read_G -- 数据回填 --> Read_E;\n        Read_E -- 数据回填 --> Read_C;\n    end\n\n    subgraph \"写请求与缓存同步 (Write & Invalidation Flow)\"\n        direction TB\n        Write_A[API Request: PUT /problem/1] --> Write_B{Problem Service 2};\n        Write_B -- 1. 更新数据库 --> Write_G[L3: MySQL];\n        Write_B -- 2. 删除 L2 缓存 --> Write_E[L2: Redis];\n        Write_B -- 3. 发送失效广播 --> Write_H[(RabbitMQ: cache.update.exchange)];\n    end\n\n    subgraph \"其他服务实例 (Cache Invalidation)\"\n        direction TB\n        Other_B{Problem Service 1} -- 4. 监听广播 --> Write_H;\n        Other_B -- 5. 删除 L1 缓存 --> Other_C[L1: Caffeine];\n        \n        Self_B{Problem Service 2} -- 4. 监听广播 --> Write_H;\n        Self_B -- 5. 删除 L1 缓存 --> Self_C[L1: Caffeine];\n    end\n\n    style Read_G fill:#f9f,stroke:#333,stroke-width:2px\n    style Write_G fill:#f9f,stroke:#333,stroke-width:2px\n    style Read_E fill:#f9f,stroke:#333,stroke-width:2px\n    style Write_E fill:#f9f,stroke:#333,stroke-width:2px\n    style Write_H fill:#ff9,stroke:#333,stroke-width:2px\n```\n\n- **L1 本地缓存 (Caffeine)**: 速度最快的内存级缓存。对于短时间内对同一个题目的重复请求，将直接在纳秒级从内存返回，无需任何网络开销。\n- **L2 分布式缓存 (Redis)**: 速度很快的集中式缓存。当 L1 缓存未命中时（例如，服务实例首次访问该数据），会查询 Redis。这避免了对数据库的直接访问，并且缓存结果由所有 `problem-service` 实例共享。\n- **L3 数据源 (MySQL)**: 最终的数据保障，只有当 L1 和 L2 缓存都未命中时，才会查询数据库。\n\n#### 核心查询流程与场景\n\n1.  **L1 命中 (理想情况)**: 请求直接由本地内存返回，性能最佳。\n2.  **L2 命中 (跨实例协同)**: L1 未命中，但从 Redis 中获取到数据。数据会**回填**到 L1 缓存中，以便下一次访问能直接命中 L1。\n3.  **L1 & L2 均失效 (缓存穿透/冷启动)**: 缓存中无数据，查询将穿透到数据库。查询成功后，数据会**依次写回**到 L2 (Redis) 和 L1 (Caffeine) 中，完成缓存的预热。\n\n#### 缓存一致性保证\n\n当题目信息被更新或删除时，为了保证数据一致性，我们采用“**L2 缓存淘汰 + MQ 广播**”的策略：\n1.  服务实例在完成数据库操作后，立即删除 Redis 中的 L2 缓存。\n2.  同时，向 RabbitMQ 的一个 Fanout 交换机（广播模式）发送一条“缓存失效”消息。\n3.  所有 `problem-service` 实例（包括操作者自身）都会监听到这条消息，并各自删除自己内存中的 L1 缓存。这确保了所有实例的本地缓存在短时间内都能达到最终一致性。\n\n### 亮点七：全链路可观测性体系 (The Three Pillars)\n\n在复杂的微服务环境中，快速定位问题和分析性能瓶颈是巨大的挑战。为此，我们构建了覆盖“日志、追踪、监控”三大黄金指标的全链路可观测性体系，让系统的内部状态不再是“黑盒”。\n\n| 支柱 | 技术选型 | 核心作用 (回答的问题) |\n| :--- | :--- | :--- |\n| **日志 (Logs)** | **ELK / Loki** | **“发生了什么？”** - 提供离散的、具体的事件记录。 |\n| **指标 (Metrics)** | **Prometheus + Grafana** | - **立体化监控与告警 (Prometheus + Grafana)**|\n| **追踪 (Traces)** | **Apache SkyWalking** | **“请求经历了什么？”** - 串联单次请求在多个服务间的完整调用路径和耗时。 |\n\n- **日志聚合 (Loki)**:\n    - **日志采集 (`Promtail`)**: 我们部署了 Promtail 作为日志采集代理。它通过 Docker 服务发现，能自动找到所有正在运行的微服务容器，并抓取它们的标准输出（stdout）日志。\n    - **日志存储与查询 (`Loki`)**: Promtail 将采集到的日志流打上标签（如 `container=\"/user-service\"`）后，发送给 Loki 进行存储。Loki 只对标签进行索引，而不是对日志内容进行全文索引，这使得它极其轻量且存储成本极低。\n    - **查询与关联 (`Grafana`)**: 在 Grafana 中，我们可以使用 LogQL（一种类似 PromQL 的查询语言）来高效地搜索和过滤日志，例如 `{container=\"/problem-service\"} |= \"ERROR\"`。更强大的是，我们可以通过 SkyWalking 的 TraceID，在 Grafana 中一键从 Trace 视图跳转到相关的日志，实现链路与日志的无缝关联，极大提升了分布式系统的排障效率。\n\n- **指标暴露 (`Actuator`)**: 每个微服务都集成了 Spring Boot Actuator 和 Micrometer，通过 `/actuator/prometheus` 端点，以 Prometheus 要求的格式，暴露出海量的、涵盖 JVM、HTTP 请求、连接池等多个维度的核心指标。\n- **指标采集 (`Prometheus`)**: 我们部署了 Prometheus 服务，并配置其通过 Nacos 进行服务发现。这使得 Prometheus 能够自动找到所有在线的微服务实例，并定期从它们的指标端点“抓取”(Scrape) 数据，存入其内置的时序数据库中。\n- **数据可视化 (`Grafana`)**: 我们使用 Grafana 作为统一的可视化平台。通过配置 Prometheus 为数据源，我们可以使用强大的 PromQL 查询语言（例如，`sum(rate(http_server_requests_seconds_count{job=\"doj-services\"}[1m])) by (service)`）来聚合和分析指标，并创建包含折线图、仪表盘、告警规则的专业监控大盘。\n\n#### SkyWalking：分布式系统的“GPS导航”\n\n在三大支柱中，SkyWalking 扮演着不可或缺的“GPS导航”角色。当一个请求变慢或出错时，日志只能告诉我们某个服务报错了，监控指标只能告诉我们某个服务的CPU升高了，但只有**链路追踪**能清晰地告诉我们：\n\n-   这个请求从网关开始，依次经过了哪些服务？\n-   在每个服务内部，它调用了哪些方法、访问了哪个数据库？\n-   整个调用链条中，到底是**哪一环**花费了最长的时间，或者**哪一环**最先抛出了异常？\n\n**实现原理**: 我们通过 `-javaagent` 的方式，为每个微服务无侵入式地挂载了 SkyWalking 的探针。该探针会自动拦截所有进出的 HTTP 请求、JDBC 调用、MQ 消息等，并生成和传递一个全局唯一的 `TraceID`，最终将整条调用链的数据上报给 SkyWalking OAP 服务器进行分析和存储。\n\n#### 实际应用场景与查询指南\n\n**场景**: 用户反馈“提交代码后，页面卡了很久才显示结果”。\n\n**如何使用 SkyWalking 排查？**\n\n1.  **定位慢请求**: 打开 SkyWalking UI (`http://localhost:9999`)，进入“追踪”页面。通过服务名称 (`gateway-service`)、接口路径 (`/submission/create`) 或设置最小耗时，筛选出那些响应缓慢的请求记录。\n\n2.  **分析链路拓扑**: 点击一条慢请求的追踪记录，您会看到一个清晰的**火焰图或列表**，它展示了该请求从 `gateway-service` 开始，到 `submission-service`，再到 `sandbox-service` 的完整调用过程。\n\n    ```\n    gateway-service  (2010ms)\n    └── submission-service (2005ms)\n        ├── (DB) INSERT ... (5ms)\n        ├── (Redis) LPUSH ... (1ms)\n        └── (Feign) POST /sandbox/validate (2000ms)  <-- 发现瓶颈！\n            └── sandbox-service (1998ms)\n                └── ...\n    ```\n\n3.  **发现根因**: 从上图可以一目了然地发现，整个请求的耗时主要集中在 `submission-service` 对 `sandbox-service` 的 Feign 调用上。这立刻将我们的排查范围缩小到了 `sandbox-service` 内部，而不是去怀疑数据库或 Redis。这正是链路追踪的巨大价值所在。\n\n\n### 亮点八：自动化CI/CD流水线 (GitHub Actions)\n\n为了实现快速、可靠、一致的软件交付，我们基于 GitHub Actions 构建了一套完整的自动化 CI/CD (持续集成/持续部署) 流水线。开发者只需将代码推送到 `main` 分支，后续的测试、构建、打包、推送镜像、服务器部署等所有繁琐的流程都将自动完成。\n\n- **并行构建 (`Matrix Strategy`)**: 流水线利用 GitHub Actions 的 `strategy.matrix` 特性，为每一个微服务（`user-service`, `problem-service` 等）创建一个独立的构建任务。这些任务在云端并行执行，极大地缩短了所有服务的总构建时间。\n\n- **优化的多阶段构建 (`Dockerfile`)**: 我们为每个微服务都编写了优化的多阶段 `Dockerfile`。在“构建阶段”，使用包含完整 Maven 环境的镜像来编译代码和下载依赖；在“运行阶段”，仅将编译好的 `.jar` 文件复制到一个轻量的 JRE 镜像中。这使得最终生成的生产镜像体积最小化，并能充分利用 Docker 的构建缓存机制，提升后续构建的速度。\n\n- **职责分离的 Jobs (`build-and-push` & `deploy`)**:\n    1.  **`build-and-push` Job**: 负责编译所有代码，构建所有微服务的 Docker 镜像，并将其推送到 Docker Hub 镜像仓库进行统一的版本管理。\n    2.  **`deploy` Job**: 该任务依赖于 `build-and-push` 的成功。它会通过 SSH 安全地连接到生产服务器，执行预设的部署脚本，自动拉取最新的镜像并以 `docker-compose` 的方式重启所有服务，完成部署的“最后一公里”。\n\n通过这套流水线，我们实现了从“代码提交”到“服务上线”的全流程自动化，确保了每次部署都是可重复、可预测和高度可靠的。\n\n### 亮点九：生产级的容器化部署实践\n\n将一个复杂的微服务应用正确地部署到容器中，会遇到一系列经典的网络与文件系统挑战。本项目通过优雅的工程实践，解决了两大核心难题，确保了部署的健壮性和可移植性。\n\n- **优雅的 Nginx 服务发现与路径重写**: \n    - **问题**: 在 Docker 环境中，当后端服务（如网关）重启并获得新 IP 时，Nginx 因 DNS 缓存会连接到旧的、已失效的 IP，导致 `Connection Refused`。同时，前端的 `/api` 路径前缀也需要被正确剥离。\n    - **解决方案**: 我们采用了 `upstream` + `resolver` + `rewrite` 的黄金组合。通过 `upstream` 块定义服务池，并利用 `resolver` 指令强制 Nginx 动态解析其中的域名，彻底解决了 DNS 缓存问题。同时，使用 `rewrite` 指令清晰地处理路径重写，将职责完美分离，保证了配置的健壮与可读性。\n\n- **轻量级的 Docker-out-of-Docker 判题沙箱**:\n    - **目标**: `sandbox-service`（判题服务）自身运行在容器中，但它需要具备动态创建和管理其他“一次性”判题容器的能力。\n    - **核心实现**: 我们采用了业界标准的 “Docker-out-of-Docker” 模式，而非在容器内再嵌套一个完整 Docker 环境的 “Docker-in-Docker” 重模式。其实现包含两个关键点：\n        1.  **共享通信套接字 (`docker.sock`)**: 我们通过 `volumes` 将宿主机的 Unix Socket 文件 `/var/run/docker.sock` 挂载到 `sandbox-service` 容器内部。这个 `sock` 文件是本地 Docker 客户端与 Docker 守护进程之间进行通信的 IPC 通道。通过共享它，我们巧妙地让容器内的进程能够直接与宿主机上的 Docker 守护进程对话。\n        2.  **安装 Docker 客户端**: 我们在 `sandbox-service` 的 `Dockerfile` 中，通过 `apt-get` 安装了 `docker.io` 包。这为容器提供了 `docker` 命令行客户端。当服务在代码中执行 `docker run` 命令时，这个客户端就会通过挂载进来的 `docker.sock` 文件，将指令发送给宿主机的守护进程去执行，从而实现了“在容器内编排和管理宿主机上的其他容器”这一强大功能。\n\n### 亮点十：高性能全文检索 (Elasticsearch)\n\n为了提供毫秒级的题目搜索体验并支持复杂的文本查询，我们引入了 Elasticsearch 作为项目核心的搜索引擎，彻底取代了传统关系型数据库在全文检索场景下的性能瓶颈。\n\n- **技术选型与动机**: 传统的 `MySQL LIKE '%keyword%'` 查询无法利用索引，在数据量增大时性能会急剧下降，且不支持分词、相关性排序等高级功能。Elasticsearch 作为专业的搜索引擎，通过其核心的**倒排索引**机制，能提供近乎实时的搜索响应。\n\n- **双写同步架构**: 我们采用了经典的“双写”模式来保证 MySQL 和 Elasticsearch 之间的数据一致性。在 `problem-service` 中，任何对题目数据的增、删、改操作，都会在完成对主数据源 MySQL 的操作后，**同步地**对 Elasticsearch 中的对应文档执行相同的操作。这确保了两个数据源之间的准实时同步。\n\n- **IK 中文分词集成**: 为了实现精准的中文内容搜索，我们构建了集成了 **IK Analyzer** 分词插件的自定义 Elasticsearch 镜像。在 `ProblemDocument` 实体中，我们通过 `@Field` 注解为题目标题、描述等文本字段指定了 `ik_max_word`（用于索引）和 `ik_smart`（用于搜索）分词器，实现了对中文的智能切分和检索。\n\n- **动态搜索策略**: 在 `ProblemServiceImpl` 中，我们实现了一套优雅的动态搜索策略。系统会根据用户的查询条件（`needElasticsearchSearch` 方法）来智能判断：如果用户输入了题目名称、标签等需要进行全文检索的关键词，则调用 `searchByElasticsearch` 方法，利用 ES 的强大能力；如果只是简单的精确条件查询，则回退到 `searchByDatabase` 方法，直接查询 MySQL，从而为不同场景选择了最优的查询路径。\n\n- **数据一致性保障**: 我们在 `ProblemController` 中提供了一个手动的 `/syncProblemToEs` 接口，可以随时触发全量数据从 MySQL 到 Elasticsearch 的同步。这个生产级的管理功能，为数据初始化和在极端情况下的数据不一致问题提供了一个可靠的最终修复手段。\n\n---\n\n## 附录：关键技术与设计决策\n\n### 1. 全局统一异常处理\n\n为了避免在每个 Controller 方法中都使用 `try-catch` 来处理异常，并向前端返回统一、规范的错误格式，项目采用了 Spring 的全局异常处理机制。\n\n- **实现**: 通过创建一个带有 `@RestControllerAdvice` 注解的类，并为不同类型的异常（如我们自定义的 `BadRequestException`, `UnauthorizedException`，以及通用的 `Exception`）编写被 `@ExceptionHandler` 注解标记的方法。\n- **优势**: 所有的业务异常都会被这个切面捕获，并被统一包装成 JSON 格式返回给前端。这极大地简化了业务代码，并保证了 API 错误响应的一致性。\n\n### 2. 双配置文件启动模型 (`bootstrap` vs `application`)\n\n在 Spring Cloud 环境中，我们为每个微服务都配置了 `bootstrap.yaml` 和 `application.yaml` 两个文件，它们在应用启动时有不同的加载时机和用途。\n\n- **`bootstrap.yaml` (引导上下文)**:\n    - **加载时机**: **优先于** `application.yaml` 加载。\n    - **核心用途**: 主要用于存放那些需要**引导应用、发现配置中心**的配置。在本项目中，它只存放两项最关键的信息：\n        1.  `spring.application.name`: 应用的名称，用于服务注册和配置拉取。\n        2.  `spring.cloud.nacos.server-addr`: Nacos 服务器的地址。\n    - **工作流程**: Spring Cloud 应用启动时，会先创建一个“引导上下文”，加载 `bootstrap.yaml`。通过这里面的配置，它知道了自己是谁、要去哪里找配置中心。然后，它会去 Nacos 拉取所有共享和私有的配置。\n\n- **`application.yaml` (应用上下文)**:\n    - **加载时机**: 在 `bootstrap` 阶段**之后**加载。\n    - **核心用途**: 用于存放应用的**常规业务配置**，例如服务器端口 (`server.port`)、数据库地址、Swagger 扫描路径等。这些配置的值可以被 Nacos 中的同名配置所覆盖。\n\n通过这种分离，我们确保了应用能够先找到“大本营”（Nacos），然后再从“大本营”获取详细的“作战计划”（业务配置），流程清晰且可靠。\n"
  },
  {
    "path": "docs/1.docker部署.md",
    "content": "## 安装docker\n\n部署网络\n\n```\ndocker network create doj\n```\n\n## MySQL\n\n```bash\nmkdir -p mysql/{data,conf,init}\n\ndocker run -d \\\n  --name mysql \\\n  -p 3306:3306 \\\n  -e TZ=Asia/Shanghai \\\n  -e MYSQL_ROOT_PASSWORD=123 \\\n  -v $(pwd)/mysql/data:/var/lib/mysql \\\n  -v $(pwd)/mysql/conf:/etc/mysql/conf.d \\\n  -v $(pwd)/mysql/init:/docker-entrypoint-initdb.d \\\n  --network doj\\\n  mysql\n```\n\n## nacos\n\n首先得在上面创建的mysql数据库中新建一个nacos表\n\n```sql\ncreate database nacos;\n```\n\n然后初始化一下待会要使用的表（参考`SQL/nacos.sql`）。\n\n接着再配置一下nacos的一些参数文件\n\n```bash\nmkdir nacos/custom.env\n\nPREFER_HOST_MODE=hostname\nMODE=standalone\nSPRING_DATASOURCE_PLATFORM=mysql\nMYSQL_SERVICE_HOST=172.18.0.2\nMYSQL_SERVICE_DB_NAME=nacos\nMYSQL_SERVICE_PORT=3306\nMYSQL_SERVICE_USER=root\nMYSQL_SERVICE_PASSWORD=123\nMYSQL_SERVICE_DB_PARAM=characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&allowPublicKeyRetrieval=true&queryTimeout=2400&serverTimezone=Asia/Shanghai\n```\n\n简单介绍一下上面这些参数：\n\n```properties\n//直译：首选主机模式\nPREFER_HOST_MODE=hostname\n//使用模式单机模式\nMODE=standalone\n//直译过来是spring数据源平台（由于我们用的是mysql所以不用改）\nSPRING_DATASOURCE_PLATFORM=mysql\n//mysql链接参数\nMYSQL_SERVICE_DB_PARAM=characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&allowPublicKeyRetrieval=true&queryTimeout=2400&serverTimezone=Asia/Shanghai\n//mysql主机地址\nMYSQL_SERVICE_HOST=mysql\n//mysql数据库名\nMYSQL_SERVICE_DB_NAME=nacos\n//mysql端口\nMYSQL_SERVICE_PORT=3306\n//mysql用户名\nMYSQL_SERVICE_USER=root\n//mysql密码\nMYSQL_SERVICE_PASSWORD=123\n```\n\n就是上面的`IPAddress`\n\n最后按照下面的命令运行，要注意设置`--network doj`\n\n即确保nacos可以访问到mysql（放在docker中同一个网络下）\n\n```bash\ndocker run -d \\\n    --name nacos \\\n    --env-file ./nacos/custom.env \\\n    -p 8848:8848 \\\n    -p 9848:9848 \\\n    -p 9849:9849 \\\n    --network doj\\\n    nacos/nacos-server:v2.5.1-slim\n```\n\n## Redis\n\n为了支持长短令牌认证和分布式缓存，项目需要 Redis 服务。\n\n1.  **创建本地目录和配置文件**\n\n    ```sh\n    # 创建用于挂载数据和配置的目录\n    mkdir -p redis/data\n    # 创建一个基础的配置文件\n    touch redis/redis.conf\n    ```\n    在 `redis/redis.conf` 中可以添加自定义配置，例如 `bind 0.0.0.0`。\n\n2.  **运行 Redis 容器**\n\n    ```sh\n    docker run -d \\\n      --name redis \\\n      -p 6379:6379 \\\n      -v ${PWD}/redis/data:/data \\\n      -v ${PWD}/redis/redis.conf:/usr/local/etc/redis/redis.conf \\\n      --network doj \\\n      redis:alpine redis-server /usr/local/etc/redis/redis.conf\n    ```\n\n    **命令解释**:\n    *   `redis:alpine`: 使用基于 Alpine Linux 的轻量级 Redis 镜像。\n    *   `-v ${PWD}/redis/data:/data`: 将主机当前目录下的 `redis/data` 文件夹挂载到容器内的 `/data` 目录，用于持久化存储 Redis 数据。\n    *   `-v ${PWD}/redis/redis.conf:/usr/local/etc/redis/redis.conf`: 将我们本地的配置文件挂载到容器内对应的位置。\n    *   `redis-server /usr/local/etc/redis/redis.conf`: 容器启动时，使用我们指定的配置文件来启动 Redis 服务。\n\n## RabbitMQ (消息队列)\n\n为了实现服务间的异步通信和解耦（例如，判题结果的更新），项目引入了 RabbitMQ。\n\n```sh\ndocker run -d \\\n  --name rabbitmq \\\n  -p 5672:5672 \\\n  -p 15672:15672 \\\n  --hostname my-rabbit \\\n  --network doj \\\n  rabbitmq:3-management\n```\n\n**命令解释**:\n*   `rabbitmq:3-management`: 使用包含 Web 管理界面的官方镜像。\n*   `-p 5672:5672`: 映射 AMQP 协议端口，供后端服务连接。\n*   `-p 15672:15672`: 映射 Web 管理后台端口。您可以通过 `http://localhost:15672` 访问它（默认用户名/密码: `guest`/`guest`）。\n\n## Elasticsearch (ES) 和 Kibana\n\nElasticsearch 作为核心的搜索引擎，Kibana 提供数据可视化界面。由于我们需要中文分词功能，因此需要构建一个包含 IK 分词插件的自定义 Elasticsearch 镜像。\n\n1.  **创建数据目录**:\n    ```sh\n    mkdir -p ./elasticsearch/data\n    ```\n\n2.  **构建带 IK 分词器的 Elasticsearch 镜像**:\n    首先，在项目根目录下创建一个 `Dockerfile` 文件:\n    \n    ```dockerfile\n    # 使用官方 Elasticsearch 7.17.6 作为基础镜像\n    FROM elasticsearch:7.17.6\n    \n    # 安装 IK 分词器插件（指定版本一致）\n    RUN bin/elasticsearch-plugin install --batch https://release.infinilabs.com/analysis-ik/stable/elasticsearch-analysis-ik-7.17.6.zip\n    \n    # 关闭安全认证 + 启动为单节点\n    ENV discovery.type=single-node\n    ENV xpack.security.enabled=false\n    ENV xpack.ml.enabled=false\n    ENV xpack.watcher.enabled=false\n    ENV xpack.license.self_generated.type=basic\n    \n    # 暴露端口\n    EXPOSE 9200 9300\n    \n    # 保持默认的 Elasticsearch 启动命令\n    ```\n    \n    然后，在包含此 `Dockerfile` 的目录下执行构建命令：\n    ```sh\n    docker build -t elasticsearch-with-ik:7.17.6 .\n    ```\n    \n3.  **启动 Elasticsearch (使用自定义镜像)**:\n    ```sh\n    docker run -d --name elasticsearch \\\n      -p 9200:9200 -p 9300:9300 \\\n      --memory=\"1g\" \\\n      -v $(pwd)/elasticsearch/data:/usr/share/elasticsearch/data \\\n      --network doj \\\n      elasticsearch-with-ik:7.17.6\n    ```\n    -   `--memory=1g`: 限制 ES 内存使用，避免 OOM。\n    \n4.  **启动 Kibana**:\n    ```sh\n    docker run -d --name kibana -p 5601:5601 \\\n      -e \"ELASTICSEARCH_HOSTS=http://elasticsearch:9200\" \\\n      --network doj \\\n      kibana:7.17.6\n    ```\n    -   `ELASTICSEARCH_HOSTS`: 指向 Elasticsearch 服务名和端口。\n\n## Sentinel (流量治理)\n\n项目使用 Sentinel 进行流量控制和熔断降级。\n\n```sh\ndocker run -d \\\n  --name sentinel \\\n  -p 8858:8858 \\\n  --network doj \\\n  bladex/sentinel-dashboard\n```\n\n**注意**: Sentinel Dashboard 默认端口为 `8080`，与我们的网关冲突。因此，这里将其映射到主机的 `8858` 端口。\n\n部署成功后，访问 `http://localhost:8858` 进入管理后台 (默认用户名/密码: `sentinel`/`sentinel`)。\n\n## 配置写入\n\n然后按照上面的配置依次修改每个微服务项目中的配置文件\n\n第一个：application.yaml\n\n```yaml\nserver:\n    port: ${doj.port.user-service}\ndoj:\n    db:\n        host: 127.0.0.1\n        name: doj_user\n        user: root\n        pwd: 123\n    swagger:\n        title: 用户服务接口文档\n        scan: com.decade.doj.user.controller\n```\n\n第二个：bootstrap.yaml\n\n```yaml\nspring:\n    application:\n        name: user-service\n    profiles:\n        active: dev, common\n    cloud:\n        nacos:\n            server-addr: 127.0.0.1:8848\n            config:\n                file-extension: yaml\n                shared-configs:\n                    - dataId: shared-jdbc.yaml\n                    - dataId: shared-swagger.yaml\n                    - dataId: shared-jwt.yaml\n                    - dataId: shared-rabbitmq.yaml\n```\n\n然后还有四个在nacos中共享的配置文件\n\n- shared-jdbc.yaml\n\n```yaml\nspring:\n    datasource:\n        driver-class-name: com.mysql.cj.jdbc.Driver\n        url: jdbc:mysql://${doj.db.host:127.0.0.1}:3306/${doj.db.name}\n        username: ${doj.db.user:root}\n        password: ${doj.db.pwd:123}\n  redis:\n    host: ${doj.redis.host:localhost}\n    port: 6379\n\nmybatis-plus:\n    global-config:\n        db-config:\n            update-strategy: not_null\n            id-type: auto\n```\n\n- shared-swagger.yaml\n\n```yaml\nlogging:\n    level:\n        com.decade: debug\n    pattern:\n        dateformat: HH:mm:ss:SSS\nknife4j:\n    enable: true\n    openapi:\n        title: ${doj.swagger.title}\n        description: \"Duck Online Judge API\"\n        email: \"decade-qzj@foxmail.com\"\n        version: v1.0.0\n        group:\n            default:\n                group-name: default\n                api-rule: package\n                api-rule-resources:\n                    - ${doj.swagger.scan}\n```\n\n- shared-jwt.yaml\n\n```yaml\ndoj:\n    jwt:\n        location: classpath:doj.jks\n        alias: decade\n        password: doj123\n        tokenTTL: 30m\n        refreshTokenTTL: 7d\n        authorization: \"authorization\"\n        secret-key: \"uid\"\n```\n\n\n-   shared-rabbitmq.yaml\n\n```yaml\nspring:\n    rabbitmq:\n        host: ${doj.mq.host:127.0.0.1}\n        port: 5672\n        username: guest\n        password: guest\n```"
  },
  {
    "path": "docs/2.Common项目配置.md",
    "content": "## JWT身份认证密钥生成\n\n在终端里执行，得到`jks`文件，放在resources下\n\n```sh\nkeytool -genkeypair -alias decade -keyalg RSA -keysize 2048 -validity 365 -keypass doj123 -keystore doj.jks\n```\n\n在application.yaml里配置参数\n\n要显示地写出打包后的路径\n\n> resource下的文件打包后都在`classpath:`下\n\n```yaml\njwt:\n    location: classpath:doj.jks\n    alias: decade\n    password: doj123\n    tokenTTL: 30m\n```\n\n## 静态资源存储地址映射\n\n```yaml\nresource:\n\t# location 对应的是 实际存储在磁盘上的位置\n    location: \"/share/files/\"\n    # request 对应的是 通过网页可以访问的url地址位置\n    # 例如 https://127.0.0.1:8080/static/avator.jpg\n    request: \"/static/\"\n\t\n    code-path: \"/share/codes/\"\n    problem-code-path: \"/share/problem-code/\"\n```\n\n## 微服务项目端口配置\n\n```yaml\nport:\n    gateway-service: 8080\n    user-service: 8081\n    sandbox-service: 8082\n    problem-service: 8083\n```\n"
  },
  {
    "path": "docs/3.Gateway项目配置.md",
    "content": "## application.yaml\n\n```yaml\nserver:\n    port: ${doj.port.gateway-service}\ndoj:\n    auth:\n        excludePaths:\n            - /\n            - /static/**\n            - /user/login\n            - /user/register\n            - /user/refresh\n```\n\nexcludePaths表示不需要进行路由拦截（过滤）的请求。\n\n因为gateway存在时，我们希望**所有的请求都需要经过gateway**，这样我们才能实现对所有访问微服务的请求都进行权限(身份)认证。\n\n我们在gateway中主要验证用户的JWT Token，后面user-service中用户登录时会绑定JWT Token。\n\n## bootstrap.yaml\n\n```yaml\nspring:\n    application:\n        name: gateway-service\n    profiles:\n        active: dev, common\n    cloud:\n        nacos:\n            server-addr: 127.0.0.1:8848\n            config:\n                file-extension: yaml\n                shared-configs:\n                    -   dataId: shared-jwt.yaml\n        gateway:\n            routes:\n                -   id: user\n                    uri: lb://user-service\n                    predicates:\n                        - Path=/user/**,/static/**\n                -   id: problem\n                    uri: lb://problem-service\n                    predicates:\n                        - Path=/problem/**\n                -   id: sandbox\n                    uri: lb://sandbox-service\n                    predicates:\n                        - Path=/sandbox/**\n                -   id: submission\n                    uri: lb://submission-service\n                    predicates:\n                        - Path=/submission/**\n```\n\n每个路由包含：\n\n- `id`: 路由 ID，唯一标识\n- `uri`: 服务地址，`lb://` 表示使用服务发现（LoadBalancer），服务名由注册中心（如 Nacos）识别\n- `predicates`: 路由谓词，决定哪些请求会命中该路由（比如根据路径）\n\n## Sentinel 配置\n\n为了实现流量治理，网关集成了 Sentinel，并将规则持久化到 Nacos。\n\n### `application.yaml` 配置\n\n```yaml\nspring:\n  cloud:\n    sentinel:\n      transport:\n        dashboard: localhost:8858 # 指向 Sentinel 控制台地址\n      datasource:\n        # 流控规则数据源\n        flow:\n          nacos:\n            server-addr: ${spring.cloud.nacos.server-addr}\n            data-id: gateway-sentinel-rules\n            rule-type: flow\n            # ...\n        # 熔断降级规则数据源\n        degrade:\n          nacos:\n            server-addr: ${spring.cloud.nacos.server-addr}\n            data-id: gateway-sentinel-degrade-rules\n            rule-type: degrade\n            # ...\n```\n\n- **`transport.dashboard`**: 配置 Sentinel 控制台的地址和端口。\n- **`datasource`**: 配置动态规则的数据源。我们配置了两个 Nacos 数据源，分别用于加载**流控规则** (`flow`) 和**熔断降级规则** (`degrade`)。\n\n### Nacos 中的规则示例\n\n- **`gateway-sentinel-rules` (流控)**: 用于配置 QPS 限制等规则。\n- **`gateway-sentinel-degrade-rules` (熔断)**: 用于配置慢调用、异常比例等熔断策略。\n\n通过这种方式，所有流量治理规则都实现了集中化、动态化和持久化管理。"
  },
  {
    "path": "docs/4.User-service项目配置.md",
    "content": "## 建库建表\n\n建库建表语句请参考 `docs/SQL/user.sql` 文件。\n\n> 插入用户密码时，由于数据库中保存的是用aes加密后的密文，所以”123“并不是最终的密码。\n>\n> 123对应的密文是\"1okxVZ0I1b0jrfTp7wyGpg==\"\n>\n> ```java\n> @Test\n> public void testAes() {\n>     String pods = \"123\";\n>     System.out.println(aesTool.encode(pods, aesTool.fnv1aHash(pods)));\n>     System.out.println(aesTool.match(pods, \"1okxVZ0I1b0jrfTp7wyGpg==\"));\n> }\n> ```"
  },
  {
    "path": "docs/5.Problem-service项目配置.md",
    "content": "## 建库建表\n\n参考`SQL/problem.sql`内容\n\n"
  },
  {
    "path": "docs/6.Submission-service项目配置.md",
    "content": "## 建库建表\n\n建库建表语句请参考 `docs/SQL/submission.sql` 文件。\n\n"
  },
  {
    "path": "docs/7.Sandbox-service项目配置.md",
    "content": "# Sandbox-Service: 异步判题引擎\n\n`sandbox-service` 是 D-OnlineJudge 的核心计算单元，它不再作为传统的 HTTP 服务存在，而是被设计成一个高性能、可水平扩展的**异步任务处理引擎**。\n\n---\n\n## 1. 架构角色\n\n`sandbox-service` 在新的架构中扮演两个角色：\n\n1.  **HTTP 接口提供者 (用于即时运行)**: 它保留了 `/code` 和 `/problem` 两个端点，用于处理需要立即返回结果的临时代码运行请求。这部分逻辑通过 `@Async` 注解在独立的线程池中执行，以避免阻塞 API 线程。\n\n2.  **分布式任务消费者 (用于提交验证)**: 这是它的核心职责。服务在启动后，会创建多个后台 **Worker 线程**，持续地从 Redis 的 `judging:queue` 任务队列中拉取判题任务并执行。\n\n---\n\n## 2. 核心工作流程 (异步验证)\n\n```mermaid\ngraph TD\n    A[Controller] -- 1. /validate 请求 --> B{构造 JudgingTask};\n    B -- 2. 推入 Redis 队列 --> C[(Redis List: judging:queue)];\n    \n    subgraph \"后台 Worker 线程池\"\n        D[Worker 1] -- 3. 阻塞式拉取 --> C;\n        E[Worker 2] -- 3. 阻塞式拉取 --> C;\n        F[Worker N] -- 3. 阻塞式拉取 --> C;\n    end\n\n    D -- 4. 执行判题 --> G{Docker 容器};\n    G -- 5. 返回结果 --> D;\n    D -- 6. 发布结果到 MQ --> H[(RabbitMQ: doj.topic)];\n```\n\n1.  **任务投递**: `SandboxController` 的 `/validate` 接口接收到请求后，仅负责创建 `Submission` 记录并获取 `submissionId`，然后将包含所有判题信息的 `JudgingTask` 对象推入 Redis 列表，并立即返回 `submissionId`。\n2.  **任务消费**: `JudgingWorker` 类在应用启动时，会根据配置（`doj.sandbox.worker-threads`）创建多个后台线程。每个线程都通过 `BRPOP` 命令阻塞式地等待任务队列。\n3.  **任务执行**: Worker 线程一旦获取到任务，就会调用同步的 `JudgingServiceImpl` 来执行完整的判题逻辑，包括创建临时文件、调用 Docker、解析结果等。\n4.  **结果广播**: 判题完成后，`JudgingServiceImpl` 会将包含 `submissionId` 和判题结果的消息，通过 `RabbitTemplate` 发送到 `doj.topic` 交换机，路由键为 `judging.result`。\n\n---\n\n## 3. 优势\n\n- **高吞吐量**: 提交接口（`/validate`）的响应时间被降至最低，因为它只执行一次远程调用和一次 Redis `LPUSH` 操作。\n- **可扩展性**: 当判题任务积压时，只需水平扩展 `sandbox-service` 的实例数量，新的实例会自动加入到消费者的行列中，共同处理任务，判题能力可以线性增长。\n- **高可用性**: 即使所有 `sandbox-service` 实例都宕机，未处理的判题任务依然安全地存储在 Redis 队列中，等待服务恢复后继续执行。"
  },
  {
    "path": "docs/SQL/doj_problem.sql",
    "content": "/*\n Navicat Premium Dump SQL\n\n Source Server         : doj\n Source Server Type    : MySQL\n Source Server Version : 90400 (9.4.0)\n Source Host           : localhost:3306\n Source Schema         : doj_problem\n\n Target Server Type    : MySQL\n Target Server Version : 90400 (9.4.0)\n File Encoding         : 65001\n\n Date: 25/01/2026 16:43:21\n*/\n\nSET NAMES utf8mb4;\nSET FOREIGN_KEY_CHECKS = 0;\n\n-- ----------------------------\n-- Table structure for problem\n-- ----------------------------\nDROP TABLE IF EXISTS `problem`;\nCREATE TABLE `problem` (\n  `id` bigint NOT NULL AUTO_INCREMENT,\n  `name` varchar(128) NOT NULL,\n  `description` mediumtext NOT NULL,\n  `input_style` text NOT NULL,\n  `output_style` text NOT NULL,\n  `input_sample` json NOT NULL,\n  `output_sample` json NOT NULL,\n  `difficulty` varchar(10) NOT NULL,\n  `time_limit` int NOT NULL,\n  `memory_limit` int NOT NULL,\n  `hint` text,\n  `total_pass` int NOT NULL DEFAULT '0',\n  `total_attempt` int NOT NULL DEFAULT '0',\n  `test_data` longtext NOT NULL,\n  `test_ans` longtext NOT NULL,\n  PRIMARY KEY (`id`),\n  KEY `idx_problem_difficulty` (`difficulty`)\n) ENGINE=InnoDB AUTO_INCREMENT=21 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;\n\n-- ----------------------------\n-- Table structure for problem_tag\n-- ----------------------------\nDROP TABLE IF EXISTS `problem_tag`;\nCREATE TABLE `problem_tag` (\n  `problem_id` bigint NOT NULL,\n  `tag_id` bigint NOT NULL,\n  PRIMARY KEY (`problem_id`,`tag_id`),\n  KEY `idx_tag_id` (`tag_id`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;\n\n-- ----------------------------\n-- Table structure for tag\n-- ----------------------------\nDROP TABLE IF EXISTS `tag`;\nCREATE TABLE `tag` (\n  `id` bigint NOT NULL AUTO_INCREMENT,\n  `name` varchar(64) NOT NULL,\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `name` (`name`)\n) ENGINE=InnoDB AUTO_INCREMENT=26 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;\n\nSET FOREIGN_KEY_CHECKS = 1;\n"
  },
  {
    "path": "docs/SQL/doj_submission.sql",
    "content": "/*\n Navicat Premium Dump SQL\n\n Source Server         : doj\n Source Server Type    : MySQL\n Source Server Version : 90400 (9.4.0)\n Source Host           : localhost:3306\n Source Schema         : doj_submission\n\n Target Server Type    : MySQL\n Target Server Version : 90400 (9.4.0)\n File Encoding         : 65001\n\n Date: 25/01/2026 16:43:29\n*/\n\nSET NAMES utf8mb4;\nSET FOREIGN_KEY_CHECKS = 0;\n\n-- ----------------------------\n-- Table structure for submission\n-- ----------------------------\nDROP TABLE IF EXISTS `submission`;\nCREATE TABLE `submission` (\n  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '提交记录主键',\n  `user_id` bigint NOT NULL COMMENT '用户ID（来源于 doj_user.user）',\n  `problem_id` bigint NOT NULL COMMENT '题目ID（来源于 doj_problem.problem）',\n  `user_name` varchar(12) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '用户名称',\n  `problem_name` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '题目名称',\n  `language` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT 'cpp' COMMENT '编程语言',\n  `code` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT '提交的代码文本内容',\n  `exit_value` int DEFAULT NULL COMMENT '程序退出码',\n  `status` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '判题状态',\n  `message` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT '判题详细信息',\n  `time` double DEFAULT NULL COMMENT '运行时间（单位：秒）',\n  `memory` bigint DEFAULT NULL COMMENT '内存使用（单位：KB）',\n  `submit_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '提交时间',\n  PRIMARY KEY (`id`)\n) ENGINE=InnoDB AUTO_INCREMENT=58 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='代码提交记录表';\n\nSET FOREIGN_KEY_CHECKS = 1;\n"
  },
  {
    "path": "docs/SQL/doj_user.sql",
    "content": "/*\n Navicat Premium Dump SQL\n\n Source Server         : doj\n Source Server Type    : MySQL\n Source Server Version : 90400 (9.4.0)\n Source Host           : localhost:3306\n Source Schema         : doj_user\n\n Target Server Type    : MySQL\n Target Server Version : 90400 (9.4.0)\n File Encoding         : 65001\n\n Date: 25/01/2026 16:43:37\n*/\n\nSET NAMES utf8mb4;\nSET FOREIGN_KEY_CHECKS = 0;\n\n-- ----------------------------\n-- Table structure for announcement\n-- ----------------------------\nDROP TABLE IF EXISTS `announcement`;\nCREATE TABLE `announcement` (\n  `id` bigint NOT NULL AUTO_INCREMENT,\n  `title` varchar(255) NOT NULL,\n  `content` text,\n  `create_time` datetime DEFAULT NULL,\n  `update_time` datetime DEFAULT NULL,\n  `creator_id` bigint DEFAULT NULL,\n  `deleted` tinyint(1) DEFAULT '0',\n  PRIMARY KEY (`id`)\n) ENGINE=InnoDB AUTO_INCREMENT=11 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;\n\n-- ----------------------------\n-- Table structure for user\n-- ----------------------------\nDROP TABLE IF EXISTS `user`;\nCREATE TABLE `user` (\n  `id` bigint NOT NULL AUTO_INCREMENT,\n  `username` varchar(12) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n  `avatar` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n  `email` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n  `password` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n  `score` int DEFAULT NULL,\n  `ranks` int DEFAULT NULL,\n  `school` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n  `gender` tinyint(1) DEFAULT NULL,\n  `easy_solve` int DEFAULT NULL,\n  `middle_solve` int DEFAULT NULL,\n  `hard_solve` int DEFAULT NULL,\n  `role` tinyint(1) DEFAULT NULL,\n  `url` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n  `sign` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci,\n  `fans` bigint DEFAULT NULL,\n  `subscribe` bigint DEFAULT NULL,\n  `ban` tinyint(1) DEFAULT NULL,\n  PRIMARY KEY (`id`)\n) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;\n\nSET FOREIGN_KEY_CHECKS = 1;\n"
  },
  {
    "path": "docs/SQL/nacos.sql",
    "content": "/*\n Navicat Premium Dump SQL\n\n Source Server         : doj\n Source Server Type    : MySQL\n Source Server Version : 90400 (9.4.0)\n Source Host           : localhost:3306\n Source Schema         : nacos\n\n Target Server Type    : MySQL\n Target Server Version : 90400 (9.4.0)\n File Encoding         : 65001\n\n Date: 25/01/2026 16:43:52\n*/\n\nSET NAMES utf8mb4;\nSET FOREIGN_KEY_CHECKS = 0;\n\n-- ----------------------------\n-- Table structure for config_info\n-- ----------------------------\nDROP TABLE IF EXISTS `config_info`;\nCREATE TABLE `config_info` (\n  `id` bigint NOT NULL AUTO_INCREMENT COMMENT 'id',\n  `data_id` varchar(255) COLLATE utf8mb3_bin NOT NULL COMMENT 'data_id',\n  `group_id` varchar(128) COLLATE utf8mb3_bin DEFAULT NULL COMMENT 'group_id',\n  `content` longtext COLLATE utf8mb3_bin NOT NULL COMMENT 'content',\n  `md5` varchar(32) COLLATE utf8mb3_bin DEFAULT NULL COMMENT 'md5',\n  `gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',\n  `gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',\n  `src_user` text COLLATE utf8mb3_bin COMMENT 'source user',\n  `src_ip` varchar(50) COLLATE utf8mb3_bin DEFAULT NULL COMMENT 'source ip',\n  `app_name` varchar(128) COLLATE utf8mb3_bin DEFAULT NULL COMMENT 'app_name',\n  `tenant_id` varchar(128) COLLATE utf8mb3_bin DEFAULT '' COMMENT '租户字段',\n  `c_desc` varchar(256) COLLATE utf8mb3_bin DEFAULT NULL COMMENT 'configuration description',\n  `c_use` varchar(64) COLLATE utf8mb3_bin DEFAULT NULL COMMENT 'configuration usage',\n  `effect` varchar(64) COLLATE utf8mb3_bin DEFAULT NULL COMMENT '配置生效的描述',\n  `type` varchar(64) COLLATE utf8mb3_bin DEFAULT NULL COMMENT '配置的类型',\n  `c_schema` text COLLATE utf8mb3_bin COMMENT '配置的模式',\n  `encrypted_data_key` varchar(1024) COLLATE utf8mb3_bin NOT NULL DEFAULT '' COMMENT '密钥',\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `uk_configinfo_datagrouptenant` (`data_id`,`group_id`,`tenant_id`)\n) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_bin COMMENT='config_info';\n\n-- ----------------------------\n-- Table structure for config_info_gray\n-- ----------------------------\nDROP TABLE IF EXISTS `config_info_gray`;\nCREATE TABLE `config_info_gray` (\n  `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT 'id',\n  `data_id` varchar(255) NOT NULL COMMENT 'data_id',\n  `group_id` varchar(128) NOT NULL COMMENT 'group_id',\n  `content` longtext NOT NULL COMMENT 'content',\n  `md5` varchar(32) DEFAULT NULL COMMENT 'md5',\n  `src_user` text COMMENT 'src_user',\n  `src_ip` varchar(100) DEFAULT NULL COMMENT 'src_ip',\n  `gmt_create` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT 'gmt_create',\n  `gmt_modified` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT 'gmt_modified',\n  `app_name` varchar(128) DEFAULT NULL COMMENT 'app_name',\n  `tenant_id` varchar(128) DEFAULT '' COMMENT 'tenant_id',\n  `gray_name` varchar(128) NOT NULL COMMENT 'gray_name',\n  `gray_rule` text NOT NULL COMMENT 'gray_rule',\n  `encrypted_data_key` varchar(256) NOT NULL DEFAULT '' COMMENT 'encrypted_data_key',\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `uk_configinfogray_datagrouptenantgray` (`data_id`,`group_id`,`tenant_id`,`gray_name`),\n  KEY `idx_dataid_gmt_modified` (`data_id`,`gmt_modified`),\n  KEY `idx_gmt_modified` (`gmt_modified`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COMMENT='config_info_gray';\n\n-- ----------------------------\n-- Table structure for config_tags_relation\n-- ----------------------------\nDROP TABLE IF EXISTS `config_tags_relation`;\nCREATE TABLE `config_tags_relation` (\n  `id` bigint NOT NULL COMMENT 'id',\n  `tag_name` varchar(128) COLLATE utf8mb3_bin NOT NULL COMMENT 'tag_name',\n  `tag_type` varchar(64) COLLATE utf8mb3_bin DEFAULT NULL COMMENT 'tag_type',\n  `data_id` varchar(255) COLLATE utf8mb3_bin NOT NULL COMMENT 'data_id',\n  `group_id` varchar(128) COLLATE utf8mb3_bin NOT NULL COMMENT 'group_id',\n  `tenant_id` varchar(128) COLLATE utf8mb3_bin DEFAULT '' COMMENT 'tenant_id',\n  `nid` bigint NOT NULL AUTO_INCREMENT COMMENT 'nid, 自增长标识',\n  PRIMARY KEY (`nid`),\n  UNIQUE KEY `uk_configtagrelation_configidtag` (`id`,`tag_name`,`tag_type`),\n  KEY `idx_tenant_id` (`tenant_id`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_bin COMMENT='config_tag_relation';\n\n-- ----------------------------\n-- Table structure for group_capacity\n-- ----------------------------\nDROP TABLE IF EXISTS `group_capacity`;\nCREATE TABLE `group_capacity` (\n  `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',\n  `group_id` varchar(128) COLLATE utf8mb3_bin NOT NULL DEFAULT '' COMMENT 'Group ID，空字符表示整个集群',\n  `quota` int unsigned NOT NULL DEFAULT '0' COMMENT '配额，0表示使用默认值',\n  `usage` int unsigned NOT NULL DEFAULT '0' COMMENT '使用量',\n  `max_size` int unsigned NOT NULL DEFAULT '0' COMMENT '单个配置大小上限，单位为字节，0表示使用默认值',\n  `max_aggr_count` int unsigned NOT NULL DEFAULT '0' COMMENT '聚合子配置最大个数，，0表示使用默认值',\n  `max_aggr_size` int unsigned NOT NULL DEFAULT '0' COMMENT '单个聚合数据的子配置大小上限，单位为字节，0表示使用默认值',\n  `max_history_count` int unsigned NOT NULL DEFAULT '0' COMMENT '最大变更历史数量',\n  `gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',\n  `gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `uk_group_id` (`group_id`)\n) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_bin COMMENT='集群、各Group容量信息表';\n\n-- ----------------------------\n-- Table structure for his_config_info\n-- ----------------------------\nDROP TABLE IF EXISTS `his_config_info`;\nCREATE TABLE `his_config_info` (\n  `id` bigint unsigned NOT NULL COMMENT 'id',\n  `nid` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT 'nid, 自增标识',\n  `data_id` varchar(255) COLLATE utf8mb3_bin NOT NULL COMMENT 'data_id',\n  `group_id` varchar(128) COLLATE utf8mb3_bin NOT NULL COMMENT 'group_id',\n  `app_name` varchar(128) COLLATE utf8mb3_bin DEFAULT NULL COMMENT 'app_name',\n  `content` longtext COLLATE utf8mb3_bin NOT NULL COMMENT 'content',\n  `md5` varchar(32) COLLATE utf8mb3_bin DEFAULT NULL COMMENT 'md5',\n  `gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',\n  `gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',\n  `src_user` text COLLATE utf8mb3_bin COMMENT 'source user',\n  `src_ip` varchar(50) COLLATE utf8mb3_bin DEFAULT NULL COMMENT 'source ip',\n  `op_type` char(10) COLLATE utf8mb3_bin DEFAULT NULL COMMENT 'operation type',\n  `tenant_id` varchar(128) COLLATE utf8mb3_bin DEFAULT '' COMMENT '租户字段',\n  `encrypted_data_key` varchar(1024) COLLATE utf8mb3_bin NOT NULL DEFAULT '' COMMENT '密钥',\n  `publish_type` varchar(50) COLLATE utf8mb3_bin DEFAULT 'formal' COMMENT 'publish type gray or formal',\n  `gray_name` varchar(50) COLLATE utf8mb3_bin DEFAULT NULL COMMENT 'gray name',\n  `ext_info` longtext COLLATE utf8mb3_bin COMMENT 'ext info',\n  PRIMARY KEY (`nid`),\n  KEY `idx_gmt_create` (`gmt_create`),\n  KEY `idx_gmt_modified` (`gmt_modified`),\n  KEY `idx_did` (`data_id`)\n) ENGINE=InnoDB AUTO_INCREMENT=15 DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_bin COMMENT='多租户改造';\n\n-- ----------------------------\n-- Table structure for permissions\n-- ----------------------------\nDROP TABLE IF EXISTS `permissions`;\nCREATE TABLE `permissions` (\n  `role` varchar(50) NOT NULL COMMENT 'role',\n  `resource` varchar(128) NOT NULL COMMENT 'resource',\n  `action` varchar(8) NOT NULL COMMENT 'action',\n  UNIQUE KEY `uk_role_permission` (`role`,`resource`,`action`) USING BTREE\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;\n\n-- ----------------------------\n-- Table structure for roles\n-- ----------------------------\nDROP TABLE IF EXISTS `roles`;\nCREATE TABLE `roles` (\n  `username` varchar(50) NOT NULL COMMENT 'username',\n  `role` varchar(50) NOT NULL COMMENT 'role',\n  UNIQUE KEY `idx_user_role` (`username`,`role`) USING BTREE\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;\n\n-- ----------------------------\n-- Table structure for tenant_capacity\n-- ----------------------------\nDROP TABLE IF EXISTS `tenant_capacity`;\nCREATE TABLE `tenant_capacity` (\n  `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',\n  `tenant_id` varchar(128) COLLATE utf8mb3_bin NOT NULL DEFAULT '' COMMENT 'Tenant ID',\n  `quota` int unsigned NOT NULL DEFAULT '0' COMMENT '配额，0表示使用默认值',\n  `usage` int unsigned NOT NULL DEFAULT '0' COMMENT '使用量',\n  `max_size` int unsigned NOT NULL DEFAULT '0' COMMENT '单个配置大小上限，单位为字节，0表示使用默认值',\n  `max_aggr_count` int unsigned NOT NULL DEFAULT '0' COMMENT '聚合子配置最大个数',\n  `max_aggr_size` int unsigned NOT NULL DEFAULT '0' COMMENT '单个聚合数据的子配置大小上限，单位为字节，0表示使用默认值',\n  `max_history_count` int unsigned NOT NULL DEFAULT '0' COMMENT '最大变更历史数量',\n  `gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',\n  `gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `uk_tenant_id` (`tenant_id`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_bin COMMENT='租户容量信息表';\n\n-- ----------------------------\n-- Table structure for tenant_info\n-- ----------------------------\nDROP TABLE IF EXISTS `tenant_info`;\nCREATE TABLE `tenant_info` (\n  `id` bigint NOT NULL AUTO_INCREMENT COMMENT 'id',\n  `kp` varchar(128) COLLATE utf8mb3_bin NOT NULL COMMENT 'kp',\n  `tenant_id` varchar(128) COLLATE utf8mb3_bin DEFAULT '' COMMENT 'tenant_id',\n  `tenant_name` varchar(128) COLLATE utf8mb3_bin DEFAULT '' COMMENT 'tenant_name',\n  `tenant_desc` varchar(256) COLLATE utf8mb3_bin DEFAULT NULL COMMENT 'tenant_desc',\n  `create_source` varchar(32) COLLATE utf8mb3_bin DEFAULT NULL COMMENT 'create_source',\n  `gmt_create` bigint NOT NULL COMMENT '创建时间',\n  `gmt_modified` bigint NOT NULL COMMENT '修改时间',\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `uk_tenant_info_kptenantid` (`kp`,`tenant_id`),\n  KEY `idx_tenant_id` (`tenant_id`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_bin COMMENT='tenant_info';\n\n-- ----------------------------\n-- Table structure for users\n-- ----------------------------\nDROP TABLE IF EXISTS `users`;\nCREATE TABLE `users` (\n  `username` varchar(50) NOT NULL COMMENT 'username',\n  `password` varchar(500) NOT NULL COMMENT 'password',\n  `enabled` tinyint(1) NOT NULL COMMENT 'enabled',\n  PRIMARY KEY (`username`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;\n\nSET FOREIGN_KEY_CHECKS = 1;\n"
  },
  {
    "path": "prometheus/grafana-dashboard.json",
    "content": "{\n  \"__inputs\": [\n    {\n      \"name\": \"DS_PROMETHEUS\",\n      \"label\": \"Prometheus\",\n      \"description\": \"\",\n      \"type\": \"datasource\",\n      \"pluginId\": \"prometheus\",\n      \"pluginName\": \"Prometheus\"\n    }\n  ],\n  \"__requires\": [\n    {\n      \"type\": \"grafana\",\n      \"id\": \"grafana\",\n      \"name\": \"Grafana\",\n      \"version\": \"6.2.5\"\n    },\n    {\n      \"type\": \"datasource\",\n      \"id\": \"prometheus\",\n      \"name\": \"Prometheus\",\n      \"version\": \"1.0.0\"\n    }\n  ],\n  \"annotations\": {\n    \"list\": [\n      {\n        \"builtIn\": 1,\n        \"datasource\": {\n          \"type\": \"grafana\",\n          \"uid\": \"-- Grafana --\"\n        },\n        \"enable\": true,\n        \"hide\": true,\n        \"iconColor\": \"rgba(0, 211, 255, 1)\",\n        \"name\": \"Annotations & Alerts\",\n        \"type\": \"dashboard\"\n      }\n    ]\n  },\n  \"editable\": true,\n  \"gnetId\": 4701,\n  \"graphTooltip\": 0,\n  \"id\": null,\n  \"iteration\": 1675179939092,\n  \"links\": [],\n  \"panels\": [\n    {\n      \"collapsed\": false,\n      \"datasource\": \"Prometheus\",\n      \"gridPos\": {\n        \"h\": 1,\n        \"w\": 24,\n        \"x\": 0,\n        \"y\": 0\n      },\n      \"id\": 19,\n      \"panels\": [],\n      \"title\": \"System\",\n      \"type\": \"row\"\n    },\n    {\n      \"aliasColors\": {},\n      \"bars\": false,\n      \"dashLength\": 10,\n      \"dashes\": false,\n      \"datasource\": \"Prometheus\",\n      \"fill\": 1,\n      \"gridPos\": {\n        \"h\": 7,\n        \"w\": 12,\n        \"x\": 0,\n        \"y\": 1\n      },\n      \"id\": 1,\n      \"legend\": {\n        \"alignAsTable\": true,\n        \"avg\": false,\n        \"current\": true,\n        \"max\": false,\n        \"min\": false,\n        \"rightSide\": true,\n        \"show\": true,\n        \"total\": false,\n        \"values\": true\n      },\n      \"lines\": true,\n      \"linewidth\": 1,\n      \"links\": [],\n      \"nullPointMode\": \"null as zero\",\n      \"percentage\": false,\n      \"pointradius\": 5,\n      \"points\": false,\n      \"renderer\": \"flot\",\n      \"seriesOverrides\": [],\n      \"spaceLength\": 10,\n      \"stack\": false,\n      \"steppedLine\": false,\n      \"targets\": [\n        {\n          \"expr\": \"sum(rate(process_cpu_usage{job=\\\"doj-services\\\"}[1m])) by (service)\",\n          \"format\": \"time_series\",\n          \"intervalFactor\": 1,\n          \"legendFormat\": \"{{service}}\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"thresholds\": [],\n      \"timeFrom\": null,\n      \"timeShift\": null,\n      \"title\": \"CPU Usage\",\n      \"tooltip\": {\n        \"shared\": true,\n        \"sort\": 0,\n        \"value_type\": \"individual\"\n      },\n      \"type\": \"graph\",\n      \"xaxis\": {\n        \"buckets\": null,\n        \"mode\": \"time\",\n        \"name\": null,\n        \"show\": true,\n        \"values\": []\n      },\n      \"yaxes\": [\n        {\n          \"format\": \"percentunit\",\n          \"label\": null,\n          \"logBase\": 1,\n          \"max\": \"1\",\n          \"min\": \"0\",\n          \"show\": true\n        },\n        {\n          \"format\": \"short\",\n          \"label\": null,\n          \"logBase\": 1,\n          \"max\": null,\n          \"min\": null,\n          \"show\": true\n        }\n      ]\n    },\n    {\n      \"aliasColors\": {},\n      \"bars\": false,\n      \"dashLength\": 10,\n      \"dashes\": false,\n      \"datasource\": \"Prometheus\",\n      \"fill\": 1,\n      \"gridPos\": {\n        \"h\": 7,\n        \"w\": 12,\n        \"x\": 12,\n        \"y\": 1\n      },\n      \"id\": 2,\n      \"legend\": {\n        \"alignAsTable\": true,\n        \"avg\": false,\n        \"current\": true,\n        \"max\": false,\n        \"min\": false,\n        \"rightSide\": true,\n        \"show\": true,\n        \"total\": false,\n        \"values\": true\n      },\n      \"lines\": true,\n      \"linewidth\": 1,\n      \"links\": [],\n      \"nullPointMode\": \"null as zero\",\n      \"percentage\": false,\n      \"pointradius\": 5,\n      \"points\": false,\n      \"renderer\": \"flot\",\n      \"seriesOverrides\": [],\n      \"spaceLength\": 10,\n      \"stack\": false,\n      \"steppedLine\": false,\n      \"targets\": [\n        {\n          \"expr\": \"sum(system_cpu_usage{job=\\\"doj-services\\\"}) by (service)\",\n          \"format\": \"time_series\",\n          \"intervalFactor\": 1,\n          \"legendFormat\": \"{{service}}\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"thresholds\": [],\n      \"timeFrom\": null,\n      \"timeShift\": null,\n      \"title\": \"System CPU Usage\",\n      \"tooltip\": {\n        \"shared\": true,\n        \"sort\": 0,\n        \"value_type\": \"individual\"\n      },\n      \"type\": \"graph\",\n      \"xaxis\": {\n        \"buckets\": null,\n        \"mode\": \"time\",\n        \"name\": null,\n        \"show\": true,\n        \"values\": []\n      },\n      \"yaxes\": [\n        {\n          \"format\": \"percentunit\",\n          \"label\": null,\n          \"logBase\": 1,\n          \"max\": \"1\",\n          \"min\": \"0\",\n          \"show\": true\n        },\n        {\n          \"format\": \"short\",\n          \"label\": null,\n          \"logBase\": 1,\n          \"max\": null,\n          \"min\": null,\n          \"show\": true\n        }\n      ]\n    },\n    {\n      \"collapsed\": false,\n      \"datasource\": \"Prometheus\",\n      \"gridPos\": {\n        \"h\": 1,\n        \"w\": 24,\n        \"x\": 0,\n        \"y\": 8\n      },\n      \"id\": 20,\n      \"panels\": [],\n      \"title\": \"JVM\",\n      \"type\": \"row\"\n    },\n    {\n      \"aliasColors\": {},\n      \"bars\": false,\n      \"dashLength\": 10,\n      \"dashes\": false,\n      \"datasource\": \"Prometheus\",\n      \"fill\": 1,\n      \"gridPos\": {\n        \"h\": 8,\n        \"w\": 24,\n        \"x\": 0,\n        \"y\": 9\n      },\n      \"id\": 3,\n      \"legend\": {\n        \"alignAsTable\": true,\n        \"avg\": false,\n        \"current\": true,\n        \"max\": false,\n        \"min\": false,\n        \"rightSide\": true,\n        \"show\": true,\n        \"total\": false,\n        \"values\": true\n      },\n      \"lines\": true,\n      \"linewidth\": 1,\n      \"links\": [],\n      \"nullPointMode\": \"null\",\n      \"percentage\": false,\n      \"pointradius\": 5,\n      \"points\": false,\n      \"renderer\": \"flot\",\n      \"seriesOverrides\": [],\n      \"spaceLength\": 10,\n      \"stack\": true,\n      \"steppedLine\": false,\n      \"targets\": [\n        {\n          \"expr\": \"sum(jvm_memory_used_bytes{job=\\\"doj-services\\\"}) by (service, area)\",\n          \"format\": \"time_series\",\n          \"intervalFactor\": 1,\n          \"legendFormat\": \"{{service}} - {{area}}\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"thresholds\": [],\n      \"timeFrom\": null,\n      \"timeShift\": null,\n      \"title\": \"JVM Memory\",\n      \"tooltip\": {\n        \"shared\": true,\n        \"sort\": 0,\n        \"value_type\": \"individual\"\n      },\n      \"type\": \"graph\",\n      \"xaxis\": {\n        \"buckets\": null,\n        \"mode\": \"time\",\n        \"name\": null,\n        \"show\": true,\n        \"values\": []\n      },\n      \"yaxes\": [\n        {\n          \"format\": \"bytes\",\n          \"label\": null,\n          \"logBase\": 1,\n          \"max\": null,\n          \"min\": \"0\",\n          \"show\": true\n        },\n        {\n          \"format\": \"short\",\n          \"label\": null,\n          \"logBase\": 1,\n          \"max\": null,\n          \"min\": null,\n          \"show\": true\n        }\n      ]\n    },\n    {\n      \"aliasColors\": {},\n      \"bars\": false,\n      \"dashLength\": 10,\n      \"dashes\": false,\n      \"datasource\": \"Prometheus\",\n      \"fill\": 1,\n      \"gridPos\": {\n        \"h\": 8,\n        \"w\": 12,\n        \"x\": 0,\n        \"y\": 17\n      },\n      \"id\": 4,\n      \"legend\": {\n        \"alignAsTable\": true,\n        \"avg\": false,\n        \"current\": true,\n        \"max\": false,\n        \"min\": false,\n        \"rightSide\": true,\n        \"show\": true,\n        \"total\": false,\n        \"values\": true\n      },\n      \"lines\": true,\n      \"linewidth\": 1,\n      \"links\": [],\n      \"nullPointMode\": \"null as zero\",\n      \"percentage\": false,\n      \"pointradius\": 5,\n      \"points\": false,\n      \"renderer\": \"flot\",\n      \"seriesOverrides\": [],\n      \"spaceLength\": 10,\n      \"stack\": false,\n      \"steppedLine\": false,\n      \"targets\": [\n        {\n          \"expr\": \"sum(rate(jvm_gc_pause_seconds_sum{job=\\\"doj-services\\\"}[1m])) by (service, action)\",\n          \"format\": \"time_series\",\n          \"intervalFactor\": 1,\n          \"legendFormat\": \"{{service}} - {{action}}\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"thresholds\": [],\n      \"timeFrom\": null,\n      \"timeShift\": null,\n      \"title\": \"JVM GC Pause\",\n      \"tooltip\": {\n        \"shared\": true,\n        \"sort\": 0,\n        \"value_type\": \"individual\"\n      },\n      \"type\": \"graph\",\n      \"xaxis\": {\n        \"buckets\": null,\n        \"mode\": \"time\",\n        \"name\": null,\n        \"show\": true,\n        \"values\": []\n      },\n      \"yaxes\": [\n        {\n          \"format\": \"s\",\n          \"label\": null,\n          \"logBase\": 1,\n          \"max\": null,\n          \"min\": \"0\",\n          \"show\": true\n        },\n        {\n          \"format\": \"short\",\n          \"label\": null,\n          \"logBase\": 1,\n          \"max\": null,\n          \"min\": null,\n          \"show\": true\n        }\n      ]\n    },\n    {\n      \"aliasColors\": {},\n      \"bars\": false,\n      \"dashLength\": 10,\n      \"dashes\": false,\n      \"datasource\": \"Prometheus\",\n      \"fill\": 1,\n      \"gridPos\": {\n        \"h\": 8,\n        \"w\": 12,\n        \"x\": 12,\n        \"y\": 17\n      },\n      \"id\": 5,\n      \"legend\": {\n        \"alignAsTable\": true,\n        \"avg\": false,\n        \"current\": true,\n        \"max\": false,\n        \"min\": false,\n        \"rightSide\": true,\n        \"show\": true,\n        \"total\": false,\n        \"values\": true\n      },\n      \"lines\": true,\n      \"linewidth\": 1,\n      \"links\": [],\n      \"nullPointMode\": \"null as zero\",\n      \"percentage\": false,\n      \"pointradius\": 5,\n      \"points\": false,\n      \"renderer\": \"flot\",\n      \"seriesOverrides\": [],\n      \"spaceLength\": 10,\n      \"stack\": false,\n      \"steppedLine\": false,\n      \"targets\": [\n        {\n          \"expr\": \"sum(jvm_threads_live_threads{job=\\\"doj-services\\\"}) by (service)\",\n          \"format\": \"time_series\",\n          \"intervalFactor\": 1,\n          \"legendFormat\": \"{{service}}\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"thresholds\": [],\n      \"timeFrom\": null,\n      \"timeShift\": null,\n      \"title\": \"JVM Threads\",\n      \"tooltip\": {\n        \"shared\": true,\n        \"sort\": 0,\n        \"value_type\": \"individual\"\n      },\n      \"type\": \"graph\",\n      \"xaxis\": {\n        \"buckets\": null,\n        \"mode\": \"time\",\n        \"name\": null,\n        \"show\": true,\n        \"values\": []\n      },\n      \"yaxes\": [\n        {\n          \"format\": \"short\",\n          \"label\": null,\n          \"logBase\": 1,\n          \"max\": null,\n          \"min\": \"0\",\n          \"show\": true\n        },\n        {\n          \"format\": \"short\",\n          \"label\": null,\n          \"logBase\": 1,\n          \"max\": null,\n          \"min\": null,\n          \"show\": true\n        }\n      ]\n    },\n    {\n      \"collapsed\": false,\n      \"datasource\": \"Prometheus\",\n      \"gridPos\": {\n        \"h\": 1,\n        \"w\": 24,\n        \"x\": 0,\n        \"y\": 25\n      },\n      \"id\": 21,\n      \"panels\": [],\n      \"title\": \"HTTP\",\n      \"type\": \"row\"\n    },\n    {\n      \"aliasColors\": {},\n      \"bars\": false,\n      \"dashLength\": 10,\n      \"dashes\": false,\n      \"datasource\": \"Prometheus\",\n      \"fill\": 1,\n      \"gridPos\": {\n        \"h\": 8,\n        \"w\": 24,\n        \"x\": 0,\n        \"y\": 26\n      },\n      \"id\": 6,\n      \"legend\": {\n        \"alignAsTable\": true,\n        \"avg\": false,\n        \"current\": true,\n        \"max\": false,\n        \"min\": false,\n        \"rightSide\": true,\n        \"show\": true,\n        \"total\": false,\n        \"values\": true\n      },\n      \"lines\": true,\n      \"linewidth\": 1,\n      \"links\": [],\n      \"nullPointMode\": \"null as zero\",\n      \"percentage\": false,\n      \"pointradius\": 5,\n      \"points\": false,\n      \"renderer\": \"flot\",\n      \"seriesOverrides\": [],\n      \"spaceLength\": 10,\n      \"stack\": false,\n      \"steppedLine\": false,\n      \"targets\": [\n        {\n          \"expr\": \"sum(rate(http_server_requests_seconds_count{job=\\\"doj-services\\\", uri!=\\\"/actuator/prometheus\\\"}[1m])) by (service, uri, status)\",\n          \"format\": \"time_series\",\n          \"intervalFactor\": 1,\n          \"legendFormat\": \"{{service}} - {{uri}} - {{status}}\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"thresholds\": [],\n      \"timeFrom\": null,\n      \"timeShift\": null,\n      \"title\": \"HTTP Requests\",\n      \"tooltip\": {\n        \"shared\": true,\n        \"sort\": 0,\n        \"value_type\": \"individual\"\n      },\n      \"type\": \"graph\",\n      \"xaxis\": {\n        \"buckets\": null,\n        \"mode\": \"time\",\n        \"name\": null,\n        \"show\": true,\n        \"values\": []\n      },\n      \"yaxes\": [\n        {\n          \"format\": \"reqps\",\n          \"label\": null,\n          \"logBase\": 1,\n          \"max\": null,\n          \"min\": \"0\",\n          \"show\": true\n        },\n        {\n          \"format\": \"short\",\n          \"label\": null,\n          \"logBase\": 1,\n          \"max\": null,\n          \"min\": null,\n          \"show\": true\n        }\n      ]\n    },\n    {\n      \"aliasColors\": {},\n      \"bars\": false,\n      \"dashLength\": 10,\n      \"dashes\": false,\n      \"datasource\": \"Prometheus\",\n      \"fill\": 1,\n      \"gridPos\": {\n        \"h\": 8,\n        \"w\": 24,\n        \"x\": 0,\n        \"y\": 34\n      },\n      \"id\": 7,\n      \"legend\": {\n        \"alignAsTable\": true,\n        \"avg\": false,\n        \"current\": true,\n        \"max\": false,\n        \"min\": false,\n        \"rightSide\": true,\n        \"show\": true,\n        \"total\": false,\n        \"values\": true\n      },\n      \"lines\": true,\n      \"linewidth\": 1,\n      \"links\": [],\n      \"nullPointMode\": \"null as zero\",\n      \"percentage\": false,\n      \"pointradius\": 5,\n      \"points\": false,\n      \"renderer\": \"flot\",\n      \"seriesOverrides\": [],\n      \"spaceLength\": 10,\n      \"stack\": false,\n      \"steppedLine\": false,\n      \"targets\": [\n        {\n          \"expr\": \"histogram_quantile(0.95, sum(rate(http_server_requests_seconds_bucket{job=\\\"doj-services\\\", uri!=\\\"/actuator/prometheus\\\"}[1m])) by (le, service, uri))\",\n          \"format\": \"time_series\",\n          \"intervalFactor\": 1,\n          \"legendFormat\": \"{{service}} - {{uri}}\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"thresholds\": [],\n      \"timeFrom\": null,\n      \"timeShift\": null,\n      \"title\": \"HTTP Requests P95 Latency\",\n      \"tooltip\": {\n        \"shared\": true,\n        \"sort\": 0,\n        \"value_type\": \"individual\"\n      },\n      \"type\": \"graph\",\n      \"xaxis\": {\n        \"buckets\": null,\n        \"mode\": \"time\",\n        \"name\": null,\n        \"show\": true,\n        \"values\": []\n      },\n      \"yaxes\": [\n        {\n          \"format\": \"s\",\n          \"label\": null,\n          \"logBase\": 1,\n          \"max\": null,\n          \"min\": \"0\",\n          \"show\": true\n        },\n        {\n          \"format\": \"short\",\n          \"label\": null,\n          \"logBase\": 1,\n          \"max\": null,\n          \"min\": null,\n          \"show\": true\n        }\n      ]\n    }\n  ],\n  \"refresh\": \"10s\",\n  \"schemaVersion\": 16,\n  \"style\": \"dark\",\n  \"tags\": [\"spring\", \"actuator\", \"prometheus\"],\n  \"templating\": {\n    \"list\": []\n  },\n  \"time\": {\n    \"from\": \"now-5m\",\n    \"to\": \"now\"\n  },\n  \"timepicker\": {\n    \"refresh_intervals\": [\n      \"5s\",\n      \"10s\",\n      \"30s\",\n      \"1m\",\n      \"5m\",\n      \"15m\",\n      \"30m\",\n      \"1h\",\n      \"2h\",\n      \"1d\"\n    ],\n    \"time_options\": [\n      \"5m\",\n      \"15m\",\n      \"1h\",\n      \"6h\",\n      \"12h\",\n      \"24h\",\n      \"2d\",\n      \"7d\",\n      \"30d\"\n    ]\n  },\n  \"timezone\": \"\",\n  \"title\": \"Spring Boot 2.1 Statistics\",\n  \"version\": 10\n}"
  },
  {
    "path": "prometheus/prometheus.yml",
    "content": "global:\n  scrape_interval: 15s\n\nscrape_configs:\n  - job_name: 'doj-services'\n    metrics_path: /actuator/prometheus\n    static_configs:\n      - targets: [\"host.docker.internal:8080\"]\n        labels:\n          service: \"gateway-service\"\n      - targets: [\"host.docker.internal:8081\"]\n        labels:\n          service: \"user-service\"\n      - targets: [\"host.docker.internal:8082\"]\n        labels:\n          service: \"sandbox-service\"\n      - targets: [\"host.docker.internal:8083\"]\n        labels:\n          service: \"problem-service\"\n      - targets: [\"host.docker.internal:8084\"]\n        labels:\n          service: \"submission-service\""
  },
  {
    "path": "prometheus/promtail-config.yml",
    "content": "server:\n  http_listen_port: 9080\n  grpc_listen_port: 0\n\npositions:\n  filename: /tmp/positions.yaml\n\nclients:\n  - url: http://loki:3100/loki/api/v1/push\n\nscrape_configs:\n  - job_name: containers\n    docker_sd_configs:\n      - host: unix:///var/run/docker.sock\n        refresh_interval: 5s\n    relabel_configs:\n      - source_labels: [__meta_docker_container_name]\n        regex: /(.+)\n        target_label: container\n      - source_labels: [__meta_docker_container_log_stream]\n        target_label: stream\n"
  }
]