feat: 添加管理员权限检查功能和Maven打包配置优化
主要更新: 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 配置
This commit is contained in:
301
CLAUDE.md
301
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 <module-name>`
|
||||
- **Example**: `mvn spring-boot:run -pl aioj-backend-auth`
|
||||
|
||||
### Docker Image Build (using JIB)
|
||||
- **Build and push Docker image**: `mvn package jib:build -pl <module-name>`
|
||||
- **Build Docker image to tar file**: `mvn package jib:buildTar -pl <module-name>`
|
||||
- **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`
|
||||
- **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.<service-name>
|
||||
├── 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
|
||||
└── <ServiceName>Application.java # Main application class
|
||||
```
|
||||
|
||||
### Naming Conventions
|
||||
- **Controllers**: `<Resource>Controller` (e.g., `QuestionController`)
|
||||
- **Services**: `<Resource>Service` (interface) and `<Resource>ServiceImpl` (implementation)
|
||||
- **Mappers**: `<Resource>Mapper` (e.g., `QuestionMapper`)
|
||||
- **Entities**: Noun representing the domain object (e.g., `Question`, `User`)
|
||||
- **DTOs**: `<Operation><Resource>DTO` (e.g., `CreateQuestionDTO`, `UpdateQuestionDTO`)
|
||||
- **Validation Chains**: `<Resource><Property>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`
|
||||
|
||||
291
README.md
291
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 <repository-url>
|
||||
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文件:
|
||||
- `<service-name>-1.0.0.jar` - 可执行的Fat JAR(包含所有依赖,约60-100MB)
|
||||
- `<service-name>-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 <module>`: 指定要构建的模块
|
||||
- `-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 <service>.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`: 构建/工具链相关
|
||||
|
||||
## 许可证
|
||||
|
||||
[添加许可证信息]
|
||||
|
||||
## 联系方式
|
||||
|
||||
[添加联系方式]
|
||||
|
||||
@@ -99,6 +99,15 @@
|
||||
</extension>
|
||||
</extensions>
|
||||
<plugins>
|
||||
<!-- 启用 Spring Boot Maven 插件的 repackage -->
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<skip>false</skip>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
<!-- Protobuf 编译插件 -->
|
||||
<plugin>
|
||||
<groupId>org.xolstice.maven.plugins</groupId>
|
||||
|
||||
@@ -107,4 +107,17 @@
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<!-- 启用 Spring Boot Maven 插件的 repackage -->
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<skip>false</skip>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@@ -96,4 +96,17 @@
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<!-- 启用 Spring Boot Maven 插件的 repackage -->
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<skip>false</skip>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
|
||||
@@ -90,4 +90,17 @@
|
||||
<artifactId>hutool-crypto</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<!-- 启用 Spring Boot Maven 插件的 repackage -->
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<skip>false</skip>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@@ -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={}",
|
||||
|
||||
@@ -83,4 +83,17 @@
|
||||
<artifactId>spring-boot-starter-actuator</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<!-- 启用 Spring Boot Maven 插件的 repackage -->
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<skip>false</skip>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@@ -94,5 +94,28 @@
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-actuator</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-devtools</artifactId>
|
||||
<scope>runtime</scope>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<!-- 启用 Spring Boot Maven 插件的 repackage -->
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<skip>false</skip>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@@ -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() {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Void> 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<Page<QuestionSubmitResponseDTO>> getSubmitPage(
|
||||
@Parameter(description = "查询条件") QuestionSubmitQueryRequestDTO request) {
|
||||
Page<QuestionSubmitResponseDTO> dtoPage = questionSubmitService.listQuestionSubmits(request);
|
||||
return Results.success(dtoPage);
|
||||
}
|
||||
/**
|
||||
* 内部接口:更新提交状态
|
||||
* PATCH /v1/question-submits/{id}/status
|
||||
|
||||
@@ -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<Question
|
||||
return 20;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -55,3 +55,4 @@ public class QuestionDifficultyVerifyChain implements AbstractChianHandler<Quest
|
||||
return 30;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -47,3 +47,4 @@ public class QuestionEditContentVerifyChain implements AbstractChianHandler<Ques
|
||||
return 30;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -55,3 +55,4 @@ public class QuestionEditDifficultyVerifyChain implements AbstractChianHandler<Q
|
||||
return 40;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -103,3 +103,4 @@ public class QuestionEditJudgeConfigVerifyChain implements AbstractChianHandler<
|
||||
return 50;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -78,3 +78,4 @@ public class QuestionEditTagsVerifyChain implements AbstractChianHandler<Questio
|
||||
return 60;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -47,3 +47,4 @@ public class QuestionEditTitleVerifyChain implements AbstractChianHandler<Questi
|
||||
return 20;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -105,3 +105,4 @@ public class QuestionJudgeConfigVerifyChain implements AbstractChianHandler<Ques
|
||||
return 40;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -88,3 +88,4 @@ public class QuestionTagsVerifyChain implements AbstractChianHandler<QuestionCre
|
||||
return 50;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -52,3 +52,4 @@ public class QuestionTitleVerifyChain implements AbstractChianHandler<QuestionCr
|
||||
return 10;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -47,3 +47,4 @@ public class QuestionUpdateContentVerifyChain implements AbstractChianHandler<Qu
|
||||
return 30;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -55,3 +55,4 @@ public class QuestionUpdateDifficultyVerifyChain implements AbstractChianHandler
|
||||
return 40;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -51,3 +51,4 @@ public class QuestionUpdateExistVerifyChain implements AbstractChianHandler<Ques
|
||||
return 10;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -103,3 +103,4 @@ public class QuestionUpdateJudgeConfigVerifyChain implements AbstractChianHandle
|
||||
return 50;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -78,3 +78,4 @@ public class QuestionUpdateTagsVerifyChain implements AbstractChianHandler<Quest
|
||||
return 60;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -47,3 +47,4 @@ public class QuestionUpdateTitleVerifyChain implements AbstractChianHandler<Ques
|
||||
return 20;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package cn.meowrain.aioj.backend.question.dto.chains;
|
||||
package cn.meowrain.aioj.backend.question.dto.chains.submit;
|
||||
|
||||
import cn.meowrain.aioj.backend.framework.core.designpattern.chains.AbstractChianHandler;
|
||||
import cn.meowrain.aioj.backend.framework.core.errorcode.ErrorCode;
|
||||
@@ -87,3 +87,4 @@ public class CodeVerifyChain implements AbstractChianHandler<QuestionSubmitReque
|
||||
return 40;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
package cn.meowrain.aioj.backend.question.dto.chains;
|
||||
package cn.meowrain.aioj.backend.question.dto.chains.submit;
|
||||
|
||||
import cn.meowrain.aioj.backend.framework.core.designpattern.chains.AbstractChianHandler;
|
||||
import cn.meowrain.aioj.backend.framework.core.errorcode.ErrorCode;
|
||||
import cn.meowrain.aioj.backend.framework.core.exception.ClientException;
|
||||
import cn.meowrain.aioj.backend.question.common.enums.ChainMarkEnums;
|
||||
import cn.meowrain.aioj.backend.question.common.enums.LanguageEnum;
|
||||
import cn.meowrain.aioj.backend.question.dto.req.QuestionSubmitRequestDTO;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
@@ -11,6 +12,7 @@ import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 编程语言校验责任链处理器
|
||||
@@ -22,23 +24,9 @@ public class LanguageVerifyChain implements AbstractChianHandler<QuestionSubmitR
|
||||
/**
|
||||
* 支持的编程语言列表
|
||||
*/
|
||||
private static final List<String> SUPPORTED_LANGUAGES = Arrays.asList(
|
||||
"java",
|
||||
"cpp",
|
||||
"python",
|
||||
"go",
|
||||
"javascript",
|
||||
"c",
|
||||
"csharp",
|
||||
"rust",
|
||||
"php",
|
||||
"swift",
|
||||
"kotlin",
|
||||
"typescript",
|
||||
"ruby",
|
||||
"shell"
|
||||
);
|
||||
|
||||
private static final List<String> 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<QuestionSubmitR
|
||||
return 30;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package cn.meowrain.aioj.backend.question.dto.chains;
|
||||
package cn.meowrain.aioj.backend.question.dto.chains.submit;
|
||||
|
||||
import cn.meowrain.aioj.backend.framework.core.designpattern.chains.AbstractChianHandler;
|
||||
import cn.meowrain.aioj.backend.framework.core.errorcode.ErrorCode;
|
||||
@@ -46,3 +46,4 @@ public class QuestionExistVerifyChain implements AbstractChianHandler<QuestionSu
|
||||
return 10;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
package cn.meowrain.aioj.backend.question.dto.chains;
|
||||
package cn.meowrain.aioj.backend.question.dto.chains.submit;
|
||||
|
||||
import cn.meowrain.aioj.backend.framework.core.designpattern.chains.AbstractChianHandler;
|
||||
import cn.meowrain.aioj.backend.framework.core.errorcode.ErrorCode;
|
||||
import cn.meowrain.aioj.backend.framework.core.exception.ClientException;
|
||||
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.Question;
|
||||
import cn.meowrain.aioj.backend.question.dto.req.QuestionSubmitRequestDTO;
|
||||
import cn.meowrain.aioj.backend.question.service.QuestionService;
|
||||
@@ -55,3 +56,4 @@ public class QuestionStatusVerifyChain implements AbstractChianHandler<QuestionS
|
||||
return 20;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package cn.meowrain.aioj.backend.question.dto.chains.submit;
|
||||
|
||||
import cn.meowrain.aioj.backend.framework.core.designpattern.chains.AbstractChianHandler;
|
||||
import cn.meowrain.aioj.backend.framework.core.errorcode.ErrorCode;
|
||||
import cn.meowrain.aioj.backend.framework.core.exception.ClientException;
|
||||
import cn.meowrain.aioj.backend.question.common.enums.ChainMarkEnums;
|
||||
import cn.meowrain.aioj.backend.question.dto.req.QuestionSubmitRequestDTO;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 题目提交 - 题目ID校验责任链处理器
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class QuestionSubmitIdVerifyChain implements AbstractChianHandler<QuestionSubmitRequestDTO> {
|
||||
|
||||
@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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<QuestionSubmit> 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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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<QuestionSubmit> {
|
||||
* @return 提交记录
|
||||
*/
|
||||
QuestionSubmit getSubmitById(Long submitId);
|
||||
|
||||
/**
|
||||
* 分页查询题目提交记录
|
||||
* @param request 请求体
|
||||
* @return
|
||||
*/
|
||||
Page<QuestionSubmitResponseDTO> listQuestionSubmits(QuestionSubmitQueryRequestDTO request);
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ public class QuestionServiceImpl extends ServiceImpl<QuestionMapper, Question> 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<QuestionMapper, Question> i
|
||||
}
|
||||
|
||||
// 处理复杂字段
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
if (requestDTO.getTags() != null && !requestDTO.getTags().isEmpty()) {
|
||||
try {
|
||||
questionToUpdate.setTags(mapper.writeValueAsString(requestDTO.getTags()));
|
||||
|
||||
@@ -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<QuestionSubmitMapper, QuestionSubmit> 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<QuestionSubmitResponseDTO> listQuestionSubmits(QuestionSubmitQueryRequestDTO request) {
|
||||
LambdaQueryWrapper<QuestionSubmit> 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<QuestionSubmit> page = this.page(request, wrapper);
|
||||
Page<QuestionSubmitResponseDTO> 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()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,11 @@ spring:
|
||||
name: aioj-question-service
|
||||
profiles:
|
||||
active: @env@
|
||||
devtools:
|
||||
livereload:
|
||||
enabled: true
|
||||
restart:
|
||||
enabled: true
|
||||
server:
|
||||
port: 18083
|
||||
servlet:
|
||||
|
||||
@@ -41,4 +41,17 @@
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<!-- 启用 Spring Boot Maven 插件的 repackage -->
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<skip>false</skip>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@@ -95,4 +95,17 @@
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<!-- 启用 Spring Boot Maven 插件的 repackage -->
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<skip>false</skip>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@@ -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();
|
||||
|
||||
@@ -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<UserMapper, User> 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 {
|
||||
|
||||
252
docs/admin-permission-guide.md
Normal file
252
docs/admin-permission-guide.md
Normal file
@@ -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<Void> deleteQuestion(@PathVariable Long id) {
|
||||
// 检查管理员权限
|
||||
if (!ContextHolderUtils.isAdmin()) {
|
||||
throw new ClientException("只有管理员才能删除题目");
|
||||
}
|
||||
|
||||
questionService.deleteById(id);
|
||||
return Result.success();
|
||||
}
|
||||
```
|
||||
|
||||
### 示例2:根据角色返回不同数据
|
||||
|
||||
```java
|
||||
@GetMapping("/list")
|
||||
@Operation(summary = "获取题目列表")
|
||||
public Result<List<QuestionDTO>> 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<Void> 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<Void> 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<Long> 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<Void> 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<Void> deleteQuestion(@PathVariable Long id) {
|
||||
if (!ContextHolderUtils.isAdmin()) {
|
||||
throw new ClientException("只有管理员才能删除题目");
|
||||
}
|
||||
questionService.deleteById(id);
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
@Operation(summary = "获取题目详情")
|
||||
public Result<QuestionDTO> getQuestion(@PathVariable Long id) {
|
||||
// 普通用户和管理员都可以查看
|
||||
QuestionDTO question = questionService.getById(id);
|
||||
return Result.success(question);
|
||||
}
|
||||
}
|
||||
```
|
||||
11
pom.xml
11
pom.xml
@@ -129,6 +129,17 @@
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<version>${spring-boot.version}</version>
|
||||
<configuration>
|
||||
<!-- 默认跳过 repackage,避免 common 模块打包失败 -->
|
||||
<skip>true</skip>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>repackage</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
|
||||
<!-- Flatten 插件:处理 ${revision} 变量 -->
|
||||
|
||||
Reference in New Issue
Block a user