对象存储-初始化
This commit is contained in:
10
pom.xml
10
pom.xml
@@ -15,6 +15,16 @@
|
|||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
<dependencies>
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.minio</groupId>
|
||||||
|
<artifactId>minio</artifactId>
|
||||||
|
<version>8.5.7</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springdoc</groupId>
|
||||||
|
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
|
||||||
|
<version>2.3.0</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>
|
||||||
|
|||||||
35
src/main/java/com/blog/aspect/CommonAspect.java
Normal file
35
src/main/java/com/blog/aspect/CommonAspect.java
Normal file
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,11 +2,13 @@ package com.blog.aspect;
|
|||||||
|
|
||||||
import com.blog.common.Captcha;
|
import com.blog.common.Captcha;
|
||||||
import com.blog.common.R;
|
import com.blog.common.R;
|
||||||
|
import com.blog.dto.UserLoginDto;
|
||||||
import com.blog.dto.UserRegisterDto;
|
import com.blog.dto.UserRegisterDto;
|
||||||
import com.blog.holder.GlobalContextHolder;
|
import com.blog.holder.GlobalContextHolder;
|
||||||
import org.aspectj.lang.ProceedingJoinPoint;
|
import org.aspectj.lang.ProceedingJoinPoint;
|
||||||
import org.aspectj.lang.annotation.Around;
|
import org.aspectj.lang.annotation.Around;
|
||||||
import org.aspectj.lang.annotation.Aspect;
|
import org.aspectj.lang.annotation.Aspect;
|
||||||
|
import org.springframework.core.annotation.Order;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
@@ -15,7 +17,9 @@ import java.time.Instant;
|
|||||||
@Component
|
@Component
|
||||||
public class UserAspect {
|
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) {
|
public Object registerBefore(ProceedingJoinPoint joinPoint) {
|
||||||
Object[] args = joinPoint.getArgs();
|
Object[] args = joinPoint.getArgs();
|
||||||
for (Object arg : args) {
|
for (Object arg : args) {
|
||||||
@@ -26,7 +30,27 @@ public class UserAspect {
|
|||||||
if(captcha.getTtl()< Instant.now().getEpochSecond()){
|
if(captcha.getTtl()< Instant.now().getEpochSecond()){
|
||||||
return R.err("验证码已过期");
|
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("无效的验证码");
|
return R.err("无效的验证码");
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
|||||||
12
src/main/java/com/blog/common/User.java
Normal file
12
src/main/java/com/blog/common/User.java
Normal file
@@ -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;
|
||||||
|
}
|
||||||
12
src/main/java/com/blog/common/UserSession.java
Normal file
12
src/main/java/com/blog/common/UserSession.java
Normal file
@@ -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;
|
||||||
|
}
|
||||||
27
src/main/java/com/blog/common/constant/UserConstant.java
Normal file
27
src/main/java/com/blog/common/constant/UserConstant.java
Normal file
@@ -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";
|
||||||
|
}
|
||||||
@@ -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 = "无效的用户";
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package com.blog.common.constant.message.success;
|
||||||
|
|
||||||
|
public class SpaceSuccessMessage {
|
||||||
|
|
||||||
|
public static final String MY_SPACE = "获取空间成功";
|
||||||
|
}
|
||||||
@@ -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 = "获取文章成功";
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.blog.config;
|
package com.blog.config;
|
||||||
|
|
||||||
import com.blog.common.R;
|
import com.blog.common.R;
|
||||||
|
import com.blog.filter.JwtAuthenticationFilter;
|
||||||
import com.blog.filter.TokenBucketFilter;
|
import com.blog.filter.TokenBucketFilter;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
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.bcrypt.BCryptPasswordEncoder;
|
||||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
import org.springframework.security.web.SecurityFilterChain;
|
import org.springframework.security.web.SecurityFilterChain;
|
||||||
|
import org.springframework.security.web.access.intercept.AuthorizationFilter;
|
||||||
import org.springframework.security.web.context.SecurityContextHolderFilter;
|
import org.springframework.security.web.context.SecurityContextHolderFilter;
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
@@ -26,6 +28,9 @@ public class AuthorizationServerConfig {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private TokenBucketFilter tokenBucketFilter;
|
private TokenBucketFilter tokenBucketFilter;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private JwtAuthenticationFilter jwtAuthenticationFilter;
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public PasswordEncoder passwordEncoder() {
|
public PasswordEncoder passwordEncoder() {
|
||||||
// 默认使用 BCrypt,它自带随机盐机制
|
// 默认使用 BCrypt,它自带随机盐机制
|
||||||
@@ -36,11 +41,16 @@ public class AuthorizationServerConfig {
|
|||||||
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||||
http
|
http
|
||||||
.addFilterBefore(tokenBucketFilter, SecurityContextHolderFilter.class)
|
.addFilterBefore(tokenBucketFilter, SecurityContextHolderFilter.class)
|
||||||
|
.addFilterBefore(jwtAuthenticationFilter, AuthorizationFilter.class)
|
||||||
.authorizeHttpRequests(auth -> auth
|
.authorizeHttpRequests(auth -> auth
|
||||||
.requestMatchers("/user/login",
|
.requestMatchers("/user/login",
|
||||||
"/user/captcha/**",
|
"/user/captcha/**",
|
||||||
"/user/exist/**",
|
"/user/exist/**",
|
||||||
"/user/register").permitAll()
|
"/user/register",
|
||||||
|
"/user/refreshToken",
|
||||||
|
"/swagger-ui/**",
|
||||||
|
"/v3/api-docs/**",
|
||||||
|
"/doc").permitAll()
|
||||||
.anyRequest().authenticated()
|
.anyRequest().authenticated()
|
||||||
)
|
)
|
||||||
.formLogin(AbstractHttpConfigurer::disable)
|
.formLogin(AbstractHttpConfigurer::disable)
|
||||||
|
|||||||
34
src/main/java/com/blog/config/MinioConfig.java
Normal file
34
src/main/java/com/blog/config/MinioConfig.java
Normal file
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
19
src/main/java/com/blog/config/MybatisPlusConfig.java
Normal file
19
src/main/java/com/blog/config/MybatisPlusConfig.java
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
25
src/main/java/com/blog/config/PipelineConfig.java
Normal file
25
src/main/java/com/blog/config/PipelineConfig.java
Normal file
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
21
src/main/java/com/blog/config/SwaggerConfig.java
Normal file
21
src/main/java/com/blog/config/SwaggerConfig.java
Normal file
@@ -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")));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ package com.blog.context;
|
|||||||
|
|
||||||
import com.blog.common.Captcha;
|
import com.blog.common.Captcha;
|
||||||
import com.blog.common.CaptchaLimit;
|
import com.blog.common.CaptchaLimit;
|
||||||
|
import com.blog.common.UserSession;
|
||||||
import com.google.common.util.concurrent.RateLimiter;
|
import com.google.common.util.concurrent.RateLimiter;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
@@ -16,12 +17,18 @@ public class GlobalContext {
|
|||||||
// 全局限流:每秒允许 10 个请求
|
// 全局限流:每秒允许 10 个请求
|
||||||
private final RateLimiter globalLimiter = RateLimiter.create(100.0);
|
private final RateLimiter globalLimiter = RateLimiter.create(100.0);
|
||||||
|
|
||||||
private Map<String, Captcha> captchas;
|
// 验证码池
|
||||||
|
private final Map<String, Captcha> captchas;
|
||||||
|
|
||||||
private Map<String, CaptchaLimit> ipLimitPool;
|
// ip池
|
||||||
|
private final Map<String, CaptchaLimit> ipLimitPool;
|
||||||
|
|
||||||
|
// userId映射map
|
||||||
|
private final Map<String, UserSession> userSessionMap;
|
||||||
|
|
||||||
public GlobalContext() {
|
public GlobalContext() {
|
||||||
captchas = new ConcurrentHashMap<>();
|
captchas = new ConcurrentHashMap<>();
|
||||||
ipLimitPool = new ConcurrentHashMap<>();
|
ipLimitPool = new ConcurrentHashMap<>();
|
||||||
|
userSessionMap = new ConcurrentHashMap<>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
18
src/main/java/com/blog/context/RequestContext.java
Normal file
18
src/main/java/com/blog/context/RequestContext.java
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
package com.blog.context;
|
||||||
|
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
public class RequestContext {
|
||||||
|
|
||||||
|
Map<String, Object> params;
|
||||||
|
|
||||||
|
public Object getReqValue(String key) {
|
||||||
|
return params.get(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
27
src/main/java/com/blog/context/RequestImplicitContext.java
Normal file
27
src/main/java/com/blog/context/RequestImplicitContext.java
Normal file
@@ -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<String,Object> resContextMap;
|
||||||
|
|
||||||
|
private Map<String,Object> reqContextMap;
|
||||||
|
|
||||||
|
private List<String> errorMessages;
|
||||||
|
|
||||||
|
private List<String> warningMessages;
|
||||||
|
|
||||||
|
private Object data;
|
||||||
|
}
|
||||||
16
src/main/java/com/blog/controller/ArticleController.java
Normal file
16
src/main/java/com/blog/controller/ArticleController.java
Normal file
@@ -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;
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
33
src/main/java/com/blog/controller/SpaceController.java
Normal file
33
src/main/java/com/blog/controller/SpaceController.java
Normal file
@@ -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<Object> mySpace() {
|
||||||
|
|
||||||
|
if (pathService.listPath(RequestImplicitContextHolder.getUserId())) {
|
||||||
|
return R.ok(SpaceSuccessMessage.MY_SPACE,RequestImplicitContextHolder.getData());
|
||||||
|
}
|
||||||
|
return R.err(RequestImplicitContextHolder.popErrorMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,18 +1,29 @@
|
|||||||
package com.blog.controller;
|
package com.blog.controller;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
import com.blog.common.Captcha;
|
import com.blog.common.Captcha;
|
||||||
import com.blog.common.R;
|
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.UserLoginDto;
|
||||||
import com.blog.dto.UserRegisterDto;
|
import com.blog.dto.UserRegisterDto;
|
||||||
import com.blog.holder.GlobalContextHolder;
|
import com.blog.holder.GlobalContextHolder;
|
||||||
|
import com.blog.holder.RequestImplicitContextHolder;
|
||||||
|
import com.blog.service.ArticleService;
|
||||||
import com.blog.service.UserService;
|
import com.blog.service.UserService;
|
||||||
import com.wf.captcha.GifCaptcha;
|
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 jakarta.validation.Valid;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Tag(name = "用户管理", description = "用户相关接口")
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/user")
|
@RequestMapping("/user")
|
||||||
public class UserController {
|
public class UserController {
|
||||||
@@ -20,12 +31,37 @@ public class UserController {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private UserService userService;
|
private UserService userService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ArticleService articleService;
|
||||||
|
|
||||||
|
@Operation(summary = "注册", description = "注册新用户")
|
||||||
@PostMapping("/register")
|
@PostMapping("/register")
|
||||||
public R register(@RequestBody @Valid UserRegisterDto userDto) {
|
public<T> T register(@RequestBody @Valid UserRegisterDto userDto) {
|
||||||
userService.register(userDto);
|
String errMsg = userService.register(userDto);
|
||||||
return login(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}")
|
@GetMapping("/exist/{username}")
|
||||||
public R exist(@PathVariable("username") String username) {
|
public R exist(@PathVariable("username") String username) {
|
||||||
boolean exist = userService.exist(username);
|
boolean exist = userService.exist(username);
|
||||||
@@ -35,11 +71,24 @@ public class UserController {
|
|||||||
return R.err("用户不存在");
|
return R.err("用户不存在");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@PreAuthorize("isAuthenticated()")
|
||||||
@GetMapping("/test")
|
@GetMapping("/test")
|
||||||
public R test() {
|
public R test() {
|
||||||
return R.ok("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")
|
@PostMapping("/login")
|
||||||
public R login(@Valid @RequestBody UserLoginDto user) {
|
public R login(@Valid @RequestBody UserLoginDto user) {
|
||||||
Object res = userService.login(user);
|
Object res = userService.login(user);
|
||||||
@@ -49,6 +98,7 @@ public class UserController {
|
|||||||
return R.ok("登录成功", res);
|
return R.ok("登录成功", res);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "获取验证码", description = "获取验证码")
|
||||||
@GetMapping("/captcha/{captchaId}")
|
@GetMapping("/captcha/{captchaId}")
|
||||||
public R captcha(@PathVariable("captchaId") String captchaId) throws Exception {
|
public R captcha(@PathVariable("captchaId") String captchaId) throws Exception {
|
||||||
if (captchaId.isBlank() || captchaId.length() > 36) {
|
if (captchaId.isBlank() || captchaId.length() > 36) {
|
||||||
|
|||||||
@@ -13,6 +13,9 @@ public class UserLoginDto {
|
|||||||
@NotBlank(message = "密码不能为空")
|
@NotBlank(message = "密码不能为空")
|
||||||
@Size(min = 6, max = 32, message = "密码长度必须在 6 到 32 位之间")
|
@Size(min = 6, max = 32, message = "密码长度必须在 6 到 32 位之间")
|
||||||
private String password;
|
private String password;
|
||||||
|
@NotBlank(message = "验证码不能为空")
|
||||||
|
private String verificationCode;
|
||||||
|
|
||||||
|
@NotBlank(message = "key 不能为空")
|
||||||
|
private String key;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,9 +14,4 @@ public class UserRegisterDto extends UserLoginDto {
|
|||||||
@NotBlank(message = "邀请码不能为空")
|
@NotBlank(message = "邀请码不能为空")
|
||||||
private String inviteCode;
|
private String inviteCode;
|
||||||
|
|
||||||
@NotBlank(message = "验证码不能为空")
|
|
||||||
private String verificationCode;
|
|
||||||
|
|
||||||
@NotBlank(message = "key 不能为空")
|
|
||||||
private String key;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
64
src/main/java/com/blog/filter/JwtAuthenticationFilter.java
Normal file
64
src/main/java/com/blog/filter/JwtAuthenticationFilter.java
Normal file
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -41,5 +41,6 @@ public class TokenBucketFilter extends OncePerRequestFilter {
|
|||||||
response.setContentType("application/json;charset=UTF-8");
|
response.setContentType("application/json;charset=UTF-8");
|
||||||
// 使用内置 JSON 或直接写字符串
|
// 使用内置 JSON 或直接写字符串
|
||||||
response.getWriter().write(objectMapper.writeValueAsString(R.err("系统繁忙,请稍后再试")));
|
response.getWriter().write(objectMapper.writeValueAsString(R.err("系统繁忙,请稍后再试")));
|
||||||
|
response.getWriter().flush();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,35 +1,42 @@
|
|||||||
package com.blog.helper;
|
package com.blog.helper;
|
||||||
|
|
||||||
|
import com.blog.common.UserSession;
|
||||||
|
import com.blog.common.constant.UserConstant;
|
||||||
import com.blog.entity.UserEntity;
|
import com.blog.entity.UserEntity;
|
||||||
|
import com.blog.holder.GlobalContextHolder;
|
||||||
|
import io.jsonwebtoken.Claims;
|
||||||
import io.jsonwebtoken.Jwts;
|
import io.jsonwebtoken.Jwts;
|
||||||
import io.jsonwebtoken.SignatureAlgorithm;
|
import io.jsonwebtoken.SignatureAlgorithm;
|
||||||
import io.jsonwebtoken.security.Keys;
|
import io.jsonwebtoken.security.Keys;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.security.Key;
|
import java.security.Key;
|
||||||
|
import java.time.Instant;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
@Service
|
@Component
|
||||||
public class UserHelper {
|
public class UserHelper {
|
||||||
public Map<String, String> obtainJWT(UserEntity userEntity) {
|
public Map<String, String> 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<String, Object> claims = new HashMap<>();
|
Map<String, Object> claims = new HashMap<>();
|
||||||
claims.put("userId", userEntity.getId());
|
claims.put(UserConstant.USER_ID, userId);
|
||||||
claims.put("username", userEntity.getUsername());
|
claims.put(UserConstant.USERNAME, userEntity.getUsername());
|
||||||
claims.put("nickname", userEntity.getNickname());
|
|
||||||
|
|
||||||
String accessToken = createToken(claims, 2*60);
|
String accessToken = createToken(claims, 2*60);
|
||||||
|
|
||||||
// 3. 签发 Refresh Token (有效期长,如 7 天)
|
// 3. 签发 Refresh Token (有效期长,如 7 天)
|
||||||
// 提示:Refresh Token 通常可以只存一个 userId,减少体积
|
// 提示: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<String, String> tokens = Map.of(
|
Map<String, String> tokens = Map.of(
|
||||||
"accessToken", accessToken,
|
UserConstant.ACCESS_TOKENS, accessToken,
|
||||||
"refreshToken", refreshToken,
|
UserConstant.REFRESH_TOKENS, refreshToken,
|
||||||
"tokenType", "Bearer"
|
UserConstant.TOKEN_TYPES, UserConstant.BEARER
|
||||||
);
|
);
|
||||||
|
|
||||||
return tokens;
|
return tokens;
|
||||||
@@ -48,4 +55,24 @@ public class UserHelper {
|
|||||||
.signWith(KEY, SignatureAlgorithm.HS256)
|
.signWith(KEY, SignatureAlgorithm.HS256)
|
||||||
.compact();
|
.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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<RequestImplicitContext> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.blog.initialization;
|
package com.blog.initializer;
|
||||||
|
|
||||||
import com.blog.context.GlobalContext;
|
import com.blog.context.GlobalContext;
|
||||||
import com.blog.holder.GlobalContextHolder;
|
import com.blog.holder.GlobalContextHolder;
|
||||||
@@ -6,7 +6,7 @@ import jakarta.annotation.PostConstruct;
|
|||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
public class GlobalContextInitialization {
|
public class GlobalContextInitializer {
|
||||||
|
|
||||||
@PostConstruct
|
@PostConstruct
|
||||||
public void initGlobalContext(){
|
public void initGlobalContext(){
|
||||||
78
src/main/java/com/blog/initializer/MinioInitializer.java
Normal file
78
src/main/java/com/blog/initializer/MinioInitializer.java
Normal file
@@ -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<String> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -38,7 +38,7 @@ public class IpLimitInterceptor implements HandlerInterceptor {
|
|||||||
// --- 允许访问:更新上下文 ---
|
// --- 允许访问:更新上下文 ---
|
||||||
// 使用简单的对象封装或直接存 Long 均可
|
// 使用简单的对象封装或直接存 Long 均可
|
||||||
if (limit == null||limit.getTtl()<nowSed) {
|
if (limit == null||limit.getTtl()<nowSed) {
|
||||||
pool.put(ip, CaptchaLimit.builder().ttl(nowSed +30).build());
|
pool.put(ip, CaptchaLimit.builder().ttl(nowSed +5).build());
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -46,7 +46,8 @@ public class IpLimitInterceptor implements HandlerInterceptor {
|
|||||||
private void handleBlocked(HttpServletResponse response) throws IOException {
|
private void handleBlocked(HttpServletResponse response) throws IOException {
|
||||||
response.setStatus(429);
|
response.setStatus(429);
|
||||||
response.setContentType("application/json;charset=UTF-8");
|
response.setContentType("application/json;charset=UTF-8");
|
||||||
response.getWriter().write(objectMapper.writeValueAsString(R.err("30秒内请勿重复操作")));
|
response.getWriter().write(objectMapper.writeValueAsString(R.err("5秒内请勿重复操作")));
|
||||||
|
response.getWriter().flush();
|
||||||
}
|
}
|
||||||
|
|
||||||
private String getClientIp(HttpServletRequest request) {
|
private String getClientIp(HttpServletRequest request) {
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
package com.blog.mapper;
|
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
|
||||||
import com.blog.entity.UserEntity;
|
|
||||||
import org.apache.ibatis.annotations.Mapper;
|
|
||||||
|
|
||||||
@Mapper
|
|
||||||
public interface UserMapper extends BaseMapper<UserEntity> {
|
|
||||||
}
|
|
||||||
40
src/main/java/com/blog/pipeline/RegisterPipeline.java
Normal file
40
src/main/java/com/blog/pipeline/RegisterPipeline.java
Normal file
@@ -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<Processor<UserRegisterDto>> processList;
|
||||||
|
|
||||||
|
public boolean start(UserRegisterDto userRegisterDto) {
|
||||||
|
AtomicBoolean result = new AtomicBoolean(false);
|
||||||
|
transactionTemplate.execute(status -> {
|
||||||
|
try {
|
||||||
|
for (Processor<UserRegisterDto> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
8
src/main/java/com/blog/processor/Processor.java
Normal file
8
src/main/java/com/blog/processor/Processor.java
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
package com.blog.processor;
|
||||||
|
|
||||||
|
public interface Processor<T> {
|
||||||
|
|
||||||
|
boolean process(T t);
|
||||||
|
|
||||||
|
boolean rollback(T t);
|
||||||
|
}
|
||||||
@@ -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<UserRegisterDto> {
|
||||||
|
|
||||||
|
@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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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> T login(UserLoginDto user);
|
|
||||||
|
|
||||||
String register(UserRegisterDto userDto);
|
|
||||||
|
|
||||||
boolean exist(String username);
|
|
||||||
}
|
|
||||||
@@ -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<UserMapper, UserEntity> 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> T login(UserLoginDto user) {
|
|
||||||
LambdaQueryWrapper<UserEntity> 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<UserEntity> 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<UserEntity>().eq(UserEntity::getUsername, username));
|
|
||||||
if (userEntity == null) return false;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,6 +2,7 @@ package com.blog.task;
|
|||||||
|
|
||||||
import com.blog.common.Captcha;
|
import com.blog.common.Captcha;
|
||||||
import com.blog.common.CaptchaLimit;
|
import com.blog.common.CaptchaLimit;
|
||||||
|
import com.blog.common.UserSession;
|
||||||
import com.blog.holder.GlobalContextHolder;
|
import com.blog.holder.GlobalContextHolder;
|
||||||
import org.springframework.scheduling.annotation.Async;
|
import org.springframework.scheduling.annotation.Async;
|
||||||
import org.springframework.scheduling.annotation.Scheduled;
|
import org.springframework.scheduling.annotation.Scheduled;
|
||||||
@@ -34,4 +35,15 @@ public class GlobalCleanupTask {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Async
|
||||||
|
@Scheduled(fixedRate = 60000)
|
||||||
|
public void purgeOnline() {
|
||||||
|
Map<String, UserSession> ipLimitPool = GlobalContextHolder.getGlobalContext().getUserSessionMap();
|
||||||
|
ipLimitPool.forEach((k,v)->{
|
||||||
|
if(v.getTtl()< Instant.now().getEpochSecond()){
|
||||||
|
ipLimitPool.remove(k);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,18 @@
|
|||||||
package com.blog.vaildator;
|
package com.blog.validator;
|
||||||
import com.blog.dto.UserLoginDto;
|
import com.blog.dto.UserLoginDto;
|
||||||
|
import com.blog.entity.InviteCodeEntity;
|
||||||
import com.blog.entity.UserEntity;
|
import com.blog.entity.UserEntity;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.Instant;
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
public class UserValidator {
|
public class UserValidator {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private PasswordEncoder passwordEncoder;
|
private PasswordEncoder passwordEncoder;
|
||||||
public String validateUser(UserLoginDto user, UserEntity userEntity) {
|
public String validateUser(UserLoginDto user, UserEntity userEntity) {
|
||||||
@@ -21,16 +24,26 @@ public class UserValidator {
|
|||||||
return "密码错误";
|
return "密码错误";
|
||||||
}
|
}
|
||||||
if (userEntity.getEnabled() == 0) 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 ("账号封禁中,解禁日期:" + userEntity.getReleaseDate());
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String validateRegister(UserEntity userEntity) {
|
public String validateRegister(UserEntity userEntity, InviteCodeEntity inviteCodeEntity) {
|
||||||
if(userEntity!=null){
|
if(userEntity!=null){
|
||||||
return "用户名已存在";
|
return "用户名已存在";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(inviteCodeEntity==null){
|
||||||
|
return "无效的邀请码";
|
||||||
|
}
|
||||||
|
if(inviteCodeEntity.getExpireTime()!=null&&inviteCodeEntity.getExpireTime().toInstant().isBefore(Instant.now())){
|
||||||
|
return "邀请码已过期";
|
||||||
|
}
|
||||||
|
if(inviteCodeEntity.getMaxUses()<=inviteCodeEntity.getUsedCount()){
|
||||||
|
return "邀请码使用次数已用尽";
|
||||||
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -21,9 +21,20 @@ logging:
|
|||||||
level:
|
level:
|
||||||
org.springframework.security: DEBUG
|
org.springframework.security: DEBUG
|
||||||
com.blog.mapper: DEBUG
|
com.blog.mapper: DEBUG
|
||||||
|
|
||||||
mybatis-plus:
|
mybatis-plus:
|
||||||
mapper-locations: classpath:/mapper/**.xml
|
mapper-locations: classpath:/mapper/**.xml
|
||||||
configuration:
|
configuration:
|
||||||
local-cache-scope: statement
|
local-cache-scope: statement
|
||||||
cache-enabled: false
|
cache-enabled: false
|
||||||
log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl
|
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
|
||||||
@@ -1,85 +1,126 @@
|
|||||||
-- 1. 用户表 (包含你要求的: 密码>6位、邀请码、状态、解禁日期等)
|
-- 1. 用户表 (包含你要求的: 密码>6位、邀请码、状态、解禁日期等)
|
||||||
CREATE TABLE `sys_user` (
|
CREATE TABLE `sys_user` (
|
||||||
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
|
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||||
`username` VARCHAR(64) NOT NULL COMMENT '用户名',
|
`username` VARCHAR(64) NOT NULL COMMENT '用户名',
|
||||||
`password` VARCHAR(255) NOT NULL COMMENT '密码(BCrypt加密)',
|
`password` VARCHAR(255) NOT NULL COMMENT '密码(BCrypt加密)',
|
||||||
`nickname` VARCHAR(64) NOT NULL COMMENT '用户昵称',
|
`nickname` VARCHAR(64) NOT NULL COMMENT '用户昵称',
|
||||||
`email` VARCHAR(128) NOT NULL COMMENT '邮箱',
|
`email` VARCHAR(128) NOT NULL COMMENT '邮箱',
|
||||||
`phone` VARCHAR(20) DEFAULT NULL COMMENT '手机号',
|
`phone` VARCHAR(20) DEFAULT NULL COMMENT '手机号',
|
||||||
`avatar` VARCHAR(255) DEFAULT NULL COMMENT '头像URL',
|
`avatar` VARCHAR(255) DEFAULT NULL COMMENT '头像URL',
|
||||||
`gender` TINYINT DEFAULT 0 COMMENT '性别: 1男, 2女, 0未知',
|
`gender` TINYINT DEFAULT 0 COMMENT '性别: 1男, 2女, 0未知',
|
||||||
`enabled` TINYINT(1) DEFAULT 1 COMMENT '状态: 1启用, 0禁用',
|
`enabled` TINYINT(1) DEFAULT 1 COMMENT '状态: 1启用, 0禁用',
|
||||||
`invite_code_id` VARCHAR(64) NOT NULL COMMENT '使用的邀请码ID',
|
`invite_code_id` BIGINT NOT NULL COMMENT '使用的邀请码ID',
|
||||||
`release_date` DATETIME DEFAULT NULL COMMENT '解禁日期',
|
`release_date` DATETIME DEFAULT NULL COMMENT '解禁日期',
|
||||||
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '注册时间',
|
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '注册时间',
|
||||||
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE 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-已删',
|
`is_deleted` TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除标记:0-未删, 1-已删',
|
||||||
PRIMARY KEY (`id`),
|
PRIMARY KEY (`id`),
|
||||||
UNIQUE KEY `uk_username` (`username`)
|
UNIQUE KEY `uk_username` (`username`)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';
|
||||||
|
|
||||||
-- 2. 角色表
|
-- 2. 角色表
|
||||||
CREATE TABLE `sys_role` (
|
CREATE TABLE `sys_role` (
|
||||||
`id` BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
`id` BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||||
`name` VARCHAR(32) NOT NULL COMMENT '角色名称(如:超级管理员)',
|
`name` VARCHAR(32) NOT NULL COMMENT '角色名称(如:超级管理员)',
|
||||||
`code` VARCHAR(32) NOT NULL COMMENT '角色标识(如:ROLE_ADMIN)',
|
`code` VARCHAR(32) NOT NULL COMMENT '角色标识(如:ROLE_ADMIN)',
|
||||||
`description` VARCHAR(255) DEFAULT NULL COMMENT '描述',
|
`description` VARCHAR(255) DEFAULT NULL COMMENT '描述',
|
||||||
`status` TINYINT NOT NULL DEFAULT 1 COMMENT '状态(1:正常, 0:禁用)',
|
`status` TINYINT NOT NULL DEFAULT 1 COMMENT '状态(1:正常, 0:禁用)',
|
||||||
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
UNIQUE KEY `uk_code` (`code`)
|
UNIQUE KEY `uk_code` (`code`)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='角色表';
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='角色表';
|
||||||
|
|
||||||
-- 3. 权限/菜单表
|
-- 3. 权限/菜单表
|
||||||
CREATE TABLE `sys_menu` (
|
CREATE TABLE `sys_menu` (
|
||||||
`id` BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
`id` BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||||
`parent_id` BIGINT DEFAULT 0 COMMENT '父菜单ID',
|
`parent_id` BIGINT DEFAULT 0 COMMENT '父菜单ID',
|
||||||
`name` VARCHAR(64) NOT NULL COMMENT '权限名称',
|
`name` VARCHAR(64) NOT NULL COMMENT '权限名称',
|
||||||
`perms` VARCHAR(100) DEFAULT NULL COMMENT '权限标识(如: sys:user:add)',
|
`perms` VARCHAR(100) DEFAULT NULL COMMENT '权限标识(如: sys:user:add)',
|
||||||
`type` TINYINT NOT NULL COMMENT '类型(0:目录, 1:菜单, 2:按钮)',
|
`type` TINYINT NOT NULL COMMENT '类型(0:目录, 1:菜单, 2:按钮)',
|
||||||
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='权限菜单表';
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='权限菜单表';
|
||||||
|
|
||||||
-- 4. 用户-角色关联表
|
-- 4. 用户-角色关联表
|
||||||
CREATE TABLE `sys_user_role` (
|
CREATE TABLE `sys_user_role` (
|
||||||
`user_id` BIGINT NOT NULL,
|
`user_id` BIGINT NOT NULL,
|
||||||
`role_id` BIGINT NOT NULL,
|
`role_id` BIGINT NOT NULL,
|
||||||
PRIMARY KEY (`user_id`, `role_id`)
|
PRIMARY KEY (`user_id`, `role_id`)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
-- 5. 角色-权限关联表
|
-- 5. 角色-权限关联表
|
||||||
CREATE TABLE `sys_role_menu` (
|
CREATE TABLE `sys_role_menu` (
|
||||||
`role_id` BIGINT NOT NULL,
|
`role_id` BIGINT NOT NULL,
|
||||||
`menu_id` BIGINT NOT NULL,
|
`menu_id` BIGINT NOT NULL,
|
||||||
PRIMARY KEY (`role_id`, `menu_id`)
|
PRIMARY KEY (`role_id`, `menu_id`)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
-- 6. 邀请码表
|
-- 6. 邀请码表
|
||||||
CREATE TABLE `sys_invite_code` (
|
CREATE TABLE `sys_invite_code` (
|
||||||
`id` BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
`id` BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||||
`code` VARCHAR(32) NOT NULL COMMENT '唯一邀请码',
|
`code` VARCHAR(32) NOT NULL COMMENT '唯一邀请码',
|
||||||
`creator_id` BIGINT DEFAULT NULL COMMENT '生成人ID',
|
`creator_id` BIGINT DEFAULT NULL COMMENT '生成人ID',
|
||||||
`max_uses` INT NOT NULL DEFAULT 1 COMMENT '最大使用次数',
|
`max_uses` INT NOT NULL DEFAULT 1 COMMENT '最大使用次数',
|
||||||
`used_count` INT NOT NULL DEFAULT 0 COMMENT '已使用次数',
|
`used_count` INT NOT NULL DEFAULT 0 COMMENT '已使用次数',
|
||||||
`status` TINYINT NOT NULL DEFAULT 1 COMMENT '1-有效, 0-失效',
|
`status` TINYINT NOT NULL DEFAULT 1 COMMENT '1-有效, 0-失效',
|
||||||
`expire_time` DATETIME DEFAULT NULL COMMENT '过期时间',
|
`expire_time` DATETIME DEFAULT NULL COMMENT '过期时间',
|
||||||
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP,
|
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
UNIQUE KEY `uk_code` (`code`)
|
UNIQUE KEY `uk_code` (`code`)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='邀请码表';
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='邀请码表';
|
||||||
|
|
||||||
-- 7. 文章表
|
-- 7. 文章表
|
||||||
CREATE TABLE `sys_article` (
|
CREATE TABLE `sys_article` (
|
||||||
`id` BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
`id` BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||||
`title` VARCHAR(255) NOT NULL COMMENT '标题',
|
`title` VARCHAR(255) NOT NULL COMMENT '标题',
|
||||||
`content` LONGTEXT NOT NULL COMMENT '内容',
|
`content` LONGTEXT NOT NULL COMMENT '内容',
|
||||||
`author_id` BIGINT NOT NULL COMMENT '作者ID',
|
`author_id` BIGINT NOT NULL COMMENT '作者ID',
|
||||||
`status` TINYINT NOT NULL DEFAULT 0 COMMENT '0-草稿, 1-发布',
|
`status` TINYINT NOT NULL DEFAULT 0 COMMENT '0-草稿, 1-发布, 2下架, 3删除',
|
||||||
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
`views` BIGINT DEFAULT 0 COMMENT '浏览数',
|
||||||
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
`collection_count` BIGINT DEFAULT 0 COMMENT '收藏数',
|
||||||
`is_deleted` TINYINT NOT NULL DEFAULT 0,
|
`like_count` BIGINT DEFAULT 0 COMMENT '点赞数',
|
||||||
KEY `idx_author` (`author_id`)
|
`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='文章表';
|
) 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`)
|
INSERT INTO `sys_role` (`name`, `code`, `description`)
|
||||||
VALUES ('超级管理员', 'ROLE_ADMIN', '拥有系统所有权限');
|
VALUES ('超级管理员', 'ROLE_ADMIN', '拥有系统所有权限');
|
||||||
@@ -87,16 +128,21 @@ VALUES ('超级管理员', 'ROLE_ADMIN', '拥有系统所有权限');
|
|||||||
-- 初始化管理员用户 (密码: admin123)
|
-- 初始化管理员用户 (密码: admin123)
|
||||||
-- 注意: invite_code_id 不能为空,这里填 SYSTEM
|
-- 注意: invite_code_id 不能为空,这里填 SYSTEM
|
||||||
INSERT INTO `sys_user` (
|
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 (
|
) VALUES (
|
||||||
'admin',
|
'admin',
|
||||||
'admin123',
|
'$2a$10$HoxAaokfbkslPrrBKkKDdOw7eBV/q.5Rj4P81d1h37y92i2Jd/.VW',
|
||||||
'超级管理员',
|
'超级管理员',
|
||||||
'admin@example.com',
|
'2920370144@qq.com',
|
||||||
'SYSTEM',
|
0,
|
||||||
1,
|
1,
|
||||||
0
|
0
|
||||||
);
|
);
|
||||||
|
|
||||||
-- 关联管理员与角色 (假设 ID 都是 1)
|
-- 关联管理员与角色 (假设 ID 都是 1)
|
||||||
INSERT INTO `sys_user_role` (`user_id`, `role_id`) VALUES (1, 1);
|
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);
|
||||||
Reference in New Issue
Block a user