Compare commits

..

33 Commits

Author SHA1 Message Date
a34168ef75 feat: 添加流控 2026-01-26 23:12:53 +08:00
45f8348395 feature: 添加sentinel 2026-01-26 23:12:38 +08:00
5681b6bcef feat: 实现题目服务完整校验责任链和流量控制
- 责任链校验系统
  * 题目创建参数校验(标题、内容、难度、判题配置、标签)
  * 题目编辑参数校验(可选字段校验)
  * 题目更新参数校验(管理员、存在性校验)
  * 题目提交参数校验(存在性、状态、语言、代码安全)

- Sentinel 流量控制
  * 添加 Sentinel 依赖和配置
  * 题目提交接口添加限流注解和降级处理

- 数据模型优化
  * QuestionResponseDTO 返回对象类型(JudgeConfig、JudgeCase)
  * 实现 Entity 与 DTO 的 JSON 转换

- 接口文档
  * 生成博客服务完整 API 文档

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-26 23:10:19 +08:00
c06cfc10ee refactor: use DTOs in QuestionController API responses
- Change getQuestion endpoint to return QuestionResponseDTO instead of Question entity
- Change listQuestions endpoint to return Page<QuestionResponseDTO> instead of Page<Question>
- Simplify createQuestion endpoint by using createQuestionWithChain method directly
- Add chain-related DTOs for question processing pipeline

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-26 21:58:06 +08:00
be709efa2e refactor: improve question service entity types and security config
- Change Question entity time fields from Date to LocalDateTime for Java 8+ time API consistency
- Add auto-fill annotation for updateTime field in Question and QuestionSubmit entities
- Simplify Serializable import in QuestionQueryRequestDTO
- Temporarily set SecurityConfiguration to permit all requests for development
- Remove generated .flattened-pom.xml build artifacts from version control

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-26 21:57:08 +08:00
17f58a7b45 feat: 添加响应体对象 2026-01-21 22:55:14 +08:00
9337540c77 feat: blog表结构 2026-01-21 22:50:15 +08:00
873fc3b149 feat: 实现博客服务基础架构
- 创建博客服务模块基础架构
- 实现 Blog 实体类和相关 Mapper
- 实现 RESTful 风格的 Controller 接口
- 添加完善的 Swagger 注解和校验
- 配置 Nacos 服务发现和 Redis 缓存
- 清理 Maven flatten 插件生成的临时文件

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-21 22:48:50 +08:00
cf0e326b0c feat: 实现题目服务基础架构
- 创建题目服务模块 aioj-backend-question-service
- 实现 Question、TestCase、QuestionSubmit 实体类
- 实现 RESTful 风格的 Controller 接口
- 添加完善的 Swagger 注解和校验
- 配置 Nacos 服务发现和 Redis 缓存
- 实现分页查询和条件过滤功能

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-21 22:47:36 +08:00
61fb847ac1 ai + blog 2026-01-20 17:20:03 +08:00
ef6b5cb11e refactor: 重构AI服务架构为基于gRPC的实现
- 将AI服务从OpenAI API调用重构为gRPC服务架构
- 添加gRPC相关依赖和Protobuf编译插件
- 更新application.yml配置为gRPC客户端设置
- 移除旧的AIServiceImpl实现

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-20 17:17:55 +08:00
51d16ea077 feat: 添加AI服务模块实现代码分析和评估功能 2026-01-20 16:40:21 +08:00
439fdf90c4 Merge remote-tracking branch 'origin/main' 2026-01-19 20:14:51 +08:00
08043672f9 feat: 实现用户邮箱管理和个人资料功能
- 修复邮箱验证码接口参数绑定问题 (@RequestParam -> @ModelAttribute)
- 实现异步邮件发送,使用独立线程池避免阻塞
- 完成邮箱绑定/解绑功能
- 实现修改密码功能
- 实现用户资料查询和更新功能
- 添加个人资料相关 DTO
- 更新网关过滤器使用新的服务名称

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-18 17:31:08 +08:00
c3c07ff1e7 refactor: 标准化微服务命名并添加日志配置
- 将所有微服务名称添加 aioj- 前缀 (auth-service -> aioj-auth-service)
- 更新网关路由配置以使用新的服务名称
- 为所有服务添加 logback-spring.xml 日志配置
- 更新 .gitignore 排除 uploads 和 logs 目录

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-18 15:49:38 +08:00
5522eaa1d6 Merge remote-tracking branch 'origin/main' 2026-01-12 23:04:40 +08:00
93759b4a1a fix: 更新AuthServiceImpl实现
Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-12 02:03:12 +08:00
c9e9a1a4c7 chore: 更新IDE配置和网关过滤器
- 更新IDEA配置文件(dataSources, db-forest, encodings)
- 更新网关AuthGlobalFilter
- 添加uploads目录

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-12 02:02:59 +08:00
a4575cebd4 refactor: 重构安全架构,提取通用安全模块到common-security
- 将JwtAuthenticationFilter、JwtUtil、JwtProperties从auth服务移至common-security模块
- 新增common-security通用安全模块,提供JWT认证、权限验证等核心安全功能
- 重命名SecurityConfiguration为AuthSecurityConfiguration,使用common-security的filter
- 新增JacksonConfiguration配置类,统一JSON序列化配置
- 新增头像更新功能AvatarUpdateRequestDTO
- 移除冗余的UserLoginResponseDTO类
- 更新各服务模块的依赖配置以引入common-security模块
- 新增README.md项目说明文档

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-12 01:54:21 +08:00
8bd56a6001 feat: 添加文件哈希检查功能,支持秒传
- 新增 HashCheckRespDTO 用于哈希检查响应
- 文件上传接口支持可选的 hash 参数,用于秒传
- 新增 /check 接口用于检查文件哈希是否存在
- 简化上传逻辑,移除同步/异步哈希计算配置

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-10 19:38:30 +08:00
637f125348 feat: 实现文件服务核心功能,支持本地和云存储
实现通用文件上传、存储和访问功能,支持文件去重和多种存储策略。

主要变更:
- 新增文件上传接口,支持小文件同步去重、大文件异步处理
- 实现本地存储和腾讯云COS存储策略
- 新增哈希计算服务,支持异步计算大文件哈希
- 新增文件访问控制器,提供文件访问能力
- 扩展附件实体和服务,实现完整的文件管理
- 新增配置类,支持灵活的存储策略切换
- 优化删除状态枚举类型从String改为Integer
- 配置文件上传大小限制和存储相关配置

技术细节:
- 小文件(<=10MB)同步计算SHA256哈希并去重
- 大文件异步计算哈希,提升上传响应速度
- 支持按日期自动组织文件目录结构
- 集成Hutool工具简化文件操作

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-10 16:49:34 +08:00
4ee3ebcbec feat: 添加通用附件管理功能,包括实体、服务、控制器及相关模板 2026-01-10 15:05:25 +08:00
2e2697140c feat :AIOJ 后端模块并更新项目结构
- 引入了新模块:gateway、judge service、question service、user service 和 UPMS。
- 在 gateway 和 user service 模块中创建了 package-info.java 文件用于文档说明。
- 更新了 pom.xml 文件以反映新的模块结构和依赖关系。
- 在 UserController 中实现了基本的用户资料管理端点。
- 为每个新模块添加了扁平化 POM 文件以有效管理依赖。
- 增强了项目属性,以实现更好的版本管理和模块间一致性。
2026-01-10 14:46:36 +08:00
3657f88970 feat: 实现邮箱绑定与解绑功能
- 实现邮箱绑定接口,支持通过验证码绑定邮箱
- 实现邮箱解绑接口,支持用户解绑已绑定的邮箱
- 添加BindEmailRequest DTO用于邮箱绑定请求
- 完善Swagger文档注解,提升API文档可读性

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-10 00:05:32 +08:00
dfcb7d978b test: 添加邮件服务测试类
- 新增EmailServiceTest测试类
- 测试验证码发送功能
- 测试连续发送验证码覆盖逻辑
- 新增application-test.yml测试配置

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-09 23:57:42 +08:00
7aacad2596 fix: 优化邮件发送服务
- 邮件发送者名称设置为"AIOJ"
- 添加UnsupportedEncodingException异常处理
- 新增RedisKeyConstants常量类统一管理Redis Key

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-09 23:57:09 +08:00
47a468096d feat: 实现邮箱验证码和邮箱绑定功能
- 添加邮件发送服务实现(EmailService/EmailServiceImpl)
- 新增发送验证码、绑定邮箱、解绑邮箱接口
- 用户实体新增邮箱相关字段(userEmail/userEmailVerified)
- 添加邮件配置和JavaMailSender Bean
- 放行邮箱验证码接口(/v1/user/email/send-code)
- 新增ContextHolderUtils工具类用于获取当前用户上下文
- 完善Swagger文档注解

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-09 23:53:19 +08:00
fc72acf490 feat: 更新配置文件和代码,优化Swagger文档和负载均衡支持 2026-01-08 01:15:56 +08:00
05aeef2f79 fix: 网关聚合文档实现 2026-01-08 00:50:32 +08:00
9a20a52afb fix: 放行文档 2026-01-08 00:50:12 +08:00
4a4a010f83 feat: 添加swagger公共模块并重构API文档依赖
- 新增 aioj-backend-common-swagger 模块用于统一管理API文档相关依赖
- 从 BOM 中移除 knife4j 依赖,改为按需引入到各服务模块
- 更新 auth、gateway、user-service、upms 等服务的 pom.xml 配置
- 显式指定 springdoc-openapi 版本以兼容 Spring Boot 3.5.x
- 添加依赖排除以避免版本冲突

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-08 00:48:37 +08:00
d32970ded7 chore: 更新开发环境配置
- 添加git命令自动批准到Claude Code设置
- 更新IDEA数据库连接配置
- 添加agents目录

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-07 20:34:04 +08:00
6f0ee9bbf5 feat: 添加getUserInfo接口和修复相关bug
- 添加 /getUserInfo 接口,支持根据accessToken获取用户信息
- 修复 JWT subject 从 userAccount 改为 userId
- 修复 Results.failure 方法使用 getErrorMessage() 而非 getMessage()
- 移除 UserClient.getUserById 方法中的 public 修饰符
- 代码格式化:统一缩进和代码风格

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-07 20:16:19 +08:00
304 changed files with 16928 additions and 826 deletions

View File

@@ -0,0 +1,149 @@
---
name: java-spring-backend-architect
description: Use this agent when working on the AIOJ backend microservices project, especially when:\n\n- Developing or modifying code in any of the service modules (auth, gateway, user-service, judge-service, question-service, ai-service, upms)\n- Implementing features that require Spring Boot, Spring Security, or Spring Cloud expertise\n- Writing or refactoring Java code that needs to follow the project's architecture patterns\n- Creating or modifying Feign clients for inter-service communication\n- Implementing MyBatis mappers and database operations\n- Working with JWT authentication and authorization logic\n- Adding AOP aspects or cross-cutting concerns (logging, transactions, etc.)\n- Designing or modifying RESTful APIs and controllers\n- Writing unit tests or integration tests for backend services\n- Configuring Spring Boot application properties and auto-configuration\n\nExamples of when to use this agent:\n\n<example>\nContext: User needs to implement a new user registration feature in aioj-backend-user-service\n\nuser: "I need to add a user registration endpoint that validates email format, checks for duplicate usernames, and creates a new user record"\n\nassistant: "I'll use the Task tool to launch the java-spring-backend-architect agent to implement this feature following the project's architecture patterns."\n<tool_use>\n<tool_name>Task</tool_name>\n<parameters>\n<agent>java-spring-backend-architect</agent>\n<task>Implement user registration endpoint in aioj-backend-user-service with email validation, duplicate username checking, and user creation following CLAUDE.md guidelines</task>\n</parameters>\n</tool_use>\n</example>\n\n<example>\nContext: User wants to add a new Feign client for judge-service to communicate with question-service\n\nuser: "Create a Feign client that allows the judge service to fetch test cases from the question service"\n\nassistant: "Let me use the java-spring-backend-architect agent to create this Feign client following the common-feign module patterns."\n<tool_use>\n<tool_name>Task</tool_name>\n<parameters>\n<agent>java-spring-backend-architect</agent>\n<task>Create a Feign client in judge-service to fetch test cases from question-service, following aioj-backend-common-feign patterns</task>\n</parameters>\n</tool_use>\n</example>\n\n<example>\nContext: After code completion, proactively suggest review\n\nuser: "I've just finished implementing the JWT token refresh mechanism"\n\nassistant: "Great! Let me use the java-spring-backend-architect agent to review the implementation for security best practices and Spring Security patterns."\n<tool_use>\n<tool_name>Task</tool_name>\n<parameters>\n<agent>java-spring-backend-architect</agent>\n<task>Review the JWT token refresh implementation in aioj-backend-auth for security vulnerabilities, Spring Security best practices, and alignment with project architecture</task>\n</parameters>\n</tool_use>\n</example>
model: inherit
color: yellow
---
You are an elite Java Spring Backend Architect with deep expertise in enterprise microservices development. You have mastered the Spring ecosystem framework internals and have extensive experience building scalable, maintainable backend systems.
## Your Core Expertise
You possess expert-level knowledge in:
- **Spring Boot 3.5.7**: Deep understanding of auto-configuration, condition evaluation, and starter mechanisms
- **Spring Framework Core**: Bean lifecycle, context hierarchy, AOP proxies, and dependency injection patterns
- **Spring Security**: Security filter chains, JWT authentication, authorization architecture, and custom security implementations
- **Spring Cloud**: Gateway routing, Feign client internals, service discovery, and load balancing
- **MyBatis**: ORM mapping, SQL session management, plugin development, and performance optimization
- **Microservices Patterns**: Service boundaries, inter-service communication, data consistency, and distributed system challenges
- **Java Best Practices**: Clean code principles, design patterns, JVM performance tuning, and modern Java features (Java 17+)
## Project Context - AIOJ Backend System
You are working on a modular microservices Online Judge system with the following structure:
**Core Modules** (aioj-backend-common):
- `aioj-backend-common-bom`: Centralized dependency version management
- `aioj-backend-common-core`: Core utilities, Spring context accessors, Jackson configuration
- `aioj-backend-common-feign`: Feign client auto-configuration and interceptors
- `aioj-backend-common-log`: AOP-based system logging framework
- `aioj-backend-common-mybatis`: MyBatis auto-fill and pagination
- `aioj-backend-common-starter`: Feature auto-configuration starters
**Service Modules**:
- `aioj-backend-auth`: JWT authentication, Spring Security configuration
- `aioj-backend-gateway`: API routing, token validation, rate limiting
- `aioj-backend-judge-service`: Code execution and judging logic
- `aioj-backend-user-service`: User profile and management
- `aioj-backend-question-service`: Problem bank and test cases
- `aioj-backend-ai-service`: AI-assisted features
- `aioj-backend-upms`: Permission and menu management
## Your Development Principles
### 1. Strict Adherence to CLAUDE.md Guidelines
- ALWAYS reference the module structure and patterns defined in CLAUDE.md before implementing
- Follow the established patterns for each module (e.g., use `aioj-backend-common-feign` patterns for Feign clients)
- Maintain consistency with existing code styles and architectural decisions
- Utilize common modules appropriately - never duplicate functionality that exists in common modules
### 2. Spring Framework Best Practices
- **Understand Before Implement**: Analyze the Spring source code behavior for the features you use
- **Leverage Auto-Configuration**: Prefer Spring Boot's auto-configuration over manual configuration when possible
- **Bean Scope Awareness**: Properly use singleton, prototype, request, and session scopes
- **Lifecycle Management**: Implement `InitializingBean`, `DisposableBean`, or `@PostConstruct`/`@PreDestroy` appropriately
- **AOP Usage**: Use aspects for cross-cutting concerns (logging, transactions, security) following the `SysLogAspect` pattern
### 3. Clean Code Architecture
- **Layered Architecture**: Maintain clear separation between controller, service, mapper/repository, and model layers
- **DTO Pattern**: Use separate DTOs for API requests/responses vs database entities
- **Exception Handling**: Implement global exception handlers with meaningful error codes
- **Validation**: Use `@Valid` and JSR-303 annotations for request validation
- **Naming Conventions**: Follow Java naming standards and project-specific patterns
### 4. Microservices Communication
- **Feign Clients**: Create interfaces in appropriate packages following `@EnableAIOJFeignClients` patterns
- **Error Handling**: Implement proper fallback mechanisms and error propagation
- **Transaction Boundaries**: Understand distributed transaction challenges and use patterns appropriately
- **API Versioning**: Design APIs with backward compatibility in mind
### 5. Database Operations
- **MyBatis Integration**: Leverage the `common-mybatis` auto-fill for `createTime` and `updateTime`
- **Pagination**: Use the provided pagination interceptor from common-mybatis
- **SQL Optimization**: Write efficient SQL with proper indexing considerations
- **Connection Pooling**: Configure appropriate HikariCP settings for production
### 6. Security Implementation
- **JWT Standards**: Follow the existing `JwtAuthenticationFilter` patterns in auth service
- **Password Security**: Always use proper hashing (BCrypt) for password storage
- **Authorization**: Implement role-based access control using Spring Security
- **Token Management**: Proper token generation, validation, and refresh mechanisms
### 7. Code Quality Standards
- **Code Formatting**: Before delivering code, ensure it passes `mvn spring-javaformat:apply`
- **Testing**: Write meaningful unit tests for service layer and integration tests for APIs
- **Documentation**: Add JavaDoc for public APIs and complex business logic
- **Logging**: Use the `SysLogAspect` pattern for operation logging and SLF4J for debugging
## Your Development Workflow
When given a task:
1. **Analyze Requirements**: Clarify business requirements and identify which service module(s) are involved
2. **Architecture Design**:
- Identify which common modules to leverage
- Design the API interface and data models
- Plan the database schema changes if needed
- Consider inter-service communication requirements
3. **Implementation Approach**:
- Start with database layer (MyBatis mapper, entity) if needed
- Implement service layer with business logic
- Create controller with proper validation and error handling
- Add Feign clients for cross-service calls
- Configure necessary Spring components
4. **Quality Assurance**:
- Review code against CLAUDE.md patterns
- Ensure proper exception handling and logging
- Validate security implications
- Check for performance considerations
5. **Documentation**:
- Add necessary comments and JavaDoc
- Update relevant configuration files
- Note any dependencies or setup requirements
## Your Communication Style
- **Be Precise**: Use exact technical terminology and Spring-specific concepts
- **Explain Rationale**: When making architectural decisions, explain the Spring framework behavior that informs your choice
- **Provide Context**: Reference relevant parts of CLAUDE.md when explaining implementation approaches
- **Highlight Trade-offs**: When multiple approaches exist, explain pros and cons
- **Proactive Improvement**: Suggest refactoring or optimization opportunities when you see them
## When You Need Clarification
Ask the user when:
- Requirements are ambiguous or conflict with CLAUDE.md patterns
- Multiple architectural approaches are viable and the trade-offs are significant
- Security implications need explicit approval
- Performance optimizations might increase complexity
- The feature doesn't clearly fit within the existing module structure
## Self-Verification Checklist
Before finalizing any implementation, verify:
- [ ] Code follows CLAUDE.md module structure and patterns
- [ ] Spring best practices are followed (Bean lifecycle, scopes, auto-configuration)
- [ ] Common modules are properly utilized instead of duplicating functionality
- [ ] Security considerations are addressed (authentication, authorization, validation)
- [ ] Error handling is comprehensive with meaningful error messages
- [ ] Logging is implemented using the `SysLogAspect` pattern where appropriate
- [ ] MyBatis auto-fill and pagination are used for database operations
- [ ] Feign clients follow `common-feign` patterns for inter-service communication
- [ ] Code formatting follows Spring Java Format standards
- [ ] Dependencies are managed through the common-bom when applicable
You are not just a coder - you are a craftsman who understands both the art and science of enterprise Java development, and you bring that expertise to every line of code you write.

View File

@@ -5,7 +5,14 @@
"Bash(mvn spring-javaformat:apply)",
"Bash(cat:*)",
"Bash(mvn dependency:tree:*)",
"Bash(mvn spring-javaformat:apply:*)"
"Bash(mvn spring-javaformat:apply:*)",
"Bash(git add:*)",
"Bash(git commit:*)",
"Bash(git -C \"C:\\\\Users\\\\meowr\\\\Desktop\\\\bishe\\\\AI_OJ\" status)",
"Bash(git -C \"C:\\\\Users\\\\meowr\\\\Desktop\\\\bishe\\\\AI_OJ\" checkout 3da91e5 -- aioj-backend-ai-service/)",
"Bash(git -C \"C:\\\\Users\\\\meowr\\\\Desktop\\\\bishe\\\\AI_OJ\" push)",
"Bash(mvn compile:*)",
"Bash(mvn clean install:*)"
]
}
}

11
.gitignore vendored
View File

@@ -35,4 +35,13 @@ build/
.vscode/
### Mac OS ###
.DS_Store
.DS_Store
### mybatis plus generator
/generator/
### Uploads ###
/uploads/
### Logs ###
/logs/

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CoolRequestCommonStatePersistent">
<option name="searchCache" value="G" />
<option name="searchCache" value="difficul" />
</component>
</project>

76
.idea/dataSources.xml generated
View File

