Compare commits

...

4 Commits

Author SHA1 Message Date
4912e48922 fix: 确保项目可以启动 2025-12-12 23:50:55 +08:00
c61ee69561 fix: 修复日志功能 2025-12-08 22:51:51 +08:00
lirui
6f7963a73b feat:依赖修复,完善core和mybatis还有log模块,log模块待完成 2025-11-25 17:06:50 +08:00
lirui
d89960f51c feat: 添加代码格式化 2025-11-25 13:53:29 +08:00
109 changed files with 3098 additions and 1404 deletions

BIN
.idea/.cache/.easy-yapi/.api.cache.v1.1.db generated Normal file

Binary file not shown.

View File

View File

View File

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

6
.idea/CoolRequestSetting.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CoolRequestSetting">
<option name="projectCachePath" value="project-e82c3cb9-7dfc-4fa6-b498-45789b6b3803" />
</component>
</project>

6
.idea/copilot.data.migration.agent.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AgentMigrationStateService">
<option name="migrationStatus" value="COMPLETED" />
</component>
</project>

5
.idea/encodings.xml generated
View File

@@ -9,6 +9,7 @@
<file url="file://$PROJECT_DIR$/aioj-backend-common/aioj-backend-common-bom/src/main/java" charset="UTF-8" /> <file url="file://$PROJECT_DIR$/aioj-backend-common/aioj-backend-common-bom/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/aioj-backend-common/aioj-backend-common-bom/src/main/resources" charset="UTF-8" /> <file url="file://$PROJECT_DIR$/aioj-backend-common/aioj-backend-common-bom/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/aioj-backend-common/aioj-backend-common-core/src/main/java" charset="UTF-8" /> <file url="file://$PROJECT_DIR$/aioj-backend-common/aioj-backend-common-core/src/main/java" charset="UTF-8" />
<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-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-mybatis/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/java" charset="UTF-8" />
@@ -32,7 +33,7 @@
<file url="file://$PROJECT_DIR$/aioj-backend-user-service/src/main/resources" charset="UTF-8" /> <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/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/src/main/resources" 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/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/../../../../../Windows/System32/src/main/resources" charset="UTF-8" /> <file url="file://$PROJECT_DIR$/../../../../Windows/System32/src/main/resources" charset="UTF-8" />
</component> </component>
</project> </project>

196
CLAUDE.md Normal file
View File

@@ -0,0 +1,196 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
This is a microservices-based Online Judge (OJ) system with AI integration called AIOJ (AI Online Judge). It's built with Spring Boot 3.5.7 and Spring Cloud 2025.0.0, following a modular Maven multi-module architecture.
## Common Development Commands
### Building the Project
```bash
# Build entire project
mvn clean install
# Build with specific environment profile
mvn clean install -P dev # Development (default)
mvn clean install -P test # Testing
mvn clean install -P prod # Production
# Format code according to Spring standards
mvn spring-javaformat:apply
# Build Docker images using Jib
mvn clean package jib:build
# Or use Maven wrapper
./mvnw clean install
```
### Running Services
Each service runs on different ports:
- Gateway: 8085
- Other services: configured via Nacos
Run individual services from their respective directories:
```bash
cd aioj-backend-gateway
mvn spring-boot:run
# Or with specific profile
mvn spring-boot:run -Dspring.profiles.active=dev
```
### Database Setup
1. Create databases using the provided script:
```bash
mysql -u root -p < db/create_db.sql
```
This creates three databases: `aioj_dev`, `aioj_test`, and `aioj_prod`
## Architecture Overview
### Microservices Architecture
The system consists of seven main services:
1. **aioj-backend-gateway** (Port 8085)
- API Gateway using Spring Cloud Gateway
- Routes requests to appropriate services
- Built with WebFlux for reactive programming
2. **aioj-backend-auth**
- OAuth2 authentication and authorization service
- Manages user credentials and tokens
3. **aioj-backend-user-service**
- User management and profiles
- Handles registration, login, profile updates
- Integrates with Redis for session management
4. **aioj-backend-question-service**
- Problem/question management
- Handles problem storage and retrieval
5. **aioj-backend-judge-service**
- Core OJ functionality for code execution
- Supports multiple programming languages
6. **aioj-backend-ai-service**
- AI integration for enhanced features
- Code analysis and automated feedback
7. **aioj-backend-upms** (User Permission Management System)
- Role-based access control
- Permission management
### Common Modules
- **aioj-backend-common**: Shared utilities with sub-modules:
- `core`: Core utilities and configurations
- `log`: Custom logging implementation
- `starter`: Auto-configuration starters
- `mybatis`: Database access layer
- `feign`: HTTP client for service communication
- `bom`: Bill of Materials for dependency management
## Technology Stack
### Core Technologies
- **Java 17**
- **Spring Boot 3.5.7**
- **Spring Cloud 2025.0.0**
- **Spring Cloud Alibaba 2025.0.0.0**
- **Maven** for build management
### Database & Persistence
- **MySQL 9.4.0** as primary database
- **MyBatis-Plus 3.5.14** for ORM
- **Redis** for caching and session management
### Cloud & Infrastructure
- **Nacos** for service discovery and configuration (server: 10.0.0.10:8848)
- **Spring Cloud Gateway** for API routing
- **Docker** with Jib plugin for containerization
- **Sentinel** for circuit breaking
### Security
- **Spring Security 6.5.6** with OAuth2
- JWT token-based authentication
- Role-based access control
### API Documentation
- **Knife4j** (OpenAPI 3) integrated across services
## Configuration Management
### Environment-Specific Configuration
Three environments are supported:
- `dev` (development, default)
- `test` (testing)
- `prod` (production)
Configuration files:
- `bootstrap.yml` - Nacos service discovery configuration
- `application.yml` - Main application configuration
- `application-{env}.yml` - Environment-specific settings
### Nacos Integration
All services use Nacos for:
- Service discovery
- Configuration management
- Centralized properties management
Default Nacos configuration:
```yaml
spring:
cloud:
nacos:
discovery:
server-addr: 10.0.0.10:8848
username: nacos
password: nacos
```
## Database Schema
### Environment Databases
- Development: `aioj_dev`
- Testing: `aioj_test`
- Production: `aioj_prod`
All databases use UTF-8 character set with `utf8mb4_general_ci` collation.
## Development Guidelines
### Code Formatting
- Uses Spring JavaFormat plugin for consistent code style
- IDE plugin available: https://repo1.maven.org/maven2/io/spring/javaformat/spring-javaformat-intellij-idea-plugin
- Run `mvn spring-javaformat:apply` before commits
### Docker Integration
- Jib plugin configured for container builds
- Target registry: `10.0.0.3/aioj/{service-name}:{version}`
- JVM memory configured: -Xms512m -Xmx512m
### Service Communication
- Uses OpenFeign for inter-service communication
- Load balancing with Spring Cloud LoadBalancer
- Circuit breaking with Sentinel
### Logging
- Custom logging implementation in `aioj-backend-common-log`
- Integrates with Spring Security for context logging
- Uses Hutool utilities for enhanced logging
## Testing
- Spring Boot Test framework included
- Spring Security Test for authentication testing
- Test structure is evolving - check individual modules for test coverage
## Important Notes
1. **Service Dependencies**: Services depend on Nacos for discovery - ensure Nacos is running before starting services
2. **Database Setup**: Run the database creation script before first run
3. **Port Configuration**: Gateway runs on 8085, other services are dynamically registered
4. **Environment Profiles**: Default is `dev` - use appropriate profiles for different environments
5. **Configuration Management**: Most configuration is externalized to Nacos - check Nacos for service-specific settings

View File

@@ -19,40 +19,51 @@
</properties> </properties>
<dependencies> <dependencies>
<!-- 核心模块 -->
<dependency>
<groupId>cn.meowrain</groupId>
<artifactId>aioj-backend-common-core</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>cn.meowrain</groupId>
<artifactId>aioj-backend-common-feign</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!-- spring cloud发现服务--> <!-- 工具类 -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-crypto</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<!-- Spring Cloud服务发现 -->
<dependency> <dependency>
<groupId>com.alibaba.cloud</groupId> <groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency> </dependency>
<!-- OAuth2 Client -->
<!-- Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- OAuth2 & Spring Security -->
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId> <artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency> </dependency>
<!-- Spring Security -->
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId> <artifactId>spring-boot-starter-security</artifactId>
</dependency> </dependency>
<dependency> <!-- JWT -->
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--JWT-->
<dependency> <dependency>
<groupId>io.jsonwebtoken</groupId> <groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId> <artifactId>jjwt-api</artifactId>
@@ -70,37 +81,42 @@
<version>0.13.0</version> <version>0.13.0</version>
<scope>runtime</scope> <scope>runtime</scope>
</dependency> </dependency>
<!-- https://mvnrepository.com/artifact/com.github.xiaoymin/knife4j-openapi3-jakarta-spring-boot-starter -->
<dependency> <!-- Feign客户端 -->
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
</dependency>
<!--
引用通用模块
-->
<dependency>
<groupId>cn.meowrain</groupId>
<artifactId>aioj-backend-common-starter</artifactId>
<version>1.0-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
<!--引入openfeign-->
<!-- https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-starter-openfeign -->
<dependency> <dependency>
<groupId>org.springframework.cloud</groupId> <groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId> <artifactId>spring-cloud-starter-openfeign</artifactId>
<version>4.3.0</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.springframework.cloud</groupId> <groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId> <artifactId>spring-cloud-starter-loadbalancer</artifactId>
<version>4.3.0</version>
</dependency> </dependency>
<!-- 引入redis存储refreshToken--> <!-- Redis用于存储refreshToken -->
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId> <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency> </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>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<!-- 测试 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies> </dependencies>
</project> </project>

View File

@@ -7,7 +7,9 @@ import org.springframework.cloud.openfeign.EnableFeignClients;
@EnableFeignClients(basePackages = "cn.meowrain.aioj.backend.auth.clients") @EnableFeignClients(basePackages = "cn.meowrain.aioj.backend.auth.clients")
@SpringBootApplication @SpringBootApplication
public class AIOJAuthApplication { public class AIOJAuthApplication {
public static void main(String[] args) {
SpringApplication.run(AIOJAuthApplication.class, args); public static void main(String[] args) {
} SpringApplication.run(AIOJAuthApplication.class, args);
}
} }

View File

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

View File

@@ -1,5 +1,7 @@
package cn.meowrain.aioj.backend.auth.common.constants; package cn.meowrain.aioj.backend.auth.common.constants;
public class RedisKeyConstants { public class RedisKeyConstants {
public static String REFRESH_TOKEN_KEY_PREFIX = "refresh_token:%s";
public static String REFRESH_TOKEN_KEY_PREFIX = "refresh_token:%s";
} }

View File

@@ -5,16 +5,18 @@ import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor @RequiredArgsConstructor
public enum ChainMarkEnums { public enum ChainMarkEnums {
/**
* 用户登录请求验证
*/
USER_LOGIN_REQ_PARAM_VERIFY("USER_LOGIN_REQ_PARAM_VERIFY");
@Getter /**
private final String markName; * 用户登录请求验证
*/
USER_LOGIN_REQ_PARAM_VERIFY("USER_LOGIN_REQ_PARAM_VERIFY");
@Getter
private final String markName;
@Override
public String toString() {
return markName;
}
@Override
public String toString() {
return markName;
}
} }

View File

@@ -15,33 +15,27 @@ import org.springframework.security.web.SecurityFilterChain;
@EnableWebSecurity @EnableWebSecurity
public class SecurityConfiguration { public class SecurityConfiguration {
@Bean @Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http http.csrf(csrf -> csrf.disable())
.csrf(csrf -> csrf.disable()) .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(auth -> auth
.authorizeHttpRequests(auth -> auth .requestMatchers("/v1/auth/**", "/doc.html", "/swagger-ui/**", "/swagger-resources/**", "/webjars/**",
.requestMatchers( "/v3/api-docs/**", "/favicon.ico")
"/v1/auth/**", .permitAll()
"/doc.html", .anyRequest()
"/swagger-ui/**", .authenticated());
"/swagger-resources/**", return http.build();
"/webjars/**", }
"/v3/api-docs/**",
"/favicon.ico"
)
.permitAll()
.anyRequest().authenticated());
return http.build();
}
@Bean @Bean
public PasswordEncoder passwordEncoder() { public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(); return new BCryptPasswordEncoder();
} }
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {
return configuration.getAuthenticationManager();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {
return configuration.getAuthenticationManager();
}
} }

View File

@@ -16,23 +16,25 @@ import org.springframework.context.annotation.Configuration;
@Configuration @Configuration
@EnableKnife4j @EnableKnife4j
public class SwaggerConfiguration implements ApplicationRunner { public class SwaggerConfiguration implements ApplicationRunner {
@Value("${server.port:8080}")
private String serverPort;
@Value("${server.servlet.context-path:}")
private String contextPath;
@Bean @Value("${server.port:8080}")
public OpenAPI customerOpenAPI() { private String serverPort;
return new OpenAPI()
.info(new Info() @Value("${server.servlet.context-path:}")
.title("AIOJ-renz微服务✨") private String contextPath;
.description("用户认证功能")
.version("v1.0.0") @Bean
.contact(new Contact().name("meowrain").email("meowrain@126.com")) public OpenAPI customerOpenAPI() {
.license(new License().name("MeowRain").url("https://meowrain.cn"))); return new OpenAPI().info(new Info().title("AIOJ-renz微服务✨")
} .description("用户认证功能")
@Override .version("v1.0.0")
public void run(ApplicationArguments args) throws Exception { .contact(new Contact().name("meowrain").email("meowrain@126.com"))
log.info("✨API Document: http://127.0.0.1:{}{}/doc.html", serverPort, contextPath); .license(new License().name("MeowRain").url("https://meowrain.cn")));
} }
@Override
public void run(ApplicationArguments args) throws Exception {
log.info("✨API Document: http://127.0.0.1:{}{}/doc.html", serverPort, contextPath);
}
} }

View File

@@ -8,18 +8,22 @@ import org.springframework.stereotype.Component;
@Data @Data
@ConfigurationProperties(value = JwtPropertiesConfiguration.PREFIX) @ConfigurationProperties(value = JwtPropertiesConfiguration.PREFIX)
public class JwtPropertiesConfiguration { public class JwtPropertiesConfiguration {
public static final String PREFIX = "jwt";
/**
* JWT 密钥(必须 32 字节以上)
*/
private String secret;
/** public static final String PREFIX = "jwt";
* 过期时间(单位:毫秒)
*/ /**
private long accessExpire; // access token TTL * JWT 密钥(必须 32 字节以上)
/** */
* 刷新令牌时间 private String secret;
*/
private long refreshExpire; // refresh token TTL /**
* 过期时间(单位:毫秒)
*/
private long accessExpire; // access token TTL
/**
* 刷新令牌时间
*/
private long refreshExpire; // refresh token TTL
} }

View File

