Compare commits
33 Commits
cacf7ed820
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 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>
|
||||
78
README.md
Normal file
78
README.md
Normal file
@@ -0,0 +1,78 @@
|
||||
# AIOJ - Online Judge System
|
||||
|
||||
基于 Spring Boot 微服务架构的在线判题系统。
|
||||
|
||||
## 服务端口配置
|
||||
|
||||
| 服务名称 | 端口 | 说明 |
|
||||
|---------|------|------|
|
||||
| Gateway | 18085 | API 网关服务 |
|
||||
| Auth Service | 18081 | 认证授权服务 |
|
||||
| User Service | 18082 | 用户服务 |
|
||||
| UPMS | 18083 | 用户权限管理服务 |
|
||||
| File Service | 18066 | 文件服务 |
|
||||
|
||||
## 模块结构
|
||||
|
||||
### 核心模块 (aioj-backend-common)
|
||||
|
||||
- **aioj-backend-common-bom** - 依赖管理
|
||||
- **aioj-backend-common-core** - 核心工具类
|
||||
- **aioj-backend-common-feign** - Feign 客户端配置
|
||||
- **aioj-backend-common-log** - 日志框架
|
||||
- **aioj-backend-common-mybatis** - MyBatis 扩展
|
||||
- **aioj-backend-common-starter** - 自动配置启动器
|
||||
|
||||
### 服务模块
|
||||
|
||||
- **aioj-backend-gateway** - API 网关
|
||||
- **aioj-backend-auth** - 认证服务
|
||||
- **aioj-backend-user-service** - 用户服务
|
||||
- **aioj-backend-upms** - 权限管理服务
|
||||
- **aioj-backend-file-service** - 文件服务
|
||||
- **aioj-backend-judge-service** - 判题服务(开发中)
|
||||
- **aioj-backend-question-service** - 题库服务(开发中)
|
||||
- **aioj-backend-ai-service** - AI 服务(开发中)
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 构建项目
|
||||
|
||||
```bash
|
||||
mvn clean compile
|
||||
```
|
||||
|
||||
### 运行服务
|
||||
|
||||
```bash
|
||||
# 运行网关
|
||||
mvn spring-boot:run -pl aioj-backend-gateway
|
||||
|
||||
# 运行认证服务
|
||||
mvn spring-boot:run -pl aioj-backend-auth
|
||||
|
||||
# 运行用户服务
|
||||
mvn spring-boot:run -pl aioj-backend-user-service
|
||||
```
|
||||
|
||||
### 访问地址
|
||||
|
||||
- Gateway: http://localhost:18085
|
||||
- Auth Service: http://localhost:18081/api
|
||||
- User Service: http://localhost:18082/api
|
||||
- UPMS: http://localhost:18083/api
|
||||
- File Service: http://localhost:18066/api
|
||||
|
||||
## 常用命令
|
||||
|
||||
### 代码格式化
|
||||
|
||||
```bash
|
||||
mvn spring-javaformat:apply
|
||||
```
|
||||
|
||||
### 运行测试
|
||||
|
||||
```bash
|
||||
mvn test
|
||||
```
|
||||
@@ -3,18 +3,122 @@
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>cn.meowrain</groupId>
|
||||
<groupId>cn.meowrain.aioj</groupId>
|
||||
<artifactId>ai-oj</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
<version>${revision}</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>aioj-backend-ai-service</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
<description>AIOJ AI服务</description>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.source>17</maven.compiler.source>
|
||||
<maven.compiler.target>17</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
<dependencies>
|
||||
<!-- ==================== API文档 ==================== -->
|
||||
<dependency>
|
||||
<groupId>org.springdoc</groupId>
|
||||
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.github.xiaoymin</groupId>
|
||||
<artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
|
||||
</dependency>
|
||||
|
||||
</project>
|
||||
<!-- ==================== 内部模块 ==================== -->
|
||||
<dependency>
|
||||
<groupId>cn.meowrain.aioj</groupId>
|
||||
<artifactId>aioj-backend-common-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>cn.meowrain.aioj</groupId>
|
||||
<artifactId>aioj-backend-common-log</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- ==================== Web ==================== -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- ==================== gRPC ==================== -->
|
||||
<dependency>
|
||||
<groupId>io.grpc</groupId>
|
||||
<artifactId>grpc-netty-shaded</artifactId>
|
||||
<version>1.68.1</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.grpc</groupId>
|
||||
<artifactId>grpc-protobuf</artifactId>
|
||||
<version>1.68.1</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.grpc</groupId>
|
||||
<artifactId>grpc-stub</artifactId>
|
||||
<version>1.68.1</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.grpc</groupId>
|
||||
<artifactId>grpc-services</artifactId>
|
||||
<version>1.68.1</version>
|
||||
</dependency>
|
||||
<!-- Protobuf -->
|
||||
<dependency>
|
||||
<groupId>com.google.protobuf</groupId>
|
||||
<artifactId>protobuf-java</artifactId>
|
||||
<version>4.29.2</version>
|
||||
</dependency>
|
||||
<!-- 对于 Java 9+ 需要 javax.annotation -->
|
||||
<dependency>
|
||||
<groupId>org.apache.tomcat</groupId>
|
||||
<artifactId>annotations-api</artifactId>
|
||||
<version>6.0.53</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- ==================== Spring Cloud 服务发现 ==================== -->
|
||||
<dependency>
|
||||
<groupId>com.alibaba.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- ==================== 测试 ==================== -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<extensions>
|
||||
<extension>
|
||||
<groupId>kr.motd.maven</groupId>
|
||||
<artifactId>os-maven-plugin</artifactId>
|
||||
<version>1.7.1</version>
|
||||
</extension>
|
||||
</extensions>
|
||||
<plugins>
|
||||
<!-- Protobuf 编译插件 -->
|
||||
<plugin>
|
||||
<groupId>org.xolstice.maven.plugins</groupId>
|
||||
<artifactId>protobuf-maven-plugin</artifactId>
|
||||
<version>0.6.1</version>
|
||||
<configuration>
|
||||
<protocArtifact>com.google.protobuf:protoc:4.29.2:exe:${os.detected.classifier}</protocArtifact>
|
||||
<pluginId>grpc-java</pluginId>
|
||||
<pluginArtifact>io.grpc:protoc-gen-grpc-java:1.68.1:exe:${os.detected.classifier}</pluginArtifact>
|
||||
<protoSourceRoot>${project.basedir}/src/main/proto</protoSourceRoot>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>compile</goal>
|
||||
<goal>compile-custom</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
|
||||
@@ -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,7 +100,7 @@
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<!-- 测试 -->
|
||||
<!-- ==================== 测试 ==================== -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
|
||||
@@ -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
|
||||
99
aioj-backend-blog-service/pom.xml
Normal file
99
aioj-backend-blog-service/pom.xml
Normal file
@@ -0,0 +1,99 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>cn.meowrain.aioj</groupId>
|
||||
<artifactId>ai-oj</artifactId>
|
||||
<version>${revision}</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>aioj-backend-blog-service</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
<description>AIOJ 博客服务 - 用于用户发帖、分享技术经验、写文章</description>
|
||||
|
||||
<dependencies>
|
||||
<!-- ==================== API文档 ==================== -->
|
||||
<dependency>
|
||||
<groupId>org.springdoc</groupId>
|
||||
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.github.xiaoymin</groupId>
|
||||
<artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- ==================== 内部模块 ==================== -->
|
||||
<dependency>
|
||||
<groupId>cn.meowrain.aioj</groupId>
|
||||
<artifactId>aioj-backend-common-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>cn.meowrain.aioj</groupId>
|
||||
<artifactId>aioj-backend-common-log</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>cn.meowrain.aioj</groupId>
|
||||
<artifactId>aioj-backend-common-mybatis</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>cn.meowrain.aioj</groupId>
|
||||
<artifactId>aioj-backend-common-security</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>cn.meowrain.aioj</groupId>
|
||||
<artifactId>aioj-backend-common-feign</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- ==================== Web ==================== -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- ==================== 工具类 ==================== -->
|
||||
<dependency>
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-extra</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- ==================== Markdown 处理 ==================== -->
|
||||
<dependency>
|
||||
<groupId>com.vladsch.flexmark</groupId>
|
||||
<artifactId>flexmark-all</artifactId>
|
||||
<version>0.64.8</version>
|
||||
</dependency>
|
||||
|
||||
<!-- ==================== Redis & 缓存 ==================== -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-redis</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-cache</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- ==================== 数据库 ==================== -->
|
||||
<dependency>
|
||||
<groupId>com.mysql</groupId>
|
||||
<artifactId>mysql-connector-j</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- ==================== Spring Cloud 服务发现 ==================== -->
|
||||
<dependency>
|
||||
<groupId>com.alibaba.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- ==================== 测试 ==================== -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package cn.meowrain.aioj.backend.blogservice.dto.req;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 收藏夹创建请求DTO
|
||||
*
|
||||
* @author AIOJ
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "收藏夹创建请求")
|
||||
public class CollectionFolderCreateRequestDTO {
|
||||
|
||||
@NotBlank(message = "收藏夹名称不能为空")
|
||||
@Schema(description = "收藏夹名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "我的技术收藏")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "收藏夹描述", example = "收集技术相关的优质文章")
|
||||
private String description;
|
||||
|
||||
@Schema(description = "是否公开(1=公开,0=私有)", example = "0")
|
||||
private Integer isPublic = 0;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package cn.meowrain.aioj.backend.blogservice.dto.req;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 收藏请求DTO
|
||||
*
|
||||
* @author AIOJ
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "收藏请求")
|
||||
public class CollectionRequestDTO {
|
||||
|
||||
@NotNull(message = "文章ID不能为空")
|
||||
@Schema(description = "文章ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
|
||||
private Long articleId;
|
||||
|
||||
@Schema(description = "收藏夹ID(0表示默认收藏夹)", example = "0")
|
||||
private Long folderId = 0L;
|
||||
|
||||
@Schema(description = "收藏备注", example = "很好的文章,值得学习")
|
||||
private String note;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user