From 1945cc2fb1e620eba28c46af4569af49865b2805 Mon Sep 17 00:00:00 2001 From: meowrain Date: Wed, 28 Jan 2026 23:01:48 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E5=91=98=E6=9D=83=E9=99=90=E6=A3=80=E6=9F=A5=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E5=92=8CMaven=E6=89=93=E5=8C=85=E9=85=8D=E7=BD=AE=E4=BC=98?= =?UTF-8?q?=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 主要更新: 1. 新增管理员权限检查功能 - 添加 UserRoleEnum 枚举类统一管理用户角色(USER, ADMIN, BAN) - 改进 ContextHolderUtils.isAdmin() 方法,支持不区分大小写的角色比较 - 更新 UserServiceImpl 使用枚举常量代替硬编码字符串 - 新增管理员权限使用指南文档 (docs/admin-permission-guide.md) 2. 修复Maven打包配置 - 配置根POM的spring-boot-maven-plugin默认跳过repackage - 为所有服务模块启用repackage,确保可以打包为可执行JAR - 修复公共库模块打包失败的问题 - 涉及服务:gateway, auth, user-service, question-service, file-service, blog-service, upms-biz, ai-service 3. 更新项目文档 - README.md:添加详细的打包说明、首次克隆准备工作、服务启动顺序等 - CLAUDE.md:更新项目架构说明和开发指南 4. 重构题目服务责任链结构 - 将责任链类按功能分类到 question/ 和 submit/ 子目录 - 新增 QuestionSubmitJudgeInfoEnum 和相关查询功能 - 改进题目提交服务的实现 5. 其他改进 - 添加 Feign Token 中继拦截器 - 更新 AsyncConfig 配置 - 优化 Jackson 和 Security 配置 --- CLAUDE.md | 301 ++++++++++++++++-- README.md | 291 ++++++++++++++++- aioj-backend-ai-service/pom.xml | 9 + aioj-backend-auth/pom.xml | 13 + aioj-backend-blog-service/pom.xml | 13 + .../framework/core/enums/UserRoleEnum.java | 64 ++++ .../core/jackson/JacksonConfiguration.java | 5 +- .../core/utils/ContextHolderUtils.java | 9 +- .../feign/FeignAutoConfiguration.java | 8 + .../TokenRelayRequestInterceptor.java | 60 ++++ .../filter/JwtAuthenticationFilter.java | 3 + aioj-backend-file-service/pom.xml | 13 + .../fileservice/config/AsyncConfig.java | 2 + aioj-backend-gateway/pom.xml | 13 + aioj-backend-question-service/pom.xml | 23 ++ .../common/constants/RedisKeyConstants.java | 5 + .../enums/QuestionSubmitJudgeInfoEnum.java | 82 +++++ .../enums/QuestionSubmitStatusEnum.java | 18 ++ .../controller/QuestionSubmitController.java | 15 +- .../QuestionContentVerifyChain.java | 3 +- .../QuestionDifficultyVerifyChain.java | 3 +- .../QuestionEditContentVerifyChain.java | 3 +- .../QuestionEditDifficultyVerifyChain.java | 3 +- .../QuestionEditJudgeConfigVerifyChain.java | 3 +- .../QuestionEditTagsVerifyChain.java | 3 +- .../QuestionEditTitleVerifyChain.java | 3 +- .../QuestionJudgeConfigVerifyChain.java | 3 +- .../QuestionTagsVerifyChain.java | 3 +- .../QuestionTitleVerifyChain.java | 3 +- .../QuestionUpdateContentVerifyChain.java | 3 +- .../QuestionUpdateDifficultyVerifyChain.java | 3 +- .../QuestionUpdateExistVerifyChain.java | 3 +- .../QuestionUpdateJudgeConfigVerifyChain.java | 3 +- .../QuestionUpdateTagsVerifyChain.java | 3 +- .../QuestionUpdateTitleVerifyChain.java | 3 +- .../chains/{ => submit}/CodeVerifyChain.java | 3 +- .../{ => submit}/LanguageVerifyChain.java | 25 +- .../QuestionExistVerifyChain.java | 3 +- .../QuestionStatusVerifyChain.java | 4 +- .../submit/QuestionSubmitIdVerifyChain.java | 37 +++ .../req/QuestionSubmitQueryRequestDTO.java | 41 +++ .../dto/resp/QuestionSubmitResponseDTO.java | 29 +- .../service/QuestionSubmitService.java | 10 + .../service/impl/QuestionServiceImpl.java | 3 +- .../impl/QuestionSubmitServiceImpl.java | 201 ++++++++++-- .../src/main/resources/application.yml | 5 + .../aioj-backend-upms-biz/pom.xml | 13 + aioj-backend-user-service/pom.xml | 13 + .../userservice/config/AsyncConfig.java | 2 + .../service/impl/UserServiceImpl.java | 3 +- docs/admin-permission-guide.md | 252 +++++++++++++++ pom.xml | 11 + 52 files changed, 1561 insertions(+), 89 deletions(-) create mode 100644 aioj-backend-common/aioj-backend-common-core/src/main/java/cn/meowrain/aioj/backend/framework/core/enums/UserRoleEnum.java create mode 100644 aioj-backend-common/aioj-backend-common-feign/src/main/java/cn/meowrain/aioj/backend/framework/feign/interceptor/TokenRelayRequestInterceptor.java create mode 100644 aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/common/enums/QuestionSubmitJudgeInfoEnum.java rename aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/dto/chains/{ => question}/QuestionContentVerifyChain.java (95%) rename aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/dto/chains/{ => question}/QuestionDifficultyVerifyChain.java (96%) rename aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/dto/chains/{ => question}/QuestionEditContentVerifyChain.java (95%) rename aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/dto/chains/{ => question}/QuestionEditDifficultyVerifyChain.java (96%) rename aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/dto/chains/{ => question}/QuestionEditJudgeConfigVerifyChain.java (97%) rename aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/dto/chains/{ => question}/QuestionEditTagsVerifyChain.java (97%) rename aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/dto/chains/{ => question}/QuestionEditTitleVerifyChain.java (95%) rename aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/dto/chains/{ => question}/QuestionJudgeConfigVerifyChain.java (97%) rename aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/dto/chains/{ => question}/QuestionTagsVerifyChain.java (97%) rename aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/dto/chains/{ => question}/QuestionTitleVerifyChain.java (96%) rename aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/dto/chains/{ => question}/QuestionUpdateContentVerifyChain.java (95%) rename aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/dto/chains/{ => question}/QuestionUpdateDifficultyVerifyChain.java (96%) rename aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/dto/chains/{ => question}/QuestionUpdateExistVerifyChain.java (96%) rename aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/dto/chains/{ => question}/QuestionUpdateJudgeConfigVerifyChain.java (97%) rename aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/dto/chains/{ => question}/QuestionUpdateTagsVerifyChain.java (97%) rename aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/dto/chains/{ => question}/QuestionUpdateTitleVerifyChain.java (95%) rename aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/dto/chains/{ => submit}/CodeVerifyChain.java (97%) rename aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/dto/chains/{ => submit}/LanguageVerifyChain.java (87%) rename aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/dto/chains/{ => submit}/QuestionExistVerifyChain.java (95%) rename aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/dto/chains/{ => submit}/QuestionStatusVerifyChain.java (93%) create mode 100644 aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/dto/chains/submit/QuestionSubmitIdVerifyChain.java create mode 100644 aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/dto/req/QuestionSubmitQueryRequestDTO.java create mode 100644 docs/admin-permission-guide.md diff --git a/CLAUDE.md b/CLAUDE.md index 2fe2580..7ad8c53 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Codebase Overview -This is a microservices architecture for an Online Judge (OJ) system, built on **Spring Boot 3.5.7**. The project uses Maven as the build tool and follows a modular monorepo structure, with clearly separated core modules and service modules. +This is a microservices architecture for an Online Judge (OJ) system, built on **Spring Boot 3.5.7** with **Spring Cloud 2025.0.0** and **Spring Cloud Alibaba 2025.0.0.0**. The project uses Maven as the build tool and follows a modular monorepo structure, with clearly separated core modules and service modules. ### Core Modules @@ -21,26 +21,35 @@ The `aioj-backend-common` directory contains shared components and utilities use - Common constants and enumerations - Application-level configurations and auto-configuration classes -3. **aioj-backend-common-feign** +3. **aioj-backend-common-security** + Security components for JWT authentication and Spring Security: + - `JwtAuthenticationFilter`: JWT token validation filter + - `JwtUtil`: JWT token generation and parsing utilities + - `SecurityConfiguration`: Spring Security configuration with JWT support + - `SecurityAutoConfiguration`: Auto-configuration for security features + - JWT properties configuration with customizable secret and expiration + +4. **aioj-backend-common-feign** Feign client configurations for seamless inter-service communication: - Auto-configuration for Feign clients with default settings - `@EnableAIOJFeignClients` annotation for enabling Feign clients with predefined base packages - Feign interceptors and error handling mechanisms + - Integrated with Sentinel for circuit breaking -4. **aioj-backend-common-log** +5. **aioj-backend-common-log** Aspect-oriented programming (AOP) based logging framework: - `SysLogAspect`: Aspect for logging system operations (controller methods, service calls) - `SysLogEvent` and `SysLogListener`: Event-driven logging mechanism - `SysLogUtils`: Utility class for creating and managing log entries - Configuration properties for logging behavior -5. **aioj-backend-common-mybatis** +6. **aioj-backend-common-mybatis** MyBatis ORM framework extensions: - Auto-fill functionality for `createTime` and `updateTime` fields - Pagination interceptor implementation - MyBatis configuration auto-configuration classes -6. **aioj-backend-common-starter** +7. **aioj-backend-common-starter** Auto-configuration starters for easily enabling common features in service modules. @@ -57,9 +66,10 @@ The service modules represent the individual microservices that make up the syst 2. **aioj-backend-gateway** API gateway for request routing and filtering: - - Request routing to appropriate service modules + - Request routing to appropriate service modules based on path patterns - Authentication token validation before forwarding requests - - Rate limiting and request filtering mechanisms + - Sentinel integration for rate limiting and circuit breaking + - Load balancing and service discovery via Nacos 3. **aioj-backend-judge-service** Code judge service (under development): @@ -74,27 +84,51 @@ The service modules represent the individual microservices that make up the syst - Integration with the auth service for authentication 5. **aioj-backend-question-service** - Question bank service (under development): - - Will manage programming problems, test cases, and problem categories - - Support for problem difficulty levels and tags - - Integration with the judge service for problem submission + Question bank service with advanced validation and flow control: + - Programming problem management with CRUD operations + - **Chain of Responsibility pattern** for request validation: + - `QuestionTitleVerifyChain`, `QuestionContentVerifyChain`, etc. for create operations + - `QuestionUpdateExistVerifyChain`, `QuestionUpdateTitleVerifyChain`, etc. for update operations + - `QuestionExistVerifyChain`, `QuestionStatusVerifyChain`, `CodeVerifyChain`, `LanguageVerifyChain` for submission validation + - **Sentinel flow control** with Nacos datasource for dynamic rule management + - Redis integration for caching and session management + - Support for problem difficulty levels, tags, and judge configuration + - Integration with judge service for code submission 6. **aioj-backend-ai-service** AI-related functionality service (under development): - Will provide AI-assisted features like problem recommendation, code analysis, etc. 7. **aioj-backend-upms** - User, permission, and menu management service: + User, permission, and menu management service (modular structure): + - **aioj-backend-upms-api**: API definitions and DTOs for UPMS functionality + - **aioj-backend-upms-biz**: Business logic implementation - Low-level user and permission management - Menu and resource access control - Integration with other services for authorization +8. **aioj-backend-file-service** + File storage and management service: + - Tencent Cloud COS integration for object storage + - File upload, download, and management APIs + - Support for various file types + - Nacos service discovery integration + +9. **aioj-backend-blog-service** + Blog and content sharing service: + - User article creation and publishing + - **Markdown support** with Flexmark parser + - **Redis caching** for improved performance + - Spring Session for distributed session management + - Technical experience sharing and discussion platform + ## Commonly Used Commands ### Build - **Build the entire project**: `mvn clean compile` - **Build with tests**: `mvn clean install` - **Build a single module**: `mvn clean compile -pl aioj-backend-auth` +- **Skip tests**: `mvn clean install -DskipTests` ### Code Formatting - **Format code**: `mvn spring-javaformat:apply` @@ -108,16 +142,247 @@ The service modules represent the individual microservices that make up the syst - **Run a service locally**: Use Spring Boot's `Application` class directly in IDE or use `mvn spring-boot:run -pl ` - **Example**: `mvn spring-boot:run -pl aioj-backend-auth` +### Docker Image Build (using JIB) +- **Build and push Docker image**: `mvn package jib:build -pl ` +- **Build Docker image to tar file**: `mvn package jib:buildTar -pl ` +- **Example**: `mvn package jib:build -pl aioj-backend-gateway` + +## Technology Stack + +### Core Framework +- **Spring Boot**: 3.5.7 +- **Spring Cloud**: 2025.0.0 +- **Spring Cloud Alibaba**: 2025.0.0.0 +- **Java Version**: 17 + +### Infrastructure Components +- **Service Discovery**: Alibaba Nacos +- **Flow Control**: Alibaba Sentinel +- **API Gateway**: Spring Cloud Gateway +- **Load Balancing**: Spring Cloud LoadBalancer + +### Data & Persistence +- **ORM Framework**: MyBatis +- **Database**: MySQL +- **Cache**: Redis (Spring Data Redis) +- **Session**: Spring Session with Redis + +### Security & Authentication +- **Security Framework**: Spring Security +- **Authentication**: JWT (JSON Web Tokens) +- **JWT Library**: JJWT (io.jsonwebtoken) + +### Service Communication +- **HTTP Client**: OpenFeign +- **Circuit Breaker**: Sentinel + +### Storage & Content +- **Object Storage**: Tencent Cloud COS +- **Markdown Parser**: Flexmark 0.64.8 + +### Documentation +- **API Documentation**: SpringDoc OpenAPI 3 +- **API UI**: Knife4j + +### Utilities +- **Java Utilities**: Hutool (hutool-core, hutool-extra, hutool-crypto) +- **Logging**: SLF4J with Logback (via Spring Boot) +- **JSON Processing**: Jackson (via Spring Boot) + +### Build & Deploy +- **Build Tool**: Maven +- **Code Formatting**: Spring Java Format Maven Plugin 0.0.47 +- **Docker Build**: JIB Maven Plugin 3.4.5 +- **Git Info**: Git Commit ID Plugin 9.0.2 +- **Version Management**: Flatten Maven Plugin 1.6.0 + ## Architecture Highlights -- **Authentication**: JWT-based authentication implemented in `aioj-backend-auth` with `JwtAuthenticationFilter` -- **Logging**: Aspect-oriented logging with `SysLogAspect` in `aioj-backend-common-log` -- **Database**: MyBatis with auto-fill for create/update times implemented in `common-mybatis` -- **Inter-service communication**: Feign clients with auto-configuration from `common-feign` -- **Banner**: Custom application banner system in `common-core` +- **Authentication & Security**: + - JWT-based authentication with `JwtAuthenticationFilter` in `common-security` + - Centralized security configuration shared across services + - Token generation, parsing, and validation utilities in `JwtUtil` + +- **Flow Control & Rate Limiting**: + - **Alibaba Sentinel** integration for flow control and circuit breaking + - Dynamic rule management with Nacos datasource + - Applied in question-service for submission rate limiting + - Gateway-level traffic control and service protection + +- **Request Validation**: + - **Chain of Responsibility pattern** in question-service for complex validation logic + - Modular and extensible validation chains for different operations + - Context-based validation with clear separation of concerns + +- **Logging**: + - Aspect-oriented logging with `SysLogAspect` in `common-log` + - Event-driven logging mechanism with `SysLogEvent` and `SysLogListener` + +- **Database**: + - MyBatis with auto-fill for create/update times in `common-mybatis` + - Pagination support with interceptor implementation + +- **Caching & Session**: + - Redis integration for distributed caching + - Spring Session for distributed session management + - Applied in question-service and blog-service + +- **Inter-service communication**: + - Feign clients with auto-configuration from `common-feign` + - Integrated with Sentinel for circuit breaking + - Nacos service discovery for dynamic service routing + +- **File Storage**: + - Tencent Cloud COS integration in file-service + - Support for object storage and CDN acceleration + +- **Content Processing**: + - Markdown parsing with Flexmark in blog-service + - Rich text content support for technical articles + +- **Banner**: + - Custom application banner system in `common-core` ## Key Entry Points - **Gateway Application**: `aioj-backend-gateway/src/main/java/cn/meowrain/aioj/backend/gateway/AIOJGatewayApplication.java` - **Auth Application**: `aioj-backend-auth/src/main/java/cn/meowrain/aioj/backend/auth/AIOJAuthApplication.java` -- **User Service Application**: `aioj-backend-user-service/src/main/java/cn/meowrain/aioj/backend/userservice/UserServiceApplication.java` \ No newline at end of file +- **User Service Application**: `aioj-backend-user-service/src/main/java/cn/meowrain/aioj/backend/userservice/UserServiceApplication.java` +- **Question Service Application**: `aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/QuestionServiceApplication.java` +- **File Service Application**: `aioj-backend-file-service/src/main/java/cn/meowrain/aioj/backend/file/FileServiceApplication.java` +- **Blog Service Application**: `aioj-backend-blog-service/src/main/java/cn/meowrain/aioj/backend/blog/BlogServiceApplication.java` +- **UPMS Service Application**: `aioj-backend-upms/aioj-backend-upms-biz/src/main/java/cn/meowrain/aioj/backend/upms/UpmsApplication.java` + +## Key Design Patterns + +### Chain of Responsibility Pattern (Question Service) +Located in `aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/dto/chains/`: + +- **Create Question Validation**: `QuestionTitleVerifyChain`, `QuestionContentVerifyChain`, `QuestionDifficultyVerifyChain`, `QuestionTagsVerifyChain`, `QuestionJudgeConfigVerifyChain` +- **Update Question Validation**: `QuestionUpdateExistVerifyChain`, `QuestionUpdateTitleVerifyChain`, `QuestionUpdateContentVerifyChain`, etc. +- **Submit Question Validation**: `QuestionExistVerifyChain`, `QuestionStatusVerifyChain`, `LanguageVerifyChain`, `CodeVerifyChain` +- **Context Objects**: `QuestionCreateRequestParamVerifyContext`, `QuestionUpdateRequestParamVerifyContext`, `QuestionSubmitRequestParamVerifyContext` + +### Security Filter Chain (Common Security) +Located in `aioj-backend-common/aioj-backend-common-security/`: + +- `JwtAuthenticationFilter`: Pre-authentication filter for JWT token validation +- `SecurityConfiguration`: Spring Security filter chain configuration +- `SecurityAutoConfiguration`: Auto-configuration for security beans + +## Development Guidelines + +### Module Dependencies +- Service modules should depend on common modules, not other service modules +- Use Feign clients for inter-service communication +- Common modules should be lightweight and not depend on service-specific logic + +### Code Style +- Follow Spring Java Format conventions +- Run `mvn spring-javaformat:apply` before committing code +- Use Lombok annotations to reduce boilerplate code +- Keep classes and methods focused and single-purpose + +### Security Best Practices +- Use `@PreAuthorize` annotations for method-level security +- Never expose sensitive information in API responses +- Always validate and sanitize user input +- Use the chain of responsibility pattern for complex validation logic + +### API Design +- Follow RESTful conventions +- Use appropriate HTTP methods (GET, POST, PUT, DELETE) +- Return consistent response structures using common DTOs +- Document APIs using SpringDoc annotations + +### Error Handling +- Use custom exception classes for domain-specific errors +- Implement global exception handlers in controllers +- Return meaningful error messages to clients +- Log errors appropriately with context information + +### Testing +- Write unit tests for service layer logic +- Use integration tests for controller endpoints +- Mock external dependencies in tests +- Aim for high code coverage on critical paths + +### Configuration Management +- Use `application.yml` for service configuration +- Externalize environment-specific settings +- Use Nacos for centralized configuration management +- Never commit sensitive credentials to version control + +### Flow Control with Sentinel +- Configure Sentinel rules in Nacos for dynamic updates +- Use `@SentinelResource` annotations for method-level flow control +- Define fallback methods for degraded service behavior +- Monitor Sentinel dashboard for real-time metrics +- Apply flow rules at both gateway and service levels + +## Project Structure Best Practices + +### Package Organization +``` +cn.meowrain.aioj.backend. +├── controller/ # REST API endpoints +├── service/ # Business logic +│ └── impl/ # Service implementations +├── mapper/ # MyBatis mappers +├── entity/ # Database entities +├── dto/ # Data transfer objects +│ └── chains/ # Validation chains (if applicable) +├── config/ # Spring configuration classes +├── common/ # Service-specific common utilities +│ ├── enums/ # Enumerations +│ └── constants/ # Constants +└── Application.java # Main application class +``` + +### Naming Conventions +- **Controllers**: `Controller` (e.g., `QuestionController`) +- **Services**: `Service` (interface) and `ServiceImpl` (implementation) +- **Mappers**: `Mapper` (e.g., `QuestionMapper`) +- **Entities**: Noun representing the domain object (e.g., `Question`, `User`) +- **DTOs**: `DTO` (e.g., `CreateQuestionDTO`, `UpdateQuestionDTO`) +- **Validation Chains**: `VerifyChain` (e.g., `QuestionTitleVerifyChain`) + +## Infrastructure Setup + +### Required Services +Before running the microservices, ensure the following infrastructure components are running: + +1. **Nacos Server** (Service Discovery & Configuration Center) + - Default URL: http://localhost:8848/nacos + - Required for service registration and configuration management + +2. **Sentinel Dashboard** (Optional, for flow control monitoring) + - Monitor real-time traffic and configure flow rules + - Rules are persisted to Nacos + +3. **MySQL Database** + - Required for persistent data storage + - Each service may have its own database schema + +4. **Redis Server** + - Required for caching and session management + - Used by question-service and blog-service + +5. **Tencent Cloud COS** (for file-service) + - Configure credentials in application configuration + - Required only if using file-service + +### Service Startup Order (Recommended) +1. Start infrastructure services (MySQL, Redis, Nacos) +2. Start auth service (authentication required by other services) +3. Start gateway service (API entry point) +4. Start business services (user-service, question-service, etc.) in any order + +### Configuration Files +Each service has `application.yml` with profiles: +- `application.yml`: Common configuration +- `application-dev.yml`: Development environment +- `application-test.yml`: Test environment +- `application-prod.yml`: Production environment + +Activate profiles using: `spring.profiles.active=dev` or `-Dspring.profiles.active=dev` diff --git a/README.md b/README.md index 518184d..199d696 100644 --- a/README.md +++ b/README.md @@ -36,13 +36,156 @@ ## 快速开始 +### 前置要求 + +- JDK 17 或更高版本 +- Maven 3.6 或更高版本 +- MySQL 8.0 或更高版本 +- Redis 6.0 或更高版本 +- Nacos Server 2.x(用于服务注册和配置管理) + +### 首次克隆后的准备工作 + +1. **克隆项目** + ```bash + git clone + cd AI_OJ + ``` + +2. **安装公共依赖模块** + + 首次克隆后,需要先将公共模块安装到本地Maven仓库: + ```bash + mvn clean install -DskipTests + ``` + + 这个命令会: + - 编译所有模块 + - 将公共库模块(aioj-backend-common-*)安装到本地Maven仓库 + - 打包所有服务模块为可执行JAR + +3. **配置数据库和中间件** + + 根据各服务的 `application.yml` 配置文件,设置: + - MySQL 数据库连接信息 + - Redis 连接信息 + - Nacos 服务地址 + ### 构建项目 ```bash +# 编译所有模块(不打包) mvn clean compile + +# 编译并跳过测试 +mvn clean compile -DskipTests ``` -### 运行服务 +## 项目打包 + +### 打包说明 + +本项目采用模块化架构,分为**公共库模块**和**服务模块**: + +- **公共库模块**(aioj-backend-common-*):编译为普通JAR,供其他模块依赖 +- **服务模块**:打包为包含所有依赖的可执行Fat JAR(Spring Boot可执行JAR) + +### 打包所有服务 + +```bash +# 打包所有服务(推荐) +mvn clean package -DskipTests + +# 打包并运行测试 +mvn clean package +``` + +打包完成后,每个服务模块的 `target` 目录下会生成两个JAR文件: +- `-1.0.0.jar` - 可执行的Fat JAR(包含所有依赖,约60-100MB) +- `-1.0.0.jar.original` - 原始JAR(仅包含本模块代码,约100KB) + +### 打包单个服务 + +如果只需要打包某个特定服务: + +```bash +# 打包网关服务 +mvn clean package -pl aioj-backend-gateway -am -DskipTests + +# 打包认证服务 +mvn clean package -pl aioj-backend-auth -am -DskipTests + +# 打包用户服务 +mvn clean package -pl aioj-backend-user-service -am -DskipTests + +# 打包题库服务 +mvn clean package -pl aioj-backend-question-service -am -DskipTests + +# 打包文件服务 +mvn clean package -pl aioj-backend-file-service -am -DskipTests + +# 打包博客服务 +mvn clean package -pl aioj-backend-blog-service -am -DskipTests + +# 打包权限管理服务 +mvn clean package -pl aioj-backend-upms/aioj-backend-upms-biz -am -DskipTests +``` + +**参数说明:** +- `-pl `: 指定要构建的模块 +- `-am`: 同时构建该模块依赖的其他模块(also-make) +- `-DskipTests`: 跳过测试 + +### 打包后的文件位置 + +打包完成后,可执行JAR文件位于各服务模块的 `target` 目录: + +``` +aioj-backend-gateway/target/aioj-backend-gateway-1.0.0.jar +aioj-backend-auth/target/aioj-backend-auth-1.0.0.jar +aioj-backend-user-service/target/aioj-backend-user-service-1.0.0.jar +aioj-backend-question-service/target/aioj-backend-question-service-1.0.0.jar +aioj-backend-file-service/target/aioj-backend-file-service-1.0.0.jar +aioj-backend-blog-service/target/aioj-backend-blog-service-1.0.0.jar +aioj-backend-upms/aioj-backend-upms-biz/target/aioj-backend-upms-biz-1.0.0.jar +``` + +### 运行打包后的服务 + +使用 `java -jar` 命令运行打包后的服务: + +```bash +# 运行网关服务 +java -jar aioj-backend-gateway/target/aioj-backend-gateway-1.0.0.jar + +# 运行认证服务 +java -jar aioj-backend-auth/target/aioj-backend-auth-1.0.0.jar + +# 运行用户服务 +java -jar aioj-backend-user-service/target/aioj-backend-user-service-1.0.0.jar + +# 指定配置文件运行 +java -jar aioj-backend-gateway/target/aioj-backend-gateway-1.0.0.jar --spring.profiles.active=prod + +# 指定JVM参数运行 +java -Xms512m -Xmx1024m -jar aioj-backend-gateway/target/aioj-backend-gateway-1.0.0.jar +``` + +### 使用Docker部署(可选) + +项目已配置JIB插件,可以直接构建Docker镜像: + +```bash +# 构建Docker镜像到本地 +mvn package jib:dockerBuild -pl aioj-backend-gateway -am -DskipTests + +# 构建并推送到远程仓库 +mvn package jib:build -pl aioj-backend-gateway -am -DskipTests +``` + +### 开发模式运行 + +在开发过程中,可以使用 `spring-boot:run` 直接运行服务(无需打包): ```bash # 运行网关 @@ -68,11 +211,157 @@ mvn spring-boot:run -pl aioj-backend-user-service ### 代码格式化 ```bash +# 格式化所有代码 mvn spring-javaformat:apply + +# 检查代码格式 +mvn spring-javaformat:check ``` ### 运行测试 ```bash +# 运行所有测试 mvn test + +# 运行单个模块的测试 +mvn test -pl aioj-backend-user-service ``` + +### 清理构建产物 + +```bash +# 清理所有模块的target目录 +mvn clean + +# 清理单个模块 +mvn clean -pl aioj-backend-gateway +``` + +## 常见问题 + +### 1. 打包时提示找不到依赖 + +**问题**:打包服务时提示找不到 `aioj-backend-common-core` 等公共模块。 + +**解决方案**:首次克隆或更新公共模块后,需要先安装公共模块到本地仓库: +```bash +mvn clean install -DskipTests +``` + +### 2. 打包失败:Unable to find main class + +**问题**:公共库模块(aioj-backend-common-*)打包时报错。 + +**解决方案**:这是正常的,公共库模块不应该被打包为可执行JAR。项目已配置为跳过公共模块的repackage。如果遇到此问题,请确保使用最新的配置。 + +### 3. 服务启动失败 + +**问题**:运行JAR时服务无法启动。 + +**可能原因**: +- 数据库连接失败:检查MySQL是否启动,连接信息是否正确 +- Redis连接失败:检查Redis是否启动 +- Nacos连接失败:检查Nacos Server是否启动 +- 端口被占用:检查服务端口是否被其他程序占用 + +**解决方案**:查看日志输出,根据错误信息排查问题。 + +### 4. 内存不足 + +**问题**:打包或运行时提示内存不足。 + +**解决方案**: +```bash +# 增加Maven构建内存 +export MAVEN_OPTS="-Xmx2048m" + +# 或在Windows上 +set MAVEN_OPTS=-Xmx2048m + +# 运行服务时指定内存 +java -Xms512m -Xmx1024m -jar .jar +``` + +## 服务启动顺序 + +为确保系统正常运行,建议按以下顺序启动服务: + +1. **基础设施服务**(必须先启动) + - MySQL 数据库 + - Redis 缓存服务 + - Nacos 服务注册中心 + +2. **核心服务** + - `aioj-backend-auth` - 认证服务(其他服务可能依赖认证) + - `aioj-backend-gateway` - API网关(统一入口) + +3. **业务服务**(可并行启动) + - `aioj-backend-user-service` - 用户服务 + - `aioj-backend-upms-biz` - 权限管理服务 + - `aioj-backend-question-service` - 题库服务 + - `aioj-backend-file-service` - 文件服务 + - `aioj-backend-blog-service` - 博客服务 + +## 技术栈 + +### 核心框架 +- **Spring Boot**: 3.5.7 +- **Spring Cloud**: 2025.0.0 +- **Spring Cloud Alibaba**: 2025.0.0.0 +- **Java**: 17 + +### 基础设施 +- **服务注册与发现**: Alibaba Nacos +- **流量控制**: Alibaba Sentinel +- **API网关**: Spring Cloud Gateway +- **负载均衡**: Spring Cloud LoadBalancer + +### 数据存储 +- **数据库**: MySQL 8.0 +- **ORM框架**: MyBatis +- **缓存**: Redis +- **会话管理**: Spring Session (Redis) + +### 安全认证 +- **安全框架**: Spring Security +- **认证方式**: JWT (JSON Web Tokens) + +### 其他组件 +- **对象存储**: 腾讯云COS +- **Markdown解析**: Flexmark +- **API文档**: SpringDoc OpenAPI 3 + Knife4j +- **工具库**: Hutool + +## 项目文档 + +- **[CLAUDE.md](./CLAUDE.md)** - 项目架构和开发指南 +- **[管理员权限使用指南](./docs/admin-permission-guide.md)** - 管理员权限检查功能说明 + +## 开发规范 + +### 代码风格 +- 遵循 Spring Java Format 规范 +- 提交前运行 `mvn spring-javaformat:apply` 格式化代码 + +### 分支管理 +- `main` - 主分支,保持稳定 +- `develop` - 开发分支 +- `feature/*` - 功能分支 +- `bugfix/*` - 修复分支 + +### 提交规范 +- `feat`: 新功能 +- `fix`: 修复bug +- `docs`: 文档更新 +- `refactor`: 代码重构 +- `test`: 测试相关 +- `chore`: 构建/工具链相关 + +## 许可证 + +[添加许可证信息] + +## 联系方式 + +[添加联系方式] diff --git a/aioj-backend-ai-service/pom.xml b/aioj-backend-ai-service/pom.xml index 1ed6ab8..30159d9 100644 --- a/aioj-backend-ai-service/pom.xml +++ b/aioj-backend-ai-service/pom.xml @@ -99,6 +99,15 @@ + + + org.springframework.boot + spring-boot-maven-plugin + + false + + + org.xolstice.maven.plugins diff --git a/aioj-backend-auth/pom.xml b/aioj-backend-auth/pom.xml index 98318ff..ad63330 100644 --- a/aioj-backend-auth/pom.xml +++ b/aioj-backend-auth/pom.xml @@ -107,4 +107,17 @@ test + + + + + + org.springframework.boot + spring-boot-maven-plugin + + false + + + + \ No newline at end of file diff --git a/aioj-backend-blog-service/pom.xml b/aioj-backend-blog-service/pom.xml index 45497ef..384d3d5 100644 --- a/aioj-backend-blog-service/pom.xml +++ b/aioj-backend-blog-service/pom.xml @@ -96,4 +96,17 @@ test + + + + + + org.springframework.boot + spring-boot-maven-plugin + + false + + + + diff --git a/aioj-backend-common/aioj-backend-common-core/src/main/java/cn/meowrain/aioj/backend/framework/core/enums/UserRoleEnum.java b/aioj-backend-common/aioj-backend-common-core/src/main/java/cn/meowrain/aioj/backend/framework/core/enums/UserRoleEnum.java new file mode 100644 index 0000000..8539644 --- /dev/null +++ b/aioj-backend-common/aioj-backend-common-core/src/main/java/cn/meowrain/aioj/backend/framework/core/enums/UserRoleEnum.java @@ -0,0 +1,64 @@ +package cn.meowrain.aioj.backend.framework.core.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 用户角色枚举 + */ +@Getter +@AllArgsConstructor +public enum UserRoleEnum { + + /** + * 普通用户 + */ + USER("user", "普通用户"), + + /** + * 管理员 + */ + ADMIN("admin", "管理员"), + + /** + * 封禁用户 + */ + BAN("ban", "封禁用户"); + + /** + * 角色代码(数据库存储值) + */ + private final String code; + + /** + * 角色描述 + */ + private final String description; + + /** + * 根据角色代码获取枚举 + * @param code 角色代码 + * @return 角色枚举,未找到返回 null + */ + public static UserRoleEnum fromCode(String code) { + if (code == null) { + return null; + } + for (UserRoleEnum role : values()) { + if (role.code.equalsIgnoreCase(code)) { + return role; + } + } + return null; + } + + /** + * 判断是否为管理员角色 + * @param code 角色代码 + * @return true-是管理员, false-不是管理员 + */ + public static boolean isAdmin(String code) { + return ADMIN.code.equalsIgnoreCase(code); + } + +} \ No newline at end of file diff --git a/aioj-backend-common/aioj-backend-common-core/src/main/java/cn/meowrain/aioj/backend/framework/core/jackson/JacksonConfiguration.java b/aioj-backend-common/aioj-backend-common-core/src/main/java/cn/meowrain/aioj/backend/framework/core/jackson/JacksonConfiguration.java index 80f6395..d24a8e2 100644 --- a/aioj-backend-common/aioj-backend-common-core/src/main/java/cn/meowrain/aioj/backend/framework/core/jackson/JacksonConfiguration.java +++ b/aioj-backend-common/aioj-backend-common-core/src/main/java/cn/meowrain/aioj/backend/framework/core/jackson/JacksonConfiguration.java @@ -25,7 +25,10 @@ public class JacksonConfiguration { return builder -> { // 注册 JavaTimeModule 处理时间类型 builder.modules(new JavaTimeModule()); - + + // 输出格式化 JSON,便于调试与阅读 + builder.indentOutput(true); + // Long 和 long 类型序列化为 String,避免前端精度丢失 builder.serializerByType(Long.class, ToStringSerializer.instance); builder.serializerByType(Long.TYPE, ToStringSerializer.instance); diff --git a/aioj-backend-common/aioj-backend-common-core/src/main/java/cn/meowrain/aioj/backend/framework/core/utils/ContextHolderUtils.java b/aioj-backend-common/aioj-backend-common-core/src/main/java/cn/meowrain/aioj/backend/framework/core/utils/ContextHolderUtils.java index 6c5ec09..36d6e85 100644 --- a/aioj-backend-common/aioj-backend-common-core/src/main/java/cn/meowrain/aioj/backend/framework/core/utils/ContextHolderUtils.java +++ b/aioj-backend-common/aioj-backend-common-core/src/main/java/cn/meowrain/aioj/backend/framework/core/utils/ContextHolderUtils.java @@ -1,5 +1,6 @@ package cn.meowrain.aioj.backend.framework.core.utils; +import cn.meowrain.aioj.backend.framework.core.enums.UserRoleEnum; import lombok.experimental.UtilityClass; import lombok.extern.slf4j.Slf4j; import org.springframework.security.core.Authentication; @@ -81,7 +82,13 @@ public class ContextHolderUtils { * @return true-是管理员, false-不是管理员 */ public static boolean isAdmin() { - return "ADMIN".equals(getCurrentUserRole()); + try { + String role = getCurrentUserRole(); + return UserRoleEnum.isAdmin(role); + } + catch (Exception e) { + return false; + } } } diff --git a/aioj-backend-common/aioj-backend-common-feign/src/main/java/cn/meowrain/aioj/backend/framework/feign/FeignAutoConfiguration.java b/aioj-backend-common/aioj-backend-common-feign/src/main/java/cn/meowrain/aioj/backend/framework/feign/FeignAutoConfiguration.java index 25f160f..9bb7ef5 100644 --- a/aioj-backend-common/aioj-backend-common-feign/src/main/java/cn/meowrain/aioj/backend/framework/feign/FeignAutoConfiguration.java +++ b/aioj-backend-common/aioj-backend-common-feign/src/main/java/cn/meowrain/aioj/backend/framework/feign/FeignAutoConfiguration.java @@ -1,8 +1,16 @@ package cn.meowrain.aioj.backend.framework.feign; +import cn.meowrain.aioj.backend.framework.feign.interceptor.TokenRelayRequestInterceptor; +import feign.RequestInterceptor; import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.context.annotation.Bean; @AutoConfiguration public class FeignAutoConfiguration { + @Bean + public RequestInterceptor tokenRelayRequestInterceptor() { + return new TokenRelayRequestInterceptor(); + } + } diff --git a/aioj-backend-common/aioj-backend-common-feign/src/main/java/cn/meowrain/aioj/backend/framework/feign/interceptor/TokenRelayRequestInterceptor.java b/aioj-backend-common/aioj-backend-common-feign/src/main/java/cn/meowrain/aioj/backend/framework/feign/interceptor/TokenRelayRequestInterceptor.java new file mode 100644 index 0000000..3729bc3 --- /dev/null +++ b/aioj-backend-common/aioj-backend-common-feign/src/main/java/cn/meowrain/aioj/backend/framework/feign/interceptor/TokenRelayRequestInterceptor.java @@ -0,0 +1,60 @@ +package cn.meowrain.aioj.backend.framework.feign.interceptor; + +import cn.meowrain.aioj.backend.framework.feign.annotation.NoToken; +import feign.RequestInterceptor; +import feign.RequestTemplate; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.util.StringUtils; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import java.lang.reflect.Method; + +/** + * Feign 调用时透传 Authorization 头 + */ +public class TokenRelayRequestInterceptor implements RequestInterceptor { + + private static final String HEADER_NAME = "Authorization"; + private static final String TOKEN_PREFIX = "Bearer "; + + @Override + public void apply(RequestTemplate template) { + if (isNoToken(template) || template.headers().containsKey(HEADER_NAME)) { + return; + } + + String authorization = resolveAuthorization(); + if (StringUtils.hasText(authorization)) { + template.header(HEADER_NAME, authorization); + } + } + + private boolean isNoToken(RequestTemplate template) { + if (template.methodMetadata() == null) { + return false; + } + Method method = template.methodMetadata().method(); + return method != null && (method.isAnnotationPresent(NoToken.class) + || method.getDeclaringClass().isAnnotationPresent(NoToken.class)); + } + + private String resolveAuthorization() { + ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); + if (attributes != null) { + HttpServletRequest request = attributes.getRequest(); + return request.getHeader(HEADER_NAME); + } + + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication != null && authentication.getDetails() instanceof String token + && StringUtils.hasText(token)) { + return token.startsWith(TOKEN_PREFIX) ? token : TOKEN_PREFIX + token; + } + + return null; + } + +} diff --git a/aioj-backend-common/aioj-backend-common-security/src/main/java/cn/meowrain/aioj/backend/framework/security/filter/JwtAuthenticationFilter.java b/aioj-backend-common/aioj-backend-common-security/src/main/java/cn/meowrain/aioj/backend/framework/security/filter/JwtAuthenticationFilter.java index 8a78081..aeafc34 100644 --- a/aioj-backend-common/aioj-backend-common-security/src/main/java/cn/meowrain/aioj/backend/framework/security/filter/JwtAuthenticationFilter.java +++ b/aioj-backend-common/aioj-backend-common-security/src/main/java/cn/meowrain/aioj/backend/framework/security/filter/JwtAuthenticationFilter.java @@ -43,6 +43,9 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { if (StringUtils.hasText(token) && jwtUtil.isTokenValid(token)) { Claims claims = jwtUtil.parseClaims(token); Authentication authentication = createAuthentication(claims); + if (authentication instanceof UsernamePasswordAuthenticationToken authToken) { + authToken.setDetails(token); + } SecurityContextHolder.getContext().setAuthentication(authentication); log.debug("JWT Authentication successful for user: {}", claims.getSubject()); diff --git a/aioj-backend-file-service/pom.xml b/aioj-backend-file-service/pom.xml index 74d98fd..aad91f4 100644 --- a/aioj-backend-file-service/pom.xml +++ b/aioj-backend-file-service/pom.xml @@ -90,4 +90,17 @@ hutool-crypto + + + + + + org.springframework.boot + spring-boot-maven-plugin + + false + + + + \ No newline at end of file diff --git a/aioj-backend-file-service/src/main/java/cn/meowrain/aioj/backend/fileservice/config/AsyncConfig.java b/aioj-backend-file-service/src/main/java/cn/meowrain/aioj/backend/fileservice/config/AsyncConfig.java index 323b437..ceec652 100644 --- a/aioj-backend-file-service/src/main/java/cn/meowrain/aioj/backend/fileservice/config/AsyncConfig.java +++ b/aioj-backend-file-service/src/main/java/cn/meowrain/aioj/backend/fileservice/config/AsyncConfig.java @@ -6,6 +6,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; +import org.springframework.security.task.DelegatingSecurityContextTaskDecorator; import java.util.concurrent.Executor; import java.util.concurrent.ThreadPoolExecutor; @@ -40,6 +41,7 @@ public class AsyncConfig { executor.setMaxPoolSize(maxPoolSize); executor.setQueueCapacity(queueCapacity); executor.setThreadNamePrefix(threadNamePrefix); + executor.setTaskDecorator(new DelegatingSecurityContextTaskDecorator()); executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); executor.initialize(); log.info("文件异步线程池初始化完成: coreSize={}, maxSize={}, queueCapacity={}", diff --git a/aioj-backend-gateway/pom.xml b/aioj-backend-gateway/pom.xml index 78540e1..ef26d19 100644 --- a/aioj-backend-gateway/pom.xml +++ b/aioj-backend-gateway/pom.xml @@ -83,4 +83,17 @@ spring-boot-starter-actuator + + + + + + org.springframework.boot + spring-boot-maven-plugin + + false + + + + \ No newline at end of file diff --git a/aioj-backend-question-service/pom.xml b/aioj-backend-question-service/pom.xml index 4a53feb..e53c484 100644 --- a/aioj-backend-question-service/pom.xml +++ b/aioj-backend-question-service/pom.xml @@ -94,5 +94,28 @@ spring-boot-starter-test test + + org.springframework.boot + spring-boot-starter-actuator + + + org.springframework.boot + spring-boot-devtools + runtime + true + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + false + + + + \ No newline at end of file diff --git a/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/common/constants/RedisKeyConstants.java b/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/common/constants/RedisKeyConstants.java index 7cb4418..3a79110 100644 --- a/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/common/constants/RedisKeyConstants.java +++ b/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/common/constants/RedisKeyConstants.java @@ -30,6 +30,11 @@ public class RedisKeyConstants { */ public static final String QUESTION_SUBMIT_CACHE_KEY_PREFIX = "question_submit:"; + /** + * 题目提交并发锁 Key 前缀(userId + questionId) + */ + public static final String QUESTION_SUBMIT_LOCK_KEY_PREFIX = "question_submit_lock:"; + private RedisKeyConstants() { } } diff --git a/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/common/enums/QuestionSubmitJudgeInfoEnum.java b/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/common/enums/QuestionSubmitJudgeInfoEnum.java new file mode 100644 index 0000000..f7fe8ed --- /dev/null +++ b/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/common/enums/QuestionSubmitJudgeInfoEnum.java @@ -0,0 +1,82 @@ +package cn.meowrain.aioj.backend.question.common.enums; + +import lombok.Getter; + +/** + * 判题信息消息枚举 + */ +@Getter +public enum QuestionSubmitJudgeInfoEnum { + + /** + * 成功 + */ + ACCEPTED("Accepted", "成功"), + + /** + * 答案错误 + */ + WRONG_ANSWER("Wrong Answer", "答案错误"), + + /** + * 编译错误 + */ + COMPILE_ERROR("Compile Error", "编译错误"), + + /** + * 内存溢出 + */ + MEMORY_LIMIT_EXCEEDED("Memory Limit Exceeded", "内存溢出"), + + /** + * 超时 + */ + TIME_LIMIT_EXCEEDED("Time Limit Exceeded", "超时"), + + /** + * 展示错误 + */ + PRESENTATION_ERROR("Presentation Error", "展示错误"), + + /** + * 输出溢出 + */ + OUTPUT_LIMIT_EXCEEDED("Output Limit Exceeded", "输出溢出"), + + /** + * 等待中 + */ + WAITING("Waiting", "等待中"), + + /** + * 危险操作 + */ + DANGEROUS_OPERATION("Dangerous Operation", "危险操作"), + + /** + * 运行错误(用户程序的问题) + */ + RUNTIME_ERROR("Runtime Error", "运行错误(用户程序的问题)"), + + /** + * 系统错误(做系统人的问题) + */ + SYSTEM_ERROR("System Error", "系统错误(做系统人的问题)"), + + ; + + /** + * 判题信息值 + */ + private final String value; + + /** + * 描述 + */ + private final String desc; + + QuestionSubmitJudgeInfoEnum(String value, String desc) { + this.value = value; + this.desc = desc; + } +} diff --git a/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/common/enums/QuestionSubmitStatusEnum.java b/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/common/enums/QuestionSubmitStatusEnum.java index 9bf2a80..eff931e 100644 --- a/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/common/enums/QuestionSubmitStatusEnum.java +++ b/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/common/enums/QuestionSubmitStatusEnum.java @@ -2,6 +2,8 @@ package cn.meowrain.aioj.backend.question.common.enums; import lombok.Getter; +import java.util.Arrays; + /** * 题目提交状态枚举 */ @@ -44,4 +46,20 @@ public enum QuestionSubmitStatusEnum { this.value = value; this.desc = desc; } + + /** + * 根据value获取desc + * @param value + * @return + */ + public static String getDescByValue(Integer value) { + if (value == null) { + return null; + } + return Arrays.stream(QuestionSubmitStatusEnum.values()) + .filter(e -> e.getValue().equals(value)) + .map(QuestionSubmitStatusEnum::getDesc) + .findFirst() + .orElse(null); + } } diff --git a/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/controller/QuestionSubmitController.java b/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/controller/QuestionSubmitController.java index 8e84bb3..9a756bb 100644 --- a/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/controller/QuestionSubmitController.java +++ b/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/controller/QuestionSubmitController.java @@ -4,10 +4,14 @@ import cn.meowrain.aioj.backend.framework.core.errorcode.ErrorCode; import cn.meowrain.aioj.backend.framework.core.web.Result; import cn.meowrain.aioj.backend.framework.core.web.Results; import cn.meowrain.aioj.backend.question.dao.entity.QuestionSubmit; +import cn.meowrain.aioj.backend.question.dto.req.QuestionSubmitQueryRequestDTO; import cn.meowrain.aioj.backend.question.dto.req.QuestionSubmitRequestDTO; +import cn.meowrain.aioj.backend.question.dto.resp.QuestionSubmitResponseDTO; import cn.meowrain.aioj.backend.question.service.QuestionSubmitService; import com.alibaba.csp.sentinel.annotation.SentinelResource; import com.alibaba.csp.sentinel.slots.block.BlockException; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; @@ -15,6 +19,7 @@ import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.beans.BeanUtils; import org.springframework.web.bind.annotation.*; +import org.apache.commons.lang3.StringUtils; /** * 题目提交管理控制器 - RESTful API @@ -46,8 +51,6 @@ public class QuestionSubmitController { public Result handleException(QuestionSubmitRequestDTO request, BlockException ex) { System.out.println("被限流了: " + ex.getClass().getCanonicalName()); - // 假设你的 Results 工具类支持返回错误信息 - // 这里的 code (比如 429) 和 message 根据你的 Result 结构来定 return Results.failure(ErrorCode.API_REQUEST_ERROR.code(),"系统繁忙,请稍后再试!(这是自定义的限流提示)"); } @@ -64,6 +67,14 @@ public class QuestionSubmitController { return Results.success(submit); } + @GetMapping + @Operation(summary = "分页查询",description = "根据用户id,题目id,查找提交记录") +// 根据用户id,题目id,编程语言,题目状态,查找提交记录 + public Result> getSubmitPage( + @Parameter(description = "查询条件") QuestionSubmitQueryRequestDTO request) { + Page dtoPage = questionSubmitService.listQuestionSubmits(request); + return Results.success(dtoPage); + } /** * 内部接口:更新提交状态 * PATCH /v1/question-submits/{id}/status diff --git a/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/dto/chains/QuestionContentVerifyChain.java b/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/dto/chains/question/QuestionContentVerifyChain.java similarity index 95% rename from aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/dto/chains/QuestionContentVerifyChain.java rename to aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/dto/chains/question/QuestionContentVerifyChain.java index f1ee56d..f514e95 100644 --- a/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/dto/chains/QuestionContentVerifyChain.java +++ b/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/dto/chains/question/QuestionContentVerifyChain.java @@ -1,4 +1,4 @@ -package cn.meowrain.aioj.backend.question.dto.chains; +package cn.meowrain.aioj.backend.question.dto.chains.question; import cn.meowrain.aioj.backend.framework.core.designpattern.chains.AbstractChianHandler; import cn.meowrain.aioj.backend.framework.core.errorcode.ErrorCode; @@ -48,3 +48,4 @@ public class QuestionContentVerifyChain implements AbstractChianHandler SUPPORTED_LANGUAGES = Arrays.asList( - "java", - "cpp", - "python", - "go", - "javascript", - "c", - "csharp", - "rust", - "php", - "swift", - "kotlin", - "typescript", - "ruby", - "shell" - ); - + private static final List SUPPORTED_LANGUAGES = Arrays.stream(LanguageEnum.values()) + .map(LanguageEnum::getValue) + .toList(); @Override public void handle(QuestionSubmitRequestDTO requestParam) { String language = requestParam.getLanguage(); @@ -72,3 +60,4 @@ public class LanguageVerifyChain implements AbstractChianHandler { + + @Override + public void handle(QuestionSubmitRequestDTO requestParam) { + Long questionId = requestParam.getQuestionId(); + if (questionId == null || questionId <= 0) { + throw new ClientException("题目ID不能为空", ErrorCode.PARAMS_ERROR); + } + log.debug("题目ID校验通过,题目ID: {}", questionId); + } + + @Override + public String mark() { + return ChainMarkEnums.QUESTION_SUBMIT_REQ_PARAM_VERIFY_CHAIN.getMark(); + } + + @Override + public int getOrder() { + return 5; + } +} + diff --git a/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/dto/req/QuestionSubmitQueryRequestDTO.java b/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/dto/req/QuestionSubmitQueryRequestDTO.java new file mode 100644 index 0000000..138643c --- /dev/null +++ b/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/dto/req/QuestionSubmitQueryRequestDTO.java @@ -0,0 +1,41 @@ +package cn.meowrain.aioj.backend.question.dto.req; + +import cn.meowrain.aioj.backend.question.dao.entity.QuestionSubmit; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.io.Serial; +import java.io.Serializable; + +/** + * 题目提交查询请求 DTO + */ +@Data +@EqualsAndHashCode(callSuper = true) +@Schema(description = "题目提交查询请求") +public class QuestionSubmitQueryRequestDTO extends Page implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + @Schema(description = "用户ID", example = "1") + private Long userId; + + @Schema(description = "题目ID", example = "1") + private Long questionId; + + @Schema(description = "编程语言",example = "go") + private String language; + + @Schema(description = "提交状态", example = "0") + private Integer status; + + + @Schema(description = "排序字段", example = "createTime") + private String sortField; + + @Schema(description = "排序方向", example = "desc", allowableValues = {"asc", "desc"}) + private String sortOrder; +} diff --git a/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/dto/resp/QuestionSubmitResponseDTO.java b/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/dto/resp/QuestionSubmitResponseDTO.java index 30ec4ca..94d42bb 100644 --- a/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/dto/resp/QuestionSubmitResponseDTO.java +++ b/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/dto/resp/QuestionSubmitResponseDTO.java @@ -1,9 +1,11 @@ package cn.meowrain.aioj.backend.question.dto.resp; +import cn.meowrain.aioj.backend.question.dto.req.JudgeInfo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import java.io.Serializable; +import java.util.Date; /** * 题目提交响应 DTO @@ -12,6 +14,31 @@ import java.io.Serializable; @Schema(description = "题目提交响应") public class QuestionSubmitResponseDTO implements Serializable { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 1L; + @Schema(description = "提交ID", example = "1") + private Long id; + + @Schema(description = "代码",example = "public class Main {}") + private String code; + @Schema(description = "编程语言", example = "java") + private String language; + + @Schema(description = "判题信息(JSON)") + private JudgeInfo judgeInfo; + + @Schema(description = "判题状态", example = "0") + private Integer status; + + @Schema(description = "题目ID", example = "1") + private Long questionId; + + @Schema(description = "用户ID", example = "1") + private Long userId; + + @Schema(description = "创建时间") + private Date createTime; + + @Schema(description = "更新时间") + private Date updateTime; } diff --git a/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/service/QuestionSubmitService.java b/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/service/QuestionSubmitService.java index 3bb4ccc..eeb100d 100644 --- a/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/service/QuestionSubmitService.java +++ b/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/service/QuestionSubmitService.java @@ -1,6 +1,9 @@ package cn.meowrain.aioj.backend.question.service; import cn.meowrain.aioj.backend.question.dao.entity.QuestionSubmit; +import cn.meowrain.aioj.backend.question.dto.req.QuestionSubmitQueryRequestDTO; +import cn.meowrain.aioj.backend.question.dto.resp.QuestionSubmitResponseDTO; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.service.IService; /** @@ -28,4 +31,11 @@ public interface QuestionSubmitService extends IService { * @return 提交记录 */ QuestionSubmit getSubmitById(Long submitId); + + /** + * 分页查询题目提交记录 + * @param request 请求体 + * @return + */ + Page listQuestionSubmits(QuestionSubmitQueryRequestDTO request); } diff --git a/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/service/impl/QuestionServiceImpl.java b/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/service/impl/QuestionServiceImpl.java index 14ec64f..b605357 100644 --- a/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/service/impl/QuestionServiceImpl.java +++ b/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/service/impl/QuestionServiceImpl.java @@ -32,7 +32,7 @@ public class QuestionServiceImpl extends ServiceImpl i private final QuestionCreateRequestParamVerifyContext questionCreateChainContext; private final QuestionEditRequestParamVerifyContext questionEditChainContext; - + private final ObjectMapper mapper; @Override @Transactional(rollbackFor = Exception.class) public Long createQuestionWithChain(QuestionCreateRequestDTO requestDTO) { @@ -136,7 +136,6 @@ public class QuestionServiceImpl extends ServiceImpl i } // 处理复杂字段 - ObjectMapper mapper = new ObjectMapper(); if (requestDTO.getTags() != null && !requestDTO.getTags().isEmpty()) { try { questionToUpdate.setTags(mapper.writeValueAsString(requestDTO.getTags())); diff --git a/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/service/impl/QuestionSubmitServiceImpl.java b/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/service/impl/QuestionSubmitServiceImpl.java index 14d2cd0..0fc4607 100644 --- a/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/service/impl/QuestionSubmitServiceImpl.java +++ b/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/service/impl/QuestionSubmitServiceImpl.java @@ -2,19 +2,34 @@ package cn.meowrain.aioj.backend.question.service.impl; import cn.meowrain.aioj.backend.framework.core.errorcode.ErrorCode; import cn.meowrain.aioj.backend.framework.core.exception.ClientException; +import cn.meowrain.aioj.backend.framework.core.exception.ServiceException; +import cn.meowrain.aioj.backend.framework.core.utils.ContextHolderUtils; +import cn.meowrain.aioj.backend.question.common.constants.RedisKeyConstants; import cn.meowrain.aioj.backend.question.common.enums.ChainMarkEnums; +import cn.meowrain.aioj.backend.question.common.enums.QuestionSubmitStatusEnum; import cn.meowrain.aioj.backend.question.dao.entity.QuestionSubmit; import cn.meowrain.aioj.backend.question.dao.mapper.QuestionSubmitMapper; import cn.meowrain.aioj.backend.question.dto.chains.context.QuestionSubmitRequestParamVerifyContext; +import cn.meowrain.aioj.backend.question.dto.req.JudgeInfo; +import cn.meowrain.aioj.backend.question.dto.req.QuestionSubmitQueryRequestDTO; import cn.meowrain.aioj.backend.question.dto.req.QuestionSubmitRequestDTO; +import cn.meowrain.aioj.backend.question.dto.resp.QuestionSubmitResponseDTO; import cn.meowrain.aioj.backend.question.service.QuestionSubmitService; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; import org.springframework.beans.BeanUtils; +import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.time.Duration; + /** * 题目提交服务实现 */ @@ -23,47 +38,183 @@ import org.springframework.transaction.annotation.Transactional; @RequiredArgsConstructor public class QuestionSubmitServiceImpl extends ServiceImpl implements QuestionSubmitService { - private final QuestionSubmitRequestParamVerifyContext submitChainContext; + private final ObjectMapper objectMapper; + private final QuestionSubmitRequestParamVerifyContext submitChainContext; + private final StringRedisTemplate stringRedisTemplate; + + private static final Duration SUBMIT_LOCK_TTL = Duration.ofMinutes(30); @Transactional(rollbackFor = Exception.class) @Override public Long createSubmit(QuestionSubmit questionSubmit) { - return createSubmitWithChain(questionSubmit); + // TODO: 接入微服务后从请求头中获取 + Long userId = 1L; +// Long userId = ContextHolderUtils.getCurrentUserId(); +// questionSubmit.setUserId(userId); + + Long questionId = questionSubmit.getQuestionId(); + questionSubmit.setUserId(userId); + if (!tryAcquireSubmitLock(userId, questionId)) { + throw new ClientException("当前有判题进行中,请稍后再提交", ErrorCode.OPERATION_ERROR); + } + + try { + ensureNoInProgressSubmit(userId, questionId); + return createSubmitWithChain(questionSubmit); + } catch (Exception ex) { + releaseSubmitLock(userId, questionId); + throw ex; + } } - /** - * 使用责任链模式创建题目提交 - */ - private Long createSubmitWithChain(QuestionSubmit questionSubmit) { - // 将 QuestionSubmit 转换为 QuestionSubmitRequestDTO 用于责任链校验 - QuestionSubmitRequestDTO requestDTO = new QuestionSubmitRequestDTO(); - requestDTO.setQuestionId(questionSubmit.getQuestionId()); - requestDTO.setLanguage(questionSubmit.getLanguage()); - requestDTO.setCode(questionSubmit.getCode()); + /** + * 使用责任链模式创建题目提交 + */ + private Long createSubmitWithChain(QuestionSubmit questionSubmit) { + // 将 QuestionSubmit 转换为 QuestionSubmitRequestDTO 用于责任链校验 + QuestionSubmitRequestDTO requestDTO = new QuestionSubmitRequestDTO(); + requestDTO.setQuestionId(questionSubmit.getQuestionId()); + requestDTO.setLanguage(questionSubmit.getLanguage()); + requestDTO.setCode(questionSubmit.getCode()); - // 执行责任链校验 - log.info("开始执行题目提交责任链校验,题目ID: {}", questionSubmit.getQuestionId()); - submitChainContext.handler( - ChainMarkEnums.QUESTION_SUBMIT_REQ_PARAM_VERIFY_CHAIN.getMark(), - requestDTO - ); - log.info("题目提交责任链校验通过"); + // 执行责任链校验 + log.info("开始执行题目提交责任链校验,题目ID: {}", questionSubmit.getQuestionId()); + submitChainContext.handler( + ChainMarkEnums.QUESTION_SUBMIT_REQ_PARAM_VERIFY_CHAIN.getMark(), + requestDTO + ); + log.info("题目提交责任链校验通过"); - // 校验通过,保存提交记录 - // 设置初始状态:0 - 待判题 - questionSubmit.setStatus(0); + // TODO: 判题机设置judgeInfo + JudgeInfo judgeInfo = new JudgeInfo(); + String judgeInfos; + try { + judgeInfos = objectMapper.writeValueAsString(judgeInfo); + } catch (JsonProcessingException e) { + throw new ServiceException("判题信息序列化失败", e, ErrorCode.SYSTEM_ERROR); + } + questionSubmit.setJudgeInfo(judgeInfos); - this.save(questionSubmit); - return questionSubmit.getId(); - } + // 校验通过,保存提交记录 + // 设置初始状态:0 - 待判题 + questionSubmit.setStatus(QuestionSubmitStatusEnum.WAITING.getValue()); + + this.save(questionSubmit); + return questionSubmit.getId(); + } @Override public Boolean updateSubmitStatus(QuestionSubmit questionSubmit) { - return this.updateById(questionSubmit); + Boolean updated = this.updateById(questionSubmit); + if (Boolean.TRUE.equals(updated) && shouldReleaseLock(questionSubmit.getStatus())) { + Long userId = questionSubmit.getUserId(); + Long questionId = questionSubmit.getQuestionId(); + if (userId == null && questionSubmit.getId() != null) { + QuestionSubmit existing = this.getById(questionSubmit.getId()); + if (existing != null) { + userId = existing.getUserId(); + questionId = existing.getQuestionId(); + } + } + if (userId != null && questionId != null) { + releaseSubmitLock(userId, questionId); + } + } + return updated; } @Override public QuestionSubmit getSubmitById(Long submitId) { return this.getById(submitId); } + + @Override + public Page listQuestionSubmits(QuestionSubmitQueryRequestDTO request) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + if (request.getUserId() != null) { + wrapper.eq(QuestionSubmit::getUserId, request.getUserId()); + } + if (request.getQuestionId() != null) { + wrapper.eq(QuestionSubmit::getQuestionId, request.getQuestionId()); + } + if (request.getStatus() != null) { + wrapper.eq(QuestionSubmit::getStatus, request.getStatus()); + } + if (StringUtils.isNotBlank(request.getLanguage())) { + wrapper.eq(QuestionSubmit::getLanguage, request.getLanguage()); + } + + + // 排序字段 + String sortField = request.getSortField(); + if (StringUtils.isNotBlank(sortField)) { + boolean asc = "asc".equalsIgnoreCase(request.getSortOrder()); + if ("createTime".equals(sortField)) { + wrapper.orderBy(true, asc, QuestionSubmit::getCreateTime); + } else if ("updateTime".equals(sortField)) { + wrapper.orderBy(true, asc, QuestionSubmit::getUpdateTime); + } + } else { + wrapper.orderByDesc(QuestionSubmit::getCreateTime); + } + + Page page = this.page(request, wrapper); + Page dtoPage = new Page<>(page.getCurrent(), page.getSize(), page.getTotal()); + dtoPage.setRecords(page.getRecords().stream() + .map(this::convertToDTO) + .toList()); + return dtoPage; + } + + private QuestionSubmitResponseDTO convertToDTO(QuestionSubmit submit) { + if (submit == null) { + return null; + } + QuestionSubmitResponseDTO dto = new QuestionSubmitResponseDTO(); + BeanUtils.copyProperties(submit, dto); + if (StringUtils.isBlank(submit.getJudgeInfo())) { + return dto; + } + try { + JudgeInfo judgeInfo = objectMapper.readValue(submit.getJudgeInfo(), JudgeInfo.class); + dto.setJudgeInfo(judgeInfo); + + }catch (JsonProcessingException e) { + throw new ServiceException("判题信息解析失败", e, ErrorCode.SYSTEM_ERROR); + } + return dto; + } + + private boolean tryAcquireSubmitLock(Long userId, Long questionId) { + String lockKey = getSubmitLockKey(userId, questionId); + return Boolean.TRUE.equals( + stringRedisTemplate.opsForValue().setIfAbsent(lockKey, String.valueOf(System.currentTimeMillis()), SUBMIT_LOCK_TTL) + ); + } + + private void releaseSubmitLock(Long userId, Long questionId) { + stringRedisTemplate.delete(getSubmitLockKey(userId, questionId)); + } + + private String getSubmitLockKey(Long userId, Long questionId) { + return RedisKeyConstants.QUESTION_SUBMIT_LOCK_KEY_PREFIX + userId + ":" + questionId; + } + + private void ensureNoInProgressSubmit(Long userId, Long questionId) { + Long count = this.lambdaQuery() + .eq(QuestionSubmit::getUserId, userId) + .eq(QuestionSubmit::getQuestionId, questionId) + .in(QuestionSubmit::getStatus, + QuestionSubmitStatusEnum.WAITING.getValue(), + QuestionSubmitStatusEnum.JUDGING.getValue()) + .count(); + if (count != null && count > 0) { + throw new ClientException("当前有判题进行中,请稍后再提交", ErrorCode.OPERATION_ERROR); + } + } + + private boolean shouldReleaseLock(Integer status) { + return status != null && (status.equals(QuestionSubmitStatusEnum.SUCCESS.getValue()) + || status.equals(QuestionSubmitStatusEnum.FAILED.getValue())); + } } diff --git a/aioj-backend-question-service/src/main/resources/application.yml b/aioj-backend-question-service/src/main/resources/application.yml index a134019..db304f2 100644 --- a/aioj-backend-question-service/src/main/resources/application.yml +++ b/aioj-backend-question-service/src/main/resources/application.yml @@ -3,6 +3,11 @@ spring: name: aioj-question-service profiles: active: @env@ + devtools: + livereload: + enabled: true + restart: + enabled: true server: port: 18083 servlet: diff --git a/aioj-backend-upms/aioj-backend-upms-biz/pom.xml b/aioj-backend-upms/aioj-backend-upms-biz/pom.xml index e604039..11ddf5a 100644 --- a/aioj-backend-upms/aioj-backend-upms-biz/pom.xml +++ b/aioj-backend-upms/aioj-backend-upms-biz/pom.xml @@ -41,4 +41,17 @@ spring-boot-starter-web + + + + + + org.springframework.boot + spring-boot-maven-plugin + + false + + + + \ No newline at end of file diff --git a/aioj-backend-user-service/pom.xml b/aioj-backend-user-service/pom.xml index 059a541..c84fceb 100644 --- a/aioj-backend-user-service/pom.xml +++ b/aioj-backend-user-service/pom.xml @@ -95,4 +95,17 @@ test + + + + + + org.springframework.boot + spring-boot-maven-plugin + + false + + + + \ No newline at end of file diff --git a/aioj-backend-user-service/src/main/java/cn/meowrain/aioj/backend/userservice/config/AsyncConfig.java b/aioj-backend-user-service/src/main/java/cn/meowrain/aioj/backend/userservice/config/AsyncConfig.java index d985b08..9049771 100644 --- a/aioj-backend-user-service/src/main/java/cn/meowrain/aioj/backend/userservice/config/AsyncConfig.java +++ b/aioj-backend-user-service/src/main/java/cn/meowrain/aioj/backend/userservice/config/AsyncConfig.java @@ -5,6 +5,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; +import org.springframework.security.task.DelegatingSecurityContextTaskDecorator; import java.util.concurrent.Executor; import java.util.concurrent.ThreadPoolExecutor; @@ -31,6 +32,7 @@ public class AsyncConfig { executor.setQueueCapacity(100); // 线程名前缀 executor.setThreadNamePrefix("email-async-"); + executor.setTaskDecorator(new DelegatingSecurityContextTaskDecorator()); // 拒绝策略:由调用线程执行 executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); executor.initialize(); diff --git a/aioj-backend-user-service/src/main/java/cn/meowrain/aioj/backend/userservice/service/impl/UserServiceImpl.java b/aioj-backend-user-service/src/main/java/cn/meowrain/aioj/backend/userservice/service/impl/UserServiceImpl.java index d88afac..1977ac0 100644 --- a/aioj-backend-user-service/src/main/java/cn/meowrain/aioj/backend/userservice/service/impl/UserServiceImpl.java +++ b/aioj-backend-user-service/src/main/java/cn/meowrain/aioj/backend/userservice/service/impl/UserServiceImpl.java @@ -1,6 +1,7 @@ package cn.meowrain.aioj.backend.userservice.service.impl; import cn.hutool.crypto.digest.BCrypt; +import cn.meowrain.aioj.backend.framework.core.enums.UserRoleEnum; import cn.meowrain.aioj.backend.framework.core.errorcode.ErrorCode; import cn.meowrain.aioj.backend.framework.core.exception.ClientException; import cn.meowrain.aioj.backend.framework.core.exception.ServiceException; @@ -52,7 +53,7 @@ public class UserServiceImpl extends ServiceImpl implements Us String encryptPassword = BCrypt.hashpw(request.getUserPassword(), salt); User user = new User().setUserAccount(request.getUserAccount()) .setUserPassword(encryptPassword) - .setUserRole("user") + .setUserRole(UserRoleEnum.USER.getCode()) .setCreateTime(now) .setUpdateTime(now); try { diff --git a/docs/admin-permission-guide.md b/docs/admin-permission-guide.md new file mode 100644 index 0000000..f2dc4aa --- /dev/null +++ b/docs/admin-permission-guide.md @@ -0,0 +1,252 @@ +# 管理员权限检查功能使用指南 + +## 功能位置 + +全局管理员权限检查功能已在 `aioj-backend-common-core` 模块中实现,所有服务模块都可以直接使用。 + +## 核心类 + +### 1. UserRoleEnum(角色枚举) +**位置**: `cn.meowrain.aioj.backend.framework.core.enums.UserRoleEnum` + +定义了系统中的所有用户角色: +- `USER` - 普通用户(code: "user") +- `ADMIN` - 管理员(code: "admin") +- `BAN` - 封禁用户(code: "ban") + +### 2. ContextHolderUtils(上下文工具类) +**位置**: `cn.meowrain.aioj.backend.framework.core.utils.ContextHolderUtils` + +提供了获取当前登录用户信息的工具方法。 + +## 使用方法 + +### 1. 检查当前用户是否为管理员 + +```java +import cn.meowrain.aioj.backend.framework.core.utils.ContextHolderUtils; + +// 在任何需要检查管理员权限的地方 +if (ContextHolderUtils.isAdmin()) { + // 管理员才能执行的操作 + log.info("当前用户是管理员"); +} else { + // 非管理员的处理逻辑 + throw new ClientException("无权限执行此操作"); +} +``` + +### 2. 获取当前用户ID + +```java +// 获取当前登录用户ID(未登录会抛出异常) +Long userId = ContextHolderUtils.getCurrentUserId(); + +// 获取当前用户ID(未登录返回null) +Long userId = ContextHolderUtils.getCurrentUserIdOrNull(); +``` + +### 3. 获取当前用户角色 + +```java +// 获取当前用户角色字符串 +String role = ContextHolderUtils.getCurrentUserRole(); + +// 使用枚举进行角色判断 +if (UserRoleEnum.isAdmin(role)) { + // 管理员逻辑 +} +``` + +### 4. 检查用户是否已登录 + +```java +if (ContextHolderUtils.isAuthenticated()) { + // 用户已登录 +} +``` + +## 在Controller中使用示例 + +### 示例1:限制删除操作仅管理员可用 + +```java +@DeleteMapping("/{id}") +@Operation(summary = "删除题目") +public Result deleteQuestion(@PathVariable Long id) { + // 检查管理员权限 + if (!ContextHolderUtils.isAdmin()) { + throw new ClientException("只有管理员才能删除题目"); + } + + questionService.deleteById(id); + return Result.success(); +} +``` + +### 示例2:根据角色返回不同数据 + +```java +@GetMapping("/list") +@Operation(summary = "获取题目列表") +public Result> listQuestions() { + boolean isAdmin = ContextHolderUtils.isAdmin(); + + if (isAdmin) { + // 管理员可以看到所有题目(包括未发布的) + return Result.success(questionService.listAll()); + } else { + // 普通用户只能看到已发布的题目 + return Result.success(questionService.listPublished()); + } +} +``` + +### 示例3:限制更新操作(仅作者或管理员) + +```java +@PutMapping("/{id}") +@Operation(summary = "更新题目") +public Result updateQuestion( + @PathVariable Long id, + @RequestBody QuestionUpdateDTO request) { + + Question question = questionService.getById(id); + Long currentUserId = ContextHolderUtils.getCurrentUserId(); + boolean isAdmin = ContextHolderUtils.isAdmin(); + + // 只有题目创建者或管理员可以更新 + if (!isAdmin && !question.getCreatorId().equals(currentUserId)) { + throw new ClientException("无权限修改此题目"); + } + + questionService.updateQuestion(id, request); + return Result.success(); +} +``` + +## 在Service层使用示例 + +```java +@Service +public class QuestionServiceImpl implements QuestionService { + + @Override + public void deleteQuestion(Long id) { + // 在Service层也可以进行权限检查 + if (!ContextHolderUtils.isAdmin()) { + throw new ServiceException("权限不足"); + } + + // 执行删除逻辑 + this.removeById(id); + } +} +``` + +## 使用Spring Security注解(推荐) + +除了手动检查,还可以使用Spring Security的注解: + +```java +import org.springframework.security.access.prepost.PreAuthorize; + +@DeleteMapping("/{id}") +@PreAuthorize("hasRole('admin')") // 注意:角色名会自动转换为小写 +@Operation(summary = "删除题目") +public Result deleteQuestion(@PathVariable Long id) { + questionService.deleteById(id); + return Result.success(); +} +``` + +**注意**:使用 `@PreAuthorize` 需要在配置类上启用方法级安全: + +```java +@Configuration +@EnableMethodSecurity // Spring Boot 3.x +public class SecurityConfig { + // ... +} +``` + +## 角色枚举使用 + +在需要设置或比较角色时,使用枚举常量: + +```java +import cn.meowrain.aioj.backend.framework.core.enums.UserRoleEnum; + +// 设置用户角色 +user.setUserRole(UserRoleEnum.ADMIN.getCode()); + +// 判断角色 +if (UserRoleEnum.isAdmin(user.getUserRole())) { + // 管理员逻辑 +} + +// 根据代码获取枚举 +UserRoleEnum role = UserRoleEnum.fromCode("admin"); +if (role == UserRoleEnum.ADMIN) { + // 管理员逻辑 +} +``` + +## 注意事项 + +1. **大小写不敏感**:角色比较不区分大小写,"admin"、"Admin"、"ADMIN" 都会被识别为管理员 +2. **异常处理**:`getCurrentUserId()` 和 `getCurrentUserRole()` 在用户未登录时会抛出 `IllegalStateException` +3. **安全性**:建议在Controller和Service层都进行权限检查,实现多层防护 +4. **JWT依赖**:此功能依赖JWT认证,确保请求头中包含有效的JWT token + +## 完整示例:题目管理Controller + +```java +@RestController +@RequestMapping("/api/question") +@RequiredArgsConstructor +public class QuestionController { + + private final QuestionService questionService; + + @PostMapping + @Operation(summary = "创建题目(仅管理员)") + public Result createQuestion(@RequestBody QuestionCreateDTO request) { + if (!ContextHolderUtils.isAdmin()) { + throw new ClientException("只有管理员才能创建题目"); + } + Long questionId = questionService.create(request); + return Result.success(questionId); + } + + @PutMapping("/{id}") + @Operation(summary = "更新题目(仅管理员)") + public Result updateQuestion( + @PathVariable Long id, + @RequestBody QuestionUpdateDTO request) { + if (!ContextHolderUtils.isAdmin()) { + throw new ClientException("只有管理员才能更新题目"); + } + questionService.update(id, request); + return Result.success(); + } + + @DeleteMapping("/{id}") + @Operation(summary = "删除题目(仅管理员)") + public Result deleteQuestion(@PathVariable Long id) { + if (!ContextHolderUtils.isAdmin()) { + throw new ClientException("只有管理员才能删除题目"); + } + questionService.deleteById(id); + return Result.success(); + } + + @GetMapping("/{id}") + @Operation(summary = "获取题目详情") + public Result getQuestion(@PathVariable Long id) { + // 普通用户和管理员都可以查看 + QuestionDTO question = questionService.getById(id); + return Result.success(question); + } +} +``` diff --git a/pom.xml b/pom.xml index 471303b..4be22f2 100644 --- a/pom.xml +++ b/pom.xml @@ -129,6 +129,17 @@ org.springframework.boot spring-boot-maven-plugin ${spring-boot.version} + + + true + + + + + repackage + + +