@@ -3,8 +3,9 @@ package cn.meowrain.aioj.backend.auth.controller;
import cn.meowrain.aioj.backend.auth.dto.req.UserLoginRequestDTO; import cn.meowrain.aioj.backend.auth.dto.req.UserLoginRequestDTO;
import cn.meowrain.aioj.backend.auth.dto.resp.UserLoginResponseDTO; import cn.meowrain.aioj.backend.auth.dto.resp.UserLoginResponseDTO;
import cn.meowrain.aioj.backend.auth.service.AuthService; import cn.meowrain.aioj.backend.auth.service.AuthService;
import cn.meowrain.aioj.backend.framework.web.Results;
import cn.meowrain.aioj.backend.framework.web.Result; import cn.meowrain.aioj.backend.framework.core.web.Result;
import cn.meowrain.aioj.backend.framework.core.web.Results;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
@@ -13,25 +14,36 @@ import org.springframework.web.bind.annotation.*;
@RequestMapping("/v1/auth") @RequestMapping("/v1/auth")
public class AuthController { public class AuthController {
private final AuthService authService; private final AuthService authService;
@PostMapping("/login") @PostMapping("/login")
public Result<UserLoginResponseDTO> login(@RequestBody UserLoginRequestDTO userLoginRequest) { public Result<UserLoginResponseDTO> login(@RequestBody UserLoginRequestDTO userLoginRequest) {
UserLoginResponseDTO userLoginResponse = authService.userLogin(userLoginRequest); UserLoginResponseDTO userLoginResponse = authService.userLogin(userLoginRequest);
return Results.success(userLoginResponse); return Results.success(userLoginResponse);
} }
@PostMapping("/refresh") @PostMapping("/refresh")
public Result<UserLoginResponseDTO> refresh(@RequestParam String refreshToken) { public Result<UserLoginResponseDTO> refresh(@RequestParam String refreshToken) {
return Results.success(authService.refreshToken(refreshToken)); return Results.success(authService.refreshToken(refreshToken));
} }
@PostMapping("/auth") @PostMapping("/auth")
public Result<String> auth(@RequestBody UserLoginRequestDTO userLoginRequest) { public Result<String> auth(@RequestBody UserLoginRequestDTO userLoginRequest) {
UserLoginResponseDTO userLoginResponseDTO = authService.userLogin(userLoginRequest); UserLoginResponseDTO userLoginResponseDTO = authService.userLogin(userLoginRequest);
return Results.success(userLoginResponseDTO.getAccessToken()); return Results.success(userLoginResponseDTO.getAccessToken());
} }
@PostMapping("/validate")
public Result<Boolean> validate(@RequestHeader(value = "Authorization", required = false) String authorization) {
// 从Authorization头中提取Bearer token
String token = null;
if (authorization != null && authorization.startsWith("Bearer ")) {
token = authorization.substring(7);
}
Boolean isValid = authService.validateToken(token);
return Results.success(isValid);
}
} }

View File

@@ -2,9 +2,10 @@ package cn.meowrain.aioj.backend.auth.dto.chains;
import cn.meowrain.aioj.backend.auth.common.enums.ChainMarkEnums; import cn.meowrain.aioj.backend.auth.common.enums.ChainMarkEnums;
import cn.meowrain.aioj.backend.auth.dto.req.UserLoginRequestDTO; import cn.meowrain.aioj.backend.auth.dto.req.UserLoginRequestDTO;
import cn.meowrain.aioj.backend.framework.designpattern.chains.AbstractChianHandler;
import cn.meowrain.aioj.backend.framework.errorcode.ErrorCode; import cn.meowrain.aioj.backend.framework.core.designpattern.chains.AbstractChianHandler;
import cn.meowrain.aioj.backend.framework.exception.ClientException; import cn.meowrain.aioj.backend.framework.core.errorcode.ErrorCode;
import cn.meowrain.aioj.backend.framework.core.exception.ClientException;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@@ -12,26 +13,28 @@ import org.springframework.stereotype.Component;
@Component @Component
@Slf4j @Slf4j
public class UserLoginRequestParamVerifyChain implements AbstractChianHandler<UserLoginRequestDTO> { public class UserLoginRequestParamVerifyChain implements AbstractChianHandler<UserLoginRequestDTO> {
@Override
public void handle(UserLoginRequestDTO requestParam) {
if (StringUtils.isAnyBlank(requestParam.getUserAccount(), requestParam.getUserPassword())) {
throw new ClientException("参数为空", ErrorCode.PARAMS_ERROR);
}
if (requestParam.getUserAccount().length() < 4) {
throw new ClientException("账号长度不小于4位", ErrorCode.PARAMS_ERROR);
}
if (requestParam.getUserPassword().length() < 8) {
throw new ClientException("密码长度不小于8位", ErrorCode.PARAMS_ERROR);
}
}
@Override @Override
public String mark() { public void handle(UserLoginRequestDTO requestParam) {
return ChainMarkEnums.USER_LOGIN_REQ_PARAM_VERIFY.getMarkName(); if (StringUtils.isAnyBlank(requestParam.getUserAccount(), requestParam.getUserPassword())) {
} throw new ClientException("参数为空", ErrorCode.PARAMS_ERROR);
}
if (requestParam.getUserAccount().length() < 4) {
throw new ClientException("账号长度不小于4位", ErrorCode.PARAMS_ERROR);
}
if (requestParam.getUserPassword().length() < 8) {
throw new ClientException("密码长度不小于8位", ErrorCode.PARAMS_ERROR);
}
}
@Override
public String mark() {
return ChainMarkEnums.USER_LOGIN_REQ_PARAM_VERIFY.getMarkName();
}
@Override
public int getOrder() {
return 10;
}
@Override
public int getOrder() {
return 10;
}
} }

View File

@@ -6,4 +6,5 @@ import org.springframework.stereotype.Component;
@Component @Component
public class UserLoginRequestParamVerifyContext extends CommonChainContext<UserLoginRequestDTO> { public class UserLoginRequestParamVerifyContext extends CommonChainContext<UserLoginRequestDTO> {
} }

View File

@@ -4,6 +4,9 @@ import lombok.Data;
@Data @Data
public class UserLoginRequestDTO { public class UserLoginRequestDTO {
private String userAccount;
private String userPassword; private String userAccount;
private String userPassword;
} }

View File

@@ -10,59 +10,59 @@ import java.util.Date;
@Data @Data
public class UserAuthRespDTO { public class UserAuthRespDTO {
/** /**
* id * id
*/ */
private Long id; private Long id;
/** /**
* 用户账号 * 用户账号
*/ */
private String userAccount; private String userAccount;
/**
* 用户密码
*/
private String userPassword;
/** /**
* 开放平台id * 用户密码
*/ */
private String unionId; private String userPassword;
/** /**
* 公众号openId * 开放平台id
*/ */
private String mpOpenId; private String unionId;
/** /**
* 用户昵称 * 公众号openId
*/ */
private String userName; private String mpOpenId;
/** /**
* 用户头像 * 用户昵称
*/ */
private String userAvatar; private String userName;
/** /**
* 用户简介 * 用户头像
*/ */
private String userProfile; private String userAvatar;
/** /**
* 用户角色user/admin/ban * 用户简介
*/ */
private String userRole; private String userProfile;
/** /**
* 创建时间 * 用户角色user/admin/ban
*/ */
private Date createTime; private String userRole;
/** /**
* 更新时间 * 创建时间
*/ */
private Date updateTime; private Date createTime;
/**
* 更新时间
*/
private Date updateTime;
} }

View File

@@ -7,24 +7,28 @@ import java.util.Date;
@Data @Data
public class UserLoginResponseDTO implements Serializable { public class UserLoginResponseDTO implements Serializable {
/**
* id
*/
private Long id;
/** /**
* 用户账号 * id
*/ */
private String userAccount; private Long id;
/** /**
* 开放平台id * 用户账号
*/ */
private String unionId; private String userAccount;
private String accessToken; /**
private String refreshToken; * 开放平台id
private Long expire; */
private String unionId;
private String accessToken;
private String refreshToken;
private Long expire;
private static final long serialVersionUID = 1L;
private static final long serialVersionUID = 1L;
} }

View File

@@ -7,4 +7,5 @@ import org.springframework.stereotype.Component;
*/ */
@Component @Component
public class JwtAuthenticationFilter { public class JwtAuthenticationFilter {
} }

View File

@@ -4,17 +4,26 @@ import cn.meowrain.aioj.backend.auth.dto.req.UserLoginRequestDTO;
import cn.meowrain.aioj.backend.auth.dto.resp.UserLoginResponseDTO; import cn.meowrain.aioj.backend.auth.dto.resp.UserLoginResponseDTO;
public interface AuthService { public interface AuthService {
/**
* 用户登录
* @param request {@link UserLoginRequestDTO}
* @return {@link UserLoginResponseDTO}
*/
UserLoginResponseDTO userLogin(UserLoginRequestDTO request);
/** /**
* 刷新token * 用户登录
* @param refreshToken * @param request {@link UserLoginRequestDTO}
* @return * @return {@link UserLoginResponseDTO}
*/ */
UserLoginResponseDTO refreshToken(String refreshToken); UserLoginResponseDTO userLogin(UserLoginRequestDTO request);
/**
* 刷新token
* @param refreshToken
* @return
*/
UserLoginResponseDTO refreshToken(String refreshToken);
/**
* 验证token的有效性
* @param accessToken 访问令牌
* @return token是否有效
*/
Boolean validateToken(String accessToken);
} }

View File

@@ -26,81 +26,142 @@ import java.util.concurrent.TimeUnit;
@RequiredArgsConstructor @RequiredArgsConstructor
@Slf4j @Slf4j
public class AuthServiceImpl implements AuthService { public class AuthServiceImpl implements AuthService {
private final JwtUtil jwtUtil;
private final UserLoginRequestParamVerifyContext userLoginRequestParamVerifyContext;
private final UserClient userClient;
private final StringRedisTemplate stringRedisTemplate;
private final JwtPropertiesConfiguration jwtPropertiesConfiguration;
@Override private final JwtUtil jwtUtil;
public UserLoginResponseDTO userLogin(UserLoginRequestDTO requestParam) {
// 1.校验
userLoginRequestParamVerifyContext.handler(ChainMarkEnums.USER_LOGIN_REQ_PARAM_VERIFY.getMarkName(),
requestParam);
// 如果调用user-service失败那么就说明是系统内部错误
Result<UserAuthRespDTO> userResp = userClient.getUserByUserName(requestParam.getUserAccount());
if (userResp.isFail()) {
log.error("调用user-service返回失败{}", userResp.getMessage());
throw new ServiceException(ErrorCode.SYSTEM_ERROR);
}
UserAuthRespDTO user = userResp.getData();
if (ObjectUtil.isNull(user) private final UserLoginRequestParamVerifyContext userLoginRequestParamVerifyContext;
|| !BCrypt.checkpw(requestParam.getUserPassword(), user.getUserPassword())) {
throw new ServiceException("用户不存在或者密码错误", ErrorCode.NOT_LOGIN_ERROR);
}
// 生成 JWT
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);
// refresh token存入到REDIS里面 private final UserClient userClient;
stringRedisTemplate.opsForValue().set(
String.format(RedisKeyConstants.REFRESH_TOKEN_KEY_PREFIX, user.getId()),
refreshToken,
jwtPropertiesConfiguration.getRefreshExpire(),
TimeUnit.MILLISECONDS);
return resp;
}
/** private final StringRedisTemplate stringRedisTemplate;
* 更新access token使用refresh token
* @param refreshToken
* @return
*/
@Override
public UserLoginResponseDTO refreshToken(String refreshToken) {
UserLoginResponseDTO userLoginResponseDTO = new UserLoginResponseDTO();
if (!jwtUtil.isTokenValid(refreshToken)) {
throw new RuntimeException("Refresh Token 已过期");
}
Long userId = Long.valueOf(jwtUtil.parseClaims(refreshToken).getSubject()); private final JwtPropertiesConfiguration jwtPropertiesConfiguration;
String cacheKey = String.format(RedisKeyConstants.REFRESH_TOKEN_KEY_PREFIX, userId); @Override
String cacheValue = stringRedisTemplate.opsForValue().get(cacheKey); public UserLoginResponseDTO userLogin(UserLoginRequestDTO requestParam) {
log.info("用户登录请求: userAccount={}", requestParam.getUserAccount());
if (cacheValue == null || !cacheValue.equals(refreshToken)) { // 1.校验
throw new RuntimeException("Refresh Token 已失效"); userLoginRequestParamVerifyContext.handler(ChainMarkEnums.USER_LOGIN_REQ_PARAM_VERIFY.getMarkName(),
} requestParam);
// 再次签发新的 Access Token // 如果调用user-service失败那么就说明是系统内部错误
// 此处你需要查用户,拿 userName, role log.info("正在调用user-service查询用户信息...");
Result<UserAuthRespDTO> userResult = userClient.getUserById(String.valueOf(userId)); Result<UserAuthRespDTO> userResp = userClient.getUserByUserName(requestParam.getUserAccount());
if (userResult.isFail()) {
log.error("通过id查找用户失败:{}", userResult.getMessage()); if (userResp.isFail()) {
throw new ServiceException(ErrorCode.SYSTEM_ERROR); log.error("调用user-service返回失败{}", userResp.getMessage());
} throw new ServiceException(ErrorCode.SYSTEM_ERROR);
UserAuthRespDTO user = userResult.getData(); }
String newAccessToken = jwtUtil.generateAccessToken(user);
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);
}
// 生成 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);
// 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;
}
/**
* 更新access token使用refresh token
* @param refreshToken
* @return
*/
@Override
public UserLoginResponseDTO refreshToken(String refreshToken) {
UserLoginResponseDTO userLoginResponseDTO = new UserLoginResponseDTO();
if (!jwtUtil.isTokenValid(refreshToken)) {
throw new RuntimeException("Refresh Token 已过期");
}
Long userId = Long.valueOf(jwtUtil.parseClaims(refreshToken).getSubject());
String cacheKey = String.format(RedisKeyConstants.REFRESH_TOKEN_KEY_PREFIX, userId);
String cacheValue = stringRedisTemplate.opsForValue().get(cacheKey);
if (cacheValue == null || !cacheValue.equals(refreshToken)) {
throw new RuntimeException("Refresh Token 已失效");
}
// 再次签发新的 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);
// 设置refresh token和access token
userLoginResponseDTO.setRefreshToken(refreshToken);
userLoginResponseDTO.setAccessToken(newAccessToken);
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;
}
// 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;
}
// 4. 验证用户是否存在(可选,增加安全性)
Result<UserAuthRespDTO> userResult = userClient.getUserById(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;
}
}
//设置refresh token和access token
userLoginResponseDTO.setRefreshToken(refreshToken);
userLoginResponseDTO.setAccessToken(newAccessToken);
return userLoginResponseDTO;
}
} }

View File

@@ -17,58 +17,56 @@ import java.util.Map;
@Component @Component
public class JwtUtil { public class JwtUtil {
private final JwtPropertiesConfiguration jwtConfig; private final JwtPropertiesConfiguration jwtConfig;
private SecretKey getSigningKey() { private SecretKey getSigningKey() {
return Keys.hmacShaKeyFor(jwtConfig.getSecret().getBytes()); return Keys.hmacShaKeyFor(jwtConfig.getSecret().getBytes());
} }
/** 生成 Access Token */ /** 生成 Access Token */
public String generateAccessToken(UserAuthRespDTO user) { public String generateAccessToken(UserAuthRespDTO user) {
long now = System.currentTimeMillis(); long now = System.currentTimeMillis();
Map<String, Object> claims = new HashMap<>(); Map<String, Object> claims = new HashMap<>();
claims.put("userId", user.getId()); claims.put("userId", user.getId());
claims.put("userName", user.getUserName()); claims.put("userName", user.getUserName());
claims.put("role", user.getUserRole()); claims.put("role", user.getUserRole());
return Jwts.builder() return Jwts.builder()
.subject(user.getUserAccount()) .subject(user.getUserAccount())
.issuedAt(new Date(now)) .issuedAt(new Date(now))
.expiration(new Date(now + jwtConfig.getAccessExpire())) .expiration(new Date(now + jwtConfig.getAccessExpire()))
.claims(claims) .claims(claims)
.signWith(getSigningKey(), Jwts.SIG.HS256) .signWith(getSigningKey(), Jwts.SIG.HS256)
.compact(); .compact();
} }
/** 生成 Refresh Token只含 userId */ /** 生成 Refresh Token只含 userId */
public String generateRefreshToken(Long userId) { public String generateRefreshToken(Long userId) {
long now = System.currentTimeMillis(); long now = System.currentTimeMillis();
return Jwts.builder() return Jwts.builder()
.subject(String.valueOf(userId)) .subject(String.valueOf(userId))
.issuedAt(new Date(now)) .issuedAt(new Date(now))
.expiration(new Date(now + jwtConfig.getRefreshExpire())) .expiration(new Date(now + jwtConfig.getRefreshExpire()))
.signWith(getSigningKey(), Jwts.SIG.HS256) .signWith(getSigningKey(), Jwts.SIG.HS256)
.compact(); .compact();
} }
/** 解析 Token */ /** 解析 Token */
public Claims parseClaims(String token) { public Claims parseClaims(String token) {
return Jwts.parser() return Jwts.parser().verifyWith(getSigningKey()).build().parseSignedClaims(token).getPayload();
.verifyWith(getSigningKey()) }
.build()
.parseSignedClaims(token) /** 校验 Token 是否过期 */
.getPayload(); public boolean isTokenValid(String token) {
} try {
Claims claims = parseClaims(token);
return claims.getExpiration().after(new Date());
}
catch (Exception ignored) {
return false;
}
}
/** 校验 Token 是否过期 */
public boolean isTokenValid(String token) {
try {
Claims claims = parseClaims(token);
return claims.getExpiration().after(new Date());
} catch (Exception ignored) {
return false;
}
}
} }