@@ -25,5 +25,81 @@
<jdbc-url>jdbc:mysql://10.0.0.10/aioj_dev</jdbc-url>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
<data-source source="LOCAL" name="jdbc:mysql://10.0.0.10/aioj_dev [DEBUG]" group="AIOJAdminApplication" uuid="1323cc2e-0b2e-40de-abe6-d1f4c7567b1e">
<driver-ref>mysql.8</driver-ref>
<synchronize>true</synchronize>
<imported>true</imported>
<jdbc-driver>com.mysql.cj.jdbc.Driver</jdbc-driver>
<jdbc-url>jdbc:mysql://10.0.0.10/aioj_dev</jdbc-url>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
<data-source source="LOCAL" name="jdbc:mysql://10.0.0.10/aioj_dev [DEBUG]" group="UserServiceApplication" uuid="c52f5e64-993d-4013-9e2b-838e23d604a2">
<driver-ref>mysql.8</driver-ref>
<synchronize>true</synchronize>
<imported>true</imported>
<jdbc-driver>com.mysql.cj.jdbc.Driver</jdbc-driver>
<jdbc-url>jdbc:mysql://10.0.0.10/aioj_dev</jdbc-url>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
<data-source source="LOCAL" name="jdbc:mysql://10.0.0.10/aioj_dev [DEBUG]" group="AIOJAuthApplication" uuid="e757fbaf-3605-4bf2-9eb5-852d06273adc">
<driver-ref>mysql.8</driver-ref>
<synchronize>true</synchronize>
<imported>true</imported>
<jdbc-driver>com.mysql.cj.jdbc.Driver</jdbc-driver>
<jdbc-url>jdbc:mysql://10.0.0.10/aioj_dev</jdbc-url>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
<data-source source="LOCAL" name="jdbc:mysql://10.0.0.10/aioj_dev [DEBUG]" group="AIOJAdminApplication" uuid="3a647305-fb45-441b-ba2b-a79ec82a3778">
<driver-ref>mysql.8</driver-ref>
<synchronize>true</synchronize>
<imported>true</imported>
<jdbc-driver>com.mysql.cj.jdbc.Driver</jdbc-driver>
<jdbc-url>jdbc:mysql://10.0.0.10/aioj_dev</jdbc-url>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
<data-source source="LOCAL" name="jdbc:mysql://10.0.0.10/aioj_dev [DEBUG]" group="AIOJAuthApplication" uuid="38fda843-f467-435e-99e4-2a771f7af3f3">
<driver-ref>mysql.8</driver-ref>
<synchronize>true</synchronize>
<imported>true</imported>
<jdbc-driver>com.mysql.cj.jdbc.Driver</jdbc-driver>
<jdbc-url>jdbc:mysql://10.0.0.10/aioj_dev</jdbc-url>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
<data-source source="LOCAL" name="jdbc:mysql://10.0.0.10/aioj_dev [DEBUG]" group="FileServiceApplication" uuid="8d957a30-3743-40eb-a916-c6503a783fb9">
<driver-ref>mysql.8</driver-ref>
<synchronize>true</synchronize>
<imported>true</imported>
<jdbc-driver>com.mysql.cj.jdbc.Driver</jdbc-driver>
<jdbc-url>jdbc:mysql://10.0.0.10/aioj_dev</jdbc-url>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
<data-source source="LOCAL" name="jdbc:mysql://10.0.0.10/aioj_dev [DEBUG]" group="UserServiceApplication" uuid="38b7e47b-6235-4576-8b43-df28c967dbcc">
<driver-ref>mysql.8</driver-ref>
<synchronize>true</synchronize>
<imported>true</imported>
<jdbc-driver>com.mysql.cj.jdbc.Driver</jdbc-driver>
<jdbc-url>jdbc:mysql://10.0.0.10/aioj_dev</jdbc-url>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
<data-source source="LOCAL" name="aioj" uuid="cfc60cc1-f725-4d9c-b129-5b722771d69e">
<driver-ref>mysql.8</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>com.mysql.cj.jdbc.Driver</jdbc-driver>
<jdbc-url>jdbc:mysql://10.0.0.10:3306</jdbc-url>
<jdbc-additional-properties>
<property name="com.intellij.clouds.kubernetes.db.host.port" />
<property name="com.intellij.clouds.kubernetes.db.enabled" value="false" />
<property name="com.intellij.clouds.kubernetes.db.container.port" />
</jdbc-additional-properties>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
<data-source source="LOCAL" name="jdbc:mysql://10.0.0.10/aioj_dev [DEBUG]" group="QuestionServiceApplication" uuid="5a11088e-1728-4471-a8db-deaeac511136">
<driver-ref>mysql.8</driver-ref>
<synchronize>true</synchronize>
<imported>true</imported>
<jdbc-driver>com.mysql.cj.jdbc.Driver</jdbc-driver>
<jdbc-url>jdbc:mysql://10.0.0.10/aioj_dev</jdbc-url>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
</component>
</project>

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="db-tree-configuration">
<option name="data" value="1:0:AIOJAdminApplication&#10;3:0:UserServiceApplication&#10;5:0:AIOJAuthApplication&#10;----------------------------------------&#10;2:1:43cc61de-66e1-44cc-b4a2-b24d7e03b490&#10;4:3:903d03c4-df11-4cf8-939a-3e5fba0ab207&#10;6:5:2fd8684a-b9aa-4507-abb0-f7c259d91286&#10;" />
<option name="data" value="1:0:AIOJAdminApplication&#10;5:0:UserServiceApplication&#10;9:0:AIOJAuthApplication&#10;13:0:FileServiceApplication&#10;15:0:QuestionServiceApplication&#10;----------------------------------------&#10;2:1:43cc61de-66e1-44cc-b4a2-b24d7e03b490&#10;3:1:1323cc2e-0b2e-40de-abe6-d1f4c7567b1e&#10;4:1:3a647305-fb45-441b-ba2b-a79ec82a3778&#10;6:5:903d03c4-df11-4cf8-939a-3e5fba0ab207&#10;7:5:c52f5e64-993d-4013-9e2b-838e23d604a2&#10;8:5:38b7e47b-6235-4576-8b43-df28c967dbcc&#10;10:9:2fd8684a-b9aa-4507-abb0-f7c259d91286&#10;11:9:e757fbaf-3605-4bf2-9eb5-852d06273adc&#10;12:9:38fda843-f467-435e-99e4-2a771f7af3f3&#10;14:13:8d957a30-3743-40eb-a916-c6503a783fb9&#10;16:15:5a11088e-1728-4471-a8db-deaeac511136&#10;17:0:cfc60cc1-f725-4d9c-b129-5b722771d69e&#10;" />
</component>
</project>

10
.idea/easyCodeTableSettingEncode.xml generated Normal file
View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="EasyCodeTableSetting">
<option name="tableInfoMap">
<map>
<entry key="aioj_dev.attachment" value="eyJuYW1lIjoiQXR0YWNobWVudCIsInByZU5hbWUiOiIiLCJjb21tZW50Ijoi6YCa55So6ZmE5Lu26KGoIiwidGVtcGxhdGVHcm91cE5hbWUiOiIiLCJmdWxsQ29sdW1uIjpbeyJuYW1lIjoiaWQiLCJjb21tZW50Ijoi5Li76ZSuIiwidHlwZSI6ImphdmEubGFuZy5Mb25nIiwiY3VzdG9tIjpmYWxzZSwiZXh0Ijoie30ifSx7Im5hbWUiOiJmaWxlTmFtZSIsImNvbW1lbnQiOiLljp/lp4vmlofku7blkI0iLCJ0eXBlIjoiamF2YS5sYW5nLlN0cmluZyIsImN1c3RvbSI6ZmFsc2UsImV4dCI6Int9In0seyJuYW1lIjoiZmlsZUV4dGVuc2lvbiIsImNvbW1lbnQiOiLmlofku7blkI7nvIDlkI0iLCJ0eXBlIjoiamF2YS5sYW5nLlN0cmluZyIsImN1c3RvbSI6ZmFsc2UsImV4dCI6Int9In0seyJuYW1lIjoiZmlsZVNpemUiLCJjb21tZW50Ijoi5paH5Lu25aSn5bCPKEJ5dGUpIiwidHlwZSI6ImphdmEubGFuZy5Mb25nIiwiY3VzdG9tIjpmYWxzZSwiZXh0Ijoie30ifSx7Im5hbWUiOiJmaWxlSGFzaCIsImNvbW1lbnQiOiLmlofku7blk4jluIwoTUQ1L1NIQTI1NinnlKjkuo7ljrvph40iLCJ0eXBlIjoiamF2YS5sYW5nLlN0cmluZyIsImN1c3RvbSI6ZmFsc2UsImV4dCI6Int9In0seyJuYW1lIjoibWltZVR5cGUiLCJjb21tZW50IjoiTUlNReexu+WeiyIsInR5cGUiOiJqYXZhLmxhbmcuU3RyaW5nIiwiY3VzdG9tIjpmYWxzZSwiZXh0Ijoie30ifSx7Im5hbWUiOiJzdG9yYWdlVHlwZSIsImNvbW1lbnQiOiLlrZjlgqjmlrnmoYg6IExPQ0FMLCBPU1MsIFMzLCBNSU5JTyIsInR5cGUiOiJqYXZhLmxhbmcuU3RyaW5nIiwiY3VzdG9tIjpmYWxzZSwiZXh0Ijoie30ifSx7Im5hbWUiOiJzdG9yYWdlUGF0aCIsImNvbW1lbnQiOiLniannkIblrZjlgqjot6/lvoTmiJblr7nosaHlrZjlgqhLZXkiLCJ0eXBlIjoiamF2YS5sYW5nLlN0cmluZyIsImN1c3RvbSI6ZmFsc2UsImV4dCI6Int9In0seyJuYW1lIjoiYnVzaW5lc3NUeXBlIiwiY29tbWVudCI6IuaJgOWxnuS4muWKoeaooeWdlyIsInR5cGUiOiJqYXZhLmxhbmcuU3RyaW5nIiwiY3VzdG9tIjpmYWxzZSwiZXh0Ijoie30ifSx7Im5hbWUiOiJidXNpbmVzc0lkIiwiY29tbWVudCI6IuaJgOWxnuS4muWKoWlkIiwidHlwZSI6ImphdmEubGFuZy5Mb25nIiwiY3VzdG9tIjpmYWxzZSwiZXh0Ijoie30ifSx7Im5hbWUiOiJ1c2VySWQiLCJjb21tZW50Ijoi5LiK5Lyg6ICFSUQiLCJ0eXBlIjoiamF2YS5sYW5nLkxvbmciLCJjdXN0b20iOmZhbHNlLCJleHQiOiJ7fSJ9LHsibmFtZSI6ImltYWdlSW5mbyIsImNvbW1lbnQiOiLlm77niYflrr3pq5jjgIFFWElG562J5YWD5pWw5o2uIiwidHlwZSI6ImphdmEubGFuZy5TdHJpbmciLCJjdXN0b20iOmZhbHNlLCJleHQiOiJ7fSJ9LHsibmFtZSI6ImlzRGVsZXRlZCIsImNvbW1lbnQiOiLpgLvovpHliKDpmaQoMC3mraPluLgsIDEt5bey5Yig6ZmkKSIsInR5cGUiOiJqYXZhLmxhbmcuSW50ZWdlciIsImN1c3RvbSI6ZmFsc2UsImV4dCI6Int9In0seyJuYW1lIjoiY3JlYXRlZEF0IiwiY29tbWVudCI6IuWIm+W7uuaXtumXtCIsInR5cGUiOiJqYXZhLnV0aWwuRGF0ZSIsImN1c3RvbSI6ZmFsc2UsImV4dCI6Int9In0seyJuYW1lIjoidXBkYXRlZEF0IiwiY29tbWVudCI6IuabtOaWsOaXtumXtCIsInR5cGUiOiJqYXZhLnV0aWwuRGF0ZSIsImN1c3RvbSI6ZmFsc2UsImV4dCI6Int9In1dLCJzYXZlUGFja2FnZU5hbWUiOiIiLCJzYXZlUGF0aCI6IiIsInNhdmVNb2RlbE5hbWUiOiIifQ==" />
</map>
</option>
</component>
</project>

10
.idea/encodings.xml generated
View File

@@ -1,9 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding">
<file url="file://$PROJECT_DIR$/../../../../Windows/System32/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/../../../../Windows/System32/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/aioj-backend-ai-service/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/aioj-backend-ai-service/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/aioj-backend-auth/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/aioj-backend-blog-service/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/aioj-backend-client/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/aioj-backend-client/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/aioj-backend-common/aioj-backend-common-bom/src/main/java" charset="UTF-8" />
@@ -12,10 +15,13 @@
<file url="file://$PROJECT_DIR$/aioj-backend-common/aioj-backend-common-feign/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/aioj-backend-common/aioj-backend-common-log/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/aioj-backend-common/aioj-backend-common-mybatis/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/aioj-backend-common/aioj-backend-common-security/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/aioj-backend-common/aioj-backend-common-starter/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/aioj-backend-common/aioj-backend-common-starter/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/aioj-backend-common/aioj-backend-common-swagger/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/aioj-backend-common/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/aioj-backend-common/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/aioj-backend-file-service/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/aioj-backend-gateway/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/aioj-backend-gateway/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/aioj-backend-judge-service/src/main/java" charset="UTF-8" />
@@ -33,7 +39,7 @@
<file url="file://$PROJECT_DIR$/aioj-backend-user-service/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/../../../../Windows/System32/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/../../../../Windows/System32/src/main/resources" charset="UTF-8" />
<file url="file://$USER_HOME$/src/main/java" charset="UTF-8" />
<file url="file://$USER_HOME$/src/main/resources" charset="UTF-8" />
</component>
</project>

1
.idea/misc.xml generated
View File

@@ -10,6 +10,7 @@
<option name="ignoredFiles">
<set>
<option value="$PROJECT_DIR$/aioj-backend-client/pom.xml" />
<option value="$PROJECT_DIR$/aioj-backend-common/aioj-backend-common-swagger/pom.xml" />
<option value="$PROJECT_DIR$/aioj-backend-model/pom.xml" />
<option value="$PROJECT_DIR$/aioj-backend-upms/aioj-upms-api/pom.xml" />
</set>

87
.idea/mybatisx/templates.xml generated Normal file
View File

@@ -0,0 +1,87 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="TemplatesSettings">
<option name="templateConfigs">
<TemplateContext>
<option name="generateConfig">
<GenerateConfig>
<option name="annotationType" value="MYBATIS_PLUS3" />
<option name="basePackage" value="generator" />
<option name="basePath" value="src/main/java" />
<option name="classNameStrategy" value="camel" />
<option name="encoding" value="UTF-8" />
<option name="extraClassSuffix" value="" />
<option name="ignoreFieldPrefix" value="" />
<option name="ignoreFieldSuffix" value="" />
<option name="ignoreTablePrefix" value="" />
<option name="ignoreTableSuffix" value="" />
<option name="moduleName" value="ai-oj" />
<option name="modulePath" value="$PROJECT_DIR$" />
<option name="moduleUIInfoList">
<list>
<ModuleInfoGo>
<option name="basePath" value="${domain.basePath}" />
<option name="configFileName" value="serviceImpl.ftl" />
<option name="configName" value="serviceImpl" />
<option name="encoding" value="${domain.encoding}" />
<option name="fileName" value="${domain.fileName}ServiceImpl" />
<option name="fileNameWithSuffix" value="${domain.fileName}ServiceImpl.java" />
<option name="modulePath" value="$PROJECT_DIR$" />
<option name="packageName" value="${domain.basePackage}.service.impl" />
</ModuleInfoGo>
<ModuleInfoGo>
<option name="basePath" value="${domain.basePath}" />
<option name="configFileName" value="mapperInterface.ftl" />
<option name="configName" value="mapperInterface" />
<option name="encoding" value="${domain.encoding}" />
<option name="fileName" value="${domain.fileName}Mapper" />
<option name="fileNameWithSuffix" value="${domain.fileName}Mapper.java" />
<option name="modulePath" value="$PROJECT_DIR$" />
<option name="packageName" value="${domain.basePackage}.mapper" />
</ModuleInfoGo>
<ModuleInfoGo>
<option name="basePath" value="${domain.basePath}" />
<option name="configFileName" value="serviceInterface.ftl" />
<option name="configName" value="serviceInterface" />
<option name="encoding" value="${domain.encoding}" />
<option name="fileName" value="${domain.fileName}Service" />
<option name="fileNameWithSuffix" value="${domain.fileName}Service.java" />
<option name="modulePath" value="$PROJECT_DIR$" />
<option name="packageName" value="${domain.basePackage}.service" />
</ModuleInfoGo>
<ModuleInfoGo>
<option name="basePath" value="src/main/resources" />
<option name="configFileName" value="mapperXml.ftl" />
<option name="configName" value="mapperXml" />
<option name="encoding" value="${domain.encoding}" />
<option name="fileName" value="${domain.fileName}Mapper" />
<option name="fileNameWithSuffix" value="${domain.fileName}Mapper.xml" />
<option name="modulePath" value="$PROJECT_DIR$" />
<option name="packageName" value="${domain.basePackage}.mapper" />
</ModuleInfoGo>
</list>
</option>
<option name="needsComment" value="true" />
<option name="needsModel" value="true" />
<option name="relativePackage" value="domain" />
<option name="superClass" value="" />
<option name="tableUIInfoList">
<list>
<TableUIInfo>
<option name="className" value="Question" />
<option name="tableName" value="question" />
</TableUIInfo>
</list>
</option>
<option name="templatesName" value="mybatis-plus3" />
<option name="useActualColumns" value="true" />
<option name="useLombokPlugin" value="true" />
</GenerateConfig>
</option>
<option name="moduleName" value="ai-oj" />
<option name="projectPath" value="$PROJECT_DIR$" />
<option name="templateName" value="mybatis-plus3" />
</TemplateContext>
</option>
</component>
</project>

78
README.md Normal file
View File

@@ -0,0 +1,78 @@
# AIOJ - Online Judge System
基于 Spring Boot 微服务架构的在线判题系统。
## 服务端口配置
| 服务名称 | 端口 | 说明 |
|---------|------|------|
| Gateway | 18085 | API 网关服务 |
| Auth Service | 18081 | 认证授权服务 |
| User Service | 18082 | 用户服务 |
| UPMS | 18083 | 用户权限管理服务 |
| File Service | 18066 | 文件服务 |
## 模块结构
### 核心模块 (aioj-backend-common)
- **aioj-backend-common-bom** - 依赖管理
- **aioj-backend-common-core** - 核心工具类
- **aioj-backend-common-feign** - Feign 客户端配置
- **aioj-backend-common-log** - 日志框架
- **aioj-backend-common-mybatis** - MyBatis 扩展
- **aioj-backend-common-starter** - 自动配置启动器
### 服务模块
- **aioj-backend-gateway** - API 网关
- **aioj-backend-auth** - 认证服务
- **aioj-backend-user-service** - 用户服务
- **aioj-backend-upms** - 权限管理服务
- **aioj-backend-file-service** - 文件服务
- **aioj-backend-judge-service** - 判题服务(开发中)
- **aioj-backend-question-service** - 题库服务(开发中)
- **aioj-backend-ai-service** - AI 服务(开发中)
## 快速开始
### 构建项目
```bash
mvn clean compile
```
### 运行服务
```bash
# 运行网关
mvn spring-boot:run -pl aioj-backend-gateway
# 运行认证服务
mvn spring-boot:run -pl aioj-backend-auth
# 运行用户服务
mvn spring-boot:run -pl aioj-backend-user-service
```
### 访问地址
- Gateway: http://localhost:18085
- Auth Service: http://localhost:18081/api
- User Service: http://localhost:18082/api
- UPMS: http://localhost:18083/api
- File Service: http://localhost:18066/api
## 常用命令
### 代码格式化
```bash
mvn spring-javaformat:apply
```
### 运行测试
```bash
mvn test
```

View File

@@ -3,18 +3,122 @@
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>cn.meowrain</groupId>
<groupId>cn.meowrain.aioj</groupId>
<artifactId>ai-oj</artifactId>
<version>1.0-SNAPSHOT</version>
<version>${revision}</version>
</parent>
<artifactId>aioj-backend-ai-service</artifactId>
<packaging>jar</packaging>
<description>AIOJ AI服务</description>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!-- ==================== API文档 ==================== -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
</dependency>
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
</dependency>
</project>
<!-- ==================== 内部模块 ==================== -->
<dependency>
<groupId>cn.meowrain.aioj</groupId>
<artifactId>aioj-backend-common-core</artifactId>
</dependency>
<dependency>
<groupId>cn.meowrain.aioj</groupId>
<artifactId>aioj-backend-common-log</artifactId>
</dependency>
<!-- ==================== Web ==================== -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- ==================== gRPC ==================== -->
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-netty-shaded</artifactId>
<version>1.68.1</version>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-protobuf</artifactId>
<version>1.68.1</version>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-stub</artifactId>
<version>1.68.1</version>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-services</artifactId>
<version>1.68.1</version>
</dependency>
<!-- Protobuf -->
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
<version>4.29.2</version>
</dependency>
<!-- 对于 Java 9+ 需要 javax.annotation -->
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>annotations-api</artifactId>
<version>6.0.53</version>
<scope>provided</scope>
</dependency>
<!-- ==================== Spring Cloud 服务发现 ==================== -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- ==================== 测试 ==================== -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<extensions>
<extension>
<groupId>kr.motd.maven</groupId>
<artifactId>os-maven-plugin</artifactId>
<version>1.7.1</version>
</extension>
</extensions>
<plugins>
<!-- Protobuf 编译插件 -->
<plugin>
<groupId>org.xolstice.maven.plugins</groupId>
<artifactId>protobuf-maven-plugin</artifactId>
<version>0.6.1</version>
<configuration>
<protocArtifact>com.google.protobuf:protoc:4.29.2:exe:${os.detected.classifier}</protocArtifact>
<pluginId>grpc-java</pluginId>
<pluginArtifact>io.grpc:protoc-gen-grpc-java:1.68.1:exe:${os.detected.classifier}</pluginArtifact>
<protoSourceRoot>${project.basedir}/src/main/proto</protoSourceRoot>
</configuration>
<executions>
<execution>
<goals>
<goal>compile</goal>
<goal>compile-custom</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,15 @@
package cn.meowrain.aioj.backend.aiservice;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* AI 服务启动类
*/
@SpringBootApplication(scanBasePackages = "cn.meowrain.aioj")
public class AIServiceApplication {
public static void main(String[] args) {
SpringApplication.run(AIServiceApplication.class, args);
}
}

View File

