refactor: 统一注释规范与代码风格,优化分层结构
This commit is contained in:
26
pom.xml
26
pom.xml
@@ -6,7 +6,7 @@
|
||||
|
||||
<groupId>com.youlai</groupId>
|
||||
<artifactId>youlai-boot</artifactId>
|
||||
<version>4.3.1</version>
|
||||
<version>4.3.2</version>
|
||||
<description>基于 Java 17 + SpringBoot 4 + Spring Security 构建的权限管理系统。</description>
|
||||
|
||||
<parent>
|
||||
@@ -26,9 +26,9 @@
|
||||
<druid.version>1.2.24</druid.version>
|
||||
<!-- Spring Boot 4.x 必须使用更新的版本 -->
|
||||
<mybatis-plus.version>3.5.15</mybatis-plus.version>
|
||||
<dynamic-datasource.version>4.3.1</dynamic-datasource.version>
|
||||
|
||||
<knife4j.version>4.5.0</knife4j.version>
|
||||
<springdoc.version>2.8.9</springdoc.version>
|
||||
|
||||
<mapstruct.version>1.6.3</mapstruct.version>
|
||||
<lombok-mapstruct-binding.version>0.2.0</lombok-mapstruct-binding.version>
|
||||
@@ -54,8 +54,8 @@
|
||||
<ip2region.version>2.7.0</ip2region.version>
|
||||
|
||||
<!-- 阿里云短信 -->
|
||||
<aliyun.java.sdk.core.version>4.7.6</aliyun.java.sdk.core.version>
|
||||
<aliyun.java.sdk.dysmsapi.version>2.2.1</aliyun.java.sdk.dysmsapi.version>
|
||||
<aliyun-sdk-core.version>4.7.6</aliyun-sdk-core.version>
|
||||
<aliyun-sdk-dysmsapi.version>2.2.1</aliyun-sdk-dysmsapi.version>
|
||||
|
||||
<caffeine.version>2.9.3</caffeine.version>
|
||||
|
||||
@@ -64,7 +64,6 @@
|
||||
|
||||
<weixin-java-miniapp.version>4.8.1.B</weixin-java-miniapp.version>
|
||||
|
||||
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
@@ -81,7 +80,7 @@
|
||||
<version>${hutool.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- TransmittableThreadLocal: 支持异步场景的租户上下文传递 -->
|
||||
<!-- TransmittableThreadLocal -->
|
||||
<dependency>
|
||||
<groupId>com.alibaba</groupId>
|
||||
<artifactId>transmittable-thread-local</artifactId>
|
||||
@@ -178,7 +177,7 @@
|
||||
<dependency>
|
||||
<groupId>org.springdoc</groupId>
|
||||
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
|
||||
<version>2.8.9</version>
|
||||
<version>${springdoc.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- MapStruct 对象映射 -->
|
||||
@@ -201,7 +200,7 @@
|
||||
<version>${xxl-job.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Excel 工具(EasyExcel-PLus ) -->
|
||||
<!-- Excel 工具(EasyExcel-Plus) -->
|
||||
<dependency>
|
||||
<groupId>cn.idev.excel</groupId>
|
||||
<artifactId>fastexcel</artifactId>
|
||||
@@ -253,13 +252,13 @@
|
||||
<dependency>
|
||||
<groupId>com.aliyun</groupId>
|
||||
<artifactId>aliyun-java-sdk-core</artifactId>
|
||||
<version>${aliyun.java.sdk.core.version}</version>
|
||||
<version>${aliyun-sdk-core.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.aliyun</groupId>
|
||||
<artifactId>aliyun-java-sdk-dysmsapi</artifactId>
|
||||
<version>${aliyun.java.sdk.dysmsapi.version}</version>
|
||||
<version>${aliyun-sdk-dysmsapi.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 本地缓存 -->
|
||||
@@ -276,13 +275,6 @@
|
||||
<version>${weixin-java-miniapp.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 动态多数据源 -->
|
||||
<!--<dependency>
|
||||
<groupId>com.baomidou</groupId>
|
||||
<artifactId>dynamic-datasource-spring-boot3-starter</artifactId>
|
||||
<version>${dynamic-datasource.version}</version>
|
||||
</dependency>-->
|
||||
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
|
||||
@@ -8,7 +8,7 @@ import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
* 应用启动类
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 0.0.1
|
||||
* @since 3.0.0
|
||||
*/
|
||||
@EnableScheduling
|
||||
@SpringBootApplication
|
||||
|
||||
@@ -15,7 +15,13 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/**
|
||||
* 认证控制层
|
||||
|
||||
@@ -5,7 +5,7 @@ import java.lang.annotation.*;
|
||||
/**
|
||||
* 数据权限注解
|
||||
*
|
||||
* @author zc
|
||||
* @author Ray.Hao
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Documented
|
||||
|
||||
@@ -3,13 +3,17 @@ package com.youlai.boot.common.annotation;
|
||||
import com.youlai.boot.common.enums.ActionTypeEnum;
|
||||
import com.youlai.boot.common.enums.LogModuleEnum;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* 日志注解
|
||||
*
|
||||
* @author Ray
|
||||
* @since 2024/6/25
|
||||
* @author Ray.Hao
|
||||
* @since 3.0.0
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.METHOD)
|
||||
|
||||
@@ -6,6 +6,9 @@ import java.util.concurrent.TimeUnit;
|
||||
/**
|
||||
* 接口限流
|
||||
* <p>标注在 Controller 方法上,基于 Redis 计数窗口实现</p>
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 4.3.1
|
||||
*/
|
||||
@Target(ElementType.METHOD)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
|
||||
@@ -1,102 +0,0 @@
|
||||
package com.youlai.boot.common.aspect;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.crypto.digest.DigestUtil;
|
||||
import com.youlai.boot.common.constant.RedisConstants;
|
||||
import com.youlai.boot.common.constant.SecurityConstants;
|
||||
import com.youlai.boot.common.result.ResultCode;
|
||||
import com.youlai.boot.common.exception.BusinessException;
|
||||
import com.youlai.boot.common.annotation.RepeatSubmit;
|
||||
import com.youlai.boot.common.util.IPUtils;
|
||||
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.annotation.Pointcut;
|
||||
import org.redisson.api.RLock;
|
||||
import org.redisson.api.RedissonClient;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* 防重复提交切面
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 2.3.0
|
||||
*/
|
||||
@Aspect
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class RepeatSubmitAspect {
|
||||
|
||||
private final RedissonClient redissonClient;
|
||||
|
||||
/**
|
||||
* 防重复提交切点
|
||||
*/
|
||||
@Pointcut("@annotation(repeatSubmit)")
|
||||
public void repeatSubmitPointCut(RepeatSubmit repeatSubmit) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 环绕通知:处理防重复提交逻辑
|
||||
*/
|
||||
@Around(value = "repeatSubmitPointCut(repeatSubmit)", argNames = "pjp,repeatSubmit")
|
||||
public Object handleRepeatSubmit(ProceedingJoinPoint pjp, RepeatSubmit repeatSubmit) throws Throwable {
|
||||
String lockKey = buildLockKey();
|
||||
|
||||
int expire = repeatSubmit.expire();
|
||||
RLock lock = redissonClient.getLock(lockKey);
|
||||
|
||||
boolean locked = lock.tryLock(0, expire, TimeUnit.SECONDS);
|
||||
if (!locked) {
|
||||
throw new BusinessException(ResultCode.DUPLICATE_SUBMISSION);
|
||||
}
|
||||
return pjp.proceed();
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成防重复提交锁的 key
|
||||
* @return 锁的 key
|
||||
*/
|
||||
private String buildLockKey() {
|
||||
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
|
||||
// 用户唯一标识
|
||||
String userIdentifier = getUserIdentifier(request);
|
||||
// 请求唯一标识 = 请求方法 + 请求路径 + 请求参数(严谨的做法)
|
||||
String requestIdentifier = StrUtil.join(":", request.getMethod(), request.getRequestURI());
|
||||
return StrUtil.format(RedisConstants.Lock.RESUBMIT, userIdentifier, requestIdentifier);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户唯一标识
|
||||
* 1. 从请求头中获取 Token,使用 SHA-256 加密 Token 作为用户唯一标识
|
||||
* 2. 如果 Token 为空,使用 IP 作为用户唯一标识
|
||||
*
|
||||
* @param request 请求对象
|
||||
* @return 用户唯一标识
|
||||
*/
|
||||
private String getUserIdentifier(HttpServletRequest request) {
|
||||
// 用户身份唯一标识
|
||||
String userIdentifier;
|
||||
// 从请求头中获取 Token
|
||||
String tokenHeader = request.getHeader(HttpHeaders.AUTHORIZATION);
|
||||
if (StrUtil.isNotBlank(tokenHeader) && tokenHeader.startsWith(SecurityConstants.BEARER_TOKEN_PREFIX)) {
|
||||
String rawToken = tokenHeader.substring(SecurityConstants.BEARER_TOKEN_PREFIX.length()); // 去掉 Bearer 后的 Token
|
||||
userIdentifier = DigestUtil.sha256Hex(rawToken); // 使用 SHA-256 加密 Token 作为用户唯一标识
|
||||
} else {
|
||||
userIdentifier = IPUtils.getIpAddr(request); // 使用 IP 作为用户唯一标识
|
||||
}
|
||||
return userIdentifier;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package com.youlai.boot.common.base;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
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.fasterxml.jackson.annotation.JsonFormat;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import lombok.Data;
|
||||
@@ -14,8 +17,8 @@ import java.time.LocalDateTime;
|
||||
*
|
||||
* <p>实体类的基类,包含了实体类的公共属性,如创建时间、更新时间、逻辑删除标识等</p>
|
||||
*
|
||||
* @author Ray
|
||||
* @since 2024/6/23
|
||||
* @author Ray.Hao
|
||||
* @since 4.3.1
|
||||
*/
|
||||
@Data
|
||||
public class BaseEntity implements Serializable {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package com.youlai.boot.common.base;
|
||||
|
||||
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
|
||||
import java.util.EnumSet;
|
||||
@@ -9,8 +8,8 @@ import java.util.Objects;
|
||||
/**
|
||||
* 枚举通用接口
|
||||
*
|
||||
* @author haoxr
|
||||
* @since 2022/3/27 12:06
|
||||
* @author Ray.Hao
|
||||
* @since 4.3.1
|
||||
*/
|
||||
public interface IBaseEnum<T> {
|
||||
|
||||
@@ -37,12 +36,12 @@ public interface IBaseEnum<T> {
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据文本标签获取值
|
||||
* 根据值获取标签
|
||||
*
|
||||
* @param value
|
||||
* @param clazz
|
||||
* @param <E>
|
||||
* @return
|
||||
* @param value 枚举值
|
||||
* @param clazz 枚举类型
|
||||
* @param <E> 枚举
|
||||
* @return 标签文本
|
||||
*/
|
||||
static <E extends Enum<E> & IBaseEnum> String getLabelByValue(Object value, Class<E> clazz) {
|
||||
Objects.requireNonNull(value);
|
||||
@@ -59,14 +58,13 @@ public interface IBaseEnum<T> {
|
||||
return label;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 根据文本标签获取值
|
||||
* 根据标签获取值
|
||||
*
|
||||
* @param label
|
||||
* @param clazz
|
||||
* @param <E>
|
||||
* @return
|
||||
* @param label 标签文本
|
||||
* @param clazz 枚举类型
|
||||
* @param <E> 枚举
|
||||
* @return 枚举值
|
||||
*/
|
||||
static <E extends Enum<E> & IBaseEnum> Object getValueByLabel(String label, Class<E> clazz) {
|
||||
Objects.requireNonNull(label);
|
||||
@@ -84,5 +82,4 @@ public interface IBaseEnum<T> {
|
||||
return value;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ package com.youlai.boot.common.constant;
|
||||
* Redis 常量
|
||||
*
|
||||
* @author Theo
|
||||
* @since 2024-7-29 11:46:08
|
||||
* @since 4.3.1
|
||||
*/
|
||||
public interface RedisConstants {
|
||||
|
||||
@@ -32,7 +32,7 @@ public interface RedisConstants {
|
||||
String REFRESH_TOKEN_USER = "auth:token:refresh:{}";
|
||||
// 用户与访问令牌的映射(userId -> accessToken)
|
||||
String USER_ACCESS_TOKEN = "auth:user:access:{}";
|
||||
// 用户与刷新令牌的映射(userId -> refreshToken
|
||||
// 用户与刷新令牌的映射(userId -> refreshToken))
|
||||
String USER_REFRESH_TOKEN = "auth:user:refresh:{}";
|
||||
// 已撤销 Token 的 JTI(单端退出/会话注销):如果 jti 在撤销列表中,则 Token 立即无效
|
||||
String BLACKLIST_TOKEN = "auth:token:blacklist:{}";
|
||||
|
||||
@@ -6,7 +6,7 @@ import lombok.Getter;
|
||||
/**
|
||||
* 环境枚举
|
||||
*
|
||||
* @author Ray
|
||||
* @author Ray.Hao
|
||||
* @since 4.0.0
|
||||
*/
|
||||
@Getter
|
||||
|
||||
@@ -6,18 +6,17 @@ import lombok.Getter;
|
||||
/**
|
||||
* 状态枚举
|
||||
*
|
||||
* @author haoxr
|
||||
* @author Ray.Hao
|
||||
* @since 2022/10/14
|
||||
*/
|
||||
@Getter
|
||||
public enum StatusEnum implements IBaseEnum<Integer> {
|
||||
|
||||
ENABLE(1, "启用"),
|
||||
DISABLE (0, "禁用");
|
||||
DISABLE(0, "禁用");
|
||||
|
||||
private final Integer value;
|
||||
|
||||
|
||||
private final String label;
|
||||
|
||||
StatusEnum(Integer value, String label) {
|
||||
|
||||
@@ -20,13 +20,11 @@ public class BusinessException extends RuntimeException {
|
||||
this.resultCode = errorCode;
|
||||
}
|
||||
|
||||
|
||||
public BusinessException(IResultCode errorCode, String message) {
|
||||
super(message);
|
||||
this.resultCode = errorCode;
|
||||
}
|
||||
|
||||
|
||||
public BusinessException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import java.io.Serializable;
|
||||
* 统一响应结构体
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 2022/1/30
|
||||
* @since 4.3.1
|
||||
**/
|
||||
@Data
|
||||
public class Result<T> implements Serializable {
|
||||
@@ -64,10 +64,6 @@ public class Result<T> implements Serializable {
|
||||
return result(resultCode.getCode(), StrUtil.isNotBlank(msg) ? msg : resultCode.getMsg(), data);
|
||||
}
|
||||
|
||||
private static <T> Result<T> result(IResultCode resultCode, T data) {
|
||||
return result(resultCode.getCode(), resultCode.getMsg(), data);
|
||||
}
|
||||
|
||||
private static <T> Result<T> result(String code, String msg, T data) {
|
||||
Result<T> result = new Result<>();
|
||||
result.setCode(code);
|
||||
|
||||
@@ -48,7 +48,7 @@ import java.io.Serializable;
|
||||
* </pre>
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 2020/6/23
|
||||
* @since 4.3.1
|
||||
**/
|
||||
public enum ResultCode implements IResultCode, Serializable {
|
||||
|
||||
|
||||
@@ -12,10 +12,10 @@ import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.transaction.annotation.EnableTransactionManagement;
|
||||
|
||||
/**
|
||||
* mybatis-plus 配置类
|
||||
* MyBatis-Plus 配置类
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 2022/7/2
|
||||
* @since 4.3.1
|
||||
*/
|
||||
@Configuration
|
||||
@EnableTransactionManagement
|
||||
@@ -38,7 +38,7 @@ public class MybatisConfig {
|
||||
}
|
||||
|
||||
/**
|
||||
* 自动填充数据库创建人、创建时间、更新人、更新时间
|
||||
* 自动填充创建时间和更新时间
|
||||
*/
|
||||
@Bean
|
||||
public GlobalConfig globalConfig() {
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
package com.youlai.boot.framework.mybatis.handler;
|
||||
|
||||
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.ibatis.reflection.MetaObject;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* mybatis-plus 字段自动填充
|
||||
* MyBatis-Plus 字段自动填充
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 3.0.0
|
||||
*/
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class AutoFillMetaObjectHandler implements MetaObjectHandler {
|
||||
|
||||
@Override
|
||||
|
||||
@@ -11,7 +11,8 @@ import com.youlai.boot.framework.security.model.SysUserDetails;
|
||||
import com.youlai.boot.framework.security.util.SecurityUtils;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import net.sf.jsqlparser.expression.*;
|
||||
import net.sf.jsqlparser.expression.Expression;
|
||||
import net.sf.jsqlparser.expression.LongValue;
|
||||
import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
|
||||
import net.sf.jsqlparser.expression.operators.conditional.OrExpression;
|
||||
import net.sf.jsqlparser.expression.operators.relational.EqualsTo;
|
||||
@@ -30,8 +31,8 @@ import java.util.List;
|
||||
* <p>
|
||||
* 使用 JSQLParser 构建 SQL 条件,避免字符串拼接,提高代码安全性和可读性。
|
||||
*
|
||||
* @author zc
|
||||
* @since 2021-12-10 13:28
|
||||
* @author Ray.Hao
|
||||
* @since 2021/12/10
|
||||
*/
|
||||
@Slf4j
|
||||
public class MyDataPermissionHandler implements DataPermissionHandler {
|
||||
|
||||
@@ -13,10 +13,9 @@ import com.youlai.boot.framework.security.token.TokenManager;
|
||||
import com.youlai.boot.framework.security.service.SysUserDetailsService;
|
||||
import com.youlai.boot.system.service.UserService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.authentication.ProviderManager;
|
||||
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
|
||||
@@ -35,7 +34,7 @@ import org.springframework.security.web.authentication.UsernamePasswordAuthentic
|
||||
* Spring Security 配置类
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 2023/2/17
|
||||
* @since 4.3.1
|
||||
*/
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package com.youlai.boot.framework.web.advice;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import tools.jackson.core.JacksonException;
|
||||
import com.youlai.boot.common.exception.BusinessException;
|
||||
import com.youlai.boot.common.result.Result;
|
||||
import com.youlai.boot.common.result.ResultCode;
|
||||
@@ -25,6 +24,7 @@ import org.springframework.web.bind.annotation.ResponseStatus;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
|
||||
import org.springframework.web.servlet.NoHandlerFoundException;
|
||||
import tools.jackson.core.JacksonException;
|
||||
|
||||
import java.sql.SQLIntegrityConstraintViolationException;
|
||||
import java.sql.SQLSyntaxErrorException;
|
||||
@@ -34,18 +34,14 @@ import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 全局系统异常处理器
|
||||
* <p>
|
||||
* 调整异常处理的HTTP状态码,丰富异常处理类型
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 4.3.1
|
||||
*/
|
||||
@RestControllerAdvice
|
||||
@Slf4j
|
||||
public class GlobalExceptionHandler {
|
||||
|
||||
/**
|
||||
* 处理绑定异常
|
||||
* <p>
|
||||
* 当请求参数绑定到对象时发生错误,会抛出 BindException 异常。
|
||||
*/
|
||||
@ExceptionHandler(BindException.class)
|
||||
@ResponseStatus(HttpStatus.OK)
|
||||
public <T> Result<T> processException(BindException e) {
|
||||
@@ -54,12 +50,6 @@ public class GlobalExceptionHandler {
|
||||
return Result.failed(ResultCode.USER_REQUEST_PARAMETER_ERROR, msg);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 @RequestParam 参数校验异常
|
||||
* <p>
|
||||
* 当请求参数在校验过程中发生违反约束条件的异常时(如 @RequestParam 验证不通过),
|
||||
* 会捕获到 ConstraintViolationException 异常。
|
||||
*/
|
||||
@ExceptionHandler(ConstraintViolationException.class)
|
||||
@ResponseStatus(HttpStatus.OK)
|
||||
public <T> Result<T> processException(ConstraintViolationException e) {
|
||||
@@ -68,12 +58,6 @@ public class GlobalExceptionHandler {
|
||||
return Result.failed(ResultCode.INVALID_USER_INPUT, msg);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理方法参数校验异常
|
||||
* <p>
|
||||
* 当使用 @Valid 或 @Validated 注解对方法参数进行验证时,如果验证失败,
|
||||
* 会抛出 MethodArgumentNotValidException 异常。
|
||||
*/
|
||||
@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||
@ResponseStatus(HttpStatus.OK)
|
||||
public <T> Result<T> processException(MethodArgumentNotValidException e) {
|
||||
@@ -82,11 +66,6 @@ public class GlobalExceptionHandler {
|
||||
return Result.failed(ResultCode.INVALID_USER_INPUT, msg);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理接口不存在的异常
|
||||
* <p>
|
||||
* 当客户端请求一个不存在的路径时,会抛出 NoHandlerFoundException 异常。
|
||||
*/
|
||||
@ExceptionHandler(NoHandlerFoundException.class)
|
||||
@ResponseStatus(HttpStatus.NOT_FOUND)
|
||||
public <T> Result<T> processException(NoHandlerFoundException e) {
|
||||
@@ -94,11 +73,6 @@ public class GlobalExceptionHandler {
|
||||
return Result.failed(ResultCode.INTERFACE_NOT_EXIST);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理缺少请求参数的异常
|
||||
* <p>
|
||||
* 当请求缺少必需的参数时,会抛出 MissingServletRequestParameterException 异常。
|
||||
*/
|
||||
@ExceptionHandler(MissingServletRequestParameterException.class)
|
||||
@ResponseStatus(HttpStatus.OK)
|
||||
public <T> Result<T> processException(MissingServletRequestParameterException e) {
|
||||
@@ -106,11 +80,6 @@ public class GlobalExceptionHandler {
|
||||
return Result.failed(ResultCode.REQUEST_REQUIRED_PARAMETER_IS_EMPTY);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理方法参数类型不匹配的异常
|
||||
* <p>
|
||||
* 当请求参数类型不匹配时,会抛出 MethodArgumentTypeMismatchException 异常。
|
||||
*/
|
||||
@ExceptionHandler(MethodArgumentTypeMismatchException.class)
|
||||
@ResponseStatus(HttpStatus.OK)
|
||||
public <T> Result<T> processException(MethodArgumentTypeMismatchException e) {
|
||||
@@ -118,11 +87,6 @@ public class GlobalExceptionHandler {
|
||||
return Result.failed(ResultCode.PARAMETER_FORMAT_MISMATCH, "类型错误");
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 Servlet 异常
|
||||
* <p>
|
||||
* 当 Servlet 处理请求时发生异常时,会抛出 ServletException 异常。
|
||||
*/
|
||||
@ExceptionHandler(ServletException.class)
|
||||
@ResponseStatus(HttpStatus.OK)
|
||||
public <T> Result<T> processException(ServletException e) {
|
||||
@@ -130,11 +94,6 @@ public class GlobalExceptionHandler {
|
||||
return Result.failed(e.getMessage());
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理非法参数异常
|
||||
* <p>
|
||||
* 当方法接收到非法参数时,会抛出 IllegalArgumentException 异常。
|
||||
*/
|
||||
@ExceptionHandler(IllegalArgumentException.class)
|
||||
@ResponseStatus(HttpStatus.OK)
|
||||
public <T> Result<T> handleIllegalArgumentException(IllegalArgumentException e) {
|
||||
@@ -142,11 +101,6 @@ public class GlobalExceptionHandler {
|
||||
return Result.failed(e.getMessage());
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 JSON 处理异常
|
||||
* <p>
|
||||
* 当处理 JSON 数据时发生错误,会抛出 JacksonException 异常。
|
||||
*/
|
||||
@ExceptionHandler(JacksonException.class)
|
||||
@ResponseStatus(HttpStatus.OK)
|
||||
public <T> Result<T> handleJacksonException(JacksonException e) {
|
||||
@@ -154,11 +108,6 @@ public class GlobalExceptionHandler {
|
||||
return Result.failed(e.getMessage());
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理请求体不可读的异常
|
||||
* <p>
|
||||
* 当请求体不可读时,会抛出 HttpMessageNotReadableException 异常。
|
||||
*/
|
||||
@ExceptionHandler(HttpMessageNotReadableException.class)
|
||||
@ResponseStatus(HttpStatus.OK)
|
||||
public <T> Result<T> processException(HttpMessageNotReadableException e) {
|
||||
@@ -171,11 +120,6 @@ public class GlobalExceptionHandler {
|
||||
return Result.failed(errorMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理类型不匹配异常
|
||||
* <p>
|
||||
* 当方法参数类型不匹配时,会抛出 TypeMismatchException 异常。
|
||||
*/
|
||||
@ExceptionHandler(TypeMismatchException.class)
|
||||
@ResponseStatus(HttpStatus.OK)
|
||||
public <T> Result<T> processException(TypeMismatchException e) {
|
||||
@@ -183,11 +127,6 @@ public class GlobalExceptionHandler {
|
||||
return Result.failed(e.getMessage());
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 SQL 语法错误异常
|
||||
* <p>
|
||||
* 当 SQL 语法错误时,会抛出 BadSqlGrammarException 异常。
|
||||
*/
|
||||
@ExceptionHandler(BadSqlGrammarException.class)
|
||||
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
public <T> Result<T> handleBadSqlGrammarException(BadSqlGrammarException e) {
|
||||
@@ -198,11 +137,6 @@ public class GlobalExceptionHandler {
|
||||
return Result.failed(ResultCode.DATABASE_EXECUTION_ERROR);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 SQL 语法错误异常
|
||||
* <p>
|
||||
* 当 SQL 语法错误时,会抛出 SQLSyntaxErrorException 异常。
|
||||
*/
|
||||
@ExceptionHandler(SQLSyntaxErrorException.class)
|
||||
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
public <T> Result<T> processSQLSyntaxErrorException(SQLSyntaxErrorException e) {
|
||||
@@ -210,12 +144,6 @@ public class GlobalExceptionHandler {
|
||||
return Result.failed(ResultCode.DATABASE_EXECUTION_SYNTAX_ERROR);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 处理 SQL 违反了完整性约束
|
||||
* <p>
|
||||
* 当 SQL 违反了完整性约束时,会抛出 SQLIntegrityConstraintViolationException 异常。
|
||||
*/
|
||||
@ExceptionHandler(SQLIntegrityConstraintViolationException.class)
|
||||
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
public <T> Result<T> handleSQLIntegrityConstraintViolationException(SQLIntegrityConstraintViolationException e) {
|
||||
@@ -223,22 +151,12 @@ public class GlobalExceptionHandler {
|
||||
return Result.failed(ResultCode.INTEGRITY_CONSTRAINT_VIOLATION);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 Token 无效异常
|
||||
* <p>
|
||||
* 当 access_token 或 refresh_token 过期/无效时,返回 401。
|
||||
*/
|
||||
@ExceptionHandler(TokenInvalidException.class)
|
||||
@ResponseStatus(HttpStatus.UNAUTHORIZED)
|
||||
public <T> Result<T> handleTokenInvalidException(TokenInvalidException e) {
|
||||
return Result.failed(e.getResultCode());
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理业务异常
|
||||
* <p>
|
||||
* 当业务逻辑发生错误时,会抛出 BusinessException 异常。
|
||||
*/
|
||||
@ExceptionHandler(BusinessException.class)
|
||||
@ResponseStatus(HttpStatus.OK)
|
||||
public <T> Result<T> handleBizException(BusinessException e) {
|
||||
@@ -249,15 +167,9 @@ public class GlobalExceptionHandler {
|
||||
return Result.failed(e.getMessage());
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理所有未捕获的异常
|
||||
* <p>
|
||||
* 当发生未捕获的异常时,会抛出 Exception 异常。
|
||||
*/
|
||||
@ExceptionHandler(Exception.class)
|
||||
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
public <T> Result<T> handleException(Exception e) throws Exception {
|
||||
// 将 Spring Security 异常继续抛出,以便交给自定义处理器处理
|
||||
if (e instanceof AccessDeniedException
|
||||
|| e instanceof AuthenticationException) {
|
||||
throw e;
|
||||
@@ -266,12 +178,6 @@ public class GlobalExceptionHandler {
|
||||
return Result.failed(e.getLocalizedMessage());
|
||||
}
|
||||
|
||||
/**
|
||||
* 传参类型错误时,用于消息转换
|
||||
*
|
||||
* @param throwable 异常
|
||||
* @return 错误信息
|
||||
*/
|
||||
private String convertMessage(Throwable throwable) {
|
||||
String error = throwable.toString();
|
||||
String regulation = "\\[\"(.*?)\"]+";
|
||||
|
||||
@@ -26,6 +26,9 @@ import java.util.concurrent.TimeUnit;
|
||||
/**
|
||||
* 接口限流切面
|
||||
* <p>对 @RateLimit 标注的方法做 Redis 计数,超阈值则拒绝</p>
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 4.3.1
|
||||
*/
|
||||
@Aspect
|
||||
@Component
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
package com.youlai.boot.framework.web.aspect;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.crypto.digest.DigestUtil;
|
||||
import com.youlai.boot.common.constant.RedisConstants;
|
||||
import com.youlai.boot.common.constant.SecurityConstants;
|
||||
import com.youlai.boot.common.result.ResultCode;
|
||||
import com.youlai.boot.common.exception.BusinessException;
|
||||
import com.youlai.boot.common.annotation.RepeatSubmit;
|
||||
import com.youlai.boot.common.util.IPUtils;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.aspectj.lang.ProceedingJoinPoint;
|
||||
import org.aspectj.lang.annotation.Around;
|
||||
import org.aspectj.lang.annotation.Aspect;
|
||||
import org.aspectj.lang.annotation.Pointcut;
|
||||
import org.redisson.api.RLock;
|
||||
import org.redisson.api.RedissonClient;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* 防重复提交切面
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 4.3.1
|
||||
*/
|
||||
@Aspect
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class RepeatSubmitAspect {
|
||||
|
||||
private final RedissonClient redissonClient;
|
||||
|
||||
@Pointcut("@annotation(repeatSubmit)")
|
||||
public void repeatSubmitPointCut(RepeatSubmit repeatSubmit) {
|
||||
}
|
||||
|
||||
@Around(value = "repeatSubmitPointCut(repeatSubmit)", argNames = "pjp,repeatSubmit")
|
||||
public Object handleRepeatSubmit(ProceedingJoinPoint pjp, RepeatSubmit repeatSubmit) throws Throwable {
|
||||
String lockKey = buildLockKey();
|
||||
|
||||
int expire = repeatSubmit.expire();
|
||||
RLock lock = redissonClient.getLock(lockKey);
|
||||
|
||||
boolean locked = lock.tryLock(0, expire, TimeUnit.SECONDS);
|
||||
if (!locked) {
|
||||
throw new BusinessException(ResultCode.DUPLICATE_SUBMISSION);
|
||||
}
|
||||
return pjp.proceed();
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成防重复提交锁的 key
|
||||
*/
|
||||
private String buildLockKey() {
|
||||
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
|
||||
String userIdentifier = getUserIdentifier(request);
|
||||
String requestIdentifier = StrUtil.join(":", request.getMethod(), request.getRequestURI());
|
||||
return StrUtil.format(RedisConstants.Lock.RESUBMIT, userIdentifier, requestIdentifier);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户唯一标识
|
||||
* 1. 从请求头中获取 Token,使用 SHA-256 加密 Token 作为用户唯一标识
|
||||
* 2. 如果 Token 为空,使用 IP 作为用户唯一标识
|
||||
*/
|
||||
private String getUserIdentifier(HttpServletRequest request) {
|
||||
String userIdentifier;
|
||||
String tokenHeader = request.getHeader(HttpHeaders.AUTHORIZATION);
|
||||
if (StrUtil.isNotBlank(tokenHeader) && tokenHeader.startsWith(SecurityConstants.BEARER_TOKEN_PREFIX)) {
|
||||
String rawToken = tokenHeader.substring(SecurityConstants.BEARER_TOKEN_PREFIX.length());
|
||||
userIdentifier = DigestUtil.sha256Hex(rawToken);
|
||||
} else {
|
||||
userIdentifier = IPUtils.getIpAddr(request);
|
||||
}
|
||||
return userIdentifier;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -9,11 +9,9 @@ import java.util.concurrent.ThreadPoolExecutor;
|
||||
|
||||
/**
|
||||
* 操作日志线程池
|
||||
* <p>core=1 确保日志按序写入,max=2 峰值扩容,queue=1000 缓冲突发流量,
|
||||
* CallerRunsPolicy 保证满载时由调用线程兜底执行不丢数据。</p>
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 0.0.1
|
||||
* @since 3.0.0
|
||||
*/
|
||||
@Configuration
|
||||
public class OperationLogExecutorConfig {
|
||||
|
||||
@@ -2,11 +2,11 @@ package com.youlai.boot.system.controller;
|
||||
|
||||
import cn.idev.excel.EasyExcel;
|
||||
import cn.idev.excel.ExcelWriter;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||
import com.youlai.boot.common.annotation.Log;
|
||||
import com.youlai.boot.common.annotation.RepeatSubmit;
|
||||
import com.youlai.boot.common.enums.ActionTypeEnum;
|
||||
import com.youlai.boot.common.enums.LogModuleEnum;
|
||||
import com.youlai.boot.common.enums.StatusEnum;
|
||||
import com.youlai.boot.framework.security.util.SecurityUtils;
|
||||
import com.youlai.boot.common.model.Option;
|
||||
import com.youlai.boot.common.result.ExcelResult;
|
||||
@@ -14,9 +14,14 @@ import com.youlai.boot.common.result.PageResult;
|
||||
import com.youlai.boot.common.result.Result;
|
||||
import com.youlai.boot.common.util.ExcelUtils;
|
||||
import com.youlai.boot.system.listener.UserImportListener;
|
||||
import com.youlai.boot.system.model.form.*;
|
||||
import com.youlai.boot.system.model.form.EmailUpdateForm;
|
||||
import com.youlai.boot.system.model.form.MobileUpdateForm;
|
||||
import com.youlai.boot.system.model.form.PasswordUpdateForm;
|
||||
import com.youlai.boot.system.model.form.PasswordVerifyForm;
|
||||
import com.youlai.boot.system.model.form.UserForm;
|
||||
import com.youlai.boot.system.model.form.UserImportForm;
|
||||
import com.youlai.boot.system.model.form.UserProfileForm;
|
||||
import com.youlai.boot.system.model.vo.UserExportVO;
|
||||
import com.youlai.boot.system.model.entity.SysUser;
|
||||
import com.youlai.boot.system.model.query.UserQuery;
|
||||
import com.youlai.boot.system.model.vo.CurrentUserVO;
|
||||
import com.youlai.boot.system.model.vo.UserPageVO;
|
||||
@@ -32,7 +37,16 @@ import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PatchMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.File;
|
||||
@@ -46,7 +60,7 @@ import java.util.List;
|
||||
* 用户控制层
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 2022/10/16
|
||||
* @since 4.3.1
|
||||
*/
|
||||
@Tag(name = "02.用户接口")
|
||||
@RestController
|
||||
@@ -120,12 +134,9 @@ public class UserController {
|
||||
@Parameter(description = "用户ID") @PathVariable Long userId,
|
||||
@Parameter(description = "用户状态(1:启用;0:禁用)") @RequestParam Integer status
|
||||
) {
|
||||
boolean result = userService.update(new LambdaUpdateWrapper<SysUser>()
|
||||
.eq(SysUser::getId, userId)
|
||||
.set(SysUser::getStatus, status)
|
||||
);
|
||||
boolean result = userService.updateUserStatus(userId, status);
|
||||
// 用户禁用时立即失效其会话
|
||||
if (result && status == 0) {
|
||||
if (result && StatusEnum.DISABLE.getValue().equals(status)) {
|
||||
tokenManager.invalidateUserSessions(userId);
|
||||
}
|
||||
return Result.judge(result);
|
||||
|
||||
@@ -48,6 +48,11 @@ public class Menu extends BaseEntity {
|
||||
*/
|
||||
private String component;
|
||||
|
||||
/**
|
||||
* 外链地址
|
||||
*/
|
||||
private String externalUrl;
|
||||
|
||||
/**
|
||||
* 权限标识
|
||||
*/
|
||||
|
||||
@@ -38,6 +38,9 @@ public class MenuForm {
|
||||
@Schema(description = "组件路径(vue页面完整路径,省略.vue后缀)")
|
||||
private String component;
|
||||
|
||||
@Schema(description = "外链地址")
|
||||
private String externalUrl;
|
||||
|
||||
@Schema(description = "权限标识")
|
||||
private String perm;
|
||||
|
||||
|
||||
@@ -31,6 +31,9 @@ public class MenuVO {
|
||||
@Schema(description = "组件路径")
|
||||
private String component;
|
||||
|
||||
@Schema(description = "外链地址")
|
||||
private String externalUrl;
|
||||
|
||||
@Schema(description = "菜单排序(数字越小排名越靠前)")
|
||||
private Integer sort;
|
||||
|
||||
|
||||
@@ -55,6 +55,15 @@ public interface UserService extends IService<SysUser> {
|
||||
*/
|
||||
boolean updateUser(Long userId, UserForm userForm);
|
||||
|
||||
/**
|
||||
* 修改用户状态
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param status 状态值
|
||||
* @return {@link Boolean} 是否修改成功
|
||||
*/
|
||||
boolean updateUserStatus(Long userId, Integer status);
|
||||
|
||||
|
||||
/**
|
||||
* 删除用户
|
||||
|
||||
@@ -664,6 +664,14 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, SysUser> implements
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean updateUserStatus(Long userId, Integer status) {
|
||||
return this.update(new LambdaUpdateWrapper<SysUser>()
|
||||
.eq(SysUser::getId, userId)
|
||||
.set(SysUser::getStatus, status)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户选项列表
|
||||
*
|
||||
|
||||
@@ -162,14 +162,16 @@ springdoc:
|
||||
packages-to-scan: # 扫描的 Controller 包,限制只生成指定包的接口文档
|
||||
- com.youlai.boot.auth.controller
|
||||
- com.youlai.boot.system.controller
|
||||
- com.youlai.boot.module.file.controller
|
||||
- com.youlai.boot.module.codegen.controller
|
||||
default-flat-param-object: true # 将对象参数扁平化显示在文档中
|
||||
- com.youlai.boot.file.controller
|
||||
- com.youlai.boot.codegen.controller
|
||||
- com.youlai.boot.message.controller
|
||||
default-flat-param-object: true
|
||||
|
||||
# knife4j 配置
|
||||
knife4j:
|
||||
enable: true # 是否启用 Knife4j 增强功能
|
||||
production: false # 是否启用生产环境保护(true=生产环境隐藏文档,false=开发环境可访问)
|
||||
enable: true
|
||||
# 生产环境设置为 true 关闭文档
|
||||
production: false
|
||||
setting:
|
||||
language: zh_cn
|
||||
|
||||
|
||||
Reference in New Issue
Block a user