View File

@@ -18,10 +18,19 @@
<mybatis-plus.version>3.5.14</mybatis-plus.version> <mybatis-plus.version>3.5.14</mybatis-plus.version>
<spring-boot.version>3.5.7</spring-boot.version> <spring-boot.version>3.5.7</spring-boot.version>
<spring-cloud-alibaba.version>2025.0.0.0</spring-cloud-alibaba.version> <spring-cloud-alibaba.version>2025.0.0.0</spring-cloud-alibaba.version>
<mysql.version>9.5.0</mysql.version> <mysql.version>9.4.0</mysql.version>
<jackson.bom>3.0.2</jackson.bom>
</properties> </properties>
<dependencyManagement> <dependencyManagement>
<dependencies> <dependencies>
<!-- https://mvnrepository.com/artifact/tools.jackson/jackson-bom -->
<dependency>
<groupId>tools.jackson</groupId>
<artifactId>jackson-bom</artifactId>
<version>${jackson.bom}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/cn.hutool/hutool-bom --> <!-- https://mvnrepository.com/artifact/cn.hutool/hutool-bom -->
<dependency> <dependency>
<groupId>cn.hutool</groupId> <groupId>cn.hutool</groupId>
@@ -72,7 +81,7 @@
<dependency> <dependency>
<groupId>org.springframework.security</groupId> <groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId> <artifactId>spring-security-test</artifactId>
<version>6.5.7</version> <version>6.5.6</version>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
@@ -82,6 +91,7 @@
<artifactId>spring-boot-starter-security</artifactId> <artifactId>spring-boot-starter-security</artifactId>
<version>3.5.7</version> <version>3.5.7</version>
</dependency> </dependency>
</dependencies> </dependencies>
</dependencyManagement> </dependencyManagement>
</project> </project>

View File

@@ -26,5 +26,42 @@
<groupId>cn.hutool</groupId> <groupId>cn.hutool</groupId>
<artifactId>hutool-core</artifactId> <artifactId>hutool-core</artifactId>
</dependency> </dependency>
<!--server-api-->
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-commons</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-extra</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-http</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<optional>true</optional>
</dependency>
<!--json模块-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-json</artifactId>
</dependency>
<!--hibernate-validator-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
</dependencies> </dependencies>
</project> </project>

View File

@@ -0,0 +1,56 @@
package cn.meowrain.aioj.backend.framework.core.banner;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.core.env.Environment;
import java.lang.management.ManagementFactory;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
@Slf4j
@RequiredArgsConstructor
public class BannerApplicationRunner implements ApplicationRunner {
private final Environment env;
@Value("${spring.application.name:unknown}")
private String appName;
@Override
public void run(ApplicationArguments args) throws Exception {
// Active profiles
String profiles = String.join(",", env.getActiveProfiles());
if (profiles.isEmpty()) {
profiles = "default";
}
// Port
String port = env.getProperty("server.port", "unknown");
// JVM info
String jvm = System.getProperty("java.version") + " (" + System.getProperty("java.vendor") + ")";
// PID
String pid = ManagementFactory.getRuntimeMXBean().getPid() + "";
// Time
String time = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
// Git commit id (如果没有 git.properties不会报错)
String gitCommit = env.getProperty("git.commit.id.abbrev", "N/A");
printBanner(appName, profiles, port, jvm, pid, time, gitCommit);
}
private void printBanner(String appName, String profiles, String port, String jvm, String pid, String time,
String gitCommit) {
String banner = "\n" + "------------------------------------------------------------\n"
+ " ✨AI Online Judge✨ - " + appName + "\n" + " Environment : " + profiles + "\n" + " Port : "
+ port + "\n" + " Git Commit : " + gitCommit + "\n" + " JVM : " + jvm + "\n"
+ " PID : " + pid + "\n" + " Started At : " + time + "\n"
+ "------------------------------------------------------------\n";
System.out.println(banner);
}
}

View File

@@ -12,59 +12,50 @@ import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
@Slf4j @Slf4j
@Component @Deprecated
public class EnvironmentBanner implements ApplicationListener<ApplicationReadyEvent> { public class EnvironmentBanner implements ApplicationListener<ApplicationReadyEvent> {
@Value("${spring.application.name:unknown}") @Value("${spring.application.name:unknown}")
private String appName; private String appName;
@Override @Override
public void onApplicationEvent(ApplicationReadyEvent event) { public void onApplicationEvent(ApplicationReadyEvent event) {
Environment env = event.getApplicationContext().getEnvironment(); Environment env = event.getApplicationContext().getEnvironment();
// Active profiles // Active profiles
String profiles = String.join(",", env.getActiveProfiles()); String profiles = String.join(",", env.getActiveProfiles());
if (profiles.isEmpty()) { if (profiles.isEmpty()) {
profiles = "default"; profiles = "default";
} }
// Port // Port
String port = env.getProperty("server.port", "unknown"); String port = env.getProperty("server.port", "unknown");
// JVM info // JVM info
String jvm = System.getProperty("java.version") + " (" + System.getProperty("java.vendor") + ")"; String jvm = System.getProperty("java.version") + " (" + System.getProperty("java.vendor") + ")";
// PID // PID
String pid = ManagementFactory.getRuntimeMXBean().getPid() + ""; String pid = ManagementFactory.getRuntimeMXBean().getPid() + "";
// Time // Time
String time = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); String time = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
// Git commit id (如果没有 git.properties不会报错) // Git commit id (如果没有 git.properties不会报错)
String gitCommit = env.getProperty("git.commit.id.abbrev", "N/A"); String gitCommit = env.getProperty("git.commit.id.abbrev", "N/A");
printBanner(appName, profiles, port, jvm, pid, time, gitCommit); printBanner(appName, profiles, port, jvm, pid, time, gitCommit);
} }
private void printBanner(String appName, private void printBanner(String appName, String profiles, String port, String jvm, String pid, String time,
String profiles, String gitCommit) {
String port,
String jvm, String banner = "\n" + "------------------------------------------------------------\n"
String pid, + " ✨AI Online Judge✨ - " + appName + "\n" + " Environment : " + profiles + "\n" + " Port : "
String time, + port + "\n" + " Git Commit : " + gitCommit + "\n" + " JVM : " + jvm + "\n"
String gitCommit) { + " PID : " + pid + "\n" + " Started At : " + time + "\n"
+ "------------------------------------------------------------\n";
System.out.println(banner);
}
String banner = "\n" +
"------------------------------------------------------------\n" +
" ✨AI Online Judge✨ - " + appName + "\n" +
" Environment : " + profiles + "\n" +
" Port : " + port + "\n" +
" Git Commit : " + gitCommit + "\n" +
" JVM : " + jvm + "\n" +
" PID : " + pid + "\n" +
" Started At : " + time + "\n" +
"------------------------------------------------------------\n";
System.out.println(banner);
}
} }

View File

@@ -0,0 +1,14 @@
package cn.meowrain.aioj.backend.framework.core.banner.config;
import cn.meowrain.aioj.backend.framework.core.banner.BannerApplicationRunner;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.core.env.Environment;
@AutoConfiguration
public class AIOJBannerAutoConfiguration {
@Bean
public BannerApplicationRunner bannerApplicationRunner(Environment env) {
return new BannerApplicationRunner(env);
}
}

View File

@@ -1,15 +1,18 @@
package cn.meowrain.aioj.backend.framework.core.config; package cn.meowrain.aioj.backend.framework.core.config;
import cn.meowrain.aioj.backend.framework.core.exception.handler.GlobalExceptionHandler; import cn.meowrain.aioj.backend.framework.core.exception.handler.GlobalExceptionHandler;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
/** /**
* 注册为bean,全局异常拦截器 * 注册为bean,全局异常拦截器
*/ */
@AutoConfiguration
public class WebAutoConfiguration { public class WebAutoConfiguration {
@Bean
public GlobalExceptionHandler globalExceptionHandler() { @Bean
return new GlobalExceptionHandler(); public GlobalExceptionHandler globalExceptionHandler() {
} return new GlobalExceptionHandler();
}
} }

View File

@@ -0,0 +1,21 @@
package cn.meowrain.aioj.backend.framework.core.constants;
/**
* 全局服务名称
*/
public class ServiceNameConstants {
/**
* 用户服务 SERVICE NAME
*/
public static final String USER_SERVICE = "user-service";
/**
* 认证服务 SERVICE NAME
*/
public static final String AUTH_SERVICE = "auth-service";
/**
* UPMS模块
*/
public static final String UPMS_SERVICE = "upms-service";
}

View File

@@ -3,17 +3,17 @@ package cn.meowrain.aioj.backend.framework.core.designpattern.chains;
import org.springframework.core.Ordered; import org.springframework.core.Ordered;
public interface AbstractChianHandler<T> extends Ordered { public interface AbstractChianHandler<T> extends Ordered {
/**
* 执行责任链逻辑
*
* @param requestParam 责任链执行入参
*/
void handle(T requestParam);
/** /**
* 责任链组件标识 * 执行责任链逻辑
* * @param requestParam 责任链执行入参
* @return String */
*/ void handle(T requestParam);
String mark();
/**
* 责任链组件标识
* @return String
*/
String mark();
} }

View File

@@ -19,53 +19,50 @@ import java.util.stream.Collectors;
@Component @Component
@Slf4j @Slf4j
public class CommonChainContext<T> implements ApplicationContextAware, CommandLineRunner { public class CommonChainContext<T> implements ApplicationContextAware, CommandLineRunner {
private ApplicationContext applicationContext;
@Override private ApplicationContext applicationContext;
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
private final Map<String, List<AbstractChianHandler<T>>> abstractChainHandlerMap = new HashMap<>(); @Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
public void handler(String mark, T requestParam) { private final Map<String, List<AbstractChianHandler<T>>> abstractChainHandlerMap = new HashMap<>();
List<AbstractChianHandler<T>> merchantAdminAbstractChainHandlers = abstractChainHandlerMap.get(mark);
if (merchantAdminAbstractChainHandlers == null || merchantAdminAbstractChainHandlers.isEmpty()) {
throw new RuntimeException(String.format("[%s] Chain of Responsibility ID is undefined.", mark));
}
merchantAdminAbstractChainHandlers.forEach(h -> {
h.handle(requestParam);
});
}
@Override public void handler(String mark, T requestParam) {
public void run(String... args) throws Exception { List<AbstractChianHandler<T>> merchantAdminAbstractChainHandlers = abstractChainHandlerMap.get(mark);
log.info("【责任链路初始化】开始加载并分组所有处理器..."); if (merchantAdminAbstractChainHandlers == null || merchantAdminAbstractChainHandlers.isEmpty()) {
applicationContext.getBeansOfType(AbstractChianHandler.class) throw new RuntimeException(String.format("[%s] Chain of Responsibility ID is undefined.", mark));
.values() }
.forEach(handler -> { merchantAdminAbstractChainHandlers.forEach(h -> {
// 打印当前处理器的类名和它所属的 ChainMark h.handle(requestParam);
String handlerName = handler.getClass().getSimpleName(); });
String mark = handler.mark(); }
log.info(" -> 发现处理器:{},归属链路:{}", handlerName, mark);
abstractChainHandlerMap
.computeIfAbsent(handler.mark(), k -> new ArrayList<>())
.add(handler);
});
// 步骤 2: 对每个链路中的处理器进行排序 (Sort 阶段) @Override
abstractChainHandlerMap.forEach((mark, handlers) -> { public void run(String... args) throws Exception {
handlers.sort(Comparator.comparing(Ordered::getOrder)); log.info("【责任链路初始化】开始加载并分组所有处理器...");
applicationContext.getBeansOfType(AbstractChianHandler.class).values().forEach(handler -> {
// 打印当前处理器的类名和它所属的 ChainMark
String handlerName = handler.getClass().getSimpleName();
String mark = handler.mark();
log.info(" -> 发现处理器:{},归属链路:{}", handlerName, mark);
abstractChainHandlerMap.computeIfAbsent(handler.mark(), k -> new ArrayList<>()).add(handler);
});
// 打印排序后的 Bean 列表 // 步骤 2: 对每个链路中的处理器进行排序 (Sort 阶段)
String sortedList = handlers.stream() abstractChainHandlerMap.forEach((mark, handlers) -> {
.map(h -> String.format("%s (Order:%d)", h.getClass().getSimpleName(), h.getOrder())) handlers.sort(Comparator.comparing(Ordered::getOrder));
.collect(Collectors.joining(" -> "));
log.info(" ✅ 链路 {} 排序完成:{}", mark, sortedList); // 打印排序后的 Bean 列表
}); String sortedList = handlers.stream()
.map(h -> String.format("%s (Order:%d)", h.getClass().getSimpleName(), h.getOrder()))
.collect(Collectors.joining(" -> "));
log.info("【责任链路初始化】所有处理器链已完全就绪。"); log.info(" ✅ 链路 {} 排序完成:{}", mark, sortedList);
} });
log.info("【责任链路初始化】所有处理器链已完全就绪。");
}
} }

View File

@@ -4,14 +4,17 @@ package cn.meowrain.aioj.backend.framework.core.enums;
* 删除枚举 * 删除枚举
*/ */
public enum DelStatusEnum { public enum DelStatusEnum {
STATUS_NORMAL("0"),
STATUS_DELETE("1");
private final String code; STATUS_NORMAL("0"), STATUS_DELETE("1");
DelStatusEnum(String code) {
this.code = code; private final String code;
}
public String code() { DelStatusEnum(String code) {
return this.code; this.code = code;
} }
public String code() {
return this.code;
}
} }

View File

@@ -1,38 +1,35 @@
package cn.meowrain.aioj.backend.framework.core.errorcode; package cn.meowrain.aioj.backend.framework.core.errorcode;
public enum ErrorCode implements IErrorCode { public enum ErrorCode implements IErrorCode {
SUCCESS("0", "ok"),
PARAMS_ERROR("40000", "请求参数错误"),
NOT_LOGIN_ERROR("40100", "未登录"),
NO_AUTH_ERROR("40101", "无权限"),
NOT_FOUND_ERROR("40400", "请求数据不存在"),
FORBIDDEN_ERROR("40300", "禁止访问"),
SYSTEM_ERROR("50000", "系统内部异常"),
OPERATION_ERROR("50001", "操作失败"),
API_REQUEST_ERROR("50010", "接口调用失败");
/**
* 状态码
*/
private final String code; SUCCESS("0", "ok"), PARAMS_ERROR("40000", "请求参数错误"), NOT_LOGIN_ERROR("40100", "未登录"), NO_AUTH_ERROR("40101", "无权限"),
NOT_FOUND_ERROR("40400", "请求数据不存在"), FORBIDDEN_ERROR("40300", "禁止访问"), SYSTEM_ERROR("50000", "系统内部异常"),
OPERATION_ERROR("50001", "操作失败"), API_REQUEST_ERROR("50010", "接口调用失败");
/** /**
* 信息 * 状态码
*/ */
private final String message;
ErrorCode(String code, String message) { private final String code;
this.code = code;
this.message = message;
}
@Override /**
public String code() { * 信息
return code; */
} private final String message;
ErrorCode(String code, String message) {
this.code = code;
this.message = message;
}
@Override
public String code() {
return code;
}
@Override
public String message() {
return message;
}
@Override
public String message() {
return message;
}
} }

View File

@@ -1,6 +1,9 @@
package cn.meowrain.aioj.backend.framework.core.errorcode; package cn.meowrain.aioj.backend.framework.core.errorcode;
public interface IErrorCode { public interface IErrorCode {
String code();
String message(); String code();
String message();
} }

View File

@@ -12,13 +12,16 @@ import java.util.Optional;
@Getter @Getter
public class AbstractException extends RuntimeException { public class AbstractException extends RuntimeException {
public final String errorCode;
public final String errorMessage;
public AbstractException(String message, Throwable throwable, IErrorCode errorCode) { public final String errorCode;
super(message);
this.errorCode = errorCode.code(); public final String errorMessage;
this.errorMessage = Optional.ofNullable(StringUtils.hasLength(message) ? message : null)
.orElse(errorCode.message()); public AbstractException(String message, Throwable throwable, IErrorCode errorCode) {
} super(message);
this.errorCode = errorCode.code();
this.errorMessage = Optional.ofNullable(StringUtils.hasLength(message) ? message : null)
.orElse(errorCode.message());
}
} }

