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:
2026-01-28 23:01:48 +08:00
parent 67825a8c5c
commit 1945cc2fb1
52 changed files with 1561 additions and 89 deletions

301
CLAUDE.md
View File

@@ -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
View File

@@ -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 JARSpring 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`: 构建/工具链相关
## 许可证
[添加许可证信息]
## 联系方式
[添加联系方式]

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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);
}
}

View File

@@ -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);

View File

@@ -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;
}
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -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());

View File

@@ -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>

View File

@@ -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={}",

View File

@@ -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>

View File

@@ -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>

View File

@@ -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() {
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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);
}

View File

@@ -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()));

View File

@@ -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()));
}
}

View File

@@ -3,6 +3,11 @@ spring:
name: aioj-question-service
profiles:
active: @env@
devtools:
livereload:
enabled: true
restart:
enabled: true
server:
port: 18083
servlet:

View File

@@ -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>

View File

@@ -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>

View File

@@ -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();

View File

@@ -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 {

View 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
View File

@@ -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} 变量 -->