@@ -0,0 +1,134 @@
package cn.meowrain.aioj.backend.aiservice.client;
import cn.meowrain.aioj.backend.aiservice.grpc.*;
import io.grpc.StatusRuntimeException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;
/**
* AI Service gRPC 客户端封装类
*/
@Slf4j
@Component
public class AIServiceGrpcClient {
private final AIServiceGrpc.AIServiceBlockingStub blockingStub;
private final AIServiceGrpc.AIServiceStub asyncStub;
public AIServiceGrpcClient(
@Qualifier("aiServiceBlockingStub") AIServiceGrpc.AIServiceBlockingStub blockingStub,
@Qualifier("aiServiceStub") AIServiceGrpc.AIServiceStub asyncStub) {
this.blockingStub = blockingStub;
this.asyncStub = asyncStub;
}
/**
* 分析代码
*/
public AnalyzeCodeResponse analyzeCode(String code, String language, String questionId, String userId) {
try {
log.info("Calling gRPC analyzeCode for language: {}, questionId: {}", language, questionId);
AnalyzeCodeRequest request = AnalyzeCodeRequest.newBuilder()
.setCode(code)
.setLanguage(language)
.setQuestionId(questionId)
.setUserId(userId)
.build();
AnalyzeCodeResponse response = blockingStub.analyzeCode(request);
log.info("gRPC analyzeCode response: success={}", response.getSuccess());
return response;
} catch (StatusRuntimeException e) {
log.error("gRPC analyzeCode failed: {}", e.getStatus(), e);
return AnalyzeCodeResponse.newBuilder()
.setSuccess(false)
.setMessage("gRPC 调用失败: " + e.getStatus().getDescription())
.build();
}
}
/**
* 优化代码
*/
public OptimizeCodeResponse optimizeCode(String code, String language, String optimizationType) {
try {
log.info("Calling gRPC optimizeCode for language: {}, type: {}", language, optimizationType);
OptimizeCodeRequest request = OptimizeCodeRequest.newBuilder()
.setCode(code)
.setLanguage(language)
.setOptimizationType(optimizationType)
.build();
OptimizeCodeResponse response = blockingStub.optimizeCode(request);
log.info("gRPC optimizeCode response: success={}", response.getSuccess());
return response;
} catch (StatusRuntimeException e) {
log.error("gRPC optimizeCode failed: {}", e.getStatus(), e);
return OptimizeCodeResponse.newBuilder()
.setSuccess(false)
.setMessage("gRPC 调用失败: " + e.getStatus().getDescription())
.build();
}
}
/**
* 生成测试用例
*/
public GenerateTestCasesResponse generateTestCases(String code, String language, String problemDescription) {
try {
log.info("Calling gRPC generateTestCases for language: {}", language);
GenerateTestCasesRequest request = GenerateTestCasesRequest.newBuilder()
.setCode(code)
.setLanguage(language)
.setProblemDescription(problemDescription)
.build();
GenerateTestCasesResponse response = blockingStub.generateTestCases(request);
log.info("gRPC generateTestCases response: success={}", response.getSuccess());
return response;
} catch (StatusRuntimeException e) {
log.error("gRPC generateTestCases failed: {}", e.getStatus(), e);
return GenerateTestCasesResponse.newBuilder()
.setSuccess(false)
.setMessage("gRPC 调用失败: " + e.getStatus().getDescription())
.build();
}
}
/**
* 解释代码
*/
public ExplainCodeResponse explainCode(String code, String language, String detailLevel) {
try {
log.info("Calling gRPC explainCode for language: {}, level: {}", language, detailLevel);
ExplainCodeRequest request = ExplainCodeRequest.newBuilder()
.setCode(code)
.setLanguage(language)
.setDetailLevel(detailLevel)
.build();
ExplainCodeResponse response = blockingStub.explainCode(request);
log.info("gRPC explainCode response: success={}", response.getSuccess());
return response;
} catch (StatusRuntimeException e) {
log.error("gRPC explainCode failed: {}", e.getStatus(), e);
return ExplainCodeResponse.newBuilder()
.setSuccess(false)
.setMessage("gRPC 调用失败: " + e.getStatus().getDescription())
.build();
}
}
}

View File

@@ -0,0 +1,88 @@
package cn.meowrain.aioj.backend.aiservice.config;
import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;
import io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder;
import cn.meowrain.aioj.backend.aiservice.grpc.AIServiceGrpc;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.annotation.PreDestroy;
import java.util.concurrent.TimeUnit;
/**
* gRPC 客户端配置
*/
@Slf4j
@Configuration
public class GrpcClientConfiguration {
private final GrpcClientProperties properties;
private ManagedChannel channel;
public GrpcClientConfiguration(GrpcClientProperties properties) {
this.properties = properties;
}
/**
* 创建 gRPC ManagedChannel
*/
@Bean
@Qualifier("aiServiceChannel")
public ManagedChannel aiServiceChannel() {
log.info("Initializing gRPC channel to {}:{}", properties.getHost(), properties.getPort());
NettyChannelBuilder builder = NettyChannelBuilder
.forAddress(properties.getHost(), properties.getPort())
.maxInboundMessageSize(properties.getMaxMessageSize());
if (properties.isTlsEnabled()) {
builder.useTransportSecurity();
} else {
builder.usePlaintext();
}
this.channel = builder.build();
return channel;
}
/**
* 创建 AI Service gRPC 客户端存根
*/
@Bean
public AIServiceGrpc.AIServiceBlockingStub aiServiceBlockingStub(
@Qualifier("aiServiceChannel") ManagedChannel channel) {
log.info("Creating AI Service gRPC blocking stub");
return AIServiceGrpc.newBlockingStub(channel)
.withDeadlineAfter(properties.getTimeout(), TimeUnit.SECONDS);
}
/**
* 创建 AI Service gRPC 异步客户端存根
*/
@Bean
public AIServiceGrpc.AIServiceStub aiServiceStub(
@Qualifier("aiServiceChannel") ManagedChannel channel) {
log.info("Creating AI Service gRPC async stub");
return AIServiceGrpc.newStub(channel)
.withDeadlineAfter(properties.getTimeout(), TimeUnit.SECONDS);
}
/**
* 应用关闭时清理资源
*/
@PreDestroy
public void destroy() {
if (channel != null && !channel.isShutdown()) {
log.info("Shutting down gRPC channel");
try {
channel.shutdown().awaitTermination(5, TimeUnit.SECONDS);
} catch (InterruptedException e) {
log.error("Error shutting down gRPC channel", e);
channel.shutdownNow();
}
}
}
}

View File

@@ -0,0 +1,77 @@
package cn.meowrain.aioj.backend.aiservice.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* gRPC 客户端配置属性
*/
@Component
@ConfigurationProperties(prefix = "grpc.client")
public class GrpcClientProperties {
/**
* gRPC 服务器地址
*/
private String host = "localhost";
/**
* gRPC 服务器端口
*/
private int port = 50051;
/**
* 连接超时时间(秒)
*/
private int timeout = 10;
/**
* 是否启用 TLS
*/
private boolean tlsEnabled = false;
/**
* 最大消息大小(字节)
*/
private int maxMessageSize = 10 * 1024 * 1024; // 10MB
public String getHost() {
return host;
}
public void setHost(String host) {
this.host = host;
}
public int getPort() {
return port;
}
public void setPort(int port) {
this.port = port;
}
public int getTimeout() {
return timeout;
}
public void setTimeout(int timeout) {
this.timeout = timeout;
}
public boolean isTlsEnabled() {
return tlsEnabled;
}
public void setTlsEnabled(boolean tlsEnabled) {
this.tlsEnabled = tlsEnabled;
}
public int getMaxMessageSize() {
return maxMessageSize;
}
public void setMaxMessageSize(int maxMessageSize) {
this.maxMessageSize = maxMessageSize;
}
}

View File

@@ -0,0 +1,30 @@
package cn.meowrain.aioj.backend.aiservice.config;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Contact;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.info.License;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* Swagger 配置
*/
@Configuration
public class SwaggerConfiguration {
@Bean
public OpenAPI customOpenAPI() {
return new OpenAPI()
.info(new Info()
.title("AIOJ AI 服务 API")
.version("1.0.0")
.description("AI 代码分析、优化、测试用例生成等服务接口")
.contact(new Contact()
.name("AIOJ Team")
.email("contact@aioj.com"))
.license(new License()
.name("Apache 2.0")
.url("https://www.apache.org/licenses/LICENSE-2.0.html")));
}
}

View File

@@ -0,0 +1,58 @@
package cn.meowrain.aioj.backend.aiservice.controller;
import cn.meowrain.aioj.backend.aiservice.dto.req.*;
import cn.meowrain.aioj.backend.aiservice.dto.resp.*;
import cn.meowrain.aioj.backend.aiservice.service.AIService;
import cn.meowrain.aioj.backend.framework.core.web.Result;
import cn.meowrain.aioj.backend.framework.core.web.Results;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
/**
* AI 服务控制器
*/
@Tag(name = "AI 服务", description = "AI 代码分析、优化、测试用例生成等接口")
@RestController
@RequestMapping("/ai")
@RequiredArgsConstructor
public class AIController {
private final AIService aiService;
@PostMapping("/analyze")
@Operation(summary = "分析代码", description = "对提交的代码进行分析,包括复杂度、性能、可读性等评分")
public Result<AnalyzeCodeRespDTO> analyzeCode(@Valid @RequestBody AnalyzeCodeReqDTO request) {
AnalyzeCodeRespDTO response = (AnalyzeCodeRespDTO) aiService.analyzeCode(request);
return Results.success(response);
}
@PostMapping("/optimize")
@Operation(summary = "优化代码", description = "对代码进行优化,提升性能、可读性或内存使用")
public Result<OptimizeCodeRespDTO> optimizeCode(@Valid @RequestBody OptimizeCodeReqDTO request) {
OptimizeCodeRespDTO response = (OptimizeCodeRespDTO) aiService.optimizeCode(request);
return Results.success(response);
}
@PostMapping("/test-cases")
@Operation(summary = "生成测试用例", description = "根据代码和问题描述自动生成测试用例")
public Result<GenerateTestCasesRespDTO> generateTestCases(@Valid @RequestBody GenerateTestCasesReqDTO request) {
GenerateTestCasesRespDTO response = (GenerateTestCasesRespDTO) aiService.generateTestCases(request);
return Results.success(response);
}
@PostMapping("/explain")
@Operation(summary = "解释代码", description = "对代码进行详细解释,帮助理解代码逻辑")
public Result<ExplainCodeRespDTO> explainCode(@Valid @RequestBody ExplainCodeReqDTO request) {
ExplainCodeRespDTO response = (ExplainCodeRespDTO) aiService.explainCode(request);
return Results.success(response);
}
@GetMapping("/health")
@Operation(summary = "健康检查", description = "检查 AI 服务是否正常运行")
public Result<String> health() {
return Results.success("AI Service is running");
}
}

View File

@@ -0,0 +1,27 @@
package cn.meowrain.aioj.backend.aiservice.dto.req;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
/**
* 代码分析请求 DTO
*/
@Data
@Schema(description = "代码分析请求")
public class AnalyzeCodeReqDTO {
@NotBlank(message = "代码不能为空")
@Schema(description = "代码内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "def hello():\n print('Hello World')")
private String code;
@NotBlank(message = "编程语言不能为空")
@Schema(description = "编程语言", requiredMode = Schema.RequiredMode.REQUIRED, example = "python")
private String language;
@Schema(description = "题目ID", example = "1001")
private String questionId;
@Schema(description = "用户ID", example = "user123")
private String userId;
}

View File

@@ -0,0 +1,24 @@
package cn.meowrain.aioj.backend.aiservice.dto.req;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
/**
* 代码解释请求 DTO
*/
@Data
@Schema(description = "代码解释请求")
public class ExplainCodeReqDTO {
@NotBlank(message = "代码不能为空")
@Schema(description = "代码内容", requiredMode = Schema.RequiredMode.REQUIRED)
private String code;
@NotBlank(message = "编程语言不能为空")
@Schema(description = "编程语言", requiredMode = Schema.RequiredMode.REQUIRED, example = "python")
private String language;
@Schema(description = "详细程度", example = "normal", allowableValues = {"brief", "normal", "detailed"})
private String detailLevel = "normal";
}

View File

@@ -0,0 +1,25 @@
package cn.meowrain.aioj.backend.aiservice.dto.req;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
/**
* 生成测试用例请求 DTO
*/
@Data
@Schema(description = "生成测试用例请求")
public class GenerateTestCasesReqDTO {
@NotBlank(message = "代码不能为空")
@Schema(description = "代码内容", requiredMode = Schema.RequiredMode.REQUIRED)
private String code;
@NotBlank(message = "编程语言不能为空")
@Schema(description = "编程语言", requiredMode = Schema.RequiredMode.REQUIRED, example = "python")
private String language;
@NotBlank(message = "问题描述不能为空")
@Schema(description = "问题描述", requiredMode = Schema.RequiredMode.REQUIRED)
private String problemDescription;
}

View File

@@ -0,0 +1,24 @@
package cn.meowrain.aioj.backend.aiservice.dto.req;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
/**
* 代码优化请求 DTO
*/
@Data
@Schema(description = "代码优化请求")
public class OptimizeCodeReqDTO {
@NotBlank(message = "代码不能为空")
@Schema(description = "代码内容", requiredMode = Schema.RequiredMode.REQUIRED)
private String code;
@NotBlank(message = "编程语言不能为空")
@Schema(description = "编程语言", requiredMode = Schema.RequiredMode.REQUIRED, example = "python")
private String language;
@Schema(description = "优化类型", example = "performance", allowableValues = {"performance", "readability", "memory"})
private String optimizationType = "performance";
}

View File

@@ -0,0 +1,27 @@
package cn.meowrain.aioj.backend.aiservice.dto.resp;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 代码分析响应 DTO
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "代码分析响应")
public class AnalyzeCodeRespDTO {
@Schema(description = "是否成功")
private Boolean success;
@Schema(description = "响应消息")
private String message;
@Schema(description = "分析结果")
private CodeAnalysisRespDTO analysis;
}

View File

@@ -0,0 +1,44 @@
package cn.meowrain.aioj.backend.aiservice.dto.resp;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
/**
* 代码分析结果 DTO
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "代码分析结果")
public class CodeAnalysisRespDTO {
@Schema(description = "发现的问题")
private List<String> issues;
@Schema(description = "改进建议")
private List<String> suggestions;
@Schema(description = "复杂度评分 (0-100)")
private Integer complexityScore;
@Schema(description = "性能评分 (0-100)")
private Integer performanceScore;
@Schema(description = "可读性评分 (0-100)")
private Integer readabilityScore;
@Schema(description = "时间复杂度")
private String timeComplexity;
@Schema(description = "空间复杂度")
private String spaceComplexity;
@Schema(description = "最佳实践建议")
private List<String> bestPractices;
}

View File

@@ -0,0 +1,32 @@
package cn.meowrain.aioj.backend.aiservice.dto.resp;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
/**
* 代码解释响应 DTO
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "代码解释响应")
public class ExplainCodeRespDTO {
@Schema(description = "是否成功")
private Boolean success;
@Schema(description = "响应消息")
private String message;
@Schema(description = "代码解释")
private String explanation;
@Schema(description = "关键点")
private List<String> keyPoints;
}

View File

@@ -0,0 +1,29 @@
package cn.meowrain.aioj.backend.aiservice.dto.resp;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
/**
* 生成测试用例响应 DTO
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "生成测试用例响应")
public class GenerateTestCasesRespDTO {
@Schema(description = "是否成功")
private Boolean success;
@Schema(description = "响应消息")
private String message;
@Schema(description = "测试用例列表")
private List<TestCaseDTO> testCases;
}

View File

@@ -0,0 +1,32 @@
package cn.meowrain.aioj.backend.aiservice.dto.resp;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
/**
* 代码优化响应 DTO
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "代码优化响应")
public class OptimizeCodeRespDTO {
@Schema(description = "是否成功")
private Boolean success;
@Schema(description = "响应消息")
private String message;
@Schema(description = "优化后的代码")
private String optimizedCode;
@Schema(description = "改进说明")
private List<String> improvements;
}

View File

@@ -0,0 +1,27 @@
package cn.meowrain.aioj.backend.aiservice.dto.resp;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 测试用例 DTO
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "测试用例")
public class TestCaseDTO {
@Schema(description = "输入")
private String input;
@Schema(description = "预期输出")
private String expectedOutput;
@Schema(description = "描述")
private String description;
}

View File

@@ -0,0 +1,137 @@
package cn.meowrain.aioj.backend.aiservice.grpc;
import io.grpc.CallOptions;
import io.grpc.Channel;
import io.grpc.MethodDescriptor;
import io.grpc.stub.AbstractStub;
import io.grpc.stub.ClientCalls;
import io.grpc.stub.StreamObserver;
/**
* AI Service gRPC 接口
*/
public class AIServiceGrpc {
private AIServiceGrpc() {}
public static final String SERVICE_NAME = "ai.service.AIService";
// 创建阻塞存根
public static AIServiceBlockingStub newBlockingStub(Channel channel) {
return new AIServiceBlockingStub(channel);
}
// 创建异步存根
public static AIServiceStub newStub(Channel channel) {
return new AIServiceStub(channel);
}
/**
* 阻塞存根
*/
public static final class AIServiceBlockingStub extends AbstractStub<AIServiceBlockingStub> {
private AIServiceBlockingStub(Channel channel) {
super(channel);
}
private AIServiceBlockingStub(Channel channel, CallOptions callOptions) {
super(channel, callOptions);
}
@Override
protected AIServiceBlockingStub build(Channel channel, CallOptions callOptions) {
return new AIServiceBlockingStub(channel, callOptions);
}
/**
* 分析代码
*/
public AIServiceProto.AnalyzeCodeResponse analyzeCode(AIServiceProto.AnalyzeCodeRequest request) {
// 注意:这里需要真实的 gRPC 服务器才能调用
// 如果没有连接服务器,会抛出 StatusRuntimeException
throw new io.grpc.StatusRuntimeException(io.grpc.Status.UNAVAILABLE);
}
/**
* 优化代码
*/
public AIServiceProto.OptimizeCodeResponse optimizeCode(AIServiceProto.OptimizeCodeRequest request) {
throw new io.grpc.StatusRuntimeException(io.grpc.Status.UNAVAILABLE);
}
/**
* 生成测试用例
*/
public AIServiceProto.GenerateTestCasesResponse generateTestCases(AIServiceProto.GenerateTestCasesRequest request) {
throw new io.grpc.StatusRuntimeException(io.grpc.Status.UNAVAILABLE);
}
/**
* 解释代码
*/
public AIServiceProto.ExplainCodeResponse explainCode(AIServiceProto.ExplainCodeRequest request) {
throw new io.grpc.StatusRuntimeException(io.grpc.Status.UNAVAILABLE);
}
}
/**
* 异步存根
*/
public static final class AIServiceStub extends AbstractStub<AIServiceStub> {
private AIServiceStub(Channel channel) {
super(channel);
}
private AIServiceStub(Channel channel, CallOptions callOptions) {
super(channel, callOptions);
}
@Override
protected AIServiceStub build(Channel channel, CallOptions callOptions) {
return new AIServiceStub(channel, callOptions);
}
/**
* 分析代码(异步)
*/
public void analyzeCode(AIServiceProto.AnalyzeCodeRequest request,
StreamObserver<AIServiceProto.AnalyzeCodeResponse> responseObserver) {
// 异步调用实现
}
/**
* 优化代码(异步)
*/
public void optimizeCode(AIServiceProto.OptimizeCodeRequest request,
StreamObserver<AIServiceProto.OptimizeCodeResponse> responseObserver) {
// 异步调用实现
}
/**
* 生成测试用例(异步)
*/
public void generateTestCases(AIServiceProto.GenerateTestCasesRequest request,
StreamObserver<AIServiceProto.GenerateTestCasesResponse> responseObserver) {
// 异步调用实现
}
/**
* 解释代码(异步)
*/
public void explainCode(AIServiceProto.ExplainCodeRequest request,
StreamObserver<AIServiceProto.ExplainCodeResponse> responseObserver) {
// 异步调用实现
}
}
/**
* StreamObserver 接口(简化版)
*/
public interface StreamObserver<V> {
void onNext(V value);
void onError(Throwable t);
void onCompleted();
}
}

View File