View File

@@ -8,21 +8,22 @@ import lombok.ToString;
* 客户端异常 * 客户端异常
*/ */
@ToString @ToString
public class ClientException extends AbstractException{ public class ClientException extends AbstractException {
public ClientException(String message, Throwable throwable, IErrorCode errorCode) {
super(message, throwable, errorCode);
}
public ClientException(IErrorCode errorCode) { public ClientException(String message, Throwable throwable, IErrorCode errorCode) {
this(null, null, errorCode); super(message, throwable, errorCode);
} }
public ClientException(String message, IErrorCode errorCode) { public ClientException(IErrorCode errorCode) {
this(message, null, errorCode); this(null, null, errorCode);
} }
public ClientException(String message) { public ClientException(String message, IErrorCode errorCode) {
this(message, null, ErrorCode.PARAMS_ERROR); this(message, null, errorCode);
} }
public ClientException(String message) {
this(message, null, ErrorCode.PARAMS_ERROR);
}
} }

View File

@@ -8,20 +8,22 @@ import lombok.ToString;
* 调用第三方服务异常 * 调用第三方服务异常
*/ */
@ToString @ToString
public class RemoteException extends AbstractException{ public class RemoteException extends AbstractException {
public RemoteException(IErrorCode errorCode) {
this(null, null, errorCode);
}
public RemoteException(String message, IErrorCode errorCode) { public RemoteException(IErrorCode errorCode) {
this(message, null, errorCode); this(null, null, errorCode);
} }
public RemoteException(String message, Throwable throwable, IErrorCode errorCode) { public RemoteException(String message, IErrorCode errorCode) {
super(message, throwable, errorCode); this(message, null, errorCode);
} }
public RemoteException(String message) {
this(message, null, ErrorCode.API_REQUEST_ERROR); public RemoteException(String message, Throwable throwable, IErrorCode errorCode) {
} super(message, throwable, errorCode);
}
public RemoteException(String message) {
this(message, null, ErrorCode.API_REQUEST_ERROR);
}
} }

View File

@@ -9,20 +9,21 @@ import lombok.ToString;
*/ */
@ToString @ToString
public class ServiceException extends AbstractException { public class ServiceException extends AbstractException {
public ServiceException(String message, IErrorCode errorCode) {
this(message, null, errorCode);
}
public ServiceException(String message) { public ServiceException(String message, IErrorCode errorCode) {
this(message, null, ErrorCode.SYSTEM_ERROR); this(message, null, errorCode);
} }
public ServiceException(IErrorCode errorCode) { public ServiceException(String message) {
this(null, null, errorCode); this(message, null, ErrorCode.SYSTEM_ERROR);
} }
public ServiceException(String message, Throwable throwable, IErrorCode errorCode) { public ServiceException(IErrorCode errorCode) {
super(message, throwable, errorCode); this(null, null, errorCode);
} }
public ServiceException(String message, Throwable throwable, IErrorCode errorCode) {
super(message, throwable, errorCode);
}
} }

View File

@@ -20,64 +20,68 @@ import java.util.List;
@Slf4j @Slf4j
@RestControllerAdvice @RestControllerAdvice
public class GlobalExceptionHandler { public class GlobalExceptionHandler {
/**
* 捕获所有参数错误,然后统一捕获并且抛出
* @param request {@link HttpServletRequest}
* @param ex {@link org.springframework.validation.method.MethodValidationException}
* @return {@link Result<Void>}
*/
@ExceptionHandler(value = MethodArgumentNotValidException.class)
public Result<Void> validExceptionHandler(HttpServletRequest request, MethodArgumentNotValidException ex) {
BindingResult bindingResult = ex.getBindingResult();
// 收集所有错误字段
List<String> errorMessages = bindingResult.getFieldErrors().stream()
.map(FieldError::getDefaultMessage).toList();
String exceptionMessage = String.join(",", errorMessages);
log.error("[{}] {} [ex] {}", request.getMethod(), getUrl(request), exceptionMessage);
return Results.paramsValidFailure();
}
/** /**
* 抽象异常捕获其 * 捕获所有参数错误,然后统一捕获并且抛出
* @param request {@link HttpServletRequest} * @param request {@link HttpServletRequest}
* @param ex {@link AbstractException} * @param ex {@link org.springframework.validation.method.MethodValidationException}
* @return {@link Result<Void>} * @return {@link Result<Void>}
*/ */
@ExceptionHandler(value = {AbstractException.class}) @ExceptionHandler(value = MethodArgumentNotValidException.class)
public Result<Void> abstractExceptionHandler(HttpServletRequest request,AbstractException ex ) { public Result<Void> validExceptionHandler(HttpServletRequest request, MethodArgumentNotValidException ex) {
if (ex.getCause() != null) { BindingResult bindingResult = ex.getBindingResult();
log.error("[{}] {} [ex] {}", request.getMethod(), request.getRequestURL().toString(), ex, ex.getCause()); // 收集所有错误字段
return Results.failure(ex); List<String> errorMessages = bindingResult.getFieldErrors()
} .stream()
StringBuilder stackTraceBuilder = new StringBuilder(); .map(FieldError::getDefaultMessage)
stackTraceBuilder.append(ex.getClass().getName()).append(": ").append(ex.getErrorMessage()).append("\n"); .toList();
StackTraceElement[] stackTrace = ex.getStackTrace(); String exceptionMessage = String.join(",", errorMessages);
for (int i = 0; i < Math.min(5, stackTrace.length); i++) { log.error("[{}] {} [ex] {}", request.getMethod(), getUrl(request), exceptionMessage);
stackTraceBuilder.append("\tat ").append(stackTrace[i]).append("\n"); return Results.paramsValidFailure();
} }
log.error("[{}] {} [ex] {} \n\n{}", request.getMethod(), request.getRequestURL().toString(), ex,
stackTraceBuilder);
return Results.failure(ex);
}
/** /**
* 拦截未捕获异常 * 抽象异常捕获其
*/ * @param request {@link HttpServletRequest}
@ExceptionHandler(value = Throwable.class) * @param ex {@link AbstractException}
public Result<Void> defaultErrorHandler(HttpServletRequest request, Throwable throwable) { * @return {@link Result<Void>}
log.error("[{}] {} ", request.getMethod(), getUrl(request), throwable); */
return Results.failure(); @ExceptionHandler(value = { AbstractException.class })
} public Result<Void> abstractExceptionHandler(HttpServletRequest request, AbstractException ex) {
if (ex.getCause() != null) {
log.error("[{}] {} [ex] {}", request.getMethod(), request.getRequestURL().toString(), ex, ex.getCause());
return Results.failure(ex);
}
StringBuilder stackTraceBuilder = new StringBuilder();
stackTraceBuilder.append(ex.getClass().getName()).append(": ").append(ex.getErrorMessage()).append("\n");
StackTraceElement[] stackTrace = ex.getStackTrace();
for (int i = 0; i < Math.min(5, stackTrace.length); i++) {
stackTraceBuilder.append("\tat ").append(stackTrace[i]).append("\n");
}
log.error("[{}] {} [ex] {} \n\n{}", request.getMethod(), request.getRequestURL().toString(), ex,
stackTraceBuilder);
return Results.failure(ex);
}
/**
* 拦截未捕获异常
*/
@ExceptionHandler(value = Throwable.class)
public Result<Void> defaultErrorHandler(HttpServletRequest request, Throwable throwable) {
log.error("[{}] {} ", request.getMethod(), getUrl(request), throwable);
return Results.failure();
}
/**
* 获取请求URL
* @param request {@link HttpServletRequest}
* @return String
*/
private String getUrl(HttpServletRequest request) {
if (!StringUtils.hasText(request.getQueryString())) {
return request.getRequestURI();
}
return request.getRequestURL() + "?" + request.getQueryString();
}
/**
* 获取请求URL
* @param request {@link HttpServletRequest}
* @return String
*/
private String getUrl(HttpServletRequest request) {
if (!StringUtils.hasText(request.getQueryString())) {
return request.getRequestURI();
}
return request.getRequestURL() + "?" + request.getQueryString();
}
} }

View File

@@ -0,0 +1,50 @@
package cn.meowrain.aioj.backend.framework.core.jackson;
import cn.hutool.core.date.DatePattern;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.datatype.jsr310.PackageVersion;
import com.fasterxml.jackson.datatype.jsr310.deser.*;
import com.fasterxml.jackson.datatype.jsr310.ser.*;
import java.io.Serial;
import java.time.*;
import java.time.format.DateTimeFormatter;
public class JavaTimeModule extends SimpleModule {
@Serial
private static final long serialVersionUID = 1L;
/**
* JavaTimeModule构造函数用于初始化时间序列化和反序列化规则
*/
public JavaTimeModule() {
super(PackageVersion.VERSION);
// ======================= 时间序列化规则 ===============================
// yyyy-MM-dd HH:mm:ss
this.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DatePattern.NORM_DATETIME_FORMATTER));
// yyyy-MM-dd
this.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ISO_LOCAL_DATE));
// HH:mm:ss
this.addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ISO_LOCAL_TIME));
// Instant 类型序列化
this.addSerializer(Instant.class, InstantSerializer.INSTANCE);
// Duration 类型序列化
this.addSerializer(Duration.class, DurationSerializer.INSTANCE);
// ======================= 时间反序列化规则 ==============================
// yyyy-MM-dd HH:mm:ss
this.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DatePattern.NORM_DATETIME_FORMATTER));
// yyyy-MM-dd
this.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ISO_LOCAL_DATE));
// HH:mm:ss
this.addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ISO_LOCAL_TIME));
// Instant 反序列化
this.addDeserializer(Instant.class, InstantDeserializer.INSTANT);
// Duration 反序列化
this.addDeserializer(Duration.class, DurationDeserializer.INSTANCE);
}
}

View File

@@ -0,0 +1,77 @@
package cn.meowrain.aioj.backend.framework.core.utils;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.EnvironmentAware;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Service;
@Slf4j
@Service
public class SpringContextHolder implements ApplicationContextAware, EnvironmentAware, DisposableBean {
private static ApplicationContext applicationContext = null;
private static Environment environment = null;
/**
* 从静态变量applicationContext中取得Bean, 自动转型为所赋值对象的类型.
*/
public static <T> T getBean(String name) {
return (T) applicationContext.getBean(name);
}
/**
* 从静态变量applicationContext中取得Bean, 自动转型为所赋值对象的类型.
*/
public static <T> T getBean(Class<T> requiredType) {
return applicationContext.getBean(requiredType);
}
@Override
@SneakyThrows
public void destroy() throws Exception {
clearHolder();
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
SpringContextHolder.applicationContext = applicationContext;
}
@Override
public void setEnvironment(Environment environment) {
SpringContextHolder.environment = environment;
}
/**
* 清除SpringContextHolder中的ApplicationContext为Null.
*/
public static void clearHolder() {
if (log.isDebugEnabled()) {
log.debug("清除SpringContextHolder中的ApplicationContext:" + applicationContext);
}
applicationContext = null;
}
/**
* 发布事件
* @param event
*/
public static void publishEvent(ApplicationEvent event) {
if (applicationContext == null) {
return;
}
applicationContext.publishEvent(event);
}
/**
* 是否是微服务
* @return boolean
*/
public static boolean isMicro() {
return environment.getProperty("spring.cloud.nacos.discovery.enabled", Boolean.class, true);
}
}

View File

@@ -12,39 +12,44 @@ import java.io.Serializable;
@Data @Data
@Accessors(chain = true) @Accessors(chain = true)
public class Result<T> implements Serializable { public class Result<T> implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 正确返回码
* */
public static final String SUCCESS_CODE = "0";
/**
* 响应码
*/
private String code;
/**
* 响应数据
*/
private T data;
/**
* 响应信息
*/
private String message;
/** @Serial
* 返回是否是正确响应 private static final long serialVersionUID = 1L;
*
* @return boolean /**
*/ * 正确返回码
public boolean isSuccess() { */
return SUCCESS_CODE.equals(code); public static final String SUCCESS_CODE = "0";
}
/**
* 响应码
*/
private String code;
/**
* 响应数据
*/
private T data;
/**
* 响应信息
*/
private String message;
/**
* 返回是否是正确响应
* @return boolean
*/
public boolean isSuccess() {
return SUCCESS_CODE.equals(code);
}
/**
* 返回是否是错误响应
* @return boolean
*/
public boolean isFail() {
return !isSuccess();
}
/**
* 返回是否是错误响应
* @return boolean
*/
public boolean isFail() {
return !isSuccess();
}
} }

View File

