Compare commits
36 Commits
cacf7ed820
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| bcacf1a164 | |||
| 1945cc2fb1 | |||
| 67825a8c5c | |||
| a34168ef75 | |||
| 45f8348395 | |||
| 5681b6bcef | |||
| c06cfc10ee | |||
| be709efa2e | |||
| 17f58a7b45 | |||
| 9337540c77 | |||
| 873fc3b149 | |||
| cf0e326b0c | |||
| 61fb847ac1 | |||
| ef6b5cb11e | |||
| 51d16ea077 | |||
| 439fdf90c4 | |||
| 08043672f9 | |||
| c3c07ff1e7 | |||
| 5522eaa1d6 | |||
| 93759b4a1a | |||
| c9e9a1a4c7 | |||
| a4575cebd4 | |||
| 8bd56a6001 | |||
| 637f125348 | |||
| 4ee3ebcbec | |||
| 2e2697140c | |||
| 3657f88970 | |||
| dfcb7d978b | |||
| 7aacad2596 | |||
| 47a468096d | |||
| fc72acf490 | |||
| 05aeef2f79 | |||
| 9a20a52afb | |||
| 4a4a010f83 | |||
| d32970ded7 | |||
| 6f0ee9bbf5 |
149
.claude/agents/java-spring-backend-architect.md
Normal file
149
.claude/agents/java-spring-backend-architect.md
Normal 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.
|
||||
@@ -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
11
.gitignore
vendored
@@ -35,4 +35,13 @@ build/
|
||||
.vscode/
|
||||
|
||||
### Mac OS ###
|
||||
.DS_Store
|
||||
.DS_Store
|
||||
|
||||
|
||||
### mybatis plus generator
|
||||
/generator/
|
||||
|
||||
### Uploads ###
|
||||
/uploads/
|
||||
### Logs ###
|
||||
/logs/
|
||||
2
.idea/CoolRequestCommonStatePersistent.xml
generated
2
.idea/CoolRequestCommonStatePersistent.xml
generated
@@ -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
76
.idea/dataSources.xml
generated
@@ -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>
|
||||
2
.idea/db-forest-config.xml
generated
2
.idea/db-forest-config.xml
generated
@@ -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 3:0:UserServiceApplication 5:0:AIOJAuthApplication ---------------------------------------- 2:1:43cc61de-66e1-44cc-b4a2-b24d7e03b490 4:3:903d03c4-df11-4cf8-939a-3e5fba0ab207 6:5:2fd8684a-b9aa-4507-abb0-f7c259d91286 " />
|
||||
<option name="data" value="1:0:AIOJAdminApplication 5:0:UserServiceApplication 9:0:AIOJAuthApplication 13:0:FileServiceApplication 15:0:QuestionServiceApplication ---------------------------------------- 2:1:43cc61de-66e1-44cc-b4a2-b24d7e03b490 3:1:1323cc2e-0b2e-40de-abe6-d1f4c7567b1e 4:1:3a647305-fb45-441b-ba2b-a79ec82a3778 6:5:903d03c4-df11-4cf8-939a-3e5fba0ab207 7:5:c52f5e64-993d-4013-9e2b-838e23d604a2 8:5:38b7e47b-6235-4576-8b43-df28c967dbcc 10:9:2fd8684a-b9aa-4507-abb0-f7c259d91286 11:9:e757fbaf-3605-4bf2-9eb5-852d06273adc 12:9:38fda843-f467-435e-99e4-2a771f7af3f3 14:13:8d957a30-3743-40eb-a916-c6503a783fb9 16:15:5a11088e-1728-4471-a8db-deaeac511136 17:0:cfc60cc1-f725-4d9c-b129-5b722771d69e " />
|
||||
</component>
|
||||
</project>
|
||||
10
.idea/easyCodeTableSettingEncode.xml
generated
Normal file
10
.idea/easyCodeTableSettingEncode.xml
generated
Normal 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
10
.idea/encodings.xml
generated
@@ -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
1
.idea/misc.xml
generated
@@ -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
87
.idea/mybatisx/templates.xml
generated
Normal 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>
|
||||
96
AGENTS.md
Normal file
96
AGENTS.md
Normal file
@@ -0,0 +1,96 @@
|
||||
# Repository Guidelines
|
||||
|
||||
This repository is a Maven multi-module Spring Boot microservices system for an Online Judge (AIOJ). Use the notes below as the contributor guide for structure, workflow, and expectations.
|
||||
|
||||
## Tech Stack (Reference)
|
||||
|
||||
- Java 17
|
||||
- Spring Boot 3.5.7
|
||||
- Spring Cloud 2025.0.0
|
||||
- Spring Cloud Alibaba 2025.0.0.0
|
||||
- Maven build, Spring Java Format, JIB for Docker images
|
||||
|
||||
## Project Structure & Module Organization
|
||||
|
||||
- Root `pom.xml` aggregates all modules.
|
||||
- Shared libraries live in `aioj-backend-common`:
|
||||
- `aioj-backend-common-bom` (dependency management)
|
||||
- `aioj-backend-common-core` (core utilities + Spring extensions)
|
||||
- `aioj-backend-common-security` (JWT/Spring Security)
|
||||
- `aioj-backend-common-feign` (OpenFeign + Sentinel integration)
|
||||
- `aioj-backend-common-log` (AOP logging)
|
||||
- `aioj-backend-common-mybatis` (MyBatis extensions)
|
||||
- `aioj-backend-common-starter` (auto-config starter)
|
||||
- Service modules live at the repo root:
|
||||
- `aioj-backend-gateway`, `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`, `aioj-backend-blog-service`
|
||||
- Supporting folders include `docs`, `db`, `sentinel`, `logs`, `uploads`, and `generator`.
|
||||
|
||||
## Build, Test, and Development Commands
|
||||
|
||||
Use Maven from the repo root:
|
||||
|
||||
```bash
|
||||
mvn clean compile # build all modules (no tests)
|
||||
mvn clean install # build + run tests
|
||||
mvn clean compile -pl aioj-backend-auth # build a single module
|
||||
mvn clean install -DskipTests # build all modules, skip tests
|
||||
mvn spring-boot:run -pl aioj-backend-gateway # run a single service
|
||||
mvn test -pl aioj-backend-user-service # test a single service
|
||||
mvn spring-javaformat:apply # format code
|
||||
mvn spring-javaformat:check # verify formatting
|
||||
```
|
||||
|
||||
Docker image build (JIB):
|
||||
|
||||
```bash
|
||||
mvn package jib:build -pl <module-name> # build & push
|
||||
mvn package jib:buildTar -pl <module-name> # build tar
|
||||
```
|
||||
|
||||
## Coding Style & Naming Conventions
|
||||
|
||||
- Follow existing formatting and run `mvn spring-javaformat:apply` before submitting changes.
|
||||
- Indentation follows Java defaults (4 spaces; no tabs unless already present).
|
||||
- Use standard Java naming: `PascalCase` for classes, `camelCase` for methods/fields, `UPPER_SNAKE_CASE` for constants.
|
||||
- Controllers: `<Resource>Controller`; Services: `<Resource>Service` / `<Resource>ServiceImpl`;
|
||||
Mappers: `<Resource>Mapper`; DTOs: `<Operation><Resource>DTO`.
|
||||
- Module names follow the `aioj-backend-*` pattern; keep new module names consistent.
|
||||
|
||||
## Development Guidelines
|
||||
|
||||
- Service modules should depend on common modules, not on other service modules.
|
||||
- Use Feign clients for inter-service communication.
|
||||
- Keep classes focused and single-purpose; prefer Lombok to reduce boilerplate.
|
||||
- Validate and sanitize user input; use `@PreAuthorize` for method security.
|
||||
- Use custom exceptions + global handlers; return consistent DTO responses.
|
||||
- Externalize configuration; use Nacos for centralized config management.
|
||||
- Never commit sensitive credentials.
|
||||
|
||||
## Architecture Notes
|
||||
|
||||
- Auth uses JWT via `common-security`.
|
||||
- Gateway handles routing, auth checks, Sentinel-based flow control.
|
||||
- Question service uses Chain of Responsibility for validation.
|
||||
- Sentinel + Nacos provide dynamic flow control and rules.
|
||||
- Redis used for cache/session; MyBatis for persistence.
|
||||
- Blog service supports Markdown (Flexmark) + Redis caching.
|
||||
|
||||
## Testing Guidelines
|
||||
|
||||
- Tests live under `src/test/java` in each module.
|
||||
- Name tests clearly after the unit under test (e.g., `UserServiceTest`).
|
||||
- Run `mvn test` for full coverage or `mvn test -pl <module>` for targeted runs.
|
||||
|
||||
## Commit & Pull Request Guidelines
|
||||
|
||||
- Commit messages follow a conventional prefix pattern (e.g., `feat: add rate limiting`, `refactor: simplify DTO mapping`). Keep the subject imperative and concise.
|
||||
- PRs should include: a short summary, affected services/modules, testing performed, and any config or port changes.
|
||||
- If a change affects API behavior or configuration, update `README.md` or relevant docs.
|
||||
|
||||
## Local Service Ports (Reference)
|
||||
|
||||
- Gateway `18085`, Auth `18081`, User `18082`, UPMS `18083`, File `18066`.
|
||||
Update this list if you add or change service ports.
|
||||
301
CLAUDE.md
301
CLAUDE.md
@@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
||||
|
||||
## Codebase Overview
|
||||
|
||||
This is a microservices architecture for an Online Judge (OJ) system, built on **Spring Boot 3.5.7**. The project uses Maven as the build tool and follows a modular monorepo structure, with clearly separated core modules and service modules.
|
||||
This is a microservices architecture for an Online Judge (OJ) system, built on **Spring Boot 3.5.7** with **Spring Cloud 2025.0.0** and **Spring Cloud Alibaba 2025.0.0.0**. The project uses Maven as the build tool and follows a modular monorepo structure, with clearly separated core modules and service modules.
|
||||
|
||||
### Core Modules
|
||||
|
||||
@@ -21,26 +21,35 @@ The `aioj-backend-common` directory contains shared components and utilities use
|
||||
- Common constants and enumerations
|
||||
- Application-level configurations and auto-configuration classes
|
||||
|
||||
3. **aioj-backend-common-feign**
|
||||
3. **aioj-backend-common-security**
|
||||
Security components for JWT authentication and Spring Security:
|
||||
- `JwtAuthenticationFilter`: JWT token validation filter
|
||||
- `JwtUtil`: JWT token generation and parsing utilities
|
||||
- `SecurityConfiguration`: Spring Security configuration with JWT support
|
||||
- `SecurityAutoConfiguration`: Auto-configuration for security features
|
||||
- JWT properties configuration with customizable secret and expiration
|
||||
|
||||
4. **aioj-backend-common-feign**
|
||||
Feign client configurations for seamless inter-service communication:
|
||||
- Auto-configuration for Feign clients with default settings
|
||||
- `@EnableAIOJFeignClients` annotation for enabling Feign clients with predefined base packages
|
||||
- Feign interceptors and error handling mechanisms
|
||||
- Integrated with Sentinel for circuit breaking
|
||||
|
||||
4. **aioj-backend-common-log**
|
||||
5. **aioj-backend-common-log**
|
||||
Aspect-oriented programming (AOP) based logging framework:
|
||||
- `SysLogAspect`: Aspect for logging system operations (controller methods, service calls)
|
||||
- `SysLogEvent` and `SysLogListener`: Event-driven logging mechanism
|
||||
- `SysLogUtils`: Utility class for creating and managing log entries
|
||||
- Configuration properties for logging behavior
|
||||
|
||||
5. **aioj-backend-common-mybatis**
|
||||
6. **aioj-backend-common-mybatis**
|
||||
MyBatis ORM framework extensions:
|
||||
- Auto-fill functionality for `createTime` and `updateTime` fields
|
||||
- Pagination interceptor implementation
|
||||
- MyBatis configuration auto-configuration classes
|
||||
|
||||
6. **aioj-backend-common-starter**
|
||||
7. **aioj-backend-common-starter**
|
||||
Auto-configuration starters for easily enabling common features in service modules.
|
||||
|
||||
|
||||
@@ -57,9 +66,10 @@ The service modules represent the individual microservices that make up the syst
|
||||
|
||||
2. **aioj-backend-gateway**
|
||||
API gateway for request routing and filtering:
|
||||
- Request routing to appropriate service modules
|
||||
- Request routing to appropriate service modules based on path patterns
|
||||
- Authentication token validation before forwarding requests
|
||||
- Rate limiting and request filtering mechanisms
|
||||
- Sentinel integration for rate limiting and circuit breaking
|
||||
- Load balancing and service discovery via Nacos
|
||||
|
||||
3. **aioj-backend-judge-service**
|
||||
Code judge service (under development):
|
||||
@@ -74,27 +84,51 @@ The service modules represent the individual microservices that make up the syst
|
||||
- Integration with the auth service for authentication
|
||||
|
||||
5. **aioj-backend-question-service**
|
||||
Question bank service (under development):
|
||||
- Will manage programming problems, test cases, and problem categories
|
||||
- Support for problem difficulty levels and tags
|
||||
- Integration with the judge service for problem submission
|
||||
Question bank service with advanced validation and flow control:
|
||||
- Programming problem management with CRUD operations
|
||||
- **Chain of Responsibility pattern** for request validation:
|
||||
- `QuestionTitleVerifyChain`, `QuestionContentVerifyChain`, etc. for create operations
|
||||
- `QuestionUpdateExistVerifyChain`, `QuestionUpdateTitleVerifyChain`, etc. for update operations
|
||||
- `QuestionExistVerifyChain`, `QuestionStatusVerifyChain`, `CodeVerifyChain`, `LanguageVerifyChain` for submission validation
|
||||
- **Sentinel flow control** with Nacos datasource for dynamic rule management
|
||||
- Redis integration for caching and session management
|
||||
- Support for problem difficulty levels, tags, and judge configuration
|
||||
- Integration with judge service for code submission
|
||||
|
||||
6. **aioj-backend-ai-service**
|
||||
AI-related functionality service (under development):
|
||||
- Will provide AI-assisted features like problem recommendation, code analysis, etc.
|
||||
|
||||
7. **aioj-backend-upms**
|
||||
User, permission, and menu management service:
|
||||
User, permission, and menu management service (modular structure):
|
||||
- **aioj-backend-upms-api**: API definitions and DTOs for UPMS functionality
|
||||
- **aioj-backend-upms-biz**: Business logic implementation
|
||||
- Low-level user and permission management
|
||||
- Menu and resource access control
|
||||
- Integration with other services for authorization
|
||||
|
||||
8. **aioj-backend-file-service**
|
||||
File storage and management service:
|
||||
- Tencent Cloud COS integration for object storage
|
||||
- File upload, download, and management APIs
|
||||
- Support for various file types
|
||||
- Nacos service discovery integration
|
||||
|
||||
9. **aioj-backend-blog-service**
|
||||
Blog and content sharing service:
|
||||
- User article creation and publishing
|
||||
- **Markdown support** with Flexmark parser
|
||||
- **Redis caching** for improved performance
|
||||
- Spring Session for distributed session management
|
||||
- Technical experience sharing and discussion platform
|
||||
|
||||
## Commonly Used Commands
|
||||
|
||||
### Build
|
||||
- **Build the entire project**: `mvn clean compile`
|
||||
- **Build with tests**: `mvn clean install`
|
||||
- **Build a single module**: `mvn clean compile -pl aioj-backend-auth`
|
||||
- **Skip tests**: `mvn clean install -DskipTests`
|
||||
|
||||
### Code Formatting
|
||||
- **Format code**: `mvn spring-javaformat:apply`
|
||||
@@ -108,16 +142,247 @@ The service modules represent the individual microservices that make up the syst
|
||||
- **Run a service locally**: Use Spring Boot's `Application` class directly in IDE or use `mvn spring-boot:run -pl <module-name>`
|
||||
- **Example**: `mvn spring-boot:run -pl aioj-backend-auth`
|
||||
|
||||
### Docker Image Build (using JIB)
|
||||
- **Build and push Docker image**: `mvn package jib:build -pl <module-name>`
|
||||
- **Build Docker image to tar file**: `mvn package jib:buildTar -pl <module-name>`
|
||||
- **Example**: `mvn package jib:build -pl aioj-backend-gateway`
|
||||
|
||||
## Technology Stack
|
||||
|
||||
### Core Framework
|
||||
- **Spring Boot**: 3.5.7
|
||||
- **Spring Cloud**: 2025.0.0
|
||||
- **Spring Cloud Alibaba**: 2025.0.0.0
|
||||
- **Java Version**: 17
|
||||
|
||||
### Infrastructure Components
|
||||
- **Service Discovery**: Alibaba Nacos
|
||||
- **Flow Control**: Alibaba Sentinel
|
||||
- **API Gateway**: Spring Cloud Gateway
|
||||
- **Load Balancing**: Spring Cloud LoadBalancer
|
||||
|
||||
### Data & Persistence
|
||||
- **ORM Framework**: MyBatis
|
||||
- **Database**: MySQL
|
||||
- **Cache**: Redis (Spring Data Redis)
|
||||
- **Session**: Spring Session with Redis
|
||||
|
||||
### Security & Authentication
|
||||
- **Security Framework**: Spring Security
|
||||
- **Authentication**: JWT (JSON Web Tokens)
|
||||
- **JWT Library**: JJWT (io.jsonwebtoken)
|
||||
|
||||
### Service Communication
|
||||
- **HTTP Client**: OpenFeign
|
||||
- **Circuit Breaker**: Sentinel
|
||||
|
||||
### Storage & Content
|
||||
- **Object Storage**: Tencent Cloud COS
|
||||
- **Markdown Parser**: Flexmark 0.64.8
|
||||
|
||||
### Documentation
|
||||
- **API Documentation**: SpringDoc OpenAPI 3
|
||||
- **API UI**: Knife4j
|
||||
|
||||
### Utilities
|
||||
- **Java Utilities**: Hutool (hutool-core, hutool-extra, hutool-crypto)
|
||||
- **Logging**: SLF4J with Logback (via Spring Boot)
|
||||
- **JSON Processing**: Jackson (via Spring Boot)
|
||||
|
||||
### Build & Deploy
|
||||
- **Build Tool**: Maven
|
||||
- **Code Formatting**: Spring Java Format Maven Plugin 0.0.47
|
||||
- **Docker Build**: JIB Maven Plugin 3.4.5
|
||||
- **Git Info**: Git Commit ID Plugin 9.0.2
|
||||
- **Version Management**: Flatten Maven Plugin 1.6.0
|
||||
|
||||
## Architecture Highlights
|
||||
|
||||
- **Authentication**: JWT-based authentication implemented in `aioj-backend-auth` with `JwtAuthenticationFilter`
|
||||
- **Logging**: Aspect-oriented logging with `SysLogAspect` in `aioj-backend-common-log`
|
||||
- **Database**: MyBatis with auto-fill for create/update times implemented in `common-mybatis`
|
||||
- **Inter-service communication**: Feign clients with auto-configuration from `common-feign`
|
||||
- **Banner**: Custom application banner system in `common-core`
|
||||
- **Authentication & Security**:
|
||||
- JWT-based authentication with `JwtAuthenticationFilter` in `common-security`
|
||||
- Centralized security configuration shared across services
|
||||
- Token generation, parsing, and validation utilities in `JwtUtil`
|
||||
|
||||
- **Flow Control & Rate Limiting**:
|
||||
- **Alibaba Sentinel** integration for flow control and circuit breaking
|
||||
- Dynamic rule management with Nacos datasource
|
||||
- Applied in question-service for submission rate limiting
|
||||
- Gateway-level traffic control and service protection
|
||||
|
||||
- **Request Validation**:
|
||||
- **Chain of Responsibility pattern** in question-service for complex validation logic
|
||||
- Modular and extensible validation chains for different operations
|
||||
- Context-based validation with clear separation of concerns
|
||||
|
||||
- **Logging**:
|
||||
- Aspect-oriented logging with `SysLogAspect` in `common-log`
|
||||
- Event-driven logging mechanism with `SysLogEvent` and `SysLogListener`
|
||||
|
||||
- **Database**:
|
||||
- MyBatis with auto-fill for create/update times in `common-mybatis`
|
||||
- Pagination support with interceptor implementation
|
||||
|
||||
- **Caching & Session**:
|
||||
- Redis integration for distributed caching
|
||||
- Spring Session for distributed session management
|
||||
- Applied in question-service and blog-service
|
||||
|
||||
- **Inter-service communication**:
|
||||
- Feign clients with auto-configuration from `common-feign`
|
||||
- Integrated with Sentinel for circuit breaking
|
||||
- Nacos service discovery for dynamic service routing
|
||||
|
||||
- **File Storage**:
|
||||
- Tencent Cloud COS integration in file-service
|
||||
- Support for object storage and CDN acceleration
|
||||
|
||||
- **Content Processing**:
|
||||
- Markdown parsing with Flexmark in blog-service
|
||||
- Rich text content support for technical articles
|
||||
|
||||
- **Banner**:
|
||||
- Custom application banner system in `common-core`
|
||||
|
||||
## Key Entry Points
|
||||
|
||||
- **Gateway Application**: `aioj-backend-gateway/src/main/java/cn/meowrain/aioj/backend/gateway/AIOJGatewayApplication.java`
|
||||
- **Auth Application**: `aioj-backend-auth/src/main/java/cn/meowrain/aioj/backend/auth/AIOJAuthApplication.java`
|
||||
- **User Service Application**: `aioj-backend-user-service/src/main/java/cn/meowrain/aioj/backend/userservice/UserServiceApplication.java`
|
||||
- **User Service Application**: `aioj-backend-user-service/src/main/java/cn/meowrain/aioj/backend/userservice/UserServiceApplication.java`
|
||||
- **Question Service Application**: `aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/QuestionServiceApplication.java`
|
||||
- **File Service Application**: `aioj-backend-file-service/src/main/java/cn/meowrain/aioj/backend/file/FileServiceApplication.java`
|
||||
- **Blog Service Application**: `aioj-backend-blog-service/src/main/java/cn/meowrain/aioj/backend/blog/BlogServiceApplication.java`
|
||||
- **UPMS Service Application**: `aioj-backend-upms/aioj-backend-upms-biz/src/main/java/cn/meowrain/aioj/backend/upms/UpmsApplication.java`
|
||||
|
||||
## Key Design Patterns
|
||||
|
||||
### Chain of Responsibility Pattern (Question Service)
|
||||
Located in `aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/dto/chains/`:
|
||||
|
||||
- **Create Question Validation**: `QuestionTitleVerifyChain`, `QuestionContentVerifyChain`, `QuestionDifficultyVerifyChain`, `QuestionTagsVerifyChain`, `QuestionJudgeConfigVerifyChain`
|
||||
- **Update Question Validation**: `QuestionUpdateExistVerifyChain`, `QuestionUpdateTitleVerifyChain`, `QuestionUpdateContentVerifyChain`, etc.
|
||||
- **Submit Question Validation**: `QuestionExistVerifyChain`, `QuestionStatusVerifyChain`, `LanguageVerifyChain`, `CodeVerifyChain`
|
||||
- **Context Objects**: `QuestionCreateRequestParamVerifyContext`, `QuestionUpdateRequestParamVerifyContext`, `QuestionSubmitRequestParamVerifyContext`
|
||||
|
||||
### Security Filter Chain (Common Security)
|
||||
Located in `aioj-backend-common/aioj-backend-common-security/`:
|
||||
|
||||
- `JwtAuthenticationFilter`: Pre-authentication filter for JWT token validation
|
||||
- `SecurityConfiguration`: Spring Security filter chain configuration
|
||||
- `SecurityAutoConfiguration`: Auto-configuration for security beans
|
||||
|
||||
## Development Guidelines
|
||||
|
||||
### Module Dependencies
|
||||
- Service modules should depend on common modules, not other service modules
|
||||
- Use Feign clients for inter-service communication
|
||||
- Common modules should be lightweight and not depend on service-specific logic
|
||||
|
||||
### Code Style
|
||||
- Follow Spring Java Format conventions
|
||||
- Run `mvn spring-javaformat:apply` before committing code
|
||||
- Use Lombok annotations to reduce boilerplate code
|
||||
- Keep classes and methods focused and single-purpose
|
||||
|
||||
### Security Best Practices
|
||||
- Use `@PreAuthorize` annotations for method-level security
|
||||
- Never expose sensitive information in API responses
|
||||
- Always validate and sanitize user input
|
||||
- Use the chain of responsibility pattern for complex validation logic
|
||||
|
||||
### API Design
|
||||
- Follow RESTful conventions
|
||||
- Use appropriate HTTP methods (GET, POST, PUT, DELETE)
|
||||
- Return consistent response structures using common DTOs
|
||||
- Document APIs using SpringDoc annotations
|
||||
|
||||
### Error Handling
|
||||
- Use custom exception classes for domain-specific errors
|
||||
- Implement global exception handlers in controllers
|
||||
- Return meaningful error messages to clients
|
||||
- Log errors appropriately with context information
|
||||
|
||||
### Testing
|
||||
- Write unit tests for service layer logic
|
||||
- Use integration tests for controller endpoints
|
||||
- Mock external dependencies in tests
|
||||
- Aim for high code coverage on critical paths
|
||||
|
||||
### Configuration Management
|
||||
- Use `application.yml` for service configuration
|
||||
- Externalize environment-specific settings
|
||||
- Use Nacos for centralized configuration management
|
||||
- Never commit sensitive credentials to version control
|
||||
|
||||
### Flow Control with Sentinel
|
||||
- Configure Sentinel rules in Nacos for dynamic updates
|
||||
- Use `@SentinelResource` annotations for method-level flow control
|
||||
- Define fallback methods for degraded service behavior
|
||||
- Monitor Sentinel dashboard for real-time metrics
|
||||
- Apply flow rules at both gateway and service levels
|
||||
|
||||
## Project Structure Best Practices
|
||||
|
||||
### Package Organization
|
||||
```
|
||||
cn.meowrain.aioj.backend.<service-name>
|
||||
├── controller/ # REST API endpoints
|
||||
├── service/ # Business logic
|
||||
│ └── impl/ # Service implementations
|
||||
├── mapper/ # MyBatis mappers
|
||||
├── entity/ # Database entities
|
||||
├── dto/ # Data transfer objects
|
||||
│ └── chains/ # Validation chains (if applicable)
|
||||
├── config/ # Spring configuration classes
|
||||
├── common/ # Service-specific common utilities
|
||||
│ ├── enums/ # Enumerations
|
||||
│ └── constants/ # Constants
|
||||
└── <ServiceName>Application.java # Main application class
|
||||
```
|
||||
|
||||
### Naming Conventions
|
||||
- **Controllers**: `<Resource>Controller` (e.g., `QuestionController`)
|
||||
- **Services**: `<Resource>Service` (interface) and `<Resource>ServiceImpl` (implementation)
|
||||
- **Mappers**: `<Resource>Mapper` (e.g., `QuestionMapper`)
|
||||
- **Entities**: Noun representing the domain object (e.g., `Question`, `User`)
|
||||
- **DTOs**: `<Operation><Resource>DTO` (e.g., `CreateQuestionDTO`, `UpdateQuestionDTO`)
|
||||
- **Validation Chains**: `<Resource><Property>VerifyChain` (e.g., `QuestionTitleVerifyChain`)
|
||||
|
||||
## Infrastructure Setup
|
||||
|
||||
### Required Services
|
||||
Before running the microservices, ensure the following infrastructure components are running:
|
||||
|
||||
1. **Nacos Server** (Service Discovery & Configuration Center)
|
||||
- Default URL: http://localhost:8848/nacos
|
||||
- Required for service registration and configuration management
|
||||
|
||||
2. **Sentinel Dashboard** (Optional, for flow control monitoring)
|
||||
- Monitor real-time traffic and configure flow rules
|
||||
- Rules are persisted to Nacos
|
||||
|
||||
3. **MySQL Database**
|
||||
- Required for persistent data storage
|
||||
- Each service may have its own database schema
|
||||
|
||||
4. **Redis Server**
|
||||
- Required for caching and session management
|
||||
- Used by question-service and blog-service
|
||||
|
||||
5. **Tencent Cloud COS** (for file-service)
|
||||
- Configure credentials in application configuration
|
||||
- Required only if using file-service
|
||||
|
||||
### Service Startup Order (Recommended)
|
||||
1. Start infrastructure services (MySQL, Redis, Nacos)
|
||||
2. Start auth service (authentication required by other services)
|
||||
3. Start gateway service (API entry point)
|
||||
4. Start business services (user-service, question-service, etc.) in any order
|
||||
|
||||
### Configuration Files
|
||||
Each service has `application.yml` with profiles:
|
||||
- `application.yml`: Common configuration
|
||||
- `application-dev.yml`: Development environment
|
||||
- `application-test.yml`: Test environment
|
||||
- `application-prod.yml`: Production environment
|
||||
|
||||
Activate profiles using: `spring.profiles.active=dev` or `-Dspring.profiles.active=dev`
|
||||
|
||||
367
README.md
Normal file
367
README.md
Normal file
@@ -0,0 +1,367 @@
|
||||
# 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 服务(开发中)
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 前置要求
|
||||
|
||||
- JDK 17 或更高版本
|
||||
- Maven 3.6 或更高版本
|
||||
- MySQL 8.0 或更高版本
|
||||
- Redis 6.0 或更高版本
|
||||
- Nacos Server 2.x(用于服务注册和配置管理)
|
||||
|
||||
### 首次克隆后的准备工作
|
||||
|
||||
1. **克隆项目**
|
||||
```bash
|
||||
git clone <repository-url>
|
||||
cd AI_OJ
|
||||
```
|
||||
|
||||
2. **安装公共依赖模块**
|
||||
|
||||
首次克隆后,需要先将公共模块安装到本地Maven仓库:
|
||||
```bash
|
||||
mvn clean install -DskipTests
|
||||
```
|
||||
|
||||
这个命令会:
|
||||
- 编译所有模块
|
||||
- 将公共库模块(aioj-backend-common-*)安装到本地Maven仓库
|
||||
- 打包所有服务模块为可执行JAR
|
||||
|
||||
3. **配置数据库和中间件**
|
||||
|
||||
根据各服务的 `application.yml` 配置文件,设置:
|
||||
- MySQL 数据库连接信息
|
||||
- Redis 连接信息
|
||||
- Nacos 服务地址
|
||||
|
||||
### 构建项目
|
||||
|
||||
```bash
|
||||
# 编译所有模块(不打包)
|
||||
mvn clean compile
|
||||
|
||||
# 编译并跳过测试
|
||||
mvn clean compile -DskipTests
|
||||
```
|
||||
|
||||
## 项目打包
|
||||
|
||||
### 打包说明
|
||||
|
||||
本项目采用模块化架构,分为**公共库模块**和**服务模块**:
|
||||
|
||||
- **公共库模块**(aioj-backend-common-*):编译为普通JAR,供其他模块依赖
|
||||
- **服务模块**:打包为包含所有依赖的可执行Fat JAR(Spring Boot可执行JAR)
|
||||
|
||||
### 打包所有服务
|
||||
|
||||
```bash
|
||||
# 打包所有服务(推荐)
|
||||
mvn clean package -DskipTests
|
||||
|
||||
# 打包并运行测试
|
||||
mvn clean package
|
||||
```
|
||||
|
||||
打包完成后,每个服务模块的 `target` 目录下会生成两个JAR文件:
|
||||
- `<service-name>-1.0.0.jar` - 可执行的Fat JAR(包含所有依赖,约60-100MB)
|
||||
- `<service-name>-1.0.0.jar.original` - 原始JAR(仅包含本模块代码,约100KB)
|
||||
|
||||
### 打包单个服务
|
||||
|
||||
如果只需要打包某个特定服务:
|
||||
|
||||
```bash
|
||||
# 打包网关服务
|
||||
mvn clean package -pl aioj-backend-gateway -am -DskipTests
|
||||
|
||||
# 打包认证服务
|
||||
mvn clean package -pl aioj-backend-auth -am -DskipTests
|
||||
|
||||
# 打包用户服务
|
||||
mvn clean package -pl aioj-backend-user-service -am -DskipTests
|
||||
|
||||
# 打包题库服务
|
||||
mvn clean package -pl aioj-backend-question-service -am -DskipTests
|
||||
|
||||
# 打包文件服务
|
||||
mvn clean package -pl aioj-backend-file-service -am -DskipTests
|
||||
|
||||
# 打包博客服务
|
||||
mvn clean package -pl aioj-backend-blog-service -am -DskipTests
|
||||
|
||||
# 打包权限管理服务
|
||||
mvn clean package -pl aioj-backend-upms/aioj-backend-upms-biz -am -DskipTests
|
||||
```
|
||||
|
||||
**参数说明:**
|
||||
- `-pl <module>`: 指定要构建的模块
|
||||
- `-am`: 同时构建该模块依赖的其他模块(also-make)
|
||||
- `-DskipTests`: 跳过测试
|
||||
|
||||
### 打包后的文件位置
|
||||
|
||||
打包完成后,可执行JAR文件位于各服务模块的 `target` 目录:
|
||||
|
||||
```
|
||||
aioj-backend-gateway/target/aioj-backend-gateway-1.0.0.jar
|
||||
aioj-backend-auth/target/aioj-backend-auth-1.0.0.jar
|
||||
aioj-backend-user-service/target/aioj-backend-user-service-1.0.0.jar
|
||||
aioj-backend-question-service/target/aioj-backend-question-service-1.0.0.jar
|
||||
aioj-backend-file-service/target/aioj-backend-file-service-1.0.0.jar
|
||||
aioj-backend-blog-service/target/aioj-backend-blog-service-1.0.0.jar
|
||||
aioj-backend-upms/aioj-backend-upms-biz/target/aioj-backend-upms-biz-1.0.0.jar
|
||||
```
|
||||
|
||||
### 运行打包后的服务
|
||||
|
||||
使用 `java -jar` 命令运行打包后的服务:
|
||||
|
||||
```bash
|
||||
# 运行网关服务
|
||||
java -jar aioj-backend-gateway/target/aioj-backend-gateway-1.0.0.jar
|
||||
|
||||
# 运行认证服务
|
||||
java -jar aioj-backend-auth/target/aioj-backend-auth-1.0.0.jar
|
||||
|
||||
# 运行用户服务
|
||||
java -jar aioj-backend-user-service/target/aioj-backend-user-service-1.0.0.jar
|
||||
|
||||
# 指定配置文件运行
|
||||
java -jar aioj-backend-gateway/target/aioj-backend-gateway-1.0.0.jar --spring.profiles.active=prod
|
||||
|
||||
# 指定JVM参数运行
|
||||
java -Xms512m -Xmx1024m -jar aioj-backend-gateway/target/aioj-backend-gateway-1.0.0.jar
|
||||
```
|
||||
|
||||
### 使用Docker部署(可选)
|
||||
|
||||
项目已配置JIB插件,可以直接构建Docker镜像:
|
||||
|
||||
```bash
|
||||
# 构建Docker镜像到本地
|
||||
mvn package jib:dockerBuild -pl aioj-backend-gateway -am -DskipTests
|
||||
|
||||
# 构建并推送到远程仓库
|
||||
mvn package jib:build -pl aioj-backend-gateway -am -DskipTests
|
||||
```
|
||||
|
||||
### 开发模式运行
|
||||
|
||||
在开发过程中,可以使用 `spring-boot:run` 直接运行服务(无需打包):
|
||||
|
||||
```bash
|
||||
# 运行网关
|
||||
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
|
||||
|
||||
# 检查代码格式
|
||||
mvn spring-javaformat:check
|
||||
```
|
||||
|
||||
### 运行测试
|
||||
|
||||
```bash
|
||||
# 运行所有测试
|
||||
mvn test
|
||||
|
||||
# 运行单个模块的测试
|
||||
mvn test -pl aioj-backend-user-service
|
||||
```
|
||||
|
||||
### 清理构建产物
|
||||
|
||||
```bash
|
||||
# 清理所有模块的target目录
|
||||
mvn clean
|
||||
|
||||
# 清理单个模块
|
||||
mvn clean -pl aioj-backend-gateway
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
### 1. 打包时提示找不到依赖
|
||||
|
||||
**问题**:打包服务时提示找不到 `aioj-backend-common-core` 等公共模块。
|
||||
|
||||
**解决方案**:首次克隆或更新公共模块后,需要先安装公共模块到本地仓库:
|
||||
```bash
|
||||
mvn clean install -DskipTests
|
||||
```
|
||||
|
||||
### 2. 打包失败:Unable to find main class
|
||||
|
||||
**问题**:公共库模块(aioj-backend-common-*)打包时报错。
|
||||
|
||||
**解决方案**:这是正常的,公共库模块不应该被打包为可执行JAR。项目已配置为跳过公共模块的repackage。如果遇到此问题,请确保使用最新的配置。
|
||||
|
||||
### 3. 服务启动失败
|
||||
|
||||
**问题**:运行JAR时服务无法启动。
|
||||
|
||||
**可能原因**:
|
||||
- 数据库连接失败:检查MySQL是否启动,连接信息是否正确
|
||||
- Redis连接失败:检查Redis是否启动
|
||||
- Nacos连接失败:检查Nacos Server是否启动
|
||||
- 端口被占用:检查服务端口是否被其他程序占用
|
||||
|
||||
**解决方案**:查看日志输出,根据错误信息排查问题。
|
||||
|
||||
### 4. 内存不足
|
||||
|
||||
**问题**:打包或运行时提示内存不足。
|
||||
|
||||
**解决方案**:
|
||||
```bash
|
||||
# 增加Maven构建内存
|
||||
export MAVEN_OPTS="-Xmx2048m"
|
||||
|
||||
# 或在Windows上
|
||||
set MAVEN_OPTS=-Xmx2048m
|
||||
|
||||
# 运行服务时指定内存
|
||||
java -Xms512m -Xmx1024m -jar <service>.jar
|
||||
```
|
||||
|
||||
## 服务启动顺序
|
||||
|
||||
为确保系统正常运行,建议按以下顺序启动服务:
|
||||
|
||||
1. **基础设施服务**(必须先启动)
|
||||
- MySQL 数据库
|
||||
- Redis 缓存服务
|
||||
- Nacos 服务注册中心
|
||||
|
||||
2. **核心服务**
|
||||
- `aioj-backend-auth` - 认证服务(其他服务可能依赖认证)
|
||||
- `aioj-backend-gateway` - API网关(统一入口)
|
||||
|
||||
3. **业务服务**(可并行启动)
|
||||
- `aioj-backend-user-service` - 用户服务
|
||||
- `aioj-backend-upms-biz` - 权限管理服务
|
||||
- `aioj-backend-question-service` - 题库服务
|
||||
- `aioj-backend-file-service` - 文件服务
|
||||
- `aioj-backend-blog-service` - 博客服务
|
||||
|
||||
## 技术栈
|
||||
|
||||
### 核心框架
|
||||
- **Spring Boot**: 3.5.7
|
||||
- **Spring Cloud**: 2025.0.0
|
||||
- **Spring Cloud Alibaba**: 2025.0.0.0
|
||||
- **Java**: 17
|
||||
|
||||
### 基础设施
|
||||
- **服务注册与发现**: Alibaba Nacos
|
||||
- **流量控制**: Alibaba Sentinel
|
||||
- **API网关**: Spring Cloud Gateway
|
||||
- **负载均衡**: Spring Cloud LoadBalancer
|
||||
|
||||
### 数据存储
|
||||
- **数据库**: MySQL 8.0
|
||||
- **ORM框架**: MyBatis
|
||||
- **缓存**: Redis
|
||||
- **会话管理**: Spring Session (Redis)
|
||||
|
||||
### 安全认证
|
||||
- **安全框架**: Spring Security
|
||||
- **认证方式**: JWT (JSON Web Tokens)
|
||||
|
||||
### 其他组件
|
||||
- **对象存储**: 腾讯云COS
|
||||
- **Markdown解析**: Flexmark
|
||||
- **API文档**: SpringDoc OpenAPI 3 + Knife4j
|
||||
- **工具库**: Hutool
|
||||
|
||||
## 项目文档
|
||||
|
||||
- **[CLAUDE.md](./CLAUDE.md)** - 项目架构和开发指南
|
||||
- **[管理员权限使用指南](./docs/admin-permission-guide.md)** - 管理员权限检查功能说明
|
||||
|
||||
## 开发规范
|
||||
|
||||
### 代码风格
|
||||
- 遵循 Spring Java Format 规范
|
||||
- 提交前运行 `mvn spring-javaformat:apply` 格式化代码
|
||||
|
||||
### 分支管理
|
||||
- `main` - 主分支,保持稳定
|
||||
- `develop` - 开发分支
|
||||
- `feature/*` - 功能分支
|
||||
- `bugfix/*` - 修复分支
|
||||
|
||||
### 提交规范
|
||||
- `feat`: 新功能
|
||||
- `fix`: 修复bug
|
||||
- `docs`: 文档更新
|
||||
- `refactor`: 代码重构
|
||||
- `test`: 测试相关
|
||||
- `chore`: 构建/工具链相关
|
||||
|
||||
## 许可证
|
||||
|
||||
[添加许可证信息]
|
||||
|
||||
## 联系方式
|
||||
|
||||
[添加联系方式]
|
||||
@@ -3,18 +3,131 @@
|
||||
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>
|
||||
<!-- 启用 Spring Boot Maven 插件的 repackage -->
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<skip>false</skip>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
<!-- Protobuf 编译插件 -->
|
||||
<plugin>
|
||||
<groupId>org.xolstice.maven.plugins</groupId>
|
||||
<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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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")));
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
100
aioj-backend-ai-service/src/main/proto/ai_service.proto
Normal file
100
aioj-backend-ai-service/src/main/proto/ai_service.proto
Normal 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; // 关键点
|
||||
}
|
||||
@@ -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
|
||||
@@ -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}
|
||||
@@ -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
|
||||
57
aioj-backend-ai-service/src/main/resources/application.yml
Normal file
57
aioj-backend-ai-service/src/main/resources/application.yml
Normal 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}
|
||||
@@ -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>
|
||||
@@ -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,11 +100,24 @@
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<!-- 测试 -->
|
||||
<!-- ==================== 测试 ==================== -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<!-- 启用 Spring Boot Maven 插件的 repackage -->
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<skip>false</skip>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
@@ -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"))
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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("获取用户信息失败");
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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 客户端ID(aud)
|
||||
* @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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
spring:
|
||||
application:
|
||||
name: auth-service
|
||||
name: aioj-auth-service
|
||||
data:
|
||||
redis:
|
||||
host: 10.0.0.10
|
||||
|
||||
@@ -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}
|
||||
|
||||
110
aioj-backend-auth/src/main/resources/logback-spring.xml
Normal file
110
aioj-backend-auth/src/main/resources/logback-spring.xml
Normal 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>
|
||||
219
aioj-backend-blog-service/README.md
Normal file
219
aioj-backend-blog-service/README.md
Normal 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 Plus(ORM)
|
||||
- FlexMark(Markdown 处理)
|
||||
- Knife4j(API 文档)
|
||||
- Redis(缓存)
|
||||
- MySQL(数据库)
|
||||
|
||||
5. **待实现功能**
|
||||
- [ ] 文章 CRUD 接口开发
|
||||
- [ ] 分类管理接口
|
||||
- [ ] 标签管理接口
|
||||
- [ ] 评论系统接口
|
||||
- [ ] 点赞/收藏接口
|
||||
- [ ] 文章搜索接口
|
||||
- [ ] Markdown 渲染服务
|
||||
- [ ] 文章定时发布
|
||||
- [ ] 文章审核功能
|
||||
|
||||
**注意事项**:
|
||||
- 确保先执行 `db/blog.sql` 初始化数据库
|
||||
- 检查 `application-dev.yml` 中的数据库连接配置
|
||||
- JWT 配置需与 `aioj-backend-auth` 保持一致
|
||||
- 启动前确保 Nacos 服务已运行
|
||||
|
||||
## License
|
||||
|
||||
Copyright © 2025 AIOJ Project
|
||||
112
aioj-backend-blog-service/pom.xml
Normal file
112
aioj-backend-blog-service/pom.xml
Normal file
@@ -0,0 +1,112 @@
|
||||
<?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>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<!-- 启用 Spring Boot Maven 插件的 repackage -->
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<skip>false</skip>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@@ -0,0 +1,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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
* 父分类ID(0表示顶级分类)
|
||||
*/
|
||||
private Long parentId;
|
||||
|
||||
/**
|
||||
* 该分类下的文章数量
|
||||
*/
|
||||
private Integer articleCount;
|
||||
|
||||
/**
|
||||
* 是否启用(1=启用,0=禁用)
|
||||
*/
|
||||
private Integer isEnabled;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private Date createTime;
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
private Date updateTime;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
* 收藏夹ID(0表示默认收藏夹)
|
||||
*/
|
||||
private Long folderId;
|
||||
|
||||
/**
|
||||
* 收藏备注
|
||||
*/
|
||||
private String note;
|
||||
|
||||
/**
|
||||
* 是否已取消(1=已取消,0=有效)
|
||||
*/
|
||||
private Integer isCancelled;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private Date createTime;
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
private Date updateTime;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
* 父评论ID(0表示一级评论)
|
||||
*/
|
||||
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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
* 用户ID(NULL表示游客)
|
||||
*/
|
||||
private Long userId;
|
||||
|
||||
/**
|
||||
* IP地址
|
||||
*/
|
||||
private String ipAddress;
|
||||
|
||||
/**
|
||||
* 用户代理
|
||||
*/
|
||||
private String userAgent;
|
||||
|
||||
/**
|
||||
* 浏览时长(秒)
|
||||
*/
|
||||
private Integer duration;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private Date createTime;
|
||||
}
|
||||
@@ -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> {
|
||||
|
||||
}
|
||||
@@ -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> {
|
||||
|
||||
}
|
||||
@@ -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> {
|
||||
|
||||
}
|
||||
@@ -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> {
|
||||
|
||||
}
|
||||
@@ -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> {
|
||||
|
||||
}
|
||||
@@ -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> {
|
||||
|
||||
}
|
||||
@@ -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> {
|
||||
|
||||
}
|
||||
@@ -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> {
|
||||
|
||||
}
|
||||
@@ -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> {
|
||||
|
||||
}
|
||||
@@ -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> {
|
||||
|
||||
}
|
||||
@@ -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> {
|
||||
|
||||
}
|
||||
@@ -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> {
|
||||
|
||||
}
|
||||
@@ -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> {
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 = "父分类ID(0表示顶级分类)", example = "0")
|
||||
private Long parentId = 0L;
|
||||
|
||||
@Schema(description = "是否启用(1=启用,0=禁用)", example = "1")
|
||||
private Integer isEnabled = 1;
|
||||
}
|
||||
@@ -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 = "父分类ID(0表示顶级分类)", example = "0")
|
||||
private Long parentId;
|
||||
|
||||
@Schema(description = "是否启用(1=启用,0=禁用)", example = "1")
|
||||
private Integer isEnabled;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user