From ab574032c0f71ad9b16e086e52cce8961cc9640e Mon Sep 17 00:00:00 2001 From: Kevin987 <2920370144@qq.com> Date: Thu, 5 Feb 2026 16:27:23 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AF=B9=E8=B1=A1=E5=AD=98=E5=82=A8-=E5=88=9D?= =?UTF-8?q?=E5=A7=8B=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 10 + .../java/com/blog/aspect/CommonAspect.java | 35 ++++ src/main/java/com/blog/aspect/UserAspect.java | 28 ++- src/main/java/com/blog/common/User.java | 12 ++ .../java/com/blog/common/UserSession.java | 12 ++ .../blog/common/constant/UserConstant.java | 27 +++ .../message/error/UserErrorMessage.java | 7 + .../message/success/SpaceSuccessMessage.java | 6 + .../message/success/UserSuccessMessage.java | 7 + .../config/AuthorizationServerConfig.java | 12 +- .../java/com/blog/config/MinioConfig.java | 34 ++++ .../com/blog/config/MybatisPlusConfig.java | 19 ++ .../java/com/blog/config/PipelineConfig.java | 25 +++ .../java/com/blog/config/SwaggerConfig.java | 21 +++ .../java/com/blog/context/GlobalContext.java | 11 +- .../java/com/blog/context/RequestContext.java | 18 ++ .../blog/context/RequestImplicitContext.java | 27 +++ .../blog/controller/ArticleController.java | 16 ++ .../com/blog/controller/SpaceController.java | 33 ++++ .../com/blog/controller/UserController.java | 56 +++++- src/main/java/com/blog/dto/UserLoginDto.java | 5 +- .../java/com/blog/dto/UserRegisterDto.java | 5 - src/main/java/com/blog/entity/UserEntity.java | 42 ----- .../blog/filter/JwtAuthenticationFilter.java | 64 +++++++ .../com/blog/filter/TokenBucketFilter.java | 1 + src/main/java/com/blog/helper/UserHelper.java | 45 ++++- .../holder/RequestImplicitContextHolder.java | 82 +++++++++ .../GlobalContextInitializer.java} | 4 +- .../blog/initializer/MinioInitializer.java | 78 ++++++++ .../blog/interceptor/IpLimitInterceptor.java | 5 +- src/main/java/com/blog/mapper/UserMapper.java | 9 - .../com/blog/pipeline/RegisterPipeline.java | 40 ++++ .../java/com/blog/processor/Processor.java | 8 + .../processor/user/PathbuildProcessor.java | 58 ++++++ .../java/com/blog/service/UserService.java | 14 -- .../blog/service/impl/UserServiceImpl.java | 76 -------- .../java/com/blog/task/GlobalCleanupTask.java | 12 ++ .../UserValidator.java | 21 ++- src/main/resources/application.yaml | 13 +- .../db/migration/V1__create_table.sql | 174 +++++++++++------- 40 files changed, 935 insertions(+), 237 deletions(-) create mode 100644 src/main/java/com/blog/aspect/CommonAspect.java create mode 100644 src/main/java/com/blog/common/User.java create mode 100644 src/main/java/com/blog/common/UserSession.java create mode 100644 src/main/java/com/blog/common/constant/UserConstant.java create mode 100644 src/main/java/com/blog/common/constant/message/error/UserErrorMessage.java create mode 100644 src/main/java/com/blog/common/constant/message/success/SpaceSuccessMessage.java create mode 100644 src/main/java/com/blog/common/constant/message/success/UserSuccessMessage.java create mode 100644 src/main/java/com/blog/config/MinioConfig.java create mode 100644 src/main/java/com/blog/config/MybatisPlusConfig.java create mode 100644 src/main/java/com/blog/config/PipelineConfig.java create mode 100644 src/main/java/com/blog/config/SwaggerConfig.java create mode 100644 src/main/java/com/blog/context/RequestContext.java create mode 100644 src/main/java/com/blog/context/RequestImplicitContext.java create mode 100644 src/main/java/com/blog/controller/ArticleController.java create mode 100644 src/main/java/com/blog/controller/SpaceController.java delete mode 100644 src/main/java/com/blog/entity/UserEntity.java create mode 100644 src/main/java/com/blog/filter/JwtAuthenticationFilter.java create mode 100644 src/main/java/com/blog/holder/RequestImplicitContextHolder.java rename src/main/java/com/blog/{initialization/GlobalContextInitialization.java => initializer/GlobalContextInitializer.java} (82%) create mode 100644 src/main/java/com/blog/initializer/MinioInitializer.java delete mode 100644 src/main/java/com/blog/mapper/UserMapper.java create mode 100644 src/main/java/com/blog/pipeline/RegisterPipeline.java create mode 100644 src/main/java/com/blog/processor/Processor.java create mode 100644 src/main/java/com/blog/processor/user/PathbuildProcessor.java delete mode 100644 src/main/java/com/blog/service/UserService.java delete mode 100644 src/main/java/com/blog/service/impl/UserServiceImpl.java rename src/main/java/com/blog/{vaildator => validator}/UserValidator.java (61%) diff --git a/pom.xml b/pom.xml index 81d94f5..ebbb6ef 100644 --- a/pom.xml +++ b/pom.xml @@ -15,6 +15,16 @@ + + io.minio + minio + 8.5.7 + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + 2.3.0 + org.springframework.boot spring-boot-starter-web diff --git a/src/main/java/com/blog/aspect/CommonAspect.java b/src/main/java/com/blog/aspect/CommonAspect.java new file mode 100644 index 0000000..0e9701d --- /dev/null +++ b/src/main/java/com/blog/aspect/CommonAspect.java @@ -0,0 +1,35 @@ +package com.blog.aspect; + +import com.blog.common.User; +import com.blog.common.constant.UserConstant; +import com.blog.holder.RequestImplicitContextHolder; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.springframework.core.annotation.Order; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; + +@Aspect +@Component +public class CommonAspect { + + @Order(100) + @Around("execution(* com.blog.controller.*.*(..))"+ + "&& !within(com.blog.controller.GlobalExceptionHandler)") + public Object buildRequestImplicitContext(ProceedingJoinPoint joinPoint){ + try { + Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal(); + if(principal!=null && !principal.equals(UserConstant.ANONYMOUS_USER) && principal instanceof User){ + User currentUser = (User) principal; + RequestImplicitContextHolder.setUserId(currentUser.getUserId()); + RequestImplicitContextHolder.setUsername(currentUser.getUsername()); + } + return joinPoint.proceed(); + } catch (Throwable e) { + throw new RuntimeException(e); + } finally { + RequestImplicitContextHolder.clear(); + } + } +} diff --git a/src/main/java/com/blog/aspect/UserAspect.java b/src/main/java/com/blog/aspect/UserAspect.java index 776e195..88d3518 100644 --- a/src/main/java/com/blog/aspect/UserAspect.java +++ b/src/main/java/com/blog/aspect/UserAspect.java @@ -2,11 +2,13 @@ package com.blog.aspect; import com.blog.common.Captcha; import com.blog.common.R; +import com.blog.dto.UserLoginDto; import com.blog.dto.UserRegisterDto; import com.blog.holder.GlobalContextHolder; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; +import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; import java.time.Instant; @@ -15,7 +17,9 @@ import java.time.Instant; @Component public class UserAspect { - @Around("execution(* com.blog.controller.UserController.register(..))") + @Order(101) + @Around("execution(* com.blog.controller.UserController.register(..))"+ + "|| execution(* com.blog.controller.UserController.login(..))") public Object registerBefore(ProceedingJoinPoint joinPoint) { Object[] args = joinPoint.getArgs(); for (Object arg : args) { @@ -26,7 +30,27 @@ public class UserAspect { if(captcha.getTtl()< Instant.now().getEpochSecond()){ return R.err("验证码已过期"); } - if(!captcha.getText().equals(userRegisterDto.getVerificationCode())){ + if(!captcha.getText().equalsIgnoreCase(userRegisterDto.getVerificationCode())){ + return R.err("无效的验证码"); + } + try { + GlobalContextHolder.getGlobalContext().getCaptchas().remove(userRegisterDto.getKey()); + Object result = joinPoint.proceed(); + return result; + } catch (Throwable e) { + throw new RuntimeException(e); + } + } + return R.err("无效的验证码"); + } + if(arg instanceof UserLoginDto){ + UserLoginDto userRegisterDto = (UserLoginDto)arg; + Captcha captcha = GlobalContextHolder.getGlobalContext().getCaptchas().get(userRegisterDto.getKey()); + if(captcha != null){ + if(captcha.getTtl()< Instant.now().getEpochSecond()){ + return R.err("验证码已过期"); + } + if(!captcha.getText().equalsIgnoreCase(userRegisterDto.getVerificationCode())){ return R.err("无效的验证码"); } try { diff --git a/src/main/java/com/blog/common/User.java b/src/main/java/com/blog/common/User.java new file mode 100644 index 0000000..16a1caf --- /dev/null +++ b/src/main/java/com/blog/common/User.java @@ -0,0 +1,12 @@ +package com.blog.common; + +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class User { + + private Long userId; + private String username; +} diff --git a/src/main/java/com/blog/common/UserSession.java b/src/main/java/com/blog/common/UserSession.java new file mode 100644 index 0000000..18f79a6 --- /dev/null +++ b/src/main/java/com/blog/common/UserSession.java @@ -0,0 +1,12 @@ +package com.blog.common; + +import lombok.Builder; +import lombok.Data; + +@Builder +@Data +public class UserSession { + + private Long userId; + private Long ttl; +} diff --git a/src/main/java/com/blog/common/constant/UserConstant.java b/src/main/java/com/blog/common/constant/UserConstant.java new file mode 100644 index 0000000..953e46e --- /dev/null +++ b/src/main/java/com/blog/common/constant/UserConstant.java @@ -0,0 +1,27 @@ +package com.blog.common.constant; + +public class UserConstant { + + // 刷新头 + public static final String REFRESH_TOKEN = "refresh_token"; + + // 匿名用户 + public static final String ANONYMOUS_USER = "anonymousUser"; + + // 权限头 + public static final String AUTHORIZATION = "Authorization"; + + // 权限类型 + public static final String BEARER = "Bearer"; + + // 用户id + public static final String USER_ID = "userId"; + + public static final String NICKNAME = "nickname"; + + public static final String ACCESS_TOKENS = "accessToken"; + + public static final String REFRESH_TOKENS = "refreshToken"; + public static final String TOKEN_TYPES = "tokenType"; + public static final String USERNAME = "username"; +} diff --git a/src/main/java/com/blog/common/constant/message/error/UserErrorMessage.java b/src/main/java/com/blog/common/constant/message/error/UserErrorMessage.java new file mode 100644 index 0000000..074d50b --- /dev/null +++ b/src/main/java/com/blog/common/constant/message/error/UserErrorMessage.java @@ -0,0 +1,7 @@ +package com.blog.common.constant.message.error; + +public class UserErrorMessage { + + public static final String INVALID_REFRESH_TOKEN = "无效的 refresh_token"; + public static final String INVALID_USER = "无效的用户"; +} diff --git a/src/main/java/com/blog/common/constant/message/success/SpaceSuccessMessage.java b/src/main/java/com/blog/common/constant/message/success/SpaceSuccessMessage.java new file mode 100644 index 0000000..d6beb3c --- /dev/null +++ b/src/main/java/com/blog/common/constant/message/success/SpaceSuccessMessage.java @@ -0,0 +1,6 @@ +package com.blog.common.constant.message.success; + +public class SpaceSuccessMessage { + + public static final String MY_SPACE = "获取空间成功"; +} diff --git a/src/main/java/com/blog/common/constant/message/success/UserSuccessMessage.java b/src/main/java/com/blog/common/constant/message/success/UserSuccessMessage.java new file mode 100644 index 0000000..527f40c --- /dev/null +++ b/src/main/java/com/blog/common/constant/message/success/UserSuccessMessage.java @@ -0,0 +1,7 @@ +package com.blog.common.constant.message.success; + +public class UserSuccessMessage { + + public static final String REFRESH_TOKEN_SUCCESS = "刷新令牌成功"; + public static final String GET_ARTICLES_SUCCESS = "获取文章成功"; +} diff --git a/src/main/java/com/blog/config/AuthorizationServerConfig.java b/src/main/java/com/blog/config/AuthorizationServerConfig.java index 43b5776..c6b6598 100644 --- a/src/main/java/com/blog/config/AuthorizationServerConfig.java +++ b/src/main/java/com/blog/config/AuthorizationServerConfig.java @@ -1,6 +1,7 @@ package com.blog.config; import com.blog.common.R; +import com.blog.filter.JwtAuthenticationFilter; import com.blog.filter.TokenBucketFilter; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.beans.factory.annotation.Autowired; @@ -14,6 +15,7 @@ import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.access.intercept.AuthorizationFilter; import org.springframework.security.web.context.SecurityContextHolderFilter; @Configuration @@ -26,6 +28,9 @@ public class AuthorizationServerConfig { @Autowired private TokenBucketFilter tokenBucketFilter; + @Autowired + private JwtAuthenticationFilter jwtAuthenticationFilter; + @Bean public PasswordEncoder passwordEncoder() { // 默认使用 BCrypt,它自带随机盐机制 @@ -36,11 +41,16 @@ public class AuthorizationServerConfig { public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .addFilterBefore(tokenBucketFilter, SecurityContextHolderFilter.class) + .addFilterBefore(jwtAuthenticationFilter, AuthorizationFilter.class) .authorizeHttpRequests(auth -> auth .requestMatchers("/user/login", "/user/captcha/**", "/user/exist/**", - "/user/register").permitAll() + "/user/register", + "/user/refreshToken", + "/swagger-ui/**", + "/v3/api-docs/**", + "/doc").permitAll() .anyRequest().authenticated() ) .formLogin(AbstractHttpConfigurer::disable) diff --git a/src/main/java/com/blog/config/MinioConfig.java b/src/main/java/com/blog/config/MinioConfig.java new file mode 100644 index 0000000..22245ef --- /dev/null +++ b/src/main/java/com/blog/config/MinioConfig.java @@ -0,0 +1,34 @@ +package com.blog.config; + +import io.minio.MinioClient; +import lombok.Data; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +@Data +public class MinioConfig { + @Value("${minio.endpoint}") + private String endpoint; + @Value("${minio.accessKey}") + private String accessKey; + @Value("${minio.secretKey}") + private String secretKey; + @Value("${minio.bucketName}") + private String bucketName; + + @Value("${minio.defaultBucketName}") + private String defaultBucketName; + + @Value("${minio.defaultPublicBucketName}") + private String defaultPublicBucketName; + + @Bean + public MinioClient minioClient() { + return MinioClient.builder() + .endpoint(endpoint) + .credentials(accessKey, secretKey) + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/com/blog/config/MybatisPlusConfig.java b/src/main/java/com/blog/config/MybatisPlusConfig.java new file mode 100644 index 0000000..fe89bcf --- /dev/null +++ b/src/main/java/com/blog/config/MybatisPlusConfig.java @@ -0,0 +1,19 @@ +package com.blog.config; + +import com.baomidou.mybatisplus.annotation.DbType; +import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; +import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class MybatisPlusConfig { + + @Bean + public MybatisPlusInterceptor mybatisPlusInterceptor() { + MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); + interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); // 如果配置多个插件, 切记分页最后添加 + // 如果有多数据源可以不配具体类型, 否则都建议配上具体的 DbType + return interceptor; + } +} diff --git a/src/main/java/com/blog/config/PipelineConfig.java b/src/main/java/com/blog/config/PipelineConfig.java new file mode 100644 index 0000000..3ca9e6a --- /dev/null +++ b/src/main/java/com/blog/config/PipelineConfig.java @@ -0,0 +1,25 @@ +package com.blog.config; + +import com.blog.pipeline.RegisterPipeline; +import com.blog.processor.user.PathbuildProcessor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.transaction.support.TransactionTemplate; + +import java.util.Arrays; + +@Configuration +public class PipelineConfig { + + @Autowired + private TransactionTemplate transactionTemplate; + + @Bean + public RegisterPipeline registerPipeline(PathbuildProcessor pathbuildProcessor) { + return RegisterPipeline.builder() + .transactionTemplate(transactionTemplate) + .processList(Arrays.asList(pathbuildProcessor)) + .build(); + } +} diff --git a/src/main/java/com/blog/config/SwaggerConfig.java b/src/main/java/com/blog/config/SwaggerConfig.java new file mode 100644 index 0000000..24cc791 --- /dev/null +++ b/src/main/java/com/blog/config/SwaggerConfig.java @@ -0,0 +1,21 @@ +package com.blog.config; + +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.info.License; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class SwaggerConfig { + + @Bean + public OpenAPI blogOpenAPI() { + return new OpenAPI() + .info(new Info() + .title("Blog 系统 API 文档") + .description("基于 Spring Boot 3 的博客管理系统接口") + .version("v1.0.0") + .license(new License().name("Apache 2.0"))); + } +} diff --git a/src/main/java/com/blog/context/GlobalContext.java b/src/main/java/com/blog/context/GlobalContext.java index 9b2264c..5e7edde 100644 --- a/src/main/java/com/blog/context/GlobalContext.java +++ b/src/main/java/com/blog/context/GlobalContext.java @@ -2,6 +2,7 @@ package com.blog.context; import com.blog.common.Captcha; import com.blog.common.CaptchaLimit; +import com.blog.common.UserSession; import com.google.common.util.concurrent.RateLimiter; import lombok.Data; import org.springframework.stereotype.Component; @@ -16,12 +17,18 @@ public class GlobalContext { // 全局限流:每秒允许 10 个请求 private final RateLimiter globalLimiter = RateLimiter.create(100.0); - private Map captchas; + // 验证码池 + private final Map captchas; - private Map ipLimitPool; + // ip池 + private final Map ipLimitPool; + + // userId映射map + private final Map userSessionMap; public GlobalContext() { captchas = new ConcurrentHashMap<>(); ipLimitPool = new ConcurrentHashMap<>(); + userSessionMap = new ConcurrentHashMap<>(); } } diff --git a/src/main/java/com/blog/context/RequestContext.java b/src/main/java/com/blog/context/RequestContext.java new file mode 100644 index 0000000..1391c80 --- /dev/null +++ b/src/main/java/com/blog/context/RequestContext.java @@ -0,0 +1,18 @@ +package com.blog.context; + +import lombok.Builder; +import lombok.Data; + +import java.util.Map; + +@Data +@Builder +public class RequestContext { + + Map params; + + public Object getReqValue(String key) { + return params.get(key); + } + +} diff --git a/src/main/java/com/blog/context/RequestImplicitContext.java b/src/main/java/com/blog/context/RequestImplicitContext.java new file mode 100644 index 0000000..9fcd4a4 --- /dev/null +++ b/src/main/java/com/blog/context/RequestImplicitContext.java @@ -0,0 +1,27 @@ +package com.blog.context; + +import lombok.Builder; +import lombok.Data; + +import java.util.List; +import java.util.Map; + +@Data +@Builder +public class RequestImplicitContext { + + private String traceId; + + private Long userId; + private String username; + + private Map resContextMap; + + private Map reqContextMap; + + private List errorMessages; + + private List warningMessages; + + private Object data; +} diff --git a/src/main/java/com/blog/controller/ArticleController.java b/src/main/java/com/blog/controller/ArticleController.java new file mode 100644 index 0000000..d284b08 --- /dev/null +++ b/src/main/java/com/blog/controller/ArticleController.java @@ -0,0 +1,16 @@ +package com.blog.controller; + +import com.blog.service.ArticleService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/article") +public class ArticleController { + + @Autowired + private ArticleService articleService; + + +} diff --git a/src/main/java/com/blog/controller/SpaceController.java b/src/main/java/com/blog/controller/SpaceController.java new file mode 100644 index 0000000..626e9c7 --- /dev/null +++ b/src/main/java/com/blog/controller/SpaceController.java @@ -0,0 +1,33 @@ +package com.blog.controller; + +import com.blog.common.R; +import com.blog.common.constant.message.success.SpaceSuccessMessage; +import com.blog.holder.RequestImplicitContextHolder; +import com.blog.service.FileService; +import com.blog.service.PathService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/space") +public class SpaceController { + + @Autowired + private FileService fileService; + + @Autowired + private PathService pathService; + + @PreAuthorize("isAuthenticated()") + @GetMapping("") + public R mySpace() { + + if (pathService.listPath(RequestImplicitContextHolder.getUserId())) { + return R.ok(SpaceSuccessMessage.MY_SPACE,RequestImplicitContextHolder.getData()); + } + return R.err(RequestImplicitContextHolder.popErrorMessage()); + } + + +} diff --git a/src/main/java/com/blog/controller/UserController.java b/src/main/java/com/blog/controller/UserController.java index d67fd9f..181aa4e 100644 --- a/src/main/java/com/blog/controller/UserController.java +++ b/src/main/java/com/blog/controller/UserController.java @@ -1,18 +1,29 @@ package com.blog.controller; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.blog.common.Captcha; import com.blog.common.R; +import com.blog.common.constant.UserConstant; +import com.blog.common.constant.message.success.UserSuccessMessage; +import com.blog.context.RequestContext; import com.blog.dto.UserLoginDto; import com.blog.dto.UserRegisterDto; import com.blog.holder.GlobalContextHolder; +import com.blog.holder.RequestImplicitContextHolder; +import com.blog.service.ArticleService; import com.blog.service.UserService; import com.wf.captcha.GifCaptcha; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; import java.time.Instant; +import java.util.Map; +@Tag(name = "用户管理", description = "用户相关接口") @RestController @RequestMapping("/user") public class UserController { @@ -20,12 +31,37 @@ public class UserController { @Autowired private UserService userService; + @Autowired + private ArticleService articleService; + + @Operation(summary = "注册", description = "注册新用户") @PostMapping("/register") - public R register(@RequestBody @Valid UserRegisterDto userDto) { - userService.register(userDto); - return login(userDto); + public T register(@RequestBody @Valid UserRegisterDto userDto) { + String errMsg = userService.register(userDto); + if(errMsg != null){ + return (T) R.err(errMsg); + } + if(RequestImplicitContextHolder.errorCount()>0){ + return (T) R.err(RequestImplicitContextHolder.popErrorMessage()); + } + return (T) login(userDto); + } + @Operation(summary = "我的文章", description = "获取当前用户的文章") + @PreAuthorize("isAuthenticated()") + @GetMapping("/article") + public R myarticles(@RequestParam("current") int currentPage, @RequestParam("size") int pageSize) { + if(pageSize==0||pageSize>20){ + pageSize=20; + } + Long userId = RequestImplicitContextHolder.getUserId(); + if (articleService.getarticles(userId,Page.of(currentPage,pageSize))){ + return R.ok(UserSuccessMessage.GET_ARTICLES_SUCCESS,RequestImplicitContextHolder.getData()); + } + return R.err(RequestImplicitContextHolder.popErrorMessage()); } + + @Operation(summary = "用户名是否存在", description = "判断用户是否存在") @GetMapping("/exist/{username}") public R exist(@PathVariable("username") String username) { boolean exist = userService.exist(username); @@ -35,11 +71,24 @@ public class UserController { return R.err("用户不存在"); } + + @PreAuthorize("isAuthenticated()") @GetMapping("/test") public R test() { return R.ok("test"); } + @Operation(summary = "刷新令牌", description = "刷新令牌") + @PostMapping("/refreshToken") + public R refreshToken(@RequestBody String refreshToken) { + RequestContext requestContext = RequestContext.builder().params(Map.of(UserConstant.REFRESH_TOKEN, refreshToken)).build(); + if (userService.refreshToken(requestContext)) { + return R.ok(UserSuccessMessage.REFRESH_TOKEN_SUCCESS,RequestImplicitContextHolder.getData()); + } + return R.err(RequestImplicitContextHolder.popErrorMessage()); + } + + @Operation(summary = "登录", description = "登录") @PostMapping("/login") public R login(@Valid @RequestBody UserLoginDto user) { Object res = userService.login(user); @@ -49,6 +98,7 @@ public class UserController { return R.ok("登录成功", res); } + @Operation(summary = "获取验证码", description = "获取验证码") @GetMapping("/captcha/{captchaId}") public R captcha(@PathVariable("captchaId") String captchaId) throws Exception { if (captchaId.isBlank() || captchaId.length() > 36) { diff --git a/src/main/java/com/blog/dto/UserLoginDto.java b/src/main/java/com/blog/dto/UserLoginDto.java index 4930ed4..a5191b8 100644 --- a/src/main/java/com/blog/dto/UserLoginDto.java +++ b/src/main/java/com/blog/dto/UserLoginDto.java @@ -13,6 +13,9 @@ public class UserLoginDto { @NotBlank(message = "密码不能为空") @Size(min = 6, max = 32, message = "密码长度必须在 6 到 32 位之间") private String password; + @NotBlank(message = "验证码不能为空") + private String verificationCode; - + @NotBlank(message = "key 不能为空") + private String key; } diff --git a/src/main/java/com/blog/dto/UserRegisterDto.java b/src/main/java/com/blog/dto/UserRegisterDto.java index feb8810..b404df6 100644 --- a/src/main/java/com/blog/dto/UserRegisterDto.java +++ b/src/main/java/com/blog/dto/UserRegisterDto.java @@ -14,9 +14,4 @@ public class UserRegisterDto extends UserLoginDto { @NotBlank(message = "邀请码不能为空") private String inviteCode; - @NotBlank(message = "验证码不能为空") - private String verificationCode; - - @NotBlank(message = "key 不能为空") - private String key; } diff --git a/src/main/java/com/blog/entity/UserEntity.java b/src/main/java/com/blog/entity/UserEntity.java deleted file mode 100644 index 6eb1b65..0000000 --- a/src/main/java/com/blog/entity/UserEntity.java +++ /dev/null @@ -1,42 +0,0 @@ -package com.blog.entity; - -import com.baomidou.mybatisplus.annotation.IdType; -import com.baomidou.mybatisplus.annotation.TableId; -import com.baomidou.mybatisplus.annotation.TableLogic; -import com.baomidou.mybatisplus.annotation.TableName; -import com.blog.dto.UserRegisterDto; -import lombok.Builder; -import lombok.Data; - -import java.time.LocalDateTime; - -@Builder -@Data -@TableName("sys_user") -public class UserEntity { - @TableId(type = IdType.ASSIGN_ID) - private Long id; - private String username; - private String password; - private Integer enabled; - private Integer gender; - private String inviteCodeId; - private String nickname; - private String email; - private String phone; - private String avatar; - private LocalDateTime releaseDate; - private LocalDateTime createTime; - private LocalDateTime updateTime; - @TableLogic // 开启 MP 的逻辑删除支持 - private Integer isDeleted; - - public static UserEntity castFromRegisterDto(UserRegisterDto userDto, String password) { - return UserEntity.builder() - .username(userDto.getUsername()) - .password(password) - .email(userDto.getEmail()) - .inviteCodeId(userDto.getInviteCode()) - .build(); - } -} diff --git a/src/main/java/com/blog/filter/JwtAuthenticationFilter.java b/src/main/java/com/blog/filter/JwtAuthenticationFilter.java new file mode 100644 index 0000000..4e6403e --- /dev/null +++ b/src/main/java/com/blog/filter/JwtAuthenticationFilter.java @@ -0,0 +1,64 @@ +package com.blog.filter; + +import com.blog.common.User; +import com.blog.common.UserSession; +import com.blog.common.constant.UserConstant; +import com.blog.helper.UserHelper; +import com.blog.holder.GlobalContextHolder; +import io.jsonwebtoken.Claims; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; +import java.util.ArrayList; + +@Component +public class JwtAuthenticationFilter extends OncePerRequestFilter { + + @Autowired + private UserHelper userHelper; // 假设你有一个处理 JWT 的工具类 + + @Override + protected void doFilterInternal(HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { + + // 1. 从 Header 中获取 Authorization + String authHeader = request.getHeader(UserConstant.AUTHORIZATION); + + // 2. 校验格式 (匹配你前端传来的 tokenType + accessToken) + if (authHeader == null || !authHeader.startsWith(UserConstant.BEARER+" ")) { + filterChain.doFilter(request, response); + return; + } + + // 3. 截取真正的 Token 字符串 + String jwt = authHeader.substring(7); + + try { + // 4. 解析 JWT (此处可结合你的令牌桶或缓存逻辑) + if (userHelper.validateToken(jwt)) { + Claims claims = userHelper.parseToken(jwt); + UserSession session = GlobalContextHolder.getGlobalContext().getUserSessionMap().get(claims.get(UserConstant.USER_ID).toString()); + // 5. 构建认证对象并存入上下文 + UsernamePasswordAuthenticationToken authentication = + new UsernamePasswordAuthenticationToken(User.builder().userId(session.getUserId()).username(claims.get(UserConstant.USERNAME).toString()).build(), null, new ArrayList<>()); + + SecurityContextHolder.getContext().setAuthentication(authentication); + } + } catch (Exception e) { + // Token 过期或非法,清理上下文 + SecurityContextHolder.clearContext(); + } + + // 6. 继续执行后面的过滤器 + filterChain.doFilter(request, response); + } +} diff --git a/src/main/java/com/blog/filter/TokenBucketFilter.java b/src/main/java/com/blog/filter/TokenBucketFilter.java index 1c011cd..8208e69 100644 --- a/src/main/java/com/blog/filter/TokenBucketFilter.java +++ b/src/main/java/com/blog/filter/TokenBucketFilter.java @@ -41,5 +41,6 @@ public class TokenBucketFilter extends OncePerRequestFilter { response.setContentType("application/json;charset=UTF-8"); // 使用内置 JSON 或直接写字符串 response.getWriter().write(objectMapper.writeValueAsString(R.err("系统繁忙,请稍后再试"))); + response.getWriter().flush(); } } diff --git a/src/main/java/com/blog/helper/UserHelper.java b/src/main/java/com/blog/helper/UserHelper.java index d16965f..e1d6e03 100644 --- a/src/main/java/com/blog/helper/UserHelper.java +++ b/src/main/java/com/blog/helper/UserHelper.java @@ -1,35 +1,42 @@ package com.blog.helper; +import com.blog.common.UserSession; +import com.blog.common.constant.UserConstant; import com.blog.entity.UserEntity; +import com.blog.holder.GlobalContextHolder; +import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import io.jsonwebtoken.security.Keys; -import org.springframework.stereotype.Service; +import org.springframework.stereotype.Component; import java.nio.charset.StandardCharsets; import java.security.Key; +import java.time.Instant; import java.util.Date; import java.util.HashMap; import java.util.Map; +import java.util.UUID; -@Service +@Component public class UserHelper { public Map obtainJWT(UserEntity userEntity) { + String userId = UUID.randomUUID().toString().replaceAll("-", ""); + GlobalContextHolder.getGlobalContext().getUserSessionMap().put(userId, UserSession.builder().userId(userEntity.getId()).ttl(Instant.now().getEpochSecond()+(2*60*60)).build()); Map claims = new HashMap<>(); - claims.put("userId", userEntity.getId()); - claims.put("username", userEntity.getUsername()); - claims.put("nickname", userEntity.getNickname()); + claims.put(UserConstant.USER_ID, userId); + claims.put(UserConstant.USERNAME, userEntity.getUsername()); String accessToken = createToken(claims, 2*60); // 3. 签发 Refresh Token (有效期长,如 7 天) // 提示:Refresh Token 通常可以只存一个 userId,减少体积 - String refreshToken = createToken(Map.of("userId", userEntity.getId()), 1 * 24 * 60); + String refreshToken = createToken(Map.of(UserConstant.USERNAME, userEntity.getUsername()), 6 * 60); Map tokens = Map.of( - "accessToken", accessToken, - "refreshToken", refreshToken, - "tokenType", "Bearer" + UserConstant.ACCESS_TOKENS, accessToken, + UserConstant.REFRESH_TOKENS, refreshToken, + UserConstant.TOKEN_TYPES, UserConstant.BEARER ); return tokens; @@ -48,4 +55,24 @@ public class UserHelper { .signWith(KEY, SignatureAlgorithm.HS256) .compact(); } + + public Claims parseToken(String token) { + return Jwts.parserBuilder() + .setSigningKey(KEY) + .build() + .parseClaimsJws(token) + .getBody(); + } + + /** + * 校验 Token 是否有效 + */ + public boolean validateToken(String token) { + try { + Claims claims = parseToken(token); + return !claims.getExpiration().before(new Date()); + } catch (Exception e) { + return false; + } + } } diff --git a/src/main/java/com/blog/holder/RequestImplicitContextHolder.java b/src/main/java/com/blog/holder/RequestImplicitContextHolder.java new file mode 100644 index 0000000..0d83585 --- /dev/null +++ b/src/main/java/com/blog/holder/RequestImplicitContextHolder.java @@ -0,0 +1,82 @@ +package com.blog.holder; + +import com.blog.context.RequestImplicitContext; + +import java.util.ArrayList; +import java.util.concurrent.ConcurrentHashMap; + +public class RequestImplicitContextHolder { + + private static final ThreadLocal CONTEXT = ThreadLocal.withInitial(() -> RequestImplicitContext.builder() + .resContextMap(new ConcurrentHashMap<>()) + .resContextMap(new ConcurrentHashMap<>()) + .errorMessages(new ArrayList<>()) + .warningMessages(new ArrayList<>()) + .build()); + + public static void setReq(String key, Object value) { + CONTEXT.get().getReqContextMap().put(key, value); + } + + public static void pushErrorMessage(String errorMessage) { + CONTEXT.get().getErrorMessages().add(errorMessage); + } + public static void pushWarningMessage(String warningMessage) { + CONTEXT.get().getWarningMessages().add(warningMessage); + } + public static void setData(Object data) { + CONTEXT.get().setData(data); + } + public static Object getData() { + return CONTEXT.get().getData(); + } + public static String popErrorMessage() { + return CONTEXT.get().getErrorMessages().get(0); + } + public static int errorCount() { + return CONTEXT.get().getErrorMessages().size(); + } + public static int warningCount() { + return CONTEXT.get().getWarningMessages().size(); + } + + public static void setUsername(String username) { + CONTEXT.get().setUsername(username); + } + public static String getUsername() { + return CONTEXT.get().getUsername(); + } + + /** + * 获取数据 + */ + public static Object getReq(String key) { + return CONTEXT.get().getReqContextMap().get(key); + } + + public static void setRes(String key, Object value) { + CONTEXT.get().getResContextMap().put(key, value); + } + + /** + * 获取数据 + */ + public static Object getRes(String key) { + return CONTEXT.get().getResContextMap().get(key); + } + + public static Long getUserId() { + return CONTEXT.get().getUserId(); + } + + public static void setUserId(Long userId) { + CONTEXT.get().setUserId(userId); + } + + /** + * 彻底清除,防止线程池内存泄漏(核心步骤) + */ + public static void clear() { + CONTEXT.remove(); + } +} diff --git a/src/main/java/com/blog/initialization/GlobalContextInitialization.java b/src/main/java/com/blog/initializer/GlobalContextInitializer.java similarity index 82% rename from src/main/java/com/blog/initialization/GlobalContextInitialization.java rename to src/main/java/com/blog/initializer/GlobalContextInitializer.java index cd7b0ea..8b7a127 100644 --- a/src/main/java/com/blog/initialization/GlobalContextInitialization.java +++ b/src/main/java/com/blog/initializer/GlobalContextInitializer.java @@ -1,4 +1,4 @@ -package com.blog.initialization; +package com.blog.initializer; import com.blog.context.GlobalContext; import com.blog.holder.GlobalContextHolder; @@ -6,7 +6,7 @@ import jakarta.annotation.PostConstruct; import org.springframework.stereotype.Component; @Component -public class GlobalContextInitialization { +public class GlobalContextInitializer { @PostConstruct public void initGlobalContext(){ diff --git a/src/main/java/com/blog/initializer/MinioInitializer.java b/src/main/java/com/blog/initializer/MinioInitializer.java new file mode 100644 index 0000000..de529fe --- /dev/null +++ b/src/main/java/com/blog/initializer/MinioInitializer.java @@ -0,0 +1,78 @@ +package com.blog.initializer; + +import com.blog.config.MinioConfig; +import io.minio.BucketExistsArgs; +import io.minio.MakeBucketArgs; +import io.minio.MinioClient; +import io.minio.SetBucketPolicyArgs; +import io.minio.errors.*; +import jakarta.annotation.PostConstruct; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; + +@Component +public class MinioInitializer { + + private final List buckets; + + @Autowired + private MinioClient minioClient; + + @Autowired + private MinioConfig minioConfig; + + public MinioInitializer() { + buckets = new ArrayList<>(); + } + + @PostConstruct + public void init() { + buckets.add(minioConfig.getBucketName()); + buckets.add(minioConfig.getDefaultBucketName()); + buckets.add(minioConfig.getDefaultPublicBucketName()); + try { + for (String bucket : buckets) { + boolean exists; + exists = minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucket).build()); + if(!exists) { + minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucket).build()); + } + } + setBucketPublicReadOnly(minioConfig.getDefaultPublicBucketName()); + } catch (Exception e) { + throw new RuntimeException("MinIO 桶初始化失败", e); + } + + } + + public void setBucketPublicReadOnly(String bucketName) { + // 构造 JSON 策略字符串 + // 注意:Resource 部分必须包含 "arn:aws:s3:::桶名" 和 "arn:aws:s3:::桶名/*" + String policy = "{\n" + + " \"Version\": \"2012-10-17\",\n" + + " \"Statement\": [\n" + + " {\n" + + " \"Effect\": \"Allow\",\n" + + " \"Principal\": {\"AWS\": [\"*\"]},\n" + + " \"Action\": [\"s3:GetBucketLocation\", \"s3:GetObject\"],\n" + + " \"Resource\": [\"arn:aws:s3:::" + bucketName + "\", \"arn:aws:s3:::" + bucketName + "/*\"]\n" + + " }\n" + + " ]\n" + + "}"; + + try { + minioClient.setBucketPolicy( + SetBucketPolicyArgs.builder() + .bucket(bucketName) + .config(policy) + .build() + ); + System.out.println("桶策略已更新:公开只读"); + } catch (Exception e) { + throw new RuntimeException("配置桶策略失败", e); + } + } +} diff --git a/src/main/java/com/blog/interceptor/IpLimitInterceptor.java b/src/main/java/com/blog/interceptor/IpLimitInterceptor.java index b9ea47f..c87f4ef 100644 --- a/src/main/java/com/blog/interceptor/IpLimitInterceptor.java +++ b/src/main/java/com/blog/interceptor/IpLimitInterceptor.java @@ -38,7 +38,7 @@ public class IpLimitInterceptor implements HandlerInterceptor { // --- 允许访问:更新上下文 --- // 使用简单的对象封装或直接存 Long 均可 if (limit == null||limit.getTtl() { -} diff --git a/src/main/java/com/blog/pipeline/RegisterPipeline.java b/src/main/java/com/blog/pipeline/RegisterPipeline.java new file mode 100644 index 0000000..c1eef76 --- /dev/null +++ b/src/main/java/com/blog/pipeline/RegisterPipeline.java @@ -0,0 +1,40 @@ +package com.blog.pipeline; + +import com.blog.dto.UserRegisterDto; +import com.blog.processor.Processor; +import lombok.Builder; +import org.springframework.transaction.support.TransactionTemplate; + +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; + +@Builder +public class RegisterPipeline { + + private TransactionTemplate transactionTemplate; + + private List> processList; + + public boolean start(UserRegisterDto userRegisterDto) { + AtomicBoolean result = new AtomicBoolean(false); + transactionTemplate.execute(status -> { + try { + for (Processor processor : processList) { + boolean processed = processor.process(userRegisterDto); + if (!processed) { + status.setRollbackOnly(); + result.set(false); + return false; + } + } + result.set(true); + return true; + }catch (Exception e){ + e.printStackTrace(); + result.set(false); + return false; + } + }); + return result.get(); + } +} diff --git a/src/main/java/com/blog/processor/Processor.java b/src/main/java/com/blog/processor/Processor.java new file mode 100644 index 0000000..6e81a2b --- /dev/null +++ b/src/main/java/com/blog/processor/Processor.java @@ -0,0 +1,8 @@ +package com.blog.processor; + +public interface Processor { + + boolean process(T t); + + boolean rollback(T t); +} diff --git a/src/main/java/com/blog/processor/user/PathbuildProcessor.java b/src/main/java/com/blog/processor/user/PathbuildProcessor.java new file mode 100644 index 0000000..19bef81 --- /dev/null +++ b/src/main/java/com/blog/processor/user/PathbuildProcessor.java @@ -0,0 +1,58 @@ +package com.blog.processor.user; + +import com.blog.config.MinioConfig; +import com.blog.dto.UserRegisterDto; +import com.blog.entity.PathEntity; +import com.blog.holder.RequestImplicitContextHolder; +import com.blog.mapper.PathMapper; +import com.blog.processor.Processor; +import io.minio.MinioClient; +import io.minio.PutObjectArgs; +import io.minio.errors.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.io.ByteArrayInputStream; + +@Component +public class PathbuildProcessor implements Processor { + + @Autowired + private MinioClient minioClient; + + @Autowired + private PathMapper pathMapper; + + @Autowired + private MinioConfig minioConfig; + + + @Override + public boolean process(UserRegisterDto userRegisterDto) { + + PathEntity pathEntity = PathEntity.builder() + .userId(RequestImplicitContextHolder.getUserId()) + .pathName(userRegisterDto.getUsername()) + .build(); + pathMapper.insert(pathEntity); + try { + minioClient.putObject( + PutObjectArgs.builder() + .bucket(minioConfig.getBucketName()) + .object(userRegisterDto.getUsername()) + .stream(new ByteArrayInputStream(new byte[] {}), 0, -1) + .build() + ); + } catch (Exception e) { + RequestImplicitContextHolder.pushErrorMessage(e.getMessage()); + throw new RuntimeException(e); + } + return true; + + } + + @Override + public boolean rollback(UserRegisterDto userRegisterDto) { + return false; + } +} diff --git a/src/main/java/com/blog/service/UserService.java b/src/main/java/com/blog/service/UserService.java deleted file mode 100644 index 4bbb4f8..0000000 --- a/src/main/java/com/blog/service/UserService.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.blog.service; - -import com.blog.dto.UserLoginDto; -import com.blog.dto.UserRegisterDto; -import jakarta.validation.Valid; - -public interface UserService { - - T login(UserLoginDto user); - - String register(UserRegisterDto userDto); - - boolean exist(String username); -} diff --git a/src/main/java/com/blog/service/impl/UserServiceImpl.java b/src/main/java/com/blog/service/impl/UserServiceImpl.java deleted file mode 100644 index f7e5ec7..0000000 --- a/src/main/java/com/blog/service/impl/UserServiceImpl.java +++ /dev/null @@ -1,76 +0,0 @@ -package com.blog.service.impl; - -import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; -import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; -import com.blog.dto.UserLoginDto; -import com.blog.dto.UserRegisterDto; -import com.blog.entity.UserEntity; -import com.blog.helper.UserHelper; -import com.blog.mapper.UserMapper; -import com.blog.service.UserService; -import com.blog.utils.ChartsGenerator; -import com.blog.vaildator.UserValidator; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.stereotype.Service; -import org.springframework.transaction.support.TransactionTemplate; - - -@Service -public class UserServiceImpl extends ServiceImpl implements UserService { - - @Autowired - private UserMapper userMapper; - - @Autowired - private UserHelper userHelper; - - @Autowired - private UserValidator userValidator; - - @Autowired - private PasswordEncoder passwordEncoder; - - @Autowired - private TransactionTemplate transactionTemplate; - - - @Override - public T login(UserLoginDto user) { - LambdaQueryWrapper lambdaQueryWrapper = new LambdaQueryWrapper<>(); - lambdaQueryWrapper.eq(UserEntity::getUsername, user.getUsername()); - UserEntity userEntity = userMapper.selectOne(lambdaQueryWrapper); - String errMsg = userValidator.validateUser(user,userEntity); - if(errMsg!=null) return (T) errMsg; - return (T) userHelper.obtainJWT(userEntity); - } - - @Override - public String register(UserRegisterDto userDto) { - LambdaQueryWrapper lambdaQueryWrapper = new LambdaQueryWrapper<>(); - lambdaQueryWrapper.eq(UserEntity::getUsername, userDto.getUsername()); - UserEntity userEntity = userMapper.selectOne(lambdaQueryWrapper); - String errMsg = userValidator.validateRegister(userEntity); - if (errMsg != null) return errMsg; - UserEntity newUser = UserEntity.castFromRegisterDto(userDto,passwordEncoder.encode(userDto.getPassword())); - newUser.setNickname("游客"+ ChartsGenerator.generateCode()); - transactionTemplate.execute(status -> { - try { - userMapper.insert(newUser); - return true; - } catch (Exception e) { - e.printStackTrace(); - status.setRollbackOnly(); // 显式标记回滚 - return false; - } - }); - return null; - } - - @Override - public boolean exist(String username) { - UserEntity userEntity = userMapper.selectOne(new LambdaQueryWrapper().eq(UserEntity::getUsername, username)); - if (userEntity == null) return false; - return true; - } -} diff --git a/src/main/java/com/blog/task/GlobalCleanupTask.java b/src/main/java/com/blog/task/GlobalCleanupTask.java index de06c0e..e73e3b8 100644 --- a/src/main/java/com/blog/task/GlobalCleanupTask.java +++ b/src/main/java/com/blog/task/GlobalCleanupTask.java @@ -2,6 +2,7 @@ package com.blog.task; import com.blog.common.Captcha; import com.blog.common.CaptchaLimit; +import com.blog.common.UserSession; import com.blog.holder.GlobalContextHolder; import org.springframework.scheduling.annotation.Async; import org.springframework.scheduling.annotation.Scheduled; @@ -34,4 +35,15 @@ public class GlobalCleanupTask { } }); } + + @Async + @Scheduled(fixedRate = 60000) + public void purgeOnline() { + Map ipLimitPool = GlobalContextHolder.getGlobalContext().getUserSessionMap(); + ipLimitPool.forEach((k,v)->{ + if(v.getTtl()< Instant.now().getEpochSecond()){ + ipLimitPool.remove(k); + } + }); + } } diff --git a/src/main/java/com/blog/vaildator/UserValidator.java b/src/main/java/com/blog/validator/UserValidator.java similarity index 61% rename from src/main/java/com/blog/vaildator/UserValidator.java rename to src/main/java/com/blog/validator/UserValidator.java index 147ae18..04d1407 100644 --- a/src/main/java/com/blog/vaildator/UserValidator.java +++ b/src/main/java/com/blog/validator/UserValidator.java @@ -1,15 +1,18 @@ -package com.blog.vaildator; +package com.blog.validator; import com.blog.dto.UserLoginDto; +import com.blog.entity.InviteCodeEntity; import com.blog.entity.UserEntity; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Component; -import java.time.LocalDateTime; +import java.time.Instant; @Component public class UserValidator { + + @Autowired private PasswordEncoder passwordEncoder; public String validateUser(UserLoginDto user, UserEntity userEntity) { @@ -21,16 +24,26 @@ public class UserValidator { return "密码错误"; } if (userEntity.getEnabled() == 0) return "账号已被禁用"; - if (userEntity.getReleaseDate() != null && userEntity.getReleaseDate().isAfter(LocalDateTime.now())) { + if (userEntity.getReleaseDate() != null && userEntity.getReleaseDate().toInstant().isAfter(Instant.now())) { return ("账号封禁中,解禁日期:" + userEntity.getReleaseDate()); } return null; } - public String validateRegister(UserEntity userEntity) { + public String validateRegister(UserEntity userEntity, InviteCodeEntity inviteCodeEntity) { if(userEntity!=null){ return "用户名已存在"; } + + if(inviteCodeEntity==null){ + return "无效的邀请码"; + } + if(inviteCodeEntity.getExpireTime()!=null&&inviteCodeEntity.getExpireTime().toInstant().isBefore(Instant.now())){ + return "邀请码已过期"; + } + if(inviteCodeEntity.getMaxUses()<=inviteCodeEntity.getUsedCount()){ + return "邀请码使用次数已用尽"; + } return null; } } diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index be3145c..fca1d0c 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -21,9 +21,20 @@ logging: level: org.springframework.security: DEBUG com.blog.mapper: DEBUG + mybatis-plus: mapper-locations: classpath:/mapper/**.xml configuration: local-cache-scope: statement cache-enabled: false - log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl \ No newline at end of file + log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl +springdoc: + swagger-ui: + path: /doc +minio: + endpoint: http://127.0.0.1:9000 + accessKey: minioadmin + secretKey: minioadmin + bucketName: blog-files + defaultBucketName: private + defaultPublicBucketName: public \ No newline at end of file diff --git a/src/main/resources/db/migration/V1__create_table.sql b/src/main/resources/db/migration/V1__create_table.sql index c9bf1db..4e3d11f 100644 --- a/src/main/resources/db/migration/V1__create_table.sql +++ b/src/main/resources/db/migration/V1__create_table.sql @@ -1,85 +1,126 @@ -- 1. 用户表 (包含你要求的: 密码>6位、邀请码、状态、解禁日期等) CREATE TABLE `sys_user` ( - `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键', - `username` VARCHAR(64) NOT NULL COMMENT '用户名', - `password` VARCHAR(255) NOT NULL COMMENT '密码(BCrypt加密)', - `nickname` VARCHAR(64) NOT NULL COMMENT '用户昵称', - `email` VARCHAR(128) NOT NULL COMMENT '邮箱', - `phone` VARCHAR(20) DEFAULT NULL COMMENT '手机号', - `avatar` VARCHAR(255) DEFAULT NULL COMMENT '头像URL', - `gender` TINYINT DEFAULT 0 COMMENT '性别: 1男, 2女, 0未知', - `enabled` TINYINT(1) DEFAULT 1 COMMENT '状态: 1启用, 0禁用', - `invite_code_id` VARCHAR(64) NOT NULL COMMENT '使用的邀请码ID', - `release_date` DATETIME DEFAULT NULL COMMENT '解禁日期', - `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '注册时间', - `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后修改时间', - `is_deleted` TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除标记:0-未删, 1-已删', - PRIMARY KEY (`id`), - UNIQUE KEY `uk_username` (`username`) + `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键', + `username` VARCHAR(64) NOT NULL COMMENT '用户名', + `password` VARCHAR(255) NOT NULL COMMENT '密码(BCrypt加密)', + `nickname` VARCHAR(64) NOT NULL COMMENT '用户昵称', + `email` VARCHAR(128) NOT NULL COMMENT '邮箱', + `phone` VARCHAR(20) DEFAULT NULL COMMENT '手机号', + `avatar` VARCHAR(255) DEFAULT NULL COMMENT '头像URL', + `gender` TINYINT DEFAULT 0 COMMENT '性别: 1男, 2女, 0未知', + `enabled` TINYINT(1) DEFAULT 1 COMMENT '状态: 1启用, 0禁用', + `invite_code_id` BIGINT NOT NULL COMMENT '使用的邀请码ID', + `release_date` DATETIME DEFAULT NULL COMMENT '解禁日期', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '注册时间', + `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后修改时间', + `is_deleted` TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除标记:0-未删, 1-已删', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表'; -- 2. 角色表 CREATE TABLE `sys_role` ( - `id` BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, - `name` VARCHAR(32) NOT NULL COMMENT '角色名称(如:超级管理员)', - `code` VARCHAR(32) NOT NULL COMMENT '角色标识(如:ROLE_ADMIN)', - `description` VARCHAR(255) DEFAULT NULL COMMENT '描述', - `status` TINYINT NOT NULL DEFAULT 1 COMMENT '状态(1:正常, 0:禁用)', - `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - UNIQUE KEY `uk_code` (`code`) + `id` BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, + `name` VARCHAR(32) NOT NULL COMMENT '角色名称(如:超级管理员)', + `code` VARCHAR(32) NOT NULL COMMENT '角色标识(如:ROLE_ADMIN)', + `description` VARCHAR(255) DEFAULT NULL COMMENT '描述', + `status` TINYINT NOT NULL DEFAULT 1 COMMENT '状态(1:正常, 0:禁用)', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + UNIQUE KEY `uk_code` (`code`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='角色表'; -- 3. 权限/菜单表 CREATE TABLE `sys_menu` ( - `id` BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, - `parent_id` BIGINT DEFAULT 0 COMMENT '父菜单ID', - `name` VARCHAR(64) NOT NULL COMMENT '权限名称', - `perms` VARCHAR(100) DEFAULT NULL COMMENT '权限标识(如: sys:user:add)', - `type` TINYINT NOT NULL COMMENT '类型(0:目录, 1:菜单, 2:按钮)', - `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP + `id` BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, + `parent_id` BIGINT DEFAULT 0 COMMENT '父菜单ID', + `name` VARCHAR(64) NOT NULL COMMENT '权限名称', + `perms` VARCHAR(100) DEFAULT NULL COMMENT '权限标识(如: sys:user:add)', + `type` TINYINT NOT NULL COMMENT '类型(0:目录, 1:菜单, 2:按钮)', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='权限菜单表'; -- 4. 用户-角色关联表 CREATE TABLE `sys_user_role` ( - `user_id` BIGINT NOT NULL, - `role_id` BIGINT NOT NULL, - PRIMARY KEY (`user_id`, `role_id`) + `user_id` BIGINT NOT NULL, + `role_id` BIGINT NOT NULL, + PRIMARY KEY (`user_id`, `role_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 5. 角色-权限关联表 CREATE TABLE `sys_role_menu` ( - `role_id` BIGINT NOT NULL, - `menu_id` BIGINT NOT NULL, - PRIMARY KEY (`role_id`, `menu_id`) + `role_id` BIGINT NOT NULL, + `menu_id` BIGINT NOT NULL, + PRIMARY KEY (`role_id`, `menu_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 6. 邀请码表 CREATE TABLE `sys_invite_code` ( - `id` BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, - `code` VARCHAR(32) NOT NULL COMMENT '唯一邀请码', - `creator_id` BIGINT DEFAULT NULL COMMENT '生成人ID', - `max_uses` INT NOT NULL DEFAULT 1 COMMENT '最大使用次数', - `used_count` INT NOT NULL DEFAULT 0 COMMENT '已使用次数', - `status` TINYINT NOT NULL DEFAULT 1 COMMENT '1-有效, 0-失效', - `expire_time` DATETIME DEFAULT NULL COMMENT '过期时间', - `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, - UNIQUE KEY `uk_code` (`code`) + `id` BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, + `code` VARCHAR(32) NOT NULL COMMENT '唯一邀请码', + `creator_id` BIGINT DEFAULT NULL COMMENT '生成人ID', + `max_uses` INT NOT NULL DEFAULT 1 COMMENT '最大使用次数', + `used_count` INT NOT NULL DEFAULT 0 COMMENT '已使用次数', + `status` TINYINT NOT NULL DEFAULT 1 COMMENT '1-有效, 0-失效', + `expire_time` DATETIME DEFAULT NULL COMMENT '过期时间', + `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, + UNIQUE KEY `uk_code` (`code`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='邀请码表'; -- 7. 文章表 CREATE TABLE `sys_article` ( - `id` BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, - `title` VARCHAR(255) NOT NULL COMMENT '标题', - `content` LONGTEXT NOT NULL COMMENT '内容', - `author_id` BIGINT NOT NULL COMMENT '作者ID', - `status` TINYINT NOT NULL DEFAULT 0 COMMENT '0-草稿, 1-发布', - `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - `is_deleted` TINYINT NOT NULL DEFAULT 0, - KEY `idx_author` (`author_id`) + `id` BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, + `title` VARCHAR(255) NOT NULL COMMENT '标题', + `content` LONGTEXT NOT NULL COMMENT '内容', + `author_id` BIGINT NOT NULL COMMENT '作者ID', + `status` TINYINT NOT NULL DEFAULT 0 COMMENT '0-草稿, 1-发布, 2下架, 3删除', + `views` BIGINT DEFAULT 0 COMMENT '浏览数', + `collection_count` BIGINT DEFAULT 0 COMMENT '收藏数', + `like_count` BIGINT DEFAULT 0 COMMENT '点赞数', + `downvote_count` BIGINT DEFAULT 0 COMMENT '点踩数', + `release_date` DATETIME COMMENT '发布日期', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `is_deleted` TINYINT NOT NULL DEFAULT 0, + KEY `idx_author` (`author_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='文章表'; +-- 8. 文件元数据映射表 +CREATE TABLE `sys_file` ( + `id` bigint NOT NULL COMMENT '主键ID', + `alias_name` varchar(255) COMMENT '别名', + `file_name` varchar(255) NOT NULL COMMENT '文件原始名称', + `storage_name` varchar(255) NOT NULL COMMENT 'MinIO存储对象名(通常是UUID+后缀)', + `bucket_name` varchar(64) DEFAULT 'public' COMMENT '存储桶名称', + `file_size` bigint DEFAULT NULL COMMENT '文件大小(bytes)', + `file_type` varchar(128) DEFAULT NULL COMMENT '文件MIME类型', + `file_url` varchar(500) DEFAULT NULL COMMENT '访问地址(临时或持久)', + `user_id` BIGINT DEFAULT NULL COMMENT '上传者ID', + `trace_id` varchar(64) DEFAULT NULL COMMENT '关联的请求链路ID', + `is_deleted` tinyint(1) DEFAULT '0' COMMENT '逻辑删除', + `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '上传时间', + PRIMARY KEY (`id`), + INDEX `idx_user_id` (`user_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='文件元数据映射表'; + +-- 9. 文件目录映射表 +CREATE TABLE `sys_path` ( + `id` bigint NOT NULL COMMENT '主键ID', + `path_name` varchar(512) not null COMMENT '目录名', + `user_id` BIGINT not null COMMENT '上传者ID', + `parent_id` bigint COMMENT '父目录ID', + `is_deleted` tinyint(1) DEFAULT '0' COMMENT '逻辑删除', + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='文件目录映射表'; + +-- 10. 目录文件映射表 +CREATE TABLE `sys_path_file` +( + `path_id` bigint NOT NULL COMMENT '目录ID', + `file_id` bigint NOT NULL COMMENT '文件ID', + PRIMARY KEY (`path_id`, `file_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='目录文件映射表'; + -- 初始化管理员角色 INSERT INTO `sys_role` (`name`, `code`, `description`) VALUES ('超级管理员', 'ROLE_ADMIN', '拥有系统所有权限'); @@ -87,16 +128,21 @@ VALUES ('超级管理员', 'ROLE_ADMIN', '拥有系统所有权限'); -- 初始化管理员用户 (密码: admin123) -- 注意: invite_code_id 不能为空,这里填 SYSTEM INSERT INTO `sys_user` ( - `username`, `password`, `nickname`, `email`, `invite_code_id`, `enabled`, `is_deleted` + `username`, `password`, `nickname`, `email`, `invite_code_id`, `enabled`, `is_deleted` ) VALUES ( - 'admin', - 'admin123', - '超级管理员', - 'admin@example.com', - 'SYSTEM', - 1, - 0 - ); + 'admin', + '$2a$10$HoxAaokfbkslPrrBKkKDdOw7eBV/q.5Rj4P81d1h37y92i2Jd/.VW', + '超级管理员', + '2920370144@qq.com', + 0, + 1, + 0 + ); -- 关联管理员与角色 (假设 ID 都是 1) -INSERT INTO `sys_user_role` (`user_id`, `role_id`) VALUES (1, 1); \ No newline at end of file +INSERT INTO `sys_user_role` (`user_id`, `role_id`) VALUES (1, 1); + +INSERT INTO `sys_invite_code` +(`code`, `creator_id`, `max_uses`, `used_count`, `status`, `expire_time`) +VALUES + ('WELCOME2026', 1, 99, 0, 1, NULL); \ No newline at end of file