@@ -1,6 +1,5 @@
package cn.meowrain.aioj.backend.framework.core.web; package cn.meowrain.aioj.backend.framework.core.web;
import cn.meowrain.aioj.backend.framework.core.errorcode.ErrorCode; import cn.meowrain.aioj.backend.framework.core.errorcode.ErrorCode;
import cn.meowrain.aioj.backend.framework.core.exception.AbstractException; import cn.meowrain.aioj.backend.framework.core.exception.AbstractException;
@@ -10,70 +9,61 @@ import java.util.Optional;
* 构建响应的工具类 * 构建响应的工具类
*/ */
public final class Results { public final class Results {
/**
* 成功响应,不返回任何信息
*
* @return {@link Result<Void>}
*/
public static Result<Void> success() {
return new Result<Void>().setCode(Result.SUCCESS_CODE);
}
/** /**
* 成功响应 返回数据 * 成功响应,不返回任何信息
* * @return {@link Result<Void>}
* @param data 返回的响应体信息 */
* @param <T> 泛型 public static Result<Void> success() {
* @return {@link Result<T>} return new Result<Void>().setCode(Result.SUCCESS_CODE);
*/ }
public static <T> Result<T> success(T data) {
return new Result<T>().setCode(Result.SUCCESS_CODE)
.setData(data);
}
/** /**
* 客户端请求参数错误 * 成功响应 返回数据
* @return {@link Result<Void>} * @param data 返回的响应体信息
*/ * @param <T> 泛型
public static Result<Void> paramsValidFailure() { * @return {@link Result<T>}
return failure(ErrorCode.PARAMS_ERROR.code(), ErrorCode.PARAMS_ERROR.message()); */
} public static <T> Result<T> success(T data) {
return new Result<T>().setCode(Result.SUCCESS_CODE).setData(data);
}
/** /**
* 服务端错误默认响应 -- <b>内部错误</b> * 客户端请求参数错误
* * @return {@link Result<Void>}
* @return {@link Result<Void>} */
*/ public static Result<Void> paramsValidFailure() {
public static Result<Void> failure() { return failure(ErrorCode.PARAMS_ERROR.code(), ErrorCode.PARAMS_ERROR.message());
return new Result<Void>().setCode(ErrorCode.SYSTEM_ERROR.code()) }
.setMessage(ErrorCode.SYSTEM_ERROR.message());
}
/** /**
* 服务端错误响应 - 接收一个AbstractException里面定义了错误码和错误信息 * 服务端错误默认响应 -- <b>内部错误</b>
* * @return {@link Result<Void>}
* @param exception {@link AbstractException} */
* @return {@link Result<Void>} public static Result<Void> failure() {
*/ return new Result<Void>().setCode(ErrorCode.SYSTEM_ERROR.code()).setMessage(ErrorCode.SYSTEM_ERROR.message());
public static Result<Void> failure(AbstractException exception) { }
String errorCode = Optional.ofNullable(exception.getErrorCode()).orElse(ErrorCode.SYSTEM_ERROR.code());
String errorMessage = Optional.ofNullable(exception.getMessage()).orElse(ErrorCode.SYSTEM_ERROR.message());
return new Result<Void>() /**
.setCode(errorCode) * 服务端错误响应 - 接收一个AbstractException里面定义了错误码和错误信息
.setMessage(errorMessage); * @param exception {@link AbstractException}
} * @return {@link Result<Void>}
*/
public static Result<Void> failure(AbstractException exception) {
String errorCode = Optional.ofNullable(exception.getErrorCode()).orElse(ErrorCode.SYSTEM_ERROR.code());
String errorMessage = Optional.ofNullable(exception.getMessage()).orElse(ErrorCode.SYSTEM_ERROR.message());
return new Result<Void>().setCode(errorCode).setMessage(errorMessage);
}
/**
* 服务端错误响应,自定义错误码和错误信息
* @param errorCode {@link String}
* @param errorMessage {@link String}
* @return {@link Result<Void>}
*/
public static Result<Void> failure(String errorCode, String errorMessage) {
return new Result<Void>().setCode(errorCode).setMessage(errorMessage);
}
/**
* 服务端错误响应,自定义错误码和错误信息
*
* @param errorCode {@link String}
* @param errorMessage {@link String}
* @return {@link Result<Void>}
*/
public static Result<Void> failure(String errorCode, String errorMessage) {
return new Result<Void>()
.setCode(errorCode)
.setMessage(errorMessage);
}
} }

View File

@@ -0,0 +1,2 @@
cn.meowrain.aioj.backend.framework.core.banner.config.AIOJBannerAutoConfiguration
cn.meowrain.aioj.backend.framework.core.config.WebAutoConfiguration

View File

@@ -0,0 +1,60 @@
<?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>
<artifactId>aioj-backend-common</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>aioj-backend-common-feign</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>
<dependencies>
<dependency>
<groupId>cn.meowrain</groupId>
<artifactId>aioj-backend-common-core</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<!--feign 依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!-- okhttp 扩展 -->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-okhttp</artifactId>
</dependency>
<!-- LB 扩展 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<!--caffeine 替换LB 默认缓存实现-->
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
<!--oauth server 依赖-->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-core</artifactId>
</dependency>
<!-- 异常枚举 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,7 @@
package cn.meowrain.aioj.backend.framework.feign;
import org.springframework.boot.autoconfigure.AutoConfiguration;
@AutoConfiguration
public class FeignAutoConfiguration {
}

View File

@@ -0,0 +1,13 @@
package cn.meowrain.aioj.backend.framework.feign.annotation;
import org.springframework.cloud.openfeign.EnableFeignClients;
import java.lang.annotation.*;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@EnableFeignClients
public @interface EnableAIOJFeignClients {
String[] basePackages() default {};
}

View File

@@ -0,0 +1,16 @@
package cn.meowrain.aioj.backend.framework.feign.annotation;
import java.lang.annotation.*;
/**
* 服务无token调用声明注解
* <p>
* 只有发起方没有 token 时候才需要添加此注解, @NoToken + @Inner
* <p>
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface NoToken {
}

View File

@@ -17,4 +17,34 @@
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties> </properties>
<dependencies>
<!--安全依赖获取上下文信息-->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-core</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-core</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-extra</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-http</artifactId>
</dependency>
<dependency>
<groupId>cn.meowrain</groupId>
<artifactId>aioj-backend-common-core</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>cn.meowrain</groupId>
<artifactId>aioj-backend-upms-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
</project> </project>

View File

@@ -3,6 +3,8 @@ package cn.meowrain.aioj.backend.framework.log;
import cn.meowrain.aioj.backend.framework.log.aspect.SysLogAspect; import cn.meowrain.aioj.backend.framework.log.aspect.SysLogAspect;
import cn.meowrain.aioj.backend.framework.log.config.AIOJLogPropertiesConfiguration; import cn.meowrain.aioj.backend.framework.log.config.AIOJLogPropertiesConfiguration;
import cn.meowrain.aioj.backend.framework.log.event.SysLogListener; import cn.meowrain.aioj.backend.framework.log.event.SysLogListener;
import cn.meowrain.aioj.backend.upms.api.feign.RemoteLogService;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
@@ -14,19 +16,24 @@ import org.springframework.scheduling.annotation.EnableAsync;
@EnableConfigurationProperties(AIOJLogPropertiesConfiguration.class) @EnableConfigurationProperties(AIOJLogPropertiesConfiguration.class)
@ConditionalOnProperty(value = "aioj.log.enabled", matchIfMissing = true) @ConditionalOnProperty(value = "aioj.log.enabled", matchIfMissing = true)
public class LogAutoConfiguration { public class LogAutoConfiguration {
/**
* 创建并返回SysLogListener的Bean实例 /**
*/ * 创建并返回SysLogListener的Bean实例
@Bean */
public SysLogListener sysLogListener(AIOJLogPropertiesConfiguration logProperties, RemoteLogService remoteLogService) { @Bean
return new SysLogListener(remoteLogService, logProperties); @ConditionalOnBean(RemoteLogService.class)
} public SysLogListener sysLogListener(AIOJLogPropertiesConfiguration logProperties,
/** RemoteLogService remoteLogService) {
* 返回切面类实例 return new SysLogListener(remoteLogService, logProperties);
* @return {@link SysLogAspect} }
*/
@Bean /**
public SysLogAspect sysLogAspect() { * 返回切面类实例
return new SysLogAspect(); * @return {@link SysLogAspect}
} */
@Bean
public SysLogAspect sysLogAspect() {
return new SysLogAspect();
}
} }

View File

@@ -9,15 +9,17 @@ import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME) @Retention(RetentionPolicy.RUNTIME)
@Documented @Documented
public @interface SysLog { public @interface SysLog {
/**
* 描述
* @return {@link String}
*/
String value() default "";
/** /**
* Spel表达式 * 描述
* @return 日志描述 * @return {@link String}
*/ */
String expression() default ""; String value() default "";
/**
* Spel表达式
* @return 日志描述
*/
String expression() default "";
} }

View File

@@ -1,8 +1,14 @@
package cn.meowrain.aioj.backend.framework.log.aspect; package cn.meowrain.aioj.backend.framework.log.aspect;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import cn.meowrain.aioj.backend.framework.core.utils.SpringContextHolder;
import cn.meowrain.aioj.backend.framework.log.annotation.SysLog; import cn.meowrain.aioj.backend.framework.log.annotation.SysLog;
import cn.meowrain.aioj.backend.framework.log.enums.LogTypeEnum;
import cn.meowrain.aioj.backend.framework.log.event.SysLogEvent;
import cn.meowrain.aioj.backend.framework.log.event.SysLogEventSource;
import cn.meowrain.aioj.backend.framework.log.utils.SysLogUtils;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Around;
@@ -14,18 +20,60 @@ import org.springframework.expression.EvaluationContext;
@Slf4j @Slf4j
@RequiredArgsConstructor @RequiredArgsConstructor
public class SysLogAspect { public class SysLogAspect {
/**
* 环绕通知方法,用于处理系统日志记录
* @param point 连接点对象
* @param sysLog 系统日志注解
* @return 目标方法执行结果
* @throws Throwable 目标方法执行可能抛出的异常
*/
@Around("@annotation(sysLog)")
@SneakyThrows
public Object around(ProceedingJoinPoint point,SysLog sysLog) {
String strClassName = point.getTarget().getClass().getName();
String strMethodName = point.getSignature().getName();
log.debug("[类名]:{},[方法]:{}", strClassName, strMethodName);
@Around("@annotation(sysLog)") String value = sysLog.value();
public Object around(ProceedingJoinPoint joinPoint,SysLog sysLog) throws Throwable { String expression = sysLog.expression();
String strClassName = joinPoint.getTarget().getClass().getName(); // 当前表达式存在 SPEL会覆盖 value 的值
String strMethodName = joinPoint.getSignature().getName(); if (StrUtil.isNotBlank(expression)) {
log.debug("[类名]:{},[方法]:{}", strClassName, strMethodName); // 解析SPEL
String value = sysLog.value(); MethodSignature signature = (MethodSignature) point.getSignature();
String expression = sysLog.expression(); EvaluationContext context = SysLogUtils.getContext(point.getArgs(), signature.getMethod());
if(StrUtil.isNotBlank(expression)) { try {
// 解析SPEL value = SysLogUtils.getValue(context, expression, String.class);
MethodSignature signature = (MethodSignature) joinPoint.getSignature(); }
EvaluationContext context = SysLogUtils.getContext(point.getArgs(), signature.getMethod()); catch (Exception e) {
} // SPEL 表达式异常,获取 value 的值
} log.error("@SysLog 解析SPEL {} 异常", expression);
}
}
SysLogEventSource logVo = SysLogUtils.getSysLog();
logVo.setTitle(value);
// 获取请求body参数
if (StrUtil.isBlank(logVo.getParams())) {
logVo.setBody(point.getArgs());
}
// 发送异步日志事件
Long startTime = System.currentTimeMillis();
Object obj;
try {
obj = point.proceed();
}
catch (Exception e) {
logVo.setLogType(LogTypeEnum.ERROR.getType());
logVo.setException(e.getMessage());
throw e;
}
finally {
Long endTime = System.currentTimeMillis();
logVo.setTime(endTime - startTime);
SpringContextHolder.publishEvent(new SysLogEvent(logVo));
}
return obj;
}
} }

View File

@@ -2,20 +2,33 @@ package cn.meowrain.aioj.backend.framework.log.config;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.ConfigurationProperties;
import java.util.List;
@Getter @Getter
@Setter @Setter
@ConfigurationProperties(AIOJLogPropertiesConfiguration.PREFIX) @ConfigurationProperties(AIOJLogPropertiesConfiguration.PREFIX)
public class AIOJLogPropertiesConfiguration { public class AIOJLogPropertiesConfiguration {
public static final String PREFIX = "aioj.log";
/** public static final String PREFIX = "aioj.log";
* 开启日志记录
*/ /**
private boolean enabled = true; * 开启日志记录
*/
private boolean enabled = true;
/**
* 请求报文最大存储长度
*/
private Integer maxLength = 20000;
/** /**
* 请求报文最大存储长度 * 放行字段password,mobile,idcard,phone
*/ */
private Integer maxLength = 20000; @Value("${log.exclude-fields:password,mobile,idcard,phone}")
private List<String> excludeFields;
} }

View File

@@ -1,92 +0,0 @@
package cn.meowrain.aioj.backend.framework.log.entity;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 日志对象
*/
@Data
public class SysLog implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 编号
*/
private Long id;
/**
* 日志类型
*/
private String logType;
/**
* 日志标题
*/
private String title;
/**
* 创建者
*/
private String createBy;
/**
* 创建时间
*/
private LocalDateTime createTime;
/**
* 更新时间
*/
private LocalDateTime updateTime;
/**
* 操作IP地址
*/
private String remoteAddr;
/**
* 用户代理
*/
private String userAgent;
/**
* 请求URI
*/
private String requestUri;
/**
* 操作方式
*/
private String method;
/**
* 操作提交的数据
*/
private String params;
/**
* 执行时间
*/
private Long time;
/**
* 异常信息
*/
private String exception;
/**
* 服务ID
*/
private String serviceId;
/**
* 删除标记
*/
private String delFlag;
}

View File

@@ -1,6 +1,7 @@
package cn.meowrain.aioj.backend.framework.log.event; package cn.meowrain.aioj.backend.framework.log.event;
import cn.meowrain.aioj.backend.framework.log.annotation.SysLog;
import cn.meowrain.aioj.backend.upms.api.entity.SysLog;
import org.springframework.context.ApplicationEvent; import org.springframework.context.ApplicationEvent;
import java.io.Serial; import java.io.Serial;
@@ -9,12 +10,15 @@ import java.io.Serial;
* 系统日志事件类 * 系统日志事件类
*/ */
public class SysLogEvent extends ApplicationEvent { public class SysLogEvent extends ApplicationEvent {
@Serial
private static final long serialVersionUID = 1L; @Serial
/** private static final long serialVersionUID = 1L;
* 构造方法根据源SysLog对象创建SysLogEvent
*/ /**
public SysLogEvent(SysLog source) { * 构造方法根据源SysLog对象创建SysLogEvent
super(source); */
} public SysLogEvent(SysLog source) {
super(source);
}
} }

View File

@@ -1,24 +1,24 @@
package cn.meowrain.aioj.backend.framework.log.event; package cn.meowrain.aioj.backend.framework.log.event;
import cn.meowrain.aioj.backend.upms.api.entity.SysLog;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import java.io.Serial; import java.io.Serial;
import java.io.Serializable;
/** /**
* 系统 * 系统
*/ */
@Data @Data
@EqualsAndHashCode(callSuper = false) @EqualsAndHashCode(callSuper = false)
public class SysLogEventSource extends SysLogEvent implements Serializable { public class SysLogEventSource extends SysLog {
@Serial @Serial
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
/** /**
* 参数重写成object * 参数重写成object
*/ */
private Object body; private Object body;
} }

View File

@@ -1,8 +1,15 @@
package cn.meowrain.aioj.backend.framework.log.event; package cn.meowrain.aioj.backend.framework.log.event;
import cn.hutool.core.util.StrUtil;
import cn.meowrain.aioj.backend.framework.core.jackson.JavaTimeModule;
import cn.meowrain.aioj.backend.framework.log.config.AIOJLogPropertiesConfiguration; import cn.meowrain.aioj.backend.framework.log.config.AIOJLogPropertiesConfiguration;
import cn.meowrain.aioj.backend.framework.log.entity.SysLog; import cn.meowrain.aioj.backend.upms.api.entity.SysLog;
import cn.meowrain.aioj.backend.upms.api.feign.RemoteLogService;
import com.fasterxml.jackson.annotation.JsonFilter;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ser.FilterProvider;
import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter;
import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows; import lombok.SneakyThrows;
import org.springframework.beans.BeanUtils; import org.springframework.beans.BeanUtils;
@@ -11,26 +18,58 @@ import org.springframework.context.event.EventListener;
import org.springframework.core.annotation.Order; import org.springframework.core.annotation.Order;
import org.springframework.scheduling.annotation.Async; import org.springframework.scheduling.annotation.Async;
import java.util.Objects;
@RequiredArgsConstructor @RequiredArgsConstructor
public class SysLogListener implements InitializingBean { public class SysLogListener implements InitializingBean {
private final static ObjectMapper objectMapper = new ObjectMapper();
private final RemoteLogService remoteLogService; private final static ObjectMapper objectMapper = new ObjectMapper();
private final AIOJLogPropertiesConfiguration logProperties; private final RemoteLogService remoteLogService;
@SneakyThrows private final AIOJLogPropertiesConfiguration logProperties;
@Async
@Order
@EventListener(SysLogEvent.class)
public void saveLog(SysLogEvent sysLogEvent){
SysLogEventSource source = (SysLogEventSource) sysLogEvent.getSource();
SysLog sysLog = new SysLog();
BeanUtils.copyProperties(source, sysLog);
} @SneakyThrows
@Override @Async
public void afterPropertiesSet() throws Exception { @Order
@EventListener(SysLogEvent.class)
public void saveLog(SysLogEvent sysLogEvent) {
SysLogEventSource source = (SysLogEventSource) sysLogEvent.getSource();
SysLog sysLog = new SysLog();
BeanUtils.copyProperties(source, sysLog);
// json 格式刷参数放在异步中处理,提升性能
if (Objects.nonNull(source.getBody())) {
String params = objectMapper.writeValueAsString(source.getBody());
sysLog.setParams(StrUtil.subPre(params, logProperties.getMaxLength()));
}
remoteLogService.saveLog(sysLog);
}
/**
* 在 Bean 初始化后执行,用于初始化 ObjectMapper
* @throws Exception
*/
@Override
public void afterPropertiesSet() throws Exception {
//给 ObjectMapper 添加 MixIn用于过滤字段
objectMapper.addMixIn(Object.class, PropertyFilterMixIn.class);
String[] ignorableFieldNames = logProperties.getExcludeFields().toArray(new String[0]);
FilterProvider filters = new SimpleFilterProvider().addFilter("filter properties by name",
SimpleBeanPropertyFilter.serializeAllExcept(ignorableFieldNames));
objectMapper.setFilterProvider(filters);
objectMapper.registerModule(new JavaTimeModule());
}
/**
* 属性过滤混合类:用于通过名称过滤属性
*
* @author lengleng
* @date 2025/05/31
*/
@JsonFilter("filter properties by name")
class PropertyFilterMixIn {
} }
} }

View File

@@ -6,13 +6,15 @@ import org.springframework.core.Ordered;
import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.ConfigurableEnvironment;
public class ApplicationLoggerInitializer implements EnvironmentPostProcessor, Ordered { public class ApplicationLoggerInitializer implements EnvironmentPostProcessor, Ordered {
@Override
public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
} @Override
public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
}
@Override
public int getOrder() {
return Ordered.LOWEST_PRECEDENCE;
}
@Override
public int getOrder() {
return Ordered.LOWEST_PRECEDENCE;
}
} }