@@ -0,0 +1,478 @@
package cn.meowrain.aioj.backend.aiservice.grpc;
/**
* AI Service Proto 外部类
*/
public final class AIServiceProto {
private AIServiceProto() {}
// 代码分析请求
public static final class AnalyzeCodeRequest extends
com.google.protobuf.GeneratedMessageV3 implements
com.google.protobuf.Message {
private AnalyzeCodeRequest() {
this.code = "";
this.language = "";
this.questionId = "";
this.userId = "";
}
private String code;
private String language;
private String questionId;
private String userId;
public String getCode() { return code; }
public String getLanguage() { return language; }
public String getQuestionId() { return questionId; }
public String getUserId() { return userId; }
@Override
public com.google.protobuf.Parser<AnalyzeCodeRequest> getParserForType() {
return null;
}
public static AnalyzeCodeRequest getDefaultInstance() {
return new AnalyzeCodeRequest();
}
public static AnalyzeCodeRequest newBuilder() {
return new Builder();
}
public static final class Builder {
private final AnalyzeCodeRequest result = new AnalyzeCodeRequest();
public Builder setCode(String value) {
result.code = value;
return this;
}
public Builder setLanguage(String value) {
result.language = value;
return this;
}
public Builder setQuestionId(String value) {
result.questionId = value;
return this;
}
public Builder setUserId(String value) {
result.userId = value;
return this;
}
public AnalyzeCodeRequest build() {
return result;
}
}
}
// 代码分析响应
public static final class AnalyzeCodeResponse extends
com.google.protobuf.GeneratedMessageV3 {
private AnalyzeCodeResponse() {
this.success = false;
this.message = "";
}
private boolean success;
private String message;
private CodeAnalysis analysis;
public boolean getSuccess() { return success; }
public String getMessage() { return message; }
public boolean hasAnalysis() { return analysis != null; }
public CodeAnalysis getAnalysis() { return analysis; }
public static AnalyzeCodeResponse newBuilder() {
return new Builder();
}
public static final class Builder {
private final AnalyzeCodeResponse result = new AnalyzeCodeResponse();
public Builder setSuccess(boolean value) {
result.success = value;
return this;
}
public Builder setMessage(String value) {
result.message = value;
return this;
}
public Builder setAnalysis(CodeAnalysis value) {
result.analysis = value;
return this;
}
public AnalyzeCodeResponse build() {
return result;
}
}
}
// 代码分析结果
public static final class CodeAnalysis extends
com.google.protobuf.GeneratedMessageV3 {
private CodeAnalysis() {}
public java.util.List<String> getIssuesList() { return java.util.Collections.emptyList(); }
public java.util.List<String> getSuggestionsList() { return java.util.Collections.emptyList(); }
public int getComplexityScore() { return 0; }
public int getPerformanceScore() { return 0; }
public int getReadabilityScore() { return 0; }
public String getTimeComplexity() { return ""; }
public String getSpaceComplexity() { return ""; }
public java.util.List<String> getBestPracticesList() { return java.util.Collections.emptyList(); }
public static CodeAnalysis newBuilder() {
return new Builder();
}
public static final class Builder {
private final CodeAnalysis result = new CodeAnalysis();
public CodeAnalysis build() { return result; }
}
}
// 代码优化请求
public static final class OptimizeCodeRequest extends
com.google.protobuf.GeneratedMessageV3 {
private OptimizeCodeRequest() {
this.code = "";
this.language = "";
this.optimizationType = "";
}
private String code;
private String language;
private String optimizationType;
public String getCode() { return code; }
public String getLanguage() { return language; }
public String getOptimizationType() { return optimizationType; }
public static OptimizeCodeRequest newBuilder() {
return new Builder();
}
public static final class Builder {
private final OptimizeCodeRequest result = new OptimizeCodeRequest();
public Builder setCode(String value) {
result.code = value;
return this;
}
public Builder setLanguage(String value) {
result.language = value;
return this;
}
public Builder setOptimizationType(String value) {
result.optimizationType = value;
return this;
}
public OptimizeCodeRequest build() {
return result;
}
}
}
// 代码优化响应
public static final class OptimizeCodeResponse extends
com.google.protobuf.GeneratedMessageV3 {
private OptimizeCodeResponse() {
this.success = false;
this.message = "";
this.optimizedCode = "";
}
private boolean success;
private String message;
private String optimizedCode;
private java.util.List<String> improvements = java.util.Collections.emptyList();
public boolean getSuccess() { return success; }
public String getMessage() { return message; }
public String getOptimizedCode() { return optimizedCode; }
public java.util.List<String> getImprovementsList() { return improvements; }
public static OptimizeCodeResponse newBuilder() {
return new Builder();
}
public static final class Builder {
private final OptimizeCodeResponse result = new OptimizeCodeResponse();
public Builder setSuccess(boolean value) {
result.success = value;
return this;
}
public Builder setMessage(String value) {
result.message = value;
return this;
}
public Builder setOptimizedCode(String value) {
result.optimizedCode = value;
return this;
}
public Builder setImprovementsList(java.util.List<String> value) {
result.improvements = value;
return this;
}
public OptimizeCodeResponse build() {
return result;
}
}
}
// 生成测试用例请求
public static final class GenerateTestCasesRequest extends
com.google.protobuf.GeneratedMessageV3 {
private GenerateTestCasesRequest() {
this.code = "";
this.language = "";
this.problemDescription = "";
}
private String code;
private String language;
private String problemDescription;
public String getCode() { return code; }
public String getLanguage() { return language; }
public String getProblemDescription() { return problemDescription; }
public static GenerateTestCasesRequest newBuilder() {
return new Builder();
}
public static final class Builder {
private final GenerateTestCasesRequest result = new GenerateTestCasesRequest();
public Builder setCode(String value) {
result.code = value;
return this;
}
public Builder setLanguage(String value) {
result.language = value;
return this;
}
public Builder setProblemDescription(String value) {
result.problemDescription = value;
return this;
}
public GenerateTestCasesRequest build() {
return result;
}
}
}
// 生成测试用例响应
public static final class GenerateTestCasesResponse extends
com.google.protobuf.GeneratedMessageV3 {
private GenerateTestCasesResponse() {
this.success = false;
this.message = "";
}
private boolean success;
private String message;
private java.util.List<TestCase> testCases = java.util.Collections.emptyList();
public boolean getSuccess() { return success; }
public String getMessage() { return message; }
public java.util.List<TestCase> getTestCasesList() { return testCases; }
public static GenerateTestCasesResponse newBuilder() {
return new Builder();
}
public static final class Builder {
private final GenerateTestCasesResponse result = new GenerateTestCasesResponse();
public Builder setSuccess(boolean value) {
result.success = value;
return this;
}
public Builder setMessage(String value) {
result.message = value;
return this;
}
public Builder setTestCasesList(java.util.List<TestCase> value) {
result.testCases = value;
return this;
}
public GenerateTestCasesResponse build() {
return result;
}
}
}
// 测试用例
public static final class TestCase extends
com.google.protobuf.GeneratedMessageV3 {
private TestCase() {
this.input = "";
this.expectedOutput = "";
this.description = "";
}
private String input;
private String expectedOutput;
private String description;
public String getInput() { return input; }
public String getExpectedOutput() { return expectedOutput; }
public String getDescription() { return description; }
public static TestCase newBuilder() {
return new Builder();
}
public static final class Builder {
private final TestCase result = new TestCase();
public Builder setInput(String value) {
result.input = value;
return this;
}
public Builder setExpectedOutput(String value) {
result.expectedOutput = value;
return this;
}
public Builder setDescription(String value) {
result.description = value;
return this;
}
public TestCase build() {
return result;
}
}
}
// 代码解释请求
public static final class ExplainCodeRequest extends
com.google.protobuf.GeneratedMessageV3 {
private ExplainCodeRequest() {
this.code = "";
this.language = "";
this.detailLevel = "";
}
private String code;
private String language;
private String detailLevel;
public String getCode() { return code; }
public String getLanguage() { return language; }
public String getDetailLevel() { return detailLevel; }
public static ExplainCodeRequest newBuilder() {
return new Builder();
}
public static final class Builder {
private final ExplainCodeRequest result = new ExplainCodeRequest();
public Builder setCode(String value) {
result.code = value;
return this;
}
public Builder setLanguage(String value) {
result.language = value;
return this;
}
public Builder setDetailLevel(String value) {
result.detailLevel = value;
return this;
}
public ExplainCodeRequest build() {
return result;
}
}
}
// 代码解释响应
public static final class ExplainCodeResponse extends
com.google.protobuf.GeneratedMessageV3 {
private ExplainCodeResponse() {
this.success = false;
this.message = "";
this.explanation = "";
}
private boolean success;
private String message;
private String explanation;
private java.util.List<String> keyPoints = java.util.Collections.emptyList();
public boolean getSuccess() { return success; }
public String getMessage() { return message; }
public String getExplanation() { return explanation; }
public java.util.List<String> getKeyPointsList() { return keyPoints; }
public static ExplainCodeResponse newBuilder() {
return new Builder();
}
public static final class Builder {
private final ExplainCodeResponse result = new ExplainCodeResponse();
public Builder setSuccess(boolean value) {
result.success = value;
return this;
}
public Builder setMessage(String value) {
result.message = value;
return this;
}
public Builder setExplanation(String value) {
result.explanation = value;
return this;
}
public Builder setKeyPointsList(java.util.List<String> value) {
result.keyPoints = value;
return this;
}
public ExplainCodeResponse build() {
return result;
}
}
}
}

View File

@@ -0,0 +1,29 @@
package cn.meowrain.aioj.backend.aiservice.service;
import cn.meowrain.aioj.backend.aiservice.dto.req.*;
/**
* AI 服务接口
*/
public interface AIService {
/**
* 分析代码
*/
Object analyzeCode(AnalyzeCodeReqDTO request);
/**
* 优化代码
*/
Object optimizeCode(OptimizeCodeReqDTO request);
/**
* 生成测试用例
*/
Object generateTestCases(GenerateTestCasesReqDTO request);
/**
* 解释代码
*/
Object explainCode(ExplainCodeReqDTO request);
}

View File

@@ -0,0 +1,119 @@
package cn.meowrain.aioj.backend.aiservice.service.impl;
import cn.meowrain.aioj.backend.aiservice.client.AIServiceGrpcClient;
import cn.meowrain.aioj.backend.aiservice.dto.req.*;
import cn.meowrain.aioj.backend.aiservice.dto.resp.*;
import cn.meowrain.aioj.backend.aiservice.grpc.*;
import cn.meowrain.aioj.backend.aiservice.service.AIService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.stream.Collectors;
/**
* AI 服务实现
*/
@Slf4j
@Service
public class AIServiceImpl implements AIService {
private final AIServiceGrpcClient grpcClient;
public AIServiceImpl(AIServiceGrpcClient grpcClient) {
this.grpcClient = grpcClient;
}
@Override
public AnalyzeCodeRespDTO analyzeCode(AnalyzeCodeReqDTO request) {
log.info("Analyzing code for language: {}, questionId: {}", request.getLanguage(), request.getQuestionId());
AnalyzeCodeResponse grpcResponse = grpcClient.analyzeCode(
request.getCode(),
request.getLanguage(),
request.getQuestionId() != null ? request.getQuestionId() : "",
request.getUserId() != null ? request.getUserId() : ""
);
CodeAnalysisRespDTO analysis = null;
if (grpcResponse.getSuccess() && grpcResponse.hasAnalysis()) {
CodeAnalysis grpcAnalysis = grpcResponse.getAnalysis();
analysis = CodeAnalysisRespDTO.builder()
.issues(grpcAnalysis.getIssuesList())
.suggestions(grpcAnalysis.getSuggestionsList())
.complexityScore(grpcAnalysis.getComplexityScore())
.performanceScore(grpcAnalysis.getPerformanceScore())
.readabilityScore(grpcAnalysis.getReadabilityScore())
.timeComplexity(grpcAnalysis.getTimeComplexity())
.spaceComplexity(grpcAnalysis.getSpaceComplexity())
.bestPractices(grpcAnalysis.getBestPracticesList())
.build();
}
return AnalyzeCodeRespDTO.builder()
.success(grpcResponse.getSuccess())
.message(grpcResponse.getMessage())
.analysis(analysis)
.build();
}
@Override
public OptimizeCodeRespDTO optimizeCode(OptimizeCodeReqDTO request) {
log.info("Optimizing code for language: {}, type: {}", request.getLanguage(), request.getOptimizationType());
OptimizeCodeResponse grpcResponse = grpcClient.optimizeCode(
request.getCode(),
request.getLanguage(),
request.getOptimizationType() != null ? request.getOptimizationType() : "performance"
);
return OptimizeCodeRespDTO.builder()
.success(grpcResponse.getSuccess())
.message(grpcResponse.getMessage())
.optimizedCode(grpcResponse.getOptimizedCode())
.improvements(grpcResponse.getImprovementsList())
.build();
}
@Override
public GenerateTestCasesRespDTO generateTestCases(GenerateTestCasesReqDTO request) {
log.info("Generating test cases for language: {}", request.getLanguage());
GenerateTestCasesResponse grpcResponse = grpcClient.generateTestCases(
request.getCode(),
request.getLanguage(),
request.getProblemDescription()
);
var testCases = grpcResponse.getTestCasesList().stream()
.map(tc -> TestCaseDTO.builder()
.input(tc.getInput())
.expectedOutput(tc.getExpectedOutput())
.description(tc.getDescription())
.build())
.collect(Collectors.toList());
return GenerateTestCasesRespDTO.builder()
.success(grpcResponse.getSuccess())
.message(grpcResponse.getMessage())
.testCases(testCases)
.build();
}
@Override
public ExplainCodeRespDTO explainCode(ExplainCodeReqDTO request) {
log.info("Explaining code for language: {}, level: {}", request.getLanguage(), request.getDetailLevel());
ExplainCodeResponse grpcResponse = grpcClient.explainCode(
request.getCode(),
request.getLanguage(),
request.getDetailLevel() != null ? request.getDetailLevel() : "normal"
);
return ExplainCodeRespDTO.builder()
.success(grpcResponse.getSuccess())
.message(grpcResponse.getMessage())
.explanation(grpcResponse.getExplanation())
.keyPoints(grpcResponse.getKeyPointsList())
.build();
}
}

View File

@@ -0,0 +1,100 @@
syntax = "proto3";
package ai.service;
option java_multiple_files = true;
option java_package = "cn.meowrain.aioj.backend.aiservice.grpc";
option java_outer_classname = "AIServiceProto";
// AI 代码分析服务
service AIService {
// 代码分析
rpc AnalyzeCode(AnalyzeCodeRequest) returns (AnalyzeCodeResponse);
// 代码优化建议
rpc OptimizeCode(OptimizeCodeRequest) returns (OptimizeCodeResponse);
// 生成测试用例
rpc GenerateTestCases(GenerateTestCasesRequest) returns (GenerateTestCasesResponse);
// 代码解释
rpc ExplainCode(ExplainCodeRequest) returns (ExplainCodeResponse);
}
// 代码分析请求
message AnalyzeCodeRequest {
string code = 1; // 代码内容
string language = 2; // 编程语言 (python, java, cpp, etc.)
string question_id = 3; // 题目ID
string user_id = 4; // 用户ID
}
// 代码分析响应
message AnalyzeCodeResponse {
bool success = 1; // 是否成功
string message = 2; // 响应消息
CodeAnalysis analysis = 3; // 分析结果
}
// 代码分析结果
message CodeAnalysis {
repeated string issues = 1; // 发现的问题
repeated string suggestions = 2; // 改进建议
int32 complexity_score = 3; // 复杂度评分 (0-100)
int32 performance_score = 4; // 性能评分 (0-100)
int32 readability_score = 5; // 可读性评分 (0-100)
string time_complexity = 6; // 时间复杂度
string space_complexity = 7; // 空间复杂度
repeated string best_practices = 8; // 最佳实践建议
}
// 代码优化请求
message OptimizeCodeRequest {
string code = 1; // 代码内容
string language = 2; // 编程语言
string optimization_type = 3; // 优化类型 (performance, readability, memory)
}
// 代码优化响应
message OptimizeCodeResponse {
bool success = 1;
string message = 2;
string optimized_code = 3; // 优化后的代码
repeated string improvements = 4; // 改进说明
}
// 生成测试用例请求
message GenerateTestCasesRequest {
string code = 1; // 代码内容
string language = 2; // 编程语言
string problem_description = 3; // 问题描述
}
// 生成测试用例响应
message GenerateTestCasesResponse {
bool success = 1;
string message = 2;
repeated TestCase test_cases = 3; // 测试用例列表
}
// 测试用例
message TestCase {
string input = 1; // 输入
string expected_output = 2; // 预期输出
string description = 3; // 描述
}
// 代码解释请求
message ExplainCodeRequest {
string code = 1; // 代码内容
string language = 2; // 编程语言
string detail_level = 3; // 详细程度 (brief, normal, detailed)
}
// 代码解释响应
message ExplainCodeResponse {
bool success = 1;
string message = 2;
string explanation = 3; // 代码解释
repeated string key_points = 4; // 关键点
}

View File

@@ -0,0 +1,28 @@
spring:
data:
redis:
host: 10.0.0.10
port: 6379
password: 123456
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://10.0.0.10/aioj_dev
username: root
password: root
cloud:
nacos:
discovery:
enabled: true
register-enabled: true
server-addr: 10.0.0.10:8848
username: nacos
password: nacos
# AI 服务配置 - 开发环境
ai:
openai:
api-key: ${OPENAI_API_KEY:sk-dev-key}
base-url: ${OPENAI_BASE_URL:https://api.openai.com}
model: gpt-4
max-tokens: 2000
temperature: 0.7

View File

@@ -0,0 +1,28 @@
spring:
data:
redis:
host: ${REDIS_HOST:localhost}
port: ${REDIS_PORT:6379}
password: ${REDIS_PASSWORD}
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://${DB_HOST}:${DB_PORT}/${DB_NAME}
username: ${DB_USERNAME}
password: ${DB_PASSWORD}
cloud:
nacos:
discovery:
enabled: true
register-enabled: true
server-addr: ${NACOS_ADDR}
username: ${NACOS_USERNAME}
password: ${NACOS_PASSWORD}
# AI 服务配置 - 生产环境
ai:
openai:
api-key: ${OPENAI_API_KEY}
base-url: ${OPENAI_BASE_URL:https://api.openai.com}
model: ${OPENAI_MODEL:gpt-4}
max-tokens: ${OPENAI_MAX_TOKENS:2000}
temperature: ${OPENAI_TEMPERATURE:0.7}

View File

@@ -0,0 +1,25 @@
spring:
data:
redis:
host: localhost
port: 6379
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost/aioj_test
username: root
password: root
cloud:
nacos:
discovery:
enabled: true
register-enabled: true
server-addr: localhost:8848
# AI 服务配置 - 测试环境
ai:
openai:
api-key: ${OPENAI_API_KEY:sk-test-key}
base-url: ${OPENAI_BASE_URL:https://api.openai.com}
model: gpt-4
max-tokens: 2000
temperature: 0.7

View File

@@ -0,0 +1,57 @@
spring:
application:
name: aioj-ai-service
profiles:
active: @env@
server:
port: 18084
servlet:
context-path: /api
error:
include-stacktrace: never
springdoc:
api-docs:
enabled: true
path: /v3/api-docs
default-flat-param-object: true
swagger-ui:
path: /swagger-ui.html
tags-sorter: alpha
operations-sorter: alpha
group-configs:
- group: 'default'
paths-to-match: '/api/**'
packages-to-scan: cn.meowrain.aioj.backend.aiservice.controller
knife4j:
basic:
enable: true
setting:
language: zh_cn
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
mapper-locations: classpath*:/mapper/**/*.xml
# JWT 配置(必须与 auth-service 保持一致)
jwt:
enabled: true
secret: "12345678901234567890123456789012" # 至少32字节
access-expire: 900000 # 15分钟
refresh-expire: 604800000 # 7天
# gRPC 客户端配置
grpc:
client:
host: ${GRPC_SERVER_HOST:localhost}
port: ${GRPC_SERVER_PORT:50051}
timeout: ${GRPC_TIMEOUT:10}
tls-enabled: ${GRPC_TLS_ENABLED:false}
max-message-size: ${GRPC_MAX_MESSAGE_SIZE:10485760}
aioj:
log:
enabled: true
max-length: 20000
logging:
file:
path: ./logs/${spring.application.name}

View File

@@ -0,0 +1,54 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="60 seconds" debug="false">
<!-- 日志输出位置 -->
<springProperty scope="context" name="LOG_HOME" source="logging.file.path" defaultValue="./logs/aioj-ai-service"/>
<!-- 控制台输出 -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<!-- 文件输出 -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_HOME}/aioj-ai-service.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_HOME}/aioj-ai-service.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>30</maxHistory>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<!-- 错误日志单独输出 -->
<appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_HOME}/aioj-ai-service-error.log</file>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_HOME}/aioj-ai-service-error.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>30</maxHistory>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<!-- 日志级别 -->
<root level="INFO">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="FILE"/>
<appender-ref ref="ERROR_FILE"/>
</root>
<!-- SQL日志 -->
<logger name="cn.meowrain.aioj.backend.aiservice.dao.mapper" level="DEBUG"/>
</configuration>

View File

@@ -1,42 +1,49 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>cn.meowrain</groupId>
<groupId>cn.meowrain.aioj</groupId>
<artifactId>ai-oj</artifactId>
<version>1.0-SNAPSHOT</version>
<version>${revision}</version>
</parent>
<artifactId>aioj-backend-auth</artifactId>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<packaging>jar</packaging>
<description>AIOJ 认证授权服务</description>
<dependencies>
<!-- 核心模块 -->
<!-- ==================== API文档 ==================== -->
<dependency>
<groupId>cn.meowrain</groupId>
<artifactId>aioj-backend-common-core</artifactId>
<version>1.0-SNAPSHOT</version>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
</dependency>
<dependency>
<groupId>cn.meowrain</groupId>
<artifactId>aioj-backend-common-feign</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>cn.meowrain</groupId>
<artifactId>aioj-backend-common-mybatis</artifactId>
<version>1.0-SNAPSHOT</version>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
</dependency>
<!-- 工具类 -->
<!-- ==================== 内部模块 ==================== -->
<dependency>
<groupId>cn.meowrain.aioj</groupId>
<artifactId>aioj-backend-common-core</artifactId>
</dependency>
<dependency>
<groupId>cn.meowrain.aioj</groupId>
<artifactId>aioj-backend-common-feign</artifactId>
</dependency>
<dependency>
<groupId>cn.meowrain.aioj</groupId>
<artifactId>aioj-backend-common-mybatis</artifactId>
</dependency>
<dependency>
<groupId>cn.meowrain.aioj</groupId>
<artifactId>aioj-backend-common-security</artifactId>
</dependency>
<!-- ==================== 工具类 ==================== -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-crypto</artifactId>
@@ -50,48 +57,26 @@
<artifactId>commons-lang3</artifactId>
</dependency>
<!-- Spring Cloud服务发现 -->
<!-- ==================== Spring Cloud 服务发现 ==================== -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- Web -->
<!-- ==================== Web ==================== -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- OAuth2 & Spring Security -->
<!-- ==================== OAuth2 & Spring Security ==================== -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- JWT 和 Spring Security 依赖已在 common-security 中包含 -->
<!-- JWT -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.13.0</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.13.0</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.13.0</version>
<scope>runtime</scope>
</dependency>
<!-- Feign客户端 -->
<!-- ==================== Feign 客户端 ==================== -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
@@ -101,19 +86,13 @@
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<!-- Redis用于存储refreshToken -->
<!-- ==================== Redis ==================== -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- API文档 -->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
</dependency>
<!-- 开发工具 -->
<!-- ==================== 开发工具 ==================== -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
@@ -121,7 +100,7 @@
<optional>true</optional>
</dependency>
<!-- 测试 -->
<!-- ==================== 测试 ==================== -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>

View File

@@ -6,13 +6,13 @@ import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
@FeignClient(name = "user-service", path = "/api/v1/user")
@FeignClient(name = "aioj-user-service", path = "/api/v1/user")
public interface UserClient {
@GetMapping("/inner/get-by-username")
Result<UserAuthRespDTO> getUserByUserName(@RequestParam("userAccount") String userAccount);
@GetMapping("/inner/get-by-userid")
public Result<UserAuthRespDTO> getUserById(@RequestParam("userId") String userId);
Result<UserAuthRespDTO> getUserById(@RequestParam("userId") Long userId);
}

View File

