feat: initialize project
This commit is contained in:
13
src/main/java/com/test/demo/DemoApplication.java
Normal file
13
src/main/java/com/test/demo/DemoApplication.java
Normal file
@@ -0,0 +1,13 @@
|
||||
package com.test.demo;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
@SpringBootApplication
|
||||
public class DemoApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(DemoApplication.class, args);
|
||||
}
|
||||
|
||||
}
|
||||
165
src/main/java/com/test/demo/aspect/LogAspect.java
Normal file
165
src/main/java/com/test/demo/aspect/LogAspect.java
Normal file
@@ -0,0 +1,165 @@
|
||||
package com.test.demo.aspect;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.test.demo.bo.LoginBo;
|
||||
import com.test.demo.common.annotation.Log;
|
||||
import com.test.demo.entity.OperLogEntity;
|
||||
import com.test.demo.entity.UserEntity;
|
||||
import com.test.demo.log.event.OperLogEvent;
|
||||
import com.test.demo.security.SecurityUtil;
|
||||
import com.test.demo.service.UserService;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.aspectj.lang.ProceedingJoinPoint;
|
||||
import org.aspectj.lang.annotation.Around;
|
||||
import org.aspectj.lang.annotation.Aspect;
|
||||
import org.aspectj.lang.reflect.MethodSignature;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 操作日志切面
|
||||
*/
|
||||
@Slf4j
|
||||
@Aspect
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class LogAspect {
|
||||
|
||||
private final ApplicationEventPublisher eventPublisher;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final UserService userService;
|
||||
|
||||
@Around("@annotation(logAnnotation)")
|
||||
public Object around(ProceedingJoinPoint point, Log logAnnotation) throws Throwable {
|
||||
long startTime = System.currentTimeMillis();
|
||||
OperLogEntity operLog = new OperLogEntity();
|
||||
operLog.setTitle(logAnnotation.value());
|
||||
operLog.setLogType(logAnnotation.type().name());
|
||||
|
||||
// 请求信息
|
||||
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
||||
if (attributes != null) {
|
||||
HttpServletRequest request = attributes.getRequest();
|
||||
operLog.setRequestUrl(request.getRequestURI());
|
||||
operLog.setRequestMethod(request.getMethod());
|
||||
operLog.setIp(getClientIp(request));
|
||||
}
|
||||
|
||||
// 方法信息
|
||||
MethodSignature signature = (MethodSignature) point.getSignature();
|
||||
operLog.setMethod(signature.getDeclaringTypeName() + "." + signature.getName());
|
||||
|
||||
// 请求参数(脱敏)
|
||||
try {
|
||||
String params = objectMapper.writeValueAsString(point.getArgs());
|
||||
operLog.setRequestParam(truncate(params, 2000));
|
||||
} catch (Exception e) {
|
||||
operLog.setRequestParam("参数序列化失败");
|
||||
}
|
||||
|
||||
// 用户信息
|
||||
try {
|
||||
Long userId = SecurityUtil.getUserIdOrNull();
|
||||
operLog.setUserId(userId);
|
||||
if (userId != null) {
|
||||
UserEntity user = userService.getById(userId);
|
||||
if (user != null) {
|
||||
operLog.setUserName(user.getUsername());
|
||||
}
|
||||
}
|
||||
} catch (Exception ignored) {}
|
||||
|
||||
Object result = null;
|
||||
try {
|
||||
result = point.proceed();
|
||||
operLog.setStatus(1);
|
||||
// 返回结果(脱敏)
|
||||
try {
|
||||
String resultStr = objectMapper.writeValueAsString(result);
|
||||
operLog.setResponseResult(truncate(resultStr, 2000));
|
||||
} catch (Exception ignored) {}
|
||||
} catch (Throwable e) {
|
||||
operLog.setStatus(0);
|
||||
operLog.setErrorMsg(truncate(e.getMessage(), 2000));
|
||||
throw e;
|
||||
} finally {
|
||||
if (operLog.getUserId() == null) {
|
||||
Long userId = SecurityUtil.getUserIdOrNull();
|
||||
if (userId != null) {
|
||||
operLog.setUserId(userId);
|
||||
UserEntity user = userService.getById(userId);
|
||||
if (user != null) {
|
||||
operLog.setUserName(user.getUsername());
|
||||
}
|
||||
}
|
||||
}
|
||||
if (operLog.getUserName() == null) {
|
||||
operLog.setUserName(extractUsername(point.getArgs()));
|
||||
}
|
||||
operLog.setCostTime(System.currentTimeMillis() - startTime);
|
||||
operLog.setCreateTime(LocalDateTime.now());
|
||||
|
||||
// 日志输出
|
||||
switch (logAnnotation.level()) {
|
||||
case DEBUG -> log.debug("[{}] {} 耗时:{}ms", logAnnotation.type(), logAnnotation.value(), operLog.getCostTime());
|
||||
case WARN -> log.warn("[{}] {} 耗时:{}ms", logAnnotation.type(), logAnnotation.value(), operLog.getCostTime());
|
||||
case ERROR -> log.error("[{}] {} 耗时:{}ms", logAnnotation.type(), logAnnotation.value(), operLog.getCostTime());
|
||||
default -> log.info("[{}] {} 耗时:{}ms", logAnnotation.type(), logAnnotation.value(), operLog.getCostTime());
|
||||
}
|
||||
|
||||
// 异步写入数据库
|
||||
if (logAnnotation.saveToDB()) {
|
||||
eventPublisher.publishEvent(new OperLogEvent(this, operLog));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private String getClientIp(HttpServletRequest request) {
|
||||
String ip = request.getHeader("X-Forwarded-For");
|
||||
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
|
||||
ip = request.getHeader("X-Real-IP");
|
||||
}
|
||||
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
|
||||
ip = request.getRemoteAddr();
|
||||
}
|
||||
// 多级代理取第一个
|
||||
if (ip != null && ip.contains(",")) {
|
||||
ip = ip.split(",")[0].trim();
|
||||
}
|
||||
return ip;
|
||||
}
|
||||
|
||||
private String truncate(String str, int maxLen) {
|
||||
if (str == null) return null;
|
||||
return str.length() > maxLen ? str.substring(0, maxLen) : str;
|
||||
}
|
||||
|
||||
private String extractUsername(Object[] args) {
|
||||
if (args == null) {
|
||||
return null;
|
||||
}
|
||||
for (Object arg : args) {
|
||||
if (arg instanceof LoginBo loginBo) {
|
||||
return loginBo.getUsername();
|
||||
}
|
||||
if (arg == null) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
var method = arg.getClass().getMethod("getUsername");
|
||||
Object username = method.invoke(arg);
|
||||
if (username != null) {
|
||||
return String.valueOf(username);
|
||||
}
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
77
src/main/java/com/test/demo/aspect/RateLimitAspect.java
Normal file
77
src/main/java/com/test/demo/aspect/RateLimitAspect.java
Normal file
@@ -0,0 +1,77 @@
|
||||
package com.test.demo.aspect;
|
||||
|
||||
import com.test.demo.common.annotation.RateLimit;
|
||||
import com.test.demo.common.constant.RedisKeyConstant;
|
||||
import com.test.demo.common.exception.BizException;
|
||||
import com.test.demo.common.result.ResultCode;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.aspectj.lang.ProceedingJoinPoint;
|
||||
import org.aspectj.lang.annotation.Around;
|
||||
import org.aspectj.lang.annotation.Aspect;
|
||||
import org.aspectj.lang.reflect.MethodSignature;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.data.redis.core.script.DefaultRedisScript;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 限流切面(Redis 滑动窗口)
|
||||
*/
|
||||
@Slf4j
|
||||
@Aspect
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class RateLimitAspect {
|
||||
|
||||
private final RedisTemplate<String, Object> redisTemplate;
|
||||
|
||||
private static final String LUA_SCRIPT = """
|
||||
local key = KEYS[1]
|
||||
local limit = tonumber(ARGV[1])
|
||||
local window = tonumber(ARGV[2])
|
||||
local now = tonumber(ARGV[3])
|
||||
redis.call('ZREMRANGEBYSCORE', key, 0, now - window * 1000)
|
||||
local count = redis.call('ZCARD', key)
|
||||
if count < limit then
|
||||
redis.call('ZADD', key, now, now .. '-' .. math.random(1000000))
|
||||
redis.call('EXPIRE', key, window)
|
||||
return 1
|
||||
end
|
||||
return 0
|
||||
""";
|
||||
|
||||
@Around("@annotation(rateLimit)")
|
||||
public Object around(ProceedingJoinPoint point, RateLimit rateLimit) throws Throwable {
|
||||
String key = buildKey(point, rateLimit);
|
||||
DefaultRedisScript<Long> script = new DefaultRedisScript<>(LUA_SCRIPT, Long.class);
|
||||
List<String> keys = Collections.singletonList(key);
|
||||
Long result = redisTemplate.execute(script, keys,
|
||||
(long) rateLimit.count(), (long) rateLimit.time(), System.currentTimeMillis());
|
||||
|
||||
if (result == null || result == 0L) {
|
||||
log.warn("限流触发: key={}", key);
|
||||
throw new BizException(ResultCode.RATE_LIMIT);
|
||||
}
|
||||
return point.proceed();
|
||||
}
|
||||
|
||||
private String buildKey(ProceedingJoinPoint point, RateLimit rateLimit) {
|
||||
if (!rateLimit.key().isEmpty()) {
|
||||
return RedisKeyConstant.RATE_LIMIT + rateLimit.key();
|
||||
}
|
||||
MethodSignature signature = (MethodSignature) point.getSignature();
|
||||
String methodName = signature.getDeclaringTypeName() + "." + signature.getName();
|
||||
String ip = "";
|
||||
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
||||
if (attributes != null) {
|
||||
ip = attributes.getRequest().getRemoteAddr();
|
||||
}
|
||||
return RedisKeyConstant.RATE_LIMIT + ip + ":" + methodName;
|
||||
}
|
||||
}
|
||||
49
src/main/java/com/test/demo/bo/EnumItemBo.java
Normal file
49
src/main/java/com/test/demo/bo/EnumItemBo.java
Normal file
@@ -0,0 +1,49 @@
|
||||
package com.test.demo.bo;
|
||||
|
||||
import com.test.demo.common.validate.ValidGroup;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 枚举/字典项请求参数
|
||||
*/
|
||||
@Data
|
||||
@Schema(name = "EnumItemBo", description = "枚举/字典项请求参数")
|
||||
public class EnumItemBo {
|
||||
|
||||
@NotNull(groups = ValidGroup.Update.class, message = "字典项ID不能为空")
|
||||
@Schema(description = "字典项ID", example = "2033103240752513025")
|
||||
private Long id;
|
||||
|
||||
@NotBlank(groups = {ValidGroup.Add.class, ValidGroup.Update.class}, message = "枚举编码不能为空")
|
||||
@Schema(description = "枚举编码", example = "LogType")
|
||||
private String enumCode;
|
||||
|
||||
@NotBlank(groups = ValidGroup.Add.class, message = "字典值不能为空")
|
||||
@Schema(description = "字典值", example = "LOGIN")
|
||||
private String itemValue;
|
||||
|
||||
@NotBlank(groups = {ValidGroup.Add.class, ValidGroup.Update.class}, message = "显示文本不能为空")
|
||||
@Schema(description = "显示文本", example = "登录日志")
|
||||
private String itemLabel;
|
||||
|
||||
@Schema(description = "系统枚举常量名", example = "LOGIN")
|
||||
private String itemName;
|
||||
|
||||
@Schema(description = "是否启用", example = "1")
|
||||
private Integer enabled;
|
||||
|
||||
@Schema(description = "排序", example = "0")
|
||||
private Integer sort;
|
||||
|
||||
@Schema(description = "标签颜色", example = "#409EFF")
|
||||
private String color;
|
||||
|
||||
@Schema(description = "标签类型", example = "success")
|
||||
private String tagType;
|
||||
|
||||
@Schema(description = "备注", example = "系统默认日志类型")
|
||||
private String remark;
|
||||
}
|
||||
41
src/main/java/com/test/demo/bo/EnumTypeBo.java
Normal file
41
src/main/java/com/test/demo/bo/EnumTypeBo.java
Normal file
@@ -0,0 +1,41 @@
|
||||
package com.test.demo.bo;
|
||||
|
||||
import com.test.demo.common.validate.ValidGroup;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 枚举/字典类型请求参数
|
||||
*/
|
||||
@Data
|
||||
@Schema(name = "EnumTypeBo", description = "枚举/字典类型请求参数")
|
||||
public class EnumTypeBo {
|
||||
|
||||
@NotNull(groups = ValidGroup.Update.class, message = "类型ID不能为空")
|
||||
@Schema(description = "类型ID", example = "2033103240660238338")
|
||||
private Long id;
|
||||
|
||||
@NotBlank(groups = ValidGroup.Add.class, message = "枚举编码不能为空")
|
||||
@Schema(description = "枚举编码", example = "LogType")
|
||||
private String enumCode;
|
||||
|
||||
@Schema(description = "枚举名称", example = "日志类型")
|
||||
private String enumName;
|
||||
|
||||
@Schema(description = "枚举描述", example = "日志类型枚举")
|
||||
private String enumDesc;
|
||||
|
||||
@Schema(description = "是否启用", example = "1")
|
||||
private Integer enabled;
|
||||
|
||||
@Schema(description = "是否允许系统同步更新", example = "1")
|
||||
private Integer syncEnabled;
|
||||
|
||||
@Schema(description = "排序", example = "0")
|
||||
private Integer sort;
|
||||
|
||||
@Schema(description = "备注", example = "系统内置日志类型")
|
||||
private String remark;
|
||||
}
|
||||
27
src/main/java/com/test/demo/bo/EnumTypeQueryBo.java
Normal file
27
src/main/java/com/test/demo/bo/EnumTypeQueryBo.java
Normal file
@@ -0,0 +1,27 @@
|
||||
package com.test.demo.bo;
|
||||
|
||||
import com.test.demo.common.base.BaseBo;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/**
|
||||
* 枚举/字典类型查询参数
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Schema(name = "EnumTypeQueryBo", description = "枚举/字典类型查询参数")
|
||||
public class EnumTypeQueryBo extends BaseBo {
|
||||
|
||||
@Schema(description = "枚举编码", example = "LogType")
|
||||
private String enumCode;
|
||||
|
||||
@Schema(description = "枚举名称/描述关键字", example = "日志")
|
||||
private String keyword;
|
||||
|
||||
@Schema(description = "来源类型", example = "SYSTEM", allowableValues = {"SYSTEM", "CUSTOM"})
|
||||
private String sourceType;
|
||||
|
||||
@Schema(description = "是否启用", example = "1")
|
||||
private Integer enabled;
|
||||
}
|
||||
21
src/main/java/com/test/demo/bo/FileQueryBo.java
Normal file
21
src/main/java/com/test/demo/bo/FileQueryBo.java
Normal file
@@ -0,0 +1,21 @@
|
||||
package com.test.demo.bo;
|
||||
|
||||
import com.test.demo.common.base.BaseBo;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/**
|
||||
* 文件查询参数
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Schema(name = "FileQueryBo", description = "文件查询参数")
|
||||
public class FileQueryBo extends BaseBo {
|
||||
|
||||
@Schema(description = "相对目录", example = "2026/03/15")
|
||||
private String relativeDir;
|
||||
|
||||
@Schema(description = "是否递归查询子目录", example = "false", defaultValue = "false")
|
||||
private Boolean recursive = false;
|
||||
}
|
||||
29
src/main/java/com/test/demo/bo/LoginBo.java
Normal file
29
src/main/java/com/test/demo/bo/LoginBo.java
Normal file
@@ -0,0 +1,29 @@
|
||||
package com.test.demo.bo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 登录请求参数
|
||||
*/
|
||||
@Data
|
||||
@Schema(name = "LoginBo", description = "登录请求参数")
|
||||
public class LoginBo {
|
||||
|
||||
@NotBlank(message = "用户名不能为空")
|
||||
@Schema(description = "用户名", example = "admin")
|
||||
private String username;
|
||||
|
||||
@NotBlank(message = "密码不能为空")
|
||||
@Schema(description = "密码", example = "admin123")
|
||||
private String password;
|
||||
|
||||
@NotBlank(message = "验证码Key不能为空")
|
||||
@Schema(description = "验证码Key", example = "8f3b8fd4d2cf4d1ca6a4b8cf7ab33f9d")
|
||||
private String captchaKey;
|
||||
|
||||
@NotBlank(message = "验证码不能为空")
|
||||
@Schema(description = "验证码", example = "a8k3")
|
||||
private String captchaCode;
|
||||
}
|
||||
24
src/main/java/com/test/demo/bo/LoginLogQueryBo.java
Normal file
24
src/main/java/com/test/demo/bo/LoginLogQueryBo.java
Normal file
@@ -0,0 +1,24 @@
|
||||
package com.test.demo.bo;
|
||||
|
||||
import com.test.demo.common.base.BaseBo;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/**
|
||||
* 登录日志查询参数
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Schema(name = "LoginLogQueryBo", description = "登录日志查询参数")
|
||||
public class LoginLogQueryBo extends BaseBo {
|
||||
|
||||
@Schema(description = "用户名", example = "admin")
|
||||
private String username;
|
||||
|
||||
@Schema(description = "日志类型", example = "LOGIN", allowableValues = {"LOGIN", "LOGOUT"})
|
||||
private String logType;
|
||||
|
||||
@Schema(description = "状态", example = "1", allowableValues = {"0", "1"})
|
||||
private Integer status;
|
||||
}
|
||||
54
src/main/java/com/test/demo/bo/MenuBo.java
Normal file
54
src/main/java/com/test/demo/bo/MenuBo.java
Normal file
@@ -0,0 +1,54 @@
|
||||
package com.test.demo.bo;
|
||||
|
||||
import com.test.demo.common.validate.ValidGroup;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 菜单请求参数
|
||||
*/
|
||||
@Data
|
||||
@Schema(name = "MenuBo", description = "菜单请求参数")
|
||||
public class MenuBo {
|
||||
|
||||
@NotNull(groups = ValidGroup.Update.class, message = "菜单ID不能为空")
|
||||
@Schema(description = "菜单ID", example = "1")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "父级ID", example = "0")
|
||||
private Long parentId;
|
||||
|
||||
@NotBlank(groups = {ValidGroup.Add.class, ValidGroup.Update.class}, message = "菜单类型不能为空")
|
||||
@Schema(description = "菜单类型", example = "MENU", allowableValues = {"MENU", "BUTTON"})
|
||||
private String menuType;
|
||||
|
||||
@NotBlank(groups = {ValidGroup.Add.class, ValidGroup.Update.class}, message = "菜单名称不能为空")
|
||||
@Schema(description = "菜单名称", example = "用户管理")
|
||||
private String menuName;
|
||||
|
||||
@Schema(description = "路由路径", example = "/user")
|
||||
private String path;
|
||||
|
||||
@Schema(description = "组件路径", example = "system/user/index")
|
||||
private String component;
|
||||
|
||||
@Schema(description = "权限标识", example = "system:user:page")
|
||||
private String permission;
|
||||
|
||||
@Schema(description = "图标", example = "User")
|
||||
private String icon;
|
||||
|
||||
@Schema(description = "是否显示", example = "1")
|
||||
private Integer visible;
|
||||
|
||||
@Schema(description = "是否启用", example = "1")
|
||||
private Integer enabled;
|
||||
|
||||
@Schema(description = "排序", example = "0")
|
||||
private Integer sort;
|
||||
|
||||
@Schema(description = "备注")
|
||||
private String remark;
|
||||
}
|
||||
27
src/main/java/com/test/demo/bo/OperLogQueryBo.java
Normal file
27
src/main/java/com/test/demo/bo/OperLogQueryBo.java
Normal file
@@ -0,0 +1,27 @@
|
||||
package com.test.demo.bo;
|
||||
|
||||
import com.test.demo.common.base.BaseBo;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/**
|
||||
* 操作日志查询参数
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Schema(name = "OperLogQueryBo", description = "操作日志查询参数")
|
||||
public class OperLogQueryBo extends BaseBo {
|
||||
|
||||
@Schema(description = "操作说明", example = "新增用户")
|
||||
private String title;
|
||||
|
||||
@Schema(description = "日志类型", example = "OPER")
|
||||
private String logType;
|
||||
|
||||
@Schema(description = "操作用户名", example = "admin")
|
||||
private String userName;
|
||||
|
||||
@Schema(description = "状态", example = "1", allowableValues = {"0", "1"})
|
||||
private Integer status;
|
||||
}
|
||||
43
src/main/java/com/test/demo/bo/RegisterBo.java
Normal file
43
src/main/java/com/test/demo/bo/RegisterBo.java
Normal file
@@ -0,0 +1,43 @@
|
||||
package com.test.demo.bo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.Email;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 注册请求参数
|
||||
*/
|
||||
@Data
|
||||
@Schema(name = "RegisterBo", description = "注册请求参数")
|
||||
public class RegisterBo {
|
||||
|
||||
@NotBlank(message = "用户名不能为空")
|
||||
@Schema(description = "用户名", example = "zhangsan")
|
||||
private String username;
|
||||
|
||||
@NotBlank(message = "密码不能为空")
|
||||
@Schema(description = "密码", example = "123456")
|
||||
private String password;
|
||||
|
||||
@Schema(description = "昵称", example = "张三")
|
||||
private String nickname;
|
||||
|
||||
@Email(message = "邮箱格式不正确")
|
||||
@Schema(description = "邮箱", example = "zhangsan@example.com")
|
||||
private String email;
|
||||
|
||||
@Schema(description = "手机号", example = "13800138000")
|
||||
private String phone;
|
||||
|
||||
@Schema(description = "头像路径", example = "/avatar/default.png")
|
||||
private String avatar;
|
||||
|
||||
@NotBlank(message = "验证码Key不能为空")
|
||||
@Schema(description = "验证码Key", example = "8f3b8fd4d2cf4d1ca6a4b8cf7ab33f9d")
|
||||
private String captchaKey;
|
||||
|
||||
@NotBlank(message = "验证码不能为空")
|
||||
@Schema(description = "验证码", example = "a8k3")
|
||||
private String captchaCode;
|
||||
}
|
||||
36
src/main/java/com/test/demo/bo/RoleBo.java
Normal file
36
src/main/java/com/test/demo/bo/RoleBo.java
Normal file
@@ -0,0 +1,36 @@
|
||||
package com.test.demo.bo;
|
||||
|
||||
import com.test.demo.common.validate.ValidGroup;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 角色请求参数
|
||||
*/
|
||||
@Data
|
||||
@Schema(name = "RoleBo", description = "角色请求参数")
|
||||
public class RoleBo {
|
||||
|
||||
@NotNull(groups = ValidGroup.Update.class, message = "角色ID不能为空")
|
||||
@Schema(description = "角色ID", example = "1")
|
||||
private Long id;
|
||||
|
||||
@NotBlank(groups = ValidGroup.Add.class, message = "角色编码不能为空")
|
||||
@Schema(description = "角色编码", example = "ADMIN")
|
||||
private String roleCode;
|
||||
|
||||
@NotBlank(groups = {ValidGroup.Add.class, ValidGroup.Update.class}, message = "角色名称不能为空")
|
||||
@Schema(description = "角色名称", example = "系统管理员")
|
||||
private String roleName;
|
||||
|
||||
@Schema(description = "是否启用", example = "1")
|
||||
private Integer enabled;
|
||||
|
||||
@Schema(description = "排序", example = "0")
|
||||
private Integer sort;
|
||||
|
||||
@Schema(description = "备注")
|
||||
private String remark;
|
||||
}
|
||||
22
src/main/java/com/test/demo/bo/RoleMenuAssignBo.java
Normal file
22
src/main/java/com/test/demo/bo/RoleMenuAssignBo.java
Normal file
@@ -0,0 +1,22 @@
|
||||
package com.test.demo.bo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 角色菜单分配参数
|
||||
*/
|
||||
@Data
|
||||
@Schema(name = "RoleMenuAssignBo", description = "角色菜单分配参数")
|
||||
public class RoleMenuAssignBo {
|
||||
|
||||
@NotNull(message = "角色ID不能为空")
|
||||
@Schema(description = "角色ID", example = "1")
|
||||
private Long roleId;
|
||||
|
||||
@Schema(description = "菜单ID列表")
|
||||
private List<Long> menuIds;
|
||||
}
|
||||
21
src/main/java/com/test/demo/bo/RoleQueryBo.java
Normal file
21
src/main/java/com/test/demo/bo/RoleQueryBo.java
Normal file
@@ -0,0 +1,21 @@
|
||||
package com.test.demo.bo;
|
||||
|
||||
import com.test.demo.common.base.BaseBo;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/**
|
||||
* 角色查询参数
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Schema(name = "RoleQueryBo", description = "角色查询参数")
|
||||
public class RoleQueryBo extends BaseBo {
|
||||
|
||||
@Schema(description = "角色编码/名称关键字", example = "管理员")
|
||||
private String keyword;
|
||||
|
||||
@Schema(description = "是否启用", example = "1")
|
||||
private Integer enabled;
|
||||
}
|
||||
24
src/main/java/com/test/demo/bo/TaskQueryBo.java
Normal file
24
src/main/java/com/test/demo/bo/TaskQueryBo.java
Normal file
@@ -0,0 +1,24 @@
|
||||
package com.test.demo.bo;
|
||||
|
||||
import com.test.demo.common.base.BaseBo;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/**
|
||||
* 定时任务查询参数
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Schema(name = "TaskQueryBo", description = "定时任务查询参数")
|
||||
public class TaskQueryBo extends BaseBo {
|
||||
|
||||
@Schema(description = "任务名称", example = "清理")
|
||||
private String taskName;
|
||||
|
||||
@Schema(description = "Bean名称", example = "fileService")
|
||||
private String beanName;
|
||||
|
||||
@Schema(description = "状态", example = "1", allowableValues = {"0", "1"})
|
||||
private Integer status;
|
||||
}
|
||||
44
src/main/java/com/test/demo/bo/UserBo.java
Normal file
44
src/main/java/com/test/demo/bo/UserBo.java
Normal file
@@ -0,0 +1,44 @@
|
||||
package com.test.demo.bo;
|
||||
|
||||
import com.test.demo.common.validate.ValidGroup;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.Email;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 用户新增/编辑 Bo
|
||||
*/
|
||||
@Data
|
||||
@Schema(name = "UserBo", description = "用户请求参数")
|
||||
public class UserBo {
|
||||
|
||||
@NotNull(groups = ValidGroup.Update.class, message = "用户ID不能为空")
|
||||
@Schema(description = "用户ID(编辑时必填)", example = "1")
|
||||
private Long id;
|
||||
|
||||
@NotBlank(groups = ValidGroup.Add.class, message = "用户名不能为空")
|
||||
@Schema(description = "用户名", example = "admin")
|
||||
private String username;
|
||||
|
||||
@NotBlank(groups = ValidGroup.Add.class, message = "密码不能为空")
|
||||
@Schema(description = "密码", example = "admin123")
|
||||
private String password;
|
||||
|
||||
@Schema(description = "昵称", example = "管理员")
|
||||
private String nickname;
|
||||
|
||||
@Email(message = "邮箱格式不正确")
|
||||
@Schema(description = "邮箱", example = "admin@example.com")
|
||||
private String email;
|
||||
|
||||
@Schema(description = "手机号", example = "13800138000")
|
||||
private String phone;
|
||||
|
||||
@Schema(description = "头像路径", example = "/avatar/admin.png")
|
||||
private String avatar;
|
||||
|
||||
@Schema(description = "状态", example = "1", allowableValues = {"0", "1"})
|
||||
private Integer status;
|
||||
}
|
||||
24
src/main/java/com/test/demo/bo/UserQueryBo.java
Normal file
24
src/main/java/com/test/demo/bo/UserQueryBo.java
Normal file
@@ -0,0 +1,24 @@
|
||||
package com.test.demo.bo;
|
||||
|
||||
import com.test.demo.common.base.BaseBo;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/**
|
||||
* 用户查询 Bo
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Schema(name = "UserQueryBo", description = "用户查询参数")
|
||||
public class UserQueryBo extends BaseBo {
|
||||
|
||||
@Schema(description = "用户名(模糊查询)", example = "admin")
|
||||
private String username;
|
||||
|
||||
@Schema(description = "手机号", example = "13800138000")
|
||||
private String phone;
|
||||
|
||||
@Schema(description = "状态", example = "1", allowableValues = {"0", "1"})
|
||||
private Integer status;
|
||||
}
|
||||
22
src/main/java/com/test/demo/bo/UserRoleAssignBo.java
Normal file
22
src/main/java/com/test/demo/bo/UserRoleAssignBo.java
Normal file
@@ -0,0 +1,22 @@
|
||||
package com.test.demo.bo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 用户角色分配参数
|
||||
*/
|
||||
@Data
|
||||
@Schema(name = "UserRoleAssignBo", description = "用户角色分配参数")
|
||||
public class UserRoleAssignBo {
|
||||
|
||||
@NotNull(message = "用户ID不能为空")
|
||||
@Schema(description = "用户ID", example = "1")
|
||||
private Long userId;
|
||||
|
||||
@Schema(description = "角色ID列表")
|
||||
private List<Long> roleIds;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.test.demo.common.annotation;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import com.test.demo.serializer.DesensitizeSerializer;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
/**
|
||||
* 数据脱敏注解(标注在 VO 字段上)
|
||||
*/
|
||||
@Target(ElementType.FIELD)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
@JacksonAnnotationsInside
|
||||
@JsonSerialize(using = DesensitizeSerializer.class)
|
||||
public @interface Desensitize {
|
||||
|
||||
/**
|
||||
* 脱敏类型
|
||||
*/
|
||||
DesensitizeType type();
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.test.demo.common.annotation;
|
||||
|
||||
/**
|
||||
* 脱敏类型枚举
|
||||
*/
|
||||
public enum DesensitizeType {
|
||||
/** 手机号 */
|
||||
PHONE,
|
||||
/** 身份证 */
|
||||
ID_CARD,
|
||||
/** 邮箱 */
|
||||
EMAIL,
|
||||
/** 银行卡 */
|
||||
BANK_CARD,
|
||||
/** 姓名 */
|
||||
NAME,
|
||||
/** 地址 */
|
||||
ADDRESS
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.test.demo.common.annotation;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
/**
|
||||
* 接口幂等性注解(用于插入类接口)
|
||||
*/
|
||||
@Target(ElementType.METHOD)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
public @interface Idempotent {
|
||||
|
||||
/** 幂等有效期(秒) */
|
||||
int expireTime() default 5;
|
||||
|
||||
/** 提示信息 */
|
||||
String message() default "请勿重复提交";
|
||||
}
|
||||
30
src/main/java/com/test/demo/common/annotation/Log.java
Normal file
30
src/main/java/com/test/demo/common/annotation/Log.java
Normal file
@@ -0,0 +1,30 @@
|
||||
package com.test.demo.common.annotation;
|
||||
|
||||
import com.test.demo.enums.LogType;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
/**
|
||||
* 操作日志注解
|
||||
*/
|
||||
@Target(ElementType.METHOD)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
public @interface Log {
|
||||
|
||||
/** 操作说明(与 Knife4j @Operation summary 保持一致) */
|
||||
String value() default "";
|
||||
|
||||
/** 日志类型 */
|
||||
LogType type() default LogType.OPER;
|
||||
|
||||
/** 是否写入数据库 */
|
||||
boolean saveToDB() default false;
|
||||
|
||||
/** 日志等级 */
|
||||
LogLevel level() default LogLevel.INFO;
|
||||
|
||||
enum LogLevel {
|
||||
DEBUG, INFO, WARN, ERROR
|
||||
}
|
||||
}
|
||||
21
src/main/java/com/test/demo/common/annotation/RateLimit.java
Normal file
21
src/main/java/com/test/demo/common/annotation/RateLimit.java
Normal file
@@ -0,0 +1,21 @@
|
||||
package com.test.demo.common.annotation;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
/**
|
||||
* 限流注解
|
||||
*/
|
||||
@Target(ElementType.METHOD)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
public @interface RateLimit {
|
||||
|
||||
/** 限流次数 */
|
||||
int count() default 10;
|
||||
|
||||
/** 时间窗口(秒) */
|
||||
int time() default 60;
|
||||
|
||||
/** 限流 key(默认 IP+方法名) */
|
||||
String key() default "";
|
||||
}
|
||||
29
src/main/java/com/test/demo/common/base/BaseBo.java
Normal file
29
src/main/java/com/test/demo/common/base/BaseBo.java
Normal file
@@ -0,0 +1,29 @@
|
||||
package com.test.demo.common.base;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.Max;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* Bo 基类(分页参数)
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "分页请求基类")
|
||||
public class BaseBo implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Min(value = 1, message = "页码最小为1")
|
||||
@Schema(description = "页码", example = "1", defaultValue = "1")
|
||||
private Integer pageNum = 1;
|
||||
|
||||
@Min(value = 1, message = "每页条数最小为1")
|
||||
@Max(value = 500, message = "每页条数最大为500")
|
||||
@Schema(description = "每页条数", example = "10", defaultValue = "10")
|
||||
private Integer pageSize = 10;
|
||||
}
|
||||
36
src/main/java/com/test/demo/common/base/BaseEntity.java
Normal file
36
src/main/java/com/test/demo/common/base/BaseEntity.java
Normal file
@@ -0,0 +1,36 @@
|
||||
package com.test.demo.common.base;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* Entity 基类
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "实体基类")
|
||||
public abstract class BaseEntity implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
@Schema(description = "主键ID", example = "2033103240660238338")
|
||||
private Long id;
|
||||
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
@Schema(description = "创建时间")
|
||||
private LocalDateTime createTime;
|
||||
|
||||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||
@Schema(description = "更新时间")
|
||||
private LocalDateTime updateTime;
|
||||
|
||||
@TableLogic
|
||||
@Schema(description = "逻辑删除标记", example = "0")
|
||||
private Integer deleted;
|
||||
}
|
||||
49
src/main/java/com/test/demo/common/base/PageResult.java
Normal file
49
src/main/java/com/test/demo/common/base/PageResult.java
Normal file
@@ -0,0 +1,49 @@
|
||||
package com.test.demo.common.base;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 分页返回封装
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "分页响应结果")
|
||||
public class PageResult<T> implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "当前页数据列表")
|
||||
private List<T> list;
|
||||
@Schema(description = "总记录数", example = "100")
|
||||
private Long total;
|
||||
@Schema(description = "当前页码", example = "1")
|
||||
private Integer pageNum;
|
||||
@Schema(description = "每页条数", example = "10")
|
||||
private Integer pageSize;
|
||||
@Schema(description = "总页数", example = "10")
|
||||
private Integer totalPages;
|
||||
|
||||
public PageResult() {}
|
||||
|
||||
public PageResult(List<T> list, Long total, Integer pageNum, Integer pageSize) {
|
||||
this.list = list;
|
||||
this.total = total;
|
||||
this.pageNum = pageNum;
|
||||
this.pageSize = pageSize;
|
||||
this.totalPages = (int) Math.ceil((double) total / pageSize);
|
||||
}
|
||||
|
||||
public static <T> PageResult<T> of(List<T> list, Long total, Integer pageNum, Integer pageSize) {
|
||||
return new PageResult<>(list, total, pageNum, pageSize);
|
||||
}
|
||||
|
||||
public static <T> PageResult<T> empty(Integer pageNum, Integer pageSize) {
|
||||
return new PageResult<>(Collections.emptyList(), 0L, pageNum, pageSize);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.test.demo.common.constant;
|
||||
|
||||
/**
|
||||
* 通用常量
|
||||
*/
|
||||
public final class CommonConstant {
|
||||
|
||||
private CommonConstant() {}
|
||||
|
||||
/** 成功标记 */
|
||||
public static final int SUCCESS = 200;
|
||||
|
||||
/** 失败标记 */
|
||||
public static final int FAIL = 500;
|
||||
|
||||
/** UTF-8 编码 */
|
||||
public static final String UTF8 = "UTF-8";
|
||||
|
||||
/** 默认日期格式 */
|
||||
public static final String DATE_FORMAT = "yyyy-MM-dd";
|
||||
|
||||
/** 默认日期时间格式 */
|
||||
public static final String DATETIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
|
||||
|
||||
/** 未知用户 */
|
||||
public static final String UNKNOWN_USER = "unknown";
|
||||
|
||||
/** 签名请求头 */
|
||||
public static final String HEADER_TIMESTAMP = "X-Timestamp";
|
||||
public static final String HEADER_NONCE = "X-Nonce";
|
||||
public static final String HEADER_SIGN = "X-Sign";
|
||||
|
||||
/** 幂等性请求头 */
|
||||
public static final String HEADER_IDEMPOTENT_TOKEN = "X-Idempotent-Token";
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.test.demo.common.constant;
|
||||
|
||||
/**
|
||||
* 枚举/字典来源类型
|
||||
*/
|
||||
public final class EnumSourceType {
|
||||
|
||||
private EnumSourceType() {}
|
||||
|
||||
public static final String SYSTEM = "SYSTEM";
|
||||
|
||||
public static final String CUSTOM = "CUSTOM";
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package com.test.demo.common.constant;
|
||||
|
||||
/**
|
||||
* 权限标识常量
|
||||
*/
|
||||
public final class PermissionConstant {
|
||||
|
||||
private PermissionConstant() {}
|
||||
|
||||
public static final String USER_PAGE = "system:user:page";
|
||||
public static final String USER_DETAIL = "system:user:detail";
|
||||
public static final String USER_ADD = "system:user:add";
|
||||
public static final String USER_EDIT = "system:user:edit";
|
||||
public static final String USER_REMOVE = "system:user:remove";
|
||||
public static final String USER_RESET_PASSWORD = "system:user:reset-password";
|
||||
public static final String USER_IMPORT = "system:user:import";
|
||||
public static final String USER_EXPORT = "system:user:export";
|
||||
|
||||
public static final String ROLE_PAGE = "system:role:page";
|
||||
public static final String ROLE_LIST = "system:role:list";
|
||||
public static final String ROLE_ADD = "system:role:add";
|
||||
public static final String ROLE_EDIT = "system:role:edit";
|
||||
public static final String ROLE_REMOVE = "system:role:remove";
|
||||
public static final String ROLE_ASSIGN_MENU = "system:role:assign-menu";
|
||||
public static final String ROLE_ASSIGN_USER = "system:role:assign-user";
|
||||
|
||||
public static final String MENU_LIST = "system:menu:list";
|
||||
public static final String MENU_ADD = "system:menu:add";
|
||||
public static final String MENU_EDIT = "system:menu:edit";
|
||||
public static final String MENU_REMOVE = "system:menu:remove";
|
||||
public static final String MENU_CURRENT = "system:menu:current";
|
||||
|
||||
public static final String ENUM_TYPE_PAGE = "system:enum:type:page";
|
||||
public static final String ENUM_TYPE_ADD = "system:enum:type:add";
|
||||
public static final String ENUM_TYPE_EDIT = "system:enum:type:edit";
|
||||
public static final String ENUM_TYPE_ENABLE = "system:enum:type:enable";
|
||||
public static final String ENUM_TYPE_REMOVE = "system:enum:type:remove";
|
||||
public static final String ENUM_ITEM_LIST = "system:enum:item:list";
|
||||
public static final String ENUM_ITEM_ADD = "system:enum:item:add";
|
||||
public static final String ENUM_ITEM_EDIT = "system:enum:item:edit";
|
||||
public static final String ENUM_ITEM_ENABLE = "system:enum:item:enable";
|
||||
public static final String ENUM_ITEM_REMOVE = "system:enum:item:remove";
|
||||
|
||||
public static final String TASK_PAGE = "system:task:page";
|
||||
public static final String TASK_DETAIL = "system:task:detail";
|
||||
public static final String TASK_ADD = "system:task:add";
|
||||
public static final String TASK_EDIT = "system:task:edit";
|
||||
public static final String TASK_PAUSE = "system:task:pause";
|
||||
public static final String TASK_RESUME = "system:task:resume";
|
||||
public static final String TASK_REMOVE = "system:task:remove";
|
||||
|
||||
public static final String FILE_LIST = "system:file:list";
|
||||
public static final String FILE_UPLOAD = "system:file:upload";
|
||||
public static final String FILE_DOWNLOAD = "system:file:download";
|
||||
public static final String FILE_DELETE = "system:file:delete";
|
||||
|
||||
public static final String LOGIN_LOG_PAGE = "system:login-log:page";
|
||||
public static final String LOGIN_LOG_EXPORT = "system:login-log:export";
|
||||
public static final String OPER_LOG_PAGE = "system:oper-log:page";
|
||||
public static final String OPER_LOG_EXPORT = "system:oper-log:export";
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.test.demo.common.constant;
|
||||
|
||||
/**
|
||||
* Redis Key 常量
|
||||
*/
|
||||
public final class RedisKeyConstant {
|
||||
|
||||
private RedisKeyConstant() {}
|
||||
|
||||
/** 前缀 */
|
||||
private static final String PREFIX = "demo:";
|
||||
|
||||
/** 验证码 */
|
||||
public static final String CAPTCHA = PREFIX + "captcha:";
|
||||
|
||||
/** 限流 */
|
||||
public static final String RATE_LIMIT = PREFIX + "rate_limit:";
|
||||
|
||||
/** 幂等性 */
|
||||
public static final String IDEMPOTENT = PREFIX + "idempotent:";
|
||||
|
||||
/** 防重放 nonce */
|
||||
public static final String NONCE = PREFIX + "nonce:";
|
||||
|
||||
/** 分布式锁 */
|
||||
public static final String LOCK = PREFIX + "lock:";
|
||||
|
||||
/** 用户权限缓存 */
|
||||
public static final String USER_PERMISSION = PREFIX + "user:permission:";
|
||||
|
||||
/** 用户角色缓存 */
|
||||
public static final String USER_ROLE = PREFIX + "user:role:";
|
||||
|
||||
/** 登录失败次数 */
|
||||
public static final String LOGIN_FAIL = PREFIX + "login:fail:";
|
||||
}
|
||||
15
src/main/java/com/test/demo/common/enums/EnumDesc.java
Normal file
15
src/main/java/com/test/demo/common/enums/EnumDesc.java
Normal file
@@ -0,0 +1,15 @@
|
||||
package com.test.demo.common.enums;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
/**
|
||||
* 枚举描述注解 - 标记在枚举类上,提供枚举说明信息
|
||||
*/
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
public @interface EnumDesc {
|
||||
|
||||
/** 枚举说明 */
|
||||
String value() default "";
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package com.test.demo.common.enums;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonParser;
|
||||
import com.fasterxml.jackson.databind.*;
|
||||
import com.fasterxml.jackson.databind.deser.ContextualDeserializer;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* 枚举 Jackson 反序列化器 - 按 value 匹配枚举
|
||||
*/
|
||||
public class EnumDeserializer extends JsonDeserializer<IBaseEnum<?>> implements ContextualDeserializer {
|
||||
|
||||
private Class<?> enumClass;
|
||||
|
||||
public EnumDeserializer() {}
|
||||
|
||||
public EnumDeserializer(Class<?> enumClass) {
|
||||
this.enumClass = enumClass;
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||
public IBaseEnum<?> deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
|
||||
if (enumClass == null) {
|
||||
return null;
|
||||
}
|
||||
String text = p.getText();
|
||||
for (Object constant : enumClass.getEnumConstants()) {
|
||||
IBaseEnum baseEnum = (IBaseEnum) constant;
|
||||
if (String.valueOf(baseEnum.getValue()).equals(text)) {
|
||||
return baseEnum;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonDeserializer<?> createContextual(DeserializationContext ctxt, BeanProperty property) {
|
||||
JavaType type = ctxt.getContextualType();
|
||||
if (type == null && property != null) {
|
||||
type = property.getType();
|
||||
}
|
||||
if (type != null) {
|
||||
Class<?> rawClass = type.getRawClass();
|
||||
if (IBaseEnum.class.isAssignableFrom(rawClass)) {
|
||||
return new EnumDeserializer(rawClass);
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
}
|
||||
134
src/main/java/com/test/demo/common/enums/EnumRegistry.java
Normal file
134
src/main/java/com/test/demo/common/enums/EnumRegistry.java
Normal file
@@ -0,0 +1,134 @@
|
||||
package com.test.demo.common.enums;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.test.demo.common.constant.EnumSourceType;
|
||||
import com.test.demo.entity.EnumItemEntity;
|
||||
import com.test.demo.entity.EnumTypeEntity;
|
||||
import com.test.demo.service.EnumItemService;
|
||||
import com.test.demo.service.EnumTypeService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.ApplicationArguments;
|
||||
import org.springframework.boot.ApplicationRunner;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
|
||||
import org.springframework.core.type.classreading.CachingMetadataReaderFactory;
|
||||
import org.springframework.core.type.classreading.MetadataReader;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 系统枚举注册中心 - 启动时扫描并同步到数据库
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class EnumRegistry implements ApplicationRunner {
|
||||
|
||||
private final EnumTypeService enumTypeService;
|
||||
private final EnumItemService enumItemService;
|
||||
|
||||
@Override
|
||||
public void run(ApplicationArguments args) throws Exception {
|
||||
syncEnums("com.test.demo");
|
||||
}
|
||||
|
||||
private void syncEnums(String basePackage) throws Exception {
|
||||
String pattern = "classpath*:" + basePackage.replace('.', '/') + "/**/enums/**/*.class";
|
||||
PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
|
||||
CachingMetadataReaderFactory factory = new CachingMetadataReaderFactory(resolver);
|
||||
Resource[] resources = resolver.getResources(pattern);
|
||||
int count = 0;
|
||||
|
||||
for (Resource resource : resources) {
|
||||
MetadataReader reader = factory.getMetadataReader(resource);
|
||||
String className = reader.getClassMetadata().getClassName();
|
||||
try {
|
||||
Class<?> clazz = Class.forName(className);
|
||||
if (clazz.isEnum() && IBaseEnum.class.isAssignableFrom(clazz)) {
|
||||
EnumDesc desc = clazz.getAnnotation(EnumDesc.class);
|
||||
if (desc != null) {
|
||||
syncEnum(clazz, desc);
|
||||
count++;
|
||||
}
|
||||
}
|
||||
} catch (ClassNotFoundException e) {
|
||||
log.warn("枚举类加载失败: {}", className);
|
||||
}
|
||||
}
|
||||
log.info("系统枚举同步完成, 共处理 {} 个带注解枚举", count);
|
||||
}
|
||||
|
||||
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
protected void syncEnum(Class<?> clazz, EnumDesc desc) {
|
||||
String enumCode = clazz.getSimpleName();
|
||||
EnumTypeEntity existingType = enumTypeService.getByEnumCode(enumCode);
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
|
||||
if (existingType != null && EnumSourceType.CUSTOM.equals(existingType.getSourceType())) {
|
||||
log.warn("检测到自定义字典与系统枚举编码冲突, 已跳过同步: {}", enumCode);
|
||||
return;
|
||||
}
|
||||
|
||||
if (existingType == null) {
|
||||
existingType = new EnumTypeEntity();
|
||||
existingType.setEnumCode(enumCode);
|
||||
existingType.setSourceType(EnumSourceType.SYSTEM);
|
||||
existingType.setBuiltin(1);
|
||||
existingType.setEnabled(1);
|
||||
existingType.setSyncEnabled(1);
|
||||
existingType.setSort(0);
|
||||
} else if (existingType.getSyncEnabled() != null && existingType.getSyncEnabled() == 0) {
|
||||
log.info("系统枚举已关闭同步更新, 跳过: {}", enumCode);
|
||||
return;
|
||||
}
|
||||
|
||||
existingType.setEnumName(existingType.getEnumName() == null ? enumCode : existingType.getEnumName());
|
||||
existingType.setEnumDesc(desc.value());
|
||||
existingType.setClassName(clazz.getName());
|
||||
existingType.setPackageName(clazz.getPackageName());
|
||||
Object[] constants = clazz.getEnumConstants();
|
||||
Object firstValue = constants.length == 0 ? null : ((IBaseEnum) constants[0]).getValue();
|
||||
existingType.setValueType(firstValue == null ? "java.lang.Object" : firstValue.getClass().getName());
|
||||
existingType.setLastSyncTime(now);
|
||||
|
||||
if (existingType.getId() == null) {
|
||||
enumTypeService.save(existingType);
|
||||
} else {
|
||||
enumTypeService.updateById(existingType);
|
||||
}
|
||||
|
||||
List<EnumItemEntity> systemItems = new ArrayList<>();
|
||||
int sort = 0;
|
||||
for (Object constant : clazz.getEnumConstants()) {
|
||||
IBaseEnum baseEnum = (IBaseEnum) constant;
|
||||
EnumItemEntity item = new EnumItemEntity();
|
||||
item.setEnumCode(enumCode);
|
||||
item.setItemValue(String.valueOf(baseEnum.getValue()));
|
||||
item.setItemLabel(baseEnum.getDesc());
|
||||
item.setItemName(((Enum<?>) constant).name());
|
||||
item.setSourceType(EnumSourceType.SYSTEM);
|
||||
item.setBuiltin(1);
|
||||
item.setEnabled(1);
|
||||
item.setEditable(1);
|
||||
item.setDeletable(0);
|
||||
item.setSort(sort++);
|
||||
item.setLastSyncTime(now);
|
||||
systemItems.add(item);
|
||||
}
|
||||
enumItemService.syncSystemItems(enumCode, systemItems, now);
|
||||
|
||||
long staleCount = enumItemService.count(new LambdaQueryWrapper<EnumItemEntity>()
|
||||
.eq(EnumItemEntity::getEnumCode, enumCode)
|
||||
.eq(EnumItemEntity::getSourceType, EnumSourceType.SYSTEM)
|
||||
.eq(EnumItemEntity::getEnabled, 0));
|
||||
if (staleCount > 0) {
|
||||
log.info("系统枚举 {} 存在 {} 个失效项,已自动置为停用", enumCode, staleCount);
|
||||
}
|
||||
}
|
||||
}
|
||||
22
src/main/java/com/test/demo/common/enums/EnumSerializer.java
Normal file
22
src/main/java/com/test/demo/common/enums/EnumSerializer.java
Normal file
@@ -0,0 +1,22 @@
|
||||
package com.test.demo.common.enums;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonGenerator;
|
||||
import com.fasterxml.jackson.databind.JsonSerializer;
|
||||
import com.fasterxml.jackson.databind.SerializerProvider;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* 枚举 Jackson 序列化器 - 输出 {value, desc}
|
||||
*/
|
||||
public class EnumSerializer extends JsonSerializer<IBaseEnum<?>> {
|
||||
|
||||
@Override
|
||||
public void serialize(IBaseEnum<?> value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
|
||||
gen.writeStartObject();
|
||||
gen.writeFieldName("value");
|
||||
gen.writeObject(value.getValue());
|
||||
gen.writeStringField("desc", value.getDesc());
|
||||
gen.writeEndObject();
|
||||
}
|
||||
}
|
||||
13
src/main/java/com/test/demo/common/enums/IBaseEnum.java
Normal file
13
src/main/java/com/test/demo/common/enums/IBaseEnum.java
Normal file
@@ -0,0 +1,13 @@
|
||||
package com.test.demo.common.enums;
|
||||
|
||||
/**
|
||||
* 枚举基础接口 - 所有业务枚举需实现此接口
|
||||
*/
|
||||
public interface IBaseEnum<T> {
|
||||
|
||||
/** 获取枚举值(存入数据库) */
|
||||
T getValue();
|
||||
|
||||
/** 获取枚举描述 */
|
||||
String getDesc();
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package com.test.demo.common.exception;
|
||||
|
||||
import com.test.demo.common.result.ResultCode;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.io.Serial;
|
||||
|
||||
/**
|
||||
* 自定义业务异常
|
||||
*/
|
||||
@Getter
|
||||
public class BizException extends RuntimeException {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private final int code;
|
||||
|
||||
public BizException(String msg) {
|
||||
super(msg);
|
||||
this.code = ResultCode.INTERNAL_ERROR.getCode();
|
||||
}
|
||||
|
||||
public BizException(int code, String msg) {
|
||||
super(msg);
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
public BizException(ResultCode resultCode) {
|
||||
super(resultCode.getMsg());
|
||||
this.code = resultCode.getCode();
|
||||
}
|
||||
|
||||
public BizException(ResultCode resultCode, String msg) {
|
||||
super(msg);
|
||||
this.code = resultCode.getCode();
|
||||
}
|
||||
|
||||
/**
|
||||
* 快捷抛出异常
|
||||
*/
|
||||
public static void throwEx(String msg) {
|
||||
throw new BizException(msg);
|
||||
}
|
||||
|
||||
public static void throwEx(ResultCode resultCode) {
|
||||
throw new BizException(resultCode);
|
||||
}
|
||||
|
||||
public static void throwEx(int code, String msg) {
|
||||
throw new BizException(code, msg);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
package com.test.demo.common.exception;
|
||||
|
||||
import com.test.demo.common.result.R;
|
||||
import com.test.demo.common.result.ResultCode;
|
||||
import cn.dev33.satoken.exception.NotLoginException;
|
||||
import cn.dev33.satoken.exception.NotPermissionException;
|
||||
import cn.dev33.satoken.exception.NotRoleException;
|
||||
import jakarta.validation.ConstraintViolation;
|
||||
import jakarta.validation.ConstraintViolationException;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.validation.FieldError;
|
||||
import org.springframework.web.HttpRequestMethodNotSupportedException;
|
||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||
import org.springframework.web.bind.MissingServletRequestParameterException;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
import org.springframework.web.multipart.MaxUploadSizeExceededException;
|
||||
import org.springframework.web.servlet.resource.NoResourceFoundException;
|
||||
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 全局异常处理器
|
||||
*/
|
||||
@Slf4j
|
||||
@RestControllerAdvice
|
||||
public class GlobalExceptionHandler {
|
||||
|
||||
@ExceptionHandler(BizException.class)
|
||||
public ResponseEntity<R<Void>> handleBizException(BizException e) {
|
||||
log.warn("业务异常: {}", e.getMessage());
|
||||
return ResponseEntity.status(resolveStatus(e.getCode()))
|
||||
.body(R.fail(e.getCode(), e.getMessage()));
|
||||
}
|
||||
|
||||
@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||
public ResponseEntity<R<Void>> handleValidException(MethodArgumentNotValidException e) {
|
||||
String msg = e.getBindingResult().getFieldErrors().stream()
|
||||
.map(FieldError::getDefaultMessage)
|
||||
.collect(Collectors.joining("; "));
|
||||
log.warn("参数校验失败: {}", msg);
|
||||
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
|
||||
.body(R.fail(ResultCode.BAD_REQUEST, msg));
|
||||
}
|
||||
|
||||
@ExceptionHandler(ConstraintViolationException.class)
|
||||
public ResponseEntity<R<Void>> handleConstraintViolation(ConstraintViolationException e) {
|
||||
String msg = e.getConstraintViolations().stream()
|
||||
.map(ConstraintViolation::getMessage)
|
||||
.collect(Collectors.joining("; "));
|
||||
log.warn("约束校验失败: {}", msg);
|
||||
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
|
||||
.body(R.fail(ResultCode.BAD_REQUEST, msg));
|
||||
}
|
||||
|
||||
@ExceptionHandler(MissingServletRequestParameterException.class)
|
||||
public ResponseEntity<R<Void>> handleMissingParam(MissingServletRequestParameterException e) {
|
||||
log.warn("缺少请求参数: {}", e.getParameterName());
|
||||
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
|
||||
.body(R.fail(ResultCode.BAD_REQUEST, "缺少参数: " + e.getParameterName()));
|
||||
}
|
||||
|
||||
@ExceptionHandler(NotLoginException.class)
|
||||
public ResponseEntity<R<Void>> handleNotLogin(NotLoginException e) {
|
||||
log.warn("未登录访问: {}", e.getMessage());
|
||||
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
|
||||
.body(R.fail(ResultCode.UNAUTHORIZED));
|
||||
}
|
||||
|
||||
@ExceptionHandler(NotPermissionException.class)
|
||||
public ResponseEntity<R<Void>> handleNotPermission(NotPermissionException e) {
|
||||
log.warn("无权限: {}", e.getPermission());
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN)
|
||||
.body(R.fail(ResultCode.FORBIDDEN));
|
||||
}
|
||||
|
||||
@ExceptionHandler(NotRoleException.class)
|
||||
public ResponseEntity<R<Void>> handleNotRole(NotRoleException e) {
|
||||
log.warn("无角色: {}", e.getRole());
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN)
|
||||
.body(R.fail(ResultCode.FORBIDDEN));
|
||||
}
|
||||
|
||||
@ExceptionHandler(MaxUploadSizeExceededException.class)
|
||||
public ResponseEntity<R<Void>> handleMaxUploadSize(MaxUploadSizeExceededException e) {
|
||||
log.warn("上传文件过大: {}", e.getMessage());
|
||||
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
|
||||
.body(R.fail(ResultCode.BAD_REQUEST, "上传文件大小超出限制"));
|
||||
}
|
||||
|
||||
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
|
||||
public ResponseEntity<R<Void>> handleMethodNotSupported(HttpRequestMethodNotSupportedException e) {
|
||||
return ResponseEntity.status(HttpStatus.METHOD_NOT_ALLOWED)
|
||||
.body(R.fail(ResultCode.METHOD_NOT_ALLOWED));
|
||||
}
|
||||
|
||||
@ExceptionHandler(NoResourceFoundException.class)
|
||||
public ResponseEntity<R<Void>> handleNoResource(NoResourceFoundException e) {
|
||||
return ResponseEntity.status(HttpStatus.NOT_FOUND)
|
||||
.body(R.fail(ResultCode.NOT_FOUND));
|
||||
}
|
||||
|
||||
@ExceptionHandler(Exception.class)
|
||||
public ResponseEntity<R<Void>> handleException(Exception e) {
|
||||
log.error("系统异常", e);
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
.body(R.fail(ResultCode.INTERNAL_ERROR));
|
||||
}
|
||||
|
||||
private HttpStatus resolveStatus(int code) {
|
||||
return switch (code) {
|
||||
case 400 -> HttpStatus.BAD_REQUEST;
|
||||
case 401 -> HttpStatus.UNAUTHORIZED;
|
||||
case 403 -> HttpStatus.FORBIDDEN;
|
||||
case 404 -> HttpStatus.NOT_FOUND;
|
||||
case 405 -> HttpStatus.METHOD_NOT_ALLOWED;
|
||||
case 409 -> HttpStatus.CONFLICT;
|
||||
case 415 -> HttpStatus.UNSUPPORTED_MEDIA_TYPE;
|
||||
case 423 -> HttpStatus.LOCKED;
|
||||
case 429 -> HttpStatus.TOO_MANY_REQUESTS;
|
||||
default -> HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
};
|
||||
}
|
||||
}
|
||||
63
src/main/java/com/test/demo/common/result/R.java
Normal file
63
src/main/java/com/test/demo/common/result/R.java
Normal file
@@ -0,0 +1,63 @@
|
||||
package com.test.demo.common.result;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 统一返回结果
|
||||
*/
|
||||
@Data
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public class R<T> implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private int code;
|
||||
private boolean success;
|
||||
private String msg;
|
||||
private T data;
|
||||
private LocalDateTime timestamp;
|
||||
|
||||
private R() {}
|
||||
|
||||
private R(int code, String msg, T data) {
|
||||
this.code = code;
|
||||
this.success = code == ResultCode.SUCCESS.getCode();
|
||||
this.msg = msg;
|
||||
this.data = data;
|
||||
this.timestamp = LocalDateTime.now();
|
||||
}
|
||||
|
||||
public static <T> R<T> ok() {
|
||||
return new R<>(ResultCode.SUCCESS.getCode(), ResultCode.SUCCESS.getMsg(), null);
|
||||
}
|
||||
|
||||
public static <T> R<T> ok(T data) {
|
||||
return new R<>(ResultCode.SUCCESS.getCode(), ResultCode.SUCCESS.getMsg(), data);
|
||||
}
|
||||
|
||||
public static <T> R<T> ok(T data, String msg) {
|
||||
return new R<>(ResultCode.SUCCESS.getCode(), msg, data);
|
||||
}
|
||||
|
||||
public static <T> R<T> fail(String msg) {
|
||||
return new R<>(ResultCode.INTERNAL_ERROR.getCode(), msg, null);
|
||||
}
|
||||
|
||||
public static <T> R<T> fail(ResultCode resultCode) {
|
||||
return new R<>(resultCode.getCode(), resultCode.getMsg(), null);
|
||||
}
|
||||
|
||||
public static <T> R<T> fail(ResultCode resultCode, String msg) {
|
||||
return new R<>(resultCode.getCode(), msg, null);
|
||||
}
|
||||
|
||||
public static <T> R<T> fail(int code, String msg) {
|
||||
return new R<>(code, msg, null);
|
||||
}
|
||||
}
|
||||
30
src/main/java/com/test/demo/common/result/ResultCode.java
Normal file
30
src/main/java/com/test/demo/common/result/ResultCode.java
Normal file
@@ -0,0 +1,30 @@
|
||||
package com.test.demo.common.result;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 返回码枚举
|
||||
*/
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum ResultCode {
|
||||
|
||||
SUCCESS(200, "操作成功"),
|
||||
BAD_REQUEST(400, "请求参数错误"),
|
||||
UNAUTHORIZED(401, "未登录或登录已过期"),
|
||||
FORBIDDEN(403, "无权限访问"),
|
||||
NOT_FOUND(404, "资源不存在"),
|
||||
CONFLICT(409, "资源冲突"),
|
||||
METHOD_NOT_ALLOWED(405, "请求方法不允许"),
|
||||
UNSUPPORTED_MEDIA_TYPE(415, "不支持的资源类型"),
|
||||
ACCOUNT_LOCKED(423, "账号已锁定"),
|
||||
RATE_LIMIT(429, "请求过于频繁"),
|
||||
INTERNAL_ERROR(500, "系统内部错误"),
|
||||
IDEMPOTENT_REJECT(601, "请勿重复提交"),
|
||||
SIGNATURE_INVALID(602, "签名校验失败"),
|
||||
REPLAY_ATTACK(603, "请求已过期或重复");
|
||||
|
||||
private final int code;
|
||||
private final String msg;
|
||||
}
|
||||
19
src/main/java/com/test/demo/common/validate/ValidGroup.java
Normal file
19
src/main/java/com/test/demo/common/validate/ValidGroup.java
Normal file
@@ -0,0 +1,19 @@
|
||||
package com.test.demo.common.validate;
|
||||
|
||||
/**
|
||||
* 参数校验分组
|
||||
*/
|
||||
public interface ValidGroup {
|
||||
|
||||
/** 新增 */
|
||||
interface Add {}
|
||||
|
||||
/** 修改 */
|
||||
interface Update {}
|
||||
|
||||
/** 查询 */
|
||||
interface Query {}
|
||||
|
||||
/** 删除 */
|
||||
interface Delete {}
|
||||
}
|
||||
204
src/main/java/com/test/demo/config/AuthDataInitializer.java
Normal file
204
src/main/java/com/test/demo/config/AuthDataInitializer.java
Normal file
@@ -0,0 +1,204 @@
|
||||
package com.test.demo.config;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.test.demo.common.constant.PermissionConstant;
|
||||
import com.test.demo.entity.MenuEntity;
|
||||
import com.test.demo.entity.RoleEntity;
|
||||
import com.test.demo.entity.RoleMenuEntity;
|
||||
import com.test.demo.entity.UserEntity;
|
||||
import com.test.demo.entity.UserRoleEntity;
|
||||
import com.test.demo.mapper.MenuMapper;
|
||||
import com.test.demo.mapper.RoleMapper;
|
||||
import com.test.demo.mapper.RoleMenuMapper;
|
||||
import com.test.demo.mapper.UserRoleMapper;
|
||||
import com.test.demo.mapper.UserMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.ApplicationArguments;
|
||||
import org.springframework.boot.ApplicationRunner;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 权限基础数据初始化
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class AuthDataInitializer implements ApplicationRunner {
|
||||
|
||||
private final RoleMapper roleMapper;
|
||||
private final MenuMapper menuMapper;
|
||||
private final UserRoleMapper userRoleMapper;
|
||||
private final RoleMenuMapper roleMenuMapper;
|
||||
private final UserMapper userMapper;
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void run(ApplicationArguments args) {
|
||||
RoleEntity adminRole = ensureAdminRole();
|
||||
List<Long> menuIds = ensureMenus();
|
||||
bindRoleMenus(adminRole.getId(), menuIds);
|
||||
bindAdminUser(adminRole.getId());
|
||||
}
|
||||
|
||||
private RoleEntity ensureAdminRole() {
|
||||
RoleEntity role = roleMapper.selectOne(new LambdaQueryWrapper<RoleEntity>()
|
||||
.eq(RoleEntity::getRoleCode, "ADMIN")
|
||||
.last("limit 1"));
|
||||
if (role != null) {
|
||||
return role;
|
||||
}
|
||||
role = new RoleEntity();
|
||||
role.setRoleCode("ADMIN");
|
||||
role.setRoleName("系统管理员");
|
||||
role.setEnabled(1);
|
||||
role.setSort(0);
|
||||
role.setRemark("系统初始化管理员角色");
|
||||
roleMapper.insert(role);
|
||||
return role;
|
||||
}
|
||||
|
||||
private List<Long> ensureMenus() {
|
||||
List<Long> ids = new ArrayList<>();
|
||||
|
||||
Long userRoot = ensureMenu(0L, "MENU", "用户管理", "/user", "system/user/index", null, "User", 1, 1, 10, "用户管理菜单");
|
||||
ids.add(userRoot);
|
||||
ids.add(ensureMenu(userRoot, "BUTTON", "用户分页", null, null, PermissionConstant.USER_PAGE, null, 1, 1, 1, null));
|
||||
ids.add(ensureMenu(userRoot, "BUTTON", "用户详情", null, null, PermissionConstant.USER_DETAIL, null, 1, 1, 2, null));
|
||||
ids.add(ensureMenu(userRoot, "BUTTON", "新增用户", null, null, PermissionConstant.USER_ADD, null, 1, 1, 3, null));
|
||||
ids.add(ensureMenu(userRoot, "BUTTON", "编辑用户", null, null, PermissionConstant.USER_EDIT, null, 1, 1, 4, null));
|
||||
ids.add(ensureMenu(userRoot, "BUTTON", "删除用户", null, null, PermissionConstant.USER_REMOVE, null, 1, 1, 5, null));
|
||||
ids.add(ensureMenu(userRoot, "BUTTON", "重置密码", null, null, PermissionConstant.USER_RESET_PASSWORD, null, 1, 1, 6, null));
|
||||
ids.add(ensureMenu(userRoot, "BUTTON", "导入用户", null, null, PermissionConstant.USER_IMPORT, null, 1, 1, 7, null));
|
||||
ids.add(ensureMenu(userRoot, "BUTTON", "导出用户", null, null, PermissionConstant.USER_EXPORT, null, 1, 1, 8, null));
|
||||
|
||||
Long roleRoot = ensureMenu(0L, "MENU", "角色管理", "/role", "system/role/index", null, "Lock", 1, 1, 20, "角色管理菜单");
|
||||
ids.add(roleRoot);
|
||||
ids.add(ensureMenu(roleRoot, "BUTTON", "角色分页", null, null, PermissionConstant.ROLE_PAGE, null, 1, 1, 1, null));
|
||||
ids.add(ensureMenu(roleRoot, "BUTTON", "角色列表", null, null, PermissionConstant.ROLE_LIST, null, 1, 1, 2, null));
|
||||
ids.add(ensureMenu(roleRoot, "BUTTON", "新增角色", null, null, PermissionConstant.ROLE_ADD, null, 1, 1, 3, null));
|
||||
ids.add(ensureMenu(roleRoot, "BUTTON", "编辑角色", null, null, PermissionConstant.ROLE_EDIT, null, 1, 1, 4, null));
|
||||
ids.add(ensureMenu(roleRoot, "BUTTON", "删除角色", null, null, PermissionConstant.ROLE_REMOVE, null, 1, 1, 5, null));
|
||||
ids.add(ensureMenu(roleRoot, "BUTTON", "分配菜单", null, null, PermissionConstant.ROLE_ASSIGN_MENU, null, 1, 1, 6, null));
|
||||
ids.add(ensureMenu(roleRoot, "BUTTON", "分配用户", null, null, PermissionConstant.ROLE_ASSIGN_USER, null, 1, 1, 7, null));
|
||||
|
||||
Long menuRoot = ensureMenu(0L, "MENU", "菜单管理", "/menu", "system/menu/index", null, "Menu", 1, 1, 30, "菜单权限管理");
|
||||
ids.add(menuRoot);
|
||||
ids.add(ensureMenu(menuRoot, "BUTTON", "菜单列表", null, null, PermissionConstant.MENU_LIST, null, 1, 1, 1, null));
|
||||
ids.add(ensureMenu(menuRoot, "BUTTON", "新增菜单", null, null, PermissionConstant.MENU_ADD, null, 1, 1, 2, null));
|
||||
ids.add(ensureMenu(menuRoot, "BUTTON", "编辑菜单", null, null, PermissionConstant.MENU_EDIT, null, 1, 1, 3, null));
|
||||
ids.add(ensureMenu(menuRoot, "BUTTON", "删除菜单", null, null, PermissionConstant.MENU_REMOVE, null, 1, 1, 4, null));
|
||||
ids.add(ensureMenu(menuRoot, "BUTTON", "当前菜单", null, null, PermissionConstant.MENU_CURRENT, null, 1, 1, 5, null));
|
||||
|
||||
Long enumRoot = ensureMenu(0L, "MENU", "字典管理", "/enum-manage", "system/enum/index", null, "List", 1, 1, 40, "字典管理");
|
||||
ids.add(enumRoot);
|
||||
ids.add(ensureMenu(enumRoot, "BUTTON", "类型分页", null, null, PermissionConstant.ENUM_TYPE_PAGE, null, 1, 1, 1, null));
|
||||
ids.add(ensureMenu(enumRoot, "BUTTON", "新增类型", null, null, PermissionConstant.ENUM_TYPE_ADD, null, 1, 1, 2, null));
|
||||
ids.add(ensureMenu(enumRoot, "BUTTON", "编辑类型", null, null, PermissionConstant.ENUM_TYPE_EDIT, null, 1, 1, 3, null));
|
||||
ids.add(ensureMenu(enumRoot, "BUTTON", "启停类型", null, null, PermissionConstant.ENUM_TYPE_ENABLE, null, 1, 1, 4, null));
|
||||
ids.add(ensureMenu(enumRoot, "BUTTON", "删除类型", null, null, PermissionConstant.ENUM_TYPE_REMOVE, null, 1, 1, 5, null));
|
||||
ids.add(ensureMenu(enumRoot, "BUTTON", "枚举项列表", null, null, PermissionConstant.ENUM_ITEM_LIST, null, 1, 1, 6, null));
|
||||
ids.add(ensureMenu(enumRoot, "BUTTON", "新增枚举项", null, null, PermissionConstant.ENUM_ITEM_ADD, null, 1, 1, 7, null));
|
||||
ids.add(ensureMenu(enumRoot, "BUTTON", "编辑枚举项", null, null, PermissionConstant.ENUM_ITEM_EDIT, null, 1, 1, 8, null));
|
||||
ids.add(ensureMenu(enumRoot, "BUTTON", "启停枚举项", null, null, PermissionConstant.ENUM_ITEM_ENABLE, null, 1, 1, 9, null));
|
||||
ids.add(ensureMenu(enumRoot, "BUTTON", "删除枚举项", null, null, PermissionConstant.ENUM_ITEM_REMOVE, null, 1, 1, 10, null));
|
||||
|
||||
Long taskRoot = ensureMenu(0L, "MENU", "定时任务", "/task", "system/task/index", null, "Clock", 1, 1, 50, "定时任务管理");
|
||||
ids.add(taskRoot);
|
||||
ids.add(ensureMenu(taskRoot, "BUTTON", "任务分页", null, null, PermissionConstant.TASK_PAGE, null, 1, 1, 1, null));
|
||||
ids.add(ensureMenu(taskRoot, "BUTTON", "任务详情", null, null, PermissionConstant.TASK_DETAIL, null, 1, 1, 2, null));
|
||||
ids.add(ensureMenu(taskRoot, "BUTTON", "新增任务", null, null, PermissionConstant.TASK_ADD, null, 1, 1, 3, null));
|
||||
ids.add(ensureMenu(taskRoot, "BUTTON", "编辑任务", null, null, PermissionConstant.TASK_EDIT, null, 1, 1, 4, null));
|
||||
ids.add(ensureMenu(taskRoot, "BUTTON", "暂停任务", null, null, PermissionConstant.TASK_PAUSE, null, 1, 1, 5, null));
|
||||
ids.add(ensureMenu(taskRoot, "BUTTON", "恢复任务", null, null, PermissionConstant.TASK_RESUME, null, 1, 1, 6, null));
|
||||
ids.add(ensureMenu(taskRoot, "BUTTON", "删除任务", null, null, PermissionConstant.TASK_REMOVE, null, 1, 1, 7, null));
|
||||
|
||||
Long fileRoot = ensureMenu(0L, "MENU", "文件管理", "/file", "system/file/index", null, "Folder", 1, 1, 60, "文件管理");
|
||||
ids.add(fileRoot);
|
||||
ids.add(ensureMenu(fileRoot, "BUTTON", "文件列表", null, null, PermissionConstant.FILE_LIST, null, 1, 1, 1, null));
|
||||
ids.add(ensureMenu(fileRoot, "BUTTON", "上传文件", null, null, PermissionConstant.FILE_UPLOAD, null, 1, 1, 2, null));
|
||||
ids.add(ensureMenu(fileRoot, "BUTTON", "下载文件", null, null, PermissionConstant.FILE_DOWNLOAD, null, 1, 1, 3, null));
|
||||
ids.add(ensureMenu(fileRoot, "BUTTON", "删除文件", null, null, PermissionConstant.FILE_DELETE, null, 1, 1, 4, null));
|
||||
|
||||
Long logRoot = ensureMenu(0L, "MENU", "日志管理", "/log", "system/log/index", null, "Document", 1, 1, 70, "日志管理");
|
||||
ids.add(logRoot);
|
||||
ids.add(ensureMenu(logRoot, "BUTTON", "登录日志分页", null, null, PermissionConstant.LOGIN_LOG_PAGE, null, 1, 1, 1, null));
|
||||
ids.add(ensureMenu(logRoot, "BUTTON", "登录日志导出", null, null, PermissionConstant.LOGIN_LOG_EXPORT, null, 1, 1, 2, null));
|
||||
ids.add(ensureMenu(logRoot, "BUTTON", "操作日志分页", null, null, PermissionConstant.OPER_LOG_PAGE, null, 1, 1, 3, null));
|
||||
ids.add(ensureMenu(logRoot, "BUTTON", "操作日志导出", null, null, PermissionConstant.OPER_LOG_EXPORT, null, 1, 1, 4, null));
|
||||
|
||||
return ids;
|
||||
}
|
||||
|
||||
private Long ensureMenu(Long parentId, String menuType, String menuName, String path, String component,
|
||||
String permission, String icon, Integer visible, Integer enabled, Integer sort, String remark) {
|
||||
MenuEntity existing = menuMapper.selectOne(new LambdaQueryWrapper<MenuEntity>()
|
||||
.eq(MenuEntity::getParentId, parentId)
|
||||
.eq(MenuEntity::getMenuName, menuName)
|
||||
.last("limit 1"));
|
||||
if (existing != null) {
|
||||
existing.setMenuType(menuType);
|
||||
existing.setPath(path);
|
||||
existing.setComponent(component);
|
||||
existing.setPermission(permission);
|
||||
existing.setIcon(icon);
|
||||
existing.setVisible(visible);
|
||||
existing.setEnabled(enabled);
|
||||
existing.setSort(sort);
|
||||
existing.setRemark(remark);
|
||||
menuMapper.updateById(existing);
|
||||
return existing.getId();
|
||||
}
|
||||
MenuEntity entity = new MenuEntity();
|
||||
entity.setParentId(parentId);
|
||||
entity.setMenuType(menuType);
|
||||
entity.setMenuName(menuName);
|
||||
entity.setPath(path);
|
||||
entity.setComponent(component);
|
||||
entity.setPermission(permission);
|
||||
entity.setIcon(icon);
|
||||
entity.setVisible(visible);
|
||||
entity.setEnabled(enabled);
|
||||
entity.setSort(sort);
|
||||
entity.setRemark(remark);
|
||||
menuMapper.insert(entity);
|
||||
return entity.getId();
|
||||
}
|
||||
|
||||
private void bindRoleMenus(Long roleId, List<Long> menuIds) {
|
||||
for (Long menuId : menuIds) {
|
||||
Long count = roleMenuMapper.selectCount(new LambdaQueryWrapper<RoleMenuEntity>()
|
||||
.eq(RoleMenuEntity::getRoleId, roleId)
|
||||
.eq(RoleMenuEntity::getMenuId, menuId));
|
||||
if (count == 0) {
|
||||
RoleMenuEntity entity = new RoleMenuEntity();
|
||||
entity.setRoleId(roleId);
|
||||
entity.setMenuId(menuId);
|
||||
roleMenuMapper.insert(entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void bindAdminUser(Long roleId) {
|
||||
UserEntity admin = userMapper.selectOne(new LambdaQueryWrapper<UserEntity>()
|
||||
.eq(UserEntity::getUsername, "admin")
|
||||
.last("limit 1"));
|
||||
if (admin == null) {
|
||||
log.warn("未找到 admin 用户,已跳过管理员角色绑定");
|
||||
return;
|
||||
}
|
||||
Long count = userRoleMapper.selectCount(new LambdaQueryWrapper<UserRoleEntity>()
|
||||
.eq(UserRoleEntity::getUserId, admin.getId())
|
||||
.eq(UserRoleEntity::getRoleId, roleId));
|
||||
if (count == 0) {
|
||||
UserRoleEntity entity = new UserRoleEntity();
|
||||
entity.setUserId(admin.getId());
|
||||
entity.setRoleId(roleId);
|
||||
userRoleMapper.insert(entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
29
src/main/java/com/test/demo/config/CaffeineCacheConfig.java
Normal file
29
src/main/java/com/test/demo/config/CaffeineCacheConfig.java
Normal file
@@ -0,0 +1,29 @@
|
||||
package com.test.demo.config;
|
||||
|
||||
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||
import org.springframework.cache.CacheManager;
|
||||
import org.springframework.cache.annotation.EnableCaching;
|
||||
import org.springframework.cache.caffeine.CaffeineCacheManager;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* Caffeine 本地缓存配置
|
||||
*/
|
||||
@EnableCaching
|
||||
@Configuration
|
||||
public class CaffeineCacheConfig {
|
||||
|
||||
@Bean
|
||||
public CacheManager caffeineCacheManager() {
|
||||
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
|
||||
cacheManager.setCaffeine(Caffeine.newBuilder()
|
||||
.initialCapacity(128)
|
||||
.maximumSize(1024)
|
||||
.expireAfterWrite(30, TimeUnit.MINUTES)
|
||||
.recordStats());
|
||||
return cacheManager;
|
||||
}
|
||||
}
|
||||
27
src/main/java/com/test/demo/config/FileConfig.java
Normal file
27
src/main/java/com/test/demo/config/FileConfig.java
Normal file
@@ -0,0 +1,27 @@
|
||||
package com.test.demo.config;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 文件上传配置
|
||||
*/
|
||||
@Data
|
||||
@Component
|
||||
@ConfigurationProperties(prefix = "app.file")
|
||||
public class FileConfig {
|
||||
|
||||
/** 上传根路径 */
|
||||
private String uploadPath = "/data/upload";
|
||||
|
||||
/** 允许的文件扩展名 */
|
||||
private List<String> allowedExtensions = List.of(
|
||||
"jpg", "jpeg", "png", "gif", "bmp", "webp",
|
||||
"pdf", "doc", "docx", "xls", "xlsx", "ppt", "pptx",
|
||||
"txt", "csv", "zip", "rar", "7z",
|
||||
"mp4", "avi", "mov", "mp3", "wav"
|
||||
);
|
||||
}
|
||||
46
src/main/java/com/test/demo/config/JacksonConfig.java
Normal file
46
src/main/java/com/test/demo/config/JacksonConfig.java
Normal file
@@ -0,0 +1,46 @@
|
||||
package com.test.demo.config;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
|
||||
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
|
||||
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
|
||||
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
|
||||
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
|
||||
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
|
||||
/**
|
||||
* Jackson 全局配置
|
||||
*/
|
||||
@Configuration
|
||||
public class JacksonConfig {
|
||||
|
||||
private static final String DATE_FORMAT = "yyyy-MM-dd";
|
||||
private static final String DATETIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
|
||||
|
||||
@Bean
|
||||
public Jackson2ObjectMapperBuilderCustomizer jacksonCustomizer() {
|
||||
return builder -> {
|
||||
// Long → String 防前端精度丢失
|
||||
builder.serializerByType(Long.class, ToStringSerializer.instance);
|
||||
builder.serializerByType(Long.TYPE, ToStringSerializer.instance);
|
||||
|
||||
// LocalDate 序列化/反序列化
|
||||
builder.serializerByType(LocalDate.class,
|
||||
new LocalDateSerializer(DateTimeFormatter.ofPattern(DATE_FORMAT)));
|
||||
builder.deserializerByType(LocalDate.class,
|
||||
new LocalDateDeserializer(DateTimeFormatter.ofPattern(DATE_FORMAT)));
|
||||
|
||||
// LocalDateTime 序列化/反序列化
|
||||
builder.serializerByType(LocalDateTime.class,
|
||||
new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DATETIME_FORMAT)));
|
||||
builder.deserializerByType(LocalDateTime.class,
|
||||
new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DATETIME_FORMAT)));
|
||||
};
|
||||
}
|
||||
}
|
||||
24
src/main/java/com/test/demo/config/Knife4jConfig.java
Normal file
24
src/main/java/com/test/demo/config/Knife4jConfig.java
Normal file
@@ -0,0 +1,24 @@
|
||||
package com.test.demo.config;
|
||||
|
||||
import io.swagger.v3.oas.models.OpenAPI;
|
||||
import io.swagger.v3.oas.models.info.Contact;
|
||||
import io.swagger.v3.oas.models.info.Info;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* Knife4j / OpenAPI 配置
|
||||
*/
|
||||
@Configuration
|
||||
public class Knife4jConfig {
|
||||
|
||||
@Bean
|
||||
public OpenAPI openAPI() {
|
||||
return new OpenAPI()
|
||||
.info(new Info()
|
||||
.title("Demo 后台管理系统 API")
|
||||
.description("Spring Boot 4 脚手架接口文档")
|
||||
.version("1.0.0")
|
||||
.contact(new Contact().name("admin").email("admin@example.com")));
|
||||
}
|
||||
}
|
||||
27
src/main/java/com/test/demo/config/MybatisPlusConfig.java
Normal file
27
src/main/java/com/test/demo/config/MybatisPlusConfig.java
Normal file
@@ -0,0 +1,27 @@
|
||||
package com.test.demo.config;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.DbType;
|
||||
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
|
||||
import com.baomidou.mybatisplus.extension.plugins.inner.BlockAttackInnerInterceptor;
|
||||
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* MyBatis-Plus 配置
|
||||
*/
|
||||
@Configuration
|
||||
public class MybatisPlusConfig {
|
||||
|
||||
@Bean
|
||||
public MybatisPlusInterceptor mybatisPlusInterceptor() {
|
||||
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
|
||||
// 分页插件
|
||||
PaginationInnerInterceptor paginationInterceptor = new PaginationInnerInterceptor(DbType.MYSQL);
|
||||
paginationInterceptor.setMaxLimit(500L);
|
||||
interceptor.addInnerInterceptor(paginationInterceptor);
|
||||
// 防全表更新与删除
|
||||
interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor());
|
||||
return interceptor;
|
||||
}
|
||||
}
|
||||
31
src/main/java/com/test/demo/config/RedisConfig.java
Normal file
31
src/main/java/com/test/demo/config/RedisConfig.java
Normal file
@@ -0,0 +1,31 @@
|
||||
package com.test.demo.config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
|
||||
import org.springframework.data.redis.serializer.StringRedisSerializer;
|
||||
|
||||
/**
|
||||
* Redis 序列化配置
|
||||
*/
|
||||
@Configuration
|
||||
public class RedisConfig {
|
||||
|
||||
@Bean
|
||||
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
|
||||
RedisTemplate<String, Object> template = new RedisTemplate<>();
|
||||
template.setConnectionFactory(factory);
|
||||
|
||||
StringRedisSerializer stringSerializer = new StringRedisSerializer();
|
||||
GenericJackson2JsonRedisSerializer jsonSerializer = new GenericJackson2JsonRedisSerializer();
|
||||
|
||||
template.setKeySerializer(stringSerializer);
|
||||
template.setHashKeySerializer(stringSerializer);
|
||||
template.setValueSerializer(jsonSerializer);
|
||||
template.setHashValueSerializer(jsonSerializer);
|
||||
template.afterPropertiesSet();
|
||||
return template;
|
||||
}
|
||||
}
|
||||
13
src/main/java/com/test/demo/config/SaTokenConfig.java
Normal file
13
src/main/java/com/test/demo/config/SaTokenConfig.java
Normal file
@@ -0,0 +1,13 @@
|
||||
package com.test.demo.config;
|
||||
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* Sa-Token 配置
|
||||
*/
|
||||
@Configuration
|
||||
public class SaTokenConfig {
|
||||
|
||||
// 如需 JWT 模式,需引入 sa-token-jwt 依赖后启用以下配置
|
||||
// 默认使用 Redis Session 模式
|
||||
}
|
||||
25
src/main/java/com/test/demo/config/ScheduleConfig.java
Normal file
25
src/main/java/com/test/demo/config/ScheduleConfig.java
Normal file
@@ -0,0 +1,25 @@
|
||||
package com.test.demo.config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.scheduling.TaskScheduler;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
|
||||
|
||||
/**
|
||||
* 定时任务调度配置
|
||||
*/
|
||||
@EnableScheduling
|
||||
@Configuration
|
||||
public class ScheduleConfig {
|
||||
|
||||
@Bean
|
||||
public TaskScheduler taskScheduler() {
|
||||
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
|
||||
scheduler.setPoolSize(5);
|
||||
scheduler.setThreadNamePrefix("schedule-");
|
||||
scheduler.setWaitForTasksToCompleteOnShutdown(true);
|
||||
scheduler.setAwaitTerminationSeconds(30);
|
||||
return scheduler;
|
||||
}
|
||||
}
|
||||
32
src/main/java/com/test/demo/config/ThreadPoolConfig.java
Normal file
32
src/main/java/com/test/demo/config/ThreadPoolConfig.java
Normal file
@@ -0,0 +1,32 @@
|
||||
package com.test.demo.config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.scheduling.annotation.EnableAsync;
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
||||
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
|
||||
/**
|
||||
* 线程池配置
|
||||
*/
|
||||
@EnableAsync
|
||||
@Configuration
|
||||
public class ThreadPoolConfig {
|
||||
|
||||
@Bean("asyncExecutor")
|
||||
public Executor asyncExecutor() {
|
||||
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
|
||||
executor.setCorePoolSize(Runtime.getRuntime().availableProcessors());
|
||||
executor.setMaxPoolSize(Runtime.getRuntime().availableProcessors() * 2);
|
||||
executor.setQueueCapacity(500);
|
||||
executor.setKeepAliveSeconds(60);
|
||||
executor.setThreadNamePrefix("async-");
|
||||
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
|
||||
executor.setWaitForTasksToCompleteOnShutdown(true);
|
||||
executor.setAwaitTerminationSeconds(30);
|
||||
executor.initialize();
|
||||
return executor;
|
||||
}
|
||||
}
|
||||
65
src/main/java/com/test/demo/config/WebMvcConfig.java
Normal file
65
src/main/java/com/test/demo/config/WebMvcConfig.java
Normal file
@@ -0,0 +1,65 @@
|
||||
package com.test.demo.config;
|
||||
|
||||
import cn.dev33.satoken.interceptor.SaInterceptor;
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import com.test.demo.interceptor.IdempotentInterceptor;
|
||||
import com.test.demo.interceptor.SignatureInterceptor;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.servlet.config.annotation.CorsRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
/**
|
||||
* Web MVC 配置(跨域 + 拦截器)
|
||||
*/
|
||||
@Configuration
|
||||
@RequiredArgsConstructor
|
||||
public class WebMvcConfig implements WebMvcConfigurer {
|
||||
|
||||
private final SignatureInterceptor signatureInterceptor;
|
||||
|
||||
private final IdempotentInterceptor idempotentInterceptor;
|
||||
|
||||
@Override
|
||||
public void addCorsMappings(CorsRegistry registry) {
|
||||
registry.addMapping("/**")
|
||||
.allowedOriginPatterns("*")
|
||||
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
|
||||
.allowedHeaders("*")
|
||||
.allowCredentials(true)
|
||||
.maxAge(3600);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addInterceptors(InterceptorRegistry registry) {
|
||||
registry.addInterceptor(signatureInterceptor)
|
||||
.addPathPatterns("/**")
|
||||
.excludePathPatterns(
|
||||
"/doc.html",
|
||||
"/webjars/**",
|
||||
"/v3/api-docs/**",
|
||||
"/swagger-resources/**",
|
||||
"/actuator/**"
|
||||
);
|
||||
|
||||
registry.addInterceptor(idempotentInterceptor)
|
||||
.addPathPatterns("/**");
|
||||
|
||||
// Sa-Token 路由拦截
|
||||
registry.addInterceptor(new SaInterceptor(handle -> StpUtil.checkLogin()))
|
||||
.addPathPatterns("/**")
|
||||
.excludePathPatterns(
|
||||
"/auth/login",
|
||||
"/auth/register",
|
||||
"/captcha/**",
|
||||
"/file/preview/**",
|
||||
"/enums/**",
|
||||
"/doc.html",
|
||||
"/webjars/**",
|
||||
"/v3/api-docs/**",
|
||||
"/swagger-resources/**",
|
||||
"/actuator/**"
|
||||
);
|
||||
}
|
||||
}
|
||||
30
src/main/java/com/test/demo/config/XssFilterConfig.java
Normal file
30
src/main/java/com/test/demo/config/XssFilterConfig.java
Normal file
@@ -0,0 +1,30 @@
|
||||
package com.test.demo.config;
|
||||
|
||||
import com.test.demo.filter.XssFilter;
|
||||
import org.springframework.boot.web.servlet.FilterRegistrationBean;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* XSS 过滤器注册
|
||||
*/
|
||||
@Configuration
|
||||
public class XssFilterConfig {
|
||||
|
||||
@Bean
|
||||
public FilterRegistrationBean<XssFilter> xssFilterRegistration() {
|
||||
FilterRegistrationBean<XssFilter> registration = new FilterRegistrationBean<>();
|
||||
registration.setFilter(new XssFilter());
|
||||
registration.addUrlPatterns("/*");
|
||||
registration.setName("xssFilter");
|
||||
registration.setOrder(1);
|
||||
// 排除路径(如富文本接口)
|
||||
Map<String, String> initParams = new HashMap<>();
|
||||
initParams.put("excludes", "/file/upload");
|
||||
registration.setInitParameters(initParams);
|
||||
return registration;
|
||||
}
|
||||
}
|
||||
51
src/main/java/com/test/demo/controller/AuthController.java
Normal file
51
src/main/java/com/test/demo/controller/AuthController.java
Normal file
@@ -0,0 +1,51 @@
|
||||
package com.test.demo.controller;
|
||||
|
||||
import com.test.demo.bo.LoginBo;
|
||||
import com.test.demo.bo.RegisterBo;
|
||||
import com.test.demo.common.result.R;
|
||||
import com.test.demo.service.AuthService;
|
||||
import com.test.demo.vo.LoginVo;
|
||||
import com.test.demo.vo.UserVo;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
/**
|
||||
* 认证接口
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/auth")
|
||||
@RequiredArgsConstructor
|
||||
@Tag(name = "认证管理")
|
||||
public class AuthController {
|
||||
|
||||
private final AuthService authService;
|
||||
|
||||
@PostMapping("/login")
|
||||
@Operation(summary = "用户登录")
|
||||
public R<LoginVo> login(@RequestBody @Valid LoginBo bo) {
|
||||
return R.ok(authService.login(bo));
|
||||
}
|
||||
|
||||
@PostMapping("/register")
|
||||
@Operation(summary = "用户注册")
|
||||
public R<Void> register(@RequestBody @Valid RegisterBo bo) {
|
||||
authService.register(bo);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
@PostMapping("/logout")
|
||||
@Operation(summary = "退出登录")
|
||||
public R<Void> logout() {
|
||||
authService.logout();
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
@PostMapping("/me")
|
||||
@Operation(summary = "获取当前登录用户")
|
||||
public R<UserVo> me() {
|
||||
return R.ok(authService.getCurrentUser());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.test.demo.controller;
|
||||
|
||||
import com.test.demo.common.result.R;
|
||||
import com.test.demo.service.CaptchaService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 验证码接口
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/captcha")
|
||||
@RequiredArgsConstructor
|
||||
@Tag(name = "验证码")
|
||||
public class CaptchaController {
|
||||
|
||||
private final CaptchaService captchaService;
|
||||
|
||||
@PostMapping("/generate")
|
||||
@Operation(summary = "获取图形验证码")
|
||||
public R<Map<String, String>> getCaptcha() {
|
||||
return R.ok(captchaService.generate());
|
||||
}
|
||||
}
|
||||
43
src/main/java/com/test/demo/controller/EnumController.java
Normal file
43
src/main/java/com/test/demo/controller/EnumController.java
Normal file
@@ -0,0 +1,43 @@
|
||||
package com.test.demo.controller;
|
||||
|
||||
import com.test.demo.common.result.R;
|
||||
import com.test.demo.service.EnumItemService;
|
||||
import com.test.demo.service.EnumTypeService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 枚举查询接口(供前端下拉框使用)
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/enums")
|
||||
@RequiredArgsConstructor
|
||||
@Tag(name = "枚举查询")
|
||||
public class EnumController {
|
||||
|
||||
private final EnumTypeService enumTypeService;
|
||||
private final EnumItemService enumItemService;
|
||||
|
||||
@PostMapping("/list")
|
||||
@Operation(summary = "获取所有带描述注解的枚举元信息")
|
||||
public R<List<Map<String, Object>>> listAll() {
|
||||
return R.ok(enumTypeService.listEnabledForApi());
|
||||
}
|
||||
|
||||
@PostMapping("/items/{enumName}")
|
||||
@Operation(summary = "获取指定枚举的所有值")
|
||||
public R<List<Map<String, Object>>> getByName(
|
||||
@Parameter(description = "枚举编码", example = "LogType")
|
||||
@PathVariable String enumName) {
|
||||
return R.ok(enumItemService.listEnabledForApi(enumName));
|
||||
}
|
||||
}
|
||||
149
src/main/java/com/test/demo/controller/EnumManageController.java
Normal file
149
src/main/java/com/test/demo/controller/EnumManageController.java
Normal file
@@ -0,0 +1,149 @@
|
||||
package com.test.demo.controller;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import com.test.demo.bo.EnumItemBo;
|
||||
import com.test.demo.bo.EnumTypeBo;
|
||||
import com.test.demo.bo.EnumTypeQueryBo;
|
||||
import com.test.demo.common.annotation.Log;
|
||||
import com.test.demo.common.annotation.Idempotent;
|
||||
import com.test.demo.common.base.PageResult;
|
||||
import com.test.demo.common.constant.PermissionConstant;
|
||||
import com.test.demo.common.result.R;
|
||||
import com.test.demo.common.validate.ValidGroup;
|
||||
import com.test.demo.entity.EnumItemEntity;
|
||||
import com.test.demo.entity.EnumTypeEntity;
|
||||
import com.test.demo.enums.LogType;
|
||||
import com.test.demo.service.EnumItemService;
|
||||
import com.test.demo.service.EnumTypeService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 枚举/字典管理接口
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/enum-manage")
|
||||
@RequiredArgsConstructor
|
||||
@Tag(name = "枚举字典管理")
|
||||
public class EnumManageController {
|
||||
|
||||
private final EnumTypeService enumTypeService;
|
||||
private final EnumItemService enumItemService;
|
||||
|
||||
@PostMapping("/type/page")
|
||||
@Operation(summary = "分页查询枚举类型")
|
||||
@SaCheckPermission(PermissionConstant.ENUM_TYPE_PAGE)
|
||||
public R<PageResult<EnumTypeEntity>> pageTypes(@RequestBody(required = false) EnumTypeQueryBo query) {
|
||||
if (query == null) {
|
||||
query = new EnumTypeQueryBo();
|
||||
}
|
||||
return R.ok(enumTypeService.page(query));
|
||||
}
|
||||
|
||||
@PostMapping("/type/add")
|
||||
@Operation(summary = "新增自定义枚举类型")
|
||||
@SaCheckPermission(PermissionConstant.ENUM_TYPE_ADD)
|
||||
@Idempotent(message = "请勿重复新增枚举类型")
|
||||
@Log(value = "新增自定义枚举类型", type = LogType.OPER, saveToDB = true)
|
||||
public R<Void> addType(@RequestBody @Validated(ValidGroup.Add.class) EnumTypeBo bo) {
|
||||
enumTypeService.add(bo);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
@PostMapping("/type/edit")
|
||||
@Operation(summary = "编辑枚举类型")
|
||||
@SaCheckPermission(PermissionConstant.ENUM_TYPE_EDIT)
|
||||
@Idempotent(message = "请勿重复编辑枚举类型")
|
||||
@Log(value = "编辑枚举类型", type = LogType.OPER, saveToDB = true)
|
||||
public R<Void> editType(@RequestBody @Validated(ValidGroup.Update.class) EnumTypeBo bo) {
|
||||
enumTypeService.edit(bo);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
@PostMapping("/type/enable/{id}")
|
||||
@Operation(summary = "启用或停用枚举类型")
|
||||
@SaCheckPermission(PermissionConstant.ENUM_TYPE_ENABLE)
|
||||
@Idempotent(message = "请勿重复变更枚举类型状态")
|
||||
@Log(value = "变更枚举类型状态", type = LogType.OPER, saveToDB = true)
|
||||
public R<Void> changeTypeEnabled(
|
||||
@Parameter(description = "类型ID", example = "2033103240660238338")
|
||||
@PathVariable Long id,
|
||||
@Parameter(description = "启用状态,1启用,0停用", example = "1")
|
||||
@RequestParam Integer enabled) {
|
||||
enumTypeService.changeEnabled(id, enabled);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
@PostMapping("/type/remove/{id}")
|
||||
@Operation(summary = "删除自定义枚举类型")
|
||||
@SaCheckPermission(PermissionConstant.ENUM_TYPE_REMOVE)
|
||||
@Idempotent(message = "请勿重复删除枚举类型")
|
||||
@Log(value = "删除自定义枚举类型", type = LogType.OPER, saveToDB = true)
|
||||
public R<Void> removeType(
|
||||
@Parameter(description = "类型ID", example = "2033103240660238338")
|
||||
@PathVariable Long id) {
|
||||
enumTypeService.remove(id);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
@PostMapping("/item/list/{enumCode}")
|
||||
@Operation(summary = "查询枚举项列表")
|
||||
@SaCheckPermission(PermissionConstant.ENUM_ITEM_LIST)
|
||||
public R<List<EnumItemEntity>> listItems(
|
||||
@Parameter(description = "枚举编码", example = "LogType")
|
||||
@PathVariable String enumCode) {
|
||||
return R.ok(enumItemService.listManageItems(enumCode));
|
||||
}
|
||||
|
||||
@PostMapping("/item/add")
|
||||
@Operation(summary = "新增枚举项")
|
||||
@SaCheckPermission(PermissionConstant.ENUM_ITEM_ADD)
|
||||
@Idempotent(message = "请勿重复新增枚举项")
|
||||
@Log(value = "新增枚举项", type = LogType.OPER, saveToDB = true)
|
||||
public R<Void> addItem(@RequestBody @Validated(ValidGroup.Add.class) EnumItemBo bo) {
|
||||
enumItemService.add(bo);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
@PostMapping("/item/edit")
|
||||
@Operation(summary = "编辑枚举项")
|
||||
@SaCheckPermission(PermissionConstant.ENUM_ITEM_EDIT)
|
||||
@Idempotent(message = "请勿重复编辑枚举项")
|
||||
@Log(value = "编辑枚举项", type = LogType.OPER, saveToDB = true)
|
||||
public R<Void> editItem(@RequestBody @Validated(ValidGroup.Update.class) EnumItemBo bo) {
|
||||
enumItemService.edit(bo);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
@PostMapping("/item/enable/{id}")
|
||||
@Operation(summary = "启用或停用枚举项")
|
||||
@SaCheckPermission(PermissionConstant.ENUM_ITEM_ENABLE)
|
||||
@Idempotent(message = "请勿重复变更枚举项状态")
|
||||
@Log(value = "变更枚举项状态", type = LogType.OPER, saveToDB = true)
|
||||
public R<Void> changeItemEnabled(
|
||||
@Parameter(description = "枚举项ID", example = "2033103240752513025")
|
||||
@PathVariable Long id,
|
||||
@Parameter(description = "启用状态,1启用,0停用", example = "1")
|
||||
@RequestParam Integer enabled) {
|
||||
enumItemService.changeEnabled(id, enabled);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
@PostMapping("/item/remove/{id}")
|
||||
@Operation(summary = "删除枚举项")
|
||||
@SaCheckPermission(PermissionConstant.ENUM_ITEM_REMOVE)
|
||||
@Idempotent(message = "请勿重复删除枚举项")
|
||||
@Log(value = "删除枚举项", type = LogType.OPER, saveToDB = true)
|
||||
public R<Void> removeItem(
|
||||
@Parameter(description = "枚举项ID", example = "2033103240752513025")
|
||||
@PathVariable Long id) {
|
||||
enumItemService.remove(id);
|
||||
return R.ok();
|
||||
}
|
||||
}
|
||||
122
src/main/java/com/test/demo/controller/FileController.java
Normal file
122
src/main/java/com/test/demo/controller/FileController.java
Normal file
@@ -0,0 +1,122 @@
|
||||
package com.test.demo.controller;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import com.test.demo.bo.FileQueryBo;
|
||||
import com.test.demo.common.annotation.Log;
|
||||
import com.test.demo.common.base.PageResult;
|
||||
import com.test.demo.common.constant.PermissionConstant;
|
||||
import com.test.demo.common.result.R;
|
||||
import com.test.demo.enums.LogType;
|
||||
import com.test.demo.service.FileService;
|
||||
import com.test.demo.vo.FileInfoVo;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.*;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
|
||||
/**
|
||||
* 文件管理接口
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/file")
|
||||
@RequiredArgsConstructor
|
||||
@Tag(name = "文件管理")
|
||||
public class FileController {
|
||||
|
||||
private final FileService fileService;
|
||||
|
||||
@PostMapping("/list")
|
||||
@Operation(summary = "查询文件列表")
|
||||
@SaCheckPermission(PermissionConstant.FILE_LIST)
|
||||
public R<PageResult<FileInfoVo>> list(@RequestBody(required = false) FileQueryBo query) {
|
||||
return R.ok(fileService.list(query));
|
||||
}
|
||||
|
||||
@PostMapping("/upload")
|
||||
@Operation(summary = "上传文件")
|
||||
@SaCheckPermission(PermissionConstant.FILE_UPLOAD)
|
||||
@Log(value = "上传文件", type = LogType.OPER)
|
||||
public R<String> upload(
|
||||
@Parameter(description = "上传文件")
|
||||
@RequestParam("file") MultipartFile file) {
|
||||
return R.ok(fileService.upload(file));
|
||||
}
|
||||
|
||||
@GetMapping("/download/{*filePath}")
|
||||
@Operation(summary = "下载文件")
|
||||
@SaCheckPermission(PermissionConstant.FILE_DOWNLOAD)
|
||||
public void download(
|
||||
@Parameter(description = "文件相对路径", example = "2026/03/15/demo.xlsx")
|
||||
@PathVariable String filePath,
|
||||
HttpServletResponse response) throws IOException {
|
||||
String absPath = fileService.getFilePath(filePath);
|
||||
Path path = Paths.get(absPath);
|
||||
if (!Files.exists(path)) {
|
||||
response.sendError(HttpServletResponse.SC_NOT_FOUND);
|
||||
return;
|
||||
}
|
||||
String fileName = path.getFileName().toString();
|
||||
response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
|
||||
response.setHeader(HttpHeaders.CONTENT_DISPOSITION,
|
||||
"attachment; filename=" + URLEncoder.encode(fileName, StandardCharsets.UTF_8));
|
||||
response.setContentLengthLong(Files.size(path));
|
||||
try (InputStream is = Files.newInputStream(path);
|
||||
OutputStream os = response.getOutputStream()) {
|
||||
is.transferTo(os);
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/preview/{*filePath}")
|
||||
@Operation(summary = "预览文件")
|
||||
public void preview(
|
||||
@Parameter(description = "文件相对路径", example = "2026/03/15/demo.xlsx")
|
||||
@PathVariable String filePath,
|
||||
HttpServletResponse response) throws IOException {
|
||||
String absPath = fileService.getFilePath(filePath);
|
||||
Path path = Paths.get(absPath);
|
||||
if (!Files.exists(path)) {
|
||||
response.sendError(HttpServletResponse.SC_NOT_FOUND);
|
||||
return;
|
||||
}
|
||||
String contentType = Files.probeContentType(path);
|
||||
if (contentType == null) {
|
||||
contentType = MediaType.APPLICATION_OCTET_STREAM_VALUE;
|
||||
}
|
||||
response.setContentType(contentType);
|
||||
response.setContentLengthLong(Files.size(path));
|
||||
|
||||
// 视频支持 Range 请求
|
||||
if (contentType.startsWith("video/")) {
|
||||
response.setHeader(HttpHeaders.ACCEPT_RANGES, "bytes");
|
||||
}
|
||||
|
||||
try (InputStream is = Files.newInputStream(path);
|
||||
OutputStream os = response.getOutputStream()) {
|
||||
is.transferTo(os);
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping("/delete/{*filePath}")
|
||||
@Operation(summary = "删除文件")
|
||||
@SaCheckPermission(PermissionConstant.FILE_DELETE)
|
||||
@Log(value = "删除文件", type = LogType.OPER, saveToDB = true)
|
||||
public R<Boolean> delete(
|
||||
@Parameter(description = "文件相对路径", example = "2026/03/15/demo.xlsx")
|
||||
@PathVariable String filePath) {
|
||||
return R.ok(fileService.delete(filePath));
|
||||
}
|
||||
}
|
||||
79
src/main/java/com/test/demo/controller/LogController.java
Normal file
79
src/main/java/com/test/demo/controller/LogController.java
Normal file
@@ -0,0 +1,79 @@
|
||||
package com.test.demo.controller;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import com.test.demo.bo.LoginLogQueryBo;
|
||||
import com.test.demo.bo.OperLogQueryBo;
|
||||
import com.test.demo.common.base.PageResult;
|
||||
import com.test.demo.common.constant.PermissionConstant;
|
||||
import com.test.demo.common.result.R;
|
||||
import com.test.demo.entity.LoginLogEntity;
|
||||
import com.test.demo.entity.OperLogEntity;
|
||||
import com.test.demo.service.LoginLogService;
|
||||
import com.test.demo.service.OperLogService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* 日志管理接口
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/log")
|
||||
@RequiredArgsConstructor
|
||||
@Tag(name = "日志管理")
|
||||
public class LogController {
|
||||
|
||||
private final LoginLogService loginLogService;
|
||||
private final OperLogService operLogService;
|
||||
|
||||
@PostMapping("/login/page")
|
||||
@Operation(summary = "分页查询登录日志")
|
||||
@SaCheckPermission(PermissionConstant.LOGIN_LOG_PAGE)
|
||||
public R<PageResult<LoginLogEntity>> loginPage(@RequestBody(required = false) LoginLogQueryBo query) {
|
||||
if (query == null) {
|
||||
query = new LoginLogQueryBo();
|
||||
}
|
||||
return R.ok(loginLogService.page(query));
|
||||
}
|
||||
|
||||
@PostMapping("/oper/page")
|
||||
@Operation(summary = "分页查询操作日志")
|
||||
@SaCheckPermission(PermissionConstant.OPER_LOG_PAGE)
|
||||
public R<PageResult<OperLogEntity>> operPage(@RequestBody(required = false) OperLogQueryBo query) {
|
||||
if (query == null) {
|
||||
query = new OperLogQueryBo();
|
||||
}
|
||||
return R.ok(operLogService.page(query));
|
||||
}
|
||||
|
||||
@PostMapping("/login/export")
|
||||
@Operation(summary = "导出登录日志")
|
||||
@SaCheckPermission(PermissionConstant.LOGIN_LOG_EXPORT)
|
||||
public void exportLogin(
|
||||
@Parameter(description = "登录日志筛选条件")
|
||||
@RequestBody(required = false) LoginLogQueryBo query,
|
||||
HttpServletResponse response) throws IOException {
|
||||
if (query == null) {
|
||||
query = new LoginLogQueryBo();
|
||||
}
|
||||
loginLogService.export(query, response);
|
||||
}
|
||||
|
||||
@PostMapping("/oper/export")
|
||||
@Operation(summary = "导出操作日志")
|
||||
@SaCheckPermission(PermissionConstant.OPER_LOG_EXPORT)
|
||||
public void exportOper(
|
||||
@Parameter(description = "操作日志筛选条件")
|
||||
@RequestBody(required = false) OperLogQueryBo query,
|
||||
HttpServletResponse response) throws IOException {
|
||||
if (query == null) {
|
||||
query = new OperLogQueryBo();
|
||||
}
|
||||
operLogService.export(query, response);
|
||||
}
|
||||
}
|
||||
81
src/main/java/com/test/demo/controller/MenuController.java
Normal file
81
src/main/java/com/test/demo/controller/MenuController.java
Normal file
@@ -0,0 +1,81 @@
|
||||
package com.test.demo.controller;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import com.test.demo.bo.MenuBo;
|
||||
import com.test.demo.common.annotation.Log;
|
||||
import com.test.demo.common.annotation.Idempotent;
|
||||
import com.test.demo.common.constant.PermissionConstant;
|
||||
import com.test.demo.common.result.R;
|
||||
import com.test.demo.common.validate.ValidGroup;
|
||||
import com.test.demo.entity.MenuEntity;
|
||||
import com.test.demo.enums.LogType;
|
||||
import com.test.demo.security.SecurityUtil;
|
||||
import com.test.demo.service.MenuService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 菜单权限管理接口
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/menu")
|
||||
@RequiredArgsConstructor
|
||||
@Tag(name = "菜单权限管理")
|
||||
public class MenuController {
|
||||
|
||||
private final MenuService menuService;
|
||||
|
||||
@PostMapping("/list")
|
||||
@Operation(summary = "查询菜单权限列表")
|
||||
@SaCheckPermission(PermissionConstant.MENU_LIST)
|
||||
public R<List<MenuEntity>> list() {
|
||||
return R.ok(menuService.listAll());
|
||||
}
|
||||
|
||||
@PostMapping("/current")
|
||||
@Operation(summary = "查询当前用户菜单")
|
||||
public R<List<MenuEntity>> current() {
|
||||
return R.ok(menuService.listCurrentUserMenus(SecurityUtil.getUserId()));
|
||||
}
|
||||
|
||||
@PostMapping("/permissions/current")
|
||||
@Operation(summary = "查询当前用户权限标识")
|
||||
public R<List<String>> currentPermissions() {
|
||||
return R.ok(menuService.listCurrentUserPermissions(SecurityUtil.getUserId()));
|
||||
}
|
||||
|
||||
@PostMapping("/add")
|
||||
@Operation(summary = "新增菜单权限")
|
||||
@SaCheckPermission(PermissionConstant.MENU_ADD)
|
||||
@Idempotent(message = "请勿重复新增菜单")
|
||||
@Log(value = "新增菜单权限", type = LogType.OPER, saveToDB = true)
|
||||
public R<Void> add(@RequestBody @Validated(ValidGroup.Add.class) MenuBo bo) {
|
||||
menuService.add(bo);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
@PostMapping("/edit")
|
||||
@Operation(summary = "编辑菜单权限")
|
||||
@SaCheckPermission(PermissionConstant.MENU_EDIT)
|
||||
@Idempotent(message = "请勿重复编辑菜单")
|
||||
@Log(value = "编辑菜单权限", type = LogType.OPER, saveToDB = true)
|
||||
public R<Void> edit(@RequestBody @Validated(ValidGroup.Update.class) MenuBo bo) {
|
||||
menuService.edit(bo);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
@PostMapping("/remove/{id}")
|
||||
@Operation(summary = "删除菜单权限")
|
||||
@SaCheckPermission(PermissionConstant.MENU_REMOVE)
|
||||
@Idempotent(message = "请勿重复删除菜单")
|
||||
@Log(value = "删除菜单权限", type = LogType.OPER, saveToDB = true)
|
||||
public R<Void> remove(@PathVariable Long id) {
|
||||
menuService.remove(id);
|
||||
return R.ok();
|
||||
}
|
||||
}
|
||||
117
src/main/java/com/test/demo/controller/RoleController.java
Normal file
117
src/main/java/com/test/demo/controller/RoleController.java
Normal file
@@ -0,0 +1,117 @@
|
||||
package com.test.demo.controller;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import com.test.demo.bo.RoleBo;
|
||||
import com.test.demo.bo.RoleMenuAssignBo;
|
||||
import com.test.demo.bo.RoleQueryBo;
|
||||
import com.test.demo.bo.UserRoleAssignBo;
|
||||
import com.test.demo.common.annotation.Log;
|
||||
import com.test.demo.common.annotation.Idempotent;
|
||||
import com.test.demo.common.base.PageResult;
|
||||
import com.test.demo.common.constant.PermissionConstant;
|
||||
import com.test.demo.common.result.R;
|
||||
import com.test.demo.common.validate.ValidGroup;
|
||||
import com.test.demo.entity.RoleEntity;
|
||||
import com.test.demo.enums.LogType;
|
||||
import com.test.demo.service.RoleService;
|
||||
import com.test.demo.vo.RoleDetailVo;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 角色管理接口
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/role")
|
||||
@RequiredArgsConstructor
|
||||
@Tag(name = "角色管理")
|
||||
public class RoleController {
|
||||
|
||||
private final RoleService roleService;
|
||||
|
||||
@PostMapping("/page")
|
||||
@Operation(summary = "分页查询角色")
|
||||
@SaCheckPermission(PermissionConstant.ROLE_PAGE)
|
||||
public R<PageResult<RoleEntity>> page(@RequestBody(required = false) RoleQueryBo query) {
|
||||
if (query == null) {
|
||||
query = new RoleQueryBo();
|
||||
}
|
||||
return R.ok(roleService.page(query));
|
||||
}
|
||||
|
||||
@PostMapping("/list")
|
||||
@Operation(summary = "查询启用角色列表")
|
||||
@SaCheckPermission(PermissionConstant.ROLE_LIST)
|
||||
public R<List<RoleEntity>> list() {
|
||||
return R.ok(roleService.listEnabled());
|
||||
}
|
||||
|
||||
@PostMapping("/detail/{id}")
|
||||
@Operation(summary = "查询角色详情及已分配菜单")
|
||||
@SaCheckPermission(PermissionConstant.ROLE_LIST)
|
||||
public R<RoleDetailVo> detail(@PathVariable Long id) {
|
||||
return R.ok(roleService.getDetail(id));
|
||||
}
|
||||
|
||||
@PostMapping("/user/{userId}/role-ids")
|
||||
@Operation(summary = "查询用户已分配角色ID列表")
|
||||
@SaCheckPermission(PermissionConstant.ROLE_ASSIGN_USER)
|
||||
public R<List<Long>> userRoleIds(@PathVariable Long userId) {
|
||||
return R.ok(roleService.listUserRoleIds(userId));
|
||||
}
|
||||
|
||||
@PostMapping("/add")
|
||||
@Operation(summary = "新增角色")
|
||||
@SaCheckPermission(PermissionConstant.ROLE_ADD)
|
||||
@Idempotent(message = "请勿重复新增角色")
|
||||
@Log(value = "新增角色", type = LogType.OPER, saveToDB = true)
|
||||
public R<Void> add(@RequestBody @Validated(ValidGroup.Add.class) RoleBo bo) {
|
||||
roleService.add(bo);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
@PostMapping("/edit")
|
||||
@Operation(summary = "编辑角色")
|
||||
@SaCheckPermission(PermissionConstant.ROLE_EDIT)
|
||||
@Idempotent(message = "请勿重复编辑角色")
|
||||
@Log(value = "编辑角色", type = LogType.OPER, saveToDB = true)
|
||||
public R<Void> edit(@RequestBody @Validated(ValidGroup.Update.class) RoleBo bo) {
|
||||
roleService.edit(bo);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
@PostMapping("/remove/{id}")
|
||||
@Operation(summary = "删除角色")
|
||||
@SaCheckPermission(PermissionConstant.ROLE_REMOVE)
|
||||
@Idempotent(message = "请勿重复删除角色")
|
||||
@Log(value = "删除角色", type = LogType.OPER, saveToDB = true)
|
||||
public R<Void> remove(@PathVariable Long id) {
|
||||
roleService.remove(id);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
@PostMapping("/assign-menu")
|
||||
@Operation(summary = "分配角色菜单权限")
|
||||
@SaCheckPermission(PermissionConstant.ROLE_ASSIGN_MENU)
|
||||
@Idempotent(message = "请勿重复分配角色菜单")
|
||||
@Log(value = "分配角色菜单权限", type = LogType.OPER, saveToDB = true)
|
||||
public R<Void> assignMenu(@RequestBody @Validated RoleMenuAssignBo bo) {
|
||||
roleService.assignMenus(bo.getRoleId(), bo.getMenuIds());
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
@PostMapping("/assign-user")
|
||||
@Operation(summary = "分配用户角色")
|
||||
@SaCheckPermission(PermissionConstant.ROLE_ASSIGN_USER)
|
||||
@Idempotent(message = "请勿重复分配用户角色")
|
||||
@Log(value = "分配用户角色", type = LogType.OPER, saveToDB = true)
|
||||
public R<Void> assignUser(@RequestBody @Validated UserRoleAssignBo bo) {
|
||||
roleService.assignUserRoles(bo.getUserId(), bo.getRoleIds());
|
||||
return R.ok();
|
||||
}
|
||||
}
|
||||
95
src/main/java/com/test/demo/controller/TaskController.java
Normal file
95
src/main/java/com/test/demo/controller/TaskController.java
Normal file
@@ -0,0 +1,95 @@
|
||||
package com.test.demo.controller;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import com.test.demo.bo.TaskQueryBo;
|
||||
import com.test.demo.common.annotation.Log;
|
||||
import com.test.demo.common.annotation.Idempotent;
|
||||
import com.test.demo.common.base.PageResult;
|
||||
import com.test.demo.common.constant.PermissionConstant;
|
||||
import com.test.demo.common.result.R;
|
||||
import com.test.demo.entity.ScheduledTaskEntity;
|
||||
import com.test.demo.enums.LogType;
|
||||
import com.test.demo.service.DynamicTaskService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
/**
|
||||
* 定时任务管理接口
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/task")
|
||||
@RequiredArgsConstructor
|
||||
@Tag(name = "定时任务管理")
|
||||
public class TaskController {
|
||||
|
||||
private final DynamicTaskService dynamicTaskService;
|
||||
|
||||
@PostMapping("/page")
|
||||
@Operation(summary = "分页查询定时任务")
|
||||
@SaCheckPermission(PermissionConstant.TASK_PAGE)
|
||||
public R<PageResult<ScheduledTaskEntity>> page(@RequestBody(required = false) TaskQueryBo query) {
|
||||
if (query == null) {
|
||||
query = new TaskQueryBo();
|
||||
}
|
||||
return R.ok(dynamicTaskService.page(query));
|
||||
}
|
||||
|
||||
@PostMapping("/detail/{id}")
|
||||
@Operation(summary = "获取定时任务详情")
|
||||
@SaCheckPermission(PermissionConstant.TASK_DETAIL)
|
||||
public R<ScheduledTaskEntity> detail(@PathVariable Long id) {
|
||||
return R.ok(dynamicTaskService.detail(id));
|
||||
}
|
||||
|
||||
@PostMapping("/add")
|
||||
@Operation(summary = "创建定时任务")
|
||||
@SaCheckPermission(PermissionConstant.TASK_ADD)
|
||||
@Idempotent(message = "请勿重复创建定时任务")
|
||||
@Log(value = "创建定时任务", type = LogType.OPER, saveToDB = true)
|
||||
public R<Void> create(@RequestBody ScheduledTaskEntity task) {
|
||||
dynamicTaskService.create(task);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
@PostMapping("/edit")
|
||||
@Operation(summary = "更新定时任务")
|
||||
@SaCheckPermission(PermissionConstant.TASK_EDIT)
|
||||
@Idempotent(message = "请勿重复更新定时任务")
|
||||
@Log(value = "更新定时任务", type = LogType.OPER, saveToDB = true)
|
||||
public R<Void> update(@RequestBody ScheduledTaskEntity task) {
|
||||
dynamicTaskService.update(task);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
@PostMapping("/pause/{id}")
|
||||
@Operation(summary = "暂停定时任务")
|
||||
@SaCheckPermission(PermissionConstant.TASK_PAUSE)
|
||||
@Idempotent(message = "请勿重复暂停定时任务")
|
||||
@Log(value = "暂停定时任务", type = LogType.OPER, saveToDB = true)
|
||||
public R<Void> pause(@PathVariable Long id) {
|
||||
dynamicTaskService.pause(id);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
@PostMapping("/resume/{id}")
|
||||
@Operation(summary = "恢复定时任务")
|
||||
@SaCheckPermission(PermissionConstant.TASK_RESUME)
|
||||
@Idempotent(message = "请勿重复恢复定时任务")
|
||||
@Log(value = "恢复定时任务", type = LogType.OPER, saveToDB = true)
|
||||
public R<Void> resume(@PathVariable Long id) {
|
||||
dynamicTaskService.resume(id);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
@PostMapping("/remove/{id}")
|
||||
@Operation(summary = "删除定时任务")
|
||||
@SaCheckPermission(PermissionConstant.TASK_REMOVE)
|
||||
@Idempotent(message = "请勿重复删除定时任务")
|
||||
@Log(value = "删除定时任务", type = LogType.OPER, saveToDB = true)
|
||||
public R<Void> delete(@PathVariable Long id) {
|
||||
dynamicTaskService.delete(id);
|
||||
return R.ok();
|
||||
}
|
||||
}
|
||||
125
src/main/java/com/test/demo/controller/UserController.java
Normal file
125
src/main/java/com/test/demo/controller/UserController.java
Normal file
@@ -0,0 +1,125 @@
|
||||
package com.test.demo.controller;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import com.test.demo.common.annotation.Log;
|
||||
import com.test.demo.common.annotation.RateLimit;
|
||||
import com.test.demo.common.annotation.Idempotent;
|
||||
import com.test.demo.common.base.PageResult;
|
||||
import com.test.demo.common.constant.PermissionConstant;
|
||||
import com.test.demo.common.result.R;
|
||||
import com.test.demo.common.validate.ValidGroup;
|
||||
import com.test.demo.enums.LogType;
|
||||
import com.test.demo.bo.UserBo;
|
||||
import com.test.demo.bo.UserQueryBo;
|
||||
import com.test.demo.service.UserService;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import com.test.demo.vo.UserVo;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
/**
|
||||
* 用户管理接口
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/user")
|
||||
@RequiredArgsConstructor
|
||||
@Tag(name = "用户管理")
|
||||
public class UserController {
|
||||
|
||||
private final UserService userService;
|
||||
|
||||
@PostMapping("/page")
|
||||
@Operation(summary = "分页查询用户")
|
||||
@SaCheckPermission(PermissionConstant.USER_PAGE)
|
||||
@Log(value = "分页查询用户", type = LogType.OPER)
|
||||
@RateLimit(count = 20, time = 60)
|
||||
public R<PageResult<UserVo>> page(@RequestBody(required = false) UserQueryBo query) {
|
||||
if (query == null) {
|
||||
query = new UserQueryBo();
|
||||
}
|
||||
return R.ok(userService.page(query));
|
||||
}
|
||||
|
||||
@PostMapping("/detail/{id}")
|
||||
@Operation(summary = "获取用户详情")
|
||||
@SaCheckPermission(PermissionConstant.USER_DETAIL)
|
||||
public R<UserVo> detail(
|
||||
@Parameter(description = "用户ID", example = "1")
|
||||
@PathVariable Long id) {
|
||||
return R.ok(userService.getDetail(id));
|
||||
}
|
||||
|
||||
@PostMapping("/add")
|
||||
@Operation(summary = "新增用户")
|
||||
@SaCheckPermission(PermissionConstant.USER_ADD)
|
||||
@Idempotent(message = "请勿重复新增用户")
|
||||
@Log(value = "新增用户", type = LogType.OPER, saveToDB = true)
|
||||
public R<Void> add(@RequestBody @Validated(ValidGroup.Add.class) UserBo bo) {
|
||||
userService.add(bo);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
@PostMapping("/edit")
|
||||
@Operation(summary = "编辑用户")
|
||||
@SaCheckPermission(PermissionConstant.USER_EDIT)
|
||||
@Idempotent(message = "请勿重复编辑用户")
|
||||
@Log(value = "编辑用户", type = LogType.OPER, saveToDB = true)
|
||||
public R<Void> edit(@RequestBody @Validated(ValidGroup.Update.class) UserBo bo) {
|
||||
userService.edit(bo);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
@PostMapping("/remove/{id}")
|
||||
@Operation(summary = "删除用户")
|
||||
@SaCheckPermission(PermissionConstant.USER_REMOVE)
|
||||
@Idempotent(message = "请勿重复删除用户")
|
||||
@Log(value = "删除用户", type = LogType.OPER, saveToDB = true)
|
||||
public R<Void> remove(
|
||||
@Parameter(description = "用户ID", example = "1")
|
||||
@PathVariable Long id) {
|
||||
userService.remove(id);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
@PostMapping("/reset-password/{id}")
|
||||
@Operation(summary = "重置密码")
|
||||
@SaCheckPermission(PermissionConstant.USER_RESET_PASSWORD)
|
||||
@Idempotent(message = "请勿重复重置密码")
|
||||
@Log(value = "重置密码", type = LogType.OPER, saveToDB = true)
|
||||
public R<Void> resetPassword(
|
||||
@Parameter(description = "用户ID", example = "1")
|
||||
@PathVariable Long id,
|
||||
@Parameter(description = "新密码", example = "123456")
|
||||
@RequestParam String newPassword) {
|
||||
userService.resetPassword(id, newPassword);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
@PostMapping("/import")
|
||||
@Operation(summary = "导入用户")
|
||||
@SaCheckPermission(PermissionConstant.USER_IMPORT)
|
||||
@Idempotent(message = "请勿重复导入用户")
|
||||
@Log(value = "导入用户", type = LogType.IMPORT, saveToDB = true)
|
||||
public R<Void> importUsers(
|
||||
@Parameter(description = "用户导入Excel文件")
|
||||
@RequestParam("file") MultipartFile file) {
|
||||
userService.importUsers(file);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
@PostMapping("/export")
|
||||
@Operation(summary = "导出用户")
|
||||
@SaCheckPermission(PermissionConstant.USER_EXPORT)
|
||||
@Log(value = "导出用户", type = LogType.EXPORT, saveToDB = true)
|
||||
public void exportUsers(@RequestBody(required = false) UserQueryBo query, HttpServletResponse response) throws Exception {
|
||||
if (query == null) {
|
||||
query = new UserQueryBo();
|
||||
}
|
||||
userService.exportUsers(query, response);
|
||||
}
|
||||
}
|
||||
33
src/main/java/com/test/demo/convert/UserConvert.java
Normal file
33
src/main/java/com/test/demo/convert/UserConvert.java
Normal file
@@ -0,0 +1,33 @@
|
||||
package com.test.demo.convert;
|
||||
|
||||
import com.test.demo.bo.UserBo;
|
||||
import com.test.demo.entity.UserEntity;
|
||||
import com.test.demo.vo.UserVo;
|
||||
import org.mapstruct.Mapper;
|
||||
import org.mapstruct.Mapping;
|
||||
import org.mapstruct.MappingTarget;
|
||||
import org.mapstruct.factory.Mappers;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 用户对象转换器(MapStruct编译时生成实现)
|
||||
*/
|
||||
@Mapper(componentModel = "spring")
|
||||
public interface UserConvert {
|
||||
|
||||
UserConvert INSTANCE = Mappers.getMapper(UserConvert.class);
|
||||
|
||||
@Mapping(target = "statusDesc", expression = "java(entity.getStatus() != null ? entity.getStatus().getDesc() : null)")
|
||||
@Mapping(target = "status", expression = "java(entity.getStatus() != null ? entity.getStatus().getValue() : null)")
|
||||
UserVo toVo(UserEntity entity);
|
||||
|
||||
List<UserVo> toVoList(List<UserEntity> list);
|
||||
|
||||
@Mapping(target = "id", ignore = true)
|
||||
@Mapping(target = "status", ignore = true)
|
||||
UserEntity toEntity(UserBo bo);
|
||||
|
||||
@Mapping(target = "status", ignore = true)
|
||||
void updateEntity(UserBo bo, @MappingTarget UserEntity entity);
|
||||
}
|
||||
74
src/main/java/com/test/demo/entity/EnumItemEntity.java
Normal file
74
src/main/java/com/test/demo/entity/EnumItemEntity.java
Normal file
@@ -0,0 +1,74 @@
|
||||
package com.test.demo.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.FieldFill;
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 枚举/字典项实体
|
||||
*/
|
||||
@Data
|
||||
@TableName("sys_enum_item")
|
||||
@Schema(name = "EnumItemEntity", description = "枚举/字典项实体")
|
||||
public class EnumItemEntity {
|
||||
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
@Schema(description = "主键ID", example = "2033103240752513025")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "枚举编码", example = "LogType")
|
||||
private String enumCode;
|
||||
|
||||
@Schema(description = "字典值", example = "LOGIN")
|
||||
private String itemValue;
|
||||
|
||||
@Schema(description = "显示文本", example = "登录日志")
|
||||
private String itemLabel;
|
||||
|
||||
@Schema(description = "系统枚举常量名", example = "LOGIN")
|
||||
private String itemName;
|
||||
|
||||
@Schema(description = "来源类型", example = "SYSTEM", allowableValues = {"SYSTEM", "CUSTOM"})
|
||||
private String sourceType;
|
||||
|
||||
@Schema(description = "是否系统内置", example = "1")
|
||||
private Integer builtin;
|
||||
|
||||
@Schema(description = "是否启用", example = "1")
|
||||
private Integer enabled;
|
||||
|
||||
@Schema(description = "是否允许后台编辑", example = "1")
|
||||
private Integer editable;
|
||||
|
||||
@Schema(description = "是否允许后台删除", example = "0")
|
||||
private Integer deletable;
|
||||
|
||||
@Schema(description = "排序", example = "0")
|
||||
private Integer sort;
|
||||
|
||||
@Schema(description = "标签颜色", example = "#409EFF")
|
||||
private String color;
|
||||
|
||||
@Schema(description = "标签类型", example = "success")
|
||||
private String tagType;
|
||||
|
||||
@Schema(description = "备注", example = "系统默认项")
|
||||
private String remark;
|
||||
|
||||
@Schema(description = "最近同步时间")
|
||||
private LocalDateTime lastSyncTime;
|
||||
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
@Schema(description = "创建时间")
|
||||
private LocalDateTime createTime;
|
||||
|
||||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||
@Schema(description = "更新时间")
|
||||
private LocalDateTime updateTime;
|
||||
}
|
||||
71
src/main/java/com/test/demo/entity/EnumTypeEntity.java
Normal file
71
src/main/java/com/test/demo/entity/EnumTypeEntity.java
Normal file
@@ -0,0 +1,71 @@
|
||||
package com.test.demo.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.FieldFill;
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 枚举/字典类型实体
|
||||
*/
|
||||
@Data
|
||||
@TableName("sys_enum_type")
|
||||
@Schema(name = "EnumTypeEntity", description = "枚举/字典类型实体")
|
||||
public class EnumTypeEntity {
|
||||
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
@Schema(description = "主键ID", example = "2033103240660238338")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "枚举编码", example = "LogType")
|
||||
private String enumCode;
|
||||
|
||||
@Schema(description = "枚举名称", example = "日志类型")
|
||||
private String enumName;
|
||||
|
||||
@Schema(description = "枚举描述", example = "日志类型枚举")
|
||||
private String enumDesc;
|
||||
|
||||
@Schema(description = "系统枚举完整类名", example = "com.test.demo.enums.LogType")
|
||||
private String className;
|
||||
|
||||
@Schema(description = "包路径", example = "com.test.demo.enums")
|
||||
private String packageName;
|
||||
|
||||
@Schema(description = "值类型", example = "java.lang.String")
|
||||
private String valueType;
|
||||
|
||||
@Schema(description = "来源类型", example = "SYSTEM", allowableValues = {"SYSTEM", "CUSTOM"})
|
||||
private String sourceType;
|
||||
|
||||
@Schema(description = "是否系统内置", example = "1")
|
||||
private Integer builtin;
|
||||
|
||||
@Schema(description = "是否启用", example = "1")
|
||||
private Integer enabled;
|
||||
|
||||
@Schema(description = "是否允许系统同步更新", example = "1")
|
||||
private Integer syncEnabled;
|
||||
|
||||
@Schema(description = "排序", example = "0")
|
||||
private Integer sort;
|
||||
|
||||
@Schema(description = "备注", example = "系统内置枚举")
|
||||
private String remark;
|
||||
|
||||
@Schema(description = "最近同步时间")
|
||||
private LocalDateTime lastSyncTime;
|
||||
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
@Schema(description = "创建时间")
|
||||
private LocalDateTime createTime;
|
||||
|
||||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||
@Schema(description = "更新时间")
|
||||
private LocalDateTime updateTime;
|
||||
}
|
||||
49
src/main/java/com/test/demo/entity/LoginLogEntity.java
Normal file
49
src/main/java/com/test/demo/entity/LoginLogEntity.java
Normal file
@@ -0,0 +1,49 @@
|
||||
package com.test.demo.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 登录日志实体
|
||||
*/
|
||||
@Data
|
||||
@TableName("sys_login_log")
|
||||
@Schema(name = "LoginLogEntity", description = "登录日志实体")
|
||||
public class LoginLogEntity {
|
||||
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
@Schema(description = "主键ID", example = "2033103240660238338")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "用户名", example = "admin")
|
||||
private String username;
|
||||
|
||||
@Schema(description = "用户ID", example = "1")
|
||||
private Long userId;
|
||||
|
||||
@Schema(description = "状态", example = "1", allowableValues = {"0", "1"})
|
||||
private Integer status;
|
||||
|
||||
@Schema(description = "日志类型", example = "LOGIN", allowableValues = {"LOGIN", "LOGOUT"})
|
||||
private String logType;
|
||||
|
||||
@Schema(description = "结果信息", example = "登录成功")
|
||||
private String message;
|
||||
|
||||
@Schema(description = "登录IP", example = "127.0.0.1")
|
||||
private String ip;
|
||||
|
||||
@Schema(description = "User-Agent")
|
||||
private String userAgent;
|
||||
|
||||
@Schema(description = "请求URL", example = "/api/auth/login")
|
||||
private String requestUrl;
|
||||
|
||||
@Schema(description = "创建时间")
|
||||
private LocalDateTime createTime;
|
||||
}
|
||||
50
src/main/java/com/test/demo/entity/MenuEntity.java
Normal file
50
src/main/java/com/test/demo/entity/MenuEntity.java
Normal file
@@ -0,0 +1,50 @@
|
||||
package com.test.demo.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.test.demo.common.base.BaseEntity;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/**
|
||||
* 菜单权限实体
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@TableName("sys_menu")
|
||||
@Schema(name = "MenuEntity", description = "菜单权限实体")
|
||||
public class MenuEntity extends BaseEntity {
|
||||
|
||||
@Schema(description = "父级ID", example = "0")
|
||||
private Long parentId;
|
||||
|
||||
@Schema(description = "菜单类型", example = "MENU", allowableValues = {"MENU", "BUTTON"})
|
||||
private String menuType;
|
||||
|
||||
@Schema(description = "菜单名称", example = "用户管理")
|
||||
private String menuName;
|
||||
|
||||
@Schema(description = "路由路径", example = "/user")
|
||||
private String path;
|
||||
|
||||
@Schema(description = "组件路径", example = "system/user/index")
|
||||
private String component;
|
||||
|
||||
@Schema(description = "权限标识", example = "system:user:page")
|
||||
private String permission;
|
||||
|
||||
@Schema(description = "图标", example = "User")
|
||||
private String icon;
|
||||
|
||||
@Schema(description = "是否显示", example = "1")
|
||||
private Integer visible;
|
||||
|
||||
@Schema(description = "是否启用", example = "1")
|
||||
private Integer enabled;
|
||||
|
||||
@Schema(description = "排序", example = "0")
|
||||
private Integer sort;
|
||||
|
||||
@Schema(description = "备注")
|
||||
private String remark;
|
||||
}
|
||||
77
src/main/java/com/test/demo/entity/OperLogEntity.java
Normal file
77
src/main/java/com/test/demo/entity/OperLogEntity.java
Normal file
@@ -0,0 +1,77 @@
|
||||
package com.test.demo.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 操作日志实体
|
||||
*/
|
||||
@Data
|
||||
@TableName("sys_oper_log")
|
||||
@Schema(name = "OperLogEntity", description = "操作日志实体")
|
||||
public class OperLogEntity {
|
||||
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
@Schema(description = "主键ID", example = "2033103240660238338")
|
||||
private Long id;
|
||||
|
||||
/** 操作说明 */
|
||||
@Schema(description = "操作说明", example = "新增用户")
|
||||
private String title;
|
||||
|
||||
/** 日志类型 */
|
||||
@Schema(description = "日志类型", example = "OPER")
|
||||
private String logType;
|
||||
|
||||
/** 请求方法(类名.方法名) */
|
||||
@Schema(description = "请求方法", example = "com.test.demo.controller.UserController.add")
|
||||
private String method;
|
||||
|
||||
/** HTTP方法 */
|
||||
@Schema(description = "HTTP方法", example = "POST")
|
||||
private String requestMethod;
|
||||
|
||||
/** 请求URL */
|
||||
@Schema(description = "请求URL", example = "/api/user")
|
||||
private String requestUrl;
|
||||
|
||||
/** 请求参数(脱敏后) */
|
||||
@Schema(description = "请求参数(脱敏后)")
|
||||
private String requestParam;
|
||||
|
||||
/** 返回结果(脱敏后) */
|
||||
@Schema(description = "返回结果(脱敏后)")
|
||||
private String responseResult;
|
||||
|
||||
/** 操作用户ID */
|
||||
@Schema(description = "操作用户ID", example = "1")
|
||||
private Long userId;
|
||||
|
||||
/** 操作用户名 */
|
||||
@Schema(description = "操作用户名", example = "admin")
|
||||
private String userName;
|
||||
|
||||
/** 操作IP */
|
||||
@Schema(description = "操作IP", example = "127.0.0.1")
|
||||
private String ip;
|
||||
|
||||
/** 操作状态(0失败 1成功) */
|
||||
@Schema(description = "操作状态", example = "1", allowableValues = {"0", "1"})
|
||||
private Integer status;
|
||||
|
||||
/** 错误信息 */
|
||||
@Schema(description = "错误信息")
|
||||
private String errorMsg;
|
||||
|
||||
/** 耗时(ms) */
|
||||
@Schema(description = "耗时(ms)", example = "35")
|
||||
private Long costTime;
|
||||
|
||||
@Schema(description = "创建时间")
|
||||
private LocalDateTime createTime;
|
||||
}
|
||||
32
src/main/java/com/test/demo/entity/RoleEntity.java
Normal file
32
src/main/java/com/test/demo/entity/RoleEntity.java
Normal file
@@ -0,0 +1,32 @@
|
||||
package com.test.demo.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.test.demo.common.base.BaseEntity;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/**
|
||||
* 角色实体
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@TableName("sys_role")
|
||||
@Schema(name = "RoleEntity", description = "角色实体")
|
||||
public class RoleEntity extends BaseEntity {
|
||||
|
||||
@Schema(description = "角色编码", example = "ADMIN")
|
||||
private String roleCode;
|
||||
|
||||
@Schema(description = "角色名称", example = "系统管理员")
|
||||
private String roleName;
|
||||
|
||||
@Schema(description = "是否启用", example = "1")
|
||||
private Integer enabled;
|
||||
|
||||
@Schema(description = "排序", example = "0")
|
||||
private Integer sort;
|
||||
|
||||
@Schema(description = "备注", example = "默认管理员角色")
|
||||
private String remark;
|
||||
}
|
||||
31
src/main/java/com/test/demo/entity/RoleMenuEntity.java
Normal file
31
src/main/java/com/test/demo/entity/RoleMenuEntity.java
Normal file
@@ -0,0 +1,31 @@
|
||||
package com.test.demo.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 角色菜单关联实体
|
||||
*/
|
||||
@Data
|
||||
@TableName("sys_role_menu")
|
||||
@Schema(name = "RoleMenuEntity", description = "角色菜单关联实体")
|
||||
public class RoleMenuEntity {
|
||||
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
@Schema(description = "主键ID")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "角色ID", example = "1")
|
||||
private Long roleId;
|
||||
|
||||
@Schema(description = "菜单ID", example = "1")
|
||||
private Long menuId;
|
||||
|
||||
@Schema(description = "创建时间")
|
||||
private LocalDateTime createTime;
|
||||
}
|
||||
45
src/main/java/com/test/demo/entity/ScheduledTaskEntity.java
Normal file
45
src/main/java/com/test/demo/entity/ScheduledTaskEntity.java
Normal file
@@ -0,0 +1,45 @@
|
||||
package com.test.demo.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import com.test.demo.common.base.BaseEntity;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/**
|
||||
* 定时任务实体
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@TableName("sys_scheduled_task")
|
||||
@Schema(name = "ScheduledTaskEntity", description = "定时任务实体")
|
||||
public class ScheduledTaskEntity extends BaseEntity {
|
||||
|
||||
/** 任务名称 */
|
||||
@Schema(description = "任务名称", example = "清理临时文件")
|
||||
private String taskName;
|
||||
|
||||
/** cron表达式 */
|
||||
@Schema(description = "Cron表达式", example = "0 0/5 * * * ?")
|
||||
private String cronExpression;
|
||||
|
||||
/** Bean名称 */
|
||||
@Schema(description = "Bean名称", example = "fileService")
|
||||
private String beanName;
|
||||
|
||||
/** 方法名 */
|
||||
@Schema(description = "方法名", example = "clearTempFiles")
|
||||
private String methodName;
|
||||
|
||||
/** 方法参数 */
|
||||
@Schema(description = "方法参数", example = "7")
|
||||
private String methodParams;
|
||||
|
||||
/** 状态(0暂停 1运行) */
|
||||
@Schema(description = "状态", example = "1", allowableValues = {"0", "1"})
|
||||
private Integer status;
|
||||
|
||||
/** 备注 */
|
||||
@Schema(description = "备注", example = "每5分钟执行一次")
|
||||
private String remark;
|
||||
}
|
||||
50
src/main/java/com/test/demo/entity/UserEntity.java
Normal file
50
src/main/java/com/test/demo/entity/UserEntity.java
Normal file
@@ -0,0 +1,50 @@
|
||||
package com.test.demo.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.test.demo.common.base.BaseEntity;
|
||||
import com.test.demo.enums.UserStatusEnum;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/**
|
||||
* 用户实体
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@TableName("sys_user")
|
||||
@Schema(name = "UserEntity", description = "用户实体")
|
||||
public class UserEntity extends BaseEntity {
|
||||
|
||||
/** 用户名 */
|
||||
@Schema(description = "用户名", example = "admin")
|
||||
private String username;
|
||||
|
||||
/** SM3加密密码 */
|
||||
@Schema(description = "SM3加密密码")
|
||||
private String password;
|
||||
|
||||
/** 密码盐 */
|
||||
@Schema(description = "密码盐")
|
||||
private String salt;
|
||||
|
||||
/** 昵称 */
|
||||
@Schema(description = "昵称", example = "管理员")
|
||||
private String nickname;
|
||||
|
||||
/** 邮箱 */
|
||||
@Schema(description = "邮箱", example = "admin@example.com")
|
||||
private String email;
|
||||
|
||||
/** 手机号 */
|
||||
@Schema(description = "手机号", example = "13800138000")
|
||||
private String phone;
|
||||
|
||||
/** 头像路径 */
|
||||
@Schema(description = "头像路径", example = "/avatar/admin.png")
|
||||
private String avatar;
|
||||
|
||||
/** 状态 */
|
||||
@Schema(description = "状态枚举")
|
||||
private UserStatusEnum status;
|
||||
}
|
||||
31
src/main/java/com/test/demo/entity/UserRoleEntity.java
Normal file
31
src/main/java/com/test/demo/entity/UserRoleEntity.java
Normal file
@@ -0,0 +1,31 @@
|
||||
package com.test.demo.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 用户角色关联实体
|
||||
*/
|
||||
@Data
|
||||
@TableName("sys_user_role")
|
||||
@Schema(name = "UserRoleEntity", description = "用户角色关联实体")
|
||||
public class UserRoleEntity {
|
||||
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
@Schema(description = "主键ID")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "用户ID", example = "1")
|
||||
private Long userId;
|
||||
|
||||
@Schema(description = "角色ID", example = "1")
|
||||
private Long roleId;
|
||||
|
||||
@Schema(description = "创建时间")
|
||||
private LocalDateTime createTime;
|
||||
}
|
||||
37
src/main/java/com/test/demo/enums/LogType.java
Normal file
37
src/main/java/com/test/demo/enums/LogType.java
Normal file
@@ -0,0 +1,37 @@
|
||||
package com.test.demo.enums;
|
||||
|
||||
import com.test.demo.common.enums.EnumDesc;
|
||||
import com.test.demo.common.enums.IBaseEnum;
|
||||
|
||||
/**
|
||||
* 日志类型枚举
|
||||
*/
|
||||
@EnumDesc("日志类型枚举")
|
||||
public enum LogType implements IBaseEnum<String> {
|
||||
|
||||
LOGIN("LOGIN", "登录日志"),
|
||||
LOGOUT("LOGOUT", "登出日志"),
|
||||
OPER("OPER", "操作日志"),
|
||||
MAIL("MAIL", "邮件日志"),
|
||||
EXPORT("EXPORT", "导出日志"),
|
||||
IMPORT("IMPORT", "导入日志"),
|
||||
OTHER("OTHER", "其他日志");
|
||||
|
||||
private final String value;
|
||||
private final String desc;
|
||||
|
||||
LogType(String value, String desc) {
|
||||
this.value = value;
|
||||
this.desc = desc;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getValue() {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDesc() {
|
||||
return this.desc;
|
||||
}
|
||||
}
|
||||
23
src/main/java/com/test/demo/enums/UserStatusEnum.java
Normal file
23
src/main/java/com/test/demo/enums/UserStatusEnum.java
Normal file
@@ -0,0 +1,23 @@
|
||||
package com.test.demo.enums;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.EnumValue;
|
||||
import com.test.demo.common.enums.EnumDesc;
|
||||
import com.test.demo.common.enums.IBaseEnum;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
/**
|
||||
* 用户状态枚举
|
||||
*/
|
||||
@EnumDesc("用户状态枚举")
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
public enum UserStatusEnum implements IBaseEnum<Integer> {
|
||||
|
||||
DISABLED(0, "禁用"),
|
||||
NORMAL(1, "正常");
|
||||
|
||||
@EnumValue
|
||||
private final Integer value;
|
||||
private final String desc;
|
||||
}
|
||||
25
src/main/java/com/test/demo/filter/RepeatableReadFilter.java
Normal file
25
src/main/java/com/test/demo/filter/RepeatableReadFilter.java
Normal file
@@ -0,0 +1,25 @@
|
||||
package com.test.demo.filter;
|
||||
|
||||
import jakarta.servlet.*;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* 可重复读取请求体过滤器
|
||||
*/
|
||||
public class RepeatableReadFilter implements Filter {
|
||||
|
||||
@Override
|
||||
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
|
||||
throws IOException, ServletException {
|
||||
HttpServletRequest req = (HttpServletRequest) request;
|
||||
String contentType = req.getContentType();
|
||||
// 仅对 JSON 请求体做包装
|
||||
if (contentType != null && contentType.contains("application/json")) {
|
||||
chain.doFilter(new RepeatableReadRequestWrapper(req), response);
|
||||
} else {
|
||||
chain.doFilter(request, response);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package com.test.demo.filter;
|
||||
|
||||
import jakarta.servlet.ReadListener;
|
||||
import jakarta.servlet.ServletInputStream;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletRequestWrapper;
|
||||
|
||||
import java.io.*;
|
||||
|
||||
/**
|
||||
* 可重复读取请求体的包装器(用于日志记录、签名校验等需要多次读取 body 的场景)
|
||||
*/
|
||||
public class RepeatableReadRequestWrapper extends HttpServletRequestWrapper {
|
||||
|
||||
private final byte[] body;
|
||||
|
||||
public RepeatableReadRequestWrapper(HttpServletRequest request) throws IOException {
|
||||
super(request);
|
||||
try (InputStream is = request.getInputStream();
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
|
||||
byte[] buffer = new byte[1024];
|
||||
int len;
|
||||
while ((len = is.read(buffer)) != -1) {
|
||||
baos.write(buffer, 0, len);
|
||||
}
|
||||
this.body = baos.toByteArray();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ServletInputStream getInputStream() {
|
||||
ByteArrayInputStream bais = new ByteArrayInputStream(body);
|
||||
return new ServletInputStream() {
|
||||
@Override
|
||||
public boolean isFinished() {
|
||||
return bais.available() == 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isReady() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setReadListener(ReadListener listener) {
|
||||
// no-op
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read() {
|
||||
return bais.read();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public BufferedReader getReader() {
|
||||
return new BufferedReader(new InputStreamReader(getInputStream()));
|
||||
}
|
||||
|
||||
public byte[] getBody() {
|
||||
return body;
|
||||
}
|
||||
}
|
||||
43
src/main/java/com/test/demo/filter/XssFilter.java
Normal file
43
src/main/java/com/test/demo/filter/XssFilter.java
Normal file
@@ -0,0 +1,43 @@
|
||||
package com.test.demo.filter;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import jakarta.servlet.*;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* XSS 防护过滤器
|
||||
*/
|
||||
public class XssFilter implements Filter {
|
||||
|
||||
private List<String> excludes;
|
||||
|
||||
@Override
|
||||
public void init(FilterConfig filterConfig) {
|
||||
String excludeStr = filterConfig.getInitParameter("excludes");
|
||||
if (StrUtil.isNotBlank(excludeStr)) {
|
||||
excludes = Arrays.asList(excludeStr.split(","));
|
||||
} else {
|
||||
excludes = List.of();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
|
||||
throws IOException, ServletException {
|
||||
HttpServletRequest req = (HttpServletRequest) request;
|
||||
if (isExclude(req)) {
|
||||
chain.doFilter(request, response);
|
||||
} else {
|
||||
chain.doFilter(new XssRequestWrapper(req), response);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isExclude(HttpServletRequest request) {
|
||||
String uri = request.getServletPath();
|
||||
return excludes.stream().anyMatch(uri::startsWith);
|
||||
}
|
||||
}
|
||||
40
src/main/java/com/test/demo/filter/XssRequestWrapper.java
Normal file
40
src/main/java/com/test/demo/filter/XssRequestWrapper.java
Normal file
@@ -0,0 +1,40 @@
|
||||
package com.test.demo.filter;
|
||||
|
||||
import cn.hutool.http.HtmlUtil;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletRequestWrapper;
|
||||
|
||||
/**
|
||||
* XSS 请求包装器 - 对参数和请求头做 HTML 转义
|
||||
*/
|
||||
public class XssRequestWrapper extends HttpServletRequestWrapper {
|
||||
|
||||
public XssRequestWrapper(HttpServletRequest request) {
|
||||
super(request);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getParameter(String name) {
|
||||
String value = super.getParameter(name);
|
||||
return value != null ? HtmlUtil.escape(value) : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getParameterValues(String name) {
|
||||
String[] values = super.getParameterValues(name);
|
||||
if (values == null) {
|
||||
return null;
|
||||
}
|
||||
String[] escaped = new String[values.length];
|
||||
for (int i = 0; i < values.length; i++) {
|
||||
escaped[i] = HtmlUtil.escape(values[i]);
|
||||
}
|
||||
return escaped;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHeader(String name) {
|
||||
String value = super.getHeader(name);
|
||||
return value != null ? HtmlUtil.escape(value) : null;
|
||||
}
|
||||
}
|
||||
25
src/main/java/com/test/demo/handler/MyMetaObjectHandler.java
Normal file
25
src/main/java/com/test/demo/handler/MyMetaObjectHandler.java
Normal file
@@ -0,0 +1,25 @@
|
||||
package com.test.demo.handler;
|
||||
|
||||
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
|
||||
import org.apache.ibatis.reflection.MetaObject;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* MyBatis-Plus 自动填充处理器
|
||||
*/
|
||||
@Component
|
||||
public class MyMetaObjectHandler implements MetaObjectHandler {
|
||||
|
||||
@Override
|
||||
public void insertFill(MetaObject metaObject) {
|
||||
this.strictInsertFill(metaObject, "createTime", LocalDateTime::now, LocalDateTime.class);
|
||||
this.strictInsertFill(metaObject, "updateTime", LocalDateTime::now, LocalDateTime.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateFill(MetaObject metaObject) {
|
||||
this.strictUpdateFill(metaObject, "updateTime", LocalDateTime::now, LocalDateTime.class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package com.test.demo.interceptor;
|
||||
|
||||
import cn.hutool.crypto.digest.DigestUtil;
|
||||
import com.test.demo.common.annotation.Idempotent;
|
||||
import com.test.demo.common.constant.CommonConstant;
|
||||
import com.test.demo.common.constant.RedisKeyConstant;
|
||||
import com.test.demo.common.exception.BizException;
|
||||
import com.test.demo.common.result.ResultCode;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.method.HandlerMethod;
|
||||
import org.springframework.web.servlet.HandlerInterceptor;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* 幂等性拦截器
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class IdempotentInterceptor implements HandlerInterceptor {
|
||||
|
||||
private final RedisTemplate<String, Object> redisTemplate;
|
||||
|
||||
@Override
|
||||
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
|
||||
if (!(handler instanceof HandlerMethod handlerMethod)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
Idempotent idempotent = handlerMethod.getMethodAnnotation(Idempotent.class);
|
||||
if (idempotent == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 构建幂等 key: token + url + 参数hash
|
||||
String token = request.getHeader(CommonConstant.HEADER_IDEMPOTENT_TOKEN);
|
||||
if (token == null) {
|
||||
token = request.getHeader("Authorization");
|
||||
}
|
||||
String url = request.getRequestURI();
|
||||
String queryString = request.getQueryString() != null ? request.getQueryString() : "";
|
||||
String key = RedisKeyConstant.IDEMPOTENT + DigestUtil.md5Hex(token + url + queryString);
|
||||
|
||||
Boolean absent = redisTemplate.opsForValue().setIfAbsent(key, "1", idempotent.expireTime(), TimeUnit.SECONDS);
|
||||
if (Boolean.FALSE.equals(absent)) {
|
||||
log.warn("幂等性拦截: key={}", key);
|
||||
throw new BizException(ResultCode.IDEMPOTENT_REJECT.getCode(), idempotent.message());
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package com.test.demo.interceptor;
|
||||
|
||||
import cn.hutool.crypto.digest.DigestUtil;
|
||||
import com.test.demo.common.constant.CommonConstant;
|
||||
import com.test.demo.common.constant.RedisKeyConstant;
|
||||
import com.test.demo.common.exception.BizException;
|
||||
import com.test.demo.common.result.ResultCode;
|
||||
import com.test.demo.filter.RepeatableReadRequestWrapper;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.servlet.HandlerInterceptor;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* 接口签名/防重放拦截器
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class SignatureInterceptor implements HandlerInterceptor {
|
||||
|
||||
private final RedisTemplate<String, Object> redisTemplate;
|
||||
|
||||
@Value("${security.sign.secret:change-me-sign-secret}")
|
||||
private String signSecret;
|
||||
|
||||
@Value("${security.sign.expire-seconds:300}")
|
||||
private long expireSeconds;
|
||||
|
||||
@Override
|
||||
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
|
||||
String timestamp = request.getHeader(CommonConstant.HEADER_TIMESTAMP);
|
||||
String nonce = request.getHeader(CommonConstant.HEADER_NONCE);
|
||||
String sign = request.getHeader(CommonConstant.HEADER_SIGN);
|
||||
|
||||
// 缺少签名头则跳过(可根据需求改为拒绝)
|
||||
if (timestamp == null || nonce == null || sign == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 时间戳校验
|
||||
long ts = Long.parseLong(timestamp);
|
||||
long now = System.currentTimeMillis() / 1000;
|
||||
if (Math.abs(now - ts) > expireSeconds) {
|
||||
throw new BizException(ResultCode.REPLAY_ATTACK);
|
||||
}
|
||||
|
||||
// nonce 防重放
|
||||
String nonceKey = RedisKeyConstant.NONCE + nonce;
|
||||
Boolean absent = redisTemplate.opsForValue().setIfAbsent(nonceKey, "1", expireSeconds, TimeUnit.SECONDS);
|
||||
if (Boolean.FALSE.equals(absent)) {
|
||||
throw new BizException(ResultCode.REPLAY_ATTACK);
|
||||
}
|
||||
|
||||
// 签名校验
|
||||
String body = "";
|
||||
if (request instanceof RepeatableReadRequestWrapper wrapper) {
|
||||
body = new String(wrapper.getBody(), StandardCharsets.UTF_8);
|
||||
}
|
||||
String rawSign = timestamp + nonce + body + signSecret;
|
||||
String expectedSign = DigestUtil.sha256Hex(rawSign);
|
||||
if (!expectedSign.equalsIgnoreCase(sign)) {
|
||||
throw new BizException(ResultCode.SIGNATURE_INVALID);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
12
src/main/java/com/test/demo/mapper/EnumItemMapper.java
Normal file
12
src/main/java/com/test/demo/mapper/EnumItemMapper.java
Normal file
@@ -0,0 +1,12 @@
|
||||
package com.test.demo.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.test.demo.entity.EnumItemEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* 枚举/字典项 Mapper
|
||||
*/
|
||||
@Mapper
|
||||
public interface EnumItemMapper extends BaseMapper<EnumItemEntity> {
|
||||
}
|
||||
12
src/main/java/com/test/demo/mapper/EnumTypeMapper.java
Normal file
12
src/main/java/com/test/demo/mapper/EnumTypeMapper.java
Normal file
@@ -0,0 +1,12 @@
|
||||
package com.test.demo.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.test.demo.entity.EnumTypeEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* 枚举/字典类型 Mapper
|
||||
*/
|
||||
@Mapper
|
||||
public interface EnumTypeMapper extends BaseMapper<EnumTypeEntity> {
|
||||
}
|
||||
12
src/main/java/com/test/demo/mapper/LoginLogMapper.java
Normal file
12
src/main/java/com/test/demo/mapper/LoginLogMapper.java
Normal file
@@ -0,0 +1,12 @@
|
||||
package com.test.demo.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.test.demo.entity.LoginLogEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* 登录日志 Mapper
|
||||
*/
|
||||
@Mapper
|
||||
public interface LoginLogMapper extends BaseMapper<LoginLogEntity> {
|
||||
}
|
||||
48
src/main/java/com/test/demo/mapper/MenuMapper.java
Normal file
48
src/main/java/com/test/demo/mapper/MenuMapper.java
Normal file
@@ -0,0 +1,48 @@
|
||||
package com.test.demo.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.test.demo.entity.MenuEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 菜单 Mapper
|
||||
*/
|
||||
@Mapper
|
||||
public interface MenuMapper extends BaseMapper<MenuEntity> {
|
||||
|
||||
@Select("""
|
||||
SELECT DISTINCT m.id, m.parent_id, m.menu_type, m.menu_name, m.path, m.component, m.permission,
|
||||
m.icon, m.visible, m.enabled, m.sort, m.remark, m.create_time, m.update_time, m.deleted
|
||||
FROM sys_menu m
|
||||
INNER JOIN sys_role_menu rm ON rm.menu_id = m.id
|
||||
INNER JOIN sys_user_role ur ON ur.role_id = rm.role_id
|
||||
INNER JOIN sys_role r ON r.id = rm.role_id
|
||||
WHERE ur.user_id = #{userId}
|
||||
AND m.enabled = 1
|
||||
AND m.visible = 1
|
||||
AND m.deleted = 0
|
||||
AND r.enabled = 1
|
||||
AND r.deleted = 0
|
||||
ORDER BY m.sort ASC, m.id ASC
|
||||
""")
|
||||
List<MenuEntity> selectMenusByUserId(Long userId);
|
||||
|
||||
@Select("""
|
||||
SELECT DISTINCT m.permission
|
||||
FROM sys_menu m
|
||||
INNER JOIN sys_role_menu rm ON rm.menu_id = m.id
|
||||
INNER JOIN sys_user_role ur ON ur.role_id = rm.role_id
|
||||
INNER JOIN sys_role r ON r.id = rm.role_id
|
||||
WHERE ur.user_id = #{userId}
|
||||
AND m.enabled = 1
|
||||
AND m.deleted = 0
|
||||
AND r.enabled = 1
|
||||
AND r.deleted = 0
|
||||
AND m.permission IS NOT NULL
|
||||
AND m.permission <> ''
|
||||
""")
|
||||
List<String> selectPermissionsByUserId(Long userId);
|
||||
}
|
||||
12
src/main/java/com/test/demo/mapper/OperLogMapper.java
Normal file
12
src/main/java/com/test/demo/mapper/OperLogMapper.java
Normal file
@@ -0,0 +1,12 @@
|
||||
package com.test.demo.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.test.demo.entity.OperLogEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* 操作日志 Mapper
|
||||
*/
|
||||
@Mapper
|
||||
public interface OperLogMapper extends BaseMapper<OperLogEntity> {
|
||||
}
|
||||
23
src/main/java/com/test/demo/mapper/RoleMapper.java
Normal file
23
src/main/java/com/test/demo/mapper/RoleMapper.java
Normal file
@@ -0,0 +1,23 @@
|
||||
package com.test.demo.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.test.demo.entity.RoleEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 角色 Mapper
|
||||
*/
|
||||
@Mapper
|
||||
public interface RoleMapper extends BaseMapper<RoleEntity> {
|
||||
|
||||
@Select("""
|
||||
SELECT DISTINCT r.role_code
|
||||
FROM sys_role r
|
||||
INNER JOIN sys_user_role ur ON ur.role_id = r.id
|
||||
WHERE ur.user_id = #{userId} AND r.enabled = 1 AND r.deleted = 0
|
||||
""")
|
||||
List<String> selectRoleCodesByUserId(Long userId);
|
||||
}
|
||||
12
src/main/java/com/test/demo/mapper/RoleMenuMapper.java
Normal file
12
src/main/java/com/test/demo/mapper/RoleMenuMapper.java
Normal file
@@ -0,0 +1,12 @@
|
||||
package com.test.demo.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.test.demo.entity.RoleMenuEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* 角色菜单 Mapper
|
||||
*/
|
||||
@Mapper
|
||||
public interface RoleMenuMapper extends BaseMapper<RoleMenuEntity> {
|
||||
}
|
||||
12
src/main/java/com/test/demo/mapper/ScheduledTaskMapper.java
Normal file
12
src/main/java/com/test/demo/mapper/ScheduledTaskMapper.java
Normal file
@@ -0,0 +1,12 @@
|
||||
package com.test.demo.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.test.demo.entity.ScheduledTaskEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* 定时任务 Mapper
|
||||
*/
|
||||
@Mapper
|
||||
public interface ScheduledTaskMapper extends BaseMapper<ScheduledTaskEntity> {
|
||||
}
|
||||
12
src/main/java/com/test/demo/mapper/UserMapper.java
Normal file
12
src/main/java/com/test/demo/mapper/UserMapper.java
Normal file
@@ -0,0 +1,12 @@
|
||||
package com.test.demo.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.test.demo.entity.UserEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* 用户 Mapper
|
||||
*/
|
||||
@Mapper
|
||||
public interface UserMapper extends BaseMapper<UserEntity> {
|
||||
}
|
||||
12
src/main/java/com/test/demo/mapper/UserRoleMapper.java
Normal file
12
src/main/java/com/test/demo/mapper/UserRoleMapper.java
Normal file
@@ -0,0 +1,12 @@
|
||||
package com.test.demo.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.test.demo.entity.UserRoleEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* 用户角色 Mapper
|
||||
*/
|
||||
@Mapper
|
||||
public interface UserRoleMapper extends BaseMapper<UserRoleEntity> {
|
||||
}
|
||||
57
src/main/java/com/test/demo/security/SecurityUtil.java
Normal file
57
src/main/java/com/test/demo/security/SecurityUtil.java
Normal file
@@ -0,0 +1,57 @@
|
||||
package com.test.demo.security;
|
||||
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
|
||||
/**
|
||||
* 安全工具类
|
||||
*/
|
||||
public final class SecurityUtil {
|
||||
|
||||
private SecurityUtil() {}
|
||||
|
||||
/**
|
||||
* 获取当前登录用户ID
|
||||
*/
|
||||
public static Long getUserId() {
|
||||
return StpUtil.getLoginIdAsLong();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前登录用户ID(未登录返回null)
|
||||
*/
|
||||
public static Long getUserIdOrNull() {
|
||||
try {
|
||||
return StpUtil.getLoginIdAsLong();
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否已登录
|
||||
*/
|
||||
public static boolean isLogin() {
|
||||
return StpUtil.isLogin();
|
||||
}
|
||||
|
||||
/**
|
||||
* 登录
|
||||
*/
|
||||
public static void login(Long userId) {
|
||||
StpUtil.login(userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 登出
|
||||
*/
|
||||
public static void logout() {
|
||||
StpUtil.logout();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前 Token
|
||||
*/
|
||||
public static String getToken() {
|
||||
return StpUtil.getTokenValue();
|
||||
}
|
||||
}
|
||||
34
src/main/java/com/test/demo/security/StpInterfaceImpl.java
Normal file
34
src/main/java/com/test/demo/security/StpInterfaceImpl.java
Normal file
@@ -0,0 +1,34 @@
|
||||
package com.test.demo.security;
|
||||
|
||||
import cn.dev33.satoken.stp.StpInterface;
|
||||
import com.test.demo.mapper.MenuMapper;
|
||||
import com.test.demo.mapper.RoleMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Sa-Token 权限/角色数据加载实现
|
||||
*/
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class StpInterfaceImpl implements StpInterface {
|
||||
|
||||
private final RoleMapper roleMapper;
|
||||
private final MenuMapper menuMapper;
|
||||
|
||||
@Override
|
||||
public List<String> getPermissionList(Object loginId, String loginType) {
|
||||
Long userId = Long.parseLong(String.valueOf(loginId));
|
||||
return menuMapper.selectPermissionsByUserId(userId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getRoleList(Object loginId, String loginType) {
|
||||
Long userId = Long.parseLong(String.valueOf(loginId));
|
||||
List<String> roles = roleMapper.selectRoleCodesByUserId(userId);
|
||||
return roles != null ? roles : Collections.emptyList();
|
||||
}
|
||||
}
|
||||
47
src/main/java/com/test/demo/security/crypto/SM3Util.java
Normal file
47
src/main/java/com/test/demo/security/crypto/SM3Util.java
Normal file
@@ -0,0 +1,47 @@
|
||||
package com.test.demo.security.crypto;
|
||||
|
||||
import cn.hutool.core.util.HexUtil;
|
||||
import cn.hutool.core.util.RandomUtil;
|
||||
import org.bouncycastle.crypto.digests.SM3Digest;
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.Security;
|
||||
|
||||
/**
|
||||
* SM3 国密哈希工具(密码加密)
|
||||
*/
|
||||
public final class SM3Util {
|
||||
|
||||
static {
|
||||
Security.addProvider(new BouncyCastleProvider());
|
||||
}
|
||||
|
||||
private SM3Util() {}
|
||||
|
||||
/**
|
||||
* 生成随机盐
|
||||
*/
|
||||
public static String generateSalt() {
|
||||
return RandomUtil.randomString(16);
|
||||
}
|
||||
|
||||
/**
|
||||
* SM3 哈希(加盐)
|
||||
*/
|
||||
public static String hash(String data, String salt) {
|
||||
byte[] input = (salt + data).getBytes(StandardCharsets.UTF_8);
|
||||
SM3Digest digest = new SM3Digest();
|
||||
digest.update(input, 0, input.length);
|
||||
byte[] result = new byte[digest.getDigestSize()];
|
||||
digest.doFinal(result, 0);
|
||||
return HexUtil.encodeHexStr(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证密码
|
||||
*/
|
||||
public static boolean verify(String data, String salt, String hash) {
|
||||
return hash(data, salt).equals(hash);
|
||||
}
|
||||
}
|
||||
61
src/main/java/com/test/demo/security/crypto/SM4Util.java
Normal file
61
src/main/java/com/test/demo/security/crypto/SM4Util.java
Normal file
@@ -0,0 +1,61 @@
|
||||
package com.test.demo.security.crypto;
|
||||
|
||||
import cn.hutool.core.util.HexUtil;
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.Security;
|
||||
import java.util.Base64;
|
||||
|
||||
/**
|
||||
* SM4 国密对称加解密工具
|
||||
*/
|
||||
public final class SM4Util {
|
||||
|
||||
private static final String ALGORITHM = "SM4";
|
||||
private static final String TRANSFORMATION = "SM4/ECB/PKCS5Padding";
|
||||
|
||||
static {
|
||||
Security.addProvider(new BouncyCastleProvider());
|
||||
}
|
||||
|
||||
private SM4Util() {}
|
||||
|
||||
/**
|
||||
* 加密(返回 Base64)
|
||||
* @param plainText 明文
|
||||
* @param hexKey 16字节密钥的十六进制字符串(32个hex字符)
|
||||
*/
|
||||
public static String encrypt(String plainText, String hexKey) {
|
||||
try {
|
||||
byte[] keyBytes = HexUtil.decodeHex(hexKey);
|
||||
SecretKeySpec keySpec = new SecretKeySpec(keyBytes, ALGORITHM);
|
||||
Cipher cipher = Cipher.getInstance(TRANSFORMATION, "BC");
|
||||
cipher.init(Cipher.ENCRYPT_MODE, keySpec);
|
||||
byte[] encrypted = cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8));
|
||||
return Base64.getEncoder().encodeToString(encrypted);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("SM4加密失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解密
|
||||
* @param cipherText Base64 密文
|
||||
* @param hexKey 16字节密钥的十六进制字符串
|
||||
*/
|
||||
public static String decrypt(String cipherText, String hexKey) {
|
||||
try {
|
||||
byte[] keyBytes = HexUtil.decodeHex(hexKey);
|
||||
SecretKeySpec keySpec = new SecretKeySpec(keyBytes, ALGORITHM);
|
||||
Cipher cipher = Cipher.getInstance(TRANSFORMATION, "BC");
|
||||
cipher.init(Cipher.DECRYPT_MODE, keySpec);
|
||||
byte[] decrypted = cipher.doFinal(Base64.getDecoder().decode(cipherText));
|
||||
return new String(decrypted, StandardCharsets.UTF_8);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("SM4解密失败", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package com.test.demo.serializer;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonGenerator;
|
||||
import com.fasterxml.jackson.databind.BeanProperty;
|
||||
import com.fasterxml.jackson.databind.JsonSerializer;
|
||||
import com.fasterxml.jackson.databind.SerializerProvider;
|
||||
import com.fasterxml.jackson.databind.ser.ContextualSerializer;
|
||||
import com.test.demo.common.annotation.Desensitize;
|
||||
import com.test.demo.common.annotation.DesensitizeType;
|
||||
import com.test.demo.util.DesensitizeUtil;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* 数据脱敏 Jackson 序列化器
|
||||
*/
|
||||
@NoArgsConstructor
|
||||
public class DesensitizeSerializer extends JsonSerializer<String> implements ContextualSerializer {
|
||||
|
||||
private DesensitizeType type;
|
||||
|
||||
public DesensitizeSerializer(DesensitizeType type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void serialize(String value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
|
||||
if (value == null) {
|
||||
gen.writeNull();
|
||||
return;
|
||||
}
|
||||
String result = switch (type) {
|
||||
case PHONE -> DesensitizeUtil.phone(value);
|
||||
case ID_CARD -> DesensitizeUtil.idCard(value);
|
||||
case EMAIL -> DesensitizeUtil.email(value);
|
||||
case BANK_CARD -> DesensitizeUtil.bankCard(value);
|
||||
case NAME -> DesensitizeUtil.name(value);
|
||||
case ADDRESS -> DesensitizeUtil.address(value);
|
||||
};
|
||||
gen.writeString(result);
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property) {
|
||||
if (property != null) {
|
||||
Desensitize annotation = property.getAnnotation(Desensitize.class);
|
||||
if (annotation != null) {
|
||||
return new DesensitizeSerializer(annotation.type());
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
}
|
||||
20
src/main/java/com/test/demo/service/AuthService.java
Normal file
20
src/main/java/com/test/demo/service/AuthService.java
Normal file
@@ -0,0 +1,20 @@
|
||||
package com.test.demo.service;
|
||||
|
||||
import com.test.demo.bo.LoginBo;
|
||||
import com.test.demo.bo.RegisterBo;
|
||||
import com.test.demo.vo.LoginVo;
|
||||
import com.test.demo.vo.UserVo;
|
||||
|
||||
/**
|
||||
* 认证 Service
|
||||
*/
|
||||
public interface AuthService {
|
||||
|
||||
LoginVo login(LoginBo bo);
|
||||
|
||||
void register(RegisterBo bo);
|
||||
|
||||
void logout();
|
||||
|
||||
UserVo getCurrentUser();
|
||||
}
|
||||
57
src/main/java/com/test/demo/service/CaptchaService.java
Normal file
57
src/main/java/com/test/demo/service/CaptchaService.java
Normal file
@@ -0,0 +1,57 @@
|
||||
package com.test.demo.service;
|
||||
|
||||
import cn.hutool.core.lang.UUID;
|
||||
import com.test.demo.common.constant.RedisKeyConstant;
|
||||
import com.wf.captcha.SpecCaptcha;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* 验证码服务
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class CaptchaService {
|
||||
|
||||
private final RedisTemplate<String, Object> redisTemplate;
|
||||
|
||||
/** 验证码有效期(秒) */
|
||||
private static final long CAPTCHA_EXPIRE = 120;
|
||||
|
||||
/**
|
||||
* 生成验证码
|
||||
* @return {key: 缓存key, image: base64图片}
|
||||
*/
|
||||
public Map<String, String> generate() {
|
||||
SpecCaptcha captcha = new SpecCaptcha(130, 48, 4);
|
||||
String code = captcha.text().toLowerCase();
|
||||
String key = UUID.randomUUID().toString(true);
|
||||
|
||||
redisTemplate.opsForValue().set(
|
||||
RedisKeyConstant.CAPTCHA + key, code, CAPTCHA_EXPIRE, TimeUnit.SECONDS);
|
||||
|
||||
Map<String, String> result = new HashMap<>(2);
|
||||
result.put("key", key);
|
||||
result.put("image", captcha.toBase64());
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验验证码
|
||||
*/
|
||||
public boolean verify(String key, String code) {
|
||||
if (key == null || code == null) {
|
||||
return false;
|
||||
}
|
||||
String redisKey = RedisKeyConstant.CAPTCHA + key;
|
||||
Object cached = redisTemplate.opsForValue().get(redisKey);
|
||||
// 验证后立即删除,防止重复使用
|
||||
redisTemplate.delete(redisKey);
|
||||
return cached != null && cached.toString().equalsIgnoreCase(code);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user