View File

@@ -1,9 +1,101 @@
package cn.meowrain.aioj.backend.framework.log.utils; package cn.meowrain.aioj.backend.framework.log.utils;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.URLUtil;
import cn.hutool.extra.servlet.JakartaServletUtil;
import cn.hutool.extra.spring.SpringUtil;
import cn.hutool.http.HttpUtil;
import cn.meowrain.aioj.backend.framework.core.utils.SpringContextHolder;
import cn.meowrain.aioj.backend.framework.log.config.AIOJLogPropertiesConfiguration;
import cn.meowrain.aioj.backend.framework.log.enums.LogTypeEnum;
import cn.meowrain.aioj.backend.framework.log.event.SysLogEventSource; import cn.meowrain.aioj.backend.framework.log.event.SysLogEventSource;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.core.StandardReflectionParameterNameDiscoverer;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.http.HttpHeaders;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
public final class SysLogUtils { public final class SysLogUtils {
public static SysLogEventSource getSysLog() {
}
/**
* 获取系统日志事件源
* @return 系统日志事件源对象
*/
public static SysLogEventSource getSysLog() {
HttpServletRequest request = ((ServletRequestAttributes) Objects
.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
SysLogEventSource sysLog = new SysLogEventSource();
sysLog.setLogType(LogTypeEnum.NORMAL.getType());
sysLog.setRequestUri(URLUtil.getPath(request.getRequestURI()));
sysLog.setMethod(request.getMethod());
sysLog.setRemoteAddr(JakartaServletUtil.getClientIP(request));
sysLog.setUserAgent(request.getHeader(HttpHeaders.USER_AGENT));
sysLog.setCreateBy(getUsername());
sysLog.setServiceId(SpringUtil.getProperty("spring.application.name"));
// get 参数脱敏
AIOJLogPropertiesConfiguration logProperties = SpringContextHolder.getBean(AIOJLogPropertiesConfiguration.class);
Map<String, String[]> paramsMap = MapUtil.removeAny(new HashMap<>(request.getParameterMap()),
ArrayUtil.toArray(logProperties.getExcludeFields(), String.class));
sysLog.setParams(HttpUtil.toParams(paramsMap));
return sysLog;
}
/**
* 获取用户名称
* @return username
*/
private static String getUsername() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null) {
return null;
}
return authentication.getName();
}
/**
* 获取spel 定义的参数值
* @param context 参数容器
* @param key key
* @param clazz 需要返回的类型
* @param <T> 返回泛型
* @return 参数值
*/
public static <T> T getValue(EvaluationContext context, String key, Class<T> clazz) {
SpelExpressionParser spelExpressionParser = new SpelExpressionParser();
Expression expression = spelExpressionParser.parseExpression(key);
return expression.getValue(context, clazz);
}
/**
* 获取参数容器
* @param arguments 方法的参数列表
* @param signatureMethod 被执行的方法体
* @return 装载参数的容器
*/
public static EvaluationContext getContext(Object[] arguments, Method signatureMethod) {
String[] parameterNames = new StandardReflectionParameterNameDiscoverer().getParameterNames(signatureMethod);
EvaluationContext context = new StandardEvaluationContext();
if (parameterNames == null) {
return context;
}
for (int i = 0; i < arguments.length; i++) {
context.setVariable(parameterNames[i], arguments[i]);
}
return context;
}
} }

View File

@@ -25,6 +25,11 @@
<groupId>cn.hutool</groupId> <groupId>cn.hutool</groupId>
<artifactId>hutool-core</artifactId> <artifactId>hutool-core</artifactId>
</dependency> </dependency>
<!-- orm 模块-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
</dependency>
<!--mybatis--> <!--mybatis-->
<dependency> <dependency>
<groupId>com.baomidou</groupId> <groupId>com.baomidou</groupId>

View File

@@ -3,25 +3,25 @@ package cn.meowrain.backend.common.mybaits;
import cn.meowrain.backend.common.mybaits.config.MybatisPlusMetaObjectHandler; import cn.meowrain.backend.common.mybaits.config.MybatisPlusMetaObjectHandler;
import cn.meowrain.backend.common.mybaits.plugins.PaginationInterceptor; import cn.meowrain.backend.common.mybaits.plugins.PaginationInterceptor;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration(proxyBeanMethods = false) @AutoConfiguration
public class MybatisPlusAutoConfiguration { public class MybatisPlusAutoConfiguration {
@Bean @Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() { public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInterceptor()); interceptor.addInnerInterceptor(new PaginationInterceptor());
return interceptor; return interceptor;
} }
/** /**
* 创建并返回MybatisPlusMetaObjectHandler实例用于审计字段自动填充 * 创建并返回MybatisPlusMetaObjectHandler实例用于审计字段自动填充
*/ */
@Bean @Bean
public MybatisPlusMetaObjectHandler mybatisPlusMetaObjectHandler() { public MybatisPlusMetaObjectHandler mybatisPlusMetaObjectHandler() {
return new MybatisPlusMetaObjectHandler(); return new MybatisPlusMetaObjectHandler();
} }
} }

View File

@@ -11,38 +11,41 @@ import java.io.Serializable;
import java.time.LocalDateTime; import java.time.LocalDateTime;
/** /**
* 基础实体抽象类,包含通用实体字段 * 基础实体抽象类,包含通用实体字段
*/ */
@Getter @Getter
@Setter @Setter
public class BaseEntity implements Serializable { public class BaseEntity implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 创建者
*/
@Schema(description = "创建人")
@TableField(fill = FieldFill.INSERT)
private String createBy;
/** @Serial
* 创建时间 private static final long serialVersionUID = 1L;
*/
@Schema(description = "创建时间")
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
/** /**
* 更新 * 创建
*/ */
@Schema(description = "更新") @Schema(description = "创建")
@TableField(fill = FieldFill.INSERT_UPDATE) @TableField(fill = FieldFill.INSERT)
private String updateBy; private String createBy;
/**
* 创建时间
*/
@Schema(description = "创建时间")
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
/**
* 更新者
*/
@Schema(description = "更新人")
@TableField(fill = FieldFill.INSERT_UPDATE)
private String updateBy;
/**
* 更新时间
*/
@Schema(description = "更新时间")
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
/**
* 更新时间
*/
@Schema(description = "更新时间")
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
} }

View File

@@ -17,54 +17,56 @@ import java.util.Optional;
*/ */
@Slf4j @Slf4j
public class MybatisPlusMetaObjectHandler implements MetaObjectHandler { public class MybatisPlusMetaObjectHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
log.debug("mybatis plus start insert fill ....");
LocalDateTime now = LocalDateTime.now();
fillValIfNullByName("createTime", now, metaObject, true); @Override
fillValIfNullByName("updateTime", now, metaObject, true); public void insertFill(MetaObject metaObject) {
fillValIfNullByName("createBy", getUserName(), metaObject, true); log.debug("mybatis plus start insert fill ....");
fillValIfNullByName("updateBy", getUserName(), metaObject, true); LocalDateTime now = LocalDateTime.now();
// 删除标记自动填充 fillValIfNullByName("createTime", now, metaObject, true);
fillValIfNullByName("delFlag", DelStatusEnum.STATUS_NORMAL.code(), metaObject, true); fillValIfNullByName("updateTime", now, metaObject, true);
} fillValIfNullByName("createBy", getUserName(), metaObject, true);
fillValIfNullByName("updateBy", getUserName(), metaObject, true);
@Override // 删除标记自动填充
public void updateFill(MetaObject metaObject) { fillValIfNullByName("delFlag", DelStatusEnum.STATUS_NORMAL.code(), metaObject, true);
log.debug("mybatis plus start update fill ...."); }
fillValIfNullByName("updateTime", LocalDateTime.now(), metaObject, true);
fillValIfNullByName("updateBy", getUserName(), metaObject, true);
}
private void fillValIfNullByName(String fieldName, Object fieldVal, MetaObject metaObject, boolean isCover) { @Override
//如果填充值为空 public void updateFill(MetaObject metaObject) {
if (fieldVal == null) { log.debug("mybatis plus start update fill ....");
return; fillValIfNullByName("updateTime", LocalDateTime.now(), metaObject, true);
} fillValIfNullByName("updateBy", getUserName(), metaObject, true);
// 没有 set 方法 }
if (!metaObject.hasSetter(fieldName)) {
return;
}
// field 类型相同时设置
Class<?> getterType = metaObject.getGetterType(fieldName);
if (ClassUtils.isAssignableValue(getterType, fieldVal)) {
metaObject.setValue(fieldName, fieldVal);
}
}
private Object getUserName() { private void fillValIfNullByName(String fieldName, Object fieldVal, MetaObject metaObject, boolean isCover) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); // 如果填充值为空
// 匿名接口直接返回 if (fieldVal == null) {
if (authentication instanceof AnonymousAuthenticationToken) { return;
return null; }
} // 没有 set 方法
if (!metaObject.hasSetter(fieldName)) {
return;
}
// field 类型相同时设置
Class<?> getterType = metaObject.getGetterType(fieldName);
if (ClassUtils.isAssignableValue(getterType, fieldVal)) {
metaObject.setValue(fieldName, fieldVal);
}
}
if (Optional.ofNullable(authentication).isPresent()) { private Object getUserName() {
return authentication.getName(); Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
} // 匿名接口直接返回
if (authentication instanceof AnonymousAuthenticationToken) {
return null;
}
if (Optional.ofNullable(authentication).isPresent()) {
return authentication.getName();
}
return null;
}
return null;
}
} }

View File

@@ -1,6 +1,5 @@
package cn.meowrain.backend.common.mybaits.plugins; package cn.meowrain.backend.common.mybaits.plugins;
import com.baomidou.mybatisplus.annotation.DbType; import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.ParameterUtils; import com.baomidou.mybatisplus.core.toolkit.ParameterUtils;
@@ -16,47 +15,49 @@ import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds; import org.apache.ibatis.session.RowBounds;
/** /**
* * 分页拦截器实现类,用于处理分页查询逻辑 * * 分页拦截器实现类,用于处理分页查询逻辑 *
* * <p> * <p>
* * 当分页大小小于0时自动设置为0防止全表查询 * * 当分页大小小于0时自动设置为0防止全表查询
*/ */
@Data @Data
@NoArgsConstructor @NoArgsConstructor
@EqualsAndHashCode(callSuper = false) @EqualsAndHashCode(callSuper = false)
public class PaginationInterceptor extends PaginationInnerInterceptor { public class PaginationInterceptor extends PaginationInnerInterceptor {
/**
* 数据库类型
* <p>
* 查看 {@link #findIDialect(Executor)} 逻辑
*/
private DbType dbType;
/** /**
* 方言实现类 * 数据库类型
* <p> * <p>
* 查看 {@link #findIDialect(Executor)} 逻辑 * 查看 {@link #findIDialect(Executor)} 逻辑
*/ */
private IDialect dialect; private DbType dbType;
/**
* 方言实现类
* <p>
* 查看 {@link #findIDialect(Executor)} 逻辑
*/
private IDialect dialect;
public PaginationInterceptor(DbType dbType) { public PaginationInterceptor(DbType dbType) {
this.dbType = dbType; this.dbType = dbType;
} }
public PaginationInterceptor(IDialect dialect) { public PaginationInterceptor(IDialect dialect) {
this.dialect = dialect; this.dialect = dialect;
} }
/**
* 在执行查询前处理分页参数
*/
@Override
public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds,
ResultHandler resultHandler, BoundSql boundSql) {
IPage<?> page = ParameterUtils.findPage(parameter).orElse(null);
// size 小于 0 直接设置为 0 , 即不查询任何数据
if (null != page && page.getSize() < 0) {
page.setSize(0);
}
super.beforeQuery(executor, ms, page, rowBounds, resultHandler, boundSql);
}
/**
* 在执行查询前处理分页参数
*/
@Override
public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
IPage<?> page = ParameterUtils.findPage(parameter).orElse(null);
// size 小于 0 直接设置为 0 , 即不查询任何数据
if (null != page && page.getSize() < 0) {
page.setSize(0);
}
super.beforeQuery(executor, ms, page, rowBounds, resultHandler, boundSql);
}
} }

View File

@@ -17,6 +17,7 @@
<module>aioj-backend-common-starter</module> <module>aioj-backend-common-starter</module>
<module>aioj-backend-common-mybatis</module> <module>aioj-backend-common-mybatis</module>
<module>aioj-backend-common-bom</module> <module>aioj-backend-common-bom</module>
<module>aioj-backend-common-feign</module>
</modules> </modules>
<properties> <properties>

View File

@@ -18,11 +18,6 @@
<spring-cloud-gateway.version>4.3.2</spring-cloud-gateway.version> <spring-cloud-gateway.version>4.3.2</spring-cloud-gateway.version>
</properties> </properties>
<dependencies> <dependencies>
<dependency>
<groupId>cn.meowrain</groupId>
<artifactId>aioj-backend-common-starter</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId> <artifactId>spring-boot-starter-webflux</artifactId>
@@ -66,5 +61,27 @@
<artifactId>spring-cloud-starter-bootstrap</artifactId> <artifactId>spring-cloud-starter-bootstrap</artifactId>
<version>4.3.0</version> <version>4.3.0</version>
</dependency> </dependency>
<dependency>
<groupId>cn.meowrain</groupId>
<artifactId>aioj-backend-common-core</artifactId>
<version>1.0-SNAPSHOT</version>
<exclusions>
<!-- 🚫 必须排除Spring MVC 核心 -->
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</exclusion>
<!-- 🚫 必须排除Servlet API (Gateway用不到) -->
<exclusion>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
</exclusion>
<!-- 🚫 必须排除Spring WebMVC -->
<exclusion>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies> </dependencies>
</project> </project>

View File

@@ -5,10 +5,12 @@ import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties;
@EnableConfigurationProperties(value = {GatewayPropertiesConfiguration.class}) @EnableConfigurationProperties(value = { GatewayPropertiesConfiguration.class })
@SpringBootApplication @SpringBootApplication
public class AIOJGatewayApplication { public class AIOJGatewayApplication {
public static void main(String[] args) {
SpringApplication.run(AIOJGatewayApplication.class, args); public static void main(String[] args) {
} SpringApplication.run(AIOJGatewayApplication.class, args);
}
} }

View File

@@ -12,57 +12,49 @@ import java.util.List;
/** /**
* 全局 CORS 配置WebFlux 环境使用 CorsWebFilter * 全局 CORS 配置WebFlux 环境使用 CorsWebFilter
* *
* WebFlux 不使用 Spring MVC 的 CorsFilter * WebFlux 不使用 Spring MVC 的 CorsFilter 而是使用专门的 CorsWebFilter 处理跨域。
* 而是使用专门的 CorsWebFilter 处理跨域。
* *
* 此配置实现了: * 此配置实现了: - 允许任意域名访问AllowedOriginPatterns = "*" - 允许所有请求方法GET、POST、PUT... - 允许所有请求头 -
* - 允许任意域名访问AllowedOriginPatterns = "*" * 允许跨域携带 CookieAllowCredentials - 对所有路径生效(/**
* - 允许所有请求方法GET、POST、PUT...
* - 允许所有请求头
* - 允许跨域携带 CookieAllowCredentials
* - 对所有路径生效(/**
*/ */
@Configuration @Configuration
public class CorsConfig { public class CorsConfig {
/** /**
* 注册全局 CORS 过滤器 * 注册全局 CORS 过滤器
* * @return CorsWebFilter 跨域过滤器
* @return CorsWebFilter 跨域过滤器 */
*/ @Bean
@Bean public CorsWebFilter corsWebFilter() {
public CorsWebFilter corsWebFilter() {
// 创建跨域配置对象 // 创建跨域配置对象
CorsConfiguration corsConfiguration = new CorsConfiguration(); CorsConfiguration corsConfiguration = new CorsConfiguration();
// 允许所有请求方式GET、POST、PUT、DELETE、OPTIONS... // 允许所有请求方式GET、POST、PUT、DELETE、OPTIONS...
corsConfiguration.addAllowedMethod("*"); corsConfiguration.addAllowedMethod("*");
// 是否允许携带 Cookie 信息。跨域默认不允许,需要显式开启 // 是否允许携带 Cookie 信息。跨域默认不允许,需要显式开启
corsConfiguration.setAllowCredentials(true); corsConfiguration.setAllowCredentials(true);
/** /**
* 允许跨域的来源域名 * 允许跨域的来源域名 使用 setAllowedOriginPatterns("*") 是 WebFlux 推荐方式, 因为
* 使用 setAllowedOriginPatterns("*") 是 WebFlux 推荐方式, * setAllowedOrigins("*") 在 allowCredentials=true 时会被拦截。
* 因为 setAllowedOrigins("*") 在 allowCredentials=true 时会被拦截。 */
*/ corsConfiguration.setAllowedOriginPatterns(List.of("*"));
corsConfiguration.setAllowedOriginPatterns(List.of("*"));
// 允许所有请求头,例如 Authorization、Content-Type 等 // 允许所有请求头,例如 Authorization、Content-Type 等
corsConfiguration.addAllowedHeader("*"); corsConfiguration.addAllowedHeader("*");
/** /**
* 基于 URL 的跨域配置源, * 基于 URL 的跨域配置源, PathPatternParser 用于解析路径模式(更高性能)
* PathPatternParser 用于解析路径模式(更高性能) */
*/ UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(new PathPatternParser());
UrlBasedCorsConfigurationSource source =
new UrlBasedCorsConfigurationSource(new PathPatternParser());
// 对所有路径应用跨域设置 // 对所有路径应用跨域设置
source.registerCorsConfiguration("/**", corsConfiguration); source.registerCorsConfiguration("/**", corsConfiguration);
// 创建并返回 WebFlux 专用的 CORS 过滤器
return new CorsWebFilter(source);
}
// 创建并返回 WebFlux 专用的 CORS 过滤器
return new CorsWebFilter(source);
}
} }

