refactor: 目录结构优化

This commit is contained in:
ray committed 2024-08-30 08:18:53 +08:00
1 parent 7795c4d538
commit 95ef5dfd1f
215 files changed
+581 -727

No files matched your search

@@ -0,0 +1,15 @@
package com.youlai.boot;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication
@ConfigurationPropertiesScan
@EnableScheduling
public class BootApplication {
public static void main(String[] args) {
SpringApplication.run(BootApplication.class, args);
}
}
@@ -0,0 +1,28 @@
package com.youlai.boot.common.annotation;
import java.lang.annotation.*;
/**
* MP数据权限注解
*
* @author zc
* @since 2.0.0
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface DataPermission {
/**
* 数据权限 {@link com.baomidou.mybatisplus.extension.plugins.inner.DataPermissionInterceptor}
*/
String deptAlias() default "";
String deptIdColumnName() default "dept_id";
String userAlias() default "";
String userIdColumnName() default "create_by";
}
@@ -0,0 +1,23 @@
package com.youlai.boot.common.annotation;
import com.youlai.system.enums.LogModuleEnum;
import java.lang.annotation.*;
/**
* 日志注解
*
* @author Ray
* @since 2024/6/25
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface LogAnnotation {
String value() default "";
LogModuleEnum module() ;
}
@@ -0,0 +1,28 @@
package com.youlai.boot.common.annotation;
import java.lang.annotation.*;
/**
* 防止重复提交注解
* <p>
* 该注解用于方法上,防止在指定时间内的重复提交。
* 默认时间为5秒。
*
* @author haoxr
* @since 2.3.0
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface NoRepeat {
/**
* 锁过期时间(秒)
* <p>
* 默认5秒内不允许重复提交
*/
int expire() default 5;
}
@@ -0,0 +1,15 @@
package com.youlai.boot.common.base;
import com.alibaba.excel.event.AnalysisEventListener;
/**
* 自定义解析结果监听器
*
* @author haoxr
* @since 2023/03/01
*/
public abstract class BaseAnalysisEventListener<T> extends AnalysisEventListener<T> {
private String msg;
public abstract String getMsg();
}
@@ -0,0 +1,48 @@
package com.youlai.boot.common.base;
import com.baomidou.mybatisplus.annotation.*;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 基础实体类
*
* <p>实体类的基类,包含了实体类的公共属性,如创建时间、更新时间、逻辑删除标识等</p>
*
* @author Ray
* @since 2024/6/23
*/
@Data
public class BaseEntity implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 主键ID
*/
@TableId(type = IdType.AUTO)
private Long id;
/**
* 创建时间
*/
@TableField(fill = FieldFill.INSERT)
@JsonInclude(value = JsonInclude.Include.NON_NULL)
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;
/**
* 更新时间
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
@JsonInclude(value = JsonInclude.Include.NON_NULL)
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime updateTime;
}
@@ -0,0 +1,22 @@
package com.youlai.boot.common.base;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* 基础分页请求对象
*
* @author haoxr
* @since 2021/2/28
*/
@Data
@Schema
public class BasePageQuery {
@Schema(description = "页码", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private int pageNum = 1;
@Schema(description = "每页记录数", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
private int pageSize = 10;
}
@@ -0,0 +1,21 @@
package com.youlai.boot.common.base;
import lombok.Data;
import lombok.ToString;
import java.io.Serial;
import java.io.Serializable;
/**
* 视图对象基类
*
* @author haoxr
* @since 2022/10/22
*/
@Data
@ToString
public class BaseVO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
}
@@ -0,0 +1,88 @@
package com.youlai.boot.common.base;
import cn.hutool.core.util.ObjectUtil;
import java.util.EnumSet;
import java.util.Objects;
/**
* 枚举通用接口
*
* @author haoxr
* @since 2022/3/27 12:06
*/
public interface IBaseEnum<T> {
T getValue();
String getLabel();
/**
* 根据值获取枚举
*
* @param value
* @param clazz
* @param <E> 枚举
* @return
*/
static <E extends Enum<E> & IBaseEnum> E getEnumByValue(Object value, Class<E> clazz) {
Objects.requireNonNull(value);
EnumSet<E> allEnums = EnumSet.allOf(clazz); // 获取类型下的所有枚举
E matchEnum = allEnums.stream()
.filter(e -> ObjectUtil.equal(e.getValue(), value))
.findFirst()
.orElse(null);
return matchEnum;
}
/**
* 根据文本标签获取值
*
* @param value
* @param clazz
* @param <E>
* @return
*/
static <E extends Enum<E> & IBaseEnum> String getLabelByValue(Object value, Class<E> clazz) {
Objects.requireNonNull(value);
EnumSet<E> allEnums = EnumSet.allOf(clazz); // 获取类型下的所有枚举
E matchEnum = allEnums.stream()
.filter(e -> ObjectUtil.equal(e.getValue(), value))
.findFirst()
.orElse(null);
String label = null;
if (matchEnum != null) {
label = matchEnum.getLabel();
}
return label;
}
/**
* 根据文本标签获取值
*
* @param label
* @param clazz
* @param <E>
* @return
*/
static <E extends Enum<E> & IBaseEnum> Object getValueByLabel(String label, Class<E> clazz) {
Objects.requireNonNull(label);
EnumSet<E> allEnums = EnumSet.allOf(clazz); // 获取类型下的所有枚举
String finalLabel = label;
E matchEnum = allEnums.stream()
.filter(e -> ObjectUtil.equal(e.getLabel(), finalLabel))
.findFirst()
.orElse(null);
Object value = null;
if (matchEnum != null) {
value = matchEnum.getValue();
}
return value;
}
}
@@ -0,0 +1,33 @@
package com.youlai.boot.common.constant;
/**
* JWT Claims声明常量
* <p>
* JWT Claims 属于 Payload 的一部分,包含了一些实体(通常指的用户)的状态和额外的元数据。
*
* @author haoxr
* @since 2023/11/24
*/
public interface JwtClaimConstants {
/**
* 用户ID
*/
String USER_ID = "userId";
/**
* 部门ID
*/
String DEPT_ID = "deptId";
/**
* 数据权限
*/
String DATA_SCOPE = "dataScope";
/**
* 权限(角色Code)集合
*/
String AUTHORITIES = "authorities";
}
@@ -0,0 +1,43 @@
package com.youlai.boot.common.constant;
/**
* Redis Key常量
*
* @author Theo
* @since 2024-7-29 11:46:08
*/
public interface RedisConstants {
/**
* 系统配置Redis-key
*/
String SYSTEM_CONFIG_KEY = "system:config";
/**
* IP限流Redis-key
*/
String IP_RATE_LIMITER_KEY = "ip:rate:limiter:";
/**
* 防重复提交Redis-key
*/
String RESUBMIT_LOCK_PREFIX = "resubmit:lock:";
/**
* 单个IP请求的最大每秒查询数(QPS)阈值Key
*/
String IP_QPS_THRESHOLD_LIMIT_KEY = "IP_QPS_THRESHOLD_LIMIT";
/**
* 手机验证码缓存前缀
*/
String MOBILE_VERIFICATION_CODE_PREFIX = "VERIFICATION_CODE:MOBILE:";
/**
* 邮箱验证码缓存前缀
*/
String EMAIL_VERIFICATION_CODE_PREFIX = "VERIFICATION_CODE:EMAIL:";
}
@@ -0,0 +1,39 @@
package com.youlai.boot.common.constant;
/**
* 缓存常量
*
* @author haoxr
* @since 2023/11/24
*/
public interface SecurityConstants {
/**
* 验证码缓存前缀
*/
String CAPTCHA_CODE_PREFIX = "captcha_code:";
/**
* 角色和权限缓存前缀
*/
String ROLE_PERMS_PREFIX = "role_perms:";
/**
* 黑名单Token缓存前缀
*/
String BLACKLIST_TOKEN_PREFIX = "token:blacklist:";
/**
* 登录路径
*/
String LOGIN_PATH = "/api/v1/auth/login";
/**
* JWT Token 前缀
*/
String JWT_TOKEN_PREFIX = "Bearer ";
}
@@ -0,0 +1,120 @@
package com.youlai.boot.common.constant;
/**
* 符号和特殊符号常用类
*
* @author Theo
* @since 2024-7-29 11:46:08
*/
public interface SymbolConstant {
/**
* 符号:点
*/
String SPOT = ".";
/**
* 符号:双斜杠
*/
String DOUBLE_BACKSLASH = "\\";
/**
* 符号:冒号
*/
String COLON = ":";
/**
* 符号:逗号
*/
String COMMA = ",";
/**
* 符号:左花括号 {
*/
String LEFT_CURLY_BRACKET = "{";
/**
* 符号:右花括号 }
*/
String RIGHT_CURLY_BRACKET = "}";
/**
* 符号:井号 #
*/
String WELL_NUMBER = "#";
/**
* 符号:单斜杠
*/
String SINGLE_SLASH = "/";
/**
* 符号:双斜杠
*/
String DOUBLE_SLASH = "//";
/**
* 符号:感叹号
*/
String EXCLAMATORY_MARK = "!";
/**
* 符号:下划线
*/
String UNDERLINE = "_";
/**
* 符号:单引号
*/
String SINGLE_QUOTATION_MARK = "'";
/**
* 符号:星号
*/
String ASTERISK = "*";
/**
* 符号:百分号
*/
String PERCENT_SIGN = "%";
/**
* 符号:美元 $
*/
String DOLLAR = "$";
/**
* 符号:和 &
*/
String AND = "&";
/**
* 符号:../
*/
String SPOT_SINGLE_SLASH = "../";
/**
* 符号:..\\
*/
String SPOT_DOUBLE_BACKSLASH = "..\\";
/**
* 系统变量前缀 #{
*/
String SYS_VAR_PREFIX = "#{";
/**
* 符号 {{
*/
String DOUBLE_LEFT_CURLY_BRACKET = "{{";
/**
* 符号:[
*/
String SQUARE_BRACKETS_LEFT = "[";
/**
* 符号:]
*/
String SQUARE_BRACKETS_RIGHT = "]";
}
@@ -0,0 +1,28 @@
package com.youlai.boot.common.constant;
/**
* 系统常量
*
* @author haoxr
* @since 1.0.0
*/
public interface SystemConstants {
/**
* 根节点ID
*/
Long ROOT_NODE_ID = 0L;
/**
* 系统默认密码
*/
String DEFAULT_PASSWORD = "123456";
/**
* 超级管理员角色编码
*/
String ROOT_ROLE_CODE = "ROOT";
}
@@ -0,0 +1,27 @@
package com.youlai.boot.common.enums;
/**
* EasyCaptcha 验证码类型枚举
*
* @author haoxr
* @since 2.5.1
*/
public enum CaptchaTypeEnum {
/**
* 圆圈干扰验证码
*/
CIRCLE,
/**
* GIF验证码
*/
GIF,
/**
* 干扰线验证码
*/
LINE,
/**
* 扭曲干扰验证码
*/
SHEAR
}
@@ -0,0 +1,19 @@
package com.youlai.boot.common.enums;
/**
* 联系方式类型
*
* @author Ray
* @since 2.10.0
*/
public enum ContactType {
/**
* 手机
*/
MOBILE,
/**
* 邮箱
*/
EMAIL
}
@@ -0,0 +1,31 @@
package com.youlai.boot.common.enums;
import com.youlai.system.common.base.IBaseEnum;
import lombok.Getter;
/**
* 数据权限枚举
*
* @author haoxr
* @since 2.3.0
*/
@Getter
public enum DataScopeEnum implements IBaseEnum<Integer> {
/**
* value 越小,数据权限范围越大
*/
ALL(0, "所有数据"),
DEPT_AND_SUB(1, "部门及子部门数据"),
DEPT(2, "本部门数据"),
SELF(3, "本人数据");
private final Integer value;
private final String label;
DataScopeEnum(Integer value, String label) {
this.value = value;
this.label = label;
}
}
@@ -0,0 +1,84 @@
package com.youlai.boot.common.enums;
import com.baomidou.mybatisplus.annotation.EnumValue;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonValue;
import com.youlai.system.common.base.IBaseEnum;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
/**
* 表单类型枚举
*
* @author Ray
* @since 2.10.0
*/
@Getter
@RequiredArgsConstructor
public enum FormTypeEnum implements IBaseEnum<Integer> {
/**
* 输入框
*/
INPUT(1, "输入框"),
/**
* 下拉框
*/
SELECT(2, "下拉框"),
/**
* 单选框
*/
RADIO(3, "单选框"),
/**
* 复选框
*/
CHECK_BOX(4, "复选框"),
/**
* 数字输入框
*/
INPUT_NUMBER(5, "数字输入框"),
/**
* 开关
*/
SWITCH(6, "开关"),
/**
* 文本域
*/
TEXT_AREA(7, "文本域"),
/**
* 日期时间框
*/
DATE(8, "日期框"),
/**
* 日期框
*/
DATE_TIME(9, "日期时间框");
// Mybatis-Plus 提供注解表示插入数据库时插入该值
@EnumValue
@JsonValue
private final Integer value;
// @JsonValue // 表示对枚举序列化时返回此字段
private final String label;
@JsonCreator
public static QueryTypeEnum fromValue(Integer value) {
for (QueryTypeEnum type : QueryTypeEnum.values()) {
if (type.getValue().equals(value)) {
return type;
}
}
throw new IllegalArgumentException("No enum constant with value " + value);
}
}
@@ -0,0 +1,28 @@
package com.youlai.boot.common.enums;
import com.youlai.system.common.base.IBaseEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
/**
* 性别枚举
*
* @author haoxr
* @since 2022/10/14
*/
@Getter
@Schema(enumAsRef = true)
public enum GenderEnum implements IBaseEnum<Integer> {
MALE(1, "男"),
FEMALE (2, "女");
private final Integer value;
private final String label;
GenderEnum(Integer value, String label) {
this.value = value;
this.label = label;
}
}
@@ -0,0 +1,84 @@
package com.youlai.boot.common.enums;
import lombok.Getter;
import java.util.HashMap;
import java.util.Map;
/**
* 表单类型枚举
*
* @author Ray
* @since 2.10.0
*/
@Getter
public enum JavaTypeEnum {
VARCHAR("varchar", "String", "string"),
CHAR("char", "String", "string"),
BLOB("blob", "byte[]", "Uint8Array"),
TEXT("text", "String", "string"),
JSON("json", "String", "any"),
INTEGER("int", "Integer", "number"),
TINYINT("tinyint", "Integer", "number"),
SMALLINT("smallint", "Integer", "number"),
MEDIUMINT("mediumint", "Integer", "number"),
BIGINT("bigint", "Long", "bigint"),
FLOAT("float", "Float", "number"),
DOUBLE("double", "Double", "number"),
DECIMAL("decimal", "BigDecimal", "number"),
DATE("date", "LocalDate", "Date"),
DATETIME("datetime", "LocalDateTime", "Date");
// 数据库类型
private final String dbType;
// Java类型
private final String javaType;
// TypeScript类型
private final String tsType;
// 数据库类型和Java类型的映射
private static final Map<String, JavaTypeEnum> typeMap = new HashMap<>();
// 初始化映射关系
static {
for (JavaTypeEnum javaTypeEnum : JavaTypeEnum.values()) {
typeMap.put(javaTypeEnum.getDbType(), javaTypeEnum);
}
}
JavaTypeEnum(String dbType, String javaType, String tsType) {
this.dbType = dbType;
this.javaType = javaType;
this.tsType = tsType;
}
/**
* 根据数据库类型获取对应的Java类型
*
* @param columnType 列类型
* @return 对应的Java类型
*/
public static String getJavaTypeByColumnType(String columnType) {
JavaTypeEnum javaTypeEnum = typeMap.get(columnType);
if (javaTypeEnum != null) {
return javaTypeEnum.getJavaType();
}
return null;
}
/**
* 根据Java类型获取对应的TypeScript类型
*
* @param javaType Java类型
* @return 对应的TypeScript类型
*/
public static String getTsTypeByJavaType(String javaType) {
for (JavaTypeEnum javaTypeEnum : JavaTypeEnum.values()) {
if (javaTypeEnum.getJavaType().equals(javaType)) {
return javaTypeEnum.getTsType();
}
}
return null;
}
}
@@ -0,0 +1,33 @@
package com.youlai.boot.common.enums;
import com.fasterxml.jackson.annotation.JsonValue;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
/**
* 日志模块枚举
*
* @author Ray
* @since 2.10.0
*/
@Schema(enumAsRef = true)
@Getter
public enum LogModuleEnum {
LOGIN("登录"),
USER("用户"),
DEPT("部门"),
ROLE("角色"),
MENU("菜单"),
DICT("字典"),
OTHER("其他")
;
@JsonValue
private final String moduleName;
LogModuleEnum(String moduleName) {
this.moduleName = moduleName;
}
}
@@ -0,0 +1,34 @@
package com.youlai.boot.common.enums;
import com.baomidou.mybatisplus.annotation.EnumValue;
import com.youlai.system.common.base.IBaseEnum;
import lombok.Getter;
/**
* 菜单类型枚举
*
* @author haoxr
* @since 2022/4/23 9:36
*/
@Getter
public enum MenuTypeEnum implements IBaseEnum<Integer> {
NULL(0, null),
MENU(1, "菜单"),
CATALOG(2, "目录"),
EXTLINK(3, "外链"),
BUTTON(4, "按钮");
// Mybatis-Plus 提供注解表示插入数据库时插入该值
@EnumValue
private final Integer value;
// @JsonValue // 表示对枚举序列化时返回此字段
private final String label;
MenuTypeEnum(Integer value, String label) {
this.value = value;
this.label = label;
}
}
@@ -0,0 +1,73 @@
package com.youlai.boot.common.enums;
import com.baomidou.mybatisplus.annotation.EnumValue;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonValue;
import com.youlai.system.common.base.IBaseEnum;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
/**
* 查询类型枚举
*
* @author Ray
* @since 2.10.0
*/
@Getter
@RequiredArgsConstructor
public enum QueryTypeEnum implements IBaseEnum<Integer> {
/** 等于 */
EQ(1, "="),
/** 模糊匹配 */
LIKE(2, "LIKE '%s%'"),
/** 包含 */
IN(3, "IN"),
/** 范围 */
BETWEEN(4, "BETWEEN"),
/** 大于 */
GT(5, ">"),
/** 大于等于 */
GE(6, ">="),
/** 小于 */
LT(7, "<"),
/** 小于等于 */
LE(8, "<="),
/** 不等于 */
NE(9, "!="),
/** 左模糊匹配 */
LIKE_LEFT(10, "LIKE '%s'"),
/** 右模糊匹配 */
LIKE_RIGHT(11, "LIKE 's%'");
// 存储在数据库中的枚举属性值
@EnumValue
@JsonValue
private final Integer value;
// 序列化成 JSON 时的属性值
private final String label;
@JsonCreator
public static QueryTypeEnum fromValue(Integer value) {
for (QueryTypeEnum type : QueryTypeEnum.values()) {
if (type.getValue().equals(value)) {
return type;
}
}
throw new IllegalArgumentException("No enum constant with value " + value);
}
}
@@ -0,0 +1,27 @@
package com.youlai.boot.common.enums;
import com.youlai.system.common.base.IBaseEnum;
import lombok.Getter;
/**
* 状态枚举
*
* @author haoxr
* @since 2022/10/14
*/
public enum StatusEnum implements IBaseEnum<Integer> {
ENABLE(1, "启用"),
DISABLE (0, "禁用");
@Getter
private Integer value;
@Getter
private String label;
StatusEnum(Integer value, String label) {
this.value = value;
this.label = label;
}
}
@@ -0,0 +1,38 @@
package com.youlai.boot.common.exception;
import com.youlai.boot.common.result.IResultCode;
import lombok.Getter;
import org.slf4j.helpers.MessageFormatter;
/**
* 自定义业务异常
*
* @author Ray
* @since 2022/7/31
*/
@Getter
public class BusinessException extends RuntimeException {
public IResultCode resultCode;
public BusinessException(IResultCode errorCode) {
super(errorCode.getMsg());
this.resultCode = errorCode;
}
public BusinessException(String message, Throwable cause) {
super(message, cause);
}
public BusinessException(Throwable cause) {
super(cause);
}
public BusinessException(String message, Object... args) {
super(formatMessage(message, args));
}
private static String formatMessage(String message, Object... args) {
return MessageFormatter.arrayFormat(message, args).getMessage();
}
}
@@ -0,0 +1,219 @@
package com.youlai.boot.common.exception;
import cn.hutool.core.util.StrUtil;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.youlai.system.common.result.Result;
import com.youlai.system.common.result.ResultCode;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.TypeMismatchException;
import org.springframework.context.support.DefaultMessageSourceResolvable;
import org.springframework.http.HttpStatus;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.jdbc.BadSqlGrammarException;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.validation.BindException;
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.method.annotation.MethodArgumentTypeMismatchException;
import org.springframework.web.servlet.NoHandlerFoundException;
import jakarta.servlet.ServletException;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.ConstraintViolationException;
import java.sql.SQLSyntaxErrorException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
/**
* 全局系统异常处理器
* <p>
* 调整异常处理的HTTP状态码,丰富异常处理类型
*
* @author Gadfly
* @since 2020-02-25 13:54
**/
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
@ExceptionHandler(BindException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public <T> Result<T> processException(BindException e) {
log.error("BindException:{}", e.getMessage());
String msg = e.getAllErrors().stream().map(DefaultMessageSourceResolvable::getDefaultMessage).collect(Collectors.joining(";"));
return Result.failed(ResultCode.PARAM_ERROR, msg);
}
/**
* RequestParam参数的校验
*
* @param e
* @param <T>
* @return
*/
@ExceptionHandler(ConstraintViolationException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public <T> Result<T> processException(ConstraintViolationException e) {
log.error("ConstraintViolationException:{}", e.getMessage());
String msg = e.getConstraintViolations().stream().map(ConstraintViolation::getMessage).collect(Collectors.joining(";"));
return Result.failed(ResultCode.PARAM_ERROR, msg);
}
/**
* RequestBody参数的校验
*
* @param e
* @param <T>
* @return
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public <T> Result<T> processException(MethodArgumentNotValidException e) {
log.error("MethodArgumentNotValidException:{}", e.getMessage());
String msg = e.getBindingResult().getAllErrors().stream().map(DefaultMessageSourceResolvable::getDefaultMessage).collect(Collectors.joining(";"));
return Result.failed(ResultCode.PARAM_ERROR, msg);
}
@ExceptionHandler(NoHandlerFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public <T> Result<T> processException(NoHandlerFoundException e) {
log.error(e.getMessage(), e);
return Result.failed(ResultCode.RESOURCE_NOT_FOUND);
}
/**
* MissingServletRequestParameterException
*/
@ExceptionHandler(MissingServletRequestParameterException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public <T> Result<T> processException(MissingServletRequestParameterException e) {
log.error(e.getMessage(), e);
return Result.failed(ResultCode.PARAM_IS_NULL);
}
/**
* MethodArgumentTypeMismatchException
*/
@ExceptionHandler(MethodArgumentTypeMismatchException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public <T> Result<T> processException(MethodArgumentTypeMismatchException e) {
log.error(e.getMessage(), e);
return Result.failed(ResultCode.PARAM_ERROR, "类型错误");
}
/**
* ServletException
*/
@ExceptionHandler(ServletException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public <T> Result<T> processException(ServletException e) {
log.error(e.getMessage(), e);
return Result.failed(e.getMessage());
}
@ExceptionHandler(IllegalArgumentException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public <T> Result<T> handleIllegalArgumentException(IllegalArgumentException e) {
log.error("非法参数异常,异常原因:{}", e.getMessage(), e);
return Result.failed(e.getMessage());
}
@ExceptionHandler(JsonProcessingException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public <T> Result<T> handleJsonProcessingException(JsonProcessingException e) {
log.error("Json转换异常,异常原因:{}", e.getMessage(), e);
return Result.failed(e.getMessage());
}
/**
* HttpMessageNotReadableException
*/
@ExceptionHandler(HttpMessageNotReadableException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public <T> Result<T> processException(HttpMessageNotReadableException e) {
log.error(e.getMessage(), e);
String errorMessage = "请求体不可为空";
Throwable cause = e.getCause();
if (cause != null) {
errorMessage = convertMessage(cause);
}
return Result.failed(errorMessage);
}
@ExceptionHandler(TypeMismatchException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public <T> Result<T> processException(TypeMismatchException e) {
log.error(e.getMessage(), e);
return Result.failed(e.getMessage());
}
@ExceptionHandler(BadSqlGrammarException.class)
@ResponseStatus(HttpStatus.FORBIDDEN)
public <T> Result<T> handleBadSqlGrammarException(BadSqlGrammarException e) {
log.error(e.getMessage(), e);
String errorMsg = e.getMessage();
if (StrUtil.isNotBlank(errorMsg) && errorMsg.contains("denied to user")) {
return Result.failed(ResultCode.FORBIDDEN_OPERATION);
} else {
return Result.failed(e.getMessage());
}
}
@ExceptionHandler(SQLSyntaxErrorException.class)
@ResponseStatus(HttpStatus.FORBIDDEN)
public <T> Result<T> processSQLSyntaxErrorException(SQLSyntaxErrorException e) {
log.error(e.getMessage(), e);
return Result.failed(e.getMessage());
}
@ExceptionHandler(BusinessException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public <T> Result<T> handleBizException(BusinessException e) {
log.error("biz exception: {}", e.getMessage());
if (e.getResultCode() != null) {
return Result.failed(e.getResultCode());
}
return Result.failed(e.getMessage());
}
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public <T> Result<T> handleException(Exception e) throws Exception{
// 将 Spring Security 异常继续抛出,以便交给自定义处理器处理
if (e instanceof AccessDeniedException
|| e instanceof AuthenticationException) {
throw e;
}
log.error("unknown exception: {}", e.getMessage());
e.printStackTrace();
return Result.failed(e.getLocalizedMessage());
}
/**
* 传参类型错误时,用于消息转换
*
* @param throwable 异常
* @return 错误信息
*/
private String convertMessage(Throwable throwable) {
String error = throwable.toString();
String regulation = "\\[\"(.*?)\"]+";
Pattern pattern = Pattern.compile(regulation);
Matcher matcher = pattern.matcher(error);
String group = "";
if (matcher.find()) {
String matchString = matcher.group();
matchString = matchString.replace("[", "").replace("]", "");
matchString = "%s字段类型错误".formatted(matchString.replaceAll("\"", ""));
group += matchString;
}
return group;
}
}
@@ -0,0 +1,32 @@
package com.youlai.boot.common.model;
import com.fasterxml.jackson.annotation.JsonInclude;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
/**
* 下拉选项对象
*
* @author haoxr
* @since 2024/5/25
*/
@Schema(description ="键值对")
@Data
@NoArgsConstructor
public class KeyValue{
public KeyValue(String key, String value) {
this.key = key;
this.value = value;
}
@Schema(description="选项的值")
private String key;
@Schema(description="选项的标签")
private String value;
}
@@ -0,0 +1,42 @@
package com.youlai.boot.common.model;
import com.fasterxml.jackson.annotation.JsonInclude;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
/**
* 下拉选项对象
*
* @author haoxr
* @since 2022/1/22
*/
@Schema(description ="下拉选项对象")
@Data
@NoArgsConstructor
public class Option<T> {
public Option(T value, String label) {
this.value = value;
this.label = label;
}
public Option(T value, String label, List<Option<T>> children) {
this.value = value;
this.label = label;
this.children= children;
}
@Schema(description="选项的值")
private T value;
@Schema(description="选项的标签")
private String label;
@Schema(description="子选项列表")
@JsonInclude(value = JsonInclude.Include.NON_EMPTY)
private List<Option<T>> children;
}
@@ -0,0 +1,15 @@
package com.youlai.boot.common.result;
/**
* 响应码接口
*
* @author Ray
* @since 2022/2/18
**/
public interface IResultCode {
String getCode();
String getMsg();
}
@@ -0,0 +1,46 @@
package com.youlai.boot.common.result;
import com.baomidou.mybatisplus.core.metadata.IPage;
import lombok.Data;
import java.io.Serializable;
import java.util.List;
/**
* 分页响应结构体
*
* @author Ray
* @since 2022/2/18
*/
@Data
public class PageResult<T> implements Serializable {
private String code;
private Data<T> data;
private String msg;
public static <T> PageResult<T> success(IPage<T> page) {
PageResult<T> result = new PageResult<>();
result.setCode(ResultCode.SUCCESS.getCode());
Data data = new Data<T>();
data.setList(page.getRecords());
data.setTotal(page.getTotal());
result.setData(data);
result.setMsg(ResultCode.SUCCESS.getMsg());
return result;
}
@lombok.Data
public static class Data<T> {
private List<T> list;
private long total;
}
}
@@ -0,0 +1,73 @@
package com.youlai.boot.common.result;
import lombok.Data;
import java.io.Serializable;
/**
* 统一响应结构体
*
* @author Ray
* @since 2022/1/30
**/
@Data
public class Result<T> implements Serializable {
private String code;
private T data;
private String msg;
public static <T> Result<T> success() {
return success(null);
}
public static <T> Result<T> success(T data) {
Result<T> result = new Result<>();
result.setCode(ResultCode.SUCCESS.getCode());
result.setMsg(ResultCode.SUCCESS.getMsg());
result.setData(data);
return result;
}
public static <T> Result<T> failed() {
return result(ResultCode.SYSTEM_EXECUTION_ERROR.getCode(), ResultCode.SYSTEM_EXECUTION_ERROR.getMsg(), null);
}
public static <T> Result<T> failed(String msg) {
return result(ResultCode.SYSTEM_EXECUTION_ERROR.getCode(), msg, null);
}
public static <T> Result<T> judge(boolean status) {
if (status) {
return success();
} else {
return failed();
}
}
public static <T> Result<T> failed(IResultCode resultCode) {
return result(resultCode.getCode(), resultCode.getMsg(), null);
}
public static <T> Result<T> failed(IResultCode resultCode, String msg) {
return result(resultCode.getCode(), msg, null);
}
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);
result.setData(data);
result.setMsg(msg);
return result;
}
public static boolean isSuccess(Result<?> result) {
return result != null && ResultCode.SUCCESS.getCode().equals(result.getCode());
}
}
@@ -0,0 +1,116 @@
package com.youlai.boot.common.result;
import lombok.AllArgsConstructor;
import lombok.NoArgsConstructor;
import java.io.Serializable;
/**
* 响应码枚举
* <p>
* 参考阿里巴巴开发手册响应码规范
*
* @author Ray
* @since 2020/6/23
**/
@AllArgsConstructor
@NoArgsConstructor
public enum ResultCode implements IResultCode, Serializable {
SUCCESS("00000", "一切ok"),
USER_ERROR("A0001", "用户端错误"),
REPEAT_SUBMIT_ERROR("A0002", "您的请求已提交,请不要重复提交或等待片刻再尝试。"),
USER_LOGIN_ERROR("A0200", "用户登录异常"),
USER_NOT_EXIST("A0201", "用户不存在"),
USER_ACCOUNT_LOCKED("A0202", "用户账户被冻结"),
USER_ACCOUNT_INVALID("A0203", "用户账户已作废"),
USERNAME_OR_PASSWORD_ERROR("A0210", "用户名或密码错误"),
PASSWORD_ENTER_EXCEED_LIMIT("A0211", "用户输入密码次数超限"),
CLIENT_AUTHENTICATION_FAILED("A0212", "客户端认证失败"),
VERIFY_CODE_TIMEOUT("A0213", "验证码已过期"),
VERIFY_CODE_ERROR("A0214", "验证码错误"),
TOKEN_INVALID("A0230", "token无效或已过期"),
TOKEN_ACCESS_FORBIDDEN("A0231", "token已被禁止访问"),
AUTHORIZED_ERROR("A0300", "访问权限异常"),
ACCESS_UNAUTHORIZED("A0301", "访问未授权"),
FORBIDDEN_OPERATION("A0302", "演示环境禁止新增、修改和删除数据,请本地部署后测试"),
PARAM_ERROR("A0400", "用户请求参数错误"),
RESOURCE_NOT_FOUND("A0401", "请求资源不存在"),
PARAM_IS_NULL("A0410", "请求必填参数为空"),
USER_UPLOAD_FILE_ERROR("A0700", "用户上传文件异常"),
USER_UPLOAD_FILE_TYPE_NOT_MATCH("A0701", "用户上传文件类型不匹配"),
USER_UPLOAD_FILE_SIZE_EXCEEDS("A0702", "用户上传文件太大"),
USER_UPLOAD_IMAGE_SIZE_EXCEEDS("A0703", "用户上传图片太大"),
SYSTEM_EXECUTION_ERROR("B0001", "系统执行出错"),
SYSTEM_EXECUTION_TIMEOUT("B0100", "系统执行超时"),
SYSTEM_ORDER_PROCESSING_TIMEOUT("B0100", "系统订单处理超时"),
SYSTEM_DISASTER_RECOVERY_TRIGGER("B0200", "系统容灾功能被触发"),
FLOW_LIMITING("B0210", "系统限流,请稍后再试"),
DEGRADATION("B0220", "系统功能降级"),
SYSTEM_RESOURCE_ERROR("B0300", "系统资源异常"),
SYSTEM_RESOURCE_EXHAUSTION("B0310", "系统资源耗尽"),
SYSTEM_RESOURCE_ACCESS_ERROR("B0320", "系统资源访问异常"),
SYSTEM_READ_DISK_FILE_ERROR("B0321", "系统读取磁盘文件失败"),
CALL_THIRD_PARTY_SERVICE_ERROR("C0001", "调用第三方服务出错"),
MIDDLEWARE_SERVICE_ERROR("C0100", "中间件服务出错"),
INTERFACE_NOT_EXIST("C0113", "接口不存在"),
MESSAGE_SERVICE_ERROR("C0120", "消息服务出错"),
MESSAGE_DELIVERY_ERROR("C0121", "消息投递出错"),
MESSAGE_CONSUMPTION_ERROR("C0122", "消息消费出错"),
MESSAGE_SUBSCRIPTION_ERROR("C0123", "消息订阅出错"),
MESSAGE_GROUP_NOT_FOUND("C0124", "消息分组未查到"),
DATABASE_ERROR("C0300", "数据库服务出错"),
DATABASE_TABLE_NOT_EXIST("C0311", "表不存在"),
DATABASE_COLUMN_NOT_EXIST("C0312", "列不存在"),
DATABASE_DUPLICATE_COLUMN_NAME("C0321", "多表关联中存在多个相同名称的列"),
DATABASE_DEADLOCK("C0331", "数据库死锁"),
DATABASE_PRIMARY_KEY_CONFLICT("C0341", "主键冲突");
@Override
public String getCode() {
return code;
}
@Override
public String getMsg() {
return msg;
}
private String code;
private String msg;
@Override
public String toString() {
return "{" +
"\"code\":\"" + code + '\"' +
", \"msg\":\"" + msg + '\"' +
'}';
}
public static ResultCode getValue(String code) {
for (ResultCode value : values()) {
if (value.getCode().equals(code)) {
return value;
}
}
return SYSTEM_EXECUTION_ERROR; // 默认系统执行错误
}
}
@@ -0,0 +1,31 @@
package com.youlai.boot.common.service;
/**
* 邮件服务接口层
*
* @author Ray
* @since 2024/8/17
*/
public interface MailService {
/**
* 发送简单文本邮件
*
* @param to 收件人地址
* @param subject 邮件主题
* @param text 邮件内容
*/
boolean sendSimpleMail(String to, String subject, String text) ;
/**
* 发送带附件的邮件
*
* @param to 收件人地址
* @param subject 邮件主题
* @param text 邮件内容
* @param filePath 附件路径
*/
boolean sendMailWithAttachment(String to, String subject, String text, String filePath);
}
@@ -0,0 +1,30 @@
package com.youlai.boot.common.service;
import com.youlai.boot.module.system.model.dto.FileInfo;
import org.springframework.web.multipart.MultipartFile;
/**
* 对象存储服务接口层
*
* @author haoxr
* @since 2022/11/19
*/
public interface OssService {
/**
* 上传文件
* @param file 表单文件对象
* @return 文件信息
*/
FileInfo uploadFile(MultipartFile file);
/**
* 删除文件
*
* @param filePath 文件完整URL
* @return 删除结果
*/
boolean deleteFile(String filePath);
}
@@ -0,0 +1,22 @@
package com.youlai.boot.common.service;
/**
* 短信服务接口层
* <p>
* SMS = Short Message Service 短信服务
*
* @author Ray
* @since 2024/8/17
*/
public interface SmsService {
/**
* 发送短信
*
* @param mobile 手机号 13388886666
* @param templateCode 短信模板 SMS_194640010
* @param templateParam 模板参数 "[{"code":"123456"}]"
* @return boolean 是否发送成功
*/
boolean sendSms(String mobile, String templateCode, String templateParam);
}
@@ -0,0 +1,98 @@
package com.youlai.boot.common.service.impl;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.IdUtil;
import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import com.aliyun.oss.model.ObjectMetadata;
import com.aliyun.oss.model.PutObjectRequest;
import com.youlai.boot.module.system.model.dto.FileInfo;
import com.youlai.system.service.OssService;
import jakarta.annotation.PostConstruct;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import java.io.InputStream;
import java.time.LocalDateTime;
/**
* Aliyun 对象存储服务类
*
* @author haoxr
* @since 2.3.0
*/
@Component
@ConditionalOnProperty(value = "oss.type", havingValue = "aliyun")
@ConfigurationProperties(prefix = "oss.aliyun")
@RequiredArgsConstructor
@Data
public class AliyunOssService implements OssService {
/**
* 服务Endpoint
*/
private String endpoint;
/**
* 访问凭据
*/
private String accessKeyId;
/**
* 凭据密钥
*/
private String accessKeySecret;
/**
* 存储桶名称
*/
private String bucketName;
private OSS aliyunOssClient;
@PostConstruct
public void init() {
aliyunOssClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
}
@Override
@SneakyThrows
public FileInfo uploadFile(MultipartFile file) {
// 生成文件名(日期文件夹)
String suffix = FileUtil.getSuffix(file.getOriginalFilename());
String uuid = IdUtil.simpleUUID();
String fileName = DateUtil.format(LocalDateTime.now(), "yyyyMMdd") + "/" + uuid + "." + suffix;
// try-with-resource 语法糖自动释放流
try (InputStream inputStream = file.getInputStream()) {
// 设置上传文件的元信息,例如Content-Type
ObjectMetadata metadata = new ObjectMetadata();
metadata.setContentType(file.getContentType());
// 创建PutObjectRequest对象,指定Bucket名称、对象名称和输入流
PutObjectRequest putObjectRequest = new PutObjectRequest(bucketName, fileName, inputStream, metadata);
// 上传文件
aliyunOssClient.putObject(putObjectRequest);
} catch (Exception e) {
throw new RuntimeException("文件上传失败");
}
// 获取文件访问路径
String fileUrl = "https://" + bucketName + "." + endpoint + "/" + fileName;
FileInfo fileInfo = new FileInfo();
fileInfo.setName(fileName);
fileInfo.setUrl(fileUrl);
return fileInfo;
}
@Override
public boolean deleteFile(String filePath) {
Assert.notBlank(filePath, "删除文件路径不能为空");
String fileHost = "https://" + bucketName + "." + endpoint; // 文件主机域名
String fileName = filePath.substring(fileHost.length() + 1); // +1 是/占一个字符,截断左闭右开
aliyunOssClient.deleteObject(bucketName, fileName);
return true;
}
}
@@ -0,0 +1,77 @@
package com.youlai.boot.common.service.impl;
import com.aliyuncs.CommonRequest;
import com.aliyuncs.CommonResponse;
import com.aliyuncs.DefaultAcsClient;
import com.aliyuncs.IAcsClient;
import com.aliyuncs.exceptions.ClientException;
import com.aliyuncs.exceptions.ServerException;
import com.aliyuncs.http.MethodType;
import com.aliyuncs.profile.DefaultProfile;
import com.youlai.boot.config.property.AliyunSmsProperties;
import com.youlai.system.service.SmsService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
/**
* 阿里云短信业务类
*
* @author Ray
* @since 2024/8/17
*/
@Service
@RequiredArgsConstructor
public class AliyunSmsService implements SmsService {
private final AliyunSmsProperties aliyunSmsProperties;
/**
* 发送短信验证码
*
* @param mobile 手机号 13388886666
* @param templateCode 短信模板 SMS_194640010
* @param templateParam 模板参数 "[{"code":"123456"}]"
*
* @return boolean 是否发送成功
*/
@Override
public boolean sendSms(String mobile,String templateCode,String templateParam) {
DefaultProfile profile = DefaultProfile.getProfile(aliyunSmsProperties.getRegionId(),
aliyunSmsProperties.getAccessKeyId(), aliyunSmsProperties.getAccessKeySecret());
IAcsClient client = new DefaultAcsClient(profile);
// 创建通用的请求对象
CommonRequest request = new CommonRequest();
// 指定请求方式
request.setSysMethod(MethodType.POST);
// 短信api的请求地址(固定)
request.setSysDomain(aliyunSmsProperties.getDomain());
// 签名算法版(固定)
request.setSysVersion("2017-05-25");
// 请求 API 的名称(固定)
request.setSysAction("SendSms");
// 指定地域名称
request.putQueryParameter("RegionId", aliyunSmsProperties.getRegionId());
// 要给哪个手机号发送短信 指定手机号
request.putQueryParameter("PhoneNumbers", mobile);
// 您的申请签名
request.putQueryParameter("SignName", aliyunSmsProperties.getSignName());
// 您申请的模板 code
request.putQueryParameter("TemplateCode", templateCode);
request.putQueryParameter("TemplateParam", templateParam);
try {
CommonResponse response = client.getCommonResponse(request);
return response.getHttpResponse().isSuccess();
} catch (ServerException e) {
e.printStackTrace();
} catch (ClientException e) {
e.printStackTrace();
}
return false;
}
}
@@ -0,0 +1,83 @@
package com.youlai.boot.common.service.impl;
import com.youlai.boot.config.property.MailProperties;
import com.youlai.system.service.MailService;
import jakarta.mail.MessagingException;
import jakarta.mail.internet.MimeMessage;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.FileSystemResource;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.stereotype.Service;
import java.io.File;
/**
* 邮件服务实现类
*
* @author Ray
* @since 2024/8/17
*/
@Service
@RequiredArgsConstructor
@Slf4j
public class MailServiceImpl implements MailService {
private final JavaMailSender mailSender;
private final MailProperties mailProperties;
/**
* 发送简单文本邮件
*
* @param to 收件人地址
* @param subject 邮件主题
* @param text 邮件内容
*/
@Override
public boolean sendSimpleMail(String to, String subject, String text) {
try {
SimpleMailMessage message = new SimpleMailMessage();
message.setFrom(mailProperties.getFrom());
message.setTo(to);
message.setSubject(subject);
message.setText(text);
mailSender.send(message);
return true;
} catch (Exception e) {
e.printStackTrace();
log.error("发送邮件失败{}", e.getMessage());
return false;
}
}
/**
* 发送带附件的邮件
*
* @param to 收件人地址
* @param subject 邮件主题
* @param text 邮件内容
* @param filePath 附件路径
*/
@Override
public boolean sendMailWithAttachment(String to, String subject, String text, String filePath) {
MimeMessage message = mailSender.createMimeMessage();
try {
MimeMessageHelper helper = new MimeMessageHelper(message, true);
helper.setFrom(mailProperties.getFrom());
helper.setTo(to);
helper.setSubject(subject);
helper.setText(text, true); // true表示支持HTML内容
FileSystemResource file = new FileSystemResource(new File(filePath));
helper.addAttachment(file.getFilename(), file);
mailSender.send(message);
return true;
} catch (MessagingException e) {
return false;
}
}
}
@@ -0,0 +1,208 @@
package com.youlai.boot.common.service.impl;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import com.youlai.boot.module.system.model.dto.FileInfo;
import com.youlai.system.service.OssService;
import io.minio.*;
import io.minio.errors.*;
import io.minio.http.Method;
import jakarta.annotation.PostConstruct;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.io.InputStream;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.time.LocalDateTime;
/**
* MinIO 文件上传服务类
*
* @author haoxr
* @since 2023/6/2
*/
@Component
@ConditionalOnProperty(value = "oss.type", havingValue = "minio")
@ConfigurationProperties(prefix = "oss.minio")
@RequiredArgsConstructor
@Data
public class MinioOssService implements OssService {
/**
* 服务Endpoint
*/
private String endpoint;
/**
* 访问凭据
*/
private String accessKey;
/**
* 凭据密钥
*/
private String secretKey;
/**
* 存储桶名称
*/
private String bucketName;
/**
* 自定义域名
*/
private String customDomain;
private MinioClient minioClient;
// 依赖注入完成之后执行初始化
@PostConstruct
public void init() {
minioClient = MinioClient.builder()
.endpoint(endpoint)
.credentials(accessKey, secretKey)
.build();
// 创建存储桶(存储桶不存在)
// createBucketIfAbsent(bucketName);
}
/**
* 上传文件
*
* @param file 表单文件对象
* @return
*/
@Override
public FileInfo uploadFile(MultipartFile file) {
// 创建存储桶(存储桶不存在),如果有搭建好的minio服务,建议放在init方法中
createBucketIfAbsent(bucketName);
// 生成文件名(日期文件夹)
String suffix = FileUtil.getSuffix(file.getOriginalFilename());
String uuid = IdUtil.simpleUUID();
String fileName = DateUtil.format(LocalDateTime.now(), "yyyyMMdd") + "/" + uuid + "." + suffix;
// try-with-resource 语法糖自动释放流
try (InputStream inputStream = file.getInputStream()) {
// 文件上传
PutObjectArgs putObjectArgs = PutObjectArgs.builder()
.bucket(bucketName)
.object(fileName)
.contentType(file.getContentType())
.stream(inputStream, inputStream.available(), -1)
.build();
minioClient.putObject(putObjectArgs);
// 返回文件路径
String fileUrl;
if (StrUtil.isBlank(customDomain)) { // 未配置自定义域名
GetPresignedObjectUrlArgs getPresignedObjectUrlArgs = GetPresignedObjectUrlArgs.builder()
.bucket(bucketName).object(fileName)
.method(Method.GET)
.build();
fileUrl = minioClient.getPresignedObjectUrl(getPresignedObjectUrlArgs);
fileUrl = fileUrl.substring(0, fileUrl.indexOf("?"));
} else { // 配置自定义文件路径域名
fileUrl = customDomain + '/' + bucketName + "/" + fileName;
}
FileInfo fileInfo = new FileInfo();
fileInfo.setName(fileName);
fileInfo.setUrl(fileUrl);
return fileInfo;
} catch (Exception e) {
throw new RuntimeException("文件上传失败");
}
}
/**
* 删除文件
*
* @param filePath 文件路径
* https://oss.youlai.tech/default/20221120/test.jpg
* @return
*/
@Override
public boolean deleteFile(String filePath) {
Assert.notBlank(filePath, "删除文件路径不能为空");
try {
String fileName;
if (StrUtil.isNotBlank(customDomain)) {
// https://oss.youlai.tech/default/20221120/test.jpg → 20221120/test.jpg
fileName = filePath.substring(customDomain.length() + 1 + bucketName.length() + 1); // 两个/占了2个字符长度
} else {
// http://localhost:9000/default/20221120/test.jpg → 20221120/test.jpg
fileName = filePath.substring(endpoint.length() + 1 + bucketName.length() + 1);
}
RemoveObjectArgs removeObjectArgs = RemoveObjectArgs.builder()
.bucket(bucketName)
.object(fileName)
.build();
minioClient.removeObject(removeObjectArgs);
return true;
} catch (ErrorResponseException | InsufficientDataException | InternalException | InvalidKeyException |
InvalidResponseException | IOException | NoSuchAlgorithmException | ServerException |
XmlParserException e) {
throw new RuntimeException(e);
}
}
/**
* PUBLIC桶策略
* 如果不配置,则新建的存储桶默认是PRIVATE,则存储桶文件会拒绝访问 Access Denied
*
* @param bucketName
* @return
*/
private static String publicBucketPolicy(String bucketName) {
/**
* AWS的S3存储桶策略
* Principal: 生效用户对象
* Resource: 指定存储桶
* Action: 操作行为
*/
return "{\"Version\":\"2012-10-17\","
+ "\"Statement\":[{\"Effect\":\"Allow\","
+ "\"Principal\":{\"AWS\":[\"*\"]},"
+ "\"Action\":[\"s3:ListBucketMultipartUploads\",\"s3:GetBucketLocation\",\"s3:ListBucket\"],"
+ "\"Resource\":[\"arn:aws:s3:::" + bucketName + "\"]},"
+ "{\"Effect\":\"Allow\"," + "\"Principal\":{\"AWS\":[\"*\"]},"
+ "\"Action\":[\"s3:ListMultipartUploadParts\",\"s3:PutObject\",\"s3:AbortMultipartUpload\",\"s3:DeleteObject\",\"s3:GetObject\"],"
+ "\"Resource\":[\"arn:aws:s3:::" + bucketName + "/*\"]}]}";
}
/**
* 创建存储桶(存储桶不存在)
*
* @param bucketName
*/
@SneakyThrows
private void createBucketIfAbsent(String bucketName) {
BucketExistsArgs bucketExistsArgs = BucketExistsArgs.builder().bucket(bucketName).build();
if (!minioClient.bucketExists(bucketExistsArgs)) {
MakeBucketArgs makeBucketArgs = MakeBucketArgs.builder().bucket(bucketName).build();
minioClient.makeBucket(makeBucketArgs);
// 设置存储桶访问权限为PUBLIC, 如果不配置,则新建的存储桶默认是PRIVATE,则存储桶文件会拒绝访问 Access Denied
SetBucketPolicyArgs setBucketPolicyArgs = SetBucketPolicyArgs
.builder()
.bucket(bucketName)
.config(publicBucketPolicy(bucketName))
.build();
minioClient.setBucketPolicy(setBucketPolicyArgs);
}
}
}
@@ -0,0 +1,61 @@
package com.youlai.boot.common.util;
import cn.hutool.core.date.DateTime;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.ReflectUtil;
import cn.hutool.core.util.StrUtil;
import org.springframework.format.annotation.DateTimeFormat;
import java.lang.reflect.Field;
/**
* 日期工具类
*
* @author haoxr
* @since 2.4.2
*/
public class DateUtils {
/**
* 区间日期格式化为数据库日期格式
* <p>
* eg:2021-01-01 → 2021-01-01 00:00:00
*
* @param obj 要处理的对象
* @param startTimeFieldName 起始时间字段名
* @param endTimeFieldName 结束时间字段名
*/
public static void toDatabaseFormat(Object obj, String startTimeFieldName, String endTimeFieldName) {
Field startTimeField = ReflectUtil.getField(obj.getClass(), startTimeFieldName);
Field endTimeField = ReflectUtil.getField(obj.getClass(), endTimeFieldName);
if (startTimeField != null) {
processDateTimeField(obj, startTimeField, startTimeFieldName, "yyyy-MM-dd 00:00:00");
}
if (endTimeField != null) {
processDateTimeField(obj, endTimeField, endTimeFieldName, "yyyy-MM-dd 23:59:59");
}
}
/**
* 处理日期字段
*
* @param obj 要处理的对象
* @param field 字段
* @param fieldName 字段名
* @param targetPattern 目标数据库日期格式
*/
private static void processDateTimeField(Object obj, Field field, String fieldName, String targetPattern) {
Object fieldValue = ReflectUtil.getFieldValue(obj, fieldName);
if (fieldValue != null) {
// 得到原始的日期格式
String pattern = field.isAnnotationPresent(DateTimeFormat.class) ? field.getAnnotation(DateTimeFormat.class).pattern() : "yyyy-MM-dd";
// 转换为日期对象
DateTime dateTime = DateUtil.parse(StrUtil.toString(fieldValue), pattern);
// 转换为目标数据库日期格式
ReflectUtil.setFieldValue(obj, fieldName, dateTime.toString(targetPattern));
}
}
}
@@ -0,0 +1,20 @@
package com.youlai.boot.common.util;
import com.alibaba.excel.EasyExcel;
import com.youlai.boot.common.base.BaseAnalysisEventListener;
import java.io.InputStream;
/**
* Excel 工具类
*
* @author haoxr
* @since 2023/03/01
*/
public class ExcelUtils {
public static <T> String importExcel(InputStream is, Class clazz, BaseAnalysisEventListener<T> listener) {
EasyExcel.read(is, clazz, listener).sheet().doRead();
return listener.getMsg();
}
}
@@ -0,0 +1,139 @@
package com.youlai.boot.common.util;
import cn.hutool.core.util.StrUtil;
import jakarta.annotation.PostConstruct;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.lionsoul.ip2region.xdb.Searcher;
import org.springframework.stereotype.Component;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
/**
* IP工具类
* <p>
* 获取客户端IP地址和IP地址对应的地理位置信息
* <p>
* 使用Nginx等反向代理软件, 则不能通过request.getRemoteAddr()获取IP地址
* 如果使用了多级反向代理的话,X-Forwarded-For的值并不止一个,而是一串IP地址,X-Forwarded-For中第一个非unknown的有效IP字符串,则为真实IP地址
* </p>
*
* @author Ray
* @since 2.10.0
*/
@Slf4j
@Component
public class IPUtils {
private static final String DB_PATH = "/data/ip2region.xdb";
private static Searcher searcher;
@PostConstruct
public void init() {
try {
// 从类路径加载资源文件
InputStream inputStream = getClass().getResourceAsStream(DB_PATH);
if (inputStream == null) {
throw new FileNotFoundException("Resource not found: " + DB_PATH);
}
// 将资源文件复制到临时文件
Path tempDbPath = Files.createTempFile("ip2region", ".xdb");
Files.copy(inputStream, tempDbPath, StandardCopyOption.REPLACE_EXISTING);
// 使用临时文件初始化 Searcher 对象
searcher = Searcher.newWithFileOnly(tempDbPath.toString());
} catch (Exception e) {
log.error("IpRegionUtil initialization ERROR, {}", e.getMessage());
}
}
/**
* 获取IP地址
*
* @param request HttpServletRequest对象
* @return 客户端IP地址
*/
public static String getIpAddr(HttpServletRequest request) {
String ip = null;
try {
if (request == null) {
return "";
}
ip = request.getHeader("x-forwarded-for");
if (checkIp(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (checkIp(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (checkIp(ip)) {
ip = request.getHeader("HTTP_CLIENT_IP");
}
if (checkIp(ip)) {
ip = request.getHeader("HTTP_X_FORWARDED_FOR");
}
if (checkIp(ip)) {
ip = request.getRemoteAddr();
if ("127.0.0.1".equals(ip) || "0:0:0:0:0:0:0:1".equals(ip)) {
// 根据网卡取本机配置的IP
ip = getLocalAddr();
}
}
} catch (Exception e) {
log.error("IPUtils ERROR, {}", e.getMessage());
}
// 使用代理,则获取第一个IP地址
if (StrUtil.isNotBlank(ip) && ip.indexOf(",") > 0) {
ip = ip.substring(0, ip.indexOf(","));
}
return ip;
}
private static boolean checkIp(String ip) {
String unknown = "unknown";
return StrUtil.isEmpty(ip) || unknown.equalsIgnoreCase(ip);
}
/**
* 获取本机的IP地址
*
* @return 本机IP地址
*/
private static String getLocalAddr() {
try {
return InetAddress.getLocalHost().getHostAddress();
} catch (UnknownHostException e) {
log.error("InetAddress.getLocalHost()-error, {}", e.getMessage());
}
return null;
}
/**
* 根据IP地址获取地理位置信息
*
* @param ip IP地址
* @return 地理位置信息
*/
public static String getRegion(String ip) {
if (searcher == null) {
log.error("Searcher is not initialized");
return null;
}
try {
return searcher.search(ip);
} catch (Exception e) {
log.error("IpRegionUtil ERROR, {}", e.getMessage());
return null;
}
}
}
@@ -0,0 +1,51 @@
package com.youlai.boot.common.util;
import cn.hutool.json.JSONUtil;
import com.youlai.boot.common.result.Result;
import com.youlai.boot.common.result.ResultCode;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
/**
* 响应工具类
*
* @author Ray Hao
* @since 2.0.0
*/
@Slf4j
public class ResponseUtils {
/**
* 异常消息返回(适用过滤器中处理异常响应)
*
* @param response HttpServletResponse
* @param resultCode 响应结果码
*/
public static void writeErrMsg(HttpServletResponse response, ResultCode resultCode) {
// 根据不同的结果码设置HTTP状态
int status = switch (resultCode) {
case ACCESS_UNAUTHORIZED, TOKEN_INVALID -> HttpStatus.UNAUTHORIZED.value();
case TOKEN_ACCESS_FORBIDDEN -> HttpStatus.FORBIDDEN.value();
default -> HttpStatus.BAD_REQUEST.value();
};
response.setStatus(status);
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
try (PrintWriter writer = response.getWriter()) {
String jsonResponse = JSONUtil.toJsonStr(Result.failed(resultCode));
writer.print(jsonResponse);
writer.flush(); // 确保将响应内容写入到输出流
} catch (IOException e) {
log.error("响应异常处理失败", e);
}
}
}
@@ -0,0 +1,55 @@
package com.youlai.boot.config;
import cn.hutool.captcha.generator.CodeGenerator;
import cn.hutool.captcha.generator.MathGenerator;
import cn.hutool.captcha.generator.RandomGenerator;
import com.youlai.boot.config.property.CaptchaProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.awt.*;
/**
* 验证码自动装配配置
*
* @author haoxr
* @since 2023/11/24
*/
@Configuration
public class CaptchaConfig {
@Autowired
private CaptchaProperties captchaProperties;
/**
* 验证码文字生成器
*
* @return CodeGenerator
*/
@Bean
public CodeGenerator codeGenerator() {
String codeType = captchaProperties.getCode().getType();
int codeLength = captchaProperties.getCode().getLength();
if ("math".equalsIgnoreCase(codeType)) {
return new MathGenerator(codeLength);
} else if ("random".equalsIgnoreCase(codeType)) {
return new RandomGenerator(codeLength);
} else {
throw new IllegalArgumentException("Invalid captcha generator type: " + codeType);
}
}
/**
* 验证码字体
*/
@Bean
public Font captchaFont() {
String fontName = captchaProperties.getFont().getName();
int fontSize = captchaProperties.getFont().getSize();
int fontWight = captchaProperties.getFont().getWeight();
return new Font(fontName, fontWight, fontSize);
}
}
@@ -0,0 +1,42 @@
package com.youlai.boot.config;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import java.util.Collections;
/**
* CORS 资源共享配置
*
* @author haoxr
* @since 2023/4/17
*/
@Configuration
public class CorsConfig {
@Bean
public FilterRegistrationBean filterRegistrationBean() {
CorsConfiguration corsConfiguration = new CorsConfiguration();
//1.允许任何来源
corsConfiguration.setAllowedOriginPatterns(Collections.singletonList("*"));
//2.允许任何请求头
corsConfiguration.addAllowedHeader(CorsConfiguration.ALL);
//3.允许任何方法
corsConfiguration.addAllowedMethod(CorsConfiguration.ALL);
//4.允许凭证
corsConfiguration.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", corsConfiguration);
CorsFilter corsFilter = new CorsFilter(source);
FilterRegistrationBean<CorsFilter> filterRegistrationBean=new FilterRegistrationBean<>(corsFilter);
filterRegistrationBean.setOrder(-101); // 小于 SpringSecurity Filter的 Order(-100) 即可
return filterRegistrationBean;
}
}
@@ -0,0 +1,51 @@
package com.youlai.boot.config;
import com.youlai.boot.config.property.MailProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.JavaMailSenderImpl;
import java.util.Properties;
/**
* MailConfig 配置类,用于手动配置和注入 JavaMailSender。
* 通过读取 MailProperties 类中配置的邮件相关属性来初始化 JavaMailSender。
* <p>
* 手动注入的原因是为了避免在使用 application-dev.yml 或其他非 application.yml 配置文件时,
* IDEA 提示无法找到 JavaMailSender 的 bean。
*
* @author Ray
* @since 2024/8/17
*/
@Configuration
@EnableConfigurationProperties(MailProperties.class)
public class MailConfig {
private final MailProperties mailProperties;
public MailConfig(MailProperties mailProperties) {
this.mailProperties = mailProperties;
}
/**
* 创建并配置 JavaMailSender bean。
*
* @return 配置好的 JavaMailSender 实例
*/
@Bean
public JavaMailSender javaMailSender() {
JavaMailSenderImpl mailSender = new JavaMailSenderImpl();
mailSender.setHost(mailProperties.getHost());
mailSender.setPort(mailProperties.getPort());
mailSender.setUsername(mailProperties.getUsername());
mailSender.setPassword(mailProperties.getPassword());
Properties properties = mailSender.getJavaMailProperties();
properties.put("mail.smtp.auth", mailProperties.getProperties().getSmtp().isAuth());
properties.put("mail.smtp.starttls.enable", mailProperties.getProperties().getSmtp().getStarttls().isEnable());
return mailSender;
}
}
@@ -0,0 +1,48 @@
package com.youlai.boot.config;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.core.config.GlobalConfig;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.DataPermissionInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import com.youlai.system.plugin.mybatis.handler.MyDataPermissionHandler;
import com.youlai.system.plugin.mybatis.handler.MyMetaObjectHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.annotation.EnableTransactionManagement;
/**
* mybatis-plus 自动配置类
*
* @author haoxr
* @since 2022/7/2
*/
@Configuration
@EnableTransactionManagement
public class MybatisConfig {
/**
* 分页插件和数据权限插件
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
//数据权限
interceptor.addInnerInterceptor(new DataPermissionInterceptor(new MyDataPermissionHandler()));
//分页插件
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
/**
* 自动填充数据库创建人、创建时间、更新人、更新时间
*/
@Bean
public GlobalConfig globalConfig() {
GlobalConfig globalConfig = new GlobalConfig();
globalConfig.setMetaObjectHandler(new MyMetaObjectHandler());
return globalConfig;
}
}
@@ -0,0 +1,74 @@
package com.youlai.boot.config;
import org.springframework.boot.autoconfigure.cache.CacheProperties;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.cache.RedisCacheWriter;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.RedisSerializer;
/**
* Redis 缓存配置
*
* @author Ray
* @since 2023/12/4
*/
@EnableCaching
@EnableConfigurationProperties(CacheProperties.class)
@Configuration
@ConditionalOnProperty(name = "spring.cache.enabled") // xxl.job.enabled = true 才会自动装配
public class RedisCacheConfig {
/**
* 自定义 RedisCacheManager
* <p>
* 修改 Redis 序列化方式,默认 JdkSerializationRedisSerializer
*
* @param redisConnectionFactory {@link RedisConnectionFactory}
* @param cacheProperties {@link CacheProperties}
* @return {@link RedisCacheManager}
*/
@Bean
public RedisCacheManager redisCacheManager(RedisConnectionFactory redisConnectionFactory, CacheProperties cacheProperties){
return RedisCacheManager.builder(RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory))
.cacheDefaults(redisCacheConfiguration(cacheProperties))
.build();
}
/**
* 自定义 RedisCacheConfiguration
*
* @param cacheProperties {@link CacheProperties}
* @return {@link RedisCacheConfiguration}
*/
@Bean
RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
config = config.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(RedisSerializer.string()));
config = config.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(RedisSerializer.json()));
CacheProperties.Redis redisProperties = cacheProperties.getRedis();
if (redisProperties.getTimeToLive() != null) {
config = config.entryTtl(redisProperties.getTimeToLive());
}
if (!redisProperties.isCacheNullValues()) {
config = config.disableCachingNullValues();
}
if (!redisProperties.isUseKeyPrefix()) {
config = config.disableKeyPrefix();
}
// 覆盖默认key双冒号 CacheKeyPrefix#prefixed
config = config.computePrefixWith(name -> name + ":");
return config;
}
}
@@ -0,0 +1,42 @@
package com.youlai.boot.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.RedisSerializer;
/**
* Redis 配置
*
* @author Ray
* @since 2023/5/15
*/
@Configuration
public class RedisConfig {
/**
* 自定义 RedisTemplate
* <p>
* 修改 Redis 序列化方式,默认 JdkSerializationRedisSerializer
*
* @param redisConnectionFactory {@link RedisConnectionFactory}
* @return {@link RedisTemplate}
*/
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
redisTemplate.setKeySerializer(RedisSerializer.string());
redisTemplate.setValueSerializer(RedisSerializer.json());
redisTemplate.setHashKeySerializer(RedisSerializer.string());
redisTemplate.setHashValueSerializer(RedisSerializer.json());
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}
@@ -0,0 +1,110 @@
package com.youlai.boot.config;
import cn.hutool.captcha.generator.CodeGenerator;
import cn.hutool.core.collection.CollectionUtil;
import com.youlai.boot.common.constant.SecurityConstants;
import com.youlai.boot.config.property.SecurityProperties;
import com.youlai.boot.framework.filter.RateLimiterFilter;
import com.youlai.boot.framework.security.exception.MyAccessDeniedHandler;
import com.youlai.boot.framework.security.exception.MyAuthenticationEntryPoint;
import com.youlai.boot.framework.security.filter.JwtValidationFilter;
import com.youlai.boot.framework.security.filter.CaptchaValidationFilter;
import com.youlai.boot.module.system.service.SysConfigService;
import lombok.RequiredArgsConstructor;
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.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
/**
* Spring Security 权限配置
*
* @author Ray
* @since 2023/2/17
*/
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final MyAuthenticationEntryPoint authenticationEntryPoint;
private final MyAccessDeniedHandler accessDeniedHandler;
private final RedisTemplate<String, Object> redisTemplate;
private final CodeGenerator codeGenerator;
private final SecurityProperties securityProperties;
private final SysConfigService sysConfigService;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(requestMatcherRegistry ->
requestMatcherRegistry.requestMatchers(SecurityConstants.LOGIN_PATH).permitAll()
.anyRequest().authenticated()
)
.exceptionHandling(httpSecurityExceptionHandlingConfigurer ->
httpSecurityExceptionHandlingConfigurer
.authenticationEntryPoint(authenticationEntryPoint)
.accessDeniedHandler(accessDeniedHandler)
)
.sessionManagement(configurer -> configurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.csrf(AbstractHttpConfigurer::disable)
.headers(headers -> headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable))
;
// 限流过滤器
http.addFilterBefore(new RateLimiterFilter(redisTemplate, sysConfigService), UsernamePasswordAuthenticationFilter.class);
// 验证码校验过滤器
http.addFilterBefore(new CaptchaValidationFilter(redisTemplate, codeGenerator), UsernamePasswordAuthenticationFilter.class);
// JWT 校验过滤器
http.addFilterBefore(new JwtValidationFilter(redisTemplate,securityProperties.getJwt().getKey()), UsernamePasswordAuthenticationFilter.class);
return http.build();
}
/**
* 不走过滤器链的放行配置
*/
@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
return (web) -> {
if (CollectionUtil.isNotEmpty(securityProperties.getIgnoreUrls())) {
web.ignoring().requestMatchers(securityProperties.getIgnoreUrls().toArray(new String[0]));
}
};
}
/**
* 密码编码器
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* AuthenticationManager 手动注入
*
* @param authenticationConfiguration 认证配置
*/
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
}
@@ -0,0 +1,84 @@
package com.youlai.boot.config;
import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.security.SecurityRequirement;
import io.swagger.v3.oas.models.security.SecurityScheme;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springdoc.core.customizers.GlobalOpenApiCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.http.HttpHeaders;
/**
* Swagger 配置
* <p>
*
* @author Ray
* @see <a href="https://doc.xiaominfo.com/docs/quick-start">knife4j 快速开始</a>
* @since 2023/2/17
*/
@Configuration
@Slf4j
@RequiredArgsConstructor
public class SwaggerConfig {
private final Environment environment;
/**
* 接口信息
*/
@Bean
public OpenAPI openApi() {
String appVersion = environment.getProperty("project.version", "1.0.0");
return new OpenAPI()
.info(new Info()
.title("系统接口文档")
.version(appVersion)
)
// 配置全局鉴权参数-Authorize
.components(new Components()
.addSecuritySchemes(HttpHeaders.AUTHORIZATION,
new SecurityScheme()
.name(HttpHeaders.AUTHORIZATION)
.type(SecurityScheme.Type.APIKEY)
.in(SecurityScheme.In.HEADER)
.scheme("Bearer")
.bearerFormat("JWT")
)
);
}
/**
* 全局自定义扩展
* <p>
* 在OpenAPI规范中,Operation 是一个表示 API 端点(Endpoint)或操作的对象。
* 每个路径(Path)对象可以包含一个或多个 Operation 对象,用于描述与该路径相关联的不同 HTTP 方法(例如 GET、POST、PUT 等)。
*/
@Bean
public GlobalOpenApiCustomizer globalOpenApiCustomizer() {
return openApi -> {
// 全局添加鉴权参数
if (openApi.getPaths() != null) {
openApi.getPaths().forEach((s, pathItem) -> {
// 登录接口/验证码不需要添加鉴权参数
if (s.equals("/api/v1/auth/login") || s.equals("/api/v1/auth/captcha")) {
return;
}
// 接口添加鉴权参数
pathItem.readOperations()
.forEach(operation ->
operation.addSecurityItem(new SecurityRequirement().addList(HttpHeaders.AUTHORIZATION))
);
});
}
};
}
}
@@ -0,0 +1,69 @@
package com.youlai.boot.config;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import jakarta.validation.Validation;
import jakarta.validation.Validator;
import jakarta.validation.ValidatorFactory;
import lombok.extern.slf4j.Slf4j;
import org.hibernate.validator.HibernateValidator;
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.validation.beanvalidation.SpringConstraintValidatorFactory;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.math.BigInteger;
import java.text.SimpleDateFormat;
import java.util.List;
import java.util.TimeZone;
/**
* WebMvc 自动装配配置
*
* @author Ray
* @since 2020/10/16
*/
@Configuration
@Slf4j
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(new StringHttpMessageConverter());
MappingJackson2HttpMessageConverter jackson2HttpMessageConverter = new MappingJackson2HttpMessageConverter();
ObjectMapper objectMapper = jackson2HttpMessageConverter.getObjectMapper();
objectMapper.registerModule(new JavaTimeModule());
objectMapper.configure(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES, true);
objectMapper.configure(SerializationFeature.WRITE_ENUMS_USING_TO_STRING, true);
// 后台Long值传递给前端精度丢失问题(JS最大精度整数是Math.pow(2,53))
SimpleModule simpleModule = new SimpleModule();
simpleModule.addSerializer(Long.class, ToStringSerializer.instance);
simpleModule.addSerializer(BigInteger.class, ToStringSerializer.instance);
objectMapper.registerModule(simpleModule);
jackson2HttpMessageConverter.setObjectMapper(objectMapper);
converters.add(jackson2HttpMessageConverter);
}
@Bean
public Validator validator(final AutowireCapableBeanFactory autowireCapableBeanFactory) {
ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class)
.configure()
.failFast(true) // failFast=true 不校验所有参数,只要出现校验失败情况直接返回,不再进行后续参数校验
.constraintValidatorFactory(new SpringConstraintValidatorFactory(autowireCapableBeanFactory))
.buildValidatorFactory();
return validatorFactory.getValidator();
}
}
@@ -0,0 +1,108 @@
package com.youlai.boot.config;
import cn.hutool.core.util.StrUtil;
import cn.hutool.jwt.JWTPayload;
import cn.hutool.jwt.JWTUtil;
import com.youlai.system.common.constant.SecurityConstants;
import com.youlai.boot.module.system.event.UserConnectionEvent;
import com.youlai.system.service.WebsocketService;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpHeaders;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.simp.config.ChannelRegistration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.messaging.simp.stomp.StompCommand;
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
import org.springframework.messaging.support.ChannelInterceptor;
import org.springframework.messaging.support.MessageHeaderAccessor;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
/**
* WebSocket 自动配置类
*
* @author haoxr
* @since 2.4.0
*/
// 启用WebSocket消息代理功能和配置STOMP协议,实现实时双向通信和消息传递
@EnableWebSocketMessageBroker
@Configuration
@Slf4j
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
private final ApplicationEventPublisher eventPublisher;
public WebSocketConfig(ApplicationEventPublisher eventPublisher) {
this.eventPublisher = eventPublisher;
}
/**
* 注册一个端点,客户端通过这个端点进行连接
*/
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry
// 注册 /ws 的端点
.addEndpoint("/ws")
// 允许跨域
.setAllowedOriginPatterns("*");
}
/**
* 配置消息代理
*/
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
// 客户端发送消息的请求前缀
registry.setApplicationDestinationPrefixes("/app");
// 客户端订阅消息的请求前缀,topic一般用于广播推送,queue用于点对点推送
registry.enableSimpleBroker("/topic", "/queue");
// 服务端通知客户端的前缀,可以不设置,默认为user
registry.setUserDestinationPrefix("/user");
}
/**
* 配置客户端入站通道拦截器
* <p>
* 添加 ChannelInterceptor 拦截器,用于在消息发送前,从请求头中获取 token 并解析出用户信息(username),用于点对点发送消息给指定用户
*
* @param registration 通道注册器
*/
@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.interceptors(new ChannelInterceptor() {
@Override
public Message<?> preSend(@NotNull Message<?> message, @NotNull MessageChannel channel) {
StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
if (accessor != null) {
if (StompCommand.CONNECT.equals(accessor.getCommand())) {
String bearerToken = accessor.getFirstNativeHeader(HttpHeaders.AUTHORIZATION);
if (StrUtil.isNotBlank(bearerToken) && bearerToken.startsWith("Bearer ")) {
bearerToken = bearerToken.substring(SecurityConstants.JWT_TOKEN_PREFIX.length());
String username = JWTUtil.parseToken(bearerToken).getPayloads().getStr(JWTPayload.SUBJECT);
if (StrUtil.isNotBlank(username)) {
accessor.setUser(() -> username);
eventPublisher.publishEvent(new UserConnectionEvent(this, username, true));
}
}
} else if (StompCommand.DISCONNECT.equals(accessor.getCommand())) {
if (accessor.getUser() != null) {
String username = accessor.getUser().getName();
eventPublisher.publishEvent(new UserConnectionEvent(this, username, false));
}
}
}
return ChannelInterceptor.super.preSend(message, channel);
}
});
}
}
@@ -0,0 +1,61 @@
package com.youlai.boot.config;
import com.xxl.job.core.executor.impl.XxlJobSpringExecutor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* xxl-job config
*
* @author xuxueli 2017-04-28
*/
@Configuration
@ConditionalOnProperty(name = "xxl.job.enabled") // xxl.job.enabled = true 才会自动装配
@Slf4j
public class XxlJobConfig {
@Value("${xxl.job.admin.addresses}")
private String adminAddresses;
@Value("${xxl.job.accessToken}")
private String accessToken;
@Value("${xxl.job.executor.appname}")
private String appname;
@Value("${xxl.job.executor.address}")
private String address;
@Value("${xxl.job.executor.ip}")
private String ip;
@Value("${xxl.job.executor.port}")
private int port;
@Value("${xxl.job.executor.logpath}")
private String logPath;
@Value("${xxl.job.executor.logretentiondays}")
private int logRetentionDays;
@Bean
public XxlJobSpringExecutor xxlJobExecutor() {
log.info(">>>>>>>>>>> xxl-job config init.");
XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor();
xxlJobSpringExecutor.setAdminAddresses(adminAddresses);
xxlJobSpringExecutor.setAppname(appname);
xxlJobSpringExecutor.setAddress(address);
xxlJobSpringExecutor.setIp(ip);
xxlJobSpringExecutor.setPort(port);
xxlJobSpringExecutor.setAccessToken(accessToken);
xxlJobSpringExecutor.setLogPath(logPath);
xxlJobSpringExecutor.setLogRetentionDays(logRetentionDays);
return xxlJobSpringExecutor;
}
}
@@ -0,0 +1,50 @@
package com.youlai.boot.config.property;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import java.util.Map;
/**
* 阿里云短信配置
*
* @author Ray
* @since 2024/8/17
*/
@Configuration
@ConfigurationProperties(prefix = "sms.aliyun")
@Data
public class AliyunSmsProperties {
/**
* 阿里云账户的Access Key ID,用于API请求认证
*/
private String accessKeyId;
/**
*阿里云账户的Access Key Secret,用于API请求认证
*/
private String accessKeySecret;
/**
* 阿里云短信服务API的域名 eg: dysmsapi.aliyuncs.com
*/
private String domain;
/**
* 阿里云服务的区域ID,如cn-shanghai
*/
private String regionId;
/**
* 短信签名,必须是已经在阿里云短信服务中注册并通过审核的
*/
private String signName;
/**
* 模板编码
*/
private Map<String, String> templateCodes;
}
@@ -0,0 +1,92 @@
package com.youlai.boot.config.property;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* 验证码 属性配置
*
* @author haoxr
* @since 2023/11/24
*/
@Component
@ConfigurationProperties(prefix = "captcha")
@Data
public class CaptchaProperties {
/**
* 验证码类型 circle-圆圈干扰验证码|gif-Gif验证码|line-干扰线验证码|shear-扭曲干扰验证码
*/
private String type;
/**
* 验证码图片宽度
*/
private int width;
/**
* 验证码图片高度
*/
private int height;
/**
* 干扰线数量
*/
private int interfereCount;
/**
* 文本透明度
*/
private Float textAlpha;
/**
* 验证码过期时间,单位:秒
*/
private Long expireSeconds;
/**
* 验证码字符配置
*/
private CodeProperties code;
/**
* 验证码字体
*/
private FontProperties font;
/**
* 验证码字符配置
*/
@Data
public static class CodeProperties {
/**
* 验证码字符类型 math-算术|random-随机字符串
*/
private String type;
/**
* 验证码字符长度,type=算术时,表示运算位数(1:个位数 2:十位数);type=随机字符时,表示字符个数
*/
private int length;
}
/**
* 验证码字体配置
*/
@Data
public static class FontProperties {
/**
* 字体名称
*/
private String name;
/**
* 字体样式 0-普通|1-粗体|2-斜体
*/
private int weight;
/**
* 字体大小
*/
private int size;
}
}
@@ -0,0 +1,82 @@
package com.youlai.boot.config.property;
import cn.hutool.core.io.file.FileNameUtil;
import cn.hutool.core.map.MapUtil;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Map;
/**
* 代码生成配置属性
*
* @author Ray
* @since 2.11.0
*/
@Component
@ConfigurationProperties(prefix = "generator")
@Data
public class GeneratorProperties {
/**
* 默认配置
*/
private DefaultConfig defaultConfig ;
/**
* 模板配置
*/
private Map<String, TemplateConfig> templateConfigs = MapUtil.newHashMap(true);
/**
* 后端应用名
*/
private String backendAppName;
/**
* 前端应用名
*/
private String frontendAppName;
/**
* 下载文件名
*/
private String downloadFileName;
/**
* 排除数据表
*/
private List<String> excludeTables;
/**
* 模板配置
*/
@Data
public static class TemplateConfig {
private String templatePath;
private String subpackageName;
/**
* 文件扩展名,如 .java
*/
private String extension = FileNameUtil.EXT_JAVA;
}
/**
* 默认配置
*/
@Data
public static class DefaultConfig {
private String author;
}
}
@@ -0,0 +1,89 @@
package com.youlai.boot.config.property;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* 邮件配置类,用于接收和存储邮件相关的配置属性。
*
* @author Ray
* @since 2024/8/17
*/
@ConfigurationProperties(prefix = "spring.mail")
@Data
public class MailProperties {
/**
* 邮件服务器主机名或 IP 地址。
* 例如:smtp.example.com
*/
private String host;
/**
* 邮件服务器端口号。
* 例如:587
*/
private int port;
/**
* 用于连接邮件服务器的用户名。
* 例如:your_email@example.com
*/
private String username;
/**
* 用于连接邮件服务器的密码。
* 该密码应安全存储,不应在代码中硬编码。
*/
private String password;
/**
* 邮件发送者地址。
*/
private String from;
/**
* 邮件服务器的其他属性配置。
* 这些配置通常用于进一步定制邮件发送行为。
*/
private Properties properties = new Properties();
/**
* 内部类,用于封装邮件服务器的详细配置。
* 包含 SMTP 相关的配置选项。
*/
@Data
public static class Properties {
/**
* SMTP 配置选项类。
* 包含认证、加密等与 SMTP 协议相关的配置。
*/
private Smtp smtp = new Smtp();
@Data
public static class Smtp {
/**
* 是否启用 SMTP 认证。
* 如果为 `true`,则需要提供有效的用户名和密码进行认证。
*/
private boolean auth;
/**
* STARTTLS 加密配置选项。
*/
private StartTls starttls = new StartTls();
@Data
public static class StartTls {
/**
* 是否启用 STARTTLS 加密。
* 如果为 `true`,在发送邮件时将启用 STARTTLS 协议进行加密传输。
*/
private boolean enable;
}
}
}
}
@@ -0,0 +1,44 @@
package com.youlai.boot.config.property;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import java.util.List;
/**
* @author haoxr
* @since 2024/4/18
*/
@Data
@ConfigurationProperties(prefix = "security")
public class SecurityProperties {
/**
* 白名单 URL 集合
*/
private List<String> ignoreUrls;
/**
* JWT 配置
*/
private JwtProperty jwt;
/**
* JWT 配置
*/
@Data
public static class JwtProperty {
/**
* JWT 密钥
*/
private String key;
/**
* JWT 过期时间
*/
private Long ttl;
}
}
@@ -0,0 +1,96 @@
package com.youlai.boot.framework.aspect;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.date.TimeInterval;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.useragent.UserAgent;
import cn.hutool.http.useragent.UserAgentUtil;
import com.youlai.boot.common.constant.SecurityConstants;
import com.youlai.boot.common.util.IPUtils;
import com.youlai.boot.module.system.model.entity.SysLog;
import com.youlai.boot.common.annotation.LogAnnotation;
import com.youlai.boot.framework.security.util.SecurityUtils;
import com.youlai.boot.module.system.service.SysLogService;
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.springframework.stereotype.Component;
/**
* 日志切面
*
* @author Ray
* @since 2024/6/25
*/
@Aspect
@Component
@RequiredArgsConstructor
@Slf4j
public class LogAspect {
private final SysLogService logService;
private final HttpServletRequest request;
@Pointcut("@annotation(com.youlai.boot.common.annotation.LogAnnotation)")
public void logPointcut() {
}
@Around("logPointcut() && @annotation(logAnnotation)")
public Object logExecutionTime(ProceedingJoinPoint joinPoint, LogAnnotation logAnnotation) throws Throwable {
String requestURI = request.getRequestURI();
Long userId = null;
// 非登录请求获取用户ID,登录请求在登录成功后(joinPoint.proceed())获取用户ID
if (!SecurityConstants.LOGIN_PATH.equals(requestURI)) {
userId = SecurityUtils.getUserId();
}
TimeInterval timer = DateUtil.timer();
// 执行方法
Object proceed = joinPoint.proceed();
long executionTime = timer.interval();
// 创建日志记录
SysLog log = new SysLog();
log.setModule(logAnnotation.module());
log.setContent(logAnnotation.value());
log.setRequestUri(requestURI);
// 登录方法需要在登录成功后获取用户ID
if (userId == null) {
userId = SecurityUtils.getUserId();
}
log.setCreateBy(userId);
String ipAddr = IPUtils.getIpAddr(request);
if (StrUtil.isNotBlank(ipAddr)) {
log.setIp(ipAddr);
String region = IPUtils.getRegion(ipAddr);
// 中国|0|四川省|成都市|电信 解析省和市
if (StrUtil.isNotBlank(region)) {
String[] regionArray = region.split("\\|");
if (regionArray.length > 2) {
log.setProvince(regionArray[2]);
log.setCity(regionArray[3]);
}
}
}
log.setExecutionTime(executionTime);
// 获取浏览器和终端系统信息
String userAgentString = request.getHeader("User-Agent");
UserAgent userAgent = UserAgentUtil.parse(userAgentString);
// 系统信息
log.setOs(userAgent.getOs().getName());
// 浏览器信息
log.setBrowser(userAgent.getBrowser().getName());
log.setBrowserVersion(userAgent.getBrowser().getVersion(userAgentString));
// 保存日志到数据库
logService.save(log);
return proceed;
}
}
@@ -0,0 +1,82 @@
package com.youlai.boot.framework.aspect;
import cn.hutool.core.util.StrUtil;
import cn.hutool.jwt.JWTUtil;
import cn.hutool.jwt.RegisteredPayload;
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.NoRepeat;
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 haoxr
* @since 2.3.0
*/
@Aspect
@Component
@Slf4j
@RequiredArgsConstructor
public class NoRepeatAspect {
private final RedissonClient redissonClient;
/**
* 防重复提交切点
*/
@Pointcut("@annotation(noRepeat)")
public void preventDuplicateSubmitPointCut(NoRepeat noRepeat) {
log.info("定义防重复提交切点");
}
@Around("preventDuplicateSubmitPointCut(noRepeat)")
public Object doAround(ProceedingJoinPoint pjp, NoRepeat noRepeat) throws Throwable {
String resubmitLockKey = generateResubmitLockKey();
if (resubmitLockKey != null) {
int expire = noRepeat.expire(); // 防重提交锁过期时间
RLock lock = redissonClient.getLock(resubmitLockKey);
boolean lockResult = lock.tryLock(0, expire, TimeUnit.SECONDS); // 获取锁失败,直接返回 false
if (!lockResult) {
throw new BusinessException(ResultCode.REPEAT_SUBMIT_ERROR); // 抛出重复提交提示信息
}
}
return pjp.proceed();
}
/**
* 获取重复提交锁的 key
*/
private String generateResubmitLockKey() {
String resubmitLockKey = null;
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
String token = request.getHeader(HttpHeaders.AUTHORIZATION);
if (StrUtil.isNotBlank(token) && token.startsWith(SecurityConstants.JWT_TOKEN_PREFIX)) {
token = token.substring(SecurityConstants.JWT_TOKEN_PREFIX.length());
// 从 JWT Token 中获取 jti
String jti = (String) JWTUtil.parseToken(token).getPayload(RegisteredPayload.JWT_ID);
resubmitLockKey = RedisConstants.RESUBMIT_LOCK_PREFIX + jti + ":" + request.getMethod() + "-" + request.getRequestURI();
}
return resubmitLockKey;
}
}
@@ -0,0 +1,80 @@
package com.youlai.boot.framework.filter;
import com.youlai.boot.common.constant.RedisConstants;
import com.youlai.boot.common.result.ResultCode;
import com.youlai.boot.common.util.IPUtils;
import com.youlai.boot.common.util.ResponseUtils;
import com.youlai.boot.module.system.service.SysConfigService;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
/**
* IP限流过滤器
*
* @author Theo
* @since 2024/08/10 14:38
*/
@Slf4j
public class RateLimiterFilter extends OncePerRequestFilter {
private final RedisTemplate<String, Object> redisTemplate;
private final SysConfigService sysConfigService;
public RateLimiterFilter(RedisTemplate<String, Object> redisTemplate, SysConfigService sysConfigService) {
this.redisTemplate = redisTemplate;
this.sysConfigService = sysConfigService;
}
/**
* 确认是否限流方法
* 默认情况下:限制同一个IP的QPS最大为10,可以通过修改系统配置进行调整
* 这里也可以进行扩展,比如redis记录同一个ip每天出发限流的上限次数,记录在redis中,达到某个阈值后,进行永久封禁这个ip
*
* @param ip ip地址
* @return 是否限流
*/
public boolean rateLimit(String ip) {
String key = RedisConstants.IP_RATE_LIMITER_KEY + ip;
Long count = redisTemplate.opsForValue().increment(key);
if (count == null || count == 1) {
redisTemplate.expire(key,1, TimeUnit.SECONDS);
}
Object systemConfig = sysConfigService.getSystemConfig(RedisConstants.IP_QPS_THRESHOLD_LIMIT_KEY);
long limit = 10;
if(systemConfig != null){
limit = Long.parseLong(systemConfig.toString());
}else{
log.warn("[RedisRateLimiterFilter.rateLimit]系统配置中未配置IP请求限制QPS阈值配置,使用默认值:{},请检查配置项:{}",
limit,RedisConstants.IP_QPS_THRESHOLD_LIMIT_KEY);
}
return count != null && count > limit;
}
/**
* IP限流过滤器
* 默认情况下:限制同一个IP在一分钟内只能访问10次,可以通过修改系统配置进行调整
*
* @param request 请求体
* @param response 响应体
* @param filterChain 过滤器链
*/
@Override
protected void doFilterInternal(@NotNull HttpServletRequest request, @NotNull HttpServletResponse response,
@NotNull FilterChain filterChain) throws ServletException, IOException {
String ip = IPUtils.getIpAddr(request);
if (rateLimit(ip)) {
ResponseUtils.writeErrMsg(response, ResultCode.FLOW_LIMITING);
return;
}
filterChain.doFilter(request, response);
}
}
@@ -0,0 +1,36 @@
package com.youlai.boot.framework.filter;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.filter.CommonsRequestLoggingFilter;
/**
* 请求日志打印过滤器
*
* @author haoxr
* @since 2023/03/03
*/
@Configuration
@Slf4j
public class RequestLogFilter extends CommonsRequestLoggingFilter {
@Override
protected boolean shouldLog(HttpServletRequest request) {
// 设置日志输出级别,默认debug
return this.logger.isInfoEnabled();
}
@Override
protected void beforeRequest(HttpServletRequest request, String message) {
String requestURI = request.getRequestURI();
log.info("request uri: {}", requestURI);
super.beforeRequest(request, message);
}
@Override
protected void afterRequest(HttpServletRequest request, String message) {
super.afterRequest(request, message);
}
}
@@ -0,0 +1,25 @@
package com.youlai.boot.framework.security.exception;
import com.youlai.system.common.result.ResultCode;
import com.youlai.boot.common.util.ResponseUtils;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* Spring Security访问异常处理器
*
* @author haoxr
* @since 2022/10/18
*/
@Component
public class MyAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException {
ResponseUtils.writeErrMsg(response, ResultCode.ACCESS_UNAUTHORIZED);
}
}
@@ -0,0 +1,40 @@
package com.youlai.boot.framework.security.exception;
import com.youlai.system.common.result.ResultCode;
import com.youlai.boot.common.util.ResponseUtils;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 认证异常处理
*
* @author haoxr
* @since 2.0.0
*/
@Component
public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
int status = response.getStatus();
if (status == HttpServletResponse.SC_NOT_FOUND) {
// 资源不存在
ResponseUtils.writeErrMsg(response, ResultCode.RESOURCE_NOT_FOUND);
} else {
if(authException instanceof BadCredentialsException){
// 用户名或密码错误
ResponseUtils.writeErrMsg(response, ResultCode.USERNAME_OR_PASSWORD_ERROR);
}else {
// 未认证或者token过期
ResponseUtils.writeErrMsg(response, ResultCode.TOKEN_INVALID);
}
}
}
}
@@ -0,0 +1,72 @@
package com.youlai.boot.framework.security.filter;
import cn.hutool.captcha.generator.CodeGenerator;
import cn.hutool.core.util.StrUtil;
import com.youlai.boot.common.constant.SecurityConstants;
import com.youlai.boot.common.result.ResultCode;
import com.youlai.boot.common.util.ResponseUtils;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
/**
* 图形验证码校验过滤器
*
* @author haoxr
* @since 2022/10/1
*/
public class CaptchaValidationFilter extends OncePerRequestFilter {
private static final AntPathRequestMatcher LOGIN_PATH_REQUEST_MATCHER = new AntPathRequestMatcher(SecurityConstants.LOGIN_PATH, "POST");
public static final String CAPTCHA_CODE_PARAM_NAME = "captchaCode";
public static final String CAPTCHA_KEY_PARAM_NAME = "captchaKey";
private final RedisTemplate<String, Object> redisTemplate;
private final CodeGenerator codeGenerator;
public CaptchaValidationFilter(RedisTemplate<String, Object> redisTemplate, CodeGenerator codeGenerator) {
this.redisTemplate = redisTemplate;
this.codeGenerator = codeGenerator;
}
@Override
public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
// 检验登录接口的验证码
if (LOGIN_PATH_REQUEST_MATCHER.matches(request)) {
// 请求中的验证码
String captchaCode = request.getParameter(CAPTCHA_CODE_PARAM_NAME);
// TODO 兼容没有验证码的版本(线上请移除这个判断)
if (StrUtil.isBlank(captchaCode)) {
chain.doFilter(request, response);
return;
}
// 缓存中的验证码
String verifyCodeKey = request.getParameter(CAPTCHA_KEY_PARAM_NAME);
String cacheVerifyCode = (String) redisTemplate.opsForValue().get(SecurityConstants.CAPTCHA_CODE_PREFIX + verifyCodeKey);
if (cacheVerifyCode == null) {
ResponseUtils.writeErrMsg(response, ResultCode.VERIFY_CODE_TIMEOUT);
} else {
// 验证码比对
if (codeGenerator.verify(cacheVerifyCode, captchaCode)) {
chain.doFilter(request, response);
} else {
ResponseUtils.writeErrMsg(response, ResultCode.VERIFY_CODE_ERROR);
}
}
} else {
// 非登录接口放行
chain.doFilter(request, response);
}
}
}
@@ -0,0 +1,83 @@
package com.youlai.boot.framework.security.filter;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.jwt.JWT;
import cn.hutool.jwt.JWTPayload;
import cn.hutool.jwt.JWTUtil;
import com.youlai.boot.common.constant.SecurityConstants;
import com.youlai.boot.common.result.ResultCode;
import com.youlai.boot.framework.security.util.JwtUtils;
import com.youlai.boot.common.util.ResponseUtils;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.HttpHeaders;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
/**
* JWT token 校验过滤器
*
* @author Ray Hao
* @since 2023/9/13
*/
public class JwtValidationFilter extends OncePerRequestFilter {
private final RedisTemplate<String, Object> redisTemplate;
private final byte[] secretKey;
public JwtValidationFilter(RedisTemplate<String, Object> redisTemplate, String secretKey) {
this.redisTemplate = redisTemplate;
this.secretKey = secretKey.getBytes();
}
/**
* 从请求中获取 JWT Token,校验 JWT Token 是否合法
* <p>
* 如果合法则将 Authentication 设置到 Spring Security Context 上下文中
* 如果不合法则清空 Spring Security Context 上下文,并直接返回响应
*/
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String token = request.getHeader(HttpHeaders.AUTHORIZATION);
try {
if (StrUtil.isNotBlank(token) && token.startsWith(SecurityConstants.JWT_TOKEN_PREFIX)) {
// 去除 Bearer 前缀
token = token.substring(SecurityConstants.JWT_TOKEN_PREFIX.length());
// 解析 Token
JWT jwt = JWTUtil.parseToken(token);
// 检查 Token 是否有效(验签 + 是否过期)
boolean isValidate = jwt.setKey(secretKey).validate(0);
if (!isValidate) {
ResponseUtils.writeErrMsg(response, ResultCode.TOKEN_INVALID);
return;
}
// 检查 Token 是否已被加入黑名单(注销)
JSONObject payloads = jwt.getPayloads();
String jti = payloads.getStr(JWTPayload.JWT_ID);
boolean isTokenBlacklisted = Boolean.TRUE.equals(redisTemplate.hasKey(SecurityConstants.BLACKLIST_TOKEN_PREFIX + jti));
if (isTokenBlacklisted) {
ResponseUtils.writeErrMsg(response, ResultCode.TOKEN_INVALID);
return;
}
// Token 有效将其解析为 Authentication 对象,并设置到 Spring Security 上下文中
Authentication authentication = JwtUtils.getAuthentication(payloads);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
} catch (Exception e) {
SecurityContextHolder.clearContext();
ResponseUtils.writeErrMsg(response, ResultCode.TOKEN_INVALID);
return;
}
// Token有效或无Token时继续执行过滤链
filterChain.doFilter(request, response);
}
}
@@ -0,0 +1,100 @@
package com.youlai.boot.framework.security.model;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.ObjectUtil;
import com.youlai.boot.module.system.model.dto.UserAuthInfo;
import lombok.Data;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
import java.util.Collections;
import java.util.Set;
import java.util.stream.Collectors;
/**
* Spring Security 用户对象
*
* @author haoxr
* @since 3.0.0
*/
@Data
@NoArgsConstructor
public class SysUserDetails implements UserDetails {
@Getter
private Long userId;
private String username;
private String password;
private Boolean enabled;
private Collection<SimpleGrantedAuthority> authorities;
private Set<String> perms;
private Long deptId;
private Integer dataScope;
public SysUserDetails(UserAuthInfo user) {
this.userId = user.getUserId();
Set<String> roles = user.getRoles();
Set<SimpleGrantedAuthority> authorities;
if (CollectionUtil.isNotEmpty(roles)) {
authorities = roles.stream()
.map(role -> new SimpleGrantedAuthority("ROLE_" + role)) // 标识角色
.collect(Collectors.toSet());
} else {
authorities = Collections.EMPTY_SET;
}
this.authorities = authorities;
this.username = user.getUsername();
this.password = user.getPassword();
this.enabled = ObjectUtil.equal(user.getStatus(), 1);
this.perms = user.getPerms();
this.deptId = user.getDeptId();
this.dataScope = user.getDataScope();
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return this.authorities;
}
@Override
public String getPassword() {
return this.password;
}
@Override
public String getUsername() {
return this.username;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return this.enabled;
}
}
@@ -0,0 +1,97 @@
package com.youlai.boot.framework.security.service;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.StrUtil;
import com.youlai.system.common.constant.SecurityConstants;
import com.youlai.boot.framework.security.util.SecurityUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.PatternMatchUtils;
import java.util.*;
/**
* SpringSecurity 权限校验
*
* @author haoxr
* @since 2022/2/22
*/
@Component("ss")
@RequiredArgsConstructor
@Slf4j
public class PermissionService {
private final RedisTemplate<String, Object> redisTemplate;
/**
* 判断当前登录用户是否拥有操作权限
*
* @param requiredPerm 所需权限
* @return 是否有权限
*/
public boolean hasPerm(String requiredPerm) {
if (StrUtil.isBlank(requiredPerm)) {
return false;
}
// 超级管理员放行
if (SecurityUtils.isRoot()) {
return true;
}
// 获取当前登录用户的角色编码集合
Set<String> roleCodes = SecurityUtils.getRoles();
if (CollectionUtil.isEmpty(roleCodes)) {
return false;
}
// 获取当前登录用户的所有角色的权限列表
Set<String> rolePerms = this.getRolePermsFormCache(roleCodes);
if (CollectionUtil.isEmpty(rolePerms)) {
return false;
}
// 判断当前登录用户的所有角色的权限列表中是否包含所需权限
boolean hasPermission = rolePerms.stream()
.anyMatch(rolePerm ->
// 匹配权限,支持通配符(* 等)
PatternMatchUtils.simpleMatch(rolePerm, requiredPerm)
);
if (!hasPermission) {
log.error("用户无操作权限");
}
return hasPermission;
}
/**
* 从缓存中获取角色权限列表
*
* @param roleCodes 角色编码集合
* @return 角色权限列表
*/
public Set<String> getRolePermsFormCache(Set<String> roleCodes) {
// 检查输入是否为空
if (CollectionUtil.isEmpty(roleCodes)) {
return Collections.emptySet();
}
Set<String> perms = new HashSet<>();
// 从缓存中一次性获取所有角色的权限
Collection<Object> roleCodesAsObjects = new ArrayList<>(roleCodes);
List<Object> rolePermsList = redisTemplate.opsForHash().multiGet(SecurityConstants.ROLE_PERMS_PREFIX, roleCodesAsObjects);
for (Object rolePermsObj : rolePermsList) {
if (rolePermsObj instanceof Set) {
@SuppressWarnings("unchecked")
Set<String> rolePerms = (Set<String>) rolePermsObj;
perms.addAll(rolePerms);
}
}
return perms;
}
}
@@ -0,0 +1,49 @@
package com.youlai.boot.framework.security.service;
import com.youlai.boot.framework.security.model.SysUserDetails;
import com.youlai.boot.module.system.model.dto.UserAuthInfo;
import com.youlai.boot.module.system.service.SysUserService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
/**
* 系统用户认证 DetailsService
*
* @author Ray
* @since 2021/10/19
*/
@Service
@RequiredArgsConstructor
@Slf4j
public class SysUserDetailsService implements UserDetailsService {
private final SysUserService sysUserService;
/**
* 根据用户名获取用户信息
*
* @param username 用户名
* @return 用户信息
* @throws UsernameNotFoundException 用户名未找到异常
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
try {
UserAuthInfo userAuthInfo = sysUserService.getUserAuthInfo(username);
if (userAuthInfo == null) {
throw new UsernameNotFoundException(username);
}
return new SysUserDetails(userAuthInfo);
} catch (Exception e) {
e.printStackTrace();
// 记录异常日志
log.error("认证异常:{}", e.getMessage());
// 抛出异常
throw e;
}
}
}
@@ -0,0 +1,108 @@
package com.youlai.boot.framework.security.util;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.jwt.JWTPayload;
import cn.hutool.jwt.JWTUtil;
import com.youlai.system.common.constant.JwtClaimConstants;
import com.youlai.boot.framework.security.model.SysUserDetails;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.stereotype.Component;
import java.util.*;
import java.util.stream.Collectors;
/**
* JWT Token 工具类
*
* @author Ray Hao
* @since 2.6.0
*/
@Component
public class JwtUtils {
/**
* JWT 加解密使用的密钥
*/
private static byte[] key;
/**
* JWT Token 的有效时间(单位:秒)
*/
private static int ttl;
@Value("${security.jwt.key}")
public void setKey(String key) {
JwtUtils.key = key.getBytes();
}
@Value("${security.jwt.ttl}")
public void setTtl(Integer ttl) {
JwtUtils.ttl = ttl;
}
/**
* 生成 JWT Token
*
* @param authentication 用户认证信息
* @return Token 字符串
*/
public static String createToken(Authentication authentication) {
SysUserDetails userDetails = (SysUserDetails) authentication.getPrincipal();
Map<String, Object> payload = new HashMap<>();
payload.put(JwtClaimConstants.USER_ID, userDetails.getUserId()); // 用户ID
payload.put(JwtClaimConstants.DEPT_ID, userDetails.getDeptId()); // 部门ID
payload.put(JwtClaimConstants.DATA_SCOPE, userDetails.getDataScope()); // 数据权限范围
// claims 中添加角色信息
Set<String> roles = userDetails.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toSet());
payload.put(JwtClaimConstants.AUTHORITIES, roles);
Date now = new Date();
Date expiration = DateUtil.offsetSecond(now, ttl);
payload.put(JWTPayload.ISSUED_AT, now);
payload.put(JWTPayload.EXPIRES_AT, expiration);
payload.put(JWTPayload.SUBJECT, authentication.getName());
payload.put(JWTPayload.JWT_ID, IdUtil.simpleUUID());
return JWTUtil.createToken(payload, key);
}
/**
* 从 JWT Token 中解析 Authentication 用户认证信息
*
* @param payloads JWT 载体
* @return 用户认证信息
*/
public static UsernamePasswordAuthenticationToken getAuthentication(JSONObject payloads) {
SysUserDetails userDetails = new SysUserDetails();
userDetails.setUserId(payloads.getLong(JwtClaimConstants.USER_ID)); // 用户ID
userDetails.setDeptId(payloads.getLong(JwtClaimConstants.DEPT_ID)); // 部门ID
userDetails.setDataScope(payloads.getInt(JwtClaimConstants.DATA_SCOPE)); // 数据权限范围
userDetails.setUsername(payloads.getStr(JWTPayload.SUBJECT)); // 用户名
// 角色集合
Set<SimpleGrantedAuthority> authorities = payloads.getJSONArray(JwtClaimConstants.AUTHORITIES)
.stream()
.map(authority -> new SimpleGrantedAuthority(Convert.toStr(authority)))
.collect(Collectors.toSet());
return new UsernamePasswordAuthenticationToken(userDetails, "", authorities);
}
}
@@ -0,0 +1,109 @@
package com.youlai.boot.framework.security.util;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.StrUtil;
import com.youlai.system.common.constant.SystemConstants;
import com.youlai.boot.framework.security.model.SysUserDetails;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import java.util.Collection;
import java.util.Collections;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
/**
* Spring Security 工具类
*
* @author Ray
* @since 2021/1/10
*/
public class SecurityUtils {
/**
* 获取当前登录人信息
*
* @return Optional<SysUserDetails>
*/
public static Optional<SysUserDetails> getUser() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null) {
Object principal = authentication.getPrincipal();
if (principal instanceof SysUserDetails) {
return Optional.of((SysUserDetails) principal);
}
}
return Optional.empty();
}
/**
* 获取用户ID
*
* @return Long
*/
public static Long getUserId() {
return getUser().map(SysUserDetails::getUserId).orElse(null);
}
/**
* 获取用户账号
*
* @return String 用户账号
*/
public static String getUsername() {
return getUser().map(SysUserDetails::getUsername).orElse(null);
}
/**
* 获取部门ID
*
* @return Long
*/
public static Long getDeptId() {
return getUser().map(SysUserDetails::getDeptId).orElse(null);
}
/**
* 获取数据权限范围
*
* @return Integer
*/
public static Integer getDataScope() {
return getUser().map(SysUserDetails::getDataScope).orElse(null);
}
/**
* 获取用户角色集合
*
* @return 角色集合
*/
public static Set<String> getRoles() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null) {
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
if (CollectionUtil.isNotEmpty(authorities)) {
return authorities.stream().filter(item -> item.getAuthority().startsWith("ROLE_"))
.map(item -> StrUtil.removePrefix(item.getAuthority(), "ROLE_"))
.collect(Collectors.toSet());
}
}
return Collections.EMPTY_SET;
}
/**
* 是否超级管理员
* <p>
* 超级管理员忽视任何权限判断
*/
public static boolean isRoot() {
Set<String> roles = getRoles();
return roles.contains(SystemConstants.ROOT_ROLE_CODE);
}
}
@@ -0,0 +1,102 @@
package com.youlai.boot.module.generator.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.youlai.system.common.result.PageResult;
import com.youlai.system.common.result.Result;
import com.youlai.boot.config.property.GeneratorProperties;
import com.youlai.system.enums.LogModuleEnum;
import com.youlai.boot.module.system.model.form.GenConfigForm;
import com.youlai.boot.module.system.model.query.TablePageQuery;
import com.youlai.boot.module.system.model.vo.GeneratorPreviewVO;
import com.youlai.boot.module.system.model.vo.TablePageVO;
import com.youlai.boot.common.annotation.LogAnnotation;
import com.youlai.system.service.GeneratorService;
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.apache.commons.io.IOUtils;
import org.springframework.web.bind.annotation.*;
import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.List;
/**
* 代码生成器控制层
*
* @author Ray
* @since 2.10.0
*/
@Tag(name = "09.代码生成")
@RestController
@RequestMapping("/api/v1/generator")
@RequiredArgsConstructor
public class GeneratorController {
private final GeneratorService generatorService;
private final GeneratorProperties generatorProperties;
@Operation(summary = "获取数据表分页列表")
@GetMapping("/table/page")
@LogAnnotation(value = "代码生成分页列表", module = LogModuleEnum.OTHER)
public PageResult<TablePageVO> getTablePage(
TablePageQuery queryParams
) {
Page<TablePageVO> result = generatorService.getTablePage(queryParams);
return PageResult.success(result);
}
@Operation(summary = "获取代码生成配置")
@GetMapping("/{tableName}/config")
public Result<GenConfigForm> getGenConfigFormData(
@Parameter(description = "表名", example = "sys_user") @PathVariable String tableName
) {
GenConfigForm formData = generatorService.getGenConfigFormData(tableName);
return Result.success(formData);
}
@Operation(summary = "保存代码生成配置")
@PostMapping("/{tableName}/config")
@LogAnnotation(value = "生成代码", module = LogModuleEnum.OTHER)
public Result<?> saveGenConfig(@RequestBody GenConfigForm formData) {
generatorService.saveGenConfig(formData);
return Result.success();
}
@Operation(summary = "删除代码生成配置")
@DeleteMapping("/{tableName}/config")
public Result<?> deleteGenConfig(
@Parameter(description = "表名", example = "sys_user") @PathVariable String tableName
) {
generatorService.deleteGenConfig(tableName);
return Result.success();
}
@Operation(summary = "获取预览生成代码")
@GetMapping("/{tableName}/preview")
@LogAnnotation(value = "预览生成代码", module = LogModuleEnum.OTHER)
public Result<List<GeneratorPreviewVO>> getTablePreviewData(@PathVariable String tableName) {
List<GeneratorPreviewVO> list = generatorService.getTablePreviewData(tableName);
return Result.success(list);
}
@Operation(summary = "下载代码")
@GetMapping("/{tableName}/download")
@LogAnnotation(value = "下载代码", module = LogModuleEnum.OTHER)
public void downloadZip(HttpServletResponse response, @PathVariable String tableName) throws IOException {
String[] tableNames = tableName.split(",");
byte[] data = generatorService.downloadCode(tableNames);
response.reset();
response.setHeader("Content-Disposition", "attachment; filename=" + URLEncoder.encode(generatorProperties.getDownloadFileName(), StandardCharsets.UTF_8));
response.addHeader("Access-Control-Allow-Origin", "*");
response.setHeader("Pragma", "no-cache");
response.setHeader("Cache-Control", "no-cache");
response.addHeader("Access-Control-Expose-Headers", "Content-Disposition");
response.setContentType("application/octet-stream; charset=UTF-8");
response.setDateHeader("Expires", 0);
IOUtils.write(data, response.getOutputStream());
}
}
@@ -0,0 +1,25 @@
package com.youlai.boot.module.generator.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.youlai.boot.module.system.model.bo.ColumnMetaData;
import com.youlai.boot.module.system.model.bo.TableMetaData;
import com.youlai.boot.module.system.model.entity.SysDept;
import com.youlai.boot.module.system.model.query.TablePageQuery;
import com.youlai.boot.module.system.model.vo.TablePageVO;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@Mapper
public interface DatabaseMapper extends BaseMapper<SysDept> {
Page<TablePageVO> getTablePage(Page<TablePageVO> page, TablePageQuery queryParams);
List<ColumnMetaData> getTableColumns(String tableName);
TableMetaData getTableMetadata(String tableName);
}
@@ -0,0 +1,20 @@
package com.youlai.boot.module.generator.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.youlai.boot.module.system.model.entity.GenConfig;
import org.apache.ibatis.annotations.Mapper;
/**
* 代码生成基础配置访问层
*
* @author Ray
* @since 2.10.0
*/
@Mapper
public interface GenConfigMapper extends BaseMapper<GenConfig> {
}
@@ -0,0 +1,20 @@
package com.youlai.boot.module.generator.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.youlai.boot.module.system.model.entity.GenFieldConfig;
import org.apache.ibatis.annotations.Mapper;
/**
* 代码生成字段配置访问层
*
* @author Ray
* @since 2.10.0
*/
@Mapper
public interface GenFieldConfigMapper extends BaseMapper<GenFieldConfig> {
}
@@ -0,0 +1,16 @@
package com.youlai.boot.module.generator.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.youlai.boot.module.system.model.entity.GenConfig;
/**
* 代码生成配置接口
*
* @author Ray
* @since 2.10.0
*/
public interface GenConfigService extends IService<GenConfig> {
}
@@ -0,0 +1,14 @@
package com.youlai.boot.module.generator.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.youlai.boot.module.system.model.entity.GenFieldConfig;
/**
* 代码生成配置接口
*
* @author Ray
* @since 2.10.0
*/
public interface GenFieldConfigService extends IService<GenFieldConfig> {
}
@@ -0,0 +1,65 @@
package com.youlai.boot.module.generator.service;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.youlai.boot.module.system.model.form.GenConfigForm;
import com.youlai.boot.module.system.model.query.TablePageQuery;
import com.youlai.boot.module.system.model.vo.GeneratorPreviewVO;
import com.youlai.boot.module.system.model.vo.TablePageVO;
import java.util.List;
/**
* 代码生成配置接口
*
* @author Ray
* @since 2.10.0
*/
public interface GeneratorService {
/**
* 获取数据表分页列表
*
* @param queryParams 查询参数
* @return
*/
Page<TablePageVO> getTablePage(TablePageQuery queryParams);
/**
* 获取预览生成代码
*
* @param tableName 表名
* @return
*/
List<GeneratorPreviewVO> getTablePreviewData(String tableName);
/**
* 获取代码生成配置
*
* @param tableName 表名
* @return
*/
GenConfigForm getGenConfigFormData(String tableName);
/**
* 保存代码生成配置
*
* @param formData 表单数据
* @return
*/
void saveGenConfig(GenConfigForm formData);
/**
* 删除代码生成配置
*
* @param tableName 表名
* @return
*/
void deleteGenConfig(String tableName);
/**
* 下载代码
* @param tableNames 表名
* @return
*/
byte[] downloadCode(String[] tableNames);
}
@@ -0,0 +1,20 @@
package com.youlai.boot.module.generator.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.youlai.boot.module.generator.mapper.GenConfigMapper;
import com.youlai.boot.module.system.model.entity.GenConfig;
import com.youlai.system.service.GenConfigService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
/**
* 数据库服务实现类
*
* @author Ray
* @since 2.10.0
*/
@Service
@RequiredArgsConstructor
public class GenConfigServiceImpl extends ServiceImpl<GenConfigMapper, GenConfig> implements GenConfigService {
}
@@ -0,0 +1,21 @@
package com.youlai.boot.module.generator.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.youlai.boot.module.generator.mapper.GenFieldConfigMapper;
import com.youlai.boot.module.system.model.entity.GenFieldConfig;
import com.youlai.system.service.GenFieldConfigService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
/**
* 代码生成字段配置服务实现类
*
* @author Ray
* @since 2.10.0
*/
@Service
@RequiredArgsConstructor
public class GenFieldConfigServiceImpl extends ServiceImpl<GenFieldConfigMapper, GenFieldConfig> implements GenFieldConfigService {
}
@@ -0,0 +1,477 @@
package com.youlai.boot.module.generator.service.impl;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.template.Template;
import cn.hutool.extra.template.TemplateConfig;
import cn.hutool.extra.template.TemplateEngine;
import cn.hutool.extra.template.TemplateUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.youlai.system.SystemApplication;
import com.youlai.boot.config.property.GeneratorProperties;
import com.youlai.boot.module.system.converter.GenConfigConverter;
import com.youlai.system.enums.FormTypeEnum;
import com.youlai.system.enums.JavaTypeEnum;
import com.youlai.system.enums.QueryTypeEnum;
import com.youlai.boot.common.exception.BusinessException;
import com.youlai.boot.module.generator.mapper.DatabaseMapper;
import com.youlai.boot.module.system.model.bo.ColumnMetaData;
import com.youlai.boot.module.system.model.bo.TableMetaData;
import com.youlai.boot.module.system.model.entity.GenConfig;
import com.youlai.boot.module.system.model.entity.GenFieldConfig;
import com.youlai.boot.module.system.model.form.GenConfigForm;
import com.youlai.boot.module.system.model.query.TablePageQuery;
import com.youlai.boot.module.system.model.vo.GeneratorPreviewVO;
import com.youlai.boot.module.system.model.vo.TablePageVO;
import com.youlai.system.service.GeneratorService;
import com.youlai.system.service.GenConfigService;
import com.youlai.system.service.GenFieldConfigService;
import com.youlai.system.service.SysMenuService;
import lombok.RequiredArgsConstructor;
import org.apache.commons.io.IOUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.zip.ZipOutputStream;
/**
* 数据库服务实现类
*
* @author Ray
* @since 2.10.0
*/
@Service
@RequiredArgsConstructor
public class GeneratorServiceImpl implements GeneratorService {
private final DatabaseMapper databaseMapper;
private final GeneratorProperties generatorProperties;
private final GenConfigService genConfigService;
private final GenFieldConfigService genFieldConfigService;
private final GenConfigConverter genConfigConverter;
private final SysMenuService menuService;
@Value("${spring.profiles.active}")
private String springProfilesActive;
/**
* 数据表分页列表
*
* @param queryParams 查询参数
* @return 分页结果
*/
public Page<TablePageVO> getTablePage(TablePageQuery queryParams) {
Page<TablePageVO> page = new Page<>(queryParams.getPageNum(), queryParams.getPageSize());
// 设置排除的表
List<String> excludeTables = generatorProperties.getExcludeTables();
queryParams.setExcludeTables(excludeTables);
return databaseMapper.getTablePage(page, queryParams);
}
/**
* 获取代码生成配置
*
* @param tableName 表名 eg: sys_user
* @return 代码生成配置
*/
@Override
public GenConfigForm getGenConfigFormData(String tableName) {
// 查询表生成配置
GenConfig genConfig = genConfigService.getOne(
new LambdaQueryWrapper<>(GenConfig.class)
.eq(GenConfig::getTableName, tableName)
.last("LIMIT 1")
);
// 是否有代码生成配置
boolean hasGenConfig = genConfig != null;
// 如果没有代码生成配置,则根据表的元数据生成默认配置
if (genConfig == null) {
TableMetaData tableMetadata = databaseMapper.getTableMetadata(tableName);
Assert.isTrue(tableMetadata != null, "未找到表元数据");
genConfig = new GenConfig();
genConfig.setTableName(tableName);
String tableComment = tableMetadata.getTableComment();
if (StrUtil.isNotBlank(tableComment)) {
genConfig.setBusinessName(tableComment.replace("表", ""));
}
// 实体类名 = 表名去掉前缀后转驼峰,前缀默认为下划线分割的第一个元素
String entityName = StrUtil.toCamelCase(StrUtil.removePrefix(tableName, tableName.split("_")[0]));
genConfig.setEntityName(entityName);
String packageName = SystemApplication.class.getPackageName();
genConfig.setPackageName(StrUtil.subBefore(packageName, ".", true));
genConfig.setModuleName(StrUtil.subAfter(packageName, ".", true));
genConfig.setAuthor(generatorProperties.getDefaultConfig().getAuthor());
}
// 根据表的列 + 已经存在的字段生成配置 得到 组合后的字段生成配置
List<GenFieldConfig> genFieldConfigs = new ArrayList<>();
// 获取表的列
List<ColumnMetaData> tableColumns = databaseMapper.getTableColumns(tableName);
if (CollectionUtil.isNotEmpty(tableColumns)) {
// 查询字段生成配置
List<GenFieldConfig> fieldConfigList = genFieldConfigService.list(
new LambdaQueryWrapper<GenFieldConfig>()
.eq(GenFieldConfig::getConfigId, genConfig.getId())
.orderByAsc(GenFieldConfig::getFieldSort)
);
Integer maxSort = fieldConfigList.stream()
.map(GenFieldConfig::getFieldSort)
.filter(Objects::nonNull) // 过滤掉空值
.max(Integer::compareTo)
.orElse(0);
for (ColumnMetaData tableColumn : tableColumns) {
// 根据列名获取字段生成配置
String columnName = tableColumn.getColumnName();
GenFieldConfig fieldConfig = fieldConfigList.stream()
.filter(item -> StrUtil.equals(item.getColumnName(), columnName))
.findFirst()
.orElseGet(() -> createDefaultFieldConfig(tableColumn));
if (fieldConfig.getFieldSort() == null) {
fieldConfig.setFieldSort(++maxSort);
}
// 根据列类型设置字段类型
String fieldType = fieldConfig.getFieldType();
if (StrUtil.isBlank(fieldType)) {
String javaType = JavaTypeEnum.getJavaTypeByColumnType(fieldConfig.getColumnType());
fieldConfig.setFieldType(javaType);
}
// 如果没有代码生成配置,则默认展示在列表和表单
if (!hasGenConfig) {
fieldConfig.setIsShowInList(1);
fieldConfig.setIsShowInForm(1);
}
genFieldConfigs.add(fieldConfig);
}
}
//对genFieldConfigs按照fieldSort排序
genFieldConfigs = genFieldConfigs.stream().sorted(Comparator.comparing(GenFieldConfig::getFieldSort)).toList();
GenConfigForm genConfigForm = genConfigConverter.toGenConfigForm(genConfig, genFieldConfigs);
genConfigForm.setFrontendAppName(generatorProperties.getFrontendAppName());
genConfigForm.setBackendAppName(generatorProperties.getBackendAppName());
return genConfigForm;
}
/**
* 创建默认字段配置
*
* @param columnMetaData 表字段元数据
* @return
*/
private GenFieldConfig createDefaultFieldConfig(ColumnMetaData columnMetaData) {
GenFieldConfig fieldConfig = new GenFieldConfig();
fieldConfig.setColumnName(columnMetaData.getColumnName());
fieldConfig.setColumnType(columnMetaData.getDataType());
fieldConfig.setFieldComment(columnMetaData.getColumnComment());
fieldConfig.setFieldName(StrUtil.toCamelCase(columnMetaData.getColumnName()));
fieldConfig.setIsRequired("YES".equals(columnMetaData.getIsNullable()) ? 1 : 0);
if (fieldConfig.getColumnType().equals("date")) {
fieldConfig.setFormType(FormTypeEnum.DATE);
} else if (fieldConfig.getColumnType().equals("datetime")) {
fieldConfig.setFormType(FormTypeEnum.DATE_TIME);
} else {
fieldConfig.setFormType(FormTypeEnum.INPUT);
}
fieldConfig.setQueryType(QueryTypeEnum.EQ);
fieldConfig.setMaxLength(columnMetaData.getCharacterMaximumLength());
return fieldConfig;
}
/**
* 保存代码生成配置
*
* @param formData 代码生成配置表单
*/
@Override
public void saveGenConfig(GenConfigForm formData) {
GenConfig genConfig = genConfigConverter.toGenConfig(formData);
genConfigService.saveOrUpdate(genConfig);
// 如果选择上级菜单
Long parentMenuId = formData.getParentMenuId();
if (parentMenuId != null && springProfilesActive.equals("dev")) {
menuService.saveMenu(parentMenuId, genConfig);
}
List<GenFieldConfig> genFieldConfigs = genConfigConverter.toGenFieldConfig(formData.getFieldConfigs());
if (CollectionUtil.isEmpty(genFieldConfigs)) {
throw new BusinessException("字段配置不能为空");
}
genFieldConfigs.forEach(genFieldConfig -> {
genFieldConfig.setConfigId(genConfig.getId());
});
genFieldConfigService.saveOrUpdateBatch(genFieldConfigs);
}
/**
* 删除代码生成配置
*
* @param tableName 表名
* @return
*/
@Override
public void deleteGenConfig(String tableName) {
GenConfig genConfig = genConfigService.getOne(new LambdaQueryWrapper<GenConfig>()
.eq(GenConfig::getTableName, tableName));
boolean result = genConfigService.remove(new LambdaQueryWrapper<GenConfig>()
.eq(GenConfig::getTableName, tableName)
);
if (result) {
genFieldConfigService.remove(new LambdaQueryWrapper<GenFieldConfig>()
.eq(GenFieldConfig::getConfigId, genConfig.getId())
);
}
}
/**
* 获取预览生成代码
*
* @param tableName 表名
* @return 预览数据
*/
@Override
public List<GeneratorPreviewVO> getTablePreviewData(String tableName) {
List<GeneratorPreviewVO> list = new ArrayList<>();
GenConfig genConfig = genConfigService.getOne(new LambdaQueryWrapper<GenConfig>()
.eq(GenConfig::getTableName, tableName)
);
Assert.isTrue(genConfig != null, "未找到表生成配置");
List<GenFieldConfig> fieldConfigs = genFieldConfigService.list(new LambdaQueryWrapper<GenFieldConfig>()
.eq(GenFieldConfig::getConfigId, genConfig.getId())
.orderByAsc(GenFieldConfig::getFieldSort)
);
Assert.isTrue(CollectionUtil.isNotEmpty(fieldConfigs), "未找到字段生成配置");
// 遍历模板配置
Map<String, GeneratorProperties.TemplateConfig> templateConfigs = generatorProperties.getTemplateConfigs();
for (Map.Entry<String, GeneratorProperties.TemplateConfig> templateConfigEntry : templateConfigs.entrySet()) {
GeneratorPreviewVO previewVO = new GeneratorPreviewVO();
GeneratorProperties.TemplateConfig templateConfig = templateConfigEntry.getValue();
/* 1. 生成文件名 UserController */
// User Role Menu Dept
String entityName = genConfig.getEntityName();
// Controller Service Mapper Entity
String templateName = templateConfigEntry.getKey();
// .java .ts .vue
String extension = templateConfig.getExtension();
// 文件名 UserController.java
String fileName = getFileName(entityName, templateName, extension);
previewVO.setFileName(fileName);
/* 2. 生成文件路径 */
// com.youlai
String packageName = genConfig.getPackageName();
// system
String moduleName = genConfig.getModuleName();
// controller
String subpackageName = templateConfig.getSubpackageName();
// 文件路径 src/main/java/com/youlai/system/controller
String filePath = getFilePath(templateName, moduleName, packageName, subpackageName, entityName);
previewVO.setPath(filePath);
/* 3. 生成文件内容 */
String content = getCodeContent(templateConfig, genConfig, fieldConfigs);
previewVO.setContent(content);
list.add(previewVO);
}
return list;
}
/**
* 生成文件名
*
* @param entityName 实体类名 UserController
* @param templateName 模板名 Entity
* @param extension 文件后缀 .java
* @return 文件名
*/
private String getFileName(String entityName, String templateName, String extension) {
if ("Entity".equals(templateName)) {
return entityName + extension;
} else if ("MapperXml".equals(templateName)) {
return entityName + "Mapper" + extension;
} else if ("API".equals(templateName)) {
return StrUtil.toSymbolCase(entityName, '-') + extension;
} else if ("VIEW".equals(templateName)) {
return "index.vue";
}
return entityName + templateName + extension;
}
/**
* 生成文件路径
*
* @param templateName 模板名 Entity
* @param moduleName 模块名 system
* @param packageName 包名 com.youlai
* @param subPackageName 子包名 controller
* @param entityName 实体类名 UserController
* @return 文件路径 src/main/java/com/youlai/system/controller
*/
private String getFilePath(String templateName, String moduleName, String packageName, String subPackageName, String entityName) {
String path;
if ("MapperXml".equals(templateName)) {
path = (generatorProperties.getBackendAppName()
+ File.separator
+ "src" + File.separator + "main" + File.separator + "resources"
+ File.separator + subPackageName
);
} else if ("API".equals(templateName)) {
path = (generatorProperties.getFrontendAppName()
+ File.separator
+ "src" + File.separator + subPackageName
);
} else if ("VIEW".equals(templateName)) {
path = (generatorProperties.getFrontendAppName()
+ File.separator + "src"
+ File.separator + subPackageName
+ File.separator + moduleName
+ File.separator + StrUtil.toSymbolCase(entityName, '-')
);
} else {
path = (generatorProperties.getBackendAppName()
+ File.separator
+ "src" + File.separator + "main" + File.separator + "java"
+ File.separator + packageName
+ File.separator + moduleName
+ File.separator + subPackageName
);
}
// subPackageName = model.entity => model/entity
path = path.replace(".", File.separator);
return path;
}
/**
* 生成代码内容
*
* @param templateConfig 模板配置
* @param genConfig 生成配置
* @param fieldConfigs 字段配置
* @return 代码内容
*/
private String getCodeContent(GeneratorProperties.TemplateConfig templateConfig, GenConfig genConfig, List<GenFieldConfig> fieldConfigs) {
Map<String, Object> bindMap = new HashMap<>();
String entityName = genConfig.getEntityName();
bindMap.put("packageName", genConfig.getPackageName());
bindMap.put("moduleName", genConfig.getModuleName());
bindMap.put("subpackageName", templateConfig.getSubpackageName());
bindMap.put("date", DateUtil.format(new Date(), "yyyy-MM-dd HH:mm"));
bindMap.put("entityName", entityName);
bindMap.put("tableName", genConfig.getTableName());
bindMap.put("author", genConfig.getAuthor());
bindMap.put("lowerFirstEntityName", StrUtil.lowerFirst(entityName)); // UserTest → userTest
bindMap.put("kebabCaseEntityName", StrUtil.toSymbolCase(entityName,'-')); // UserTest → user-test
bindMap.put("businessName", genConfig.getBusinessName());
bindMap.put("fieldConfigs", fieldConfigs);
boolean hasLocalDateTime = false;
boolean hasBigDecimal = false;
boolean hasRequiredField = false;
for (GenFieldConfig fieldConfig : fieldConfigs) {
if ("LocalDateTime".equals(fieldConfig.getFieldType())) {
hasLocalDateTime = true;
}
if ("BigDecimal".equals(fieldConfig.getFieldType())) {
hasBigDecimal = true;
}
if (ObjectUtil.equals(fieldConfig.getIsRequired(), 1)) {
hasRequiredField = true;
}
fieldConfig.setTsType(JavaTypeEnum.getTsTypeByJavaType(fieldConfig.getFieldType()));
}
bindMap.put("hasLocalDateTime", hasLocalDateTime);
bindMap.put("hasBigDecimal", hasBigDecimal);
bindMap.put("hasRequiredField", hasRequiredField);
TemplateEngine templateEngine = TemplateUtil.createEngine(new TemplateConfig("templates", TemplateConfig.ResourceMode.CLASSPATH));
Template template = templateEngine.getTemplate(templateConfig.getTemplatePath());
String content = template.render(bindMap);
return content;
}
/**
* 下载代码
*
* @param tableNames 表名,可以支持多张表。
* @return 压缩文件字节数组
*/
@Override
public byte[] downloadCode(String[] tableNames) {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
ZipOutputStream zip = new ZipOutputStream(outputStream);
for (String tableName : tableNames) {
generatorCode(tableName, zip);
}
IOUtils.closeQuietly(zip);
return outputStream.toByteArray();
}
/**
* 根据表名生成代码并且压缩到zip文件中
*
* @param tableName 单个表名
* @param zip 压缩文件
*/
private void generatorCode(String tableName, ZipOutputStream zip) {
List<GeneratorPreviewVO> previewVOList = getTablePreviewData(tableName);
for (GeneratorPreviewVO previewVO : previewVOList) {
String fileName = previewVO.getFileName();
String content = previewVO.getContent();
String path = previewVO.getPath();
try {
zip.putNextEntry(new java.util.zip.ZipEntry(path + File.separator + fileName));
zip.write(content.getBytes(StandardCharsets.UTF_8));
zip.closeEntry();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
@@ -0,0 +1,57 @@
package com.youlai.boot.module.system.controller;
import com.youlai.system.enums.LogModuleEnum;
import com.youlai.system.common.result.Result;
import com.youlai.boot.module.system.model.dto.CaptchaResult;
import com.youlai.boot.module.system.model.dto.LoginResult;
import com.youlai.boot.common.annotation.LogAnnotation;
import com.youlai.system.service.AuthService;
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 lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
/**
* 认证控制层
*
* @author Ray
* @since 2022/10/16
*/
@Tag(name = "01.认证中心")
@RestController
@RequestMapping("/api/v1/auth")
@RequiredArgsConstructor
@Slf4j
public class AuthController {
private final AuthService authService;
@Operation(summary = "登录")
@PostMapping("/login")
@LogAnnotation(value = "登录", module = LogModuleEnum.LOGIN)
public Result<LoginResult> login(
@Parameter(description = "用户名", example = "admin") @RequestParam String username,
@Parameter(description = "密码", example = "123456") @RequestParam String password
) {
LoginResult loginResult = authService.login(username, password);
return Result.success(loginResult);
}
@Operation(summary = "注销")
@DeleteMapping("/logout")
@LogAnnotation(value = "注销", module = LogModuleEnum.LOGIN)
public Result<?> logout() {
authService.logout();
return Result.success();
}
@Operation(summary = "获取验证码")
@GetMapping("/captcha")
public Result<CaptchaResult> getCaptcha() {
CaptchaResult captcha = authService.getCaptcha();
return Result.success(captcha);
}
}
@@ -0,0 +1,49 @@
package com.youlai.boot.module.system.controller;
import com.youlai.system.common.result.Result;
import com.youlai.boot.module.system.model.dto.FileInfo;
import com.youlai.system.service.OssService;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.Operation;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
/**
* 文件控制层
*
* @author Ray
* @since 2022/10/16
*/
@Tag(name = "07.文件接口")
@RestController
@RequestMapping("/api/v1/files")
@RequiredArgsConstructor
public class FileController {
private final OssService ossService;
@PostMapping
@Operation(summary = "文件上传")
public Result<FileInfo> uploadFile(
@Parameter(name = "file", description = "表单文件对象", required = true, in = ParameterIn.DEFAULT, schema = @Schema(name = "file", format = "binary"))
@RequestPart(value = "file") MultipartFile file
) {
FileInfo fileInfo = ossService.uploadFile(file);
return Result.success(fileInfo);
}
@DeleteMapping
@Operation(summary = "文件删除")
@SneakyThrows
public Result<?> deleteFile(
@Parameter(description = "文件路径") @RequestParam String filePath
) {
boolean result = ossService.deleteFile(filePath);
return Result.judge(result);
}
}
@@ -0,0 +1,81 @@
package com.youlai.boot.module.system.controller;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.youlai.boot.common.result.PageResult;
import com.youlai.boot.common.result.Result;
import com.youlai.boot.module.system.model.form.ConfigForm;
import com.youlai.boot.module.system.model.query.ConfigPageQuery;
import com.youlai.boot.module.system.model.vo.ConfigVO;
import com.youlai.boot.module.system.service.SysConfigService;
import io.swagger.v3.oas.annotations.Parameter;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springdoc.core.annotations.ParameterObject;
import org.springframework.web.bind.annotation.*;
import org.springframework.security.access.prepost.PreAuthorize;
/**
* 系统配置前端控制层
*
* @author Theo
* @since 2024-07-30 11:25
*/
@Slf4j
@RestController
@RequiredArgsConstructor
@Tag(name = "10.系统配置")
@RequestMapping("/api/v1/config")
public class SysConfigController {
private final SysConfigService sysConfigService;
@GetMapping("/page")
@Operation(summary = "系统配置分页列表")
@PreAuthorize("@ss.hasPerm('sys:config:query')")
public PageResult<ConfigVO> page(@ParameterObject ConfigPageQuery configPageQuery) {
IPage<ConfigVO> result = sysConfigService.page(configPageQuery);
return PageResult.success(result);
}
@Operation(summary = "新增系统配置")
@PostMapping
@PreAuthorize("@ss.hasPerm('sys:config:add')")
public Result<?> save(@RequestBody @Valid ConfigForm configForm) {
return Result.judge(sysConfigService.save(configForm));
}
@Operation(summary = "获取系统配置表单数据")
@GetMapping("/{id}/form")
public Result<ConfigForm> getConfigForm(
@Parameter(description = "系统配置ID") @PathVariable Long id
) {
ConfigForm formData = sysConfigService.getConfigFormData(id);
return Result.success(formData);
}
@Operation(summary = "刷新系统配置缓存")
@PatchMapping
@PreAuthorize("@ss.hasPerm('sys:config:refresh')")
public Result<ConfigForm> refreshCache() {
return Result.judge(sysConfigService.refreshCache());
}
@PutMapping(value = "/{id}")
@Operation(summary = "修改系统配置")
@PreAuthorize("@ss.hasPerm('sys:config:update')")
public Result<?> update(@Valid @PathVariable Long id, @RequestBody ConfigForm configForm) {
return Result.judge(sysConfigService.edit(id, configForm));
}
@DeleteMapping("/{id}")
@Operation(summary = "删除系统配置")
@PreAuthorize("@ss.hasPerm('sys:config:delete')")
public Result<?> delete(@PathVariable Long id) {
return Result.judge(sysConfigService.delete(id));
}
}
@@ -0,0 +1,94 @@
package com.youlai.boot.module.system.controller;
import com.youlai.system.enums.LogModuleEnum;
import com.youlai.boot.common.annotation.NoRepeat;
import com.youlai.system.common.model.Option;
import com.youlai.system.common.result.Result;
import com.youlai.boot.module.system.model.form.DeptForm;
import com.youlai.boot.module.system.model.query.DeptQuery;
import com.youlai.boot.module.system.model.vo.DeptVO;
import com.youlai.boot.common.annotation.LogAnnotation;
import com.youlai.system.service.SysDeptService;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.Operation;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import jakarta.validation.Valid;
import java.util.List;
/**
* 部门控制器
*
* @author haoxr
* @since 2020/11/6
*/
@Tag(name = "05.部门接口")
@RestController
@RequestMapping("/api/v1/dept")
@RequiredArgsConstructor
public class SysDeptController {
private final SysDeptService deptService;
@Operation(summary = "部门列表")
@GetMapping
@LogAnnotation( value = "部门列表",module = LogModuleEnum.DEPT)
public Result<List<DeptVO>> getDeptList(
DeptQuery queryParams
) {
List<DeptVO> list = deptService.getDeptList(queryParams);
return Result.success(list);
}
@Operation(summary = "部门下拉列表")
@GetMapping("/options")
public Result<List<Option<Long>>> getDeptOptions() {
List<Option<Long>> list = deptService.listDeptOptions();
return Result.success(list);
}
@Operation(summary = "新增部门")
@PostMapping
@PreAuthorize("@ss.hasPerm('sys:dept:add')")
@NoRepeat
public Result<?> saveDept(
@Valid @RequestBody DeptForm formData
) {
Long id = deptService.saveDept(formData);
return Result.success(id);
}
@Operation(summary = "获取部门表单数据")
@GetMapping("/{deptId}/form")
public Result<DeptForm> getDeptForm(
@Parameter(description ="部门ID") @PathVariable Long deptId
) {
DeptForm deptForm = deptService.getDeptForm(deptId);
return Result.success(deptForm);
}
@Operation(summary = "修改部门")
@PutMapping(value = "/{deptId}")
@PreAuthorize("@ss.hasPerm('sys:dept:edit')")
public Result<?> updateDept(
@PathVariable Long deptId,
@Valid @RequestBody DeptForm formData
) {
deptId = deptService.updateDept(deptId, formData);
return Result.success(deptId);
}
@Operation(summary = "删除部门")
@DeleteMapping("/{ids}")
@PreAuthorize("@ss.hasPerm('sys:dept:delete')")
public Result<?> deleteDepartments(
@Parameter(description ="部门ID,多个以英文逗号(,)分割") @PathVariable("ids") String ids
) {
boolean result = deptService.deleteByIds(ids);
return Result.judge(result);
}
}
@@ -0,0 +1,103 @@
package com.youlai.boot.module.system.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.youlai.system.common.model.Option;
import com.youlai.system.common.result.PageResult;
import com.youlai.system.common.result.Result;
import com.youlai.system.enums.LogModuleEnum;
import com.youlai.boot.module.system.model.query.DictPageQuery;
import com.youlai.boot.module.system.model.vo.DictPageVO;
import com.youlai.boot.common.annotation.NoRepeat;
import com.youlai.boot.module.system.model.form.DictForm;
import com.youlai.boot.common.annotation.LogAnnotation;
import com.youlai.system.service.SysDictService;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.Operation;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 字典控制层
*
* @author Ray
* @since 2.9.0
*/
@Tag(name = "06.字典接口")
@RestController
@RequestMapping("/api/v1/dict")
@RequiredArgsConstructor
public class SysDictController {
private final SysDictService dictService;
@Operation(summary = "字典分页列表")
@GetMapping("/page")
@LogAnnotation( value = "字典分页列表",module = LogModuleEnum.DICT)
public PageResult<DictPageVO> getDictPage(
DictPageQuery queryParams
) {
Page<DictPageVO> result = dictService.getDictPage(queryParams);
return PageResult.success(result);
}
@Operation(summary = "字典列表")
@GetMapping("/list")
public Result<List<Option<String>>> getDictList() {
List<Option<String>> list = dictService.getDictList();
return Result.success(list);
}
@Operation(summary = "字典数据项列表")
@GetMapping("/{code}/options")
public Result<List<Option<Long>>> getDictOptions(
@Parameter(description = "字典编码") @PathVariable String code
) {
List<Option<Long>> options = dictService.listDictItemsByCode(code);
return Result.success(options);
}
@Operation(summary = "字典表单")
@GetMapping("/{id}/form")
public Result<DictForm> getDictForm(
@Parameter(description = "字典ID") @PathVariable Long id
) {
DictForm formData = dictService.getDictForm(id);
return Result.success(formData);
}
@Operation(summary = "新增字典")
@PostMapping
@PreAuthorize("@ss.hasPerm('sys:dict:add')")
@NoRepeat
public Result<?> saveDict(@RequestBody DictForm formData) {
boolean result = dictService.saveDict(formData);
return Result.judge(result);
}
@Operation(summary = "修改字典")
@PutMapping("/{id}")
@PreAuthorize("@ss.hasPerm('sys:dict:edit')")
public Result<?> updateDict(
@PathVariable Long id,
@RequestBody DictForm DictForm
) {
boolean status = dictService.updateDict(id, DictForm);
return Result.judge(status);
}
@Operation(summary = "删除字典")
@DeleteMapping("/{ids}")
@PreAuthorize("@ss.hasPerm('sys:dict:delete')")
public Result<?> deleteDictionaries(
@Parameter(description = "字典ID,多个以英文逗号(,)拼接") @PathVariable String ids
) {
dictService.deleteDictByIds(ids);
return Result.success();
}
}
@@ -0,0 +1,63 @@
package com.youlai.boot.module.system.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.youlai.system.common.result.PageResult;
import com.youlai.system.common.result.Result;
import com.youlai.boot.module.system.model.query.LogPageQuery;
import com.youlai.boot.module.system.model.vo.LogPageVO;
import com.youlai.boot.module.system.model.vo.VisitStatsVO;
import com.youlai.boot.module.system.model.vo.VisitTrendVO;
import com.youlai.system.service.SysLogService;
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.*;
import java.time.LocalDate;
import java.util.List;
/**
* 日志控制层
*
* @author Ray
* @since 2.10.0
*/
@Tag(name = "08.日志接口")
@RestController
@RequestMapping("/api/v1/logs")
@RequiredArgsConstructor
public class SysLogController {
private final SysLogService logService;
@Operation(summary = "日志分页列表")
@GetMapping("/page")
public PageResult<LogPageVO> listPagedLogs(
LogPageQuery queryParams
) {
Page<LogPageVO> result = logService.listPagedLogs(queryParams);
return PageResult.success(result);
}
@Operation(summary = "获取访问趋势")
@GetMapping("/visit-trend")
public Result<VisitTrendVO> getVisitTrend(
@Parameter(description = "开始时间", example = "yyyy-MM-dd") @RequestParam String startDate,
@Parameter(description = "结束时间", example = "yyyy-MM-dd") @RequestParam String endDate
) {
LocalDate start = LocalDate.parse(startDate);
LocalDate end = LocalDate.parse(endDate);
VisitTrendVO data = logService.getVisitTrend(start, end);
return Result.success(data);
}
@Operation(summary = "获取统计数据")
@GetMapping("/visit-stats")
public Result<List<VisitStatsVO>> getVisitStats() {
List<VisitStatsVO> list = logService.getVisitStats();
return Result.success(list);
}
}
@@ -0,0 +1,116 @@
package com.youlai.boot.module.system.controller;
import com.youlai.system.common.result.Result;
import com.youlai.system.enums.LogModuleEnum;
import com.youlai.boot.common.annotation.NoRepeat;
import com.youlai.boot.module.system.model.form.MenuForm;
import com.youlai.boot.module.system.model.query.MenuQuery;
import com.youlai.boot.module.system.model.vo.MenuVO;
import com.youlai.system.common.model.Option;
import com.youlai.boot.module.system.model.vo.RouteVO;
import com.youlai.boot.common.annotation.LogAnnotation;
import com.youlai.boot.framework.security.util.SecurityUtils;
import com.youlai.system.service.SysMenuService;
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 lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Set;
/**
* 菜单控制层
*
* @author Ray
* @since 2020/11/06
*/
@Tag(name = "04.菜单接口")
@RestController
@RequestMapping("/api/v1/menus")
@RequiredArgsConstructor
@Slf4j
public class SysMenuController {
private final SysMenuService menuService;
@Operation(summary = "菜单列表")
@GetMapping
@LogAnnotation( value = "菜单列表",module = LogModuleEnum.MENU)
public Result<List<MenuVO>> listMenus(MenuQuery queryParams) {
List<MenuVO> menuList = menuService.listMenus(queryParams);
return Result.success(menuList);
}
@Operation(summary = "菜单下拉列表")
@GetMapping("/options")
public Result<?> listMenuOptions(
@Parameter(description = "是否只查询父级菜单")
@RequestParam(required = false, defaultValue = "false") boolean onlyParent
) {
List<Option> menus = menuService.listMenuOptions(onlyParent);
return Result.success(menus);
}
@Operation(summary = "菜单路由列表")
@GetMapping("/routes")
public Result<List<RouteVO>> listRoutes() {
Set<String> roles = SecurityUtils.getRoles();
List<RouteVO> routeList = menuService.listRoutes(roles);
return Result.success(routeList);
}
@Operation(summary = "菜单表单数据")
@GetMapping("/{id}/form")
public Result<MenuForm> getMenuForm(
@Parameter(description = "菜单ID") @PathVariable Long id
) {
MenuForm menu = menuService.getMenuForm(id);
return Result.success(menu);
}
@Operation(summary = "新增菜单")
@PostMapping
@PreAuthorize("@ss.hasPerm('sys:menu:add')")
@NoRepeat
public Result<?> addMenu(@RequestBody MenuForm menuForm) {
boolean result = menuService.saveMenu(menuForm);
return Result.judge(result);
}
@Operation(summary = "修改菜单")
@PutMapping(value = "/{id}")
@PreAuthorize("@ss.hasPerm('sys:menu:edit')")
public Result<?> updateMenu(
@RequestBody MenuForm menuForm
) {
boolean result = menuService.saveMenu(menuForm);
return Result.judge(result);
}
@Operation(summary = "删除菜单")
@DeleteMapping("/{id}")
@PreAuthorize("@ss.hasPerm('sys:menu:delete')")
public Result<?> deleteMenu(
@Parameter(description = "菜单ID,多个以英文(,)分割") @PathVariable("id") Long id
) {
boolean result = menuService.deleteMenu(id);
return Result.judge(result);
}
@Operation(summary = "修改菜单显示状态")
@PatchMapping("/{menuId}")
public Result<?> updateMenuVisible(
@Parameter(description = "菜单ID") @PathVariable Long menuId,
@Parameter(description = "显示状态(1:显示;0:隐藏)") Integer visible
) {
boolean result = menuService.updateMenuVisible(menuId, visible);
return Result.judge(result);
}
}
@@ -0,0 +1,121 @@
package com.youlai.boot.module.system.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.youlai.system.enums.LogModuleEnum;
import com.youlai.boot.common.annotation.NoRepeat;
import com.youlai.system.common.model.Option;
import com.youlai.system.common.result.PageResult;
import com.youlai.system.common.result.Result;
import com.youlai.boot.module.system.model.form.RoleForm;
import com.youlai.boot.module.system.model.query.RolePageQuery;
import com.youlai.boot.module.system.model.vo.RolePageVO;
import com.youlai.boot.common.annotation.LogAnnotation;
import com.youlai.system.service.SysRoleService;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.Operation;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import jakarta.validation.Valid;
import java.util.List;
/**
* 角色控制层
*
* @author Ray
* @since 2022/10/16
*/
@Tag(name = "03.角色接口")
@RestController
@RequestMapping("/api/v1/roles")
@RequiredArgsConstructor
public class SysRoleController {
private final SysRoleService roleService;
@Operation(summary = "角色分页列表")
@GetMapping("/page")
@LogAnnotation( value = "角色分页列表",module = LogModuleEnum.ROLE)
public PageResult<RolePageVO> getRolePage(
RolePageQuery queryParams
) {
Page<RolePageVO> result = roleService.getRolePage(queryParams);
return PageResult.success(result);
}
@Operation(summary = "角色下拉列表")
@GetMapping("/options")
public Result<List<Option<Long>>> listRoleOptions() {
List<Option<Long>> list = roleService.listRoleOptions();
return Result.success(list);
}
@Operation(summary = "新增角色")
@PostMapping
@PreAuthorize("@ss.hasPerm('sys:role:add')")
@NoRepeat
public Result<?> addRole(@Valid @RequestBody RoleForm roleForm) {
boolean result = roleService.saveRole(roleForm);
return Result.judge(result);
}
@Operation(summary = "角色表单数据")
@GetMapping("/{roleId}/form")
public Result<RoleForm> getRoleForm(
@Parameter(description = "角色ID") @PathVariable Long roleId
) {
RoleForm roleForm = roleService.getRoleForm(roleId);
return Result.success(roleForm);
}
@Operation(summary = "修改角色")
@PutMapping(value = "/{id}")
@PreAuthorize("@ss.hasPerm('sys:role:edit')")
public Result<?> updateRole(@Valid @RequestBody RoleForm roleForm) {
boolean result = roleService.saveRole(roleForm);
return Result.judge(result);
}
@Operation(summary = "删除角色")
@DeleteMapping("/{ids}")
@PreAuthorize("@ss.hasPerm('sys:role:delete')")
public Result<?> deleteRoles(
@Parameter(description = "删除角色,多个以英文逗号(,)拼接") @PathVariable String ids
) {
boolean result = roleService.deleteRoles(ids);
return Result.judge(result);
}
@Operation(summary = "修改角色状态")
@PutMapping(value = "/{roleId}/status")
public Result<?> updateRoleStatus(
@Parameter(description = "角色ID") @PathVariable Long roleId,
@Parameter(description = "状态(1:启用;0:禁用)") @RequestParam Integer status
) {
boolean result = roleService.updateRoleStatus(roleId, status);
return Result.judge(result);
}
@Operation(summary = "获取角色的菜单ID集合")
@GetMapping("/{roleId}/menuIds")
public Result<List<Long>> getRoleMenuIds(
@Parameter(description = "角色ID") @PathVariable Long roleId
) {
List<Long> menuIds = roleService.getRoleMenuIds(roleId);
return Result.success(menuIds);
}
@Operation(summary = "分配菜单(包括按钮权限)给角色")
@PutMapping("/{roleId}/menus")
public Result<?> assignMenusToRole(
@PathVariable Long roleId,
@RequestBody List<Long> menuIds
) {
boolean result = roleService.assignMenusToRole(roleId, menuIds);
return Result.judge(result);
}
}
@@ -0,0 +1,234 @@
package com.youlai.boot.module.system.controller;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.ExcelWriter;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.youlai.boot.module.system.model.form.*;
import com.youlai.boot.system.model.form.*;
import com.youlai.system.common.result.PageResult;
import com.youlai.system.common.result.Result;
import com.youlai.system.enums.ContactType;
import com.youlai.boot.module.system.model.vo.UserProfileVO;
import com.youlai.boot.framework.security.util.SecurityUtils;
import com.youlai.boot.common.util.ExcelUtils;
import com.youlai.system.enums.LogModuleEnum;
import com.youlai.boot.module.system.model.dto.UserImportDTO;
import com.youlai.boot.common.annotation.NoRepeat;
import com.youlai.boot.module.system.listener.UserImportListener;
import com.youlai.boot.module.system.model.entity.SysUser;
import com.youlai.boot.module.system.model.query.UserPageQuery;
import com.youlai.boot.module.system.model.dto.UserExportDTO;
import com.youlai.boot.module.system.model.vo.UserInfoVO;
import com.youlai.boot.module.system.model.vo.UserPageVO;
import com.youlai.boot.common.annotation.LogAnnotation;
import com.youlai.system.service.SysUserService;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.Operation;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import jakarta.servlet.ServletOutputStream;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.List;
/**
* 用户控制层
*
* @author Ray
* @since 2022/10/16
*/
@Tag(name = "02.用户接口")
@RestController
@RequestMapping("/api/v1/users")
@RequiredArgsConstructor
public class SysUserController {
private final SysUserService userService;
@Operation(summary = "用户分页列表")
@GetMapping("/page")
@LogAnnotation(value = "用户分页列表", module = LogModuleEnum.USER)
public PageResult<UserPageVO> listPagedUsers(
UserPageQuery queryParams
) {
IPage<UserPageVO> result = userService.listPagedUsers(queryParams);
return PageResult.success(result);
}
@Operation(summary = "新增用户")
@PostMapping
@PreAuthorize("@ss.hasPerm('sys:user:add')")
@NoRepeat
public Result<?> saveUser(
@RequestBody @Valid UserForm userForm
) {
boolean result = userService.saveUser(userForm);
return Result.judge(result);
}
@Operation(summary = "用户表单数据")
@GetMapping("/{userId}/form")
public Result<UserForm> getUserForm(
@Parameter(description = "用户ID") @PathVariable Long userId
) {
UserForm formData = userService.getUserFormData(userId);
return Result.success(formData);
}
@Operation(summary = "修改用户")
@PutMapping(value = "/{userId}")
@PreAuthorize("@ss.hasPerm('sys:user:edit')")
public Result<?> updateUser(
@Parameter(description = "用户ID") @PathVariable Long userId,
@RequestBody @Validated UserForm userForm) {
boolean result = userService.updateUser(userId, userForm);
return Result.judge(result);
}
@Operation(summary = "删除用户")
@DeleteMapping("/{ids}")
@PreAuthorize("@ss.hasPerm('sys:user:delete')")
public Result<?> deleteUsers(
@Parameter(description = "用户ID,多个以英文逗号(,)分割") @PathVariable String ids
) {
boolean result = userService.deleteUsers(ids);
return Result.judge(result);
}
@Operation(summary = "修改用户状态")
@PatchMapping(value = "/{userId}/status")
public Result<?> updateUserStatus(
@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)
);
return Result.judge(result);
}
@Operation(summary = "获取当前登录用户信息")
@GetMapping("/me")
public Result<UserInfoVO> getCurrentUserInfo() {
UserInfoVO userInfoVO = userService.getCurrentUserInfo();
return Result.success(userInfoVO);
}
@Operation(summary = "用户导入模板下载")
@GetMapping("/template")
public void downloadTemplate(HttpServletResponse response) throws IOException {
String fileName = "用户导入模板.xlsx";
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setHeader("Content-Disposition", "attachment; filename=" + URLEncoder.encode(fileName, StandardCharsets.UTF_8));
String fileClassPath = "templates" + File.separator + "excel" + File.separator + fileName;
InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream(fileClassPath);
ServletOutputStream outputStream = response.getOutputStream();
ExcelWriter excelWriter = EasyExcel.write(outputStream).withTemplate(inputStream).build();
excelWriter.finish();
}
@Operation(summary = "导入用户")
@PostMapping("/import")
public Result<?> importUsers(MultipartFile file) throws IOException {
UserImportListener listener = new UserImportListener();
String msg = ExcelUtils.importExcel(file.getInputStream(), UserImportDTO.class, listener);
return Result.success(msg);
}
@Operation(summary = "导出用户")
@GetMapping("/export")
public void exportUsers(UserPageQuery queryParams, HttpServletResponse response) throws IOException {
String fileName = "用户列表.xlsx";
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setHeader("Content-Disposition", "attachment; filename=" + URLEncoder.encode(fileName, StandardCharsets.UTF_8));
List<UserExportDTO> exportUserList = userService.listExportUsers(queryParams);
EasyExcel.write(response.getOutputStream(), UserExportDTO.class).sheet("用户列表")
.doWrite(exportUserList);
}
@Operation(summary = "获取个人中心用户信息")
@GetMapping("/profile")
public Result<UserProfileVO> getUserProfile() {
Long userId = SecurityUtils.getUserId();
UserProfileVO userProfile = userService.getUserProfile(userId);
return Result.success(userProfile);
}
@Operation(summary = "修改个人中心用户信息")
@PutMapping("/profile")
public Result<?> updateUserProfile(@RequestBody UserProfileForm formData) {
boolean result = userService.updateUserProfile(formData);
return Result.judge(result);
}
@Operation(summary = "重置用户密码")
@PutMapping(value = "/{userId}/password/reset")
@PreAuthorize("@ss.hasPerm('sys:user:password:reset')")
public Result<?> resetPassword(
@Parameter(description = "用户ID") @PathVariable Long userId,
@RequestParam String password
) {
boolean result = userService.resetPassword(userId, password);
return Result.judge(result);
}
@Operation(summary = "修改密码")
@PutMapping(value = "/password")
public Result<?> changePassword(
@RequestBody PasswordChangeForm data
) {
Long currUserId = SecurityUtils.getUserId();
boolean result = userService.changePassword(currUserId, data);
return Result.judge(result);
}
@Operation(summary = "发送短信/邮箱验证码")
@PostMapping(value = "/send-verification-code")
public Result<?> sendVerificationCode(
@Parameter(description = "联系方式(手机号码或邮箱地址)", required = true) @RequestParam String contact,
@Parameter(description = "联系方式类型(Mobile或Email)", required = true) @RequestParam ContactType contactType
) {
boolean result = userService.sendVerificationCode(contact, contactType);
return Result.judge(result);
}
@Operation(summary = "绑定个人中心用户手机号")
@PutMapping(value = "/mobile")
public Result<?> bindMobile(
@RequestBody @Validated MobileBindingForm data
) {
boolean result = userService.bindMobile(data);
return Result.judge(result);
}
@Operation(summary = "绑定个人中心用户邮箱")
@PutMapping(value = "/email")
public Result<?> bindEmail(
@RequestBody @Validated EmailChangeForm data
) {
boolean result = userService.bindEmail(data);
return Result.judge(result);
}
}
@@ -0,0 +1,61 @@
package com.youlai.boot.module.system.controller;
import com.youlai.boot.module.system.model.dto.ChatMessage;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.messaging.handler.annotation.DestinationVariable;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.security.Principal;
/**
* WebSocket 测试控制层
*
* @author Ray
* @since 2.3.0
*/
@RestController
@RequestMapping("/websocket")
@RequiredArgsConstructor
@Slf4j
public class WebsocketController {
private final SimpMessagingTemplate messagingTemplate;
/**
* 广播发送消息
*
* @param message 消息内容
*/
@MessageMapping("/sendToAll")
@SendTo("/topic/notice")
public String sendToAll(String message) {
return "服务端通知: " + message;
}
/**
* 点对点发送消息
* <p>
* 模拟 张三 给 李四 发送消息场景
*
* @param principal 当前用户
* @param username 接收消息的用户
* @param message 消息内容
*/
@MessageMapping("/sendToUser/{username}")
public void sendToUser(Principal principal, @DestinationVariable String username, String message) {
String sender = principal.getName(); // 发送人
String receiver = username; // 接收人
log.info("发送人:{}; 接收人:{}", sender, receiver);
// 发送消息给指定用户,拼接后路径 /user/{receiver}/queue/greeting
messagingTemplate.convertAndSendToUser(receiver, "/queue/greeting", new ChatMessage(sender, message));
}
}
@@ -0,0 +1,23 @@
package com.youlai.boot.module.system.converter;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.youlai.boot.module.system.model.entity.SysConfig;
import com.youlai.boot.module.system.model.vo.ConfigVO;
import com.youlai.boot.module.system.model.form.ConfigForm;
import org.mapstruct.Mapper;
/**
* 系统配置对象转换器
*
* @author Theo
* @since 2024-7-29 11:42:49
*/
@Mapper(componentModel = "spring")
public interface ConfigConverter {
Page<ConfigVO> toPageVo(Page<SysConfig> page);
SysConfig toEntity(ConfigForm configForm);
ConfigForm toForm(SysConfig entity);
}
@@ -0,0 +1,23 @@
package com.youlai.boot.module.system.converter;
import com.youlai.boot.module.system.model.entity.SysDept;
import com.youlai.boot.module.system.model.vo.DeptVO;
import com.youlai.boot.module.system.model.form.DeptForm;
import org.mapstruct.Mapper;
/**
* 部门对象转换器
*
* @author haoxr
* @since 2022/7/29
*/
@Mapper(componentModel = "spring")
public interface DeptConverter {
DeptForm toForm(SysDept entity);
DeptVO toVo(SysDept entity);
SysDept toEntity(DeptForm deptForm);
}
@@ -0,0 +1,23 @@
package com.youlai.boot.module.system.converter;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.youlai.boot.module.system.model.entity.SysDict;
import com.youlai.boot.module.system.model.vo.DictPageVO;
import com.youlai.boot.module.system.model.form.DictForm;
import org.mapstruct.Mapper;
/**
* 字典 对象转换器
*
* @author Ray Hao
* @since 2022/6/8
*/
@Mapper(componentModel = "spring")
public interface DictConverter {
Page<DictPageVO> toPageVo(Page<SysDict> page);
DictForm toForm(SysDict entity);
SysDict toEntity(DictForm entity);
}
@@ -0,0 +1,39 @@
package com.youlai.boot.module.system.converter;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.youlai.boot.module.system.model.entity.SysDictItem;
import com.youlai.boot.module.system.model.vo.DictPageVO;
import com.youlai.system.common.model.Option;
import com.youlai.boot.module.system.model.form.DictForm;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Mappings;
import java.util.List;
/**
* 字典项 对象转换器
*
* @author Ray
* @since 2022/6/8
*/
@Mapper(componentModel = "spring")
public interface DictItemConverter {
Page<DictPageVO> toPageVo(Page<SysDictItem> page);
DictForm toForm(SysDictItem entity);
SysDictItem toEntity(DictForm.DictItem dictItems);
List<SysDictItem> toEntity(List<DictForm.DictItem> dictItems);
DictForm.DictItem toDictItem(SysDictItem entity);
List<DictForm.DictItem> toDictItem(List<SysDictItem> entities);
@Mappings({
@Mapping(target = "value", source = "value"),
@Mapping(target = "label", source = "name")
})
Option<Long> toOption(SysDictItem dictItem);
List<Option<Long>> toOption(List<SysDictItem> dictItems);
}
@@ -0,0 +1,39 @@
package com.youlai.boot.module.system.converter;
import com.youlai.boot.module.system.model.entity.GenConfig;
import com.youlai.boot.module.system.model.entity.GenFieldConfig;
import com.youlai.boot.module.system.model.form.GenConfigForm;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import java.util.List;
/**
* 代码生成配置转换器
*
* @author Ray
* @since 2.10.0
*/
@Mapper(componentModel = "spring")
public interface GenConfigConverter {
@Mapping(source = "genConfig.tableName", target = "tableName")
@Mapping(source = "genConfig.businessName", target = "businessName")
@Mapping(source = "genConfig.moduleName", target = "moduleName")
@Mapping(source = "genConfig.packageName", target = "packageName")
@Mapping(source = "genConfig.entityName", target = "entityName")
@Mapping(source = "genConfig.author", target = "author")
@Mapping(source = "fieldConfigs", target = "fieldConfigs")
GenConfigForm toGenConfigForm(GenConfig genConfig, List<GenFieldConfig> fieldConfigs);
List<GenConfigForm.FieldConfig> toGenFieldConfigForm(List<GenFieldConfig> fieldConfigs);
GenConfigForm.FieldConfig toGenFieldConfigForm(GenFieldConfig genFieldConfig);
GenConfig toGenConfig(GenConfigForm formData);
List<GenFieldConfig> toGenFieldConfig(List<GenConfigForm.FieldConfig> fieldConfigs);
GenFieldConfig toGenFieldConfig(GenConfigForm.FieldConfig fieldConfig);
}
@@ -0,0 +1,26 @@
package com.youlai.boot.module.system.converter;
import com.youlai.boot.module.system.model.entity.SysMenu;
import com.youlai.boot.module.system.model.vo.MenuVO;
import com.youlai.boot.module.system.model.form.MenuForm;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
/**
* 菜单对象转换器
*
* @author Ray Hao
* @since 2024/5/26
*/
@Mapper(componentModel = "spring")
public interface MenuConverter {
MenuVO toVo(SysMenu entity);
@Mapping(target = "params", ignore = true)
MenuForm toForm(SysMenu entity);
@Mapping(target = "params", ignore = true)
SysMenu toEntity(MenuForm menuForm);
}
@@ -0,0 +1,36 @@
package com.youlai.boot.module.system.converter;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.youlai.boot.module.system.model.entity.SysRole;
import com.youlai.boot.module.system.model.vo.RolePageVO;
import com.youlai.system.common.model.Option;
import com.youlai.boot.module.system.model.form.RoleForm;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Mappings;
import java.util.List;
/**
* 角色对象转换器
*
* @author haoxr
* @since 2022/5/29
*/
@Mapper(componentModel = "spring")
public interface RoleConverter {
Page<RolePageVO> toPageVo(Page<SysRole> page);
@Mappings({
@Mapping(target = "value", source = "id"),
@Mapping(target = "label", source = "name")
})
Option<Long> entity2Option(SysRole role);
List<Option<Long>> entities2Options(List<SysRole> roles);
SysRole toEntity(RoleForm roleForm);
RoleForm toForm(SysRole entity);
}
@@ -0,0 +1,49 @@
package com.youlai.boot.module.system.converter;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.youlai.boot.module.system.model.entity.SysUser;
import com.youlai.boot.module.system.model.vo.UserInfoVO;
import com.youlai.boot.module.system.model.vo.UserPageVO;
import com.youlai.boot.module.system.model.vo.UserProfileVO;
import com.youlai.boot.module.system.model.bo.UserBO;
import com.youlai.boot.module.system.model.form.UserForm;
import com.youlai.boot.module.system.model.dto.UserImportDTO;
import com.youlai.boot.module.system.model.form.UserProfileForm;
import org.mapstruct.InheritInverseConfiguration;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Mappings;
/**
* 用户对象转换器
*
* @author haoxr
* @since 2022/6/8
*/
@Mapper(componentModel = "spring")
public interface UserConverter {
@Mappings({
@Mapping(target = "genderLabel", expression = "java(com.youlai.boot.common.base.IBaseEnum.getLabelByValue(bo.getGender(), com.youlai.boot.common.enums.GenderEnum.class))")
})
UserPageVO toPageVo(UserBO bo);
Page<UserPageVO> toPageVo(Page<UserBO> bo);
UserForm toForm(SysUser entity);
@InheritInverseConfiguration(name = "toForm")
SysUser toEntity(UserForm entity);
@Mappings({
@Mapping(target = "userId", source = "id")
})
UserInfoVO toUserInfoVo(SysUser entity);
SysUser toEntity(UserImportDTO vo);
UserProfileVO toProfileVO(UserBO bo);
SysUser toEntity(UserProfileForm formData);
}
Loaded 100 of 215 files, more files were not shown because too many files have changed in this diff. Show more