list = new ArrayList<>();
+ list.add("t1");
+ list.add("t2");
+ list.add("t3");
+ return list.stream();
+ }
+
+ @BeforeEach
+ public void testBeforeEach() {
+ System.out.println("@BeforeEach ==================");
+ }
+
+ @AfterEach
+ public void testAfterEach() {
+ System.out.println("@AfterEach ==================");
+ }
+
+
+}
diff --git a/ruoyi-admin/src/test/java/com/ruoyi/test/TagUnitTest.java b/ruoyi-admin/src/test/java/com/ruoyi/test/TagUnitTest.java
new file mode 100644
index 0000000..04240a0
--- /dev/null
+++ b/ruoyi-admin/src/test/java/com/ruoyi/test/TagUnitTest.java
@@ -0,0 +1,54 @@
+package com.ruoyi.test;
+
+import org.junit.jupiter.api.*;
+import org.springframework.boot.test.context.SpringBootTest;
+
+/**
+ * 标签单元测试案例
+ *
+ * @author Lion Li
+ */
+@SpringBootTest
+@DisplayName("标签单元测试案例")
+public class TagUnitTest {
+
+ @Tag("dev")
+ @DisplayName("测试 @Tag dev")
+ @Test
+ public void testTagDev() {
+ System.out.println("dev");
+ }
+
+ @Tag("prod")
+ @DisplayName("测试 @Tag prod")
+ @Test
+ public void testTagProd() {
+ System.out.println("prod");
+ }
+
+ @Tag("local")
+ @DisplayName("测试 @Tag local")
+ @Test
+ public void testTagLocal() {
+ System.out.println("local");
+ }
+
+ @Tag("exclude")
+ @DisplayName("测试 @Tag exclude")
+ @Test
+ public void testTagExclude() {
+ System.out.println("exclude");
+ }
+
+ @BeforeEach
+ public void testBeforeEach() {
+ System.out.println("@BeforeEach ==================");
+ }
+
+ @AfterEach
+ public void testAfterEach() {
+ System.out.println("@AfterEach ==================");
+ }
+
+
+}
diff --git a/ruoyi-common/pom.xml b/ruoyi-common/pom.xml
new file mode 100644
index 0000000..e6e888e
--- /dev/null
+++ b/ruoyi-common/pom.xml
@@ -0,0 +1,164 @@
+
+
+
+ ruoyi-vue-plus
+ com.ruoyi
+ 4.6.0
+
+ 4.0.0
+
+ ruoyi-common
+
+
+ common通用工具
+
+
+
+
+
+
+ org.springframework
+ spring-context-support
+
+
+
+
+ org.springframework
+ spring-web
+
+
+
+
+ cn.dev33
+ sa-token-spring-boot-starter
+
+
+
+ cn.dev33
+ sa-token-jwt
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-validation
+
+
+
+
+ org.apache.commons
+ commons-lang3
+
+
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+
+
+
+ com.alibaba
+ easyexcel
+
+
+
+
+ org.yaml
+ snakeyaml
+
+
+
+
+ javax.servlet
+ javax.servlet-api
+
+
+
+ com.baomidou
+ mybatis-plus-boot-starter
+
+
+
+
+ com.baomidou
+ dynamic-datasource-spring-boot-starter
+
+
+
+ cn.hutool
+ hutool-core
+
+
+
+ cn.hutool
+ hutool-http
+
+
+
+ cn.hutool
+ hutool-captcha
+
+
+
+ cn.hutool
+ hutool-jwt
+
+
+
+ cn.hutool
+ hutool-extra
+
+
+
+ com.sun.mail
+ jakarta.mail
+
+
+
+ org.projectlombok
+ lombok
+
+
+
+ org.springdoc
+ springdoc-openapi-webmvc-core
+
+
+
+ org.springdoc
+ springdoc-openapi-javadoc
+
+
+
+
+ org.springframework.boot
+ spring-boot-configuration-processor
+
+
+
+
+ org.redisson
+ redisson-spring-boot-starter
+
+
+
+ org.redisson
+ redisson-spring-data-27
+
+
+
+ com.baomidou
+ lock4j-redisson-spring-boot-starter
+
+
+
+
+ org.bouncycastle
+ bcprov-jdk15to18
+
+
+
+
+
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/annotation/CellMerge.java b/ruoyi-common/src/main/java/com/ruoyi/common/annotation/CellMerge.java
new file mode 100644
index 0000000..4af822e
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/annotation/CellMerge.java
@@ -0,0 +1,24 @@
+package com.ruoyi.common.annotation;
+
+import com.ruoyi.common.excel.CellMergeStrategy;
+
+import java.lang.annotation.*;
+
+/**
+ * excel 列单元格合并(合并列相同项)
+ *
+ * 需搭配 {@link CellMergeStrategy} 策略使用
+ *
+ * @author Lion Li
+ */
+@Target(ElementType.FIELD)
+@Retention(RetentionPolicy.RUNTIME)
+@Inherited
+public @interface CellMerge {
+
+ /**
+ * col index
+ */
+ int index() default -1;
+
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/annotation/DataColumn.java b/ruoyi-common/src/main/java/com/ruoyi/common/annotation/DataColumn.java
new file mode 100644
index 0000000..df416ed
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/annotation/DataColumn.java
@@ -0,0 +1,28 @@
+package com.ruoyi.common.annotation;
+
+import java.lang.annotation.*;
+
+/**
+ * 数据权限
+ *
+ * 一个注解只能对应一个模板
+ *
+ * @author Lion Li
+ * @version 3.5.0
+ */
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface DataColumn {
+
+ /**
+ * 占位符关键字
+ */
+ String[] key() default "deptName";
+
+ /**
+ * 占位符替换值
+ */
+ String[] value() default "dept_id";
+
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/annotation/DataPermission.java b/ruoyi-common/src/main/java/com/ruoyi/common/annotation/DataPermission.java
new file mode 100644
index 0000000..73d9c03
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/annotation/DataPermission.java
@@ -0,0 +1,18 @@
+package com.ruoyi.common.annotation;
+
+import java.lang.annotation.*;
+
+/**
+ * 数据权限组
+ *
+ * @author Lion Li
+ * @version 3.5.0
+ */
+@Target({ElementType.METHOD, ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface DataPermission {
+
+ DataColumn[] value();
+
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/annotation/DictDataMapper.java b/ruoyi-common/src/main/java/com/ruoyi/common/annotation/DictDataMapper.java
new file mode 100644
index 0000000..564b2a4
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/annotation/DictDataMapper.java
@@ -0,0 +1,29 @@
+package com.ruoyi.common.annotation;
+
+import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import com.ruoyi.common.jackson.DictDataJsonSerializer;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * 字典数据映射注解
+ *
+ * @author itino
+ * @deprecated 建议使用通用翻译注解
+ */
+@Deprecated
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.FIELD, ElementType.METHOD})
+@JacksonAnnotationsInside
+@JsonSerialize(using = DictDataJsonSerializer.class)
+public @interface DictDataMapper {
+
+ /**
+ * 设置字典的type值 (如: sys_user_sex)
+ */
+ String dictType() default "";
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/annotation/EncryptField.java b/ruoyi-common/src/main/java/com/ruoyi/common/annotation/EncryptField.java
new file mode 100644
index 0000000..2d6a321
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/annotation/EncryptField.java
@@ -0,0 +1,44 @@
+package com.ruoyi.common.annotation;
+
+import com.ruoyi.common.enums.AlgorithmType;
+import com.ruoyi.common.enums.EncodeType;
+
+import java.lang.annotation.*;
+
+/**
+ * 字段加密注解
+ *
+ * @author 老马
+ */
+@Documented
+@Inherited
+@Target({ElementType.FIELD})
+@Retention(RetentionPolicy.RUNTIME)
+public @interface EncryptField {
+
+ /**
+ * 加密算法
+ */
+ AlgorithmType algorithm() default AlgorithmType.DEFAULT;
+
+ /**
+ * 秘钥。AES、SM4需要
+ */
+ String password() default "";
+
+ /**
+ * 公钥。RSA、SM2需要
+ */
+ String publicKey() default "";
+
+ /**
+ * 公钥。RSA、SM2需要
+ */
+ String privateKey() default "";
+
+ /**
+ * 编码方式。对加密算法为BASE64的不起作用
+ */
+ EncodeType encode() default EncodeType.DEFAULT;
+
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/annotation/ExcelDictFormat.java b/ruoyi-common/src/main/java/com/ruoyi/common/annotation/ExcelDictFormat.java
new file mode 100644
index 0000000..4bb0cc8
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/annotation/ExcelDictFormat.java
@@ -0,0 +1,32 @@
+package com.ruoyi.common.annotation;
+
+import com.ruoyi.common.utils.StringUtils;
+
+import java.lang.annotation.*;
+
+/**
+ * 字典格式化
+ *
+ * @author Lion Li
+ */
+@Target({ElementType.FIELD})
+@Retention(RetentionPolicy.RUNTIME)
+@Inherited
+public @interface ExcelDictFormat {
+
+ /**
+ * 如果是字典类型,请设置字典的type值 (如: sys_user_sex)
+ */
+ String dictType() default "";
+
+ /**
+ * 读取内容转表达式 (如: 0=男,1=女,2=未知)
+ */
+ String readConverterExp() default "";
+
+ /**
+ * 分隔符,读取字符串组内容
+ */
+ String separator() default StringUtils.SEPARATOR;
+
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/annotation/ExcelEnumFormat.java b/ruoyi-common/src/main/java/com/ruoyi/common/annotation/ExcelEnumFormat.java
new file mode 100644
index 0000000..a8f550a
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/annotation/ExcelEnumFormat.java
@@ -0,0 +1,30 @@
+package com.ruoyi.common.annotation;
+
+import java.lang.annotation.*;
+
+/**
+ * 枚举格式化
+ *
+ * @author Liang
+ */
+@Target({ElementType.FIELD})
+@Retention(RetentionPolicy.RUNTIME)
+@Inherited
+public @interface ExcelEnumFormat {
+
+ /**
+ * 字典枚举类型
+ */
+ Class extends Enum>> enumClass();
+
+ /**
+ * 字典枚举类中对应的code属性名称,默认为code
+ */
+ String codeField() default "code";
+
+ /**
+ * 字典枚举类中对应的text属性名称,默认为text
+ */
+ String textField() default "text";
+
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/annotation/Log.java b/ruoyi-common/src/main/java/com/ruoyi/common/annotation/Log.java
new file mode 100644
index 0000000..f9ba001
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/annotation/Log.java
@@ -0,0 +1,47 @@
+package com.ruoyi.common.annotation;
+
+import com.ruoyi.common.enums.BusinessType;
+import com.ruoyi.common.enums.OperatorType;
+
+import java.lang.annotation.*;
+
+/**
+ * 自定义操作日志记录注解
+ *
+ * @author ruoyi
+ */
+@Target({ElementType.PARAMETER, ElementType.METHOD})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface Log {
+ /**
+ * 模块
+ */
+ String title() default "";
+
+ /**
+ * 功能
+ */
+ BusinessType businessType() default BusinessType.OTHER;
+
+ /**
+ * 操作人类别
+ */
+ OperatorType operatorType() default OperatorType.MANAGE;
+
+ /**
+ * 是否保存请求的参数
+ */
+ boolean isSaveRequestData() default true;
+
+ /**
+ * 是否保存响应的参数
+ */
+ boolean isSaveResponseData() default true;
+
+ /**
+ * 排除指定的请求参数
+ */
+ String[] excludeParamNames() default {};
+
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/annotation/RateLimiter.java b/ruoyi-common/src/main/java/com/ruoyi/common/annotation/RateLimiter.java
new file mode 100644
index 0000000..6eeb2f2
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/annotation/RateLimiter.java
@@ -0,0 +1,41 @@
+package com.ruoyi.common.annotation;
+
+import com.ruoyi.common.enums.LimitType;
+
+import java.lang.annotation.*;
+
+/**
+ * 限流注解
+ *
+ * @author Lion Li
+ */
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface RateLimiter {
+ /**
+ * 限流key,支持使用Spring el表达式来动态获取方法上的参数值
+ * 格式类似于 #code.id #{#code}
+ */
+ String key() default "";
+
+ /**
+ * 限流时间,单位秒
+ */
+ int time() default 60;
+
+ /**
+ * 限流次数
+ */
+ int count() default 100;
+
+ /**
+ * 限流类型
+ */
+ LimitType limitType() default LimitType.DEFAULT;
+
+ /**
+ * 提示消息 支持国际化 格式为 {code}
+ */
+ String message() default "{rate.limiter.message}";
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/annotation/RepeatSubmit.java b/ruoyi-common/src/main/java/com/ruoyi/common/annotation/RepeatSubmit.java
new file mode 100644
index 0000000..d30962d
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/annotation/RepeatSubmit.java
@@ -0,0 +1,29 @@
+package com.ruoyi.common.annotation;
+
+import java.lang.annotation.*;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 自定义注解防止表单重复提交
+ *
+ * @author Lion Li
+ */
+@Inherited
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface RepeatSubmit {
+
+ /**
+ * 间隔时间(ms),小于此时间视为重复提交
+ */
+ int interval() default 5000;
+
+ TimeUnit timeUnit() default TimeUnit.MILLISECONDS;
+
+ /**
+ * 提示消息 支持国际化 格式为 {code}
+ */
+ String message() default "{repeat.submit.message}";
+
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/annotation/Sensitive.java b/ruoyi-common/src/main/java/com/ruoyi/common/annotation/Sensitive.java
new file mode 100644
index 0000000..2ad9777
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/annotation/Sensitive.java
@@ -0,0 +1,24 @@
+package com.ruoyi.common.annotation;
+
+import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import com.ruoyi.common.enums.SensitiveStrategy;
+import com.ruoyi.common.jackson.SensitiveJsonSerializer;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * 数据脱敏注解
+ *
+ * @author zhujie
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.FIELD)
+@JacksonAnnotationsInside
+@JsonSerialize(using = SensitiveJsonSerializer.class)
+public @interface Sensitive {
+ SensitiveStrategy strategy();
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/annotation/Translation.java b/ruoyi-common/src/main/java/com/ruoyi/common/annotation/Translation.java
new file mode 100644
index 0000000..ba8cd22
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/annotation/Translation.java
@@ -0,0 +1,39 @@
+package com.ruoyi.common.annotation;
+
+import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import com.ruoyi.common.translation.handler.TranslationHandler;
+
+import java.lang.annotation.*;
+
+/**
+ * 通用翻译注解
+ *
+ * @author Lion Li
+ */
+@Inherited
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.FIELD, ElementType.METHOD})
+@Documented
+@JacksonAnnotationsInside
+@JsonSerialize(using = TranslationHandler.class)
+public @interface Translation {
+
+ /**
+ * 类型 (需与实现类上的 {@link com.ruoyi.common.annotation.TranslationType} 注解type对应)
+ *
+ * 默认取当前字段的值 如果设置了 @{@link Translation#mapper()} 则取映射字段的值
+ */
+ String type();
+
+ /**
+ * 映射字段 (如果不为空则取此字段的值)
+ */
+ String mapper() default "";
+
+ /**
+ * 其他条件 例如: 字典type(sys_user_sex)
+ */
+ String other() default "";
+
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/annotation/TranslationType.java b/ruoyi-common/src/main/java/com/ruoyi/common/annotation/TranslationType.java
new file mode 100644
index 0000000..f592f6d
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/annotation/TranslationType.java
@@ -0,0 +1,21 @@
+package com.ruoyi.common.annotation;
+
+import java.lang.annotation.*;
+
+/**
+ * 翻译类型注解 (标注到{@link com.ruoyi.common.translation.TranslationInterface} 的实现类)
+ *
+ * @author Lion Li
+ */
+@Inherited
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.TYPE})
+@Documented
+public @interface TranslationType {
+
+ /**
+ * 类型
+ */
+ String type();
+
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/captcha/UnsignedMathGenerator.java b/ruoyi-common/src/main/java/com/ruoyi/common/captcha/UnsignedMathGenerator.java
new file mode 100644
index 0000000..5dd00ec
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/captcha/UnsignedMathGenerator.java
@@ -0,0 +1,85 @@
+package com.ruoyi.common.captcha;
+
+import cn.hutool.captcha.generator.CodeGenerator;
+import cn.hutool.core.math.Calculator;
+import cn.hutool.core.util.CharUtil;
+import cn.hutool.core.util.RandomUtil;
+import com.ruoyi.common.utils.StringUtils;
+
+/**
+ * 无符号计算生成器
+ *
+ * @author Lion Li
+ */
+public class UnsignedMathGenerator implements CodeGenerator {
+
+ private static final long serialVersionUID = -5514819971774091076L;
+
+ private static final String OPERATORS = "+-*";
+
+ /**
+ * 参与计算数字最大长度
+ */
+ private final int numberLength;
+
+ /**
+ * 构造
+ */
+ public UnsignedMathGenerator() {
+ this(2);
+ }
+
+ /**
+ * 构造
+ *
+ * @param numberLength 参与计算最大数字位数
+ */
+ public UnsignedMathGenerator(int numberLength) {
+ this.numberLength = numberLength;
+ }
+
+ @Override
+ public String generate() {
+ final int limit = getLimit();
+ int a = RandomUtil.randomInt(limit);
+ int b = RandomUtil.randomInt(limit);
+ String max = Integer.toString(Math.max(a,b));
+ String min = Integer.toString(Math.min(a,b));
+ max = StringUtils.rightPad(max, this.numberLength, CharUtil.SPACE);
+ min = StringUtils.rightPad(min, this.numberLength, CharUtil.SPACE);
+
+ return max + RandomUtil.randomChar(OPERATORS) + min + '=';
+ }
+
+ @Override
+ public boolean verify(String code, String userInputCode) {
+ int result;
+ try {
+ result = Integer.parseInt(userInputCode);
+ } catch (NumberFormatException e) {
+ // 用户输入非数字
+ return false;
+ }
+
+ final int calculateResult = (int) Calculator.conversion(code);
+ return result == calculateResult;
+ }
+
+ /**
+ * 获取验证码长度
+ *
+ * @return 验证码长度
+ */
+ public int getLength() {
+ return this.numberLength * 2 + 2;
+ }
+
+ /**
+ * 根据长度获取参与计算数字最大值
+ *
+ * @return 最大值
+ */
+ private int getLimit() {
+ return Integer.parseInt("1" + StringUtils.repeat('0', this.numberLength));
+ }
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/config/RuoYiConfig.java b/ruoyi-common/src/main/java/com/ruoyi/common/config/RuoYiConfig.java
new file mode 100644
index 0000000..8ce7a8c
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/config/RuoYiConfig.java
@@ -0,0 +1,54 @@
+package com.ruoyi.common.config;
+
+import lombok.Data;
+import lombok.Getter;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+/**
+ * 读取项目相关配置
+ *
+ * @author Lion Li
+ */
+
+@Data
+@Component
+@ConfigurationProperties(prefix = "ruoyi")
+public class RuoYiConfig {
+
+ /**
+ * 项目名称
+ */
+ private String name;
+
+ /**
+ * 版本
+ */
+ private String version;
+
+ /**
+ * 版权年份
+ */
+ private String copyrightYear;
+
+ /**
+ * 实例演示开关
+ */
+ private boolean demoEnabled;
+
+ /**
+ * 缓存懒加载
+ */
+ private boolean cacheLazy;
+
+ /**
+ * 获取地址开关
+ */
+ @Getter
+ private static boolean addressEnabled;
+
+ public void setAddressEnabled(boolean addressEnabled) {
+ RuoYiConfig.addressEnabled = addressEnabled;
+ }
+
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/constant/CacheConstants.java b/ruoyi-common/src/main/java/com/ruoyi/common/constant/CacheConstants.java
new file mode 100644
index 0000000..1cdf07e
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/constant/CacheConstants.java
@@ -0,0 +1,49 @@
+package com.ruoyi.common.constant;
+
+/**
+ * 缓存的key 常量
+ *
+ * @author ruoyi
+ */
+public interface CacheConstants {
+
+ /**
+ * 登录用户 redis key
+ */
+ String LOGIN_TOKEN_KEY = "Authorization:login:token:";
+
+ /**
+ * 在线用户 redis key
+ */
+ String ONLINE_TOKEN_KEY = "online_tokens:";
+
+ /**
+ * 验证码 redis key
+ */
+ String CAPTCHA_CODE_KEY = "captcha_codes:";
+
+ /**
+ * 参数管理 cache key
+ */
+ String SYS_CONFIG_KEY = "sys_config:";
+
+ /**
+ * 字典管理 cache key
+ */
+ String SYS_DICT_KEY = "sys_dict:";
+
+ /**
+ * 防重提交 redis key
+ */
+ String REPEAT_SUBMIT_KEY = "repeat_submit:";
+
+ /**
+ * 限流 redis key
+ */
+ String RATE_LIMIT_KEY = "rate_limit:";
+
+ /**
+ * 登录账户密码错误次数 redis key
+ */
+ String PWD_ERR_CNT_KEY = "pwd_err_cnt:";
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/constant/CacheNames.java b/ruoyi-common/src/main/java/com/ruoyi/common/constant/CacheNames.java
new file mode 100644
index 0000000..1396b6d
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/constant/CacheNames.java
@@ -0,0 +1,58 @@
+package com.ruoyi.common.constant;
+
+/**
+ * 缓存组名称常量
+ *
+ * key 格式为 cacheNames#ttl#maxIdleTime#maxSize
+ *
+ * ttl 过期时间 如果设置为0则不过期 默认为0
+ * maxIdleTime 最大空闲时间 根据LRU算法清理空闲数据 如果设置为0则不检测 默认为0
+ * maxSize 组最大长度 根据LRU算法清理溢出数据 如果设置为0则无限长 默认为0
+ *
+ * 例子: test#60s、test#0#60s、test#0#1m#1000、test#1h#0#500
+ *
+ * @author Lion Li
+ */
+public interface CacheNames {
+
+ /**
+ * 演示案例
+ */
+ String DEMO_CACHE = "demo:cache#60s#10m#20";
+
+ /**
+ * 系统配置
+ */
+ String SYS_CONFIG = "sys_config";
+
+ /**
+ * 数据字典
+ */
+ String SYS_DICT = "sys_dict";
+
+ /**
+ * 用户账户
+ */
+ String SYS_USER_NAME = "sys_user_name#30d";
+
+ /**
+ * 部门
+ */
+ String SYS_DEPT = "sys_dept#30d";
+
+ /**
+ * OSS内容
+ */
+ String SYS_OSS = "sys_oss#30d";
+
+ /**
+ * OSS配置
+ */
+ String SYS_OSS_CONFIG = "sys_oss_config";
+
+ /**
+ * 在线用户
+ */
+ String ONLINE_TOKEN = "online_tokens";
+
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/constant/Constants.java b/ruoyi-common/src/main/java/com/ruoyi/common/constant/Constants.java
new file mode 100644
index 0000000..e634ed2
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/constant/Constants.java
@@ -0,0 +1,76 @@
+package com.ruoyi.common.constant;
+
+/**
+ * 通用常量信息
+ *
+ * @author ruoyi
+ */
+public interface Constants {
+
+ /**
+ * UTF-8 字符集
+ */
+ String UTF8 = "UTF-8";
+
+ /**
+ * GBK 字符集
+ */
+ String GBK = "GBK";
+
+ /**
+ * www主域
+ */
+ String WWW = "www.";
+
+ /**
+ * http请求
+ */
+ String HTTP = "http://";
+
+ /**
+ * https请求
+ */
+ String HTTPS = "https://";
+
+ /**
+ * 通用成功标识
+ */
+ String SUCCESS = "0";
+
+ /**
+ * 通用失败标识
+ */
+ String FAIL = "1";
+
+ /**
+ * 登录成功
+ */
+ String LOGIN_SUCCESS = "Success";
+
+ /**
+ * 注销
+ */
+ String LOGOUT = "Logout";
+
+ /**
+ * 注册
+ */
+ String REGISTER = "Register";
+
+ /**
+ * 登录失败
+ */
+ String LOGIN_FAIL = "Error";
+
+ /**
+ * 验证码有效期(分钟)
+ */
+ Integer CAPTCHA_EXPIRATION = 2;
+
+ /**
+ * 令牌
+ */
+ String TOKEN = "token";
+
+}
+
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/constant/GenConstants.java b/ruoyi-common/src/main/java/com/ruoyi/common/constant/GenConstants.java
new file mode 100644
index 0000000..bb08d50
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/constant/GenConstants.java
@@ -0,0 +1,193 @@
+package com.ruoyi.common.constant;
+
+/**
+ * 代码生成通用常量
+ *
+ * @author ruoyi
+ */
+public interface GenConstants {
+ /**
+ * 单表(增删改查)
+ */
+ String TPL_CRUD = "crud";
+
+ /**
+ * 树表(增删改查)
+ */
+ String TPL_TREE = "tree";
+
+ /**
+ * 主子表(增删改查)
+ */
+ String TPL_SUB = "sub";
+
+ /**
+ * 树编码字段
+ */
+ String TREE_CODE = "treeCode";
+
+ /**
+ * 树父编码字段
+ */
+ String TREE_PARENT_CODE = "treeParentCode";
+
+ /**
+ * 树名称字段
+ */
+ String TREE_NAME = "treeName";
+
+ /**
+ * 上级菜单ID字段
+ */
+ String PARENT_MENU_ID = "parentMenuId";
+
+ /**
+ * 上级菜单名称字段
+ */
+ String PARENT_MENU_NAME = "parentMenuName";
+
+ /**
+ * 数据库字符串类型
+ */
+ String[] COLUMNTYPE_STR = {"char", "varchar", "nvarchar", "varchar2"};
+
+ /**
+ * 数据库文本类型
+ */
+ String[] COLUMNTYPE_TEXT = {"tinytext", "text", "mediumtext", "longtext"};
+
+ /**
+ * 数据库时间类型
+ */
+ String[] COLUMNTYPE_TIME = {"datetime", "time", "date", "timestamp"};
+
+ /**
+ * 数据库数字类型
+ */
+ String[] COLUMNTYPE_NUMBER = {"tinyint", "smallint", "mediumint", "int", "number", "integer",
+ "bit", "bigint", "float", "double", "decimal"};
+
+ /**
+ * BO对象 不需要添加字段
+ */
+ String[] COLUMNNAME_NOT_ADD = {"create_by", "create_time", "del_flag", "update_by",
+ "update_time", "version"};
+
+ /**
+ * BO对象 不需要编辑字段
+ */
+ String[] COLUMNNAME_NOT_EDIT = {"create_by", "create_time", "del_flag", "update_by",
+ "update_time", "version"};
+
+ /**
+ * VO对象 不需要返回字段
+ */
+ String[] COLUMNNAME_NOT_LIST = {"create_by", "create_time", "del_flag", "update_by",
+ "update_time", "version"};
+
+ /**
+ * BO对象 不需要查询字段
+ */
+ String[] COLUMNNAME_NOT_QUERY = {"id", "create_by", "create_time", "del_flag", "update_by",
+ "update_time", "remark", "version"};
+
+ /**
+ * Entity基类字段
+ */
+ String[] BASE_ENTITY = {"createBy", "createTime", "updateBy", "updateTime"};
+
+ /**
+ * Tree基类字段
+ */
+ String[] TREE_ENTITY = {"parentName", "parentId", "children"};
+
+ /**
+ * 文本框
+ */
+ String HTML_INPUT = "input";
+
+ /**
+ * 文本域
+ */
+ String HTML_TEXTAREA = "textarea";
+
+ /**
+ * 下拉框
+ */
+ String HTML_SELECT = "select";
+
+ /**
+ * 单选框
+ */
+ String HTML_RADIO = "radio";
+
+ /**
+ * 复选框
+ */
+ String HTML_CHECKBOX = "checkbox";
+
+ /**
+ * 日期控件
+ */
+ String HTML_DATETIME = "datetime";
+
+ /**
+ * 图片上传控件
+ */
+ String HTML_IMAGE_UPLOAD = "imageUpload";
+
+ /**
+ * 文件上传控件
+ */
+ String HTML_FILE_UPLOAD = "fileUpload";
+
+ /**
+ * 富文本控件
+ */
+ String HTML_EDITOR = "editor";
+
+ /**
+ * 字符串类型
+ */
+ String TYPE_STRING = "String";
+
+ /**
+ * 整型
+ */
+ String TYPE_INTEGER = "Integer";
+
+ /**
+ * 长整型
+ */
+ String TYPE_LONG = "Long";
+
+ /**
+ * 浮点型
+ */
+ String TYPE_DOUBLE = "Double";
+
+ /**
+ * 高精度计算类型
+ */
+ String TYPE_BIGDECIMAL = "BigDecimal";
+
+ /**
+ * 时间类型
+ */
+ String TYPE_DATE = "Date";
+
+ /**
+ * 模糊查询
+ */
+ String QUERY_LIKE = "LIKE";
+
+ /**
+ * 相等查询
+ */
+ String QUERY_EQ = "EQ";
+
+ /**
+ * 需要
+ */
+ String REQUIRE = "1";
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/constant/HttpStatus.java b/ruoyi-common/src/main/java/com/ruoyi/common/constant/HttpStatus.java
new file mode 100644
index 0000000..f007b8c
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/constant/HttpStatus.java
@@ -0,0 +1,93 @@
+package com.ruoyi.common.constant;
+
+/**
+ * 返回状态码
+ *
+ * @author Lion Li
+ */
+public interface HttpStatus {
+ /**
+ * 操作成功
+ */
+ int SUCCESS = 200;
+
+ /**
+ * 对象创建成功
+ */
+ int CREATED = 201;
+
+ /**
+ * 请求已经被接受
+ */
+ int ACCEPTED = 202;
+
+ /**
+ * 操作已经执行成功,但是没有返回数据
+ */
+ int NO_CONTENT = 204;
+
+ /**
+ * 资源已被移除
+ */
+ int MOVED_PERM = 301;
+
+ /**
+ * 重定向
+ */
+ int SEE_OTHER = 303;
+
+ /**
+ * 资源没有被修改
+ */
+ int NOT_MODIFIED = 304;
+
+ /**
+ * 参数列表错误(缺少,格式不匹配)
+ */
+ int BAD_REQUEST = 400;
+
+ /**
+ * 未授权
+ */
+ int UNAUTHORIZED = 401;
+
+ /**
+ * 访问受限,授权过期
+ */
+ int FORBIDDEN = 403;
+
+ /**
+ * 资源,服务未找到
+ */
+ int NOT_FOUND = 404;
+
+ /**
+ * 不允许的http方法
+ */
+ int BAD_METHOD = 405;
+
+ /**
+ * 资源冲突,或者资源被锁
+ */
+ int CONFLICT = 409;
+
+ /**
+ * 不支持的数据,媒体类型
+ */
+ int UNSUPPORTED_TYPE = 415;
+
+ /**
+ * 系统内部错误
+ */
+ int ERROR = 500;
+
+ /**
+ * 接口未实现
+ */
+ int NOT_IMPLEMENTED = 501;
+
+ /**
+ * 系统警告消息
+ */
+ int WARN = 601;
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/constant/TransConstant.java b/ruoyi-common/src/main/java/com/ruoyi/common/constant/TransConstant.java
new file mode 100644
index 0000000..5c5e5f8
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/constant/TransConstant.java
@@ -0,0 +1,30 @@
+package com.ruoyi.common.constant;
+
+/**
+ * 翻译常量
+ *
+ * @author Lion Li
+ */
+public interface TransConstant {
+
+ /**
+ * 用户id转账号
+ */
+ String USER_ID_TO_NAME = "user_id_to_name";
+
+ /**
+ * 部门id转名称
+ */
+ String DEPT_ID_TO_NAME = "dept_id_to_name";
+
+ /**
+ * 字典type转label
+ */
+ String DICT_TYPE_TO_LABEL = "dict_type_to_label";
+
+ /**
+ * ossId转url
+ */
+ String OSS_ID_TO_URL = "oss_id_to_url";
+
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/constant/UserConstants.java b/ruoyi-common/src/main/java/com/ruoyi/common/constant/UserConstants.java
new file mode 100644
index 0000000..4a095fa
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/constant/UserConstants.java
@@ -0,0 +1,132 @@
+package com.ruoyi.common.constant;
+
+/**
+ * 用户常量信息
+ *
+ * @author ruoyi
+ */
+public interface UserConstants {
+
+ /**
+ * 平台内系统用户的唯一标志
+ */
+ String SYS_USER = "SYS_USER";
+
+ /**
+ * 正常状态
+ */
+ String NORMAL = "0";
+
+ /**
+ * 异常状态
+ */
+ String EXCEPTION = "1";
+
+ /**
+ * 用户正常状态
+ */
+ String USER_NORMAL = "0";
+
+ /**
+ * 用户封禁状态
+ */
+ String USER_DISABLE = "1";
+
+ /**
+ * 角色正常状态
+ */
+ String ROLE_NORMAL = "0";
+
+ /**
+ * 角色封禁状态
+ */
+ String ROLE_DISABLE = "1";
+
+ /**
+ * 部门正常状态
+ */
+ String DEPT_NORMAL = "0";
+
+ /**
+ * 部门停用状态
+ */
+ String DEPT_DISABLE = "1";
+
+ /**
+ * 字典正常状态
+ */
+ String DICT_NORMAL = "0";
+
+ /**
+ * 是否为系统默认(是)
+ */
+ String YES = "Y";
+
+ /**
+ * 是否菜单外链(是)
+ */
+ String YES_FRAME = "0";
+
+ /**
+ * 是否菜单外链(否)
+ */
+ String NO_FRAME = "1";
+
+ /**
+ * 菜单正常状态
+ */
+ String MENU_NORMAL = "0";
+
+ /**
+ * 菜单停用状态
+ */
+ String MENU_DISABLE = "1";
+
+ /**
+ * 菜单类型(目录)
+ */
+ String TYPE_DIR = "M";
+
+ /**
+ * 菜单类型(菜单)
+ */
+ String TYPE_MENU = "C";
+
+ /**
+ * 菜单类型(按钮)
+ */
+ String TYPE_BUTTON = "F";
+
+ /**
+ * Layout组件标识
+ */
+ String LAYOUT = "Layout";
+
+ /**
+ * ParentView组件标识
+ */
+ String PARENT_VIEW = "ParentView";
+
+ /**
+ * InnerLink组件标识
+ */
+ String INNER_LINK = "InnerLink";
+
+ /**
+ * 用户名长度限制
+ */
+ int USERNAME_MIN_LENGTH = 2;
+ int USERNAME_MAX_LENGTH = 20;
+
+ /**
+ * 密码长度限制
+ */
+ int PASSWORD_MIN_LENGTH = 5;
+ int PASSWORD_MAX_LENGTH = 20;
+
+ /**
+ * 管理员ID
+ */
+ Long ADMIN_ID = 1L;
+
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/convert/ExcelBigNumberConvert.java b/ruoyi-common/src/main/java/com/ruoyi/common/convert/ExcelBigNumberConvert.java
new file mode 100644
index 0000000..432ab74
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/convert/ExcelBigNumberConvert.java
@@ -0,0 +1,52 @@
+package com.ruoyi.common.convert;
+
+import cn.hutool.core.convert.Convert;
+import cn.hutool.core.util.ObjectUtil;
+import com.alibaba.excel.converters.Converter;
+import com.alibaba.excel.enums.CellDataTypeEnum;
+import com.alibaba.excel.metadata.GlobalConfiguration;
+import com.alibaba.excel.metadata.data.ReadCellData;
+import com.alibaba.excel.metadata.data.WriteCellData;
+import com.alibaba.excel.metadata.property.ExcelContentProperty;
+import lombok.extern.slf4j.Slf4j;
+
+import java.math.BigDecimal;
+
+/**
+ * 大数值转换
+ * Excel 数值长度位15位 大于15位的数值转换位字符串
+ *
+ * @author Lion Li
+ */
+@Slf4j
+public class ExcelBigNumberConvert implements Converter {
+
+ @Override
+ public Class supportJavaTypeKey() {
+ return Long.class;
+ }
+
+ @Override
+ public CellDataTypeEnum supportExcelTypeKey() {
+ return CellDataTypeEnum.STRING;
+ }
+
+ @Override
+ public Long convertToJavaData(ReadCellData> cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) {
+ return Convert.toLong(cellData.getData());
+ }
+
+ @Override
+ public WriteCellData convertToExcelData(Long object, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) {
+ if (ObjectUtil.isNotNull(object)) {
+ String str = Convert.toStr(object);
+ if (str.length() > 15) {
+ return new WriteCellData<>(str);
+ }
+ }
+ WriteCellData cellData = new WriteCellData<>(new BigDecimal(object));
+ cellData.setType(CellDataTypeEnum.NUMBER);
+ return cellData;
+ }
+
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/convert/ExcelDictConvert.java b/ruoyi-common/src/main/java/com/ruoyi/common/convert/ExcelDictConvert.java
new file mode 100644
index 0000000..03dad1f
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/convert/ExcelDictConvert.java
@@ -0,0 +1,73 @@
+package com.ruoyi.common.convert;
+
+import cn.hutool.core.annotation.AnnotationUtil;
+import cn.hutool.core.convert.Convert;
+import cn.hutool.core.util.ObjectUtil;
+import com.alibaba.excel.converters.Converter;
+import com.alibaba.excel.enums.CellDataTypeEnum;
+import com.alibaba.excel.metadata.GlobalConfiguration;
+import com.alibaba.excel.metadata.data.ReadCellData;
+import com.alibaba.excel.metadata.data.WriteCellData;
+import com.alibaba.excel.metadata.property.ExcelContentProperty;
+import com.ruoyi.common.annotation.ExcelDictFormat;
+import com.ruoyi.common.core.service.DictService;
+import com.ruoyi.common.utils.StringUtils;
+import com.ruoyi.common.utils.poi.ExcelUtil;
+import com.ruoyi.common.utils.spring.SpringUtils;
+import lombok.extern.slf4j.Slf4j;
+
+import java.lang.reflect.Field;
+
+/**
+ * 字典格式化转换处理
+ *
+ * @author Lion Li
+ */
+@Slf4j
+public class ExcelDictConvert implements Converter {
+
+ @Override
+ public Class supportJavaTypeKey() {
+ return Object.class;
+ }
+
+ @Override
+ public CellDataTypeEnum supportExcelTypeKey() {
+ return null;
+ }
+
+ @Override
+ public Object convertToJavaData(ReadCellData> cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) {
+ ExcelDictFormat anno = getAnnotation(contentProperty.getField());
+ String type = anno.dictType();
+ String label = cellData.getStringValue();
+ String value;
+ if (StringUtils.isBlank(type)) {
+ value = ExcelUtil.reverseByExp(label, anno.readConverterExp(), anno.separator());
+ } else {
+ value = SpringUtils.getBean(DictService.class).getDictValue(type, label, anno.separator());
+ }
+ return Convert.convert(contentProperty.getField().getType(), value);
+ }
+
+ @Override
+ public WriteCellData convertToExcelData(Object object, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) {
+ if (ObjectUtil.isNull(object)) {
+ return new WriteCellData<>("");
+ }
+ ExcelDictFormat anno = getAnnotation(contentProperty.getField());
+ String type = anno.dictType();
+ String value = Convert.toStr(object);
+ String label;
+ if (StringUtils.isBlank(type)) {
+ label = ExcelUtil.convertByExp(value, anno.readConverterExp(), anno.separator());
+ } else {
+ label = SpringUtils.getBean(DictService.class).getDictLabel(type, value, anno.separator());
+ }
+ return new WriteCellData<>(label);
+ }
+
+ private ExcelDictFormat getAnnotation(Field field) {
+ return AnnotationUtil.getAnnotation(field, ExcelDictFormat.class);
+ }
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/convert/ExcelEnumConvert.java b/ruoyi-common/src/main/java/com/ruoyi/common/convert/ExcelEnumConvert.java
new file mode 100644
index 0000000..c69d90c
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/convert/ExcelEnumConvert.java
@@ -0,0 +1,75 @@
+package com.ruoyi.common.convert;
+
+import cn.hutool.core.annotation.AnnotationUtil;
+import cn.hutool.core.convert.Convert;
+import cn.hutool.core.util.ObjectUtil;
+import com.alibaba.excel.converters.Converter;
+import com.alibaba.excel.enums.CellDataTypeEnum;
+import com.alibaba.excel.metadata.GlobalConfiguration;
+import com.alibaba.excel.metadata.data.ReadCellData;
+import com.alibaba.excel.metadata.data.WriteCellData;
+import com.alibaba.excel.metadata.property.ExcelContentProperty;
+import com.ruoyi.common.annotation.ExcelEnumFormat;
+import com.ruoyi.common.utils.reflect.ReflectUtils;
+import lombok.extern.slf4j.Slf4j;
+
+import java.lang.reflect.Field;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 枚举格式化转换处理
+ *
+ * @author Liang
+ */
+@Slf4j
+public class ExcelEnumConvert implements Converter {
+
+ @Override
+ public Class supportJavaTypeKey() {
+ return Object.class;
+ }
+
+ @Override
+ public CellDataTypeEnum supportExcelTypeKey() {
+ return null;
+ }
+
+ @Override
+ public Object convertToJavaData(ReadCellData> cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) {
+ Object codeValue = cellData.getData();
+ // 如果是空值
+ if (ObjectUtil.isNull(codeValue)) {
+ return null;
+ }
+ Map enumValueMap = beforeConvert(contentProperty);
+ String textValue = enumValueMap.get(codeValue);
+ return Convert.convert(contentProperty.getField().getType(), textValue);
+ }
+
+ @Override
+ public WriteCellData convertToExcelData(Object object, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) {
+ if (ObjectUtil.isNull(object)) {
+ return new WriteCellData<>("");
+ }
+ Map enumValueMap = beforeConvert(contentProperty);
+ String value = Convert.toStr(enumValueMap.get(object), "");
+ return new WriteCellData<>(value);
+ }
+
+ private Map beforeConvert(ExcelContentProperty contentProperty) {
+ ExcelEnumFormat anno = getAnnotation(contentProperty.getField());
+ Map enumValueMap = new HashMap<>();
+ Enum>[] enumConstants = anno.enumClass().getEnumConstants();
+ for (Enum> enumConstant : enumConstants) {
+ Object codeValue = ReflectUtils.invokeGetter(enumConstant, anno.codeField());
+ String textValue = ReflectUtils.invokeGetter(enumConstant, anno.textField());
+ enumValueMap.put(codeValue, textValue);
+ }
+ return enumValueMap;
+ }
+
+ private ExcelEnumFormat getAnnotation(Field field) {
+ return AnnotationUtil.getAnnotation(field, ExcelEnumFormat.class);
+ }
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/core/controller/BaseController.java b/ruoyi-common/src/main/java/com/ruoyi/common/core/controller/BaseController.java
new file mode 100644
index 0000000..1e0a5d5
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/core/controller/BaseController.java
@@ -0,0 +1,69 @@
+package com.ruoyi.common.core.controller;
+
+import com.ruoyi.common.core.domain.R;
+import com.ruoyi.common.core.domain.model.LoginUser;
+import com.ruoyi.common.helper.LoginHelper;
+import com.ruoyi.common.utils.StringUtils;
+
+/**
+ * web层通用数据处理
+ *
+ * @author Lion Li
+ */
+public class BaseController {
+
+ /**
+ * 响应返回结果
+ *
+ * @param rows 影响行数
+ * @return 操作结果
+ */
+ protected R toAjax(int rows) {
+ return rows > 0 ? R.ok() : R.fail();
+ }
+
+ /**
+ * 响应返回结果
+ *
+ * @param result 结果
+ * @return 操作结果
+ */
+ protected R toAjax(boolean result) {
+ return result ? R.ok() : R.fail();
+ }
+
+ /**
+ * 页面跳转
+ */
+ public String redirect(String url) {
+ return StringUtils.format("redirect:{}", url);
+ }
+
+ /**
+ * 获取用户缓存信息
+ */
+ public LoginUser getLoginUser() {
+ return LoginHelper.getLoginUser();
+ }
+
+ /**
+ * 获取登录用户id
+ */
+ public Long getUserId() {
+ return LoginHelper.getUserId();
+ }
+
+ /**
+ * 获取登录部门id
+ */
+ public Long getDeptId() {
+ return LoginHelper.getDeptId();
+ }
+
+ /**
+ * 获取登录用户名
+ */
+ public String getUsername() {
+ return LoginHelper.getUsername();
+ }
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/BaseEntity.java b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/BaseEntity.java
new file mode 100644
index 0000000..bd31607
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/BaseEntity.java
@@ -0,0 +1,63 @@
+package com.ruoyi.common.core.domain;
+
+import com.baomidou.mybatisplus.annotation.FieldFill;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Entity基类
+ *
+ * @author Lion Li
+ */
+
+@Data
+public class BaseEntity implements Serializable {
+
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 搜索值
+ */
+ @JsonIgnore
+ @TableField(exist = false)
+ private String searchValue;
+
+ /**
+ * 创建者
+ */
+ @TableField(fill = FieldFill.INSERT)
+ private String createBy;
+
+ /**
+ * 创建时间
+ */
+ @TableField(fill = FieldFill.INSERT)
+ private Date createTime;
+
+ /**
+ * 更新者
+ */
+ @TableField(fill = FieldFill.INSERT_UPDATE)
+ private String updateBy;
+
+ /**
+ * 更新时间
+ */
+ @TableField(fill = FieldFill.INSERT_UPDATE)
+ private Date updateTime;
+
+ /**
+ * 请求参数
+ */
+ @JsonInclude(JsonInclude.Include.NON_EMPTY)
+ @TableField(exist = false)
+ private Map params = new HashMap<>();
+
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/PageQuery.java b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/PageQuery.java
new file mode 100644
index 0000000..8a45905
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/PageQuery.java
@@ -0,0 +1,112 @@
+package com.ruoyi.common.core.domain;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.ObjectUtil;
+import com.baomidou.mybatisplus.core.metadata.OrderItem;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.ruoyi.common.exception.ServiceException;
+import com.ruoyi.common.utils.StringUtils;
+import com.ruoyi.common.utils.sql.SqlUtil;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 分页查询实体类
+ *
+ * @author Lion Li
+ */
+
+@Data
+public class PageQuery implements Serializable {
+
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 分页大小
+ */
+ private Integer pageSize;
+
+ /**
+ * 当前页数
+ */
+ private Integer pageNum;
+
+ /**
+ * 排序列
+ */
+ private String orderByColumn;
+
+ /**
+ * 排序的方向desc或者asc
+ */
+ private String isAsc;
+
+ /**
+ * 当前记录起始索引 默认值
+ */
+ public static final int DEFAULT_PAGE_NUM = 1;
+
+ /**
+ * 每页显示记录数 默认值 默认查全部
+ */
+ public static final int DEFAULT_PAGE_SIZE = Integer.MAX_VALUE;
+
+ public Page build() {
+ Integer pageNum = ObjectUtil.defaultIfNull(getPageNum(), DEFAULT_PAGE_NUM);
+ Integer pageSize = ObjectUtil.defaultIfNull(getPageSize(), DEFAULT_PAGE_SIZE);
+ if (pageNum <= 0) {
+ pageNum = DEFAULT_PAGE_NUM;
+ }
+ Page page = new Page<>(pageNum, pageSize);
+ List orderItems = buildOrderItem();
+ if (CollUtil.isNotEmpty(orderItems)) {
+ page.addOrder(orderItems);
+ }
+ return page;
+ }
+
+ /**
+ * 构建排序
+ *
+ * 支持的用法如下:
+ * {isAsc:"asc",orderByColumn:"id"} order by id asc
+ * {isAsc:"asc",orderByColumn:"id,createTime"} order by id asc,create_time asc
+ * {isAsc:"desc",orderByColumn:"id,createTime"} order by id desc,create_time desc
+ * {isAsc:"asc,desc",orderByColumn:"id,createTime"} order by id asc,create_time desc
+ */
+ private List buildOrderItem() {
+ if (StringUtils.isBlank(orderByColumn) || StringUtils.isBlank(isAsc)) {
+ return null;
+ }
+ String orderBy = SqlUtil.escapeOrderBySql(orderByColumn);
+ orderBy = StringUtils.toUnderScoreCase(orderBy);
+
+ // 兼容前端排序类型
+ isAsc = StringUtils.replaceEach(isAsc, new String[]{"ascending", "descending"}, new String[]{"asc", "desc"});
+
+ String[] orderByArr = orderBy.split(StringUtils.SEPARATOR);
+ String[] isAscArr = isAsc.split(StringUtils.SEPARATOR);
+ if (isAscArr.length != 1 && isAscArr.length != orderByArr.length) {
+ throw new ServiceException("排序参数有误");
+ }
+
+ List list = new ArrayList<>();
+ // 每个字段各自排序
+ for (int i = 0; i < orderByArr.length; i++) {
+ String orderByStr = orderByArr[i];
+ String isAscStr = isAscArr.length == 1 ? isAscArr[0] : isAscArr[i];
+ if ("asc".equals(isAscStr)) {
+ list.add(OrderItem.asc(orderByStr));
+ } else if ("desc".equals(isAscStr)) {
+ list.add(OrderItem.desc(orderByStr));
+ } else {
+ throw new ServiceException("排序参数有误");
+ }
+ }
+ return list;
+ }
+
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/R.java b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/R.java
new file mode 100644
index 0000000..381a6f6
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/R.java
@@ -0,0 +1,107 @@
+package com.ruoyi.common.core.domain;
+
+import com.ruoyi.common.constant.HttpStatus;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+
+/**
+ * 响应信息主体
+ *
+ * @author Lion Li
+ */
+@Data
+@NoArgsConstructor
+public class R implements Serializable {
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 成功
+ */
+ public static final int SUCCESS = 200;
+
+ /**
+ * 失败
+ */
+ public static final int FAIL = 500;
+
+ private int code;
+
+ private String msg;
+
+ private T data;
+
+ public static R ok() {
+ return restResult(null, SUCCESS, "操作成功");
+ }
+
+ public static R ok(T data) {
+ return restResult(data, SUCCESS, "操作成功");
+ }
+
+ public static R ok(String msg) {
+ return restResult(null, SUCCESS, msg);
+ }
+
+ public static R ok(String msg, T data) {
+ return restResult(data, SUCCESS, msg);
+ }
+
+ public static R fail() {
+ return restResult(null, FAIL, "操作失败");
+ }
+
+ public static R fail(String msg) {
+ return restResult(null, FAIL, msg);
+ }
+
+ public static R fail(T data) {
+ return restResult(data, FAIL, "操作失败");
+ }
+
+ public static R fail(String msg, T data) {
+ return restResult(data, FAIL, msg);
+ }
+
+ public static R fail(int code, String msg) {
+ return restResult(null, code, msg);
+ }
+
+ /**
+ * 返回警告消息
+ *
+ * @param msg 返回内容
+ * @return 警告消息
+ */
+ public static R warn(String msg) {
+ return restResult(null, HttpStatus.WARN, msg);
+ }
+
+ /**
+ * 返回警告消息
+ *
+ * @param msg 返回内容
+ * @param data 数据对象
+ * @return 警告消息
+ */
+ public static R warn(String msg, T data) {
+ return restResult(data, HttpStatus.WARN, msg);
+ }
+
+ private static R restResult(T data, int code, String msg) {
+ R r = new R<>();
+ r.setCode(code);
+ r.setData(data);
+ r.setMsg(msg);
+ return r;
+ }
+
+ public static Boolean isError(R ret) {
+ return !isSuccess(ret);
+ }
+
+ public static Boolean isSuccess(R ret) {
+ return R.SUCCESS == ret.getCode();
+ }
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/TreeEntity.java b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/TreeEntity.java
new file mode 100644
index 0000000..c7bb9b4
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/TreeEntity.java
@@ -0,0 +1,39 @@
+package com.ruoyi.common.core.domain;
+
+import com.baomidou.mybatisplus.annotation.TableField;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Tree基类
+ *
+ * @author Lion Li
+ */
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class TreeEntity extends BaseEntity {
+
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 父菜单名称
+ */
+ @TableField(exist = false)
+ private String parentName;
+
+ /**
+ * 父菜单ID
+ */
+ private Long parentId;
+
+ /**
+ * 子部门
+ */
+ @TableField(exist = false)
+ private List children = new ArrayList<>();
+
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/dto/RoleDTO.java b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/dto/RoleDTO.java
new file mode 100644
index 0000000..e25243f
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/dto/RoleDTO.java
@@ -0,0 +1,38 @@
+package com.ruoyi.common.core.domain.dto;
+
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+
+/**
+ * 角色
+ *
+ * @author Lion Li
+ */
+
+@Data
+@NoArgsConstructor
+public class RoleDTO implements Serializable {
+
+ /**
+ * 角色ID
+ */
+ private Long roleId;
+
+ /**
+ * 角色名称
+ */
+ private String roleName;
+
+ /**
+ * 角色权限
+ */
+ private String roleKey;
+
+ /**
+ * 数据范围(1:所有数据权限;2:自定义数据权限;3:本部门数据权限;4:本部门及以下数据权限;5:仅本人数据权限)
+ */
+ private String dataScope;
+
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/dto/UserOnlineDTO.java b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/dto/UserOnlineDTO.java
new file mode 100644
index 0000000..cdd3905
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/dto/UserOnlineDTO.java
@@ -0,0 +1,60 @@
+package com.ruoyi.common.core.domain.dto;
+
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+
+/**
+ * 当前在线会话
+ *
+ * @author ruoyi
+ */
+
+@Data
+@NoArgsConstructor
+public class UserOnlineDTO implements Serializable {
+
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 会话编号
+ */
+ private String tokenId;
+
+ /**
+ * 部门名称
+ */
+ private String deptName;
+
+ /**
+ * 用户名称
+ */
+ private String userName;
+
+ /**
+ * 登录IP地址
+ */
+ private String ipaddr;
+
+ /**
+ * 登录地址
+ */
+ private String loginLocation;
+
+ /**
+ * 浏览器类型
+ */
+ private String browser;
+
+ /**
+ * 操作系统
+ */
+ private String os;
+
+ /**
+ * 登录时间
+ */
+ private Long loginTime;
+
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysDept.java b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysDept.java
new file mode 100644
index 0000000..ba4a382
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysDept.java
@@ -0,0 +1,80 @@
+package com.ruoyi.common.core.domain.entity;
+
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableLogic;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.ruoyi.common.core.domain.TreeEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import javax.validation.constraints.Email;
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+import javax.validation.constraints.Size;
+
+/**
+ * 部门表 sys_dept
+ *
+ * @author Lion Li
+ */
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+@TableName("sys_dept")
+public class SysDept extends TreeEntity {
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 部门ID
+ */
+ @TableId(value = "dept_id")
+ private Long deptId;
+
+ /**
+ * 部门名称
+ */
+ @NotBlank(message = "部门名称不能为空")
+ @Size(min = 0, max = 30, message = "部门名称长度不能超过{max}个字符")
+ private String deptName;
+
+ /**
+ * 显示顺序
+ */
+ @NotNull(message = "显示顺序不能为空")
+ private Integer orderNum;
+
+ /**
+ * 负责人
+ */
+ private String leader;
+
+ /**
+ * 联系电话
+ */
+ @Size(min = 0, max = 11, message = "联系电话长度不能超过{max}个字符")
+ private String phone;
+
+ /**
+ * 邮箱
+ */
+ @Email(message = "邮箱格式不正确")
+ @Size(min = 0, max = 50, message = "邮箱长度不能超过{max}个字符")
+ private String email;
+
+ /**
+ * 部门状态:0正常,1停用
+ */
+ private String status;
+
+ /**
+ * 删除标志(0代表存在 2代表删除)
+ */
+ @TableLogic
+ private String delFlag;
+
+ /**
+ * 祖级列表
+ */
+ private String ancestors;
+
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysDictData.java b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysDictData.java
new file mode 100644
index 0000000..4ed2c54
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysDictData.java
@@ -0,0 +1,100 @@
+package com.ruoyi.common.core.domain.entity;
+
+import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
+import com.alibaba.excel.annotation.ExcelProperty;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.ruoyi.common.annotation.ExcelDictFormat;
+import com.ruoyi.common.constant.UserConstants;
+import com.ruoyi.common.convert.ExcelDictConvert;
+import com.ruoyi.common.core.domain.BaseEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.Size;
+
+/**
+ * 字典数据表 sys_dict_data
+ *
+ * @author Lion Li
+ */
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+@TableName("sys_dict_data")
+@ExcelIgnoreUnannotated
+public class SysDictData extends BaseEntity {
+
+ /**
+ * 字典编码
+ */
+ @ExcelProperty(value = "字典编码")
+ @TableId(value = "dict_code")
+ private Long dictCode;
+
+ /**
+ * 字典排序
+ */
+ @ExcelProperty(value = "字典排序")
+ private Integer dictSort;
+
+ /**
+ * 字典标签
+ */
+ @ExcelProperty(value = "字典标签")
+ @NotBlank(message = "字典标签不能为空")
+ @Size(min = 0, max = 100, message = "字典标签长度不能超过{max}个字符")
+ private String dictLabel;
+
+ /**
+ * 字典键值
+ */
+ @ExcelProperty(value = "字典键值")
+ @NotBlank(message = "字典键值不能为空")
+ @Size(min = 0, max = 100, message = "字典键值长度不能超过{max}个字符")
+ private String dictValue;
+
+ /**
+ * 字典类型
+ */
+ @ExcelProperty(value = "字典类型")
+ @NotBlank(message = "字典类型不能为空")
+ @Size(min = 0, max = 100, message = "字典类型长度不能超过{max}个字符")
+ private String dictType;
+
+ /**
+ * 样式属性(其他样式扩展)
+ */
+ @Size(min = 0, max = 100, message = "样式属性长度不能超过{max}个字符")
+ private String cssClass;
+
+ /**
+ * 表格字典样式
+ */
+ private String listClass;
+
+ /**
+ * 是否默认(Y是 N否)
+ */
+ @ExcelProperty(value = "是否默认", converter = ExcelDictConvert.class)
+ @ExcelDictFormat(dictType = "sys_yes_no")
+ private String isDefault;
+
+ /**
+ * 状态(0正常 1停用)
+ */
+ @ExcelProperty(value = "状态", converter = ExcelDictConvert.class)
+ @ExcelDictFormat(dictType = "sys_normal_disable")
+ private String status;
+
+ /**
+ * 备注
+ */
+ private String remark;
+
+ public boolean getDefault() {
+ return UserConstants.YES.equals(this.isDefault);
+ }
+
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysDictType.java b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysDictType.java
new file mode 100644
index 0000000..76c20e1
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysDictType.java
@@ -0,0 +1,65 @@
+package com.ruoyi.common.core.domain.entity;
+
+import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
+import com.alibaba.excel.annotation.ExcelProperty;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.ruoyi.common.annotation.ExcelDictFormat;
+import com.ruoyi.common.convert.ExcelDictConvert;
+import com.ruoyi.common.core.domain.BaseEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.Pattern;
+import javax.validation.constraints.Size;
+
+/**
+ * 字典类型表 sys_dict_type
+ *
+ * @author Lion Li
+ */
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+@TableName("sys_dict_type")
+@ExcelIgnoreUnannotated
+public class SysDictType extends BaseEntity {
+
+ /**
+ * 字典主键
+ */
+ @ExcelProperty(value = "字典主键")
+ @TableId(value = "dict_id")
+ private Long dictId;
+
+ /**
+ * 字典名称
+ */
+ @ExcelProperty(value = "字典名称")
+ @NotBlank(message = "字典名称不能为空")
+ @Size(min = 0, max = 100, message = "字典类型名称长度不能超过{max}个字符")
+ private String dictName;
+
+ /**
+ * 字典类型
+ */
+ @ExcelProperty(value = "字典类型")
+ @NotBlank(message = "字典类型不能为空")
+ @Size(min = 0, max = 100, message = "字典类型类型长度不能超过{max}个字符")
+ @Pattern(regexp = "^[a-z][a-z0-9_]*$", message = "字典类型必须以字母开头,且只能为(小写字母,数字,下滑线)")
+ private String dictType;
+
+ /**
+ * 状态(0正常 1停用)
+ */
+ @ExcelProperty(value = "状态", converter = ExcelDictConvert.class)
+ @ExcelDictFormat(dictType = "sys_normal_disable")
+ private String status;
+
+ /**
+ * 备注
+ */
+ private String remark;
+
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysMenu.java b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysMenu.java
new file mode 100644
index 0000000..38bd936
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysMenu.java
@@ -0,0 +1,104 @@
+package com.ruoyi.common.core.domain.entity;
+
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.ruoyi.common.core.domain.TreeEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+import javax.validation.constraints.Size;
+
+/**
+ * 菜单权限表 sys_menu
+ *
+ * @author Lion Li
+ */
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+@TableName("sys_menu")
+public class SysMenu extends TreeEntity {
+
+ /**
+ * 菜单ID
+ */
+ @TableId(value = "menu_id")
+ private Long menuId;
+
+ /**
+ * 菜单名称
+ */
+ @NotBlank(message = "菜单名称不能为空")
+ @Size(min = 0, max = 50, message = "菜单名称长度不能超过{max}个字符")
+ private String menuName;
+
+ /**
+ * 显示顺序
+ */
+ @NotNull(message = "显示顺序不能为空")
+ private Integer orderNum;
+
+ /**
+ * 路由地址
+ */
+ @Size(min = 0, max = 200, message = "路由地址不能超过{max}个字符")
+ private String path;
+
+ /**
+ * 组件路径
+ */
+ @Size(min = 0, max = 200, message = "组件路径不能超过{max}个字符")
+ private String component;
+
+ /**
+ * 路由参数
+ */
+ private String queryParam;
+
+ /**
+ * 是否为外链(0是 1否)
+ */
+ private String isFrame;
+
+ /**
+ * 是否缓存(0缓存 1不缓存)
+ */
+ private String isCache;
+
+ /**
+ * 类型(M目录 C菜单 F按钮)
+ */
+ @NotBlank(message = "菜单类型不能为空")
+ private String menuType;
+
+ /**
+ * 显示状态(0显示 1隐藏)
+ */
+ private String visible;
+
+ /**
+ * 菜单状态(0正常 1停用)
+ */
+ private String status;
+
+ /**
+ * 权限字符串
+ */
+ @JsonInclude(JsonInclude.Include.NON_NULL)
+ @Size(min = 0, max = 100, message = "权限标识长度不能超过{max}个字符")
+ private String perms;
+
+ /**
+ * 菜单图标
+ */
+ private String icon;
+
+ /**
+ * 备注
+ */
+ private String remark;
+
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysRole.java b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysRole.java
new file mode 100644
index 0000000..5949569
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysRole.java
@@ -0,0 +1,124 @@
+package com.ruoyi.common.core.domain.entity;
+
+import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
+import com.alibaba.excel.annotation.ExcelProperty;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableLogic;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.ruoyi.common.annotation.ExcelDictFormat;
+import com.ruoyi.common.constant.UserConstants;
+import com.ruoyi.common.convert.ExcelDictConvert;
+import com.ruoyi.common.core.domain.BaseEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+import javax.validation.constraints.Size;
+
+/**
+ * 角色表 sys_role
+ *
+ * @author Lion Li
+ */
+
+@Data
+@NoArgsConstructor
+@EqualsAndHashCode(callSuper = true)
+@TableName("sys_role")
+@ExcelIgnoreUnannotated
+public class SysRole extends BaseEntity {
+
+ /**
+ * 角色ID
+ */
+ @ExcelProperty(value = "角色序号")
+ @TableId(value = "role_id")
+ private Long roleId;
+
+ /**
+ * 角色名称
+ */
+ @ExcelProperty(value = "角色名称")
+ @NotBlank(message = "角色名称不能为空")
+ @Size(min = 0, max = 30, message = "角色名称长度不能超过{max}个字符")
+ private String roleName;
+
+ /**
+ * 角色权限
+ */
+ @ExcelProperty(value = "角色权限")
+ @NotBlank(message = "权限字符不能为空")
+ @Size(min = 0, max = 100, message = "权限字符长度不能超过{max}个字符")
+ private String roleKey;
+
+ /**
+ * 角色排序
+ */
+ @ExcelProperty(value = "角色排序")
+ @NotNull(message = "显示顺序不能为空")
+ private Integer roleSort;
+
+ /**
+ * 数据范围(1:所有数据权限;2:自定义数据权限;3:本部门数据权限;4:本部门及以下数据权限;5:仅本人数据权限)
+ */
+ @ExcelProperty(value = "数据范围", converter = ExcelDictConvert.class)
+ @ExcelDictFormat(readConverterExp = "1=所有数据权限,2=自定义数据权限,3=本部门数据权限,4=本部门及以下数据权限,5=仅本人数据权限")
+ private String dataScope;
+
+ /**
+ * 菜单树选择项是否关联显示( 0:父子不互相关联显示 1:父子互相关联显示)
+ */
+ private Boolean menuCheckStrictly;
+
+ /**
+ * 部门树选择项是否关联显示(0:父子不互相关联显示 1:父子互相关联显示 )
+ */
+ private Boolean deptCheckStrictly;
+
+ /**
+ * 角色状态(0正常 1停用)
+ */
+ @ExcelProperty(value = "角色状态", converter = ExcelDictConvert.class)
+ @ExcelDictFormat(dictType = "sys_normal_disable")
+ private String status;
+
+ /**
+ * 删除标志(0代表存在 2代表删除)
+ */
+ @TableLogic
+ private String delFlag;
+
+ /**
+ * 备注
+ */
+ private String remark;
+
+ /**
+ * 用户是否存在此角色标识 默认不存在
+ */
+ @TableField(exist = false)
+ private boolean flag = false;
+
+ /**
+ * 菜单组
+ */
+ @TableField(exist = false)
+ private Long[] menuIds;
+
+ /**
+ * 部门组(数据权限)
+ */
+ @TableField(exist = false)
+ private Long[] deptIds;
+
+ public SysRole(Long roleId) {
+ this.roleId = roleId;
+ }
+
+ public boolean isAdmin() {
+ return UserConstants.ADMIN_ID.equals(this.roleId);
+ }
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysUser.java b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysUser.java
new file mode 100644
index 0000000..d9139c5
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysUser.java
@@ -0,0 +1,168 @@
+package com.ruoyi.common.core.domain.entity;
+
+import com.baomidou.mybatisplus.annotation.*;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.ruoyi.common.annotation.Sensitive;
+import com.ruoyi.common.constant.UserConstants;
+import com.ruoyi.common.core.domain.BaseEntity;
+import com.ruoyi.common.enums.SensitiveStrategy;
+import com.ruoyi.common.xss.Xss;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+
+import javax.validation.constraints.Email;
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.Size;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 用户对象 sys_user
+ *
+ * @author Lion Li
+ */
+
+@Data
+@NoArgsConstructor
+@EqualsAndHashCode(callSuper = true)
+@TableName("sys_user")
+public class SysUser extends BaseEntity {
+
+ /**
+ * 用户ID
+ */
+ @TableId(value = "user_id")
+ private Long userId;
+
+ /**
+ * 部门ID
+ */
+ private Long deptId;
+
+ /**
+ * 用户账号
+ */
+ @Xss(message = "用户账号不能包含脚本字符")
+ @NotBlank(message = "用户账号不能为空")
+ @Size(min = 0, max = 30, message = "用户账号长度不能超过{max}个字符")
+ private String userName;
+
+ /**
+ * 用户昵称
+ */
+ @Xss(message = "用户昵称不能包含脚本字符")
+ @Size(min = 0, max = 30, message = "用户昵称长度不能超过{max}个字符")
+ private String nickName;
+
+ /**
+ * 用户类型(sys_user系统用户)
+ */
+ private String userType;
+
+ /**
+ * 用户邮箱
+ */
+ @Sensitive(strategy = SensitiveStrategy.EMAIL)
+ @Email(message = "邮箱格式不正确")
+ @Size(min = 0, max = 50, message = "邮箱长度不能超过{max}个字符")
+ private String email;
+
+ /**
+ * 手机号码
+ */
+ @Sensitive(strategy = SensitiveStrategy.PHONE)
+ private String phonenumber;
+
+ /**
+ * 用户性别
+ */
+ private String sex;
+
+ /**
+ * 用户头像
+ */
+ private String avatar;
+
+ /**
+ * 密码
+ */
+ @TableField(
+ insertStrategy = FieldStrategy.NOT_EMPTY,
+ updateStrategy = FieldStrategy.NOT_EMPTY,
+ whereStrategy = FieldStrategy.NOT_EMPTY
+ )
+ private String password;
+
+ @JsonIgnore
+ @JsonProperty
+ public String getPassword() {
+ return password;
+ }
+
+ /**
+ * 帐号状态(0正常 1停用)
+ */
+ private String status;
+
+ /**
+ * 删除标志(0代表存在 2代表删除)
+ */
+ @TableLogic
+ private String delFlag;
+
+ /**
+ * 最后登录IP
+ */
+ private String loginIp;
+
+ /**
+ * 最后登录时间
+ */
+ private Date loginDate;
+
+ /**
+ * 备注
+ */
+ private String remark;
+
+ /**
+ * 部门对象
+ */
+ @TableField(exist = false)
+ private SysDept dept;
+
+ /**
+ * 角色对象
+ */
+ @TableField(exist = false)
+ private List roles;
+
+ /**
+ * 角色组
+ */
+ @TableField(exist = false)
+ private Long[] roleIds;
+
+ /**
+ * 岗位组
+ */
+ @TableField(exist = false)
+ private Long[] postIds;
+
+ /**
+ * 数据权限 当前角色ID
+ */
+ @TableField(exist = false)
+ private Long roleId;
+
+ public SysUser(Long userId) {
+ this.userId = userId;
+ }
+
+ public boolean isAdmin() {
+ return UserConstants.ADMIN_ID.equals(this.userId);
+ }
+
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/event/LogininforEvent.java b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/event/LogininforEvent.java
new file mode 100644
index 0000000..733f97f
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/event/LogininforEvent.java
@@ -0,0 +1,44 @@
+package com.ruoyi.common.core.domain.event;
+
+import lombok.Data;
+
+import javax.servlet.http.HttpServletRequest;
+import java.io.Serializable;
+
+/**
+ * 登录事件
+ *
+ * @author Lion Li
+ */
+
+@Data
+public class LogininforEvent implements Serializable {
+
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 用户账号
+ */
+ private String username;
+
+ /**
+ * 登录状态 0成功 1失败
+ */
+ private String status;
+
+ /**
+ * 提示消息
+ */
+ private String message;
+
+ /**
+ * 请求体
+ */
+ private HttpServletRequest request;
+
+ /**
+ * 其他参数
+ */
+ private Object[] args;
+
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/event/OperLogEvent.java b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/event/OperLogEvent.java
new file mode 100644
index 0000000..3a29978
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/event/OperLogEvent.java
@@ -0,0 +1,104 @@
+package com.ruoyi.common.core.domain.event;
+
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * 操作日志事件
+ *
+ * @author Lion Li
+ */
+
+@Data
+public class OperLogEvent implements Serializable {
+
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 日志主键
+ */
+ private Long operId;
+
+ /**
+ * 操作模块
+ */
+ private String title;
+
+ /**
+ * 业务类型(0其它 1新增 2修改 3删除)
+ */
+ private Integer businessType;
+
+ /**
+ * 业务类型数组
+ */
+ private Integer[] businessTypes;
+
+ /**
+ * 请求方法
+ */
+ private String method;
+
+ /**
+ * 请求方式
+ */
+ private String requestMethod;
+
+ /**
+ * 操作类别(0其它 1后台用户 2手机端用户)
+ */
+ private Integer operatorType;
+
+ /**
+ * 操作人员
+ */
+ private String operName;
+
+ /**
+ * 部门名称
+ */
+ private String deptName;
+
+ /**
+ * 请求url
+ */
+ private String operUrl;
+
+ /**
+ * 操作地址
+ */
+ private String operIp;
+
+ /**
+ * 操作地点
+ */
+ private String operLocation;
+
+ /**
+ * 请求参数
+ */
+ private String operParam;
+
+ /**
+ * 返回参数
+ */
+ private String jsonResult;
+
+ /**
+ * 操作状态(0正常 1异常)
+ */
+ private Integer status;
+
+ /**
+ * 错误消息
+ */
+ private String errorMsg;
+
+ /**
+ * 操作时间
+ */
+ private Date operTime;
+
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/model/LoginBody.java b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/model/LoginBody.java
new file mode 100644
index 0000000..4a4cfb5
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/model/LoginBody.java
@@ -0,0 +1,42 @@
+package com.ruoyi.common.core.domain.model;
+
+import com.ruoyi.common.constant.UserConstants;
+import lombok.Data;
+import org.hibernate.validator.constraints.Length;
+
+import javax.validation.constraints.NotBlank;
+
+/**
+ * 用户登录对象
+ *
+ * @author Lion Li
+ */
+
+@Data
+public class LoginBody {
+
+ /**
+ * 用户名
+ */
+ @NotBlank(message = "{user.username.not.blank}")
+ @Length(min = UserConstants.USERNAME_MIN_LENGTH, max = UserConstants.USERNAME_MAX_LENGTH, message = "{user.username.length.valid}")
+ private String username;
+
+ /**
+ * 用户密码
+ */
+ @NotBlank(message = "{user.password.not.blank}")
+ @Length(min = UserConstants.PASSWORD_MIN_LENGTH, max = UserConstants.PASSWORD_MAX_LENGTH, message = "{user.password.length.valid}")
+ private String password;
+
+ /**
+ * 验证码
+ */
+ private String code;
+
+ /**
+ * 唯一标识
+ */
+ private String uuid;
+
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/model/LoginUser.java b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/model/LoginUser.java
new file mode 100644
index 0000000..83fcf9f
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/model/LoginUser.java
@@ -0,0 +1,116 @@
+package com.ruoyi.common.core.domain.model;
+
+import com.ruoyi.common.core.domain.dto.RoleDTO;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * 登录用户身份权限
+ *
+ * @author Lion Li
+ */
+
+@Data
+@NoArgsConstructor
+public class LoginUser implements Serializable {
+
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 用户ID
+ */
+ private Long userId;
+
+ /**
+ * 部门ID
+ */
+ private Long deptId;
+
+ /**
+ * 部门名
+ */
+ private String deptName;
+
+ /**
+ * 用户唯一标识
+ */
+ private String token;
+
+ /**
+ * 用户类型
+ */
+ private String userType;
+
+ /**
+ * 登录时间
+ */
+ private Long loginTime;
+
+ /**
+ * 过期时间
+ */
+ private Long expireTime;
+
+ /**
+ * 登录IP地址
+ */
+ private String ipaddr;
+
+ /**
+ * 登录地点
+ */
+ private String loginLocation;
+
+ /**
+ * 浏览器类型
+ */
+ private String browser;
+
+ /**
+ * 操作系统
+ */
+ private String os;
+
+ /**
+ * 菜单权限
+ */
+ private Set menuPermission;
+
+ /**
+ * 角色权限
+ */
+ private Set rolePermission;
+
+ /**
+ * 用户名
+ */
+ private String username;
+
+ /**
+ * 角色对象
+ */
+ private List roles;
+
+ /**
+ * 数据权限 当前角色ID
+ */
+ private Long roleId;
+
+ /**
+ * 获取登录id
+ */
+ public String getLoginId() {
+ if (userType == null) {
+ throw new IllegalArgumentException("用户类型不能为空");
+ }
+ if (userId == null) {
+ throw new IllegalArgumentException("用户ID不能为空");
+ }
+ return userType + ":" + userId;
+ }
+
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/model/RegisterBody.java b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/model/RegisterBody.java
new file mode 100644
index 0000000..88367e7
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/model/RegisterBody.java
@@ -0,0 +1,17 @@
+package com.ruoyi.common.core.domain.model;
+
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 用户注册对象
+ *
+ * @author Lion Li
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class RegisterBody extends LoginBody {
+
+ private String userType;
+
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/model/SmsLoginBody.java b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/model/SmsLoginBody.java
new file mode 100644
index 0000000..ce774ac
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/model/SmsLoginBody.java
@@ -0,0 +1,28 @@
+package com.ruoyi.common.core.domain.model;
+
+import lombok.Data;
+
+import javax.validation.constraints.NotBlank;
+
+/**
+ * 短信登录对象
+ *
+ * @author Lion Li
+ */
+
+@Data
+public class SmsLoginBody {
+
+ /**
+ * 用户名
+ */
+ @NotBlank(message = "{user.phonenumber.not.blank}")
+ private String phonenumber;
+
+ /**
+ * 用户密码
+ */
+ @NotBlank(message = "{sms.code.not.blank}")
+ private String smsCode;
+
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/model/XcxLoginUser.java b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/model/XcxLoginUser.java
new file mode 100644
index 0000000..83313a0
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/model/XcxLoginUser.java
@@ -0,0 +1,24 @@
+package com.ruoyi.common.core.domain.model;
+
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+
+/**
+ * 小程序登录用户身份权限
+ *
+ * @author Lion Li
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@NoArgsConstructor
+public class XcxLoginUser extends LoginUser {
+
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * openid
+ */
+ private String openid;
+
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/core/mapper/BaseMapperPlus.java b/ruoyi-common/src/main/java/com/ruoyi/common/core/mapper/BaseMapperPlus.java
new file mode 100644
index 0000000..0a8539a
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/core/mapper/BaseMapperPlus.java
@@ -0,0 +1,192 @@
+package com.ruoyi.common.core.mapper;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.ObjectUtil;
+import com.baomidou.mybatisplus.core.conditions.Wrapper;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.core.toolkit.*;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.baomidou.mybatisplus.extension.toolkit.Db;
+import com.ruoyi.common.utils.BeanCopyUtils;
+import org.apache.ibatis.logging.Log;
+import org.apache.ibatis.logging.LogFactory;
+
+import java.io.Serializable;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 自定义 Mapper 接口, 实现 自定义扩展
+ *
+ * @param mapper 泛型
+ * @param table 泛型
+ * @param vo 泛型
+ * @author Lion Li
+ * @since 2021-05-13
+ */
+@SuppressWarnings("unchecked")
+public interface BaseMapperPlus extends BaseMapper {
+
+ Log log = LogFactory.getLog(BaseMapperPlus.class);
+
+ default Class currentVoClass() {
+ return (Class) ReflectionKit.getSuperClassGenericType(this.getClass(), BaseMapperPlus.class, 2);
+ }
+
+ default Class currentModelClass() {
+ return (Class) ReflectionKit.getSuperClassGenericType(this.getClass(), BaseMapperPlus.class, 1);
+ }
+
+ default Class currentMapperClass() {
+ return (Class) ReflectionKit.getSuperClassGenericType(this.getClass(), BaseMapperPlus.class, 0);
+ }
+
+ default List selectList() {
+ return this.selectList(new QueryWrapper<>());
+ }
+
+ /**
+ * 批量插入
+ */
+ default boolean insertBatch(Collection entityList) {
+ return Db.saveBatch(entityList);
+ }
+
+ /**
+ * 批量更新
+ */
+ default boolean updateBatchById(Collection entityList) {
+ return Db.updateBatchById(entityList);
+ }
+
+ /**
+ * 批量插入或更新
+ */
+ default boolean insertOrUpdateBatch(Collection entityList) {
+ return Db.saveOrUpdateBatch(entityList);
+ }
+
+ /**
+ * 批量插入(包含限制条数)
+ */
+ default boolean insertBatch(Collection entityList, int batchSize) {
+ return Db.saveBatch(entityList, batchSize);
+ }
+
+ /**
+ * 批量更新(包含限制条数)
+ */
+ default boolean updateBatchById(Collection entityList, int batchSize) {
+ return Db.updateBatchById(entityList, batchSize);
+ }
+
+ /**
+ * 批量插入或更新(包含限制条数)
+ */
+ default boolean insertOrUpdateBatch(Collection entityList, int batchSize) {
+ return Db.saveOrUpdateBatch(entityList, batchSize);
+ }
+
+ /**
+ * 插入或更新(包含限制条数)
+ */
+ default boolean insertOrUpdate(T entity) {
+ return Db.saveOrUpdate(entity);
+ }
+
+ default V selectVoById(Serializable id) {
+ return selectVoById(id, this.currentVoClass());
+ }
+
+ /**
+ * 根据 ID 查询
+ */
+ default C selectVoById(Serializable id, Class voClass) {
+ T obj = this.selectById(id);
+ if (ObjectUtil.isNull(obj)) {
+ return null;
+ }
+ return BeanCopyUtils.copy(obj, voClass);
+ }
+
+ default List selectVoBatchIds(Collection extends Serializable> idList) {
+ return selectVoBatchIds(idList, this.currentVoClass());
+ }
+
+ /**
+ * 查询(根据ID 批量查询)
+ */
+ default List selectVoBatchIds(Collection extends Serializable> idList, Class voClass) {
+ List list = this.selectBatchIds(idList);
+ if (CollUtil.isEmpty(list)) {
+ return CollUtil.newArrayList();
+ }
+ return BeanCopyUtils.copyList(list, voClass);
+ }
+
+ default List selectVoByMap(Map map) {
+ return selectVoByMap(map, this.currentVoClass());
+ }
+
+ /**
+ * 查询(根据 columnMap 条件)
+ */
+ default List selectVoByMap(Map map, Class voClass) {
+ List list = this.selectByMap(map);
+ if (CollUtil.isEmpty(list)) {
+ return CollUtil.newArrayList();
+ }
+ return BeanCopyUtils.copyList(list, voClass);
+ }
+
+ default V selectVoOne(Wrapper wrapper) {
+ return selectVoOne(wrapper, this.currentVoClass());
+ }
+
+ /**
+ * 根据 entity 条件,查询一条记录
+ */
+ default C selectVoOne(Wrapper wrapper, Class voClass) {
+ T obj = this.selectOne(wrapper);
+ if (ObjectUtil.isNull(obj)) {
+ return null;
+ }
+ return BeanCopyUtils.copy(obj, voClass);
+ }
+
+ default List selectVoList(Wrapper