View File

@@ -0,0 +1,23 @@
package cn.meowrain.aioj.backend.gateway.config;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.client.WebClient;
/**
* 网关配置类
*/
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(GatewayPropertiesConfiguration.class)
public class GatewayConfiguration {
/**
* WebClient Bean用于服务间调用
*/
@Bean
public WebClient.Builder webClientBuilder() {
return WebClient.builder();
}
}

View File

@@ -2,13 +2,22 @@ package cn.meowrain.aioj.backend.gateway.config;
import lombok.Data; import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
@Component
@ConfigurationProperties(prefix = GatewayPropertiesConfiguration.PREFIX) @ConfigurationProperties(prefix = GatewayPropertiesConfiguration.PREFIX)
@Data @Data
public class GatewayPropertiesConfiguration { public class GatewayPropertiesConfiguration {
public static final String PREFIX = "aioj-backend-gateway";
/* public static final String PREFIX = "aioj-backend-gateway";
* 白名单放行
* */ /*
private String[] whiteList; * 白名单放行路径
* 支持Ant风格路径匹配如 /api/v1/question/**
*/
private List<String> whiteList = new ArrayList<>();
} }

View File

@@ -1,27 +1,169 @@
package cn.meowrain.aioj.backend.gateway.filter; package cn.meowrain.aioj.backend.gateway.filter;
import cn.meowrain.aioj.backend.framework.core.errorcode.ErrorCode;
import cn.meowrain.aioj.backend.framework.core.exception.RemoteException;
import cn.meowrain.aioj.backend.framework.core.web.Result;
import cn.meowrain.aioj.backend.framework.core.web.Results;
import cn.meowrain.aioj.backend.gateway.config.GatewayPropertiesConfiguration;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.filter.GatewayFilterChain; import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter; import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered; import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.http.server.reactive.ServerHttpResponseDecorator;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.reactive.function.client.WebClient; import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Objects;
@Slf4j
@Component @Component
@RequiredArgsConstructor @RequiredArgsConstructor
public class AuthGlobalFilter implements GlobalFilter, Ordered { public class AuthGlobalFilter implements GlobalFilter, Ordered {
private final WebClient.Builder webClientBuilder; private final WebClient.Builder webClientBuilder;
@Override @Autowired
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { private GatewayPropertiesConfiguration gatewayPropertiesConfiguration;
return null;
} private final AntPathMatcher antPathMatcher = new AntPathMatcher();
private final ObjectMapper objectMapper = new ObjectMapper();
/**
* 不需要认证的路径
*/
private static final String[] DEFAULT_WHITE_LIST = {
"/api/v1/auth/login",
"/api/v1/auth/register",
"/api/v1/auth/refresh",
"/api/v1/user/info"
};
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
String path = request.getURI().getPath();
log.info("Auth filter processing request: {}", path);
// 检查是否在白名单中
if (isWhiteListPath(path)) {
log.info("Path {} is in whitelist, skip authentication", path);
return chain.filter(exchange);
}
// 获取Authorization头
String authHeader = request.getHeaders().getFirst(HttpHeaders.AUTHORIZATION);
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
log.warn("No valid authorization header found for path: {}", path);
return handleUnauthorized(exchange);
}
String token = authHeader.substring(7);
// 调用auth服务验证token
return validateToken(token)
.flatMap(isValid -> {
if (isValid) {
log.info("Token validation successful for path: {}", path);
return chain.filter(exchange);
} else {
log.warn("Token validation failed for path: {}", path);
return handleUnauthorized(exchange);
}
})
.onErrorResume(throwable -> {
log.error("Token validation error for path: {}", path, throwable);
return handleUnauthorized(exchange);
});
}
/**
* 检查路径是否在白名单中
*/
private boolean isWhiteListPath(String path) {
// 先检查默认白名单
for (String whitePath : DEFAULT_WHITE_LIST) {
if (antPathMatcher.match(whitePath, path)) {
return true;
}
}
// 检查配置文件中的白名单
if (gatewayPropertiesConfiguration.getWhiteList() != null && !gatewayPropertiesConfiguration.getWhiteList().isEmpty()) {
for (String whitePath : gatewayPropertiesConfiguration.getWhiteList()) {
if (antPathMatcher.match(whitePath, path)) {
return true;
}
}
}
return false;
}
/**
* 调用auth服务验证token
*/
private Mono<Boolean> validateToken(String token) {
return webClientBuilder.build()
.post()
.uri("lb://auth-service/api/v1/auth/validate")
.header(HttpHeaders.AUTHORIZATION, "Bearer " + token)
.contentType(MediaType.APPLICATION_JSON)
.retrieve()
.bodyToMono(String.class)
.map(response -> {
try {
// 解析响应,判断是否有效
Result result = objectMapper.readValue(response, Result.class);
return Objects.equals(result.getCode(), Result.SUCCESS_CODE);
} catch (JsonProcessingException e) {
log.error("Failed to parse auth response", e);
return false;
}
})
.defaultIfEmpty(false);
}
/**
* 处理未授权的请求
*/
private Mono<Void> handleUnauthorized(ServerWebExchange exchange) {
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.UNAUTHORIZED);
response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
Result<Void> result = Results.failure(new RemoteException(ErrorCode.NO_AUTH_ERROR));
String responseBody;
try {
responseBody = objectMapper.writeValueAsString(result);
} catch (JsonProcessingException e) {
responseBody = "{\"code\":401,\"message\":\"Unauthorized\",\"data\":null}";
}
DataBuffer buffer = response.bufferFactory().wrap(responseBody.getBytes(StandardCharsets.UTF_8));
return response.writeWith(Mono.just(buffer));
}
@Override
public int getOrder() {
// 设置较高的优先级,确保在其他过滤器之前执行
return -100;
}
@Override
public int getOrder() {
return 0;
}
} }

View File

@@ -7,13 +7,28 @@ spring:
webflux: webflux:
routes: routes:
- id: auth-service - id: auth-service
uri: lb://auth-service uri: lb://auth-service/api
predicates: predicates:
- Path=/api/v1/auth/** - Path=/api/v1/auth/**
- id: user-service - id: user-service
uri: lb://user-service uri: lb://user-service/api
predicates: predicates:
- Path=/api/v1/user/** - Path=/api/v1/user/**
aioj-backend-gateway:
# 白名单配置
white-list:
- /api/v1/auth/login
- /api/v1/auth/register
- /api/v1/auth/refresh
- /api/v1/user/info
- /api/v1/question/list
- /api/v1/question/detail/**
- /actuator/health
- /swagger-ui/**
- /v3/api-docs/**
- /swagger-resources/**
aioj: aioj:
log: log:
enabled: true enabled: true

View File

@@ -17,4 +17,22 @@
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties> </properties>
<dependencies>
<dependency>
<groupId>cn.meowrain</groupId>
<artifactId>aioj-backend-common-core</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>cn.meowrain</groupId>
<artifactId>aioj-backend-common-mybatis</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!--feign 注解依赖-->
<dependency>
<groupId>cn.meowrain</groupId>
<artifactId>aioj-backend-common-feign</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
</project> </project>

View File

@@ -1,5 +1,7 @@
package cn.meowrain.aioj.backend.upms.api.dto; package cn.meowrain.aioj.backend.upms.api.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.Data; import lombok.Data;
import java.time.LocalDateTime; import java.time.LocalDateTime;
@@ -8,76 +10,76 @@ import java.time.LocalDateTime;
@Schema(description = "日志查询对象") @Schema(description = "日志查询对象")
public class SysLogDTO { public class SysLogDTO {
/** /**
* 编号 * 编号
*/ */
private Long id; private Long id;
/** /**
* 日志类型 * 日志类型
*/ */
@NotBlank(message = "日志类型不能为空") @NotBlank(message = "日志类型不能为空")
private String logType; private String logType;
/** /**
* 日志标题 * 日志标题
*/ */
@NotBlank(message = "日志标题不能为空") @NotBlank(message = "日志标题不能为空")
private String title; private String title;
/** /**
* 创建者 * 创建者
*/ */
private String createBy; private String createBy;
/** /**
* 更新时间 * 更新时间
*/ */
private LocalDateTime updateTime; private LocalDateTime updateTime;
/** /**
* 操作IP地址 * 操作IP地址
*/ */
private String remoteAddr; private String remoteAddr;
/** /**
* 用户代理 * 用户代理
*/ */
private String userAgent; private String userAgent;
/** /**
* 请求URI * 请求URI
*/ */
private String requestUri; private String requestUri;
/** /**
* 操作方式 * 操作方式
*/ */
private String method; private String method;
/** /**
* 操作提交的数据 * 操作提交的数据
*/ */
private String params; private String params;
/** /**
* 执行时间 * 执行时间
*/ */
private Long time; private Long time;
/** /**
* 异常信息 * 异常信息
*/ */
private String exception; private String exception;
/** /**
* 服务ID * 服务ID
*/ */
private String serviceId; private String serviceId;
/** /**
* 创建时间区间 [开始时间,结束时间] * 创建时间区间 [开始时间,结束时间]
*/ */
private LocalDateTime[] createTime; private LocalDateTime[] createTime;
} }

View File

@@ -0,0 +1,159 @@
/*
*
* Copyright (c) 2018-2025, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* Neither the name of the pig4cloud.com developer nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
* Author: lengleng (wangiegie@gmail.com)
*
*/
package cn.meowrain.aioj.backend.upms.api.entity;
import com.baomidou.mybatisplus.annotation.*;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* <p>
* 日志表
* </p>
*
* @author lengleng
* @since 2017-11-20
*/
@Data
@Schema(description = "日志")
public class SysLog implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 编号
*/
@TableId(type = IdType.ASSIGN_ID)
// @ExcelProperty("日志编号")
@Schema(description = "日志编号")
private Long id;
/**
* 日志类型
*/
@NotBlank(message = "日志类型不能为空")
// @ExcelProperty("日志类型0-正常 9-错误)")
@Schema(description = "日志类型")
private String logType;
/**
* 日志标题
*/
@NotBlank(message = "日志标题不能为空")
// @ExcelProperty("日志标题")
@Schema(description = "日志标题")
private String title;
/**
* 创建者
*/
// @ExcelProperty("创建人")
@TableField(fill = FieldFill.INSERT)
@Schema(description = "创建人")
private String createBy;
/**
* 创建时间
*/
// @ExcelProperty("创建时间")
@TableField(fill = FieldFill.INSERT)
@Schema(description = "创建时间")
private LocalDateTime createTime;
/**
* 更新时间
*/
// @ExcelIgnore
@TableField(fill = FieldFill.UPDATE)
@Schema(description = "更新时间")
private LocalDateTime updateTime;
/**
* 操作IP地址
*/
// @ExcelProperty("操作ip地址")
@Schema(description = "操作ip地址")
private String remoteAddr;
/**
* 用户代理
*/
@Schema(description = "用户代理")
private String userAgent;
/**
* 请求URI
*/
// @ExcelProperty("浏览器")
@Schema(description = "请求uri")
private String requestUri;
/**
* 操作方式
*/
// @ExcelProperty("操作方式")
@Schema(description = "操作方式")
private String method;
/**
* 操作提交的数据
*/
// @ExcelProperty("提交数据")
@Schema(description = "提交数据")
private String params;
/**
* 执行时间
*/
// @ExcelProperty("执行时间")
@Schema(description = "方法执行时间")
private Long time;
/**
* 异常信息
*/
// @ExcelProperty("异常信息")
@Schema(description = "异常信息")
private String exception;
/**
* 服务ID
*/
// @ExcelProperty("应用标识")
@Schema(description = "应用标识")
private String serviceId;
/**
* 删除标记
*/
@TableLogic
// @ExcelIgnore
@TableField(fill = FieldFill.INSERT)
@Schema(description = "删除标记,1:已删除,0:正常")
private String delFlag;
}

View File

@@ -0,0 +1,21 @@
package cn.meowrain.aioj.backend.upms.api.feign;
import cn.meowrain.aioj.backend.framework.core.constants.ServiceNameConstants;
import cn.meowrain.aioj.backend.framework.core.web.Result;
import cn.meowrain.aioj.backend.framework.feign.annotation.NoToken;
import cn.meowrain.aioj.backend.upms.api.entity.SysLog;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
@FeignClient(contextId = "remoteLogService", value = ServiceNameConstants.UPMS_SERVICE)
public interface RemoteLogService {
/**
* 保存日志 (异步多线程调用无token)
* @param sysLog 日志实体
* @return succes、false
*/
@NoToken
@PostMapping("/log/save")
Result<Boolean> saveLog(@RequestBody SysLog sysLog);
}

View File

@@ -0,0 +1 @@
cn.meowrain.aioj.backend.upms.api.feign.RemoteLogService

View File

@@ -17,4 +17,20 @@
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties> </properties>
<dependencies>
<dependency>
<groupId>cn.meowrain</groupId>
<artifactId>aioj-backend-common-mybatis</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>cn.meowrain</groupId>
<artifactId>aioj-backend-upms-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
</project> </project>

View File

@@ -0,0 +1,11 @@
package cn.meowrain.aioj.backend.upms.biz;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class AIOJAdminApplication {
public static void main(String[] args) {
SpringApplication.run(AIOJAdminApplication.class, args);
}
}

View File

@@ -0,0 +1,78 @@
package cn.meowrain.aioj.backend.upms.biz.controller;
import cn.hutool.core.collection.CollUtil;
import cn.meowrain.aioj.backend.framework.core.web.Result;
import cn.meowrain.aioj.backend.framework.core.web.Results;
import cn.meowrain.aioj.backend.upms.api.dto.SysLogDTO;
import cn.meowrain.aioj.backend.upms.api.entity.SysLog;
import cn.meowrain.aioj.backend.upms.biz.service.SysLogService;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springdoc.core.annotations.ParameterObject;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/log")
@Tag(description = "log", name = "日志管理模块")
@RequiredArgsConstructor
public class SysLogController {
private final SysLogService sysLogService;
/**
* 分页查询系统日志
*
* @param page 分页参数对象
* @param sysLog 系统日志查询条件
* @return 包含分页结果的响应对象
*/
@GetMapping("/page")
@Operation(summary = "分页查询系统日志", description = "分页查询系统日志")
public Result<Page> getLogPage(@ParameterObject Page page, @ParameterObject SysLogDTO sysLog) {
return Results.success(sysLogService.getLogPage(page, sysLog));
}
/**
* 批量删除日志
*
* @param ids 要删除的日志ID数组
* @return 操作结果成功返回success失败返回false
*/
@DeleteMapping
// @HasPermission("sys_log_del")
@Operation(summary = "批量删除日志", description = "批量删除日志")
public Result<Boolean> removeByIds(@RequestBody Long[] ids) {
return Results.success(sysLogService.removeBatchByIds(CollUtil.toList(ids)));
}
/**
* 保存日志
*
* @param sysLog 日志实体
* @return 操作结果成功返回success失败返回false
*/
@PostMapping("/save")
@Operation(summary = "保存日志", description = "保存日志")
public Result<Boolean> saveLog(@Valid @RequestBody SysLog sysLog) {
return Results.success(sysLogService.saveLog(sysLog));
}
/**
* 导出系统日志到Excel表格
* @param sysLog 系统日志查询条件DTO
* @return 符合查询条件的系统日志列表
*/
// @ResponseExcel
// @GetMapping("/export")
// @HasPermission("sys_log_export")
// @Operation(summary = "导出系统日志到Excel表格", description = "导出系统日志到Excel表格")
// public List<SysLog> exportLogs(SysLogDTO sysLog) {
// return sysLogService.listLogs(sysLog);
// }
}