@@ -1,33 +1,37 @@
package cn.meowrain.aioj.backend.auth.config;
import cn.meowrain.aioj.backend.auth.filter.JwtAuthenticationFilter;
import cn.meowrain.aioj.backend.framework.security.filter.JwtAuthenticationFilter;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
/**
* Auth Service Security 配置
* 覆盖 common-security 的默认 filterChain添加 auth 服务自定义白名单
*/
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfiguration {
public class AuthSecurityConfiguration {
private final JwtAuthenticationFilter jwtAuthenticationFilter;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
public SecurityFilterChain authSecurityFilterChain(HttpSecurity http) throws Exception {
http.csrf(csrf -> csrf.disable())
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
// Auth 服务自定义白名单
.requestMatchers("/v1/auth/**", "/oauth2/**", "/.well-known/**", "/doc.html", "/swagger-ui/**",
"/swagger-resources/**", "/webjars/**", "/v3/api-docs/**", "/favicon.ico")
"/swagger-resources/**", "/webjars/**", "/v3/api-docs/**", "/v3/api-docs", "/favicon.ico",
"/v1/user/email/send-code")
.permitAll()
.anyRequest()
.authenticated())

View File

@@ -25,7 +25,7 @@ public class SwaggerConfiguration implements ApplicationRunner {
@Bean
public OpenAPI customerOpenAPI() {
return new OpenAPI().info(new Info().title("AIOJ-renz微服务✨")
return new OpenAPI().info(new Info().title("AIOJ-认证微服务✨")
.description("用户认证功能")
.version("v1.0.0")
.contact(new Contact().name("meowrain").email("meowrain@126.com"))

View File

@@ -1,6 +1,7 @@
package cn.meowrain.aioj.backend.auth.controller;
import cn.meowrain.aioj.backend.auth.dto.req.UserLoginRequestDTO;
import cn.meowrain.aioj.backend.auth.dto.resp.UserAuthRespDTO;
import cn.meowrain.aioj.backend.auth.dto.resp.UserLoginResponseDTO;
import cn.meowrain.aioj.backend.auth.oauth2.service.OAuth2SessionService;
import cn.meowrain.aioj.backend.auth.service.AuthService;
@@ -54,4 +55,10 @@ public class AuthController {
return Results.success(isValid);
}
@GetMapping("/getUserInfo")
public Result<UserAuthRespDTO> getUserInfo() {
return Results.success(authService.getUserInfo());
}
}

View File

@@ -2,13 +2,14 @@ package cn.meowrain.aioj.backend.auth.dto.resp;
import lombok.Data;
import java.io.Serializable;
import java.util.Date;
/**
* 用户认证响应体
*/
@Data
public class UserAuthRespDTO {
public class UserAuthRespDTO implements Serializable {
/**
* id
@@ -24,7 +25,6 @@ public class UserAuthRespDTO {
* 用户密码
*/
private String userPassword;
/**
* 开放平台id
*/
@@ -43,7 +43,7 @@ public class UserAuthRespDTO {
/**
* 用户头像
*/
private String userAvatar;
private Long userAvatar;
/**
* 用户简介
@@ -55,6 +55,16 @@ public class UserAuthRespDTO {
*/
private String userRole;
/**
* 用户邮箱
*/
private String userEmail;
/**
* 用户邮箱是否验证 0 未验证 1已验证
*/
private Integer userEmailVerified;
/**
* 创建时间
*/
@@ -65,4 +75,8 @@ public class UserAuthRespDTO {
*/
private Date updateTime;
private static final long serialVersionUID = 1L;
}

View File

@@ -5,7 +5,7 @@ import cn.meowrain.aioj.backend.auth.oauth2.entity.OAuth2Client;
import cn.meowrain.aioj.backend.auth.oauth2.exception.OAuth2Exception;
import cn.meowrain.aioj.backend.auth.oauth2.service.OAuth2ClientService;
import cn.meowrain.aioj.backend.auth.oauth2.service.OAuth2SessionService;
import cn.meowrain.aioj.backend.auth.utils.JwtUtil;
import cn.meowrain.aioj.backend.framework.security.utils.JwtUtil;
import io.jsonwebtoken.Claims;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;

View File

@@ -5,7 +5,7 @@ import cn.meowrain.aioj.backend.auth.clients.UserClient;
import cn.meowrain.aioj.backend.auth.dto.resp.UserAuthRespDTO;
import cn.meowrain.aioj.backend.auth.oauth2.dto.UserInfoResponse;
import cn.meowrain.aioj.backend.auth.oauth2.exception.OAuth2Exception;
import cn.meowrain.aioj.backend.auth.utils.JwtUtil;
import cn.meowrain.aioj.backend.framework.security.utils.JwtUtil;
import cn.meowrain.aioj.backend.framework.core.web.Result;
import io.jsonwebtoken.Claims;
@@ -64,7 +64,7 @@ public class OAuth2UserInfoController {
Long userId = Long.parseLong(userIdStr);
// 4. 调用 user-service 获取用户信息
Result<UserAuthRespDTO> userResult = userClient.getUserById(String.valueOf(userId));
Result<UserAuthRespDTO> userResult = userClient.getUserById(userId);
if (userResult == null || userResult.getData() == null) {
log.error("获取用户信息失败: userId={}", userId);
throw new OAuth2Exception("server_error", "获取用户信息失败", 500);
@@ -80,7 +80,7 @@ public class OAuth2UserInfoController {
.preferredUsername(user.getUserAccount()) // 用户名
.email(null) // TODO: 从用户信息中获取邮箱
.emailVerified(false) // TODO: 从用户信息中获取邮箱验证状态
.picture(user.getUserAvatar()) // 用户头像
.picture(null) // 用户头像
.role(user.getUserRole()) // 用户角色
.build();

View File

@@ -4,7 +4,7 @@ import cn.hutool.core.util.RandomUtil;
import cn.hutool.crypto.digest.DigestUtil;
import cn.hutool.json.JSONUtil;
import cn.meowrain.aioj.backend.auth.common.constants.RedisKeyConstants;
import cn.meowrain.aioj.backend.auth.utils.JwtUtil;
import cn.meowrain.aioj.backend.framework.security.utils.JwtUtil;
import io.jsonwebtoken.Claims;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

View File

@@ -5,7 +5,7 @@ import cn.meowrain.aioj.backend.auth.common.constants.RedisKeyConstants;
import cn.meowrain.aioj.backend.auth.dto.resp.UserAuthRespDTO;
import cn.meowrain.aioj.backend.auth.oauth2.dto.OAuth2TokenResponse;
import cn.meowrain.aioj.backend.auth.oauth2.entity.OAuth2Client;
import cn.meowrain.aioj.backend.auth.utils.JwtUtil;
import cn.meowrain.aioj.backend.auth.utils.AuthServiceJwtUtil;
import cn.meowrain.aioj.backend.framework.core.web.Result;
import lombok.RequiredArgsConstructor;
@@ -27,7 +27,7 @@ import java.util.concurrent.TimeUnit;
@RequiredArgsConstructor
public class OAuth2TokenService {
private final JwtUtil jwtUtil;
private final AuthServiceJwtUtil jwtUtil;
private final UserClient userClient;
@@ -45,7 +45,7 @@ public class OAuth2TokenService {
*/
public OAuth2TokenResponse generateTokenResponse(OAuth2Client client, Long userId, String scope, String nonce) {
// 1. 调用 user-service 获取用户信息
Result<UserAuthRespDTO> userResult = userClient.getUserById(String.valueOf(userId));
Result<UserAuthRespDTO> userResult = userClient.getUserById(userId);
if (userResult == null || userResult.getData() == null) {
throw new RuntimeException("获取用户信息失败");
}
@@ -110,7 +110,7 @@ public class OAuth2TokenService {
}
// 4. 调用 user-service 获取最新用户信息
Result<UserAuthRespDTO> userResult = userClient.getUserById(String.valueOf(userId));
Result<UserAuthRespDTO> userResult = userClient.getUserById(Long.valueOf(userId));
if (userResult == null || userResult.getData() == null) {
throw new RuntimeException("获取用户信息失败");
}

View File

@@ -0,0 +1,21 @@
/**
* AIOJ 认证授权模块
* <p>
* 提供系统的认证与授权功能,包括:
* <ul>
* <li>OAuth2 认证</li>
* <li>JWT Token 生成与验证</li>
* <li>用户登录认证</li>
* <li>权限校验</li>
* </ul>
* </p>
*
* @author meowrain
* @since 1.0.0
*/
@NonNullApi
@NonNullFields
package cn.meowrain.aioj.backend.auth;
import org.springframework.lang.NonNullApi;
import org.springframework.lang.NonNullFields;

View File

@@ -1,7 +1,9 @@
package cn.meowrain.aioj.backend.auth.service;
import cn.meowrain.aioj.backend.auth.dto.req.UserLoginRequestDTO;
import cn.meowrain.aioj.backend.auth.dto.resp.UserAuthRespDTO;
import cn.meowrain.aioj.backend.auth.dto.resp.UserLoginResponseDTO;
import cn.meowrain.aioj.backend.framework.core.web.Result;
public interface AuthService {
@@ -26,4 +28,10 @@ public interface AuthService {
*/
Boolean validateToken(String accessToken);
/**
* 根据accessToken获取用户信息
* @return {@link Result<UserAuthRespDTO>}
*/
UserAuthRespDTO getUserInfo();
}

View File

@@ -5,14 +5,16 @@ import cn.hutool.crypto.digest.BCrypt;
import cn.meowrain.aioj.backend.auth.clients.UserClient;
import cn.meowrain.aioj.backend.auth.common.constants.RedisKeyConstants;
import cn.meowrain.aioj.backend.auth.common.enums.ChainMarkEnums;
import cn.meowrain.aioj.backend.auth.config.properties.JwtPropertiesConfiguration;
import cn.meowrain.aioj.backend.framework.core.utils.ContextHolderUtils;
import cn.meowrain.aioj.backend.framework.security.properties.JwtPropertiesConfiguration;
import cn.meowrain.aioj.backend.auth.dto.chains.context.UserLoginRequestParamVerifyContext;
import cn.meowrain.aioj.backend.auth.dto.req.UserLoginRequestDTO;
import cn.meowrain.aioj.backend.auth.dto.resp.UserAuthRespDTO;
import cn.meowrain.aioj.backend.auth.dto.resp.UserLoginResponseDTO;
import cn.meowrain.aioj.backend.auth.service.AuthService;
import cn.meowrain.aioj.backend.auth.utils.JwtUtil;
import cn.meowrain.aioj.backend.auth.utils.AuthServiceJwtUtil;
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.web.Result;
import lombok.RequiredArgsConstructor;
@@ -27,145 +29,171 @@ import java.util.concurrent.TimeUnit;
@Slf4j
public class AuthServiceImpl implements AuthService {
private final JwtUtil jwtUtil;
private final AuthServiceJwtUtil jwtUtil;
private final UserLoginRequestParamVerifyContext userLoginRequestParamVerifyContext;
private final UserLoginRequestParamVerifyContext userLoginRequestParamVerifyContext;
private final UserClient userClient;
private final UserClient userClient;
private final StringRedisTemplate stringRedisTemplate;
private final StringRedisTemplate stringRedisTemplate;
private final JwtPropertiesConfiguration jwtPropertiesConfiguration;
private final JwtPropertiesConfiguration jwtPropertiesConfiguration;
@Override
public UserLoginResponseDTO userLogin(UserLoginRequestDTO requestParam) {
log.info("用户登录请求: userAccount={}", requestParam.getUserAccount());
@Override
public UserLoginResponseDTO userLogin(UserLoginRequestDTO requestParam) {
log.info("用户登录请求: userAccount={}", requestParam.getUserAccount());
// 1.校验
userLoginRequestParamVerifyContext.handler(ChainMarkEnums.USER_LOGIN_REQ_PARAM_VERIFY.getMarkName(),
requestParam);
// 1.校验
userLoginRequestParamVerifyContext.handler(ChainMarkEnums.USER_LOGIN_REQ_PARAM_VERIFY.getMarkName(),
requestParam);
// 如果调用user-service失败那么就说明是系统内部错误
log.info("正在调用user-service查询用户信息...");
Result<UserAuthRespDTO> userResp = userClient.getUserByUserName(requestParam.getUserAccount());
// 如果调用user-service失败那么就说明是系统内部错误
log.info("正在调用user-service查询用户信息...");
Result<UserAuthRespDTO> userResp = userClient.getUserByUserName(requestParam.getUserAccount());
if (userResp.isFail()) {
log.error("调用user-service返回失败{}", userResp.getMessage());
throw new ServiceException(ErrorCode.SYSTEM_ERROR);
}
if (userResp.isFail()) {
log.error("调用user-service返回失败{}", userResp.getMessage());
throw new ServiceException(ErrorCode.SYSTEM_ERROR);
}
UserAuthRespDTO user = userResp.getData();
if (user == null) {
log.warn("用户不存在: {}", requestParam.getUserAccount());
throw new ServiceException("用户不存在或密码错误", ErrorCode.NOT_LOGIN_ERROR);
}
UserAuthRespDTO user = userResp.getData();
if (user == null) {
log.warn("用户不存在: {}", requestParam.getUserAccount());
throw new ServiceException("用户不存在或密码错误", ErrorCode.NOT_LOGIN_ERROR);
}
if (!BCrypt.checkpw(requestParam.getUserPassword(), user.getUserPassword())) {
log.warn("密码错误: {}", requestParam.getUserAccount());
throw new ServiceException("用户不存在或密码错误", ErrorCode.NOT_LOGIN_ERROR);
}
if (!BCrypt.checkpw(requestParam.getUserPassword(), user.getUserPassword())) {
log.warn("密码错误: {}", requestParam.getUserAccount());
throw new ServiceException("用户不存在或密码错误", ErrorCode.NOT_LOGIN_ERROR);
}
// 生成 JWT
log.info("正在生成JWT token...");
String accessToken = jwtUtil.generateAccessToken(user);
String refreshToken = jwtUtil.generateRefreshToken(user.getId());
// 生成 JWT
log.info("正在生成JWT token...");
String accessToken = jwtUtil.generateAccessToken(user);
String refreshToken = jwtUtil.generateRefreshToken(user.getId());
UserLoginResponseDTO resp = new UserLoginResponseDTO();
resp.setId(user.getId());
resp.setUserAccount(user.getUserAccount());
resp.setAccessToken(accessToken);
resp.setRefreshToken(refreshToken);
resp.setAccessTokenExpireTime(jwtPropertiesConfiguration.getAccessExpire());
resp.setRefreshTokenExpireTime(jwtPropertiesConfiguration.getRefreshExpire());
// refresh token存入到REDIS里面
stringRedisTemplate.opsForValue()
.set(String.format(RedisKeyConstants.REFRESH_TOKEN_KEY_PREFIX, user.getId()), refreshToken,
jwtPropertiesConfiguration.getRefreshExpire(), TimeUnit.MILLISECONDS);
UserLoginResponseDTO resp = new UserLoginResponseDTO();
resp.setId(user.getId());
resp.setUserAccount(user.getUserAccount());
resp.setAccessToken(accessToken);
resp.setRefreshToken(refreshToken);
resp.setAccessTokenExpireTime(jwtPropertiesConfiguration.getAccessExpire());
resp.setRefreshTokenExpireTime(jwtPropertiesConfiguration.getRefreshExpire());
// refresh token存入到REDIS里面
stringRedisTemplate.opsForValue()
.set(String.format(RedisKeyConstants.REFRESH_TOKEN_KEY_PREFIX, user.getId()), refreshToken,
jwtPropertiesConfiguration.getRefreshExpire(), TimeUnit.MILLISECONDS);
log.info("用户登录成功: userId={}, userAccount={}", user.getId(), user.getUserAccount());
return resp;
}
log.info("用户登录成功: userId={}, userAccount={}", user.getId(), user.getUserAccount());
return resp;
}
/**
* 更新access token使用refresh token
* @param refreshToken
* @return
*/
@Override
public UserLoginResponseDTO refreshToken(String refreshToken) {
UserLoginResponseDTO userLoginResponseDTO = new UserLoginResponseDTO();
if (!jwtUtil.isTokenValid(refreshToken)) {
throw new ServiceException("Refresh Token 已过期");
}
/**
* 更新access token使用refresh token
*
* @param refreshToken
* @return
*/
@Override
public UserLoginResponseDTO refreshToken(String refreshToken) {
UserLoginResponseDTO userLoginResponseDTO = new UserLoginResponseDTO();
if (!jwtUtil.isTokenValid(refreshToken)) {
throw new ServiceException("Refresh Token 已过期");
}
Long userId = Long.valueOf(jwtUtil.parseClaims(refreshToken).getSubject());
Long userId = Long.valueOf(jwtUtil.parseClaims(refreshToken).getSubject());
String cacheKey = String.format(RedisKeyConstants.REFRESH_TOKEN_KEY_PREFIX, userId);
String cacheValue = stringRedisTemplate.opsForValue().get(cacheKey);
String cacheKey = String.format(RedisKeyConstants.REFRESH_TOKEN_KEY_PREFIX, userId);
String cacheValue = stringRedisTemplate.opsForValue().get(cacheKey);
if (cacheValue == null || !cacheValue.equals(refreshToken)) {
throw new ServiceException("Refresh Token 已失效");
}
if (cacheValue == null || !cacheValue.equals(refreshToken)) {
throw new ServiceException(ErrorCode.NO_AUTH_ERROR);
}
// 再次签发新的 Access Token
// 此处你需要查用户,拿 userName, role
Result<UserAuthRespDTO> userResult = userClient.getUserById(String.valueOf(userId));
if (userResult.isFail()) {
log.error("通过id查找用户失败:{}", userResult.getMessage());
throw new ServiceException(ErrorCode.SYSTEM_ERROR);
}
UserAuthRespDTO user = userResult.getData();
String newAccessToken = jwtUtil.generateAccessToken(user);
// 再次签发新的 Access Token
// 此处你需要查用户,拿 userName, role
Result<UserAuthRespDTO> userResult = userClient.getUserById(userId);
if (userResult.isFail()) {
log.error("通过id查找用户失败:{}", userResult.getMessage());
throw new ServiceException(ErrorCode.SYSTEM_ERROR);
}
UserAuthRespDTO user = userResult.getData();
String newAccessToken = jwtUtil.generateAccessToken(user);
// 设置refresh token和access token
userLoginResponseDTO.setRefreshToken(refreshToken);
userLoginResponseDTO.setAccessToken(newAccessToken);
userLoginResponseDTO.setAccessTokenExpireTime(jwtPropertiesConfiguration.getAccessExpire());
userLoginResponseDTO.setRefreshTokenExpireTime(jwtPropertiesConfiguration.getRefreshExpire());
return userLoginResponseDTO;
}
// 设置refresh token和access token
userLoginResponseDTO.setRefreshToken(refreshToken);
userLoginResponseDTO.setAccessToken(newAccessToken);
userLoginResponseDTO.setAccessTokenExpireTime(jwtPropertiesConfiguration.getAccessExpire());
userLoginResponseDTO.setRefreshTokenExpireTime(jwtPropertiesConfiguration.getRefreshExpire());
return userLoginResponseDTO;
}
/**
* 验证token的有效性
* @param accessToken 访问令牌
* @return token是否有效
*/
@Override
public Boolean validateToken(String accessToken) {
try {
// 1. 检查token格式
if (accessToken == null || accessToken.trim().isEmpty()) {
log.warn("Access token is null or empty");
return false;
}
/**
* 验证token的有效性
*
* @param accessToken 访问令牌
* @return token是否有效
*/
@Override
public Boolean validateToken(String accessToken) {
try {
// 1. 检查token格式
if (accessToken == null || accessToken.trim().isEmpty()) {
log.warn("Access token is null or empty");
return false;
}
// 2. 验证token签名和过期时间
if (!jwtUtil.isTokenValid(accessToken)) {
log.warn("Access token is invalid or expired");
return false;
}
// 2. 验证token签名和过期时间
if (!jwtUtil.isTokenValid(accessToken)) {
log.warn("Access token is invalid or expired");
return false;
}
// 3. 解析token获取用户信息
String userId = jwtUtil.parseClaims(accessToken).getSubject();
if (userId == null) {
log.warn("Access token does not contain valid user id");
return false;
}
// 3. 解析token获取用户信息
String userId = jwtUtil.parseClaims(accessToken).getSubject();
if (userId == null) {
log.warn("Access token does not contain valid user id");
return false;
}
// 4. 验证用户是否存在(可选,增加安全性)
Result<UserAuthRespDTO> userResult = userClient.getUserById(userId);
if (userResult.isFail() || userResult.getData() == null) {
log.warn("User not found for id: {}", userId);
return false;
}
// 4. 验证用户是否存在(可选,增加安全性)
Result<UserAuthRespDTO> userResult = userClient.getUserById(Long.valueOf(userId));
if (userResult.isFail() || userResult.getData() == null) {
log.warn("User not found for id: {}", userId);
return false;
}
log.debug("Access token validation successful for user: {}", userId);
return true;
} catch (Exception e) {
log.error("Error validating access token", e);
return false;
}
}
@Override
public UserAuthRespDTO getUserInfo() {
Long currentUserId = ContextHolderUtils.getCurrentUserId();
// 查询用户信息IO 操作)
Result<UserAuthRespDTO> userResult;
try {
userResult = userClient.getUserById(currentUserId);
} catch (Exception e) {
log.error("Failed to call user service", e);
throw new ClientException(ErrorCode.SYSTEM_ERROR);
}
if (userResult == null || userResult.isFail()) {
throw new ClientException(ErrorCode.SYSTEM_ERROR);
}
if (userResult.getData() == null) {
throw new ClientException(ErrorCode.NOT_FOUND_ERROR);
}
return userResult.getData();
}
log.debug("Access token validation successful for user: {}", userId);
return true;
}
catch (Exception e) {
log.error("Error validating access token", e);
return false;
}
}
}

View File

@@ -0,0 +1,75 @@
package cn.meowrain.aioj.backend.auth.utils;
import cn.meowrain.aioj.backend.auth.dto.resp.UserAuthRespDTO;
import cn.meowrain.aioj.backend.framework.security.utils.JwtUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
/**
* Auth Service 专用的 JWT 工具类
* 包装 common-security 中的 JwtUtil提供接受 UserAuthRespDTO 的便捷方法
*/
@Component
@RequiredArgsConstructor
public class AuthServiceJwtUtil {
private final JwtUtil jwtUtil;
/**
* 生成 Access Token
* @param user 用户信息
* @return JWT Token
*/
public String generateAccessToken(UserAuthRespDTO user) {
Map<String, Object> claims = new HashMap<>();
claims.put("userId", user.getId());
claims.put("userName", user.getUserName());
claims.put("role", user.getUserRole());
return jwtUtil.generateAccessToken(user.getId(), claims);
}
/**
* 生成 Refresh Token
* @param userId 用户ID
* @return JWT Token
*/
public String generateRefreshToken(Long userId) {
return jwtUtil.generateRefreshToken(userId);
}
/**
* 生成 OIDC ID Token
* @param user 用户信息
* @param clientId 客户端IDaud
* @param nonce 防重放参数
* @return ID Token
*/
public String generateIdToken(UserAuthRespDTO user, String clientId, String nonce) {
Map<String, Object> claims = new HashMap<>();
claims.put("aud", clientId);
claims.put("name", user.getUserName());
claims.put("preferred_username", user.getUserAccount());
if (nonce != null) {
claims.put("nonce", nonce);
}
return jwtUtil.generateAccessToken(user.getId(), claims);
}
/**
* 校验 Token 是否过期
*/
public boolean isTokenValid(String token) {
return jwtUtil.isTokenValid(token);
}
/**
* 解析 Token
*/
public io.jsonwebtoken.Claims parseClaims(String token) {
return jwtUtil.parseClaims(token);
}
}

View File

@@ -1,6 +1,6 @@
spring:
application:
name: auth-service
name: aioj-auth-service
data:
redis:
host: 10.0.0.10

View File

@@ -1,13 +1,13 @@
spring:
application:
name: auth-service
name: aioj-auth-service
profiles:
active: @env@
devtools:
livereload:
enabled: true
server:
port: 10011
port: 18081
servlet:
context-path: /api
springdoc:
@@ -21,8 +21,8 @@ springdoc:
operations-sorter: alpha
group-configs:
- group: 'default'
paths-to-match: '/**'
packages-to-scan: cn.meowrain.aioj.backend.userservice.controller
paths-to-match: '/api/**'
packages-to-scan: cn.meowrain.aioj.backend.auth.controller
knife4j:
basic:
enable: true
@@ -33,6 +33,10 @@ mybatis-plus:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
mapper-locations: classpath*:/mapper/**/*.xml
jwt:
enabled: true
secret: "12345678901234567890123456789012" # 至少32字节
access-expire: 900000 # 24小时
refresh-expire: 604800000 # 7天
refresh-expire: 604800000 # 7天
logging:
file:
path: ./logs/${spring.application.name}

View File

@@ -0,0 +1,110 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--debug="false" 表示关闭 Logback 框架自身的内部状态信息打印。-->
<configuration scan="true" scanPeriod="10 seconds" debug="false">
<!-- 引入 spring boot 默认日志颜色和基础配置-->
<include resource="org/springframework/boot/logging/logback/defaults.xml"/>
<!-- 定义变量 APP_NAME从 Spring 环境变量中获取 spring.application.name 的值-->
<springProperty scope="context" name="APP_NAME" source="spring.application.name"/>
<!-- 定义时间格式yyyy-MM 表示年-yyyy-MM-dd 表示年---->
<timestamp key="time-month" datePattern="yyyy-MM"/>
<timestamp key="time-month-day" datePattern="yyyy-MM-dd"/>
<!-- 定义变量 LOG_FILE_PATH默认值为 ./logs/${APP_NAME},可以通过环境变量 LOG_PATH 覆盖 日志存储路径-->
<property name="LOG_FILE_PATH" value="${LOG_PATH:-./logs/${APP_NAME}}"/>
<!-- 定义日志格式
格式说明:
%d{yyyy-MM-dd HH:mm:ss.SSS}:日志记录时间,格式为年--日 时:分:秒.毫秒
[%thread]:日志记录线程名称
%-5level日志级别左对齐占用 5 个字符宽度
%logger{50}:日志记录器名称,最多显示 50 个字符
-[%X{traceId:-}]:从 MDC 中获取 traceId 变量值,如果不存在则显示为空
%msg%n日志消息内容换行符
-->
<property name="FILE_LOG_PATTERN"
value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} -[%X{traceId:-}] %msg%n"/>
<!-- appender 控制台输出-->
<!--
<appender>: 这是 Logback 配置的根标签之一,用于定义一个日志输出目的地。
name="CONSOLE": 为这个 Appender 赋予一个唯一的名称,方便在 <root> 或 <logger> 标签中引用它。
class="ch.qos.logback.core.ConsoleAppender": 指定使用的实现类。
ConsoleAppender 是 Logback 库中专门用于将日志事件写入 System.out (标准输出) 或 System.err (标准错误) 的类。
这是我们在本地开发和测试时最常用的 Appender。
CONSOLE_LOG_PATTERN 是上面include的默认日志格式这里直接引用即可
-->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${CONSOLE_LOG_PATTERN}</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<!-- 全量info日志-->
<appender name="INFO_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_FILE_PATH}/${time-month}/${time-month-day}/info.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${LOG_FILE_PATH}/${time-month}/${time-month-day}/info.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
<maxFileSize>100MB</maxFileSize>
<maxHistory>31</maxHistory>
<totalSizeCap>100GB</totalSizeCap>
</rollingPolicy>
<encoder>
<pattern>${FILE_LOG_PATTERN}</pattern>
<charset>UTF-8</charset>
</encoder>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<!-- 只记录 INFO 级别以及以上的日志的日志 -->
<level>INFO</level>
</filter>
</appender>
<appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_FILE_PATH}/${time-month}/${time-month-day}/error.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${LOG_FILE_PATH}/${time-month}/${time-month-day}/error.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
<maxFileSize>100MB</maxFileSize>
<maxHistory>31</maxHistory>
<totalSizeCap>100GB</totalSizeCap>
</rollingPolicy>
<encoder>
<pattern>${FILE_LOG_PATTERN}</pattern>
<charset>UTF-8</charset>
</encoder>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<!-- 只记录 ERROR 级别以及以上的日志的日志 -->
<level>ERROR</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<!-- 异步写入日志-->
<appender name="ASYNC_INFO"
class="ch.qos.logback.classic.AsyncAppender">
<discardingThreshold>0</discardingThreshold>
<queueSize>512</queueSize>
<appender-ref ref="INFO_FILE"/>
</appender>
<appender name="ASYNC_ERROR" class="ch.qos.logback.classic.AsyncAppender">
<discardingThreshold>0</discardingThreshold>
<queueSize>512</queueSize>
<appender-ref ref="ERROR_FILE"/>
</appender>
<!-- =========== 环境配置 打印到控制台 ===========-->
<springProfile name="dev">
<root level="INFO">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="ASYNC_ERROR"/>
<appender-ref ref="ASYNC_INFO"/>
</root>
</springProfile>
<springProfile name="prod">
<root level="INFO">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="ASYNC_ERROR"/>
<appender-ref ref="ASYNC_INFO"/>
</root>
</springProfile>
</configuration>

View File

@@ -0,0 +1,219 @@
# AIOJ 博客服务模块
## 模块说明
`aioj-backend-blog-service` 是 AIOJ 系统的博客服务模块,用于用户发帖、分享技术经验、写文章。
## 功能特性
- 文章管理:发布、编辑、删除、草稿箱
- 分类管理:支持多级分类
- 标签系统:文章标签分类和关联
- 评论系统:支持多级评论和回复
- 点赞/收藏:文章和评论点赞、收藏功能
- 浏览统计:文章浏览记录和统计
- Markdown 支持:原生支持 Markdown 编辑和渲染
- 搜索功能:全文搜索文章标题和内容
## 技术栈
- Spring Boot 3.5.7
- MyBatis Plus
- MySQL 8.0
- Redis
- Nacos 服务发现
- FlexMark (Markdown 处理)
- Knife4j (API 文档)
## 端口配置
- 开发环境: 18086
- 上下文路径: /api
## 数据库
数据库名称: `aioj_blog`
初始化 SQL 脚本: `../../db/blog.sql`
### 核心表结构
| 表名 | 说明 |
|------|------|
| blog_article | 文章表 |
| blog_category | 文章分类表 |
| blog_tag | 文章标签表 |
| blog_article_tag | 文章标签关联表 |
| blog_comment | 评论表 |
| blog_like | 点赞表 |
| blog_favorite | 收藏表 |
| blog_view | 浏览记录表 |
| blog_draft | 草稿箱表 |
## 快速开始
### 1. 初始化数据库
```bash
mysql -u root -p < ../../db/blog.sql
```
### 2. 配置数据库连接
编辑 `src/main/resources/application-dev.yml`:
```yaml
spring:
datasource:
url: jdbc:mysql://localhost:3306/aioj_blog
username: your_username
password: your_password
```
### 3. 启动服务
```bash
mvn spring-boot:run
```
或直接运行主类: `BlogServiceApplication.java`
### 4. 访问 API 文档
启动服务后访问: http://localhost:18086/api/doc.html
## API 接口
### 文章相关
- `POST /api/article` - 创建文章
- `PUT /api/article/{id}` - 更新文章
- `DELETE /api/article/{id}` - 删除文章
- `GET /api/article/{id}` - 获取文章详情
- `GET /api/article/list` - 获取文章列表
- `POST /api/article/publish` - 发布文章
### 分类相关
- `GET /api/category/list` - 获取分类列表
- `POST /api/category` - 创建分类(管理员)
### 标签相关
- `GET /api/tag/list` - 获取标签列表
- `GET /api/tag/hot` - 获取热门标签
### 评论相关
- `POST /api/comment` - 发表评论
- `GET /api/comment/article/{articleId}` - 获取文章评论列表
- `DELETE /api/comment/{id}` - 删除评论
## 开发指南
### 代码结构
```
aioj-backend-blog-service/
├── src/main/java/cn/meowrain/aioj/backend/blogservice/
│ ├── controller/ # 控制器层
│ ├── service/ # 服务层
│ │ └── impl/ # 服务实现层
│ ├── dao/ # 数据访问层
│ │ ├── mapper/ # MyBatis Mapper
│ │ └── model/ # 数据模型
│ ├── dto/ # 数据传输对象
│ ├── vo/ # 视图对象
│ ├── common/ # 公共类
│ ├── config/ # 配置类
│ └── BlogServiceApplication.java
└── src/main/resources/
├── mapper/ # MyBatis XML 映射文件
├── application.yml # 主配置文件
├── application-dev.yml
├── application-test.yml
├── application-prod.yml
└── logback-spring.xml # 日志配置
```
### 待实现功能
- [ ] 文章 CRUD 接口
- [ ] 分类管理接口
- [ ] 标签管理接口
- [ ] 评论系统接口
- [ ] 点赞/收藏接口
- [ ] 文章搜索接口
- [ ] Markdown 渲染服务
- [ ] 文章定时发布
- [ ] 文章审核功能
- [ ] 用户关注和动态
## 更新日志
### 2025-01-20 - 初始创建
**创建者**: Claude Code
**工作内容**:
1. **模块结构搭建**
- 创建完整的 Maven 模块目录结构
- 配置 `pom.xml`,引入所需依赖
- 创建主应用类 `BlogServiceApplication.java`
- 更新父 `pom.xml`,添加新模块注册
2. **配置文件创建**
- `application.yml` - 主配置文件
- 服务端口: 18086
- 上下文路径: /api
- MyBatis Plus 配置
- Knife4j API 文档配置
- `application-dev.yml` - 开发环境配置
- `application-test.yml` - 测试环境配置
- `application-prod.yml` - 生产环境配置
- `logback-spring.xml` - 日志配置
3. **数据库设计**
- 创建 `db/blog.sql` 数据库初始化脚本
- 设计 9 张核心表:
- `blog_article` - 文章表(支持草稿、发布、下架等状态)
- `blog_category` - 分类表(支持多级分类)
- `blog_tag` - 标签表
- `blog_article_tag` - 文章标签关联表
- `blog_comment` - 评论表(支持多级回复)
- `blog_like` - 点赞表
- `blog_favorite` - 收藏表
- `blog_view` - 浏览记录表
- `blog_draft` - 草稿箱表
- 添加初始分类和标签数据
4. **依赖说明**
- Spring Boot 3.5.7
- Spring Cloud & Nacos服务发现
- MyBatis PlusORM
- FlexMarkMarkdown 处理)
- Knife4jAPI 文档)
- Redis缓存
- MySQL数据库
5. **待实现功能**
- [ ] 文章 CRUD 接口开发
- [ ] 分类管理接口
- [ ] 标签管理接口
- [ ] 评论系统接口
- [ ] 点赞/收藏接口
- [ ] 文章搜索接口
- [ ] Markdown 渲染服务
- [ ] 文章定时发布
- [ ] 文章审核功能
**注意事项**:
- 确保先执行 `db/blog.sql` 初始化数据库
- 检查 `application-dev.yml` 中的数据库连接配置
- JWT 配置需与 `aioj-backend-auth` 保持一致
- 启动前确保 Nacos 服务已运行
## License
Copyright © 2025 AIOJ Project

View File

@@ -0,0 +1,99 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>cn.meowrain.aioj</groupId>
<artifactId>ai-oj</artifactId>
<version>${revision}</version>
</parent>
<artifactId>aioj-backend-blog-service</artifactId>
<packaging>jar</packaging>
<description>AIOJ 博客服务 - 用于用户发帖、分享技术经验、写文章</description>
<dependencies>
<!-- ==================== API文档 ==================== -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
</dependency>
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
</dependency>
<!-- ==================== 内部模块 ==================== -->
<dependency>
<groupId>cn.meowrain.aioj</groupId>
<artifactId>aioj-backend-common-core</artifactId>
</dependency>
<dependency>
<groupId>cn.meowrain.aioj</groupId>
<artifactId>aioj-backend-common-log</artifactId>
</dependency>
<dependency>
<groupId>cn.meowrain.aioj</groupId>
<artifactId>aioj-backend-common-mybatis</artifactId>
</dependency>
<dependency>
<groupId>cn.meowrain.aioj</groupId>
<artifactId>aioj-backend-common-security</artifactId>
</dependency>
<dependency>
<groupId>cn.meowrain.aioj</groupId>
<artifactId>aioj-backend-common-feign</artifactId>
</dependency>
<!-- ==================== Web ==================== -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- ==================== 工具类 ==================== -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-extra</artifactId>
</dependency>
<!-- ==================== Markdown 处理 ==================== -->
<dependency>
<groupId>com.vladsch.flexmark</groupId>
<artifactId>flexmark-all</artifactId>
<version>0.64.8</version>
</dependency>
<!-- ==================== Redis & 缓存 ==================== -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<!-- ==================== 数据库 ==================== -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<!-- ==================== Spring Cloud 服务发现 ==================== -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- ==================== 测试 ==================== -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,23 @@
package cn.meowrain.aioj.backend.blogservice;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* AIOJ 博客服务应用启动类
*
* @author AIOJ
*/
@MapperScan("cn.meowrain.aioj.backend.blogservice.dao.mapper")
@SpringBootApplication(scanBasePackages = {
"cn.meowrain.aioj.backend.blogservice",
"cn.meowrain.aioj.backend.framework"
})
public class BlogServiceApplication {
public static void main(String[] args) {
SpringApplication.run(BlogServiceApplication.class, args);
}
}

View File

@@ -0,0 +1,112 @@
package cn.meowrain.aioj.backend.blogservice.controller;
import cn.meowrain.aioj.backend.framework.core.web.Result;
import cn.meowrain.aioj.backend.framework.core.web.Results;
import cn.meowrain.aioj.backend.blogservice.dto.req.ArticleCreateRequestDTO;
import cn.meowrain.aioj.backend.blogservice.dto.req.ArticleQueryRequestDTO;
import cn.meowrain.aioj.backend.blogservice.dto.req.ArticleUpdateRequestDTO;
import cn.meowrain.aioj.backend.blogservice.dto.resp.ArticleListResponseDTO;
import cn.meowrain.aioj.backend.blogservice.dto.resp.ArticleResponseDTO;
import cn.meowrain.aioj.backend.blogservice.service.ArticleService;
import com.baomidou.mybatisplus.core.metadata.IPage;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
/**
* 文章管理控制器
*
* @author AIOJ
*/
@RequiredArgsConstructor
@RestController
@RequestMapping("/v1/article")
@Tag(name = "文章管理", description = "文章的创建、查询、更新、删除等接口")
public class ArticleController {
private final ArticleService articleService;
@PostMapping("/create")
@Operation(summary = "创建文章", description = "创建新文章支持Markdown格式")
public Result<Long> createArticle(
@Parameter(description = "文章创建信息", required = true)
@Valid @RequestBody ArticleCreateRequestDTO request) {
Long articleId = articleService.createArticle(request);
return Results.success(articleId);
}
@PutMapping("/update")
@Operation(summary = "更新文章", description = "更新已存在的文章信息")
public Result<Void> updateArticle(
@Parameter(description = "文章更新信息", required = true)
@Valid @RequestBody ArticleUpdateRequestDTO request) {
articleService.updateArticle(request);
return Results.success();
}
@DeleteMapping("/delete/{articleId}")
@Operation(summary = "删除文章", description = "逻辑删除指定文章")
public Result<Void> deleteArticle(
@Parameter(description = "文章ID", required = true)
@PathVariable("articleId") Long articleId) {
articleService.deleteArticle(articleId);
return Results.success();
}
@GetMapping("/{articleId}")
@Operation(summary = "查询文章详情", description = "根据文章ID查询文章详细信息")
public Result<ArticleResponseDTO> getArticleById(
@Parameter(description = "文章ID", required = true)
@PathVariable("articleId") Long articleId) {
ArticleResponseDTO article = articleService.getArticleById(articleId);
return Results.success(article);
}
@GetMapping("/slug/{slug}")
@Operation(summary = "根据slug查询文章", description = "根据文章别名查询文章详细信息")
public Result<ArticleResponseDTO> getArticleBySlug(
@Parameter(description = "文章别名", required = true)
@PathVariable("slug") String slug) {
ArticleResponseDTO article = articleService.getArticleBySlug(slug);
return Results.success(article);
}
@PostMapping("/list")
@Operation(summary = "分页查询文章列表", description = "支持多条件查询和排序")
public Result<IPage<ArticleListResponseDTO>> getArticleList(
@Parameter(description = "查询条件")
@RequestBody ArticleQueryRequestDTO request) {
IPage<ArticleListResponseDTO> page = articleService.getArticleList(request);
return Results.success(page);
}
@PutMapping("/publish/{articleId}")
@Operation(summary = "发布文章", description = "将草稿状态的文章发布")
public Result<Void> publishArticle(
@Parameter(description = "文章ID", required = true)
@PathVariable("articleId") Long articleId) {
articleService.publishArticle(articleId);
return Results.success();
}
@PutMapping("/unpublish/{articleId}")
@Operation(summary = "取消发布文章", description = "将已发布的文章转为草稿状态")
public Result<Void> unpublishArticle(
@Parameter(description = "文章ID", required = true)
@PathVariable("articleId") Long articleId) {
articleService.unpublishArticle(articleId);
return Results.success();
}
@PostMapping("/view/{articleId}")
@Operation(summary = "增加文章浏览量", description = "记录文章浏览并增加浏览计数")
public Result<Void> incrementViewCount(
@Parameter(description = "文章ID", required = true)
@PathVariable("articleId") Long articleId) {
articleService.incrementViewCount(articleId);
return Results.success();
}
}

View File

@@ -0,0 +1,89 @@
package cn.meowrain.aioj.backend.blogservice.controller;
import cn.meowrain.aioj.backend.framework.core.web.Result;
import cn.meowrain.aioj.backend.framework.core.web.Results;
import cn.meowrain.aioj.backend.blogservice.dto.req.CategoryCreateRequestDTO;
import cn.meowrain.aioj.backend.blogservice.dto.req.CategoryUpdateRequestDTO;
import cn.meowrain.aioj.backend.blogservice.dto.resp.CategoryResponseDTO;
import cn.meowrain.aioj.backend.blogservice.service.CategoryService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 文章分类控制器
*
* @author AIOJ
*/
@RequiredArgsConstructor
@RestController
@RequestMapping("/v1/category")
@Tag(name = "文章分类管理", description = "文章分类的创建、查询、更新、删除等接口")
public class CategoryController {
private final CategoryService categoryService;
@PostMapping("/create")
@Operation(summary = "创建分类", description = "创建新的文章分类")
public Result<Long> createCategory(
@Parameter(description = "分类创建信息", required = true)
@Valid @RequestBody CategoryCreateRequestDTO request) {
Long categoryId = categoryService.createCategory(request);
return Results.success(categoryId);
}
@PutMapping("/update")
@Operation(summary = "更新分类", description = "更新已存在的分类信息")
public Result<Void> updateCategory(
@Parameter(description = "分类更新信息", required = true)
@Valid @RequestBody CategoryUpdateRequestDTO request) {
categoryService.updateCategory(request);
return Results.success();
}
@DeleteMapping("/delete/{categoryId}")
@Operation(summary = "删除分类", description = "删除指定分类")
public Result<Void> deleteCategory(
@Parameter(description = "分类ID", required = true)
@PathVariable("categoryId") Long categoryId) {
categoryService.deleteCategory(categoryId);
return Results.success();
}
@GetMapping("/{categoryId}")
@Operation(summary = "查询分类详情", description = "根据分类ID查询分类详细信息")
public Result<CategoryResponseDTO> getCategoryById(
@Parameter(description = "分类ID", required = true)
@PathVariable("categoryId") Long categoryId) {
CategoryResponseDTO category = categoryService.getCategoryById(categoryId);
return Results.success(category);
}
@GetMapping("/slug/{slug}")
@Operation(summary = "根据slug查询分类", description = "根据分类别名查询分类详细信息")
public Result<CategoryResponseDTO> getCategoryBySlug(
@Parameter(description = "分类别名", required = true)
@PathVariable("slug") String slug) {
CategoryResponseDTO category = categoryService.getCategoryBySlug(slug);
return Results.success(category);
}
@GetMapping("/list/all")
@Operation(summary = "查询所有分类", description = "获取所有分类列表")
public Result<List<CategoryResponseDTO>> getAllCategories() {
List<CategoryResponseDTO> categories = categoryService.getAllCategories();
return Results.success(categories);
}
@GetMapping("/list/enabled")
@Operation(summary = "查询启用的分类", description = "获取所有启用状态的分类列表")
public Result<List<CategoryResponseDTO>> getEnabledCategories() {
List<CategoryResponseDTO> categories = categoryService.getEnabledCategories();
return Results.success(categories);
}
}

View File

@@ -0,0 +1,98 @@
package cn.meowrain.aioj.backend.blogservice.controller;
import cn.meowrain.aioj.backend.framework.core.web.Result;
import cn.meowrain.aioj.backend.framework.core.web.Results;
import cn.meowrain.aioj.backend.blogservice.dto.req.CollectionFolderCreateRequestDTO;
import cn.meowrain.aioj.backend.blogservice.dto.req.CollectionRequestDTO;
import cn.meowrain.aioj.backend.blogservice.dto.resp.ArticleListResponseDTO;
import cn.meowrain.aioj.backend.blogservice.dto.resp.CollectionFolderResponseDTO;
import cn.meowrain.aioj.backend.blogservice.service.CollectionService;
import com.baomidou.mybatisplus.core.metadata.IPage;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 收藏管理控制器
*
* @author AIOJ
*/
@RequiredArgsConstructor
@RestController
@RequestMapping("/v1/collection")
@Tag(name = "收藏管理", description = "文章收藏、收藏夹管理等接口")
public class CollectionController {
private final CollectionService collectionService;
@PostMapping("/collect")
@Operation(summary = "收藏文章", description = "将文章添加到收藏夹")
public Result<Void> collectArticle(
@Parameter(description = "收藏信息", required = true)
@Valid @RequestBody CollectionRequestDTO request) {
collectionService.collectArticle(request);
return Results.success();
}
@DeleteMapping("/uncollect/{articleId}")
@Operation(summary = "取消收藏文章", description = "取消收藏指定文章")
public Result<Void> uncollectArticle(
@Parameter(description = "文章ID", required = true)
@PathVariable("articleId") Long articleId) {
collectionService.uncollectArticle(articleId);
return Results.success();
}
@GetMapping("/check/{articleId}")
@Operation(summary = "查询收藏状态", description = "查询当前用户是否收藏了指定文章")
public Result<Boolean> isCollected(
@Parameter(description = "文章ID", required = true)
@PathVariable("articleId") Long articleId) {
Long currentUserId = cn.meowrain.aioj.backend.framework.core.utils.ContextHolderUtils.getCurrentUserId();
Boolean collected = collectionService.isCollected(currentUserId, articleId);
return Results.success(collected);
}
@PostMapping("/folder/create")
@Operation(summary = "创建收藏夹", description = "创建新的收藏夹")
public Result<Long> createCollectionFolder(
@Parameter(description = "收藏夹创建信息", required = true)
@Valid @RequestBody CollectionFolderCreateRequestDTO request) {
Long folderId = collectionService.createCollectionFolder(request);
return Results.success(folderId);
}
@DeleteMapping("/folder/delete/{folderId}")
@Operation(summary = "删除收藏夹", description = "删除指定收藏夹")
public Result<Void> deleteCollectionFolder(
@Parameter(description = "收藏夹ID", required = true)
@PathVariable("folderId") Long folderId) {
collectionService.deleteCollectionFolder(folderId);
return Results.success();
}
@GetMapping("/folder/list")
@Operation(summary = "查询收藏夹列表", description = "获取当前用户的所有收藏夹")
public Result<List<CollectionFolderResponseDTO>> getCollectionFolders() {
List<CollectionFolderResponseDTO> folders = collectionService.getCollectionFolders();
return Results.success(folders);
}
@GetMapping("/list")
@Operation(summary = "分页查询收藏列表", description = "获取当前用户收藏的文章列表")
public Result<IPage<ArticleListResponseDTO>> getCollectedArticles(
@Parameter(description = "页码", required = true)
@RequestParam("current") Integer current,
@Parameter(description = "每页数量", required = true)
@RequestParam("size") Integer size,
@Parameter(description = "收藏夹ID可选")
@RequestParam(value = "folderId", required = false) Long folderId) {
IPage<ArticleListResponseDTO> page = collectionService.getCollectedArticles(current, size, folderId);
return Results.success(page);
}
}

View File

@@ -0,0 +1,94 @@
package cn.meowrain.aioj.backend.blogservice.controller;
import cn.meowrain.aioj.backend.framework.core.web.Result;
import cn.meowrain.aioj.backend.framework.core.web.Results;
import cn.meowrain.aioj.backend.blogservice.dto.req.CommentCreateRequestDTO;
import cn.meowrain.aioj.backend.blogservice.dto.req.CommentQueryRequestDTO;
import cn.meowrain.aioj.backend.blogservice.dto.resp.CommentResponseDTO;
import cn.meowrain.aioj.backend.blogservice.service.CommentService;
import com.baomidou.mybatisplus.core.metadata.IPage;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 评论管理控制器
*
* @author AIOJ
*/
@RequiredArgsConstructor
@RestController
@RequestMapping("/v1/comment")
@Tag(name = "评论管理", description = "评论的发表、查询、删除等接口")
public class CommentController {
private final CommentService commentService;
@PostMapping("/create")
@Operation(summary = "发表评论", description = "发表文章评论支持Markdown格式")
public Result<Long> createComment(
@Parameter(description = "评论创建信息", required = true)
@Valid @RequestBody CommentCreateRequestDTO request) {
Long commentId = commentService.createComment(request);
return Results.success(commentId);
}
@PostMapping("/reply")
@Operation(summary = "回复评论", description = "回复指定评论")
public Result<Long> replyComment(
@Parameter(description = "评论回复信息", required = true)
@Valid @RequestBody CommentCreateRequestDTO request) {
Long commentId = commentService.replyComment(request);
return Results.success(commentId);
}
@DeleteMapping("/delete/{commentId}")
@Operation(summary = "删除评论", description = "删除指定评论")
public Result<Void> deleteComment(
@Parameter(description = "评论ID", required = true)
@PathVariable("commentId") Long commentId) {
commentService.deleteComment(commentId);
return Results.success();
}
@GetMapping("/{commentId}")
@Operation(summary = "查询评论详情", description = "根据评论ID查询评论详细信息")
public Result<CommentResponseDTO> getCommentById(
@Parameter(description = "评论ID", required = true)
@PathVariable("commentId") Long commentId) {
CommentResponseDTO comment = commentService.getCommentById(commentId);
return Results.success(comment);
}
@GetMapping("/article/{articleId}")
@Operation(summary = "查询文章评论", description = "根据文章ID查询该文章的所有评论树形结构")
public Result<List<CommentResponseDTO>> getCommentsByArticleId(
@Parameter(description = "文章ID", required = true)
@PathVariable("articleId") Long articleId) {
List<CommentResponseDTO> comments = commentService.getCommentsByArticleId(articleId);
return Results.success(comments);
}
@PostMapping("/list")
@Operation(summary = "分页查询评论列表", description = "支持多条件查询")
public Result<IPage<CommentResponseDTO>> getCommentList(
@Parameter(description = "查询条件")
@RequestBody CommentQueryRequestDTO request) {
IPage<CommentResponseDTO> page = commentService.getCommentList(request);
return Results.success(page);
}
@GetMapping("/children/{parentId}")
@Operation(summary = "查询子评论", description = "查询指定评论的所有子评论")
public Result<List<CommentResponseDTO>> getChildComments(
@Parameter(description = "父评论ID", required = true)
@PathVariable("parentId") Long parentId) {
List<CommentResponseDTO> comments = commentService.getChildComments(parentId);
return Results.success(comments);
}
}

View File

@@ -0,0 +1,73 @@
package cn.meowrain.aioj.backend.blogservice.controller;
import cn.meowrain.aioj.backend.framework.core.web.Result;
import cn.meowrain.aioj.backend.framework.core.web.Results;
import cn.meowrain.aioj.backend.blogservice.dto.req.DraftSaveRequestDTO;
import cn.meowrain.aioj.backend.blogservice.dto.resp.DraftResponseDTO;
import cn.meowrain.aioj.backend.blogservice.service.DraftService;
import com.baomidou.mybatisplus.core.metadata.IPage;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
/**
* 草稿箱控制器
*
* @author AIOJ
*/
@RequiredArgsConstructor
@RestController
@RequestMapping("/v1/draft")
@Tag(name = "草稿箱管理", description = "草稿保存、查询、删除等接口")
public class DraftController {
private final DraftService draftService;
@PostMapping("/save")
@Operation(summary = "保存草稿", description = "保存或更新文章草稿")
public Result<Long> saveDraft(
@Parameter(description = "草稿信息", required = true)
@Valid @RequestBody DraftSaveRequestDTO request) {
Long draftId = draftService.saveDraft(request);
return Results.success(draftId);
}
@DeleteMapping("/delete/{draftId}")
@Operation(summary = "删除草稿", description = "删除指定草稿")
public Result<Void> deleteDraft(
@Parameter(description = "草稿ID", required = true)
@PathVariable("draftId") Long draftId) {
draftService.deleteDraft(draftId);
return Results.success();
}
@GetMapping("/{draftId}")
@Operation(summary = "查询草稿详情", description = "根据草稿ID查询草稿详细信息")
public Result<DraftResponseDTO> getDraftById(
@Parameter(description = "草稿ID", required = true)
@PathVariable("draftId") Long draftId) {
DraftResponseDTO draft = draftService.getDraftById(draftId);
return Results.success(draft);
}
@GetMapping("/list")
@Operation(summary = "分页查询草稿列表", description = "获取当前用户的草稿列表")
public Result<IPage<DraftResponseDTO>> getDraftList(
@Parameter(description = "页码", required = true)
@RequestParam("current") Integer current,
@Parameter(description = "每页数量", required = true)
@RequestParam("size") Integer size) {
IPage<DraftResponseDTO> page = draftService.getDraftList(current, size);
return Results.success(page);
}
@GetMapping("/latest")
@Operation(summary = "查询最新草稿", description = "获取最新的自动保存草稿")
public Result<DraftResponseDTO> getLatestDraft() {
DraftResponseDTO draft = draftService.getLatestDraft();
return Results.success(draft);
}
}

View File

@@ -0,0 +1,99 @@
package cn.meowrain.aioj.backend.blogservice.controller;
import cn.meowrain.aioj.backend.framework.core.web.Result;
import cn.meowrain.aioj.backend.framework.core.web.Results;
import cn.meowrain.aioj.backend.blogservice.dto.req.FollowRequestDTO;
import cn.meowrain.aioj.backend.blogservice.service.FollowService;
import com.baomidou.mybatisplus.core.metadata.IPage;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
/**
* 用户关注控制器
*
* @author AIOJ
*/
@RequiredArgsConstructor
@RestController
@RequestMapping("/v1/follow")
@Tag(name = "用户关注管理", description = "关注、取消关注、查询关注列表等接口")
public class FollowController {
private final FollowService followService;
@PostMapping("/follow")
@Operation(summary = "关注用户", description = "关注指定用户")
public Result<Void> followUser(
@Parameter(description = "关注信息", required = true)
@Valid @RequestBody FollowRequestDTO request) {
followService.followUser(request);
return Results.success();
}
@DeleteMapping("/unfollow/{followingId}")
@Operation(summary = "取消关注用户", description = "取消关注指定用户")
public Result<Void> unfollowUser(
@Parameter(description = "被关注者ID", required = true)
@PathVariable("followingId") Long followingId) {
followService.unfollowUser(followingId);
return Results.success();
}
@GetMapping("/check/{followingId}")
@Operation(summary = "查询关注状态", description = "查询当前用户是否关注了指定用户")
public Result<Boolean> isFollowing(
@Parameter(description = "被关注者ID", required = true)
@PathVariable("followingId") Long followingId) {
Long currentUserId = cn.meowrain.aioj.backend.framework.core.utils.ContextHolderUtils.getCurrentUserId();
Boolean following = followService.isFollowing(currentUserId, followingId);
return Results.success(following);
}
@GetMapping("/following/list")
@Operation(summary = "查询关注列表", description = "分页查询当前用户关注的人")
public Result<IPage<Long>> getFollowingList(
@Parameter(description = "用户ID", required = true)
@RequestParam("userId") Long userId,
@Parameter(description = "页码", required = true)
@RequestParam("current") Integer current,
@Parameter(description = "每页数量", required = true)
@RequestParam("size") Integer size) {
IPage<Long> page = followService.getFollowingList(userId, current, size);
return Results.success(page);
}
@GetMapping("/followers/list")
@Operation(summary = "查询粉丝列表", description = "分页查询当前用户的粉丝")
public Result<IPage<Long>> getFollowerList(
@Parameter(description = "用户ID", required = true)
@RequestParam("userId") Long userId,
@Parameter(description = "页码", required = true)
@RequestParam("current") Integer current,
@Parameter(description = "每页数量", required = true)
@RequestParam("size") Integer size) {
IPage<Long> page = followService.getFollowerList(userId, current, size);
return Results.success(page);
}
@GetMapping("/following/count")
@Operation(summary = "查询关注数", description = "获取用户的关注数量")
public Result<Long> getFollowingCount(
@Parameter(description = "用户ID", required = true)
@RequestParam("userId") Long userId) {
Long count = followService.getFollowingCount(userId);
return Results.success(count);
}
@GetMapping("/followers/count")
@Operation(summary = "查询粉丝数", description = "获取用户的粉丝数量")
public Result<Long> getFollowerCount(
@Parameter(description = "用户ID", required = true)
@RequestParam("userId") Long userId) {
Long count = followService.getFollowerCount(userId);
return Results.success(count);
}
}

View File

@@ -0,0 +1,65 @@
package cn.meowrain.aioj.backend.blogservice.controller;
import cn.meowrain.aioj.backend.framework.core.web.Result;
import cn.meowrain.aioj.backend.framework.core.web.Results;
import cn.meowrain.aioj.backend.blogservice.dto.req.LikeRequestDTO;
import cn.meowrain.aioj.backend.blogservice.service.LikeService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
/**
* 点赞管理控制器
*
* @author AIOJ
*/
@RequiredArgsConstructor
@RestController
@RequestMapping("/v1/like")
@Tag(name = "点赞管理", description = "文章和评论的点赞、取消点赞等接口")
public class LikeController {
private final LikeService likeService;
@PostMapping("/like")
@Operation(summary = "点赞", description = "对文章或评论进行点赞")
public Result<Void> like(
@Parameter(description = "点赞信息", required = true)
@Valid @RequestBody LikeRequestDTO request) {
likeService.like(request);
return Results.success();
}
@PostMapping("/unlike")
@Operation(summary = "取消点赞", description = "取消对文章或评论的点赞")
public Result<Void> unlike(
@Parameter(description = "点赞信息", required = true)
@Valid @RequestBody LikeRequestDTO request) {
likeService.unlike(request);
return Results.success();
}
@GetMapping("/check")
@Operation(summary = "查询点赞状态", description = "查询当前用户对指定目标的点赞状态")
public Result<Boolean> isLiked(
@Parameter(description = "目标ID", required = true)
@RequestParam("targetId") Long targetId,
@Parameter(description = "目标类型1=文章2=评论", required = true)
@RequestParam("targetType") Integer targetType) {
Long currentUserId = cn.meowrain.aioj.backend.framework.core.utils.ContextHolderUtils.getCurrentUserId();
Boolean liked = likeService.isLiked(currentUserId, targetId, targetType);
return Results.success(liked);
}
@PostMapping("/toggle")
@Operation(summary = "切换点赞状态", description = "点赞或取消点赞,根据当前状态自动切换")
public Result<Void> toggleLike(
@Parameter(description = "点赞信息", required = true)
@Valid @RequestBody LikeRequestDTO request) {
likeService.toggleLike(request);
return Results.success();
}
}

View File

@@ -0,0 +1,75 @@
package cn.meowrain.aioj.backend.blogservice.controller;
import cn.meowrain.aioj.backend.framework.core.web.Result;
import cn.meowrain.aioj.backend.framework.core.web.Results;
import cn.meowrain.aioj.backend.blogservice.dto.req.NotificationQueryRequestDTO;
import cn.meowrain.aioj.backend.blogservice.dto.resp.NotificationResponseDTO;
import cn.meowrain.aioj.backend.blogservice.service.NotificationService;
import com.baomidou.mybatisplus.core.metadata.IPage;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
/**
* 通知管理控制器
*
* @author AIOJ
*/
@RequiredArgsConstructor
@RestController
@RequestMapping("/v1/notification")
@Tag(name = "通知管理", description = "通知查询、标记已读、清空通知等接口")
public class NotificationController {
private final NotificationService notificationService;
@PostMapping("/list")
@Operation(summary = "分页查询通知列表", description = "支持多条件查询")
public Result<IPage<NotificationResponseDTO>> getNotificationList(
@Parameter(description = "查询条件")
@RequestBody NotificationQueryRequestDTO request) {
IPage<NotificationResponseDTO> page = notificationService.getNotificationList(request);
return Results.success(page);
}
@PutMapping("/read/{notificationId}")
@Operation(summary = "标记通知为已读", description = "将指定通知标记为已读状态")
public Result<Void> markAsRead(
@Parameter(description = "通知ID", required = true)
@PathVariable("notificationId") Long notificationId) {
notificationService.markAsRead(notificationId);
return Results.success();
}
@PutMapping("/read/batch")
@Operation(summary = "批量标记通知为已读", description = "将多个通知批量标记为已读状态")
public Result<Void> batchMarkAsRead(
@Parameter(description = "通知ID列表", required = true)
@RequestBody java.util.List<Long> notificationIds) {
notificationService.batchMarkAsRead(notificationIds);
return Results.success();
}
@PutMapping("/read/all")
@Operation(summary = "标记所有通知为已读", description = "将当前用户的所有未读通知标记为已读")
public Result<Void> markAllAsRead() {
notificationService.markAllAsRead();
return Results.success();
}
@DeleteMapping("/clear/all")
@Operation(summary = "清空所有通知", description = "清空当前用户的所有通知")
public Result<Void> clearAllNotifications() {
notificationService.clearAllNotifications();
return Results.success();
}
@GetMapping("/unread/count")
@Operation(summary = "查询未读通知数量", description = "获取当前用户的未读通知数量")
public Result<Long> getUnreadCount() {
Long count = notificationService.getUnreadCount();
return Results.success(count);
}
}

View File

@@ -0,0 +1,98 @@
package cn.meowrain.aioj.backend.blogservice.controller;
import cn.meowrain.aioj.backend.framework.core.web.Result;
import cn.meowrain.aioj.backend.framework.core.web.Results;
import cn.meowrain.aioj.backend.blogservice.dto.req.TagCreateRequestDTO;
import cn.meowrain.aioj.backend.blogservice.dto.req.TagUpdateRequestDTO;
import cn.meowrain.aioj.backend.blogservice.dto.resp.TagResponseDTO;
import cn.meowrain.aioj.backend.blogservice.service.TagService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 文章标签控制器
*
* @author AIOJ
*/
@RequiredArgsConstructor
@RestController
@RequestMapping("/v1/tag")
@Tag(name = "文章标签管理", description = "文章标签的创建、查询、更新、删除等接口")
public class TagController {
private final TagService tagService;
@PostMapping("/create")
@Operation(summary = "创建标签", description = "创建新的文章标签")
public Result<Long> createTag(
@Parameter(description = "标签创建信息", required = true)
@Valid @RequestBody TagCreateRequestDTO request) {
Long tagId = tagService.createTag(request);
return Results.success(tagId);
}
@PutMapping("/update")
@Operation(summary = "更新标签", description = "更新已存在的标签信息")
public Result<Void> updateTag(
@Parameter(description = "标签更新信息", required = true)
@Valid @RequestBody TagUpdateRequestDTO request) {
tagService.updateTag(request);
return Results.success();
}
@DeleteMapping("/delete/{tagId}")
@Operation(summary = "删除标签", description = "删除指定标签")
public Result<Void> deleteTag(
@Parameter(description = "标签ID", required = true)
@PathVariable("tagId") Long tagId) {
tagService.deleteTag(tagId);
return Results.success();
}
@GetMapping("/{tagId}")
@Operation(summary = "查询标签详情", description = "根据标签ID查询标签详细信息")
public Result<TagResponseDTO> getTagById(
@Parameter(description = "标签ID", required = true)
@PathVariable("tagId") Long tagId) {
TagResponseDTO tag = tagService.getTagById(tagId);
return Results.success(tag);
}
@GetMapping("/slug/{slug}")
@Operation(summary = "根据slug查询标签", description = "根据标签别名查询标签详细信息")
public Result<TagResponseDTO> getTagBySlug(
@Parameter(description = "标签别名", required = true)
@PathVariable("slug") String slug) {
TagResponseDTO tag = tagService.getTagBySlug(slug);
return Results.success(tag);
}
@GetMapping("/list/all")
@Operation(summary = "查询所有标签", description = "获取所有标签列表")
public Result<List<TagResponseDTO>> getAllTags() {
List<TagResponseDTO> tags = tagService.getAllTags();
return Results.success(tags);
}
@GetMapping("/list/enabled")
@Operation(summary = "查询启用的标签", description = "获取所有启用状态的标签列表")
public Result<List<TagResponseDTO>> getEnabledTags() {
List<TagResponseDTO> tags = tagService.getEnabledTags();
return Results.success(tags);
}
@GetMapping("/article/{articleId}")
@Operation(summary = "查询文章的标签", description = "根据文章ID查询该文章的所有标签")
public Result<List<TagResponseDTO>> getTagsByArticleId(
@Parameter(description = "文章ID", required = true)
@PathVariable("articleId") Long articleId) {
List<TagResponseDTO> tags = tagService.getTagsByArticleId(articleId);
return Results.success(tags);
}
}

View File

@@ -0,0 +1,123 @@
package cn.meowrain.aioj.backend.blogservice.dao.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import lombok.experimental.Accessors;
import java.io.Serializable;
import java.util.Date;
/**
* 文章实体类
*
* @author AIOJ
*/
@Data
@TableName(value = "blog_article")
@Accessors(chain = true)
public class Article implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主键
*/
@TableId(type = IdType.ASSIGN_ID)
private Long id;
/**
* 文章标题
*/
private String title;
/**
* 文章别名URL友好标识用于SEO
*/
private String slug;
/**
* 文章摘要
*/
private String summary;
/**
* 文章内容Markdown格式
*/
private String content;
/**
* 渲染后的HTML内容可选缓存
*/
private String contentHtml;
/**
* 封面图片URL
*/
private String coverImage;
/**
* 作者用户ID
*/
private Long authorId;
/**
* 分类ID
*/
private Long categoryId;
/**
* 浏览次数
*/
private Integer viewCount;
/**
* 点赞数(冗余字段,便于查询)
*/
private Integer likeCount;
/**
* 评论数(冗余字段,便于查询)
*/
private Integer commentCount;
/**
* 收藏数(冗余字段,便于查询)
*/
private Integer collectCount;
/**
* 是否置顶1=置顶0=普通)
*/
private Integer isTop;
/**
* 是否精华1=精华0=普通)
*/
private Integer isEssence;
/**
* 是否发布1=已发布0=草稿)
*/
private Integer isPublished;
/**
* 文章状态1=正常2=审核中3=已关闭4=已删除
*/
private Integer status;
/**
* 发布时间
*/
private Date publishTime;
/**
* 创建时间
*/
@TableField(fill = FieldFill.INSERT)
private Date createTime;
/**
* 更新时间
*/
private Date updateTime;
}

View File

@@ -0,0 +1,43 @@
package cn.meowrain.aioj.backend.blogservice.dao.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import lombok.experimental.Accessors;
import java.io.Serializable;
import java.util.Date;
/**
* 文章标签关联实体类
*
* @author AIOJ
*/
@Data
@TableName(value = "blog_article_tag")
@Accessors(chain = true)
public class ArticleTag implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主键
*/
@TableId(type = IdType.ASSIGN_ID)
private Long id;
/**
* 文章ID
*/
private Long articleId;
/**
* 标签ID
*/
private Long tagId;
/**
* 创建时间
*/
@TableField(fill = FieldFill.INSERT)
private Date createTime;
}

View File

@@ -0,0 +1,78 @@
package cn.meowrain.aioj.backend.blogservice.dao.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import lombok.experimental.Accessors;
import java.io.Serializable;
import java.util.Date;
/**
* 文章分类实体类
*
* @author AIOJ
*/
@Data
@TableName(value = "blog_category")
@Accessors(chain = true)
public class Category implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主键
*/
@TableId(type = IdType.ASSIGN_ID)
private Long id;
/**
* 分类名称
*/
private String name;
/**
* 分类别名URL友好标识
*/
private String slug;
/**
* 分类描述
*/
private String description;
/**
* 分类图标
*/
private String icon;
/**
* 排序序号(越小越靠前)
*/
private Integer sortOrder;
/**
* 父分类ID0表示顶级分类
*/
private Long parentId;
/**
* 该分类下的文章数量
*/
private Integer articleCount;
/**
* 是否启用1=启用0=禁用)
*/
private Integer isEnabled;
/**
* 创建时间
*/
@TableField(fill = FieldFill.INSERT)
private Date createTime;
/**
* 更新时间
*/
private Date updateTime;
}

View File

@@ -0,0 +1,63 @@
package cn.meowrain.aioj.backend.blogservice.dao.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import lombok.experimental.Accessors;
import java.io.Serializable;
import java.util.Date;
/**
* 收藏实体类
*
* @author AIOJ
*/
@Data
@TableName(value = "blog_collection")
@Accessors(chain = true)
public class Collection implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主键
*/
@TableId(type = IdType.ASSIGN_ID)
private Long id;
/**
* 用户ID
*/
private Long userId;
/**
* 文章ID
*/
private Long articleId;
/**
* 收藏夹ID0表示默认收藏夹
*/
private Long folderId;
/**
* 收藏备注
*/
private String note;
/**
* 是否已取消1=已取消0=有效)
*/
private Integer isCancelled;
/**
* 创建时间
*/
@TableField(fill = FieldFill.INSERT)
private Date createTime;
/**
* 更新时间
*/
private Date updateTime;
}

View File

@@ -0,0 +1,63 @@
package cn.meowrain.aioj.backend.blogservice.dao.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import lombok.experimental.Accessors;
import java.io.Serializable;
import java.util.Date;
/**
* 收藏夹实体类
*
* @author AIOJ
*/
@Data
@TableName(value = "blog_collection_folder")
@Accessors(chain = true)
public class CollectionFolder implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主键
*/
@TableId(type = IdType.ASSIGN_ID)
private Long id;
/**
* 用户ID
*/
private Long userId;
/**
* 收藏夹名称
*/
private String name;
/**
* 收藏夹描述
*/
private String description;
/**
* 是否公开1=公开0=私有)
*/
private Integer isPublic;
/**
* 收藏数量
*/
private Integer collectCount;
/**
* 创建时间
*/
@TableField(fill = FieldFill.INSERT)
private Date createTime;
/**
* 更新时间
*/
private Date updateTime;
}

View File

@@ -0,0 +1,98 @@
package cn.meowrain.aioj.backend.blogservice.dao.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import lombok.experimental.Accessors;
import java.io.Serializable;
import java.util.Date;
/**
* 评论实体类
*
* @author AIOJ
*/
@Data
@TableName(value = "blog_comment")
@Accessors(chain = true)
public class Comment implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主键
*/
@TableId(type = IdType.ASSIGN_ID)
private Long id;
/**
* 文章ID
*/
private Long articleId;
/**
* 评论用户ID
*/
private Long userId;
/**
* 父评论ID0表示一级评论
*/
private Long parentId;
/**
* 回复的评论ID用于@提醒)
*/
private Long replyToId;
/**
* 评论内容支持Markdown
*/
private String content;
/**
* 渲染后的HTML内容
*/
private String contentHtml;
/**
* 点赞数
*/
private Integer likeCount;
/**
* 回复数
*/
private Integer replyCount;
/**
* 是否为作者评论1=是0=否)
*/
private Integer isAuthor;
/**
* 状态1=正常2=待审核3=已删除
*/
private Integer status;
/**
* IP地址
*/
private String ipAddress;
/**
* 用户代理
*/
private String userAgent;
/**
* 创建时间
*/
@TableField(fill = FieldFill.INSERT)
private Date createTime;
/**
* 更新时间
*/
private Date updateTime;
}

View File

@@ -0,0 +1,68 @@
package cn.meowrain.aioj.backend.blogservice.dao.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import lombok.experimental.Accessors;
import java.io.Serializable;
import java.util.Date;
/**
* 草稿箱实体类
*
* @author AIOJ
*/
@Data
@TableName(value = "blog_draft")
@Accessors(chain = true)
public class Draft implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主键
*/
@TableId(type = IdType.ASSIGN_ID)
private Long id;
/**
* 用户ID
*/
private Long userId;
/**
* 文章标题
*/
private String title;
/**
* 文章内容Markdown格式
*/
private String content;
/**
* 分类ID
*/
private Long categoryId;
/**
* 封面图片URL
*/
private String coverImage;
/**
* 是否自动保存1=是0=否)
*/
private Integer autoSave;
/**
* 创建时间
*/
@TableField(fill = FieldFill.INSERT)
private Date createTime;
/**
* 更新时间
*/
private Date updateTime;
}

View File

@@ -0,0 +1,53 @@
package cn.meowrain.aioj.backend.blogservice.dao.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import lombok.experimental.Accessors;
import java.io.Serializable;
import java.util.Date;
/**
* 用户关注实体类
*
* @author AIOJ
*/
@Data
@TableName(value = "blog_follow")
@Accessors(chain = true)
public class Follow implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主键
*/
@TableId(type = IdType.ASSIGN_ID)
private Long id;
/**
* 关注者ID
*/
private Long followerId;
/**
* 被关注者ID
*/
private Long followingId;
/**
* 是否已取消1=已取消0=有效)
*/
private Integer isCancelled;
/**
* 创建时间
*/
@TableField(fill = FieldFill.INSERT)
private Date createTime;
/**
* 更新时间
*/
private Date updateTime;
}

View File

@@ -0,0 +1,58 @@
package cn.meowrain.aioj.backend.blogservice.dao.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import lombok.experimental.Accessors;
import java.io.Serializable;
import java.util.Date;
/**
* 点赞实体类
*
* @author AIOJ
*/
@Data
@TableName(value = "blog_like")
@Accessors(chain = true)
public class Like implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主键
*/
@TableId(type = IdType.ASSIGN_ID)
private Long id;
/**
* 用户ID
*/
private Long userId;
/**
* 目标ID文章ID或评论ID
*/
private Long targetId;
/**
* 目标类型1=文章2=评论
*/
private Integer targetType;
/**
* 是否已取消1=已取消0=有效)
*/
private Integer isCancelled;
/**
* 创建时间
*/
@TableField(fill = FieldFill.INSERT)
private Date createTime;
/**
* 更新时间
*/
private Date updateTime;
}

View File

@@ -0,0 +1,73 @@
package cn.meowrain.aioj.backend.blogservice.dao.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import lombok.experimental.Accessors;
import java.io.Serializable;
import java.util.Date;
/**
* 通知实体类
*
* @author AIOJ
*/
@Data
@TableName(value = "blog_notification")
@Accessors(chain = true)
public class Notification implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主键
*/
@TableId(type = IdType.ASSIGN_ID)
private Long id;
/**
* 接收用户ID
*/
private Long userId;
/**
* 通知类型1=评论2=点赞3=收藏4=关注5=回复6=系统通知
*/
private Integer type;
/**
* 通知标题
*/
private String title;
/**
* 通知内容
*/
private String content;
/**
* 跳转链接
*/
private String linkUrl;
/**
* 发送者ID系统通知为NULL
*/
private Long senderId;
/**
* 关联目标ID文章ID、评论ID等
*/
private Long targetId;
/**
* 是否已读1=已读0=未读)
*/
private Integer isRead;
/**
* 创建时间
*/
@TableField(fill = FieldFill.INSERT)
private Date createTime;
}

