初始化
This commit is contained in:
29
.gitignore
vendored
Normal file
29
.gitignore
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
### Java / Maven ###
|
||||
target/
|
||||
*.class
|
||||
*.war
|
||||
*.ear
|
||||
*.jar
|
||||
*.zip
|
||||
*.tar.gz
|
||||
|
||||
### IDEA ###
|
||||
.idea/
|
||||
*.iml
|
||||
*.iws
|
||||
*.ipr
|
||||
out/
|
||||
.shelf/
|
||||
|
||||
### Logs (很重要,高并发产生的日志别传) ###
|
||||
*.log
|
||||
logs/
|
||||
|
||||
### System / OS ###
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
### Frontend (如果你博客带前端代码) ###
|
||||
node_modules/
|
||||
dist/
|
||||
.env.local
|
||||
93
pom.xml
Normal file
93
pom.xml
Normal file
@@ -0,0 +1,93 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>com.blog</groupId>
|
||||
<artifactId>x-blog-backend</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.source>17</maven.compiler.source>
|
||||
<maven.compiler.target>17</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-security</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.mysql</groupId>
|
||||
<artifactId>mysql-connector-j</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.baomidou</groupId>
|
||||
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
|
||||
<version>3.5.5</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.flywaydb</groupId>
|
||||
<artifactId>flyway-mysql</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-validation</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-api</artifactId>
|
||||
<version>0.11.5</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-impl</artifactId>
|
||||
<scope>runtime</scope>
|
||||
<version>0.11.5</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-jackson</artifactId>
|
||||
<scope>runtime</scope>
|
||||
<version>0.11.5</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.github.whvcse</groupId>
|
||||
<artifactId>easy-captcha</artifactId>
|
||||
<version>1.6.2</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-aop</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.guava</groupId>
|
||||
<artifactId>guava</artifactId>
|
||||
<version>33.0.0-jre</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-dependencies</artifactId>
|
||||
<version>3.2.11</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
</project>
|
||||
18
src/main/java/com/blog/App.java
Normal file
18
src/main/java/com/blog/App.java
Normal file
@@ -0,0 +1,18 @@
|
||||
package com.blog;
|
||||
|
||||
import org.mybatis.spring.annotation.MapperScan;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.scheduling.annotation.EnableAsync;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
|
||||
@SpringBootApplication
|
||||
@MapperScan("com.blog.mapper")
|
||||
@EnableAsync
|
||||
@EnableScheduling
|
||||
public class App {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(App.class, args);
|
||||
}
|
||||
}
|
||||
45
src/main/java/com/blog/aspect/UserAspect.java
Normal file
45
src/main/java/com/blog/aspect/UserAspect.java
Normal file
@@ -0,0 +1,45 @@
|
||||
package com.blog.aspect;
|
||||
|
||||
import com.blog.common.Captcha;
|
||||
import com.blog.common.R;
|
||||
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.stereotype.Component;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
@Aspect
|
||||
@Component
|
||||
public class UserAspect {
|
||||
|
||||
@Around("execution(* com.blog.controller.UserController.register(..))")
|
||||
public Object registerBefore(ProceedingJoinPoint joinPoint) {
|
||||
Object[] args = joinPoint.getArgs();
|
||||
for (Object arg : args) {
|
||||
if(arg instanceof UserRegisterDto){
|
||||
UserRegisterDto userRegisterDto = (UserRegisterDto)arg;
|
||||
Captcha captcha = GlobalContextHolder.getGlobalContext().getCaptchas().get(userRegisterDto.getKey());
|
||||
if(captcha != null){
|
||||
if(captcha.getTtl()< Instant.now().getEpochSecond()){
|
||||
return R.err("验证码已过期");
|
||||
}
|
||||
if(!captcha.getText().equals(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("无效的验证码");
|
||||
}
|
||||
}
|
||||
return R.err("无效的验证码");
|
||||
}
|
||||
}
|
||||
15
src/main/java/com/blog/common/Captcha.java
Normal file
15
src/main/java/com/blog/common/Captcha.java
Normal file
@@ -0,0 +1,15 @@
|
||||
package com.blog.common;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
@Builder
|
||||
@Data
|
||||
public class Captcha {
|
||||
|
||||
private String text;
|
||||
|
||||
private long ttl;
|
||||
}
|
||||
11
src/main/java/com/blog/common/CaptchaLimit.java
Normal file
11
src/main/java/com/blog/common/CaptchaLimit.java
Normal file
@@ -0,0 +1,11 @@
|
||||
package com.blog.common;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
@Builder
|
||||
@Data
|
||||
public class CaptchaLimit {
|
||||
|
||||
private long ttl;
|
||||
}
|
||||
33
src/main/java/com/blog/common/R.java
Normal file
33
src/main/java/com/blog/common/R.java
Normal file
@@ -0,0 +1,33 @@
|
||||
package com.blog.common;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class R<T> {
|
||||
|
||||
private int code;
|
||||
private String msg;
|
||||
private T data;
|
||||
R(int code, String msg){
|
||||
this.code = code;
|
||||
this.msg = msg;
|
||||
}
|
||||
R(int code, String msg, T data){
|
||||
this.code = code;
|
||||
this.msg = msg;
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
|
||||
public static <T> R<T> ok(String msg) {
|
||||
return new R<>(0,msg);
|
||||
}
|
||||
|
||||
public static <T> R<T> ok(String msg, T data) {
|
||||
return new R<>(0,msg,data);
|
||||
}
|
||||
|
||||
public static <T> R<T> err(String msg) {
|
||||
return new R<>(-1,msg);
|
||||
}
|
||||
}
|
||||
72
src/main/java/com/blog/config/AuthorizationServerConfig.java
Normal file
72
src/main/java/com/blog/config/AuthorizationServerConfig.java
Normal file
@@ -0,0 +1,72 @@
|
||||
package com.blog.config;
|
||||
|
||||
import com.blog.common.R;
|
||||
import com.blog.filter.TokenBucketFilter;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
||||
import org.springframework.security.config.annotation.web.configurers.RequestCacheConfigurer;
|
||||
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.context.SecurityContextHolderFilter;
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
public class AuthorizationServerConfig {
|
||||
|
||||
@Autowired
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
@Autowired
|
||||
private TokenBucketFilter tokenBucketFilter;
|
||||
|
||||
@Bean
|
||||
public PasswordEncoder passwordEncoder() {
|
||||
// 默认使用 BCrypt,它自带随机盐机制
|
||||
return new BCryptPasswordEncoder();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
http
|
||||
.addFilterBefore(tokenBucketFilter, SecurityContextHolderFilter.class)
|
||||
.authorizeHttpRequests(auth -> auth
|
||||
.requestMatchers("/user/login",
|
||||
"/user/captcha/**",
|
||||
"/user/exist/**",
|
||||
"/user/register").permitAll()
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
.formLogin(AbstractHttpConfigurer::disable)
|
||||
.httpBasic(AbstractHttpConfigurer::disable)
|
||||
.csrf(AbstractHttpConfigurer::disable)
|
||||
.sessionManagement(session ->
|
||||
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
|
||||
)
|
||||
.requestCache(RequestCacheConfigurer::disable)
|
||||
.exceptionHandling(exceptions -> exceptions
|
||||
// 1. 处理未认证 (401)
|
||||
.authenticationEntryPoint((request, response, authException) -> {
|
||||
response.setContentType("application/json;charset=UTF-8");
|
||||
response.setStatus(401);
|
||||
String json = objectMapper.writeValueAsString(R.err("无效的 token"));
|
||||
response.getWriter().write(json);
|
||||
})
|
||||
// 2. 处理权限不足 (403)
|
||||
.accessDeniedHandler((request, response, accessDeniedException) -> {
|
||||
response.setContentType("application/json;charset=UTF-8");
|
||||
response.setStatus(403);
|
||||
String json = objectMapper.writeValueAsString(R.err("权限不足"));
|
||||
response.getWriter().write(json);
|
||||
})
|
||||
);
|
||||
|
||||
return http.build();
|
||||
}
|
||||
}
|
||||
24
src/main/java/com/blog/config/WebMvcConfig.java
Normal file
24
src/main/java/com/blog/config/WebMvcConfig.java
Normal file
@@ -0,0 +1,24 @@
|
||||
package com.blog.config;
|
||||
|
||||
import com.blog.interceptor.IpLimitInterceptor;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
@Configuration
|
||||
public class WebMvcConfig implements WebMvcConfigurer {
|
||||
|
||||
@Autowired
|
||||
private IpLimitInterceptor ipLimitInterceptor;
|
||||
|
||||
@Override
|
||||
public void addInterceptors(InterceptorRegistry registry) {
|
||||
// 注册拦截器
|
||||
registry.addInterceptor(ipLimitInterceptor)
|
||||
// 1. 添加拦截路径(需要限流的接口)
|
||||
.addPathPatterns("/user/captcha/**")
|
||||
// 2. 排除不拦截的路径(如静态资源)
|
||||
.excludePathPatterns("/static/**", "/assets/**");
|
||||
}
|
||||
}
|
||||
27
src/main/java/com/blog/context/GlobalContext.java
Normal file
27
src/main/java/com/blog/context/GlobalContext.java
Normal file
@@ -0,0 +1,27 @@
|
||||
package com.blog.context;
|
||||
|
||||
import com.blog.common.Captcha;
|
||||
import com.blog.common.CaptchaLimit;
|
||||
import com.google.common.util.concurrent.RateLimiter;
|
||||
import lombok.Data;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
@Component
|
||||
@Data
|
||||
public class GlobalContext {
|
||||
|
||||
// 全局限流:每秒允许 10 个请求
|
||||
private final RateLimiter globalLimiter = RateLimiter.create(100.0);
|
||||
|
||||
private Map<String, Captcha> captchas;
|
||||
|
||||
private Map<String, CaptchaLimit> ipLimitPool;
|
||||
|
||||
public GlobalContext() {
|
||||
captchas = new ConcurrentHashMap<>();
|
||||
ipLimitPool = new ConcurrentHashMap<>();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.blog.controller;
|
||||
|
||||
import com.blog.common.R;
|
||||
import org.springframework.validation.BindingResult;
|
||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
import org.springframework.web.servlet.resource.NoResourceFoundException;
|
||||
|
||||
@RestControllerAdvice
|
||||
public class GlobalExceptionHandler {
|
||||
|
||||
@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||
public R<String> handleValidationException(MethodArgumentNotValidException e) {
|
||||
e.printStackTrace();
|
||||
// 1. 从异常对象中拿到所有的错误字段信息
|
||||
BindingResult bindingResult = e.getBindingResult();
|
||||
|
||||
// 2. 提取第一条错误信息(或者拼接所有错误)
|
||||
String defaultMessage = bindingResult.getFieldError().getDefaultMessage();
|
||||
|
||||
// 3. 返回你自定义的错误格式
|
||||
return R.err(defaultMessage != null ? defaultMessage : "参数校验失败");
|
||||
}
|
||||
@ExceptionHandler(Exception.class)
|
||||
public R<String> handleException(Exception e) {
|
||||
e.printStackTrace();
|
||||
return R.err("系统异常,请联系管理员");
|
||||
}
|
||||
@ExceptionHandler(NoResourceFoundException.class)
|
||||
public R<String> handleException(NoResourceFoundException e) {
|
||||
e.printStackTrace();
|
||||
return R.err("资源不存在");
|
||||
}
|
||||
}
|
||||
65
src/main/java/com/blog/controller/UserController.java
Normal file
65
src/main/java/com/blog/controller/UserController.java
Normal file
@@ -0,0 +1,65 @@
|
||||
package com.blog.controller;
|
||||
|
||||
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 com.blog.service.UserService;
|
||||
import com.wf.captcha.GifCaptcha;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/user")
|
||||
public class UserController {
|
||||
|
||||
@Autowired
|
||||
private UserService userService;
|
||||
|
||||
@PostMapping("/register")
|
||||
public R register(@RequestBody @Valid UserRegisterDto userDto) {
|
||||
userService.register(userDto);
|
||||
return login(userDto);
|
||||
}
|
||||
|
||||
@GetMapping("/exist/{username}")
|
||||
public R exist(@PathVariable("username") String username) {
|
||||
boolean exist = userService.exist(username);
|
||||
if (exist) {
|
||||
return R.ok("用户已存在");
|
||||
}
|
||||
return R.err("用户不存在");
|
||||
}
|
||||
|
||||
@GetMapping("/test")
|
||||
public R test() {
|
||||
return R.ok("test");
|
||||
}
|
||||
|
||||
@PostMapping("/login")
|
||||
public R login(@Valid @RequestBody UserLoginDto user) {
|
||||
Object res = userService.login(user);
|
||||
if (res instanceof String) {
|
||||
return R.err(res.toString());
|
||||
}
|
||||
return R.ok("登录成功", res);
|
||||
}
|
||||
|
||||
@GetMapping("/captcha/{captchaId}")
|
||||
public R captcha(@PathVariable("captchaId") String captchaId) throws Exception {
|
||||
if (captchaId.isBlank() || captchaId.length() > 36) {
|
||||
return R.err("无效的 captchaId");
|
||||
}
|
||||
// 生成 GIF 类型的验证码 (三个参数:宽、高、位数)
|
||||
GifCaptcha specCaptcha = new GifCaptcha(130, 48, 4);
|
||||
// 获取验证码文本
|
||||
String text = specCaptcha.text().toLowerCase();
|
||||
GlobalContextHolder.getGlobalContext().getCaptchas().put(captchaId, Captcha.builder().text(text).ttl(Instant.now().getEpochSecond() + 60).build());
|
||||
String captchaBase64 = specCaptcha.toBase64();
|
||||
return R.ok("获取成功", captchaBase64);
|
||||
}
|
||||
}
|
||||
18
src/main/java/com/blog/dto/UserLoginDto.java
Normal file
18
src/main/java/com/blog/dto/UserLoginDto.java
Normal file
@@ -0,0 +1,18 @@
|
||||
package com.blog.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class UserLoginDto {
|
||||
@NotBlank(message = "用户名不能为空")
|
||||
@Size(min = 4, max = 20, message = "用户名长度必须在4-20位之间")
|
||||
private String username;
|
||||
@NotBlank(message = "密码不能为空")
|
||||
@Size(min = 6, max = 32, message = "密码长度必须在 6 到 32 位之间")
|
||||
private String password;
|
||||
|
||||
|
||||
}
|
||||
22
src/main/java/com/blog/dto/UserRegisterDto.java
Normal file
22
src/main/java/com/blog/dto/UserRegisterDto.java
Normal file
@@ -0,0 +1,22 @@
|
||||
package com.blog.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class UserRegisterDto extends UserLoginDto {
|
||||
|
||||
@NotBlank(message = "邮箱不能为空")
|
||||
@Pattern(regexp = "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", message = "邮箱格式不正确")
|
||||
private String email;
|
||||
|
||||
@NotBlank(message = "邀请码不能为空")
|
||||
private String inviteCode;
|
||||
|
||||
@NotBlank(message = "验证码不能为空")
|
||||
private String verificationCode;
|
||||
|
||||
@NotBlank(message = "key 不能为空")
|
||||
private String key;
|
||||
}
|
||||
42
src/main/java/com/blog/entity/UserEntity.java
Normal file
42
src/main/java/com/blog/entity/UserEntity.java
Normal file
@@ -0,0 +1,42 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
45
src/main/java/com/blog/filter/TokenBucketFilter.java
Normal file
45
src/main/java/com/blog/filter/TokenBucketFilter.java
Normal file
@@ -0,0 +1,45 @@
|
||||
package com.blog.filter;
|
||||
|
||||
import com.blog.common.R;
|
||||
import com.blog.holder.GlobalContextHolder;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.NonNull;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
@Component
|
||||
public class TokenBucketFilter extends OncePerRequestFilter {
|
||||
|
||||
@Autowired
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(@NonNull HttpServletRequest request,
|
||||
@NonNull HttpServletResponse response,
|
||||
@NonNull FilterChain filterChain) throws ServletException, IOException {
|
||||
|
||||
// 1. 尝试获取令牌 (tryAcquire 是非阻塞的)
|
||||
// 使用 if 判断,符合你的高性能流控思维
|
||||
if (GlobalContextHolder.getGlobalContext().getGlobalLimiter().tryAcquire()) {
|
||||
// 2. 拿到令牌,放行
|
||||
filterChain.doFilter(request, response);
|
||||
} else {
|
||||
// 3. 没拿到令牌,直接拒绝
|
||||
handleRateLimitError(response);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleRateLimitError(HttpServletResponse response) throws IOException {
|
||||
response.setStatus(429);
|
||||
response.setContentType("application/json;charset=UTF-8");
|
||||
// 使用内置 JSON 或直接写字符串
|
||||
response.getWriter().write(objectMapper.writeValueAsString(R.err("系统繁忙,请稍后再试")));
|
||||
}
|
||||
}
|
||||
51
src/main/java/com/blog/helper/UserHelper.java
Normal file
51
src/main/java/com/blog/helper/UserHelper.java
Normal file
@@ -0,0 +1,51 @@
|
||||
package com.blog.helper;
|
||||
|
||||
import com.blog.entity.UserEntity;
|
||||
import io.jsonwebtoken.Jwts;
|
||||
import io.jsonwebtoken.SignatureAlgorithm;
|
||||
import io.jsonwebtoken.security.Keys;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.Key;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@Service
|
||||
public class UserHelper {
|
||||
public Map<String, String> obtainJWT(UserEntity userEntity) {
|
||||
Map<String, Object> claims = new HashMap<>();
|
||||
claims.put("userId", userEntity.getId());
|
||||
claims.put("username", userEntity.getUsername());
|
||||
claims.put("nickname", userEntity.getNickname());
|
||||
|
||||
String accessToken = createToken(claims, 2*60);
|
||||
|
||||
// 3. 签发 Refresh Token (有效期长,如 7 天)
|
||||
// 提示:Refresh Token 通常可以只存一个 userId,减少体积
|
||||
String refreshToken = createToken(Map.of("userId", userEntity.getId()), 1 * 24 * 60);
|
||||
|
||||
Map<String, String> tokens = Map.of(
|
||||
"accessToken", accessToken,
|
||||
"refreshToken", refreshToken,
|
||||
"tokenType", "Bearer"
|
||||
);
|
||||
|
||||
return tokens;
|
||||
}
|
||||
|
||||
// 密钥至少 32 个字符
|
||||
private static final String SECRET = "your-super-secret-key-at-least-32-chars-long";
|
||||
private static final Key KEY = Keys.hmacShaKeyFor(SECRET.getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
public static String createToken(Map<String, Object> claims, int minutes) {
|
||||
long now = System.currentTimeMillis();
|
||||
return Jwts.builder()
|
||||
.setClaims(claims)
|
||||
.setIssuedAt(new Date(now))
|
||||
.setExpiration(new Date(now + (long) minutes * 60 * 1000))
|
||||
.signWith(KEY, SignatureAlgorithm.HS256)
|
||||
.compact();
|
||||
}
|
||||
}
|
||||
14
src/main/java/com/blog/holder/GlobalContextHolder.java
Normal file
14
src/main/java/com/blog/holder/GlobalContextHolder.java
Normal file
@@ -0,0 +1,14 @@
|
||||
package com.blog.holder;
|
||||
|
||||
import com.blog.context.GlobalContext;
|
||||
|
||||
public class GlobalContextHolder {
|
||||
|
||||
private static GlobalContext globalContext;
|
||||
public static GlobalContext getGlobalContext() {
|
||||
return globalContext;
|
||||
}
|
||||
public static void setGlobalContext(GlobalContext globalContext) {
|
||||
GlobalContextHolder.globalContext = globalContext;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.blog.initialization;
|
||||
|
||||
import com.blog.context.GlobalContext;
|
||||
import com.blog.holder.GlobalContextHolder;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class GlobalContextInitialization {
|
||||
|
||||
@PostConstruct
|
||||
public void initGlobalContext(){
|
||||
GlobalContext globalContext = new GlobalContext();
|
||||
GlobalContextHolder.setGlobalContext(globalContext);
|
||||
}
|
||||
}
|
||||
56
src/main/java/com/blog/interceptor/IpLimitInterceptor.java
Normal file
56
src/main/java/com/blog/interceptor/IpLimitInterceptor.java
Normal file
@@ -0,0 +1,56 @@
|
||||
package com.blog.interceptor;
|
||||
|
||||
import com.blog.common.CaptchaLimit;
|
||||
import com.blog.common.R;
|
||||
import com.blog.holder.GlobalContextHolder;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.NonNull;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.servlet.HandlerInterceptor;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.Instant;
|
||||
import java.util.Map;
|
||||
|
||||
@Component
|
||||
public class IpLimitInterceptor implements HandlerInterceptor {
|
||||
|
||||
@Autowired
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
@Override
|
||||
public boolean preHandle(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull Object handler) throws IOException {
|
||||
String ip = getClientIp(request);
|
||||
long nowSed = Instant.now().getEpochSecond();
|
||||
// 直接从全局上下文中操作 ipLimitPool
|
||||
Map<String, CaptchaLimit> pool = GlobalContextHolder.getGlobalContext().getIpLimitPool();
|
||||
CaptchaLimit limit = pool.get(ip);
|
||||
|
||||
// --- if 判断控制流 ---
|
||||
if (limit != null && limit.getTtl()>nowSed) {
|
||||
// 拒绝请求,不抛异常,直接返回 false
|
||||
handleBlocked(response);
|
||||
return false;
|
||||
}
|
||||
// --- 允许访问:更新上下文 ---
|
||||
// 使用简单的对象封装或直接存 Long 均可
|
||||
if (limit == null||limit.getTtl()<nowSed) {
|
||||
pool.put(ip, CaptchaLimit.builder().ttl(nowSed +30).build());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void handleBlocked(HttpServletResponse response) throws IOException {
|
||||
response.setStatus(429);
|
||||
response.setContentType("application/json;charset=UTF-8");
|
||||
response.getWriter().write(objectMapper.writeValueAsString(R.err("30秒内请勿重复操作")));
|
||||
}
|
||||
|
||||
private String getClientIp(HttpServletRequest request) {
|
||||
String ip = request.getHeader("X-Real-IP");
|
||||
return (ip == null) ? request.getRemoteAddr() : ip;
|
||||
}
|
||||
}
|
||||
9
src/main/java/com/blog/mapper/UserMapper.java
Normal file
9
src/main/java/com/blog/mapper/UserMapper.java
Normal file
@@ -0,0 +1,9 @@
|
||||
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> {
|
||||
}
|
||||
14
src/main/java/com/blog/service/UserService.java
Normal file
14
src/main/java/com/blog/service/UserService.java
Normal file
@@ -0,0 +1,14 @@
|
||||
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);
|
||||
}
|
||||
76
src/main/java/com/blog/service/impl/UserServiceImpl.java
Normal file
76
src/main/java/com/blog/service/impl/UserServiceImpl.java
Normal file
@@ -0,0 +1,76 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
37
src/main/java/com/blog/task/GlobalCleanupTask.java
Normal file
37
src/main/java/com/blog/task/GlobalCleanupTask.java
Normal file
@@ -0,0 +1,37 @@
|
||||
package com.blog.task;
|
||||
|
||||
import com.blog.common.Captcha;
|
||||
import com.blog.common.CaptchaLimit;
|
||||
import com.blog.holder.GlobalContextHolder;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Map;
|
||||
|
||||
@Component
|
||||
public class GlobalCleanupTask {
|
||||
|
||||
@Async
|
||||
@Scheduled(fixedRate = 60000)
|
||||
public void purgeCaptcha() {
|
||||
Map<String, Captcha> captchas = GlobalContextHolder.getGlobalContext().getCaptchas();
|
||||
captchas.forEach((k,v)->{
|
||||
if(v.getTtl()< Instant.now().getEpochSecond()){
|
||||
captchas.remove(k);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Async
|
||||
@Scheduled(fixedRate = 60000)
|
||||
public void purgeIpLimit() {
|
||||
Map<String, CaptchaLimit> ipLimitPool = GlobalContextHolder.getGlobalContext().getIpLimitPool();
|
||||
ipLimitPool.forEach((k,v)->{
|
||||
if(v.getTtl()< Instant.now().getEpochSecond()){
|
||||
ipLimitPool.remove(k);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
18
src/main/java/com/blog/utils/ChartsGenerator.java
Normal file
18
src/main/java/com/blog/utils/ChartsGenerator.java
Normal file
@@ -0,0 +1,18 @@
|
||||
package com.blog.utils;
|
||||
|
||||
import java.util.Random;
|
||||
|
||||
public class ChartsGenerator {
|
||||
|
||||
private static final String CHAR_POOL = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
|
||||
public static String generateCode() {
|
||||
Random random = new Random();
|
||||
StringBuilder sb = new StringBuilder(6);
|
||||
for (int i = 0; i < 6; i++) {
|
||||
int index = random.nextInt(CHAR_POOL.length());
|
||||
sb.append(CHAR_POOL.charAt(index));
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
36
src/main/java/com/blog/vaildator/UserValidator.java
Normal file
36
src/main/java/com/blog/vaildator/UserValidator.java
Normal file
@@ -0,0 +1,36 @@
|
||||
package com.blog.vaildator;
|
||||
import com.blog.dto.UserLoginDto;
|
||||
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;
|
||||
|
||||
@Component
|
||||
public class UserValidator {
|
||||
|
||||
@Autowired
|
||||
private PasswordEncoder passwordEncoder;
|
||||
public String validateUser(UserLoginDto user, UserEntity userEntity) {
|
||||
if (userEntity == null) {
|
||||
return "用户名不存在";
|
||||
}
|
||||
boolean isMatch = passwordEncoder.matches(user.getPassword(), userEntity.getPassword());
|
||||
if (!isMatch) {
|
||||
return "密码错误";
|
||||
}
|
||||
if (userEntity.getEnabled() == 0) return "账号已被禁用";
|
||||
if (userEntity.getReleaseDate() != null && userEntity.getReleaseDate().isAfter(LocalDateTime.now())) {
|
||||
return ("账号封禁中,解禁日期:" + userEntity.getReleaseDate());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public String validateRegister(UserEntity userEntity) {
|
||||
if(userEntity!=null){
|
||||
return "用户名已存在";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
29
src/main/resources/application.yaml
Normal file
29
src/main/resources/application.yaml
Normal file
@@ -0,0 +1,29 @@
|
||||
server:
|
||||
port: 80
|
||||
spring:
|
||||
application:
|
||||
name: blog-backend
|
||||
datasource:
|
||||
url: jdbc:mysql://localhost:33400/x-bolg?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true
|
||||
username: root
|
||||
password: g-o-n-g-q-i
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
flyway:
|
||||
# 是否启用
|
||||
enabled: true
|
||||
# 脚本位置(默认即为 db/migration)
|
||||
locations: classpath:db/migration
|
||||
# 如果数据库非空且没有 flyway 表,是否自动初始化(基准线)
|
||||
baseline-on-migrate: true
|
||||
# 针对已有的数据库,设定版本号为 1
|
||||
baseline-version: 1
|
||||
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
|
||||
102
src/main/resources/db/migration/V1__create_table.sql
Normal file
102
src/main/resources/db/migration/V1__create_table.sql
Normal file
@@ -0,0 +1,102 @@
|
||||
-- 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`)
|
||||
) 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`)
|
||||
) 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
|
||||
) 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`)
|
||||
) 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`)
|
||||
) 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`)
|
||||
) 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`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='文章表';
|
||||
|
||||
-- 初始化管理员角色
|
||||
INSERT INTO `sys_role` (`name`, `code`, `description`)
|
||||
VALUES ('超级管理员', 'ROLE_ADMIN', '拥有系统所有权限');
|
||||
|
||||
-- 初始化管理员用户 (密码: admin123)
|
||||
-- 注意: invite_code_id 不能为空,这里填 SYSTEM
|
||||
INSERT INTO `sys_user` (
|
||||
`username`, `password`, `nickname`, `email`, `invite_code_id`, `enabled`, `is_deleted`
|
||||
) VALUES (
|
||||
'admin',
|
||||
'admin123',
|
||||
'超级管理员',
|
||||
'admin@example.com',
|
||||
'SYSTEM',
|
||||
1,
|
||||
0
|
||||
);
|
||||
|
||||
-- 关联管理员与角色 (假设 ID 都是 1)
|
||||
INSERT INTO `sys_user_role` (`user_id`, `role_id`) VALUES (1, 1);
|
||||
Reference in New Issue
Block a user