View File

@@ -0,0 +1,12 @@
package cn.meowrain.aioj.backend.upms.biz.mapper;
import cn.meowrain.aioj.backend.upms.api.entity.SysLog;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
/**
* 系统日志表 Mapper 接口
*/
@Mapper
public interface SysLogMapper extends BaseMapper<SysLog> {
}

View File

@@ -0,0 +1,35 @@
package cn.meowrain.aioj.backend.upms.biz.service;
import cn.meowrain.aioj.backend.upms.api.dto.SysLogDTO;
import cn.meowrain.aioj.backend.upms.api.entity.SysLog;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService;
import java.util.List;
public interface SysLogService extends IService<SysLog> {
/**
* 分页查询系统日志
*
* @param page 分页对象
* @param sysLog 系统日志
* @return 系统日志分页数据
*/
Page getLogPage(Page page, SysLogDTO sysLog);
/**
* 保存日志
*
* @param sysLog 日志实体
* @return Boolean
*/
Boolean saveLog(SysLog sysLog);
/**
* 查询日志列表
*
* @param sysLog 查询条件
* @return 日志列表
*/
List<SysLog> listLogs(SysLogDTO sysLog);
}

View File

@@ -0,0 +1,75 @@
package cn.meowrain.aioj.backend.upms.biz.service.impl;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.StrUtil;
import cn.meowrain.aioj.backend.upms.api.dto.SysLogDTO;
import cn.meowrain.aioj.backend.upms.api.entity.SysLog;
import cn.meowrain.aioj.backend.upms.biz.mapper.SysLogMapper;
import cn.meowrain.aioj.backend.upms.biz.service.SysLogService;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
public class SysLogServiceImpl extends ServiceImpl<SysLogMapper, SysLog> implements SysLogService {
/**
* 分页查询系统日志
*
* @param page 分页参数
* @param sysLog 日志查询条件
* @return 分页结果
*/
@Override
public Page getLogPage(Page page, SysLogDTO sysLog) {
return baseMapper.selectPage(page, buildQuery(sysLog));
}
/**
* 保存日志
*
* @param sysLog 日志对象
* @return 保存成功返回true
*/
@Override
@Transactional(rollbackFor = Exception.class)
public Boolean saveLog(SysLog sysLog) {
baseMapper.insert(sysLog);
return Boolean.TRUE;
}
/**
* 查询日志列表
*
* @param sysLog 查询条件DTO对象
* @return 日志列表
*/
@Override
public List<SysLog> listLogs(SysLogDTO sysLog) {
return baseMapper.selectList(buildQuery(sysLog));
}
/**
* 构建查询条件
*
* @param sysLog 前端查询条件DTO
* @return 构建好的LambdaQueryWrapper对象
*/
private LambdaQueryWrapper buildQuery(SysLogDTO sysLog) {
LambdaQueryWrapper<SysLog> wrapper = Wrappers.lambdaQuery();
if (StrUtil.isNotBlank(sysLog.getLogType())) {
wrapper.eq(SysLog::getLogType, sysLog.getLogType());
}
if (ArrayUtil.isNotEmpty(sysLog.getCreateTime())) {
wrapper.ge(SysLog::getCreateTime, sysLog.getCreateTime()[0])
.le(SysLog::getCreateTime, sysLog.getCreateTime()[1]);
}
return wrapper;
}
}

View File

@@ -0,0 +1,19 @@
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

View File

@@ -0,0 +1,11 @@
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_prod
username: root
password: root

View File

@@ -0,0 +1,11 @@
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_test
username: root
password: 123456

View File

@@ -0,0 +1,35 @@
spring:
application:
name: user-service
profiles:
active: @env@
server:
port: 10012
servlet:
context-path: /api
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: '/**'
packages-to-scan: cn.meowrain.aioj.backend.userservice.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
aioj:
log:
enabled: true
max-length: 20000

View File

@@ -23,15 +23,20 @@
<artifactId>aioj-backend-common-starter</artifactId> <artifactId>aioj-backend-common-starter</artifactId>
<version>1.0-SNAPSHOT</version> <version>1.0-SNAPSHOT</version>
</dependency> </dependency>
<dependency>
<groupId>cn.meowrain</groupId>
<artifactId>aioj-backend-common-mybatis</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId> <artifactId>spring-boot-starter-web</artifactId>
</dependency> </dependency>
<!-- https://mvnrepository.com/artifact/com.github.xiaoymin/knife4j-openapi3-jakarta-spring-boot-starter -->
<dependency> <dependency>
<groupId>com.github.xiaoymin</groupId> <groupId>cn.hutool</groupId>
<artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId> <artifactId>hutool-crypto</artifactId>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId> <artifactId>spring-boot-starter-data-redis</artifactId>
@@ -65,5 +70,11 @@
<groupId>com.alibaba.cloud</groupId> <groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency> </dependency>
<dependency>
<groupId>cn.meowrain</groupId>
<artifactId>aioj-backend-common-core</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies> </dependencies>
</project> </project>

View File

@@ -7,7 +7,9 @@ import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication @SpringBootApplication
@MapperScan("cn.meowrain.aioj.backend.userservice.dao.mapper") @MapperScan("cn.meowrain.aioj.backend.userservice.dao.mapper")
public class UserServiceApplication { public class UserServiceApplication {
public static void main(String[] args) {
SpringApplication.run(UserServiceApplication.class, args); public static void main(String[] args) {
} SpringApplication.run(UserServiceApplication.class, args);
}
} }

View File

@@ -5,20 +5,22 @@ import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor @RequiredArgsConstructor
public enum ChainMarkEnums { public enum ChainMarkEnums {
/**
* 用户注册请求验证
*/
USER_REGISTER_REQ_PARAM_VERIFY("USER_REGISTER_REQ_PARAM_VERIFY"),
/**
* 用户登录请求验证
*/
USER_LOGIN_REQ_PARAM_VERIFY("USER_LOGIN_REQ_PARAM_VERIFY");
@Getter /**
private final String markName; * 用户注册请求验证
*/
USER_REGISTER_REQ_PARAM_VERIFY("USER_REGISTER_REQ_PARAM_VERIFY"),
/**
* 用户登录请求验证
*/
USER_LOGIN_REQ_PARAM_VERIFY("USER_LOGIN_REQ_PARAM_VERIFY");
@Getter
private final String markName;
@Override
public String toString() {
return markName;
}
@Override
public String toString() {
return markName;
}
} }

View File

@@ -5,10 +5,7 @@ import org.springframework.context.annotation.ComponentScans;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
@Configuration @Configuration
@ComponentScans({ @ComponentScans({ @ComponentScan("cn.meowrain.aioj.backend.framework.core.banner") })
@ComponentScan("cn.meowrain.aioj.backend.framework.core.banner")
})
public class FrameworkConfiguration { public class FrameworkConfiguration {
} }

View File

@@ -16,23 +16,25 @@ import org.springframework.context.annotation.Configuration;
@Configuration @Configuration
@EnableKnife4j @EnableKnife4j
public class SwaggerConfiguration implements ApplicationRunner { public class SwaggerConfiguration implements ApplicationRunner {
@Value("${server.port:8080}")
private String serverPort;
@Value("${server.servlet.context-path:}")
private String contextPath;
@Bean @Value("${server.port:8080}")
public OpenAPI customerOpenAPI() { private String serverPort;
return new OpenAPI()
.info(new Info() @Value("${server.servlet.context-path:}")
.title("AIOJ-用户微服务✨") private String contextPath;
.description("用户注册,用户登录等功能")
.version("v1.0.0") @Bean
.contact(new Contact().name("meowrain").email("meowrain@126.com")) public OpenAPI customerOpenAPI() {
.license(new License().name("MeowRain").url("https://meowrain.cn"))); return new OpenAPI().info(new Info().title("AIOJ-用户微服务✨")
} .description("用户注册,用户登录等功能")
@Override .version("v1.0.0")
public void run(ApplicationArguments args) throws Exception { .contact(new Contact().name("meowrain").email("meowrain@126.com"))
log.info("✨API Document: http://127.0.0.1:{}{}/doc.html", serverPort, contextPath); .license(new License().name("MeowRain").url("https://meowrain.cn")));
} }
@Override
public void run(ApplicationArguments args) throws Exception {
log.info("✨API Document: http://127.0.0.1:{}{}/doc.html", serverPort, contextPath);
}
} }

View File

@@ -13,25 +13,24 @@ import org.springframework.web.bind.annotation.*;
@RequestMapping("/v1/user") @RequestMapping("/v1/user")
public class UserController { public class UserController {
private final UserService userService; private final UserService userService;
@PostMapping("/register")
public Result<Long> register(@RequestBody UserRegisterRequestDTO userRegisterRequest) {
Long l = userService.userRegister(userRegisterRequest);
return Results.success(l);
}
@PostMapping("/register") @GetMapping("/inner/get-by-username")
public Result<Long> register(@RequestBody UserRegisterRequestDTO userRegisterRequest) { public Result<UserAuthRespDTO> getUserByUserName(@RequestParam("userAccount") String userAccount) {
Long l = userService.userRegister(userRegisterRequest); UserAuthRespDTO userAuthDTO = userService.findAuthInfoByUserAccount(userAccount);
return Results.success(l); return Results.success(userAuthDTO);
} }
@GetMapping("/inner/get-by-username") @GetMapping("/inner/get-by-userid")
public Result<UserAuthRespDTO> getUserByUserName(@RequestParam("userAccount") String userAccount) { public Result<UserAuthRespDTO> getUserById(@RequestParam("userId") String userid) {
UserAuthRespDTO userAuthDTO = userService.findAuthInfoByUserAccount(userAccount); UserAuthRespDTO userAuthRespDTO = userService.findAuthInfoByUserId(userid);
return Results.success(userAuthDTO); return Results.success(userAuthRespDTO);
} }
@GetMapping("/inner/get-by-userid")
public Result<UserAuthRespDTO> getUserById(@RequestParam("userId") String userid) {
UserAuthRespDTO userAuthRespDTO = userService.findAuthInfoByUserId(userid);
return Results.success(userAuthRespDTO);
}
} }

View File

@@ -12,69 +12,70 @@ import java.util.Date;
@Accessors(chain = true) @Accessors(chain = true)
public class User implements Serializable { public class User implements Serializable {
/** /**
* id * id
*/ */
@TableId(type = IdType.ASSIGN_ID) @TableId(type = IdType.ASSIGN_ID)
private Long id; private Long id;
/** /**
* 用户账号 * 用户账号
*/ */
private String userAccount; private String userAccount;
/** /**
* 用户密码 * 用户密码
*/ */
private String userPassword; private String userPassword;
/** /**
* 开放平台id * 开放平台id
*/ */
private String unionId; private String unionId;
/** /**
* 公众号openId * 公众号openId
*/ */
private String mpOpenId; private String mpOpenId;
/** /**
* 用户昵称 * 用户昵称
*/ */
private String userName; private String userName;
/** /**
* 用户头像 * 用户头像
*/ */
private String userAvatar; private String userAvatar;
/** /**
* 用户简介 * 用户简介
*/ */
private String userProfile; private String userProfile;
/** /**
* 用户角色user/admin/ban * 用户角色user/admin/ban
*/ */
private String userRole; private String userRole;
/** /**
* 创建时间 * 创建时间
*/ */
@TableField(fill = FieldFill.INSERT) @TableField(fill = FieldFill.INSERT)
private Date createTime; private Date createTime;
/** /**
* 更新时间 * 更新时间
*/ */
private Date updateTime; private Date updateTime;
/** /**
* 是否删除 * 是否删除
*/ */
@TableLogic @TableLogic
private Integer isDelete; private Integer isDelete;
@TableField(exist = false)
private static final long serialVersionUID = 1L;
@TableField(exist = false)
private static final long serialVersionUID = 1L;
} }

View File

@@ -4,4 +4,5 @@ import cn.meowrain.aioj.backend.userservice.dao.entity.User;
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper;
public interface UserMapper extends BaseMapper<User> { public interface UserMapper extends BaseMapper<User> {
} }

View File

@@ -1,8 +1,8 @@
package cn.meowrain.aioj.backend.userservice.dto.chains; package cn.meowrain.aioj.backend.userservice.dto.chains;
import cn.meowrain.aioj.backend.framework.designpattern.chains.AbstractChianHandler; import cn.meowrain.aioj.backend.framework.core.designpattern.chains.AbstractChianHandler;
import cn.meowrain.aioj.backend.framework.errorcode.ErrorCode; import cn.meowrain.aioj.backend.framework.core.errorcode.ErrorCode;
import cn.meowrain.aioj.backend.framework.exception.ClientException; import cn.meowrain.aioj.backend.framework.core.exception.ClientException;
import cn.meowrain.aioj.backend.userservice.common.enums.ChainMarkEnums; import cn.meowrain.aioj.backend.userservice.common.enums.ChainMarkEnums;
import cn.meowrain.aioj.backend.userservice.dto.req.UserRegisterRequestDTO; import cn.meowrain.aioj.backend.userservice.dto.req.UserRegisterRequestDTO;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@@ -12,32 +12,34 @@ import org.springframework.stereotype.Component;
@Component @Component
@Slf4j @Slf4j
public class UserRegisterRequestParamVerifyChain implements AbstractChianHandler<UserRegisterRequestDTO> { public class UserRegisterRequestParamVerifyChain implements AbstractChianHandler<UserRegisterRequestDTO> {
@Override
public void handle(UserRegisterRequestDTO requestParam) {
// 校验参数里面用户名和密码是不是空的
if (StringUtils.isAnyBlank(requestParam.getUserAccount(), requestParam.getUserPassword())) {
throw new ClientException("参数为空", ErrorCode.PARAMS_ERROR);
}
if (requestParam.getUserAccount().length() < 4) {
throw new ClientException("账号长度不小于4位", ErrorCode.PARAMS_ERROR);
}
if (requestParam.getUserPassword().length() < 8) {
throw new ClientException("密码长度不小于8位", ErrorCode.PARAMS_ERROR);
}
// 密码和校验密码相同
if (!requestParam.getUserPassword().equals(requestParam.getCheckPassword())) {
throw new ClientException("两次输入的密码不一致", ErrorCode.PARAMS_ERROR);
}
} @Override
public void handle(UserRegisterRequestDTO requestParam) {
// 校验参数里面用户名和密码是不是空的
if (StringUtils.isAnyBlank(requestParam.getUserAccount(), requestParam.getUserPassword())) {
throw new ClientException("参数为空", ErrorCode.PARAMS_ERROR);
}
if (requestParam.getUserAccount().length() < 4) {
throw new ClientException("账号长度不小于4位", ErrorCode.PARAMS_ERROR);
}
if (requestParam.getUserPassword().length() < 8) {
throw new ClientException("密码长度不小于8位", ErrorCode.PARAMS_ERROR);
}
// 密码和校验密码相同
if (!requestParam.getUserPassword().equals(requestParam.getCheckPassword())) {
throw new ClientException("两次输入的密码不一致", ErrorCode.PARAMS_ERROR);
}
@Override }
public String mark() {
return ChainMarkEnums.USER_REGISTER_REQ_PARAM_VERIFY.getMarkName(); @Override
} public String mark() {
return ChainMarkEnums.USER_REGISTER_REQ_PARAM_VERIFY.getMarkName();
}
@Override
public int getOrder() {
return 10;
}
@Override
public int getOrder() {
return 10;
}
} }

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