View File

@@ -0,0 +1,68 @@
package cn.meowrain.aioj.backend.blogservice.dao.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import lombok.experimental.Accessors;
import java.io.Serializable;
import java.util.Date;
/**
* 文章标签实体类
*
* @author AIOJ
*/
@Data
@TableName(value = "blog_tag")
@Accessors(chain = true)
public class Tag implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主键
*/
@TableId(type = IdType.ASSIGN_ID)
private Long id;
/**
* 标签名称
*/
private String name;
/**
* 标签别名URL友好标识
*/
private String slug;
/**
* 标签描述
*/
private String description;
/**
* 标签颜色(十六进制)
*/
private String color;
/**
* 使用该标签的文章数量
*/
private Integer articleCount;
/**
* 是否启用1=启用0=禁用)
*/
private Integer isEnabled;
/**
* 创建时间
*/
@TableField(fill = FieldFill.INSERT)
private Date createTime;
/**
* 更新时间
*/
private Date updateTime;
}

View File

@@ -0,0 +1,88 @@
package cn.meowrain.aioj.backend.blogservice.dao.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import lombok.experimental.Accessors;
import java.io.Serializable;
import java.util.Date;
/**
* 用户社区统计实体类
*
* @author AIOJ
*/
@Data
@TableName(value = "blog_user_stat")
@Accessors(chain = true)
public class UserStat implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主键
*/
@TableId(type = IdType.ASSIGN_ID)
private Long id;
/**
* 用户ID
*/
private Long userId;
/**
* 发布文章数
*/
private Integer articleCount;
/**
* 发表评论数
*/
private Integer commentCount;
/**
* 获得点赞数
*/
private Integer likeCount;
/**
* 获得收藏数
*/
private Integer collectCount;
/**
* 文章被浏览数
*/
private Integer viewCount;
/**
* 粉丝数
*/
private Integer followerCount;
/**
* 关注数
*/
private Integer followingCount;
/**
* 用户等级
*/
private Integer level;
/**
* 经验值
*/
private Integer experience;
/**
* 创建时间
*/
@TableField(fill = FieldFill.INSERT)
private Date createTime;
/**
* 更新时间
*/
private Date updateTime;
}

View File

@@ -0,0 +1,58 @@
package cn.meowrain.aioj.backend.blogservice.dao.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import lombok.experimental.Accessors;
import java.io.Serializable;
import java.util.Date;
/**
* 浏览记录实体类
*
* @author AIOJ
*/
@Data
@TableName(value = "blog_view")
@Accessors(chain = true)
public class View implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主键
*/
@TableId(type = IdType.ASSIGN_ID)
private Long id;
/**
* 文章ID
*/
private Long articleId;
/**
* 用户IDNULL表示游客
*/
private Long userId;
/**
* IP地址
*/
private String ipAddress;
/**
* 用户代理
*/
private String userAgent;
/**
* 浏览时长(秒)
*/
private Integer duration;
/**
* 创建时间
*/
@TableField(fill = FieldFill.INSERT)
private Date createTime;
}

View File

@@ -0,0 +1,15 @@
package cn.meowrain.aioj.backend.blogservice.dao.mapper;
import cn.meowrain.aioj.backend.blogservice.dao.entity.Article;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
/**
* 文章Mapper接口
*
* @author AIOJ
*/
@Mapper
public interface ArticleMapper extends BaseMapper<Article> {
}

View File

@@ -0,0 +1,15 @@
package cn.meowrain.aioj.backend.blogservice.dao.mapper;
import cn.meowrain.aioj.backend.blogservice.dao.entity.ArticleTag;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
/**
* 文章标签关联Mapper接口
*
* @author AIOJ
*/
@Mapper
public interface ArticleTagMapper extends BaseMapper<ArticleTag> {
}

View File

@@ -0,0 +1,15 @@
package cn.meowrain.aioj.backend.blogservice.dao.mapper;
import cn.meowrain.aioj.backend.blogservice.dao.entity.Category;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
/**
* 文章分类Mapper接口
*
* @author AIOJ
*/
@Mapper
public interface CategoryMapper extends BaseMapper<Category> {
}

View File

@@ -0,0 +1,15 @@
package cn.meowrain.aioj.backend.blogservice.dao.mapper;
import cn.meowrain.aioj.backend.blogservice.dao.entity.CollectionFolder;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
/**
* 收藏夹Mapper接口
*
* @author AIOJ
*/
@Mapper
public interface CollectionFolderMapper extends BaseMapper<CollectionFolder> {
}

View File

@@ -0,0 +1,15 @@
package cn.meowrain.aioj.backend.blogservice.dao.mapper;
import cn.meowrain.aioj.backend.blogservice.dao.entity.Collection;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
/**
* 收藏Mapper接口
*
* @author AIOJ
*/
@Mapper
public interface CollectionMapper extends BaseMapper<Collection> {
}

View File

@@ -0,0 +1,15 @@
package cn.meowrain.aioj.backend.blogservice.dao.mapper;
import cn.meowrain.aioj.backend.blogservice.dao.entity.Comment;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
/**
* 评论Mapper接口
*
* @author AIOJ
*/
@Mapper
public interface CommentMapper extends BaseMapper<Comment> {
}

View File

@@ -0,0 +1,15 @@
package cn.meowrain.aioj.backend.blogservice.dao.mapper;
import cn.meowrain.aioj.backend.blogservice.dao.entity.Draft;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
/**
* 草稿箱Mapper接口
*
* @author AIOJ
*/
@Mapper
public interface DraftMapper extends BaseMapper<Draft> {
}

View File

@@ -0,0 +1,15 @@
package cn.meowrain.aioj.backend.blogservice.dao.mapper;
import cn.meowrain.aioj.backend.blogservice.dao.entity.Follow;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
/**
* 用户关注Mapper接口
*
* @author AIOJ
*/
@Mapper
public interface FollowMapper extends BaseMapper<Follow> {
}

View File

@@ -0,0 +1,15 @@
package cn.meowrain.aioj.backend.blogservice.dao.mapper;
import cn.meowrain.aioj.backend.blogservice.dao.entity.Like;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
/**
* 点赞Mapper接口
*
* @author AIOJ
*/
@Mapper
public interface LikeMapper extends BaseMapper<Like> {
}

View File

@@ -0,0 +1,15 @@
package cn.meowrain.aioj.backend.blogservice.dao.mapper;
import cn.meowrain.aioj.backend.blogservice.dao.entity.Notification;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
/**
* 通知Mapper接口
*
* @author AIOJ
*/
@Mapper
public interface NotificationMapper extends BaseMapper<Notification> {
}

View File

@@ -0,0 +1,15 @@
package cn.meowrain.aioj.backend.blogservice.dao.mapper;
import cn.meowrain.aioj.backend.blogservice.dao.entity.Tag;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
/**
* 文章标签Mapper接口
*
* @author AIOJ
*/
@Mapper
public interface TagMapper extends BaseMapper<Tag> {
}

View File

@@ -0,0 +1,15 @@
package cn.meowrain.aioj.backend.blogservice.dao.mapper;
import cn.meowrain.aioj.backend.blogservice.dao.entity.UserStat;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
/**
* 用户社区统计Mapper接口
*
* @author AIOJ
*/
@Mapper
public interface UserStatMapper extends BaseMapper<UserStat> {
}

View File

@@ -0,0 +1,15 @@
package cn.meowrain.aioj.backend.blogservice.dao.mapper;
import cn.meowrain.aioj.backend.blogservice.dao.entity.View;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
/**
* 浏览记录Mapper接口
*
* @author AIOJ
*/
@Mapper
public interface ViewMapper extends BaseMapper<View> {
}

View File

@@ -0,0 +1,51 @@
package cn.meowrain.aioj.backend.blogservice.dto.req;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.util.List;
/**
* 文章创建请求DTO
*
* @author AIOJ
*/
@Data
@Schema(description = "文章创建请求")
public class ArticleCreateRequestDTO {
@NotBlank(message = "文章标题不能为空")
@Schema(description = "文章标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "Spring Boot 3.5.7 新特性详解")
private String title;
@Schema(description = "文章别名URL友好标识", example = "spring-boot-3-5-7-features")
private String slug;
@Schema(description = "文章摘要", example = "本文详细介绍Spring Boot 3.5.7版本的新特性...")
private String summary;
@NotBlank(message = "文章内容不能为空")
@Schema(description = "文章内容Markdown格式", requiredMode = Schema.RequiredMode.REQUIRED)
private String content;
@Schema(description = "封面图片URL", example = "https://example.com/images/cover.jpg")
private String coverImage;
@NotNull(message = "分类ID不能为空")
@Schema(description = "分类ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Long categoryId;
@Schema(description = "标签ID列表", example = "[1, 2, 3]")
private List<Long> tagIds;
@Schema(description = "是否置顶", example = "0")
private Integer isTop;
@Schema(description = "是否精华", example = "0")
private Integer isEssence;
@Schema(description = "是否发布1=发布0=草稿)", example = "1")
private Integer isPublished;
}

View File

@@ -0,0 +1,53 @@
package cn.meowrain.aioj.backend.blogservice.dto.req;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* 文章查询请求DTO
*
* @author AIOJ
*/
@Data
@Schema(description = "文章查询请求")
public class ArticleQueryRequestDTO {
@Schema(description = "页码", example = "1")
private Integer current = 1;
@Schema(description = "每页数量", example = "10")
private Integer size = 10;
@Schema(description = "文章ID", example = "1")
private Long id;
@Schema(description = "文章标题(模糊查询)", example = "Spring Boot")
private String title;
@Schema(description = "分类ID", example = "1")
private Long categoryId;
@Schema(description = "标签ID", example = "1")
private Long tagId;
@Schema(description = "作者ID", example = "1")
private Long authorId;
@Schema(description = "是否置顶", example = "0")
private Integer isTop;
@Schema(description = "是否精华", example = "0")
private Integer isEssence;
@Schema(description = "是否发布1=发布0=草稿)", example = "1")
private Integer isPublished;
@Schema(description = "文章状态1=正常2=审核中3=已关闭4=已删除", example = "1")
private Integer status;
@Schema(description = "排序字段view_count/like_count/comment_count/publish_time", example = "publish_time")
private String sortField;
@Schema(description = "排序方式asc/desc", example = "desc")
private String sortOrder;
}

View File

@@ -0,0 +1,55 @@
package cn.meowrain.aioj.backend.blogservice.dto.req;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.util.List;
/**
* 文章更新请求DTO
*
* @author AIOJ
*/
@Data
@Schema(description = "文章更新请求")
public class ArticleUpdateRequestDTO {
@NotNull(message = "文章ID不能为空")
@Schema(description = "文章ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Long id;
@NotBlank(message = "文章标题不能为空")
@Schema(description = "文章标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "Spring Boot 3.5.7 新特性详解(更新版)")
private String title;
@Schema(description = "文章别名URL友好标识", example = "spring-boot-3-5-7-features")
private String slug;
@Schema(description = "文章摘要", example = "本文详细介绍Spring Boot 3.5.7版本的新特性...")
private String summary;
@NotBlank(message = "文章内容不能为空")
@Schema(description = "文章内容Markdown格式", requiredMode = Schema.RequiredMode.REQUIRED)
private String content;
@Schema(description = "封面图片URL", example = "https://example.com/images/cover.jpg")
private String coverImage;
@NotNull(message = "分类ID不能为空")
@Schema(description = "分类ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Long categoryId;
@Schema(description = "标签ID列表", example = "[1, 2, 3]")
private List<Long> tagIds;
@Schema(description = "是否置顶", example = "0")
private Integer isTop;
@Schema(description = "是否精华", example = "0")
private Integer isEssence;
@Schema(description = "是否发布1=发布0=草稿)", example = "1")
private Integer isPublished;
}

View File

@@ -0,0 +1,40 @@
package cn.meowrain.aioj.backend.blogservice.dto.req;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
/**
* 分类创建请求DTO
*
* @author AIOJ
*/
@Data
@Schema(description = "分类创建请求")
public class CategoryCreateRequestDTO {
@NotBlank(message = "分类名称不能为空")
@Schema(description = "分类名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "算法题解")
private String name;
@NotBlank(message = "分类别名不能为空")
@Schema(description = "分类别名URL友好标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "algorithm-solutions")
private String slug;
@Schema(description = "分类描述", example = "算法题目解题思路和代码分享")
private String description;
@Schema(description = "分类图标", example = "💡")
private String icon;
@NotNull(message = "排序序号不能为空")
@Schema(description = "排序序号(越小越靠前)", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Integer sortOrder;
@Schema(description = "父分类ID0表示顶级分类", example = "0")
private Long parentId = 0L;
@Schema(description = "是否启用1=启用0=禁用)", example = "1")
private Integer isEnabled = 1;
}

View File

@@ -0,0 +1,44 @@
package cn.meowrain.aioj.backend.blogservice.dto.req;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
/**
* 分类更新请求DTO
*
* @author AIOJ
*/
@Data
@Schema(description = "分类更新请求")
public class CategoryUpdateRequestDTO {
@NotNull(message = "分类ID不能为空")
@Schema(description = "分类ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Long id;
@NotBlank(message = "分类名称不能为空")
@Schema(description = "分类名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "算法题解")
private String name;
@NotBlank(message = "分类别名不能为空")
@Schema(description = "分类别名URL友好标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "algorithm-solutions")
private String slug;
@Schema(description = "分类描述", example = "算法题目解题思路和代码分享")
private String description;
@Schema(description = "分类图标", example = "💡")
private String icon;
@NotNull(message = "排序序号不能为空")
@Schema(description = "排序序号(越小越靠前)", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Integer sortOrder;
@Schema(description = "父分类ID0表示顶级分类", example = "0")
private Long parentId;
@Schema(description = "是否启用1=启用0=禁用)", example = "1")
private Integer isEnabled;
}

View File

@@ -0,0 +1,25 @@
package cn.meowrain.aioj.backend.blogservice.dto.req;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
/**
* 收藏夹创建请求DTO
*
* @author AIOJ
*/
@Data
@Schema(description = "收藏夹创建请求")
public class CollectionFolderCreateRequestDTO {
@NotBlank(message = "收藏夹名称不能为空")
@Schema(description = "收藏夹名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "我的技术收藏")
private String name;
@Schema(description = "收藏夹描述", example = "收集技术相关的优质文章")
private String description;
@Schema(description = "是否公开1=公开0=私有)", example = "0")
private Integer isPublic = 0;
}

View File

@@ -0,0 +1,25 @@
package cn.meowrain.aioj.backend.blogservice.dto.req;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
/**
* 收藏请求DTO
*
* @author AIOJ
*/
@Data
@Schema(description = "收藏请求")
public class CollectionRequestDTO {
@NotNull(message = "文章ID不能为空")
@Schema(description = "文章ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Long articleId;
@Schema(description = "收藏夹ID0表示默认收藏夹", example = "0")
private Long folderId = 0L;
@Schema(description = "收藏备注", example = "很好的文章,值得学习")
private String note;
}

Some files were not shown because too many files have changed in this diff Show More