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/pusong-admin/src/test/java/com/pusong/test/TagUnitTest.java b/pusong-admin/src/test/java/com/pusong/test/TagUnitTest.java
new file mode 100644
index 0000000..c18202d
--- /dev/null
+++ b/pusong-admin/src/test/java/com/pusong/test/TagUnitTest.java
@@ -0,0 +1,54 @@
+package com.pusong.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/pusong-common/pom.xml b/pusong-common/pom.xml
new file mode 100644
index 0000000..a559481
--- /dev/null
+++ b/pusong-common/pom.xml
@@ -0,0 +1,45 @@
+
+
+
+ pusong-vue-plus
+ com.pusong
+ ${revision}
+
+ 4.0.0
+
+
+ pusong-common-bom
+ pusong-common-social
+ pusong-common-core
+ pusong-common-doc
+ pusong-common-excel
+ pusong-common-idempotent
+ pusong-common-job
+ pusong-common-log
+ pusong-common-mail
+ pusong-common-mybatis
+ pusong-common-oss
+ pusong-common-ratelimiter
+ pusong-common-redis
+ pusong-common-satoken
+ pusong-common-security
+ pusong-common-sms
+ pusong-common-web
+ pusong-common-translation
+ pusong-common-sensitive
+ pusong-common-json
+ pusong-common-encrypt
+ pusong-common-tenant
+ pusong-common-websocket
+
+
+ pusong-common
+ pom
+
+
+ common 通用模块
+
+
+
diff --git a/pusong-common/pusong-common-bom/pom.xml b/pusong-common/pusong-common-bom/pom.xml
new file mode 100644
index 0000000..a8b562d
--- /dev/null
+++ b/pusong-common/pusong-common-bom/pom.xml
@@ -0,0 +1,178 @@
+
+
+ 4.0.0
+
+ com.pusong
+ pusong-common-bom
+ ${revision}
+ pom
+
+
+ ruoyi-common-bom common依赖项
+
+
+
+ 5.2.0
+
+
+
+
+
+
+ com.pusong
+ pusong-common-core
+ ${revision}
+
+
+
+
+ com.pusong
+ pusong-common-doc
+ ${revision}
+
+
+
+
+ com.pusong
+ pusong-common-excel
+ ${revision}
+
+
+
+
+ com.pusong
+ pusong-common-idempotent
+ ${revision}
+
+
+
+
+ com.pusong
+ pusong-common-job
+ ${revision}
+
+
+
+
+ com.pusong
+ pusong-common-log
+ ${revision}
+
+
+
+
+ com.pusong
+ pusong-common-mail
+ ${revision}
+
+
+
+
+ com.pusong
+ pusong-common-mybatis
+ ${revision}
+
+
+
+
+ com.pusong
+ pusong-common-oss
+ ${revision}
+
+
+
+
+ com.pusong
+ pusong-common-ratelimiter
+ ${revision}
+
+
+
+
+ com.pusong
+ pusong-common-redis
+ ${revision}
+
+
+
+
+ com.pusong
+ pusong-common-satoken
+ ${revision}
+
+
+
+
+ com.pusong
+ pusong-common-security
+ ${revision}
+
+
+
+
+ com.pusong
+ pusong-common-sms
+ ${revision}
+
+
+
+ com.pusong
+ pusong-common-social
+ ${revision}
+
+
+
+
+ com.pusong
+ pusong-common-web
+ ${revision}
+
+
+
+
+ com.pusong
+ pusong-common-translation
+ ${revision}
+
+
+
+
+ com.pusong
+ pusong-common-sensitive
+ ${revision}
+
+
+
+
+ com.pusong
+ pusong-common-json
+ ${revision}
+
+
+
+
+ com.pusong
+ pusong-common-encrypt
+ ${revision}
+
+
+
+
+ com.pusong
+ pusong-common-tenant
+ ${revision}
+
+
+
+
+ com.pusong
+ pusong-common-websocket
+ ${revision}
+
+
+
+
+
+
diff --git a/pusong-common/pusong-common-core/pom.xml b/pusong-common/pusong-common-core/pom.xml
new file mode 100644
index 0000000..f731029
--- /dev/null
+++ b/pusong-common/pusong-common-core/pom.xml
@@ -0,0 +1,104 @@
+
+
+
+ com.pusong
+ pusong-common
+ ${revision}
+
+ 4.0.0
+
+ pusong-common-core
+
+
+ ruoyi-common-core 核心模块
+
+
+
+
+
+ org.springframework
+ spring-context-support
+
+
+
+
+ org.springframework
+ spring-web
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-validation
+
+
+
+ org.springframework.boot
+ spring-boot-starter-aop
+
+
+
+
+ org.apache.commons
+ commons-lang3
+
+
+
+
+ jakarta.servlet
+ jakarta.servlet-api
+
+
+
+ cn.hutool
+ hutool-core
+
+
+
+ cn.hutool
+ hutool-http
+
+
+
+ cn.hutool
+ hutool-extra
+
+
+
+ org.projectlombok
+ lombok
+
+
+
+
+ org.springframework.boot
+ spring-boot-configuration-processor
+
+
+
+ org.springframework.boot
+ spring-boot-properties-migrator
+ runtime
+
+
+
+ io.github.linpeilie
+ mapstruct-plus-spring-boot-starter
+
+
+
+
+ org.lionsoul
+ ip2region
+
+
+
+ com.alibaba
+ transmittable-thread-local
+
+
+
+
+
diff --git a/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/config/ApplicationConfig.java b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/config/ApplicationConfig.java
new file mode 100644
index 0000000..09c63c4
--- /dev/null
+++ b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/config/ApplicationConfig.java
@@ -0,0 +1,17 @@
+package com.pusong.common.core.config;
+
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.context.annotation.EnableAspectJAutoProxy;
+import org.springframework.scheduling.annotation.EnableAsync;
+
+/**
+ * 程序注解配置
+ *
+ * @author Lion Li
+ */
+@AutoConfiguration
+@EnableAspectJAutoProxy
+@EnableAsync(proxyTargetClass = true)
+public class ApplicationConfig {
+
+}
diff --git a/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/config/AsyncConfig.java b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/config/AsyncConfig.java
new file mode 100644
index 0000000..0d9939e
--- /dev/null
+++ b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/config/AsyncConfig.java
@@ -0,0 +1,52 @@
+package com.pusong.common.core.config;
+
+import cn.hutool.core.util.ArrayUtil;
+import com.pusong.common.core.exception.ServiceException;
+import com.pusong.common.core.utils.SpringUtils;
+import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.core.task.VirtualThreadTaskExecutor;
+import org.springframework.scheduling.annotation.AsyncConfigurer;
+
+import java.util.Arrays;
+import java.util.concurrent.Executor;
+
+/**
+ * 异步配置
+ *
+ * 如果未使用虚拟线程则生效
+ *
+ * @author Lion Li
+ */
+@AutoConfiguration
+public class AsyncConfig implements AsyncConfigurer {
+
+ /**
+ * 自定义 @Async 注解使用系统线程池
+ */
+ @Override
+ public Executor getAsyncExecutor() {
+ if(SpringUtils.isVirtual()) {
+ return new VirtualThreadTaskExecutor("async-");
+ }
+ return SpringUtils.getBean("scheduledExecutorService");
+ }
+
+ /**
+ * 异步执行异常处理
+ */
+ @Override
+ public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
+ return (throwable, method, objects) -> {
+ throwable.printStackTrace();
+ StringBuilder sb = new StringBuilder();
+ sb.append("Exception message - ").append(throwable.getMessage())
+ .append(", Method name - ").append(method.getName());
+ if (ArrayUtil.isNotEmpty(objects)) {
+ sb.append(", Parameter value - ").append(Arrays.toString(objects));
+ }
+ throw new ServiceException(sb.toString());
+ };
+ }
+
+}
diff --git a/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/config/RuoYiConfig.java b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/config/RuoYiConfig.java
new file mode 100644
index 0000000..9c27745
--- /dev/null
+++ b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/config/RuoYiConfig.java
@@ -0,0 +1,33 @@
+package com.pusong.common.core.config;
+
+import lombok.Data;
+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;
+
+}
diff --git a/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/config/ThreadPoolConfig.java b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/config/ThreadPoolConfig.java
new file mode 100644
index 0000000..52392ee
--- /dev/null
+++ b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/config/ThreadPoolConfig.java
@@ -0,0 +1,78 @@
+package com.pusong.common.core.config;
+
+import jakarta.annotation.PreDestroy;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.concurrent.BasicThreadFactory;
+import com.pusong.common.core.config.properties.ThreadPoolProperties;
+import com.pusong.common.core.utils.Threads;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
+
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+import java.util.concurrent.ThreadPoolExecutor;
+
+/**
+ * 线程池配置
+ *
+ * @author Lion Li
+ **/
+@Slf4j
+@AutoConfiguration
+@EnableConfigurationProperties(ThreadPoolProperties.class)
+public class ThreadPoolConfig {
+
+ /**
+ * 核心线程数 = cpu 核心数 + 1
+ */
+ private final int core = Runtime.getRuntime().availableProcessors() + 1;
+
+ private ScheduledExecutorService scheduledExecutorService;
+
+ @Bean(name = "threadPoolTaskExecutor")
+ @ConditionalOnProperty(prefix = "thread-pool", name = "enabled", havingValue = "true")
+ public ThreadPoolTaskExecutor threadPoolTaskExecutor(ThreadPoolProperties threadPoolProperties) {
+ ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
+ executor.setCorePoolSize(core);
+ executor.setMaxPoolSize(core * 2);
+ executor.setQueueCapacity(threadPoolProperties.getQueueCapacity());
+ executor.setKeepAliveSeconds(threadPoolProperties.getKeepAliveSeconds());
+ executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
+ return executor;
+ }
+
+ /**
+ * 执行周期性或定时任务
+ */
+ @Bean(name = "scheduledExecutorService")
+ protected ScheduledExecutorService scheduledExecutorService() {
+ ScheduledThreadPoolExecutor scheduledThreadPoolExecutor = new ScheduledThreadPoolExecutor(core,
+ new BasicThreadFactory.Builder().namingPattern("schedule-pool-%d").daemon(true).build(),
+ new ThreadPoolExecutor.CallerRunsPolicy()) {
+ @Override
+ protected void afterExecute(Runnable r, Throwable t) {
+ super.afterExecute(r, t);
+ Threads.printException(r, t);
+ }
+ };
+ this.scheduledExecutorService = scheduledThreadPoolExecutor;
+ return scheduledThreadPoolExecutor;
+ }
+
+ /**
+ * 销毁事件
+ */
+ @PreDestroy
+ public void destroy() {
+ try {
+ log.info("====关闭后台任务任务线程池====");
+ Threads.shutdownAndAwaitTermination(scheduledExecutorService);
+ } catch (Exception e) {
+ log.error(e.getMessage(), e);
+ }
+ }
+
+}
diff --git a/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/config/ValidatorConfig.java b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/config/ValidatorConfig.java
new file mode 100644
index 0000000..a1ddd3d
--- /dev/null
+++ b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/config/ValidatorConfig.java
@@ -0,0 +1,40 @@
+package com.pusong.common.core.config;
+
+import jakarta.validation.Validator;
+import org.hibernate.validator.HibernateValidator;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.context.MessageSource;
+import org.springframework.context.annotation.Bean;
+import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
+
+import java.util.Properties;
+
+/**
+ * 校验框架配置类
+ *
+ * @author Lion Li
+ */
+@AutoConfiguration
+public class ValidatorConfig {
+
+ /**
+ * 配置校验框架 快速返回模式
+ */
+ @Bean
+ public Validator validator(MessageSource messageSource) {
+ try (LocalValidatorFactoryBean factoryBean = new LocalValidatorFactoryBean()) {
+ // 国际化
+ factoryBean.setValidationMessageSource(messageSource);
+ // 设置使用 HibernateValidator 校验器
+ factoryBean.setProviderClass(HibernateValidator.class);
+ Properties properties = new Properties();
+ // 设置 快速异常返回
+ properties.setProperty("hibernate.validator.fail_fast", "true");
+ factoryBean.setValidationProperties(properties);
+ // 加载配置
+ factoryBean.afterPropertiesSet();
+ return factoryBean.getValidator();
+ }
+ }
+
+}
diff --git a/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/config/properties/ThreadPoolProperties.java b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/config/properties/ThreadPoolProperties.java
new file mode 100644
index 0000000..58a1109
--- /dev/null
+++ b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/config/properties/ThreadPoolProperties.java
@@ -0,0 +1,30 @@
+package com.pusong.common.core.config.properties;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+/**
+ * 线程池 配置属性
+ *
+ * @author Lion Li
+ */
+@Data
+@ConfigurationProperties(prefix = "thread-pool")
+public class ThreadPoolProperties {
+
+ /**
+ * 是否开启线程池
+ */
+ private boolean enabled;
+
+ /**
+ * 队列最大长度
+ */
+ private int queueCapacity;
+
+ /**
+ * 线程池维护线程所允许的空闲时间
+ */
+ private int keepAliveSeconds;
+
+}
diff --git a/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/constant/CacheConstants.java b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/constant/CacheConstants.java
new file mode 100644
index 0000000..109a866
--- /dev/null
+++ b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/constant/CacheConstants.java
@@ -0,0 +1,25 @@
+package com.pusong.common.core.constant;
+
+/**
+ * 缓存的key 常量
+ *
+ * @author Lion Li
+ */
+public interface CacheConstants {
+
+ /**
+ * 在线用户 redis key
+ */
+ String ONLINE_TOKEN_KEY = "online_tokens:";
+
+ /**
+ * 参数管理 cache key
+ */
+ String SYS_CONFIG_KEY = "sys_config:";
+
+ /**
+ * 字典管理 cache key
+ */
+ String SYS_DICT_KEY = "sys_dict:";
+
+}
diff --git a/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/constant/CacheNames.java b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/constant/CacheNames.java
new file mode 100644
index 0000000..aaf03e7
--- /dev/null
+++ b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/constant/CacheNames.java
@@ -0,0 +1,114 @@
+package com.pusong.common.core.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_TENANT = GlobalConstants.GLOBAL_REDIS_KEY + "sys_tenant#30d";
+
+ /**
+ * 客户端
+ */
+ String SYS_CLIENT = GlobalConstants.GLOBAL_REDIS_KEY + "sys_client#30d";
+
+ /**
+ * 用户账户
+ */
+ String SYS_USER_NAME = "sys_user_name#30d";
+
+ /**
+ * 用户名称
+ */
+ String SYS_NICKNAME = "sys_nickname#30d";
+
+ /**
+ * 部门
+ */
+ String SYS_DEPT = "sys_dept#30d";
+
+ /**
+ * OSS内容
+ */
+ String SYS_OSS = "sys_oss#30d";
+
+ /**
+ * OSS配置
+ */
+ String SYS_OSS_CONFIG = GlobalConstants.GLOBAL_REDIS_KEY + "sys_oss_config";
+
+ /**
+ * 在线用户
+ */
+ String ONLINE_TOKEN = "online_tokens";
+
+
+
+ /**
+ * 首页查询(成交金额)
+ */
+ String HOME_A = "home_queryA#60s";
+ /**
+ * 首页查询(回款统计与合同)
+ */
+ String HOME_B = "home_queryB#60s";
+
+ /**
+ * 首页查询(成交金额)
+ */
+ String HOME_C = "home_queryC#60s";
+
+ /**
+ * 首页查询
+ */
+ String HOME_D = "home_queryD#60s";
+ /**
+ * 首页查询
+ */
+ String HOME_E = "home_queryE#60s";
+ /**
+ * 首页查询
+ */
+ String HOME_F = "home_queryF#60s";
+ /**
+ * 首页查询
+ */
+ String HOME_G = "home_queryG#60s";
+
+ /**
+ * 首页查询
+ */
+ String HOME_H = "home_queryH#60s";
+ /**
+ * 首页查询
+ */
+ String HOME_I = "home_queryI#60s";
+}
diff --git a/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/constant/Constants.java b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/constant/Constants.java
new file mode 100644
index 0000000..c44df01
--- /dev/null
+++ b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/constant/Constants.java
@@ -0,0 +1,81 @@
+package com.pusong.common.core.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";
+
+ /**
+ * 顶级部门id
+ */
+ Long TOP_PARENT_ID = 0L;
+
+}
+
diff --git a/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/constant/GlobalConstants.java b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/constant/GlobalConstants.java
new file mode 100644
index 0000000..8f670c9
--- /dev/null
+++ b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/constant/GlobalConstants.java
@@ -0,0 +1,39 @@
+package com.pusong.common.core.constant;
+
+/**
+ * 全局的key常量 (业务无关的key)
+ *
+ * @author Lion Li
+ */
+public interface GlobalConstants {
+
+ /**
+ * 全局 redis key (业务无关的key)
+ */
+ String GLOBAL_REDIS_KEY = "global:";
+
+ /**
+ * 验证码 redis key
+ */
+ String CAPTCHA_CODE_KEY = GLOBAL_REDIS_KEY + "captcha_codes:";
+
+ /**
+ * 防重提交 redis key
+ */
+ String REPEAT_SUBMIT_KEY = GLOBAL_REDIS_KEY + "repeat_submit:";
+
+ /**
+ * 限流 redis key
+ */
+ String RATE_LIMIT_KEY = GLOBAL_REDIS_KEY + "rate_limit:";
+
+ /**
+ * 登录账户密码错误次数 redis key
+ */
+ String PWD_ERR_CNT_KEY = GLOBAL_REDIS_KEY + "pwd_err_cnt:";
+
+ /**
+ * 三方认证 redis key
+ */
+ String SOCIAL_AUTH_CODE_KEY = GLOBAL_REDIS_KEY + "social_auth_codes:";
+}
diff --git a/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/constant/HttpStatus.java b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/constant/HttpStatus.java
new file mode 100644
index 0000000..35df0b9
--- /dev/null
+++ b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/constant/HttpStatus.java
@@ -0,0 +1,93 @@
+package com.pusong.common.core.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/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/constant/RegexConstants.java b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/constant/RegexConstants.java
new file mode 100644
index 0000000..aec61e4
--- /dev/null
+++ b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/constant/RegexConstants.java
@@ -0,0 +1,54 @@
+package com.pusong.common.core.constant;
+
+import cn.hutool.core.lang.RegexPool;
+
+/**
+ * 常用正则表达式字符串
+ *
+ * 常用正则表达式集合,更多正则见: https://any86.github.io/any-rule/
+ *
+ * @author Feng
+ */
+public interface RegexConstants extends RegexPool {
+
+ /**
+ * 字典类型必须以字母开头,且只能为(小写字母,数字,下滑线)
+ */
+ String DICTIONARY_TYPE = "^[a-z][a-z0-9_]*$";
+
+ /**
+ * 权限标识必须符合 tool:build:list 格式,或者空字符串
+ */
+ String PERMISSION_STRING = "^(|^[a-zA-Z0-9_]+:[a-zA-Z0-9_]+:[a-zA-Z0-9_]+)$";
+
+ /**
+ * 身份证号码(后6位)
+ */
+ String ID_CARD_LAST_6 = "^(([0-2][1-9])|10|20|30|31)\\d{3}[0-9Xx]$";
+
+ /**
+ * QQ号码
+ */
+ String QQ_NUMBER = "^[1-9][0-9]\\d{4,9}$";
+
+ /**
+ * 邮政编码
+ */
+ String POSTAL_CODE = "^[1-9]\\d{5}$";
+
+ /**
+ * 注册账号
+ */
+ String ACCOUNT = "^[a-zA-Z][a-zA-Z0-9_]{4,15}$";
+
+ /**
+ * 密码:包含至少8个字符,包括大写字母、小写字母、数字和特殊字符
+ */
+ String PASSWORD = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[@$!%*?&])[A-Za-z\\d@$!%*?&]{8,}$";
+
+ /**
+ * 通用状态(0表示正常,1表示停用)
+ */
+ String STATUS = "^[01]$";
+
+}
diff --git a/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/constant/TenantConstants.java b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/constant/TenantConstants.java
new file mode 100644
index 0000000..d077a02
--- /dev/null
+++ b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/constant/TenantConstants.java
@@ -0,0 +1,45 @@
+package com.pusong.common.core.constant;
+
+/**
+ * 租户常量信息
+ *
+ * @author Lion Li
+ */
+public interface TenantConstants {
+
+ /**
+ * 租户正常状态
+ */
+ String NORMAL = "0";
+
+ /**
+ * 租户封禁状态
+ */
+ String DISABLE = "1";
+
+ /**
+ * 超级管理员ID
+ */
+ Long SUPER_ADMIN_ID = 1L;
+
+ /**
+ * 超级管理员角色 roleKey
+ */
+ String SUPER_ADMIN_ROLE_KEY = "superadmin";
+
+ /**
+ * 租户管理员角色 roleKey
+ */
+ String TENANT_ADMIN_ROLE_KEY = "admin";
+
+ /**
+ * 租户管理员角色名称
+ */
+ String TENANT_ADMIN_ROLE_NAME = "管理员";
+
+ /**
+ * 默认租户ID
+ */
+ String DEFAULT_TENANT_ID = "000000";
+
+}
diff --git a/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/constant/UserConstants.java b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/constant/UserConstants.java
new file mode 100644
index 0000000..0614cc3
--- /dev/null
+++ b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/constant/UserConstants.java
@@ -0,0 +1,142 @@
+package com.pusong.common.core.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 POST_NORMAL = "0";
+
+ /**
+ * 岗位停用状态
+ */
+ String POST_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 SUPER_ADMIN_ID = 1L;
+
+}
diff --git a/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/domain/R.java b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/domain/R.java
new file mode 100644
index 0000000..ba9bfdc
--- /dev/null
+++ b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/domain/R.java
@@ -0,0 +1,110 @@
+package com.pusong.common.core.domain;
+
+import com.pusong.common.core.constant.HttpStatus;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * 响应信息主体
+ *
+ * @author Lion Li
+ */
+@Data
+@NoArgsConstructor
+public class R implements Serializable {
+
+ @Serial
+ 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/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/domain/dto/OssDTO.java b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/domain/dto/OssDTO.java
new file mode 100644
index 0000000..3ee29c0
--- /dev/null
+++ b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/domain/dto/OssDTO.java
@@ -0,0 +1,46 @@
+package com.pusong.common.core.domain.dto;
+
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * OSS对象
+ *
+ * @author Lion Li
+ */
+@Data
+@NoArgsConstructor
+public class OssDTO implements Serializable {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 对象存储主键
+ */
+ private Long ossId;
+
+ /**
+ * 文件名
+ */
+ private String fileName;
+
+ /**
+ * 原名
+ */
+ private String originalName;
+
+ /**
+ * 文件后缀名
+ */
+ private String fileSuffix;
+
+ /**
+ * URL地址
+ */
+ private String url;
+
+}
diff --git a/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/domain/dto/RoleDTO.java b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/domain/dto/RoleDTO.java
new file mode 100644
index 0000000..bb51be3
--- /dev/null
+++ b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/domain/dto/RoleDTO.java
@@ -0,0 +1,42 @@
+package com.pusong.common.core.domain.dto;
+
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * 角色
+ *
+ * @author Lion Li
+ */
+
+@Data
+@NoArgsConstructor
+public class RoleDTO implements Serializable {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 角色ID
+ */
+ private Long roleId;
+
+ /**
+ * 角色名称
+ */
+ private String roleName;
+
+ /**
+ * 角色权限
+ */
+ private String roleKey;
+
+ /**
+ * 数据范围(1:所有数据权限;2:自定义数据权限;3:本部门数据权限;4:本部门及以下数据权限;5:仅本人数据权限)
+ */
+ private String dataScope;
+
+}
diff --git a/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/domain/dto/UserDTO.java b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/domain/dto/UserDTO.java
new file mode 100644
index 0000000..102f291
--- /dev/null
+++ b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/domain/dto/UserDTO.java
@@ -0,0 +1,73 @@
+package com.pusong.common.core.domain.dto;
+
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.Date;
+
+
+/**
+ * 用户
+ *
+ * @author Michelle.Chung
+ */
+@Data
+@NoArgsConstructor
+public class UserDTO implements Serializable {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 用户ID
+ */
+ private Long userId;
+
+ /**
+ * 部门ID
+ */
+ private Long deptId;
+
+ /**
+ * 用户账号
+ */
+ private String userName;
+
+ /**
+ * 用户昵称
+ */
+ private String nickName;
+
+ /**
+ * 用户类型(sys_user系统用户)
+ */
+ private String userType;
+
+ /**
+ * 用户邮箱
+ */
+ private String email;
+
+ /**
+ * 手机号码
+ */
+ private String phonenumber;
+
+ /**
+ * 用户性别(0男 1女 2未知)
+ */
+ private String sex;
+
+ /**
+ * 帐号状态(0正常 1停用)
+ */
+ private String status;
+
+ /**
+ * 创建时间
+ */
+ private Date createTime;
+
+}
diff --git a/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/domain/dto/UserOnlineDTO.java b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/domain/dto/UserOnlineDTO.java
new file mode 100644
index 0000000..55a6f62
--- /dev/null
+++ b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/domain/dto/UserOnlineDTO.java
@@ -0,0 +1,72 @@
+package com.pusong.common.core.domain.dto;
+
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * 当前在线会话
+ *
+ * @author ruoyi
+ */
+
+@Data
+@NoArgsConstructor
+public class UserOnlineDTO implements Serializable {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 会话编号
+ */
+ private String tokenId;
+
+ /**
+ * 部门名称
+ */
+ private String deptName;
+
+ /**
+ * 用户名称
+ */
+ private String userName;
+
+ /**
+ * 客户端
+ */
+ private String clientKey;
+
+ /**
+ * 设备类型
+ */
+ private String deviceType;
+
+ /**
+ * 登录IP地址
+ */
+ private String ipaddr;
+
+ /**
+ * 登录地址
+ */
+ private String loginLocation;
+
+ /**
+ * 浏览器类型
+ */
+ private String browser;
+
+ /**
+ * 操作系统
+ */
+ private String os;
+
+ /**
+ * 登录时间
+ */
+ private Long loginTime;
+
+}
diff --git a/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/domain/event/ProcessEvent.java b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/domain/event/ProcessEvent.java
new file mode 100644
index 0000000..b8aa2f0
--- /dev/null
+++ b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/domain/event/ProcessEvent.java
@@ -0,0 +1,41 @@
+package com.pusong.common.core.domain.event;
+
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * 总体流程监听
+ *
+ * @author may
+ */
+
+@Data
+public class ProcessEvent implements Serializable {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 流程定义key
+ */
+ private String key;
+
+ /**
+ * 业务id
+ */
+ private String businessKey;
+
+ /**
+ * 状态
+ */
+ private String status;
+
+ /**
+ * 当为true时为申请人节点办理
+ */
+ private boolean submit;
+
+
+}
diff --git a/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/domain/event/ProcessTaskEvent.java b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/domain/event/ProcessTaskEvent.java
new file mode 100644
index 0000000..fe0547d
--- /dev/null
+++ b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/domain/event/ProcessTaskEvent.java
@@ -0,0 +1,40 @@
+package com.pusong.common.core.domain.event;
+
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * 流程办理监听
+ *
+ * @author may
+ */
+
+@Data
+public class ProcessTaskEvent implements Serializable {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 流程定义key
+ */
+ private String key;
+
+ /**
+ * 审批节点key
+ */
+ private String taskDefinitionKey;
+
+ /**
+ * 任务id
+ */
+ private String taskId;
+
+ /**
+ * 业务id
+ */
+ private String businessKey;
+
+}
diff --git a/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/domain/model/EmailLoginBody.java b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/domain/model/EmailLoginBody.java
new file mode 100644
index 0000000..57a3150
--- /dev/null
+++ b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/domain/model/EmailLoginBody.java
@@ -0,0 +1,31 @@
+package com.pusong.common.core.domain.model;
+
+import jakarta.validation.constraints.Email;
+import jakarta.validation.constraints.NotBlank;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 邮件登录对象
+ *
+ * @author Lion Li
+ */
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class EmailLoginBody extends LoginBody {
+
+ /**
+ * 邮箱
+ */
+ @NotBlank(message = "{user.email.not.blank}")
+ @Email(message = "{user.email.not.valid}")
+ private String email;
+
+ /**
+ * 邮箱code
+ */
+ @NotBlank(message = "{email.code.not.blank}")
+ private String emailCode;
+
+}
diff --git a/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/domain/model/LoginBody.java b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/domain/model/LoginBody.java
new file mode 100644
index 0000000..9062093
--- /dev/null
+++ b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/domain/model/LoginBody.java
@@ -0,0 +1,48 @@
+package com.pusong.common.core.domain.model;
+
+import jakarta.validation.constraints.NotBlank;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * 用户登录对象
+ *
+ * @author Lion Li
+ */
+
+@Data
+public class LoginBody implements Serializable {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 客户端id
+ */
+ @NotBlank(message = "{auth.clientid.not.blank}")
+ private String clientId;
+
+ /**
+ * 授权类型
+ */
+ @NotBlank(message = "{auth.grant.type.not.blank}")
+ private String grantType;
+
+ /**
+ * 租户ID
+ */
+ private String tenantId;
+
+ /**
+ * 验证码
+ */
+ private String code;
+
+ /**
+ * 唯一标识
+ */
+ private String uuid;
+
+}
diff --git a/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/domain/model/LoginUser.java b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/domain/model/LoginUser.java
new file mode 100644
index 0000000..cfd6c38
--- /dev/null
+++ b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/domain/model/LoginUser.java
@@ -0,0 +1,148 @@
+package com.pusong.common.core.domain.model;
+
+import com.pusong.common.core.domain.dto.RoleDTO;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * 登录用户身份权限
+ *
+ * @author Lion Li
+ */
+@Data
+@NoArgsConstructor
+public class LoginUser implements Serializable {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 租户ID
+ */
+ private String tenantId;
+
+ /**
+ * 用户ID
+ */
+ private Long userId;
+
+ /**
+ * 部门ID
+ */
+ private Long deptId;
+
+ /**
+ * 部门类别编码
+ */
+ private String deptCategory;
+
+ /**
+ * 部门名
+ */
+ 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 String nickname;
+
+
+ /**
+ * 手机号
+ */
+ private String phonenumber;
+
+ /**
+ * 角色对象
+ */
+ private List roles;
+
+ /**
+ * 数据权限 当前角色ID
+ */
+ private Long roleId;
+
+ /**
+ * 客户端
+ */
+ private String clientKey;
+
+ /**
+ * 设备类型
+ */
+ private String deviceType;
+
+ /**
+ * 获取登录id
+ */
+ public String getLoginId() {
+ if (userType == null) {
+ throw new IllegalArgumentException("用户类型不能为空");
+ }
+ if (userId == null) {
+ throw new IllegalArgumentException("用户ID不能为空");
+ }
+ return userType + ":" + userId;
+ }
+
+}
diff --git a/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/domain/model/PasswordLoginBody.java b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/domain/model/PasswordLoginBody.java
new file mode 100644
index 0000000..48b6eca
--- /dev/null
+++ b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/domain/model/PasswordLoginBody.java
@@ -0,0 +1,32 @@
+package com.pusong.common.core.domain.model;
+
+import com.pusong.common.core.constant.UserConstants;
+import jakarta.validation.constraints.NotBlank;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.hibernate.validator.constraints.Length;
+
+/**
+ * 密码登录对象
+ *
+ * @author Lion Li
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class PasswordLoginBody extends 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;
+
+}
diff --git a/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/domain/model/RegisterBody.java b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/domain/model/RegisterBody.java
new file mode 100644
index 0000000..a011a07
--- /dev/null
+++ b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/domain/model/RegisterBody.java
@@ -0,0 +1,34 @@
+package com.pusong.common.core.domain.model;
+
+import com.pusong.common.core.constant.UserConstants;
+import jakarta.validation.constraints.NotBlank;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.hibernate.validator.constraints.Length;
+
+/**
+ * 用户注册对象
+ *
+ * @author Lion Li
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class RegisterBody extends 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 userType;
+
+}
diff --git a/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/domain/model/SmsLoginBody.java b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/domain/model/SmsLoginBody.java
new file mode 100644
index 0000000..7474178
--- /dev/null
+++ b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/domain/model/SmsLoginBody.java
@@ -0,0 +1,29 @@
+package com.pusong.common.core.domain.model;
+
+import jakarta.validation.constraints.NotBlank;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 短信登录对象
+ *
+ * @author Lion Li
+ */
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class SmsLoginBody extends LoginBody {
+
+ /**
+ * 手机号
+ */
+ @NotBlank(message = "{user.phonenumber.not.blank}")
+ private String phonenumber;
+
+ /**
+ * 短信code
+ */
+ @NotBlank(message = "{sms.code.not.blank}")
+ private String smsCode;
+
+}
diff --git a/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/domain/model/SocialLoginBody.java b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/domain/model/SocialLoginBody.java
new file mode 100644
index 0000000..f1b1c37
--- /dev/null
+++ b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/domain/model/SocialLoginBody.java
@@ -0,0 +1,35 @@
+package com.pusong.common.core.domain.model;
+
+import jakarta.validation.constraints.NotBlank;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 三方登录对象
+ *
+ * @author Lion Li
+ */
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class SocialLoginBody extends LoginBody {
+
+ /**
+ * 第三方登录平台
+ */
+ @NotBlank(message = "{social.source.not.blank}")
+ private String source;
+
+ /**
+ * 第三方登录code
+ */
+ @NotBlank(message = "{social.code.not.blank}")
+ private String socialCode;
+
+ /**
+ * 第三方登录socialState
+ */
+ @NotBlank(message = "{social.state.not.blank}")
+ private String socialState;
+
+}
diff --git a/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/domain/model/XcxLoginBody.java b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/domain/model/XcxLoginBody.java
new file mode 100644
index 0000000..3269a90
--- /dev/null
+++ b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/domain/model/XcxLoginBody.java
@@ -0,0 +1,28 @@
+package com.pusong.common.core.domain.model;
+
+import jakarta.validation.constraints.NotBlank;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 三方登录对象
+ *
+ * @author Lion Li
+ */
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class XcxLoginBody extends LoginBody {
+
+ /**
+ * 小程序id(多个小程序时使用)
+ */
+ private String appid;
+
+ /**
+ * 小程序code
+ */
+ @NotBlank(message = "{xcx.code.not.blank}")
+ private String xcxCode;
+
+}
diff --git a/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/domain/model/XcxLoginUser.java b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/domain/model/XcxLoginUser.java
new file mode 100644
index 0000000..b5625f4
--- /dev/null
+++ b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/domain/model/XcxLoginUser.java
@@ -0,0 +1,27 @@
+package com.pusong.common.core.domain.model;
+
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+
+import java.io.Serial;
+
+/**
+ * 小程序登录用户身份权限
+ *
+ * @author Lion Li
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@NoArgsConstructor
+public class XcxLoginUser extends LoginUser {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * openid
+ */
+ private String openid;
+
+}
diff --git a/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/enums/BusinessStatusEnum.java b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/enums/BusinessStatusEnum.java
new file mode 100644
index 0000000..d585f9b
--- /dev/null
+++ b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/enums/BusinessStatusEnum.java
@@ -0,0 +1,152 @@
+package com.pusong.common.core.enums;
+
+import cn.hutool.core.util.StrUtil;
+import com.pusong.common.core.utils.StringUtils;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import com.pusong.common.core.exception.ServiceException;
+
+import java.util.Arrays;
+
+/**
+ * 业务状态枚举
+ *
+ * @author may
+ */
+@Getter
+@AllArgsConstructor
+public enum BusinessStatusEnum {
+ /**
+ * 已撤销
+ */
+ CANCEL("cancel", "已撤销"),
+ /**
+ * 草稿
+ */
+ DRAFT("draft", "草稿"),
+ /**
+ * 待审核
+ */
+ WAITING("waiting", "待审核"),
+ /**
+ * 已完成
+ */
+ FINISH("finish", "已完成"),
+ /**
+ * 已作废
+ */
+ INVALID("invalid", "已作废"),
+ /**
+ * 已退回
+ */
+ BACK("back", "已退回"),
+ /**
+ * 已终止
+ */
+ TERMINATION("termination", "已终止");
+
+ /**
+ * 状态
+ */
+ private final String status;
+
+ /**
+ * 描述
+ */
+ private final String desc;
+
+ /**
+ * 获取业务状态
+ *
+ * @param status 状态
+ */
+ public static String findByStatus(String status) {
+ if (StringUtils.isBlank(status)) {
+ return StrUtil.EMPTY;
+ }
+ return Arrays.stream(BusinessStatusEnum.values())
+ .filter(statusEnum -> statusEnum.getStatus().equals(status))
+ .findFirst()
+ .map(BusinessStatusEnum::getDesc)
+ .orElse(StrUtil.EMPTY);
+ }
+
+ /**
+ * 启动流程校验
+ *
+ * @param status 状态
+ */
+ public static void checkStartStatus(String status) {
+ if (WAITING.getStatus().equals(status)) {
+ throw new ServiceException("该单据已提交过申请,正在审批中!");
+ } else if (FINISH.getStatus().equals(status)) {
+ throw new ServiceException("该单据已完成申请!");
+ } else if (INVALID.getStatus().equals(status)) {
+ throw new ServiceException("该单据已作废!");
+ } else if (TERMINATION.getStatus().equals(status)) {
+ throw new ServiceException("该单据已终止!");
+ } else if (StringUtils.isBlank(status)) {
+ throw new ServiceException("流程状态为空!");
+ }
+ }
+
+ /**
+ * 撤销流程校验
+ *
+ * @param status 状态
+ */
+ public static void checkCancelStatus(String status) {
+ if (CANCEL.getStatus().equals(status)) {
+ throw new ServiceException("该单据已撤销!");
+ } else if (FINISH.getStatus().equals(status)) {
+ throw new ServiceException("该单据已完成申请!");
+ } else if (INVALID.getStatus().equals(status)) {
+ throw new ServiceException("该单据已作废!");
+ } else if (TERMINATION.getStatus().equals(status)) {
+ throw new ServiceException("该单据已终止!");
+ } else if (BACK.getStatus().equals(status)) {
+ throw new ServiceException("该单据已退回!");
+ } else if (StringUtils.isBlank(status)) {
+ throw new ServiceException("流程状态为空!");
+ }
+ }
+
+ /**
+ * 驳回流程校验
+ *
+ * @param status 状态
+ */
+ public static void checkBackStatus(String status) {
+ if (BACK.getStatus().equals(status)) {
+ throw new ServiceException("该单据已退回!");
+ } else if (FINISH.getStatus().equals(status)) {
+ throw new ServiceException("该单据已完成申请!");
+ } else if (INVALID.getStatus().equals(status)) {
+ throw new ServiceException("该单据已作废!");
+ } else if (TERMINATION.getStatus().equals(status)) {
+ throw new ServiceException("该单据已终止!");
+ } else if (CANCEL.getStatus().equals(status)) {
+ throw new ServiceException("该单据已撤销!");
+ } else if (StringUtils.isBlank(status)) {
+ throw new ServiceException("流程状态为空!");
+ }
+ }
+
+ /**
+ * 作废,终止流程校验
+ *
+ * @param status 状态
+ */
+ public static void checkInvalidStatus(String status) {
+ if (FINISH.getStatus().equals(status)) {
+ throw new ServiceException("该单据已完成申请!");
+ } else if (INVALID.getStatus().equals(status)) {
+ throw new ServiceException("该单据已作废!");
+ } else if (TERMINATION.getStatus().equals(status)) {
+ throw new ServiceException("该单据已终止!");
+ } else if (StringUtils.isBlank(status)) {
+ throw new ServiceException("流程状态为空!");
+ }
+ }
+}
+
diff --git a/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/enums/DeviceType.java b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/enums/DeviceType.java
new file mode 100644
index 0000000..cbb4ac5
--- /dev/null
+++ b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/enums/DeviceType.java
@@ -0,0 +1,37 @@
+package com.pusong.common.core.enums;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * 设备类型
+ * 针对一套 用户体系
+ *
+ * @author Lion Li
+ */
+@Getter
+@AllArgsConstructor
+public enum DeviceType {
+
+ /**
+ * pc端
+ */
+ PC("pc"),
+
+ /**
+ * app端
+ */
+ APP("app"),
+
+ /**
+ * 小程序端
+ */
+ XCX("xcx"),
+
+ /**
+ * social第三方端
+ */
+ SOCIAL("social");
+
+ private final String device;
+}
diff --git a/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/enums/LoginType.java b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/enums/LoginType.java
new file mode 100644
index 0000000..16ef458
--- /dev/null
+++ b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/enums/LoginType.java
@@ -0,0 +1,44 @@
+package com.pusong.common.core.enums;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * 登录类型
+ *
+ * @author Lion Li
+ */
+@Getter
+@AllArgsConstructor
+public enum LoginType {
+
+ /**
+ * 密码登录
+ */
+ PASSWORD("user.password.retry.limit.exceed", "user.password.retry.limit.count"),
+
+ /**
+ * 短信登录
+ */
+ SMS("sms.code.retry.limit.exceed", "sms.code.retry.limit.count"),
+
+ /**
+ * 邮箱登录
+ */
+ EMAIL("email.code.retry.limit.exceed", "email.code.retry.limit.count"),
+
+ /**
+ * 小程序登录
+ */
+ XCX("", "");
+
+ /**
+ * 登录重试超出限制提示
+ */
+ final String retryLimitExceed;
+
+ /**
+ * 登录重试限制计数提示
+ */
+ final String retryLimitCount;
+}
diff --git a/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/enums/TenantStatus.java b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/enums/TenantStatus.java
new file mode 100644
index 0000000..7421c68
--- /dev/null
+++ b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/enums/TenantStatus.java
@@ -0,0 +1,30 @@
+package com.pusong.common.core.enums;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * 用户状态
+ *
+ * @author LionLi
+ */
+@Getter
+@AllArgsConstructor
+public enum TenantStatus {
+ /**
+ * 正常
+ */
+ OK("0", "正常"),
+ /**
+ * 停用
+ */
+ DISABLE("1", "停用"),
+ /**
+ * 删除
+ */
+ DELETED("2", "删除");
+
+ private final String code;
+ private final String info;
+
+}
diff --git a/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/enums/UserStatus.java b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/enums/UserStatus.java
new file mode 100644
index 0000000..8d873fa
--- /dev/null
+++ b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/enums/UserStatus.java
@@ -0,0 +1,30 @@
+package com.pusong.common.core.enums;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * 用户状态
+ *
+ * @author ruoyi
+ */
+@Getter
+@AllArgsConstructor
+public enum UserStatus {
+ /**
+ * 正常
+ */
+ OK("0", "正常"),
+ /**
+ * 停用
+ */
+ DISABLE("1", "停用"),
+ /**
+ * 删除
+ */
+ DELETED("2", "删除");
+
+ private final String code;
+ private final String info;
+
+}
diff --git a/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/enums/UserType.java b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/enums/UserType.java
new file mode 100644
index 0000000..7bfb64e
--- /dev/null
+++ b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/enums/UserType.java
@@ -0,0 +1,37 @@
+package com.pusong.common.core.enums;
+
+import com.pusong.common.core.utils.StringUtils;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * 设备类型
+ * 针对多套 用户体系
+ *
+ * @author Lion Li
+ */
+@Getter
+@AllArgsConstructor
+public enum UserType {
+
+ /**
+ * pc端
+ */
+ SYS_USER("sys_user"),
+
+ /**
+ * app端
+ */
+ APP_USER("app_user");
+
+ private final String userType;
+
+ public static UserType getUserType(String str) {
+ for (UserType value : values()) {
+ if (StringUtils.contains(str, value.getUserType())) {
+ return value;
+ }
+ }
+ throw new RuntimeException("'UserType' not found By " + str);
+ }
+}
diff --git a/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/exception/ServiceException.java b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/exception/ServiceException.java
new file mode 100644
index 0000000..7a28eb3
--- /dev/null
+++ b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/exception/ServiceException.java
@@ -0,0 +1,59 @@
+package com.pusong.common.core.exception;
+
+import lombok.*;
+
+import java.io.Serial;
+
+/**
+ * 业务异常
+ *
+ * @author ruoyi
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@NoArgsConstructor
+@AllArgsConstructor
+public final class ServiceException extends RuntimeException {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 错误码
+ */
+ private Integer code;
+
+ /**
+ * 错误提示
+ */
+ private String message;
+
+ /**
+ * 错误明细,内部调试错误
+ */
+ private String detailMessage;
+
+ public ServiceException(String message) {
+ this.message = message;
+ }
+
+ public ServiceException(String message, Integer code) {
+ this.message = message;
+ this.code = code;
+ }
+
+ @Override
+ public String getMessage() {
+ return message;
+ }
+
+ public ServiceException setMessage(String message) {
+ this.message = message;
+ return this;
+ }
+
+ public ServiceException setDetailMessage(String detailMessage) {
+ this.detailMessage = detailMessage;
+ return this;
+ }
+}
diff --git a/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/exception/base/BaseException.java b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/exception/base/BaseException.java
new file mode 100644
index 0000000..757b93c
--- /dev/null
+++ b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/exception/base/BaseException.java
@@ -0,0 +1,74 @@
+package com.pusong.common.core.exception.base;
+
+import com.pusong.common.core.utils.MessageUtils;
+import com.pusong.common.core.utils.StringUtils;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+
+import java.io.Serial;
+
+/**
+ * 基础异常
+ *
+ * @author ruoyi
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@NoArgsConstructor
+@AllArgsConstructor
+public class BaseException extends RuntimeException {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 所属模块
+ */
+ private String module;
+
+ /**
+ * 错误码
+ */
+ private String code;
+
+ /**
+ * 错误码对应的参数
+ */
+ private Object[] args;
+
+ /**
+ * 错误消息
+ */
+ private String defaultMessage;
+
+ public BaseException(String module, String code, Object[] args) {
+ this(module, code, args, null);
+ }
+
+ public BaseException(String module, String defaultMessage) {
+ this(module, null, null, defaultMessage);
+ }
+
+ public BaseException(String code, Object[] args) {
+ this(null, code, args, null);
+ }
+
+ public BaseException(String defaultMessage) {
+ this(null, null, null, defaultMessage);
+ }
+
+ @Override
+ public String getMessage() {
+ String message = null;
+ if (!StringUtils.isEmpty(code)) {
+ message = MessageUtils.message(code, args);
+ }
+ if (message == null) {
+ message = defaultMessage;
+ }
+ return message;
+ }
+
+}
diff --git a/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/exception/file/FileException.java b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/exception/file/FileException.java
new file mode 100644
index 0000000..ce2b466
--- /dev/null
+++ b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/exception/file/FileException.java
@@ -0,0 +1,21 @@
+package com.pusong.common.core.exception.file;
+
+import com.pusong.common.core.exception.base.BaseException;
+
+import java.io.Serial;
+
+/**
+ * 文件信息异常类
+ *
+ * @author ruoyi
+ */
+public class FileException extends BaseException {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ public FileException(String code, Object[] args) {
+ super("file", code, args, null);
+ }
+
+}
diff --git a/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/exception/file/FileNameLengthLimitExceededException.java b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/exception/file/FileNameLengthLimitExceededException.java
new file mode 100644
index 0000000..eb114d3
--- /dev/null
+++ b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/exception/file/FileNameLengthLimitExceededException.java
@@ -0,0 +1,18 @@
+package com.pusong.common.core.exception.file;
+
+import java.io.Serial;
+
+/**
+ * 文件名称超长限制异常类
+ *
+ * @author ruoyi
+ */
+public class FileNameLengthLimitExceededException extends FileException {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ public FileNameLengthLimitExceededException(int defaultFileNameLength) {
+ super("upload.filename.exceed.length", new Object[]{defaultFileNameLength});
+ }
+}
diff --git a/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/exception/file/FileSizeLimitExceededException.java b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/exception/file/FileSizeLimitExceededException.java
new file mode 100644
index 0000000..5991011
--- /dev/null
+++ b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/exception/file/FileSizeLimitExceededException.java
@@ -0,0 +1,18 @@
+package com.pusong.common.core.exception.file;
+
+import java.io.Serial;
+
+/**
+ * 文件名大小限制异常类
+ *
+ * @author ruoyi
+ */
+public class FileSizeLimitExceededException extends FileException {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ public FileSizeLimitExceededException(long defaultMaxSize) {
+ super("upload.exceed.maxSize", new Object[]{defaultMaxSize});
+ }
+}
diff --git a/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/exception/user/CaptchaException.java b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/exception/user/CaptchaException.java
new file mode 100644
index 0000000..3af1db7
--- /dev/null
+++ b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/exception/user/CaptchaException.java
@@ -0,0 +1,18 @@
+package com.pusong.common.core.exception.user;
+
+import java.io.Serial;
+
+/**
+ * 验证码错误异常类
+ *
+ * @author ruoyi
+ */
+public class CaptchaException extends UserException {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ public CaptchaException() {
+ super("user.jcaptcha.error");
+ }
+}
diff --git a/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/exception/user/CaptchaExpireException.java b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/exception/user/CaptchaExpireException.java
new file mode 100644
index 0000000..c235c5e
--- /dev/null
+++ b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/exception/user/CaptchaExpireException.java
@@ -0,0 +1,18 @@
+package com.pusong.common.core.exception.user;
+
+import java.io.Serial;
+
+/**
+ * 验证码失效异常类
+ *
+ * @author ruoyi
+ */
+public class CaptchaExpireException extends UserException {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ public CaptchaExpireException() {
+ super("user.jcaptcha.expire");
+ }
+}
diff --git a/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/exception/user/UserException.java b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/exception/user/UserException.java
new file mode 100644
index 0000000..db29ca7
--- /dev/null
+++ b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/exception/user/UserException.java
@@ -0,0 +1,20 @@
+package com.pusong.common.core.exception.user;
+
+import com.pusong.common.core.exception.base.BaseException;
+
+import java.io.Serial;
+
+/**
+ * 用户信息异常类
+ *
+ * @author ruoyi
+ */
+public class UserException extends BaseException {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ public UserException(String code, Object... args) {
+ super("user", code, args, null);
+ }
+}
diff --git a/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/factory/RegexPatternPoolFactory.java b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/factory/RegexPatternPoolFactory.java
new file mode 100644
index 0000000..f244416
--- /dev/null
+++ b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/factory/RegexPatternPoolFactory.java
@@ -0,0 +1,52 @@
+package com.pusong.common.core.factory;
+
+import cn.hutool.core.lang.PatternPool;
+import com.pusong.common.core.constant.RegexConstants;
+
+import java.util.regex.Pattern;
+
+/**
+ * 正则表达式模式池工厂
+ * 初始化的时候将正则表达式加入缓存池当中
+ * 提高正则表达式的性能,避免重复编译相同的正则表达式
+ *
+ * @author 21001
+ */
+public class RegexPatternPoolFactory extends PatternPool {
+
+ /**
+ * 字典类型必须以字母开头,且只能为(小写字母,数字,下滑线)
+ */
+ public static final Pattern DICTIONARY_TYPE = get(RegexConstants.DICTIONARY_TYPE);
+
+ /**
+ * 身份证号码(后6位)
+ */
+ public static final Pattern ID_CARD_LAST_6 = get(RegexConstants.ID_CARD_LAST_6);
+
+ /**
+ * QQ号码
+ */
+ public static final Pattern QQ_NUMBER = get(RegexConstants.QQ_NUMBER);
+
+ /**
+ * 邮政编码
+ */
+ public static final Pattern POSTAL_CODE = get(RegexConstants.POSTAL_CODE);
+
+ /**
+ * 注册账号
+ */
+ public static final Pattern ACCOUNT = get(RegexConstants.ACCOUNT);
+
+ /**
+ * 密码:包含至少8个字符,包括大写字母、小写字母、数字和特殊字符
+ */
+ public static final Pattern PASSWORD = get(RegexConstants.PASSWORD);
+
+ /**
+ * 通用状态(0表示正常,1表示停用)
+ */
+ public static final Pattern STATUS = get(RegexConstants.STATUS);
+
+}
diff --git a/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/factory/YmlPropertySourceFactory.java b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/factory/YmlPropertySourceFactory.java
new file mode 100644
index 0000000..9587d75
--- /dev/null
+++ b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/factory/YmlPropertySourceFactory.java
@@ -0,0 +1,31 @@
+package com.pusong.common.core.factory;
+
+import com.pusong.common.core.utils.StringUtils;
+import org.springframework.beans.factory.config.YamlPropertiesFactoryBean;
+import org.springframework.core.env.PropertiesPropertySource;
+import org.springframework.core.env.PropertySource;
+import org.springframework.core.io.support.DefaultPropertySourceFactory;
+import org.springframework.core.io.support.EncodedResource;
+
+import java.io.IOException;
+
+/**
+ * yml 配置源工厂
+ *
+ * @author Lion Li
+ */
+public class YmlPropertySourceFactory extends DefaultPropertySourceFactory {
+
+ @Override
+ public PropertySource> createPropertySource(String name, EncodedResource resource) throws IOException {
+ String sourceName = resource.getResource().getFilename();
+ if (StringUtils.isNotBlank(sourceName) && StringUtils.endsWithAny(sourceName, ".yml", ".yaml")) {
+ YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean();
+ factory.setResources(resource.getResource());
+ factory.afterPropertiesSet();
+ return new PropertiesPropertySource(sourceName, factory.getObject());
+ }
+ return super.createPropertySource(name, resource);
+ }
+
+}
diff --git a/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/service/ConfigService.java b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/service/ConfigService.java
new file mode 100644
index 0000000..c59b286
--- /dev/null
+++ b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/service/ConfigService.java
@@ -0,0 +1,18 @@
+package com.pusong.common.core.service;
+
+/**
+ * 通用 参数配置服务
+ *
+ * @author Lion Li
+ */
+public interface ConfigService {
+
+ /**
+ * 根据参数 key 获取参数值
+ *
+ * @param configKey 参数 key
+ * @return 参数值
+ */
+ String getConfigValue(String configKey);
+
+}
diff --git a/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/service/DeptService.java b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/service/DeptService.java
new file mode 100644
index 0000000..e2ceb66
--- /dev/null
+++ b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/service/DeptService.java
@@ -0,0 +1,18 @@
+package com.pusong.common.core.service;
+
+/**
+ * 通用 部门服务
+ *
+ * @author Lion Li
+ */
+public interface DeptService {
+
+ /**
+ * 通过部门ID查询部门名称
+ *
+ * @param deptIds 部门ID串逗号分隔
+ * @return 部门名称串逗号分隔
+ */
+ String selectDeptNameByIds(String deptIds);
+
+}
diff --git a/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/service/DictService.java b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/service/DictService.java
new file mode 100644
index 0000000..8f8c595
--- /dev/null
+++ b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/service/DictService.java
@@ -0,0 +1,67 @@
+package com.pusong.common.core.service;
+
+import java.util.Map;
+
+/**
+ * 通用 字典服务
+ *
+ * @author Lion Li
+ */
+public interface DictService {
+
+ /**
+ * 分隔符
+ */
+ String SEPARATOR = ",";
+
+ /**
+ * 根据字典类型和字典值获取字典标签
+ *
+ * @param dictType 字典类型
+ * @param dictValue 字典值
+ * @return 字典标签
+ */
+ default String getDictLabel(String dictType, String dictValue) {
+ return getDictLabel(dictType, dictValue, SEPARATOR);
+ }
+
+ /**
+ * 根据字典类型和字典标签获取字典值
+ *
+ * @param dictType 字典类型
+ * @param dictLabel 字典标签
+ * @return 字典值
+ */
+ default String getDictValue(String dictType, String dictLabel) {
+ return getDictValue(dictType, dictLabel, SEPARATOR);
+ }
+
+ /**
+ * 根据字典类型和字典值获取字典标签
+ *
+ * @param dictType 字典类型
+ * @param dictValue 字典值
+ * @param separator 分隔符
+ * @return 字典标签
+ */
+ String getDictLabel(String dictType, String dictValue, String separator);
+
+ /**
+ * 根据字典类型和字典标签获取字典值
+ *
+ * @param dictType 字典类型
+ * @param dictLabel 字典标签
+ * @param separator 分隔符
+ * @return 字典值
+ */
+ String getDictValue(String dictType, String dictLabel, String separator);
+
+ /**
+ * 获取字典下所有的字典值与标签
+ *
+ * @param dictType 字典类型
+ * @return dictValue为key,dictLabel为值组成的Map
+ */
+ Map getAllDictByDictType(String dictType);
+
+}
diff --git a/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/service/OssService.java b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/service/OssService.java
new file mode 100644
index 0000000..bdf7c17
--- /dev/null
+++ b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/service/OssService.java
@@ -0,0 +1,29 @@
+package com.pusong.common.core.service;
+
+import com.pusong.common.core.domain.dto.OssDTO;
+
+import java.util.List;
+
+/**
+ * 通用 OSS服务
+ *
+ * @author Lion Li
+ */
+public interface OssService {
+
+ /**
+ * 通过ossId查询对应的url
+ *
+ * @param ossIds ossId串逗号分隔
+ * @return url串逗号分隔
+ */
+ String selectUrlByIds(String ossIds);
+
+ /**
+ * 通过ossId查询列表
+ *
+ * @param ossIds ossId串逗号分隔
+ * @return 列表
+ */
+ List selectByIds(String ossIds);
+}
diff --git a/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/service/PostService.java b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/service/PostService.java
new file mode 100644
index 0000000..3eaffa9
--- /dev/null
+++ b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/service/PostService.java
@@ -0,0 +1,18 @@
+package com.pusong.common.core.service;
+
+/**
+ * 通用 岗位服务
+ *
+ * @author Lion Li
+ */
+public interface PostService {
+
+ /**
+ * 通过岗位ID查询岗位名称
+ *
+ * @param deptIds 岗位ID串逗号分隔
+ * @return 岗位名称串逗号分隔
+ */
+ String selectPostNameByIds(String deptIds);
+
+}
diff --git a/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/service/UserService.java b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/service/UserService.java
new file mode 100644
index 0000000..943399e
--- /dev/null
+++ b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/service/UserService.java
@@ -0,0 +1,69 @@
+package com.pusong.common.core.service;
+
+import com.pusong.common.core.domain.dto.UserDTO;
+
+import java.util.List;
+
+/**
+ * 通用 用户服务
+ *
+ * @author Lion Li
+ */
+public interface UserService {
+
+ /**
+ * 通过用户ID查询用户账户
+ *
+ * @param userId 用户ID
+ * @return 用户账户
+ */
+ String selectUserNameById(Long userId);
+
+ /**
+ * 通过用户ID查询用户账户
+ *
+ * @param userId 用户ID
+ * @return 用户名称
+ */
+ String selectNicknameById(Long userId);
+
+ /**
+ * 通过用户ID查询用户账户
+ *
+ * @param userIds 用户ID 多个用逗号隔开
+ * @return 用户名称
+ */
+ String selectNicknameByIds(String userIds);
+
+ /**
+ * 通过用户ID查询用户手机号
+ *
+ * @param userId 用户id
+ * @return 用户手机号
+ */
+ String selectPhonenumberById(Long userId);
+
+ /**
+ * 通过用户ID查询用户邮箱
+ *
+ * @param userId 用户id
+ * @return 用户邮箱
+ */
+ String selectEmailById(Long userId);
+
+ /**
+ * 通过用户ID查询用户列表
+ *
+ * @param userIds 用户ids
+ * @return 用户列表
+ */
+ List selectListByIds(List userIds);
+
+ /**
+ * 通过角色ID查询用户ID
+ *
+ * @param roleIds 角色ids
+ * @return 用户ids
+ */
+ List selectUserIdsByRoleIds(List roleIds);
+}
diff --git a/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/service/WorkflowService.java b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/service/WorkflowService.java
new file mode 100644
index 0000000..a9bbdf2
--- /dev/null
+++ b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/service/WorkflowService.java
@@ -0,0 +1,76 @@
+package com.pusong.common.core.service;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 通用 工作流服务
+ *
+ * @author may
+ */
+public interface WorkflowService {
+
+ /**
+ * 运行中的实例 删除程实例,删除历史记录,删除业务与流程关联信息
+ *
+ * @param businessKeys 业务id
+ * @return 结果
+ */
+ boolean deleteRunAndHisInstance(List businessKeys);
+
+ /**
+ * 获取当前流程状态
+ *
+ * @param taskId 任务id
+ */
+ String getBusinessStatusByTaskId(String taskId);
+
+ /**
+ * 获取当前流程状态
+ *
+ * @param businessKey 业务id
+ */
+ String getBusinessStatus(String businessKey);
+
+ /**
+ * 设置流程变量(全局变量)
+ *
+ * @param taskId 任务id
+ * @param variableName 变量名称
+ * @param value 变量值
+ */
+ void setVariable(String taskId, String variableName, Object value);
+
+ /**
+ * 设置流程变量(全局变量)
+ *
+ * @param taskId 任务id
+ * @param variables 流程变量
+ */
+ void setVariables(String taskId, Map variables);
+
+ /**
+ * 设置流程变量(本地变量,非全局变量)
+ *
+ * @param taskId 任务id
+ * @param variableName 变量名称
+ * @param value 变量值
+ */
+ void setVariableLocal(String taskId, String variableName, Object value);
+
+ /**
+ * 设置流程变量(本地变量,非全局变量)
+ *
+ * @param taskId 任务id
+ * @param variables 流程变量
+ */
+ void setVariablesLocal(String taskId, Map variables);
+
+ /**
+ * 按照业务id查询流程实例id
+ *
+ * @param businessKey 业务id
+ * @return 结果
+ */
+ String getInstanceIdByBusinessKey(String businessKey);
+}
diff --git a/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/utils/DateUtils.java b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/utils/DateUtils.java
new file mode 100644
index 0000000..14b01ff
--- /dev/null
+++ b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/utils/DateUtils.java
@@ -0,0 +1,212 @@
+package com.pusong.common.core.utils;
+
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+import org.apache.commons.lang3.time.DateFormatUtils;
+
+import java.lang.management.ManagementFactory;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.time.*;
+import java.time.format.DateTimeFormatter;
+import java.time.temporal.ChronoUnit;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 时间工具类
+ *
+ * @author ruoyi
+ */
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public class DateUtils extends org.apache.commons.lang3.time.DateUtils {
+
+ public static final String YYYY = "yyyy";
+
+ public static final String YYYY_MM = "yyyy-MM";
+
+ public static final String YYYY_MM_DD = "yyyy-MM-dd";
+
+ public static final String YYYYMMDDHHMMSS = "yyyyMMddHHmmss";
+
+ public static final String YYYY_MM_DD_HH_MM_SS = "yyyy-MM-dd HH:mm:ss";
+
+ private static final String[] PARSE_PATTERNS = {
+ "yyyy-MM-dd", "yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd HH:mm", "yyyy-MM",
+ "yyyy/MM/dd", "yyyy/MM/dd HH:mm:ss", "yyyy/MM/dd HH:mm", "yyyy/MM",
+ "yyyy.MM.dd", "yyyy.MM.dd HH:mm:ss", "yyyy.MM.dd HH:mm", "yyyy.MM"};
+
+ /**
+ * 获取当前Date型日期
+ *
+ * @return Date() 当前日期
+ */
+ public static Date getNowDate() {
+ return new Date();
+ }
+
+ /**
+ * 获取当前日期, 默认格式为yyyy-MM-dd
+ *
+ * @return String
+ */
+ public static String getDate() {
+ return dateTimeNow(YYYY_MM_DD);
+ }
+
+ public static String getTime() {
+ return dateTimeNow(YYYY_MM_DD_HH_MM_SS);
+ }
+
+ public static String dateTimeNow() {
+ return dateTimeNow(YYYYMMDDHHMMSS);
+ }
+
+ public static String dateTimeNow(final String format) {
+ return parseDateToStr(format, new Date());
+ }
+
+ public static String dateTime(final Date date) {
+ return parseDateToStr(YYYY_MM_DD, date);
+ }
+
+ public static String parseDateToStr(final String format, final Date date) {
+ return new SimpleDateFormat(format).format(date);
+ }
+
+ public static Date dateTime(final String format, final String ts) {
+ try {
+ return new SimpleDateFormat(format).parse(ts);
+ } catch (ParseException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * 日期路径 即年/月/日 如2018/08/08
+ */
+ public static String datePath() {
+ Date now = new Date();
+ return DateFormatUtils.format(now, "yyyy/MM/dd");
+ }
+
+ /**
+ * 日期路径 即年/月/日 如20180808
+ */
+ public static String dateTime() {
+ Date now = new Date();
+ return DateFormatUtils.format(now, "yyyyMMdd");
+ }
+
+ /**
+ * 日期型字符串转化为日期 格式
+ */
+ public static Date parseDate(Object str) {
+ if (str == null) {
+ return null;
+ }
+ try {
+ return parseDate(str.toString(), PARSE_PATTERNS);
+ } catch (ParseException e) {
+ return null;
+ }
+ }
+
+ /**
+ * 获取服务器启动时间
+ */
+ public static Date getServerStartDate() {
+ long time = ManagementFactory.getRuntimeMXBean().getStartTime();
+ return new Date(time);
+ }
+
+ /**
+ * 计算相差天数
+ */
+ public static int differentDaysByMillisecond(Date date1, Date date2) {
+ return (int) ((date2.getTime() - date1.getTime()) / (1000 * 3600 * 24));
+ }
+ /**
+ * 计算相差天数(只计算工作日:周一到周五,包括两边)
+ */
+ public static int calWorkDate(Date date1, Date date2) {
+ LocalDate startDate = date1.toInstant()
+ .atZone(ZoneId.systemDefault())
+ .toLocalDate();
+ LocalDate endDate = date2.toInstant()
+ .atZone(ZoneId.systemDefault())
+ .toLocalDate();
+
+ int i = 0;
+ while (!startDate.isAfter(endDate)) {
+ if (!startDate.getDayOfWeek().equals(DayOfWeek.SATURDAY)
+ && !startDate.getDayOfWeek().equals(DayOfWeek.SUNDAY)) {
+ i++;
+ }
+ startDate = startDate.plusDays(1);
+ }
+ return i;
+ }
+
+ /**
+ * 计算两个时间差
+ */
+ public static String getDatePoor(Date endDate, Date nowDate) {
+ long nd = 1000 * 24 * 60 * 60;
+ long nh = 1000 * 60 * 60;
+ long nm = 1000 * 60;
+ // long ns = 1000;
+ // 获得两个时间的毫秒时间差异
+ long diff = endDate.getTime() - nowDate.getTime();
+ // 计算差多少天
+ long day = diff / nd;
+ // 计算差多少小时
+ long hour = diff % nd / nh;
+ // 计算差多少分钟
+ long min = diff % nd % nh / nm;
+ // 计算差多少秒//输出结果
+ // long sec = diff % nd % nh % nm / ns;
+ return day + "天" + hour + "小时" + min + "分钟";
+ }
+
+ /**
+ * 增加 LocalDateTime ==> Date
+ */
+ public static Date toDate(LocalDateTime temporalAccessor) {
+ ZonedDateTime zdt = temporalAccessor.atZone(ZoneId.systemDefault());
+ return Date.from(zdt.toInstant());
+ }
+
+ /**
+ * 增加 LocalDate ==> Date
+ */
+ public static Date toDate(LocalDate temporalAccessor) {
+ LocalDateTime localDateTime = LocalDateTime.of(temporalAccessor, LocalTime.of(0, 0, 0));
+ ZonedDateTime zdt = localDateTime.atZone(ZoneId.systemDefault());
+ return Date.from(zdt.toInstant());
+ }
+
+ public static String toString(LocalDate date,String from){
+ // 创建一个DateTimeFormatter实例来定义日期格式
+ DateTimeFormatter formatter = DateTimeFormatter.ofPattern(from);
+
+ // 使用formatter转换LocalDate到字符串
+ return date.format(formatter);
+ }
+
+ public static String toString(LocalDate date){
+ return toString(date,"yyyy-MM-dd");
+ }
+
+ public static long differentMonth(Date date,Date date2){
+ LocalDate startDate = date.toInstant()
+ .atZone(ZoneId.systemDefault())
+ .toLocalDate();
+ LocalDate endDate = date2.toInstant()
+ .atZone(ZoneId.systemDefault())
+ .toLocalDate();
+ return ChronoUnit.MONTHS.between(startDate.withDayOfMonth(1), endDate.withDayOfMonth(1));
+
+ }
+}
diff --git a/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/utils/MapstructUtils.java b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/utils/MapstructUtils.java
new file mode 100644
index 0000000..61c40e1
--- /dev/null
+++ b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/utils/MapstructUtils.java
@@ -0,0 +1,93 @@
+package com.pusong.common.core.utils;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.map.MapUtil;
+import cn.hutool.core.util.ObjectUtil;
+import io.github.linpeilie.Converter;
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Mapstruct 工具类
+ * 参考文档:mapstruct-plus
+ *
+ *
+ * @author Michelle.Chung
+ */
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public class MapstructUtils {
+
+ private final static Converter CONVERTER = SpringUtils.getBean(Converter.class);
+
+ /**
+ * 将 T 类型对象,转换为 desc 类型的对象并返回
+ *
+ * @param source 数据来源实体
+ * @param desc 描述对象 转换后的对象
+ * @return desc
+ */
+ public static V convert(T source, Class desc) {
+ if (ObjectUtil.isNull(source)) {
+ return null;
+ }
+ if (ObjectUtil.isNull(desc)) {
+ return null;
+ }
+ return CONVERTER.convert(source, desc);
+ }
+
+ /**
+ * 将 T 类型对象,按照配置的映射字段规则,给 desc 类型的对象赋值并返回 desc 对象
+ *
+ * @param source 数据来源实体
+ * @param desc 转换后的对象
+ * @return desc
+ */
+ public static V convert(T source, V desc) {
+ if (ObjectUtil.isNull(source)) {
+ return null;
+ }
+ if (ObjectUtil.isNull(desc)) {
+ return null;
+ }
+ return CONVERTER.convert(source, desc);
+ }
+
+ /**
+ * 将 T 类型的集合,转换为 desc 类型的集合并返回
+ *
+ * @param sourceList 数据来源实体列表
+ * @param desc 描述对象 转换后的对象
+ * @return desc
+ */
+ public static List convert(List sourceList, Class desc) {
+ if (ObjectUtil.isNull(sourceList)) {
+ return null;
+ }
+ if (CollUtil.isEmpty(sourceList)) {
+ return CollUtil.newArrayList();
+ }
+ return CONVERTER.convert(sourceList, desc);
+ }
+
+ /**
+ * 将 Map 转换为 beanClass 类型的集合并返回
+ *
+ * @param map 数据来源
+ * @param beanClass bean类
+ * @return bean对象
+ */
+ public static T convert(Map map, Class beanClass) {
+ if (MapUtil.isEmpty(map)) {
+ return null;
+ }
+ if (ObjectUtil.isNull(beanClass)) {
+ return null;
+ }
+ return CONVERTER.convert(map, beanClass);
+ }
+
+}
diff --git a/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/utils/MessageUtils.java b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/utils/MessageUtils.java
new file mode 100644
index 0000000..8a9449e
--- /dev/null
+++ b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/utils/MessageUtils.java
@@ -0,0 +1,33 @@
+package com.pusong.common.core.utils;
+
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+import org.springframework.context.MessageSource;
+import org.springframework.context.NoSuchMessageException;
+import org.springframework.context.i18n.LocaleContextHolder;
+
+/**
+ * 获取i18n资源文件
+ *
+ * @author Lion Li
+ */
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public class MessageUtils {
+
+ private static final MessageSource MESSAGE_SOURCE = SpringUtils.getBean(MessageSource.class);
+
+ /**
+ * 根据消息键和参数 获取消息 委托给spring messageSource
+ *
+ * @param code 消息键
+ * @param args 参数
+ * @return 获取国际化翻译值
+ */
+ public static String message(String code, Object... args) {
+ try {
+ return MESSAGE_SOURCE.getMessage(code, args, LocaleContextHolder.getLocale());
+ } catch (NoSuchMessageException e) {
+ return code;
+ }
+ }
+}
diff --git a/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/utils/ServletUtils.java b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/utils/ServletUtils.java
new file mode 100644
index 0000000..bb8c820
--- /dev/null
+++ b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/utils/ServletUtils.java
@@ -0,0 +1,228 @@
+package com.pusong.common.core.utils;
+
+import cn.hutool.core.convert.Convert;
+import cn.hutool.extra.servlet.JakartaServletUtil;
+import cn.hutool.http.HttpStatus;
+import jakarta.servlet.ServletRequest;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+import org.springframework.http.MediaType;
+import org.springframework.util.LinkedCaseInsensitiveMap;
+import org.springframework.web.context.request.RequestAttributes;
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
+
+import java.io.IOException;
+import java.net.URLDecoder;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 客户端工具类
+ *
+ * @author ruoyi
+ */
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public class ServletUtils extends JakartaServletUtil {
+
+ /**
+ * 获取String参数
+ */
+ public static String getParameter(String name) {
+ return getRequest().getParameter(name);
+ }
+
+ /**
+ * 获取String参数
+ */
+ public static String getParameter(String name, String defaultValue) {
+ return Convert.toStr(getRequest().getParameter(name), defaultValue);
+ }
+
+ /**
+ * 获取Integer参数
+ */
+ public static Integer getParameterToInt(String name) {
+ return Convert.toInt(getRequest().getParameter(name));
+ }
+
+ /**
+ * 获取Integer参数
+ */
+ public static Integer getParameterToInt(String name, Integer defaultValue) {
+ return Convert.toInt(getRequest().getParameter(name), defaultValue);
+ }
+
+ /**
+ * 获取Boolean参数
+ */
+ public static Boolean getParameterToBool(String name) {
+ return Convert.toBool(getRequest().getParameter(name));
+ }
+
+ /**
+ * 获取Boolean参数
+ */
+ public static Boolean getParameterToBool(String name, Boolean defaultValue) {
+ return Convert.toBool(getRequest().getParameter(name), defaultValue);
+ }
+
+ /**
+ * 获得所有请求参数
+ *
+ * @param request 请求对象{@link ServletRequest}
+ * @return Map
+ */
+ public static Map getParams(ServletRequest request) {
+ final Map map = request.getParameterMap();
+ return Collections.unmodifiableMap(map);
+ }
+
+ /**
+ * 获得所有请求参数
+ *
+ * @param request 请求对象{@link ServletRequest}
+ * @return Map
+ */
+ public static Map getParamMap(ServletRequest request) {
+ Map params = new HashMap<>();
+ for (Map.Entry entry : getParams(request).entrySet()) {
+ params.put(entry.getKey(), StringUtils.join(entry.getValue(), StringUtils.SEPARATOR));
+ }
+ return params;
+ }
+
+ /**
+ * 获取request
+ */
+ public static HttpServletRequest getRequest() {
+ try {
+ return getRequestAttributes().getRequest();
+ } catch (Exception e) {
+ return null;
+ }
+ }
+
+ /**
+ * 获取response
+ */
+ public static HttpServletResponse getResponse() {
+ try {
+ return getRequestAttributes().getResponse();
+ } catch (Exception e) {
+ return null;
+ }
+ }
+
+ /**
+ * 获取session
+ */
+ public static HttpSession getSession() {
+ return getRequest().getSession();
+ }
+
+ public static ServletRequestAttributes getRequestAttributes() {
+ try {
+ RequestAttributes attributes = RequestContextHolder.getRequestAttributes();
+ return (ServletRequestAttributes) attributes;
+ } catch (Exception e) {
+ return null;
+ }
+ }
+
+ public static String getHeader(HttpServletRequest request, String name) {
+ String value = request.getHeader(name);
+ if (StringUtils.isEmpty(value)) {
+ return StringUtils.EMPTY;
+ }
+ return urlDecode(value);
+ }
+
+ public static Map getHeaders(HttpServletRequest request) {
+ Map map = new LinkedCaseInsensitiveMap<>();
+ Enumeration enumeration = request.getHeaderNames();
+ if (enumeration != null) {
+ while (enumeration.hasMoreElements()) {
+ String key = enumeration.nextElement();
+ String value = request.getHeader(key);
+ map.put(key, value);
+ }
+ }
+ return map;
+ }
+
+ /**
+ * 将字符串渲染到客户端
+ *
+ * @param response 渲染对象
+ * @param string 待渲染的字符串
+ */
+ public static void renderString(HttpServletResponse response, String string) {
+ try {
+ response.setStatus(HttpStatus.HTTP_OK);
+ response.setContentType(MediaType.APPLICATION_JSON_VALUE);
+ response.setCharacterEncoding(StandardCharsets.UTF_8.toString());
+ response.getWriter().print(string);
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * 是否是Ajax异步请求
+ *
+ * @param request
+ */
+ public static boolean isAjaxRequest(HttpServletRequest request) {
+
+ String accept = request.getHeader("accept");
+ if (accept != null && accept.contains(MediaType.APPLICATION_JSON_VALUE)) {
+ return true;
+ }
+
+ String xRequestedWith = request.getHeader("X-Requested-With");
+ if (xRequestedWith != null && xRequestedWith.contains("XMLHttpRequest")) {
+ return true;
+ }
+
+ String uri = request.getRequestURI();
+ if (StringUtils.equalsAnyIgnoreCase(uri, ".json", ".xml")) {
+ return true;
+ }
+
+ String ajax = request.getParameter("__ajax");
+ return StringUtils.equalsAnyIgnoreCase(ajax, "json", "xml");
+ }
+
+ public static String getClientIP() {
+ return getClientIP(getRequest());
+ }
+
+ /**
+ * 内容编码
+ *
+ * @param str 内容
+ * @return 编码后的内容
+ */
+ public static String urlEncode(String str) {
+ return URLEncoder.encode(str, StandardCharsets.UTF_8);
+ }
+
+ /**
+ * 内容解码
+ *
+ * @param str 内容
+ * @return 解码后的内容
+ */
+ public static String urlDecode(String str) {
+ return URLDecoder.decode(str, StandardCharsets.UTF_8);
+ }
+
+}
diff --git a/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/utils/SpringUtils.java b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/utils/SpringUtils.java
new file mode 100644
index 0000000..5e0da73
--- /dev/null
+++ b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/utils/SpringUtils.java
@@ -0,0 +1,67 @@
+package com.pusong.common.core.utils;
+
+import cn.hutool.extra.spring.SpringUtil;
+import org.springframework.beans.factory.NoSuchBeanDefinitionException;
+import org.springframework.boot.autoconfigure.thread.Threading;
+import org.springframework.context.ApplicationContext;
+import org.springframework.core.env.Environment;
+import org.springframework.stereotype.Component;
+
+/**
+ * spring工具类
+ *
+ * @author Lion Li
+ */
+@Component
+public final class SpringUtils extends SpringUtil {
+
+ /**
+ * 如果BeanFactory包含一个与所给名称匹配的bean定义,则返回true
+ */
+ public static boolean containsBean(String name) {
+ return getBeanFactory().containsBean(name);
+ }
+
+ /**
+ * 判断以给定名字注册的bean定义是一个singleton还是一个prototype。
+ * 如果与给定名字相应的bean定义没有被找到,将会抛出一个异常(NoSuchBeanDefinitionException)
+ */
+ public static boolean isSingleton(String name) throws NoSuchBeanDefinitionException {
+ return getBeanFactory().isSingleton(name);
+ }
+
+ /**
+ * @return Class 注册对象的类型
+ */
+ public static Class> getType(String name) throws NoSuchBeanDefinitionException {
+ return getBeanFactory().getType(name);
+ }
+
+ /**
+ * 如果给定的bean名字在bean定义中有别名,则返回这些别名
+ */
+ public static String[] getAliases(String name) throws NoSuchBeanDefinitionException {
+ return getBeanFactory().getAliases(name);
+ }
+
+ /**
+ * 获取aop代理对象
+ */
+ @SuppressWarnings("unchecked")
+ public static T getAopProxy(T invoker) {
+ return (T) getBean(invoker.getClass());
+ }
+
+
+ /**
+ * 获取spring上下文
+ */
+ public static ApplicationContext context() {
+ return getApplicationContext();
+ }
+
+ public static boolean isVirtual() {
+ return Threading.VIRTUAL.isActive(getBean(Environment.class));
+ }
+
+}
diff --git a/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/utils/StreamUtils.java b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/utils/StreamUtils.java
new file mode 100644
index 0000000..05a90bf
--- /dev/null
+++ b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/utils/StreamUtils.java
@@ -0,0 +1,254 @@
+package com.pusong.common.core.utils;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.map.MapUtil;
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+
+import java.util.*;
+import java.util.function.BiFunction;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+
+/**
+ * stream 流工具类
+ *
+ * @author Lion Li
+ */
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public class StreamUtils {
+
+ /**
+ * 将collection过滤
+ *
+ * @param collection 需要转化的集合
+ * @param function 过滤方法
+ * @return 过滤后的list
+ */
+ public static List filter(Collection collection, Predicate function) {
+ if (CollUtil.isEmpty(collection)) {
+ return CollUtil.newArrayList();
+ }
+ // 注意此处不要使用 .toList() 新语法 因为返回的是不可变List 会导致序列化问题
+ return collection.stream().filter(function).collect(Collectors.toList());
+ }
+
+ /**
+ * 将collection拼接
+ *
+ * @param collection 需要转化的集合
+ * @param function 拼接方法
+ * @return 拼接后的list
+ */
+ public static String join(Collection collection, Function function) {
+ return join(collection, function, StringUtils.SEPARATOR);
+ }
+
+ /**
+ * 将collection拼接
+ *
+ * @param collection 需要转化的集合
+ * @param function 拼接方法
+ * @param delimiter 拼接符
+ * @return 拼接后的list
+ */
+ public static String join(Collection collection, Function function, CharSequence delimiter) {
+ if (CollUtil.isEmpty(collection)) {
+ return StringUtils.EMPTY;
+ }
+ return collection.stream().map(function).filter(Objects::nonNull).collect(Collectors.joining(delimiter));
+ }
+
+ /**
+ * 将collection排序
+ *
+ * @param collection 需要转化的集合
+ * @param comparing 排序方法
+ * @return 排序后的list
+ */
+ public static List sorted(Collection collection, Comparator comparing) {
+ if (CollUtil.isEmpty(collection)) {
+ return CollUtil.newArrayList();
+ }
+ // 注意此处不要使用 .toList() 新语法 因为返回的是不可变List 会导致序列化问题
+ return collection.stream().filter(Objects::nonNull).sorted(comparing).collect(Collectors.toList());
+ }
+
+ /**
+ * 将collection转化为类型不变的map
+ * {@code Collection ----> Map}
+ *
+ * @param collection 需要转化的集合
+ * @param key V类型转化为K类型的lambda方法
+ * @param collection中的泛型
+ * @param map中的key类型
+ * @return 转化后的map
+ */
+ public static Map toIdentityMap(Collection collection, Function key) {
+ if (CollUtil.isEmpty(collection)) {
+ return MapUtil.newHashMap();
+ }
+ return collection.stream().filter(Objects::nonNull).collect(Collectors.toMap(key, Function.identity(), (l, r) -> l));
+ }
+
+ /**
+ * 将Collection转化为map(value类型与collection的泛型不同)
+ * {@code Collection -----> Map }
+ *
+ * @param collection 需要转化的集合
+ * @param key E类型转化为K类型的lambda方法
+ * @param value E类型转化为V类型的lambda方法
+ * @param collection中的泛型
+ * @param map中的key类型
+ * @param map中的value类型
+ * @return 转化后的map
+ */
+ public static Map toMap(Collection collection, Function key, Function value) {
+ if (CollUtil.isEmpty(collection)) {
+ return MapUtil.newHashMap();
+ }
+ return collection.stream().filter(Objects::nonNull).collect(Collectors.toMap(key, value, (l, r) -> l));
+ }
+
+ /**
+ * 将collection按照规则(比如有相同的班级id)分类成map
+ * {@code Collection -------> Map> }
+ *
+ * @param collection 需要分类的集合
+ * @param key 分类的规则
+ * @param collection中的泛型
+ * @param map中的key类型
+ * @return 分类后的map
+ */
+ public static Map> groupByKey(Collection collection, Function key) {
+ if (CollUtil.isEmpty(collection)) {
+ return MapUtil.newHashMap();
+ }
+ return collection
+ .stream().filter(Objects::nonNull)
+ .collect(Collectors.groupingBy(key, LinkedHashMap::new, Collectors.toList()));
+ }
+
+ /**
+ * 将collection按照两个规则(比如有相同的年级id,班级id)分类成双层map
+ * {@code Collection ---> Map>> }
+ *
+ * @param collection 需要分类的集合
+ * @param key1 第一个分类的规则
+ * @param key2 第二个分类的规则
+ * @param 集合元素类型
+ * @param 第一个map中的key类型
+ * @param 第二个map中的key类型
+ * @return 分类后的map
+ */
+ public static Map>> groupBy2Key(Collection collection, Function key1, Function key2) {
+ if (CollUtil.isEmpty(collection)) {
+ return MapUtil.newHashMap();
+ }
+ return collection
+ .stream().filter(Objects::nonNull)
+ .collect(Collectors.groupingBy(key1, LinkedHashMap::new, Collectors.groupingBy(key2, LinkedHashMap::new, Collectors.toList())));
+ }
+
+ /**
+ * 将collection按照两个规则(比如有相同的年级id,班级id)分类成双层map
+ * {@code Collection ---> Map> }
+ *
+ * @param collection 需要分类的集合
+ * @param key1 第一个分类的规则
+ * @param key2 第二个分类的规则
+ * @param 第一个map中的key类型
+ * @param 第二个map中的key类型
+ * @param collection中的泛型
+ * @return 分类后的map
+ */
+ public static Map> group2Map(Collection collection, Function key1, Function key2) {
+ if (CollUtil.isEmpty(collection) || key1 == null || key2 == null) {
+ return MapUtil.newHashMap();
+ }
+ return collection
+ .stream().filter(Objects::nonNull)
+ .collect(Collectors.groupingBy(key1, LinkedHashMap::new, Collectors.toMap(key2, Function.identity(), (l, r) -> l)));
+ }
+
+ /**
+ * 将collection转化为List集合,但是两者的泛型不同
+ * {@code Collection ------> List }
+ *
+ * @param collection 需要转化的集合
+ * @param function collection中的泛型转化为list泛型的lambda表达式
+ * @param collection中的泛型
+ * @param List中的泛型
+ * @return 转化后的list
+ */
+ public static List toList(Collection collection, Function function) {
+ if (CollUtil.isEmpty(collection)) {
+ return CollUtil.newArrayList();
+ }
+ return collection
+ .stream()
+ .map(function)
+ .filter(Objects::nonNull)
+ // 注意此处不要使用 .toList() 新语法 因为返回的是不可变List 会导致序列化问题
+ .collect(Collectors.toList());
+ }
+
+ /**
+ * 将collection转化为Set集合,但是两者的泛型不同
+ * {@code Collection ------> Set }
+ *
+ * @param collection 需要转化的集合
+ * @param function collection中的泛型转化为set泛型的lambda表达式
+ * @param collection中的泛型
+ * @param Set中的泛型
+ * @return 转化后的Set
+ */
+ public static Set toSet(Collection collection, Function function) {
+ if (CollUtil.isEmpty(collection) || function == null) {
+ return CollUtil.newHashSet();
+ }
+ return collection
+ .stream()
+ .map(function)
+ .filter(Objects::nonNull)
+ .collect(Collectors.toSet());
+ }
+
+
+ /**
+ * 合并两个相同key类型的map
+ *
+ * @param map1 第一个需要合并的 map
+ * @param map2 第二个需要合并的 map
+ * @param merge 合并的lambda,将key value1 value2合并成最终的类型,注意value可能为空的情况
+ * @param map中的key类型
+ * @param 第一个 map的value类型
+ * @param 第二个 map的value类型
+ * @param 最终map的value类型
+ * @return 合并后的map
+ */
+ public static Map merge(Map map1, Map map2, BiFunction merge) {
+ if (MapUtil.isEmpty(map1) && MapUtil.isEmpty(map2)) {
+ return MapUtil.newHashMap();
+ } else if (MapUtil.isEmpty(map1)) {
+ map1 = MapUtil.newHashMap();
+ } else if (MapUtil.isEmpty(map2)) {
+ map2 = MapUtil.newHashMap();
+ }
+ Set key = new HashSet<>();
+ key.addAll(map1.keySet());
+ key.addAll(map2.keySet());
+ Map map = new HashMap<>();
+ for (K t : key) {
+ X x = map1.get(t);
+ Y y = map2.get(t);
+ V z = merge.apply(x, y);
+ if (z != null) {
+ map.put(t, z);
+ }
+ }
+ return map;
+ }
+
+}
diff --git a/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/utils/StringUtils.java b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/utils/StringUtils.java
new file mode 100644
index 0000000..4c4e9db
--- /dev/null
+++ b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/utils/StringUtils.java
@@ -0,0 +1,323 @@
+package com.pusong.common.core.utils;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.convert.Convert;
+import cn.hutool.core.lang.Validator;
+import cn.hutool.core.util.StrUtil;
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+import org.springframework.util.AntPathMatcher;
+
+import java.util.*;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+/**
+ * 字符串工具类
+ *
+ * @author Lion Li
+ */
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public class StringUtils extends org.apache.commons.lang3.StringUtils {
+
+ public static final String SEPARATOR = ",";
+
+ public static final String SLASH = "/";
+
+ /**
+ * 获取参数不为空值
+ *
+ * @param str defaultValue 要判断的value
+ * @return value 返回值
+ */
+ public static String blankToDefault(String str, String defaultValue) {
+ return StrUtil.blankToDefault(str, defaultValue);
+ }
+
+ /**
+ * * 判断一个字符串是否为空串
+ *
+ * @param str String
+ * @return true:为空 false:非空
+ */
+ public static boolean isEmpty(String str) {
+ return StrUtil.isEmpty(str);
+ }
+
+ /**
+ * * 判断一个字符串是否为非空串
+ *
+ * @param str String
+ * @return true:非空串 false:空串
+ */
+ public static boolean isNotEmpty(String str) {
+ return !isEmpty(str);
+ }
+
+ /**
+ * 去空格
+ */
+ public static String trim(String str) {
+ return StrUtil.trim(str);
+ }
+
+ /**
+ * 截取字符串
+ *
+ * @param str 字符串
+ * @param start 开始
+ * @return 结果
+ */
+ public static String substring(final String str, int start) {
+ return substring(str, start, str.length());
+ }
+
+ /**
+ * 截取字符串
+ *
+ * @param str 字符串
+ * @param start 开始
+ * @param end 结束
+ * @return 结果
+ */
+ public static String substring(final String str, int start, int end) {
+ return StrUtil.sub(str, start, end);
+ }
+
+ /**
+ * 格式化文本, {} 表示占位符
+ * 此方法只是简单将占位符 {} 按照顺序替换为参数
+ * 如果想输出 {} 使用 \\转义 { 即可,如果想输出 {} 之前的 \ 使用双转义符 \\\\ 即可
+ * 例:
+ * 通常使用:format("this is {} for {}", "a", "b") -> this is a for b
+ * 转义{}: format("this is \\{} for {}", "a", "b") -> this is {} for a
+ * 转义\: format("this is \\\\{} for {}", "a", "b") -> this is \a for b
+ *
+ * @param template 文本模板,被替换的部分用 {} 表示
+ * @param params 参数值
+ * @return 格式化后的文本
+ */
+ public static String format(String template, Object... params) {
+ return StrUtil.format(template, params);
+ }
+
+ /**
+ * 是否为http(s)://开头
+ *
+ * @param link 链接
+ * @return 结果
+ */
+ public static boolean ishttp(String link) {
+ return Validator.isUrl(link);
+ }
+
+ /**
+ * 字符串转set
+ *
+ * @param str 字符串
+ * @param sep 分隔符
+ * @return set集合
+ */
+ public static Set str2Set(String str, String sep) {
+ return new HashSet<>(str2List(str, sep, true, false));
+ }
+
+ /**
+ * 字符串转list
+ *
+ * @param str 字符串
+ * @param sep 分隔符
+ * @param filterBlank 过滤纯空白
+ * @param trim 去掉首尾空白
+ * @return list集合
+ */
+ public static List str2List(String str, String sep, boolean filterBlank, boolean trim) {
+ List list = new ArrayList<>();
+ if (isEmpty(str)) {
+ return list;
+ }
+
+ // 过滤空白字符串
+ if (filterBlank && isBlank(str)) {
+ return list;
+ }
+ String[] split = str.split(sep);
+ for (String string : split) {
+ if (filterBlank && isBlank(string)) {
+ continue;
+ }
+ if (trim) {
+ string = trim(string);
+ }
+ list.add(string);
+ }
+
+ return list;
+ }
+
+ /**
+ * 查找指定字符串是否包含指定字符串列表中的任意一个字符串同时串忽略大小写
+ *
+ * @param cs 指定字符串
+ * @param searchCharSequences 需要检查的字符串数组
+ * @return 是否包含任意一个字符串
+ */
+ public static boolean containsAnyIgnoreCase(CharSequence cs, CharSequence... searchCharSequences) {
+ return StrUtil.containsAnyIgnoreCase(cs, searchCharSequences);
+ }
+
+ /**
+ * 驼峰转下划线命名
+ */
+ public static String toUnderScoreCase(String str) {
+ return StrUtil.toUnderlineCase(str);
+ }
+
+ /**
+ * 是否包含字符串
+ *
+ * @param str 验证字符串
+ * @param strs 字符串组
+ * @return 包含返回true
+ */
+ public static boolean inStringIgnoreCase(String str, String... strs) {
+ return StrUtil.equalsAnyIgnoreCase(str, strs);
+ }
+
+ /**
+ * 将下划线大写方式命名的字符串转换为驼峰式。如果转换前的下划线大写方式命名的字符串为空,则返回空字符串。 例如:HELLO_WORLD->HelloWorld
+ *
+ * @param name 转换前的下划线大写方式命名的字符串
+ * @return 转换后的驼峰式命名的字符串
+ */
+ public static String convertToCamelCase(String name) {
+ return StrUtil.upperFirst(StrUtil.toCamelCase(name));
+ }
+
+ /**
+ * 驼峰式命名法 例如:user_name->userName
+ */
+ public static String toCamelCase(String s) {
+ return StrUtil.toCamelCase(s);
+ }
+
+ /**
+ * 查找指定字符串是否匹配指定字符串列表中的任意一个字符串
+ *
+ * @param str 指定字符串
+ * @param strs 需要检查的字符串数组
+ * @return 是否匹配
+ */
+ public static boolean matches(String str, List strs) {
+ if (isEmpty(str) || CollUtil.isEmpty(strs)) {
+ return false;
+ }
+ for (String pattern : strs) {
+ if (isMatch(pattern, str)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * 判断url是否与规则配置:
+ * ? 表示单个字符;
+ * * 表示一层路径内的任意字符串,不可跨层级;
+ * ** 表示任意层路径;
+ *
+ * @param pattern 匹配规则
+ * @param url 需要匹配的url
+ */
+ public static boolean isMatch(String pattern, String url) {
+ AntPathMatcher matcher = new AntPathMatcher();
+ return matcher.match(pattern, url);
+ }
+
+ /**
+ * 数字左边补齐0,使之达到指定长度。注意,如果数字转换为字符串后,长度大于size,则只保留 最后size个字符。
+ *
+ * @param num 数字对象
+ * @param size 字符串指定长度
+ * @return 返回数字的字符串格式,该字符串为指定长度。
+ */
+ public static String padl(final Number num, final int size) {
+ return padl(num.toString(), size, '0');
+ }
+
+ /**
+ * 字符串左补齐。如果原始字符串s长度大于size,则只保留最后size个字符。
+ *
+ * @param s 原始字符串
+ * @param size 字符串指定长度
+ * @param c 用于补齐的字符
+ * @return 返回指定长度的字符串,由原字符串左补齐或截取得到。
+ */
+ public static String padl(final String s, final int size, final char c) {
+ final StringBuilder sb = new StringBuilder(size);
+ if (s != null) {
+ final int len = s.length();
+ if (s.length() <= size) {
+ sb.append(String.valueOf(c).repeat(size - len));
+ sb.append(s);
+ } else {
+ return s.substring(len - size, len);
+ }
+ } else {
+ sb.append(String.valueOf(c).repeat(Math.max(0, size)));
+ }
+ return sb.toString();
+ }
+
+ /**
+ * 切分字符串(分隔符默认逗号)
+ *
+ * @param str 被切分的字符串
+ * @return 分割后的数据列表
+ */
+ public static List splitList(String str) {
+ return splitTo(str, Convert::toStr);
+ }
+
+ /**
+ * 切分字符串
+ *
+ * @param str 被切分的字符串
+ * @param separator 分隔符
+ * @return 分割后的数据列表
+ */
+ public static List splitList(String str, String separator) {
+ return splitTo(str, separator, Convert::toStr);
+ }
+
+ /**
+ * 切分字符串自定义转换(分隔符默认逗号)
+ *
+ * @param str 被切分的字符串
+ * @param mapper 自定义转换
+ * @return 分割后的数据列表
+ */
+ public static List splitTo(String str, Function super Object, T> mapper) {
+ return splitTo(str, SEPARATOR, mapper);
+ }
+
+ /**
+ * 切分字符串自定义转换
+ *
+ * @param str 被切分的字符串
+ * @param separator 分隔符
+ * @param mapper 自定义转换
+ * @return 分割后的数据列表
+ */
+ public static List splitTo(String str, String separator, Function super Object, T> mapper) {
+ if (isBlank(str)) {
+ return new ArrayList<>(0);
+ }
+ return StrUtil.split(str, separator)
+ .stream()
+ .filter(Objects::nonNull)
+ .map(mapper)
+ .collect(Collectors.toList());
+ }
+
+}
diff --git a/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/utils/Threads.java b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/utils/Threads.java
new file mode 100644
index 0000000..82d31df
--- /dev/null
+++ b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/utils/Threads.java
@@ -0,0 +1,75 @@
+package com.pusong.common.core.utils;
+
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+
+import java.util.concurrent.*;
+
+/**
+ * 线程相关工具类.
+ *
+ * @author ruoyi
+ */
+@Slf4j
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public class Threads {
+
+ /**
+ * sleep等待,单位为毫秒
+ */
+ public static void sleep(long milliseconds) {
+ try {
+ Thread.sleep(milliseconds);
+ } catch (InterruptedException e) {
+ return;
+ }
+ }
+
+ /**
+ * 停止线程池
+ * 先使用shutdown, 停止接收新任务并尝试完成所有已存在任务.
+ * 如果超时, 则调用shutdownNow, 取消在workQueue中Pending的任务,并中断所有阻塞函数.
+ * 如果仍然超時,則強制退出.
+ * 另对在shutdown时线程本身被调用中断做了处理.
+ */
+ public static void shutdownAndAwaitTermination(ExecutorService pool) {
+ if (pool != null && !pool.isShutdown()) {
+ pool.shutdown();
+ try {
+ if (!pool.awaitTermination(120, TimeUnit.SECONDS)) {
+ pool.shutdownNow();
+ if (!pool.awaitTermination(120, TimeUnit.SECONDS)) {
+ log.info("Pool did not terminate");
+ }
+ }
+ } catch (InterruptedException ie) {
+ pool.shutdownNow();
+ Thread.currentThread().interrupt();
+ }
+ }
+ }
+
+ /**
+ * 打印线程异常信息
+ */
+ public static void printException(Runnable r, Throwable t) {
+ if (t == null && r instanceof Future>) {
+ try {
+ Future> future = (Future>) r;
+ if (future.isDone()) {
+ future.get();
+ }
+ } catch (CancellationException ce) {
+ t = ce;
+ } catch (ExecutionException ee) {
+ t = ee.getCause();
+ } catch (InterruptedException ie) {
+ Thread.currentThread().interrupt();
+ }
+ }
+ if (t != null) {
+ log.error(t.getMessage(), t);
+ }
+ }
+}
diff --git a/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/utils/TreeBuildUtils.java b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/utils/TreeBuildUtils.java
new file mode 100644
index 0000000..bfe0585
--- /dev/null
+++ b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/utils/TreeBuildUtils.java
@@ -0,0 +1,35 @@
+package com.pusong.common.core.utils;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.lang.tree.Tree;
+import cn.hutool.core.lang.tree.TreeNodeConfig;
+import cn.hutool.core.lang.tree.TreeUtil;
+import cn.hutool.core.lang.tree.parser.NodeParser;
+import com.pusong.common.core.utils.reflect.ReflectUtils;
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+
+import java.util.List;
+
+/**
+ * 扩展 hutool TreeUtil 封装系统树构建
+ *
+ * @author Lion Li
+ */
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public class TreeBuildUtils extends TreeUtil {
+
+ /**
+ * 根据前端定制差异化字段
+ */
+ public static final TreeNodeConfig DEFAULT_CONFIG = TreeNodeConfig.DEFAULT_CONFIG.setNameKey("label");
+
+ public static List> build(List list, NodeParser nodeParser) {
+ if (CollUtil.isEmpty(list)) {
+ return null;
+ }
+ K k = ReflectUtils.invokeGetter(list.get(0), "parentId");
+ return TreeUtil.build(list, k, DEFAULT_CONFIG, nodeParser);
+ }
+
+}
diff --git a/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/utils/ValidatorUtils.java b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/utils/ValidatorUtils.java
new file mode 100644
index 0000000..7f93a6a
--- /dev/null
+++ b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/utils/ValidatorUtils.java
@@ -0,0 +1,35 @@
+package com.pusong.common.core.utils;
+
+import jakarta.validation.ConstraintViolation;
+import jakarta.validation.ConstraintViolationException;
+import jakarta.validation.Validator;
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+
+import java.util.Set;
+
+/**
+ * Validator 校验框架工具
+ *
+ * @author Lion Li
+ */
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public class ValidatorUtils {
+
+ private static final Validator VALID = SpringUtils.getBean(Validator.class);
+
+ /**
+ * 对给定对象进行参数校验,并根据指定的校验组进行校验
+ *
+ * @param object 要进行校验的对象
+ * @param groups 校验组
+ * @throws ConstraintViolationException 如果校验不通过,则抛出参数校验异常
+ */
+ public static void validate(T object, Class>... groups) {
+ Set> validate = VALID.validate(object, groups);
+ if (!validate.isEmpty()) {
+ throw new ConstraintViolationException("参数校验异常", validate);
+ }
+ }
+
+}
diff --git a/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/utils/file/FileUtils.java b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/utils/file/FileUtils.java
new file mode 100644
index 0000000..f2cd817
--- /dev/null
+++ b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/utils/file/FileUtils.java
@@ -0,0 +1,43 @@
+package com.pusong.common.core.utils.file;
+
+import cn.hutool.core.io.FileUtil;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+
+/**
+ * 文件处理工具类
+ *
+ * @author Lion Li
+ */
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public class FileUtils extends FileUtil {
+
+ /**
+ * 下载文件名重新编码
+ *
+ * @param response 响应对象
+ * @param realFileName 真实文件名
+ */
+ public static void setAttachmentResponseHeader(HttpServletResponse response, String realFileName) {
+ String percentEncodedFileName = percentEncode(realFileName);
+ String contentDispositionValue = "attachment; filename=%s;filename*=utf-8''%s".formatted(percentEncodedFileName, percentEncodedFileName);
+ response.addHeader("Access-Control-Expose-Headers", "Content-Disposition,download-filename");
+ response.setHeader("Content-disposition", contentDispositionValue);
+ response.setHeader("download-filename", percentEncodedFileName);
+ }
+
+ /**
+ * 百分号编码工具方法
+ *
+ * @param s 需要百分号编码的字符串
+ * @return 百分号编码后的字符串
+ */
+ public static String percentEncode(String s) {
+ String encode = URLEncoder.encode(s, StandardCharsets.UTF_8);
+ return encode.replaceAll("\\+", "%20");
+ }
+}
diff --git a/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/utils/file/MimeTypeUtils.java b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/utils/file/MimeTypeUtils.java
new file mode 100644
index 0000000..a23d1df
--- /dev/null
+++ b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/utils/file/MimeTypeUtils.java
@@ -0,0 +1,40 @@
+package com.pusong.common.core.utils.file;
+
+/**
+ * 媒体类型工具类
+ *
+ * @author ruoyi
+ */
+public class MimeTypeUtils {
+ public static final String IMAGE_PNG = "image/png";
+
+ public static final String IMAGE_JPG = "image/jpg";
+
+ public static final String IMAGE_JPEG = "image/jpeg";
+
+ public static final String IMAGE_BMP = "image/bmp";
+
+ public static final String IMAGE_GIF = "image/gif";
+
+ public static final String[] IMAGE_EXTENSION = {"bmp", "gif", "jpg", "jpeg", "png"};
+
+ public static final String[] FLASH_EXTENSION = {"swf", "flv"};
+
+ public static final String[] MEDIA_EXTENSION = {"swf", "flv", "mp3", "wav", "wma", "wmv", "mid", "avi", "mpg",
+ "asf", "rm", "rmvb"};
+
+ public static final String[] VIDEO_EXTENSION = {"mp4", "avi", "rmvb"};
+
+ public static final String[] DEFAULT_ALLOWED_EXTENSION = {
+ // 图片
+ "bmp", "gif", "jpg", "jpeg", "png",
+ // word excel powerpoint
+ "doc", "docx", "xls", "xlsx", "ppt", "pptx", "html", "htm", "txt",
+ // 压缩文件
+ "rar", "zip", "gz", "bz2",
+ // 视频格式
+ "mp4", "avi", "rmvb",
+ // pdf
+ "pdf"};
+
+}
diff --git a/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/utils/ip/AddressUtils.java b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/utils/ip/AddressUtils.java
new file mode 100644
index 0000000..04db93a
--- /dev/null
+++ b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/utils/ip/AddressUtils.java
@@ -0,0 +1,33 @@
+package com.pusong.common.core.utils.ip;
+
+import cn.hutool.core.net.NetUtil;
+import cn.hutool.http.HtmlUtil;
+import com.pusong.common.core.utils.StringUtils;
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * 获取地址类
+ *
+ * @author Lion Li
+ */
+@Slf4j
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public class AddressUtils {
+
+ // 未知地址
+ public static final String UNKNOWN = "XX XX";
+
+ public static String getRealAddressByIP(String ip) {
+ if (StringUtils.isBlank(ip)) {
+ return UNKNOWN;
+ }
+ // 内网不查询
+ ip = StringUtils.contains(ip, "0:0:0:0:0:0:0:1") ? "127.0.0.1" : HtmlUtil.cleanHtmlTag(ip);
+ if (NetUtil.isInnerIP(ip)) {
+ return "内网IP";
+ }
+ return RegionUtils.getCityInfo(ip);
+ }
+}
diff --git a/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/utils/ip/RegionUtils.java b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/utils/ip/RegionUtils.java
new file mode 100644
index 0000000..8ecabf7
--- /dev/null
+++ b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/utils/ip/RegionUtils.java
@@ -0,0 +1,67 @@
+package com.pusong.common.core.utils.ip;
+
+import cn.hutool.core.io.FileUtil;
+import cn.hutool.core.io.resource.ClassPathResource;
+import cn.hutool.core.util.ObjectUtil;
+import com.pusong.common.core.utils.file.FileUtils;
+import com.pusong.common.core.exception.ServiceException;
+import lombok.extern.slf4j.Slf4j;
+import org.lionsoul.ip2region.xdb.Searcher;
+
+import java.io.File;
+
+/**
+ * 根据ip地址定位工具类,离线方式
+ * 参考地址:集成 ip2region 实现离线IP地址定位库
+ *
+ * @author lishuyan
+ */
+@Slf4j
+public class RegionUtils {
+
+ private static final Searcher SEARCHER;
+
+ static {
+ String fileName = "/ip2region.xdb";
+ File existFile = FileUtils.file(FileUtil.getTmpDir() + FileUtil.FILE_SEPARATOR + fileName);
+ if (!FileUtils.exist(existFile)) {
+ ClassPathResource fileStream = new ClassPathResource(fileName);
+ if (ObjectUtil.isEmpty(fileStream.getStream())) {
+ throw new ServiceException("RegionUtils初始化失败,原因:IP地址库数据不存在!");
+ }
+ FileUtils.writeFromStream(fileStream.getStream(), existFile);
+ }
+
+ String dbPath = existFile.getPath();
+
+ // 1、从 dbPath 加载整个 xdb 到内存。
+ byte[] cBuff;
+ try {
+ cBuff = Searcher.loadContentFromFile(dbPath);
+ } catch (Exception e) {
+ throw new ServiceException("RegionUtils初始化失败,原因:从ip2region.xdb文件加载内容失败!" + e.getMessage());
+ }
+ // 2、使用上述的 cBuff 创建一个完全基于内存的查询对象。
+ try {
+ SEARCHER = Searcher.newWithBuffer(cBuff);
+ } catch (Exception e) {
+ throw new ServiceException("RegionUtils初始化失败,原因:" + e.getMessage());
+ }
+ }
+
+ /**
+ * 根据IP地址离线获取城市
+ */
+ public static String getCityInfo(String ip) {
+ try {
+ ip = ip.trim();
+ // 3、执行查询
+ String region = SEARCHER.search(ip);
+ return region.replace("0|", "").replace("|0", "");
+ } catch (Exception e) {
+ log.error("IP地址离线获取城市异常 {}", ip);
+ return "未知";
+ }
+ }
+
+}
diff --git a/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/utils/reflect/ReflectUtils.java b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/utils/reflect/ReflectUtils.java
new file mode 100644
index 0000000..cef0487
--- /dev/null
+++ b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/utils/reflect/ReflectUtils.java
@@ -0,0 +1,56 @@
+package com.pusong.common.core.utils.reflect;
+
+import cn.hutool.core.util.ReflectUtil;
+import com.pusong.common.core.utils.StringUtils;
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+
+import java.lang.reflect.Method;
+
+/**
+ * 反射工具类. 提供调用getter/setter方法, 访问私有变量, 调用私有方法, 获取泛型类型Class, 被AOP过的真实类等工具函数.
+ *
+ * @author Lion Li
+ */
+@SuppressWarnings("rawtypes")
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public class ReflectUtils extends ReflectUtil {
+
+ private static final String SETTER_PREFIX = "set";
+
+ private static final String GETTER_PREFIX = "get";
+
+ /**
+ * 调用Getter方法.
+ * 支持多级,如:对象名.对象名.方法
+ */
+ @SuppressWarnings("unchecked")
+ public static E invokeGetter(Object obj, String propertyName) {
+ Object object = obj;
+ for (String name : StringUtils.split(propertyName, ".")) {
+ String getterMethodName = GETTER_PREFIX + StringUtils.capitalize(name);
+ object = invoke(object, getterMethodName);
+ }
+ return (E) object;
+ }
+
+ /**
+ * 调用Setter方法, 仅匹配方法名。
+ * 支持多级,如:对象名.对象名.方法
+ */
+ public static void invokeSetter(Object obj, String propertyName, E value) {
+ Object object = obj;
+ String[] names = StringUtils.split(propertyName, ".");
+ for (int i = 0; i < names.length; i++) {
+ if (i < names.length - 1) {
+ String getterMethodName = GETTER_PREFIX + StringUtils.capitalize(names[i]);
+ object = invoke(object, getterMethodName);
+ } else {
+ String setterMethodName = SETTER_PREFIX + StringUtils.capitalize(names[i]);
+ Method method = getMethodByName(object.getClass(), setterMethodName);
+ invoke(object, method, value);
+ }
+ }
+ }
+
+}
diff --git a/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/utils/regex/RegexUtils.java b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/utils/regex/RegexUtils.java
new file mode 100644
index 0000000..b3e86d4
--- /dev/null
+++ b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/utils/regex/RegexUtils.java
@@ -0,0 +1,30 @@
+package com.pusong.common.core.utils.regex;
+
+
+import cn.hutool.core.util.ReUtil;
+import com.pusong.common.core.constant.RegexConstants;
+
+/**
+ * 正则相关工具类
+ *
+ * @author Feng
+ */
+public final class RegexUtils extends ReUtil {
+
+ /**
+ * 从输入字符串中提取匹配的部分,如果没有匹配则返回默认值
+ *
+ * @param input 要提取的输入字符串
+ * @param regex 用于匹配的正则表达式,可以使用 {@link RegexConstants} 中定义的常量
+ * @param defaultInput 如果没有匹配时返回的默认值
+ * @return 如果找到匹配的部分,则返回匹配的部分,否则返回默认值
+ */
+ public static String extractFromString(String input, String regex, String defaultInput) {
+ try {
+ return ReUtil.get(regex, input, 1);
+ } catch (Exception e) {
+ return defaultInput;
+ }
+ }
+
+}
diff --git a/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/utils/regex/RegexValidator.java b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/utils/regex/RegexValidator.java
new file mode 100644
index 0000000..fb561af
--- /dev/null
+++ b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/utils/regex/RegexValidator.java
@@ -0,0 +1,105 @@
+package com.pusong.common.core.utils.regex;
+
+import cn.hutool.core.exceptions.ValidateException;
+import cn.hutool.core.lang.Validator;
+import com.pusong.common.core.factory.RegexPatternPoolFactory;
+
+import java.util.regex.Pattern;
+
+/**
+ * 正则字段校验器
+ * 主要验证字段非空、是否为满足指定格式等
+ *
+ * @author Feng
+ */
+public class RegexValidator extends Validator {
+
+ /**
+ * 字典类型必须以字母开头,且只能为(小写字母,数字,下滑线)
+ */
+ public static final Pattern DICTIONARY_TYPE = RegexPatternPoolFactory.DICTIONARY_TYPE;
+
+ /**
+ * 身份证号码(后6位)
+ */
+ public static final Pattern ID_CARD_LAST_6 = RegexPatternPoolFactory.ID_CARD_LAST_6;
+
+ /**
+ * QQ号码
+ */
+ public static final Pattern QQ_NUMBER = RegexPatternPoolFactory.QQ_NUMBER;
+
+ /**
+ * 邮政编码
+ */
+ public static final Pattern POSTAL_CODE = RegexPatternPoolFactory.POSTAL_CODE;
+
+ /**
+ * 注册账号
+ */
+ public static final Pattern ACCOUNT = RegexPatternPoolFactory.ACCOUNT;
+
+ /**
+ * 密码:包含至少8个字符,包括大写字母、小写字母、数字和特殊字符
+ */
+ public static final Pattern PASSWORD = RegexPatternPoolFactory.PASSWORD;
+
+ /**
+ * 通用状态(0表示正常,1表示停用)
+ */
+ public static final Pattern STATUS = RegexPatternPoolFactory.STATUS;
+
+
+ /**
+ * 检查输入的账号是否匹配预定义的规则
+ *
+ * @param value 要验证的账号
+ * @return 如果账号符合规则,返回 true;否则,返回 false。
+ */
+ public static boolean isAccount(CharSequence value) {
+ return isMatchRegex(ACCOUNT, value);
+ }
+
+ /**
+ * 验证输入的账号是否符合规则,如果不符合,则抛出 ValidateException 异常
+ *
+ * @param value 要验证的账号
+ * @param errorMsg 验证失败时抛出的异常消息
+ * @param CharSequence 的子类型
+ * @return 如果验证通过,返回输入的账号
+ * @throws ValidateException 如果验证失败
+ */
+ public static T validateAccount(T value, String errorMsg) throws ValidateException {
+ if (!isAccount(value)) {
+ throw new ValidateException(errorMsg);
+ }
+ return value;
+ }
+
+ /**
+ * 检查输入的状态是否匹配预定义的规则
+ *
+ * @param value 要验证的状态
+ * @return 如果状态符合规则,返回 true;否则,返回 false。
+ */
+ public static boolean isStatus(CharSequence value) {
+ return isMatchRegex(STATUS, value);
+ }
+
+ /**
+ * 验证输入的状态是否符合规则,如果不符合,则抛出 ValidateException 异常
+ *
+ * @param value 要验证的状态
+ * @param errorMsg 验证失败时抛出的异常消息
+ * @param CharSequence 的子类型
+ * @return 如果验证通过,返回输入的状态
+ * @throws ValidateException 如果验证失败
+ */
+ public static T validateStatus(T value, String errorMsg) throws ValidateException {
+ if (!isStatus(value)) {
+ throw new ValidateException(errorMsg);
+ }
+ return value;
+ }
+
+}
diff --git a/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/utils/sql/SqlUtil.java b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/utils/sql/SqlUtil.java
new file mode 100644
index 0000000..69e80d0
--- /dev/null
+++ b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/utils/sql/SqlUtil.java
@@ -0,0 +1,56 @@
+package com.pusong.common.core.utils.sql;
+
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+import com.pusong.common.core.utils.StringUtils;
+
+/**
+ * sql操作工具类
+ *
+ * @author ruoyi
+ */
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public class SqlUtil {
+
+ /**
+ * 定义常用的 sql关键字
+ */
+ public static final String SQL_REGEX = "select |insert |delete |update |drop |count |exec |chr |mid |master |truncate |char |and |declare ";
+
+ /**
+ * 仅支持字母、数字、下划线、空格、逗号、小数点(支持多个字段排序)
+ */
+ public static final String SQL_PATTERN = "[a-zA-Z0-9_\\ \\,\\.]+";
+
+ /**
+ * 检查字符,防止注入绕过
+ */
+ public static String escapeOrderBySql(String value) {
+ if (StringUtils.isNotEmpty(value) && !isValidOrderBySql(value)) {
+ throw new IllegalArgumentException("参数不符合规范,不能进行查询");
+ }
+ return value;
+ }
+
+ /**
+ * 验证 order by 语法是否符合规范
+ */
+ public static boolean isValidOrderBySql(String value) {
+ return value.matches(SQL_PATTERN);
+ }
+
+ /**
+ * SQL关键字检查
+ */
+ public static void filterKeyword(String value) {
+ if (StringUtils.isEmpty(value)) {
+ return;
+ }
+ String[] sqlKeywords = StringUtils.split(SQL_REGEX, "\\|");
+ for (String sqlKeyword : sqlKeywords) {
+ if (StringUtils.indexOfIgnoreCase(value, sqlKeyword) > -1) {
+ throw new IllegalArgumentException("参数存在SQL注入风险");
+ }
+ }
+ }
+}
diff --git a/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/validate/AddGroup.java b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/validate/AddGroup.java
new file mode 100644
index 0000000..cfda05c
--- /dev/null
+++ b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/validate/AddGroup.java
@@ -0,0 +1,9 @@
+package com.pusong.common.core.validate;
+
+/**
+ * 校验分组 add
+ *
+ * @author Lion Li
+ */
+public interface AddGroup {
+}
diff --git a/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/validate/EditGroup.java b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/validate/EditGroup.java
new file mode 100644
index 0000000..16f00a9
--- /dev/null
+++ b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/validate/EditGroup.java
@@ -0,0 +1,9 @@
+package com.pusong.common.core.validate;
+
+/**
+ * 校验分组 edit
+ *
+ * @author Lion Li
+ */
+public interface EditGroup {
+}
diff --git a/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/validate/QueryGroup.java b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/validate/QueryGroup.java
new file mode 100644
index 0000000..35751a2
--- /dev/null
+++ b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/validate/QueryGroup.java
@@ -0,0 +1,9 @@
+package com.pusong.common.core.validate;
+
+/**
+ * 校验分组 query
+ *
+ * @author Lion Li
+ */
+public interface QueryGroup {
+}
diff --git a/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/xss/Xss.java b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/xss/Xss.java
new file mode 100644
index 0000000..e7c6952
--- /dev/null
+++ b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/xss/Xss.java
@@ -0,0 +1,26 @@
+package com.pusong.common.core.xss;
+
+import jakarta.validation.Constraint;
+import jakarta.validation.Payload;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * 自定义xss校验注解
+ *
+ * @author Lion Li
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(value = {ElementType.METHOD, ElementType.FIELD, ElementType.CONSTRUCTOR, ElementType.PARAMETER})
+@Constraint(validatedBy = {XssValidator.class})
+public @interface Xss {
+
+ String message() default "不允许任何脚本运行";
+
+ Class>[] groups() default {};
+
+ Class extends Payload>[] payload() default {};
+
+}
diff --git a/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/xss/XssValidator.java b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/xss/XssValidator.java
new file mode 100644
index 0000000..54e2673
--- /dev/null
+++ b/pusong-common/pusong-common-core/src/main/java/com/pusong/common/core/xss/XssValidator.java
@@ -0,0 +1,21 @@
+package com.pusong.common.core.xss;
+
+import cn.hutool.core.util.ReUtil;
+import cn.hutool.http.HtmlUtil;
+
+import jakarta.validation.ConstraintValidator;
+import jakarta.validation.ConstraintValidatorContext;
+
+/**
+ * 自定义xss校验注解实现
+ *
+ * @author Lion Li
+ */
+public class XssValidator implements ConstraintValidator {
+
+ @Override
+ public boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext) {
+ return !ReUtil.contains(HtmlUtil.RE_HTML_MARK, value);
+ }
+
+}
diff --git a/pusong-common/pusong-common-core/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/pusong-common/pusong-common-core/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 0000000..2e8f6ef
--- /dev/null
+++ b/pusong-common/pusong-common-core/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -0,0 +1,6 @@
+com.pusong.common.core.config.ApplicationConfig
+com.pusong.common.core.config.AsyncConfig
+com.pusong.common.core.config.RuoYiConfig
+com.pusong.common.core.config.ThreadPoolConfig
+com.pusong.common.core.config.ValidatorConfig
+com.pusong.common.core.utils.SpringUtils
diff --git a/pusong-common/pusong-common-doc/pom.xml b/pusong-common/pusong-common-doc/pom.xml
new file mode 100644
index 0000000..8ffeaff
--- /dev/null
+++ b/pusong-common/pusong-common-doc/pom.xml
@@ -0,0 +1,66 @@
+
+
+
+ com.pusong
+ pusong-common
+ ${revision}
+
+ 4.0.0
+
+ pusong-common-doc
+
+
+ ruoyi-common-doc 系统接口
+
+
+
+
+ com.pusong
+ pusong-common-core
+
+
+
+ org.springdoc
+ springdoc-openapi-starter-webmvc-api
+
+
+
+ com.github.therapi
+ therapi-runtime-javadoc
+
+
+
+ com.fasterxml.jackson.module
+ jackson-module-kotlin
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-freemarker
+
+
+ com.itextpdf
+ itextpdf
+ 5.5.13
+
+
+ com.itextpdf.tool
+ xmlworker
+ 5.5.13
+
+
+ com.itextpdf
+ itext-asian
+ 5.2.0
+
+
+
+ com.alibaba
+ fastjson
+
+
+
+
diff --git a/pusong-common/pusong-common-doc/src/main/java/com/pusong/common/doc/config/SpringDocConfig.java b/pusong-common/pusong-common-doc/src/main/java/com/pusong/common/doc/config/SpringDocConfig.java
new file mode 100644
index 0000000..bc161f0
--- /dev/null
+++ b/pusong-common/pusong-common-doc/src/main/java/com/pusong/common/doc/config/SpringDocConfig.java
@@ -0,0 +1,126 @@
+package com.pusong.common.doc.config;
+
+import com.pusong.common.doc.config.properties.SpringDocProperties;
+import com.pusong.common.doc.handler.OpenApiHandler;
+import io.swagger.v3.oas.models.OpenAPI;
+import io.swagger.v3.oas.models.Paths;
+import io.swagger.v3.oas.models.info.Info;
+import io.swagger.v3.oas.models.security.SecurityRequirement;
+import lombok.RequiredArgsConstructor;
+import com.pusong.common.core.utils.StringUtils;
+import org.springdoc.core.configuration.SpringDocConfiguration;
+import org.springdoc.core.customizers.OpenApiBuilderCustomizer;
+import org.springdoc.core.customizers.OpenApiCustomizer;
+import org.springdoc.core.customizers.ServerBaseUrlCustomizer;
+import org.springdoc.core.properties.SpringDocConfigProperties;
+import org.springdoc.core.providers.JavadocProvider;
+import org.springdoc.core.service.OpenAPIService;
+import org.springdoc.core.service.SecurityService;
+import org.springdoc.core.utils.PropertyResolverUtils;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.autoconfigure.web.ServerProperties;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+
+/**
+ * Swagger 文档配置
+ *
+ * @author Lion Li
+ */
+@RequiredArgsConstructor
+@AutoConfiguration(before = SpringDocConfiguration.class)
+@EnableConfigurationProperties(SpringDocProperties.class)
+@ConditionalOnProperty(name = "springdoc.api-docs.enabled", havingValue = "true", matchIfMissing = true)
+public class SpringDocConfig {
+
+ private final ServerProperties serverProperties;
+
+ @Bean
+ @ConditionalOnMissingBean(OpenAPI.class)
+ public OpenAPI openApi(SpringDocProperties properties) {
+ OpenAPI openApi = new OpenAPI();
+ // 文档基本信息
+ SpringDocProperties.InfoProperties infoProperties = properties.getInfo();
+ Info info = convertInfo(infoProperties);
+ openApi.info(info);
+ // 扩展文档信息
+ openApi.externalDocs(properties.getExternalDocs());
+ openApi.tags(properties.getTags());
+ openApi.paths(properties.getPaths());
+ openApi.components(properties.getComponents());
+ Set keySet = properties.getComponents().getSecuritySchemes().keySet();
+ List list = new ArrayList<>();
+ SecurityRequirement securityRequirement = new SecurityRequirement();
+ keySet.forEach(securityRequirement::addList);
+ list.add(securityRequirement);
+ openApi.security(list);
+
+ return openApi;
+ }
+
+ private Info convertInfo(SpringDocProperties.InfoProperties infoProperties) {
+ Info info = new Info();
+ info.setTitle(infoProperties.getTitle());
+ info.setDescription(infoProperties.getDescription());
+ info.setContact(infoProperties.getContact());
+ info.setLicense(infoProperties.getLicense());
+ info.setVersion(infoProperties.getVersion());
+ return info;
+ }
+
+ /**
+ * 自定义 openapi 处理器
+ */
+ @Bean
+ public OpenAPIService openApiBuilder(Optional openAPI,
+ SecurityService securityParser,
+ SpringDocConfigProperties springDocConfigProperties, PropertyResolverUtils propertyResolverUtils,
+ Optional> openApiBuilderCustomisers,
+ Optional> serverBaseUrlCustomisers, Optional javadocProvider) {
+ return new OpenApiHandler(openAPI, securityParser, springDocConfigProperties, propertyResolverUtils, openApiBuilderCustomisers, serverBaseUrlCustomisers, javadocProvider);
+ }
+
+ /**
+ * 对已经生成好的 OpenApi 进行自定义操作
+ */
+ @Bean
+ public OpenApiCustomizer openApiCustomizer() {
+ String contextPath = serverProperties.getServlet().getContextPath();
+ String finalContextPath;
+ if (StringUtils.isBlank(contextPath) || "/".equals(contextPath)) {
+ finalContextPath = "";
+ } else {
+ finalContextPath = contextPath;
+ }
+ // 对所有路径增加前置上下文路径
+ return openApi -> {
+ Paths oldPaths = openApi.getPaths();
+ if (oldPaths instanceof PlusPaths) {
+ return;
+ }
+ PlusPaths newPaths = new PlusPaths();
+ oldPaths.forEach((k, v) -> newPaths.addPathItem(finalContextPath + k, v));
+ openApi.setPaths(newPaths);
+ };
+ }
+
+ /**
+ * 单独使用一个类便于判断 解决springdoc路径拼接重复问题
+ *
+ * @author Lion Li
+ */
+ static class PlusPaths extends Paths {
+
+ public PlusPaths() {
+ super();
+ }
+ }
+
+}
diff --git a/pusong-common/pusong-common-doc/src/main/java/com/pusong/common/doc/config/properties/SpringDocProperties.java b/pusong-common/pusong-common-doc/src/main/java/com/pusong/common/doc/config/properties/SpringDocProperties.java
new file mode 100644
index 0000000..8ab6d41
--- /dev/null
+++ b/pusong-common/pusong-common-doc/src/main/java/com/pusong/common/doc/config/properties/SpringDocProperties.java
@@ -0,0 +1,94 @@
+package com.pusong.common.doc.config.properties;
+
+import io.swagger.v3.oas.models.Components;
+import io.swagger.v3.oas.models.ExternalDocumentation;
+import io.swagger.v3.oas.models.Paths;
+import io.swagger.v3.oas.models.info.Contact;
+import io.swagger.v3.oas.models.info.License;
+import io.swagger.v3.oas.models.tags.Tag;
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.boot.context.properties.NestedConfigurationProperty;
+
+import java.util.List;
+
+/**
+ * swagger 配置属性
+ *
+ * @author Lion Li
+ */
+@Data
+@ConfigurationProperties(prefix = "springdoc")
+public class SpringDocProperties {
+
+ /**
+ * 文档基本信息
+ */
+ @NestedConfigurationProperty
+ private InfoProperties info = new InfoProperties();
+
+ /**
+ * 扩展文档地址
+ */
+ @NestedConfigurationProperty
+ private ExternalDocumentation externalDocs;
+
+ /**
+ * 标签
+ */
+ private List tags = null;
+
+ /**
+ * 路径
+ */
+ @NestedConfigurationProperty
+ private Paths paths = null;
+
+ /**
+ * 组件
+ */
+ @NestedConfigurationProperty
+ private Components components = null;
+
+ /**
+ *
+ * 文档的基础属性信息
+ *
+ *
+ * @see io.swagger.v3.oas.models.info.Info
+ *
+ * 为了 springboot 自动生产配置提示信息,所以这里复制一个类出来
+ */
+ @Data
+ public static class InfoProperties {
+
+ /**
+ * 标题
+ */
+ private String title = null;
+
+ /**
+ * 描述
+ */
+ private String description = null;
+
+ /**
+ * 联系人信息
+ */
+ @NestedConfigurationProperty
+ private Contact contact = null;
+
+ /**
+ * 许可证
+ */
+ @NestedConfigurationProperty
+ private License license = null;
+
+ /**
+ * 版本
+ */
+ private String version = null;
+
+ }
+
+}
diff --git a/pusong-common/pusong-common-doc/src/main/java/com/pusong/common/doc/handler/OpenApiHandler.java b/pusong-common/pusong-common-doc/src/main/java/com/pusong/common/doc/handler/OpenApiHandler.java
new file mode 100644
index 0000000..24c5690
--- /dev/null
+++ b/pusong-common/pusong-common-doc/src/main/java/com/pusong/common/doc/handler/OpenApiHandler.java
@@ -0,0 +1,252 @@
+package com.pusong.common.doc.handler;
+
+import cn.hutool.core.io.IoUtil;
+import io.swagger.v3.core.jackson.TypeNameResolver;
+import io.swagger.v3.core.util.AnnotationsUtils;
+import io.swagger.v3.oas.annotations.tags.Tags;
+import io.swagger.v3.oas.models.Components;
+import io.swagger.v3.oas.models.OpenAPI;
+import io.swagger.v3.oas.models.Operation;
+import io.swagger.v3.oas.models.Paths;
+import io.swagger.v3.oas.models.tags.Tag;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.springdoc.core.customizers.OpenApiBuilderCustomizer;
+import org.springdoc.core.customizers.ServerBaseUrlCustomizer;
+import org.springdoc.core.properties.SpringDocConfigProperties;
+import org.springdoc.core.providers.JavadocProvider;
+import org.springdoc.core.service.OpenAPIService;
+import org.springdoc.core.service.SecurityService;
+import org.springdoc.core.utils.PropertyResolverUtils;
+import org.springframework.context.ApplicationContext;
+import org.springframework.core.annotation.AnnotatedElementUtils;
+import org.springframework.util.CollectionUtils;
+import org.springframework.web.method.HandlerMethod;
+
+import java.io.StringReader;
+import java.lang.reflect.Method;
+import java.util.*;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/**
+ * 自定义 openapi 处理器
+ * 对源码功能进行修改 增强使用
+ */
+@Slf4j
+@SuppressWarnings("all")
+public class OpenApiHandler extends OpenAPIService {
+
+ /**
+ * The Basic error controller.
+ */
+ private static Class> basicErrorController;
+
+ /**
+ * The Security parser.
+ */
+ private final SecurityService securityParser;
+
+ /**
+ * The Mappings map.
+ */
+ private final Map mappingsMap = new HashMap<>();
+
+ /**
+ * The Springdoc tags.
+ */
+ private final Map springdocTags = new HashMap<>();
+
+ /**
+ * The Open api builder customisers.
+ */
+ private final Optional> openApiBuilderCustomisers;
+
+ /**
+ * The server base URL customisers.
+ */
+ private final Optional> serverBaseUrlCustomizers;
+
+ /**
+ * The Spring doc config properties.
+ */
+ private final SpringDocConfigProperties springDocConfigProperties;
+
+ /**
+ * The Cached open api map.
+ */
+ private final Map cachedOpenAPI = new HashMap<>();
+
+ /**
+ * The Property resolver utils.
+ */
+ private final PropertyResolverUtils propertyResolverUtils;
+
+ /**
+ * The javadoc provider.
+ */
+ private final Optional javadocProvider;
+
+ /**
+ * The Context.
+ */
+ private ApplicationContext context;
+
+ /**
+ * The Open api.
+ */
+ private OpenAPI openAPI;
+
+ /**
+ * The Is servers present.
+ */
+ private boolean isServersPresent;
+
+ /**
+ * The Server base url.
+ */
+ private String serverBaseUrl;
+
+ /**
+ * Instantiates a new Open api builder.
+ *
+ * @param openAPI the open api
+ * @param securityParser the security parser
+ * @param springDocConfigProperties the spring doc config properties
+ * @param propertyResolverUtils the property resolver utils
+ * @param openApiBuilderCustomizers the open api builder customisers
+ * @param serverBaseUrlCustomizers the server base url customizers
+ * @param javadocProvider the javadoc provider
+ */
+ public OpenApiHandler(Optional openAPI, SecurityService securityParser,
+ SpringDocConfigProperties springDocConfigProperties, PropertyResolverUtils propertyResolverUtils,
+ Optional> openApiBuilderCustomizers,
+ Optional> serverBaseUrlCustomizers,
+ Optional javadocProvider) {
+ super(openAPI, securityParser, springDocConfigProperties, propertyResolverUtils, openApiBuilderCustomizers, serverBaseUrlCustomizers, javadocProvider);
+ if (openAPI.isPresent()) {
+ this.openAPI = openAPI.get();
+ if (this.openAPI.getComponents() == null)
+ this.openAPI.setComponents(new Components());
+ if (this.openAPI.getPaths() == null)
+ this.openAPI.setPaths(new Paths());
+ if (!CollectionUtils.isEmpty(this.openAPI.getServers()))
+ this.isServersPresent = true;
+ }
+ this.propertyResolverUtils = propertyResolverUtils;
+ this.securityParser = securityParser;
+ this.springDocConfigProperties = springDocConfigProperties;
+ this.openApiBuilderCustomisers = openApiBuilderCustomizers;
+ this.serverBaseUrlCustomizers = serverBaseUrlCustomizers;
+ this.javadocProvider = javadocProvider;
+ if (springDocConfigProperties.isUseFqn())
+ TypeNameResolver.std.setUseFqn(true);
+ }
+
+ @Override
+ public Operation buildTags(HandlerMethod handlerMethod, Operation operation, OpenAPI openAPI, Locale locale) {
+
+ Set tags = new HashSet<>();
+ Set tagsStr = new HashSet<>();
+
+ buildTagsFromMethod(handlerMethod.getMethod(), tags, tagsStr, locale);
+ buildTagsFromClass(handlerMethod.getBeanType(), tags, tagsStr, locale);
+
+ if (!CollectionUtils.isEmpty(tagsStr))
+ tagsStr = tagsStr.stream()
+ .map(str -> propertyResolverUtils.resolve(str, locale))
+ .collect(Collectors.toSet());
+
+ if (springdocTags.containsKey(handlerMethod)) {
+ io.swagger.v3.oas.models.tags.Tag tag = springdocTags.get(handlerMethod);
+ tagsStr.add(tag.getName());
+ if (openAPI.getTags() == null || !openAPI.getTags().contains(tag)) {
+ openAPI.addTagsItem(tag);
+ }
+ }
+
+ if (!CollectionUtils.isEmpty(tagsStr)) {
+ if (CollectionUtils.isEmpty(operation.getTags()))
+ operation.setTags(new ArrayList<>(tagsStr));
+ else {
+ Set operationTagsSet = new HashSet<>(operation.getTags());
+ operationTagsSet.addAll(tagsStr);
+ operation.getTags().clear();
+ operation.getTags().addAll(operationTagsSet);
+ }
+ }
+
+ if (isAutoTagClasses(operation)) {
+
+
+ if (javadocProvider.isPresent()) {
+ String description = javadocProvider.get().getClassJavadoc(handlerMethod.getBeanType());
+ if (StringUtils.isNotBlank(description)) {
+ io.swagger.v3.oas.models.tags.Tag tag = new io.swagger.v3.oas.models.tags.Tag();
+
+ // 自定义部分 修改使用java注释当tag名
+ List list = IoUtil.readLines(new StringReader(description), new ArrayList<>());
+ // tag.setName(tagAutoName);
+ tag.setName(list.get(0));
+ operation.addTagsItem(list.get(0));
+
+ tag.setDescription(description);
+ if (openAPI.getTags() == null || !openAPI.getTags().contains(tag)) {
+ openAPI.addTagsItem(tag);
+ }
+ }
+ } else {
+ String tagAutoName = splitCamelCase(handlerMethod.getBeanType().getSimpleName());
+ operation.addTagsItem(tagAutoName);
+ }
+ }
+
+ if (!CollectionUtils.isEmpty(tags)) {
+ // Existing tags
+ List openApiTags = openAPI.getTags();
+ if (!CollectionUtils.isEmpty(openApiTags))
+ tags.addAll(openApiTags);
+ openAPI.setTags(new ArrayList<>(tags));
+ }
+
+ // Handle SecurityRequirement at operation level
+ io.swagger.v3.oas.annotations.security.SecurityRequirement[] securityRequirements = securityParser
+ .getSecurityRequirements(handlerMethod);
+ if (securityRequirements != null) {
+ if (securityRequirements.length == 0)
+ operation.setSecurity(Collections.emptyList());
+ else
+ securityParser.buildSecurityRequirement(securityRequirements, operation);
+ }
+
+ return operation;
+ }
+
+ private void buildTagsFromMethod(Method method, Set tags, Set tagsStr, Locale locale) {
+ // method tags
+ Set tagsSet = AnnotatedElementUtils
+ .findAllMergedAnnotations(method, Tags.class);
+ Set methodTags = tagsSet.stream()
+ .flatMap(x -> Stream.of(x.value())).collect(Collectors.toSet());
+ methodTags.addAll(AnnotatedElementUtils.findAllMergedAnnotations(method, io.swagger.v3.oas.annotations.tags.Tag.class));
+ if (!CollectionUtils.isEmpty(methodTags)) {
+ tagsStr.addAll(methodTags.stream().map(tag -> propertyResolverUtils.resolve(tag.name(), locale)).collect(Collectors.toSet()));
+ List allTags = new ArrayList<>(methodTags);
+ addTags(allTags, tags, locale);
+ }
+ }
+
+ private void addTags(List sourceTags, Set tags, Locale locale) {
+ Optional> optionalTagSet = AnnotationsUtils
+ .getTags(sourceTags.toArray(new io.swagger.v3.oas.annotations.tags.Tag[0]), true);
+ optionalTagSet.ifPresent(tagsSet -> {
+ tagsSet.forEach(tag -> {
+ tag.name(propertyResolverUtils.resolve(tag.getName(), locale));
+ tag.description(propertyResolverUtils.resolve(tag.getDescription(), locale));
+ if (tags.stream().noneMatch(t -> t.getName().equals(tag.getName())))
+ tags.add(tag);
+ });
+ });
+ }
+
+}
diff --git a/pusong-common/pusong-common-doc/src/main/java/com/pusong/common/doc/util/CustomXMLWorkerFontProvider.java b/pusong-common/pusong-common-doc/src/main/java/com/pusong/common/doc/util/CustomXMLWorkerFontProvider.java
new file mode 100644
index 0000000..4fa507c
--- /dev/null
+++ b/pusong-common/pusong-common-doc/src/main/java/com/pusong/common/doc/util/CustomXMLWorkerFontProvider.java
@@ -0,0 +1,34 @@
+package com.pusong.common.doc.util;
+
+import com.itextpdf.text.BaseColor;
+import com.itextpdf.text.Font;
+import com.itextpdf.text.pdf.BaseFont;
+import com.itextpdf.tool.xml.XMLWorkerFontProvider;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * 解决XMLWorkerHelper中文不显示。
+ * 使用iTextAsian.jar中自带的中文字体
+ *
+ */
+@Slf4j
+public class CustomXMLWorkerFontProvider extends XMLWorkerFontProvider {
+
+ @Override
+ public Font getFont(final String fontName, final String encoding, final boolean embedded, final float size, final int style,
+ final BaseColor color) {
+ BaseFont bf = null;
+ try {
+// bf = BaseFont.createFont("STSong-Light", "UniGB-UCS2-H", BaseFont.EMBEDDED);
+ bf =BaseFont.createFont("STSong-Light", "UniGB-UCS2-H",BaseFont.NOT_EMBEDDED);
+
+ Font font = new Font(bf, size, style, color);
+ font.setColor(color);
+ // log.info("PDF文档字体初始化完成!");
+ return font;
+ } catch (Exception e) {
+ log.error("exception:", e);
+ }
+ return null;
+ }
+}
diff --git a/pusong-common/pusong-common-doc/src/main/java/com/pusong/common/doc/util/NumBerTool.java b/pusong-common/pusong-common-doc/src/main/java/com/pusong/common/doc/util/NumBerTool.java
new file mode 100644
index 0000000..d370b6f
--- /dev/null
+++ b/pusong-common/pusong-common-doc/src/main/java/com/pusong/common/doc/util/NumBerTool.java
@@ -0,0 +1,131 @@
+package com.pusong.common.doc.util;
+
+import com.pusong.common.core.utils.StringUtils;
+
+public class NumBerTool {
+ private static final String[] NUMBERS = {"零", "壹", "贰", "叁", "肆", "伍", "陆",
+ "柒", "捌", "玖"};
+ /**
+ * 整数部分的单位
+ */
+ private static final String[] IUNIT = {"元", "拾", "佰", "仟", "万", "拾", "佰",
+ "仟", "亿", "拾", "佰", "仟", "万", "拾", "佰", "仟"};
+ /**
+ * 小数部分的单位
+ */
+ private static final String[] DUNIT = {"角", "分"};
+
+
+ /**
+ * 得到大写金额。
+ */
+ public static String digitUppercase(String str) {
+ str = str.replaceAll(",", "");// 去掉","
+ String integerStr;// 整数部分数字
+ String decimalStr;// 小数部分数字
+ // 初始化:分离整数部分和小数部分
+ if (str.indexOf(".") > 0) {
+ integerStr = str.substring(0, str.indexOf("."));
+ decimalStr = str.substring(str.indexOf(".") + 1);
+ } else if (str.indexOf(".") == 0) {
+ integerStr = "";
+ decimalStr = str.substring(1);
+ } else {
+ integerStr = str;
+ decimalStr = "";
+ }
+ // integerStr去掉首0,不必去掉decimalStr的尾0(超出部分舍去)
+ if (!integerStr.equals("")) {
+ integerStr = Long.toString(Long.parseLong(integerStr));
+ if (integerStr.equals("0")) {
+ integerStr = "";
+ }
+ }
+ // overflow超出处理能力,直接返回
+ if (integerStr.length() > IUNIT.length) {
+ System.out.println(str + ":超出处理能力");
+ return str;
+ }
+ int[] integers = toArray(integerStr);// 整数部分数字
+ boolean isMust5 = isMust5(integerStr);// 设置万单位
+ int[] decimals = toArray(decimalStr);// 小数部分数字
+ return getChineseInteger(integers, isMust5) + getChineseDecimal(decimals);
+ }
+
+ /**
+ * 整数部分和小数部分转换为数组,从高位至低位
+ */
+ private static int[] toArray(String number) {
+ int[] array = new int[number.length()];
+ for (int i = 0; i < number.length(); i++) {
+ array[i] = Integer.parseInt(number.substring(i, i + 1));
+ }
+ return array;
+ }
+
+ /**
+ * 得到中文金额的整数部分。
+ */
+ private static String getChineseInteger(int[] integers, boolean isMust5) {
+ StringBuffer chineseInteger = new StringBuffer("");
+ int length = integers.length;
+ for (int i = 0; i < length; i++) {
+ // 0出现在关键位置:1234(万)5678(亿)9012(万)3456(元)
+ // 特殊情况:10(拾元、壹拾元、壹拾万元、拾万元)
+ String key = "";
+ if (integers[i] == 0) {
+ if ((length - i) == 13)// 万(亿)(必填)
+ key = IUNIT[4];
+ else if ((length - i) == 9)// 亿(必填)
+ key = IUNIT[8];
+ else if ((length - i) == 5 && isMust5)// 万(不必填)
+ key = IUNIT[4];
+ else if ((length - i) == 1)// 元(必填)
+ key = IUNIT[0];
+ // 0遇非0时补零,不包含最后一位
+ if ((length - i) > 1 && integers[i + 1] != 0)
+ key += NUMBERS[0];
+ }
+ chineseInteger.append(integers[i] == 0 ? key
+ : (NUMBERS[integers[i]] + IUNIT[length - i - 1]));
+ }
+ return chineseInteger.toString();
+ }
+
+ /**
+ * 得到中文金额的小数部分。
+ */
+ private static String getChineseDecimal(int[] decimals) {
+ StringBuffer chineseDecimal = new StringBuffer("");
+ for (int i = 0; i < decimals.length; i++) {
+ // 舍去2位小数之后的
+ if (i == 2)
+ break;
+ chineseDecimal.append(decimals[i] == 0 ? ""
+ : (NUMBERS[decimals[i]] + DUNIT[i]));
+ }
+ if(StringUtils.isBlank(chineseDecimal)){
+ chineseDecimal.append("整");
+ }
+ return chineseDecimal.toString();
+ }
+
+ /**
+ * 判断第5位数字的单位"万"是否应加。
+ */
+ private static boolean isMust5(String integerStr) {
+ int length = integerStr.length();
+ if (length > 4) {
+ String subInteger = "";
+ if (length > 8) {
+ // 取得从低位数,第5到第8位的字串
+ subInteger = integerStr.substring(length - 8, length - 4);
+ } else {
+ subInteger = integerStr.substring(0, length - 4);
+ }
+ return Integer.parseInt(subInteger) > 0;
+ } else {
+ return false;
+ }
+ }
+}
\ No newline at end of file
diff --git a/pusong-common/pusong-common-doc/src/main/java/com/pusong/common/doc/util/PDFBuilder.java b/pusong-common/pusong-common-doc/src/main/java/com/pusong/common/doc/util/PDFBuilder.java
new file mode 100644
index 0000000..45d6b59
--- /dev/null
+++ b/pusong-common/pusong-common-doc/src/main/java/com/pusong/common/doc/util/PDFBuilder.java
@@ -0,0 +1,159 @@
+package com.pusong.common.doc.util;
+
+
+import java.io.IOException;
+
+import com.itextpdf.text.*;
+import com.itextpdf.text.pdf.*;
+import com.pusong.common.core.utils.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+
+/**
+ * 设置页面附加属性
+ *
+ */
+public class PDFBuilder extends PdfPageEventHelper {
+
+ private static final Logger log = LoggerFactory.getLogger(PDFBuilder.class);
+ /**
+ * 页眉
+ */
+ public String header ;
+ /**
+ * 是否签章
+ */
+ public Boolean isSign ;
+ /**
+ * 是否签章
+ */
+ public String imagepath ;
+ // 利用基础字体生成的字体对象,一般用于生成中文文字
+ public static Font fontDetail = null;
+ // 基础字体对象
+ private static BaseFont baseFont;
+ // 生成下划线空白占位符
+ private static String Blank;
+ static {
+ try {
+ // 中文字体依赖itext得itext-asian包
+ baseFont = BaseFont.createFont("STSong-Light", "UniGB-UCS2-H", BaseFont.NOT_EMBEDDED);
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < 212; i++) {
+ sb.append("\u00a0");
+ }
+ Blank = sb.toString();
+ fontDetail = new Font(baseFont,8);
+ } catch (DocumentException e) {
+ log.error("初始化字体失败", e);
+ } catch (IOException e) {
+ log.error("初始化字体失败", e);
+ }
+ }
+
+ /**
+ *
+ * Creates a new instance of PdfReportM1HeaderFooter 无参构造方法.
+ *
+ */
+ public PDFBuilder(Object header,Object isSign,String imagepath) {
+ this.header = header == null?null:header.toString();
+ this.isSign = isSign == null?null:(Boolean)isSign;
+ this.imagepath = imagepath;
+ }
+
+ /**
+ *
+ * TODO 文档打开时创建模板
+ *
+ * @see com.itextpdf.text.pdf.PdfPageEventHelper#onOpenDocument(com.itextpdf.text.pdf.PdfWriter,
+ * com.itextpdf.text.Document)
+ */
+ public void onOpenDocument(PdfWriter writer, Document document) {
+ }
+
+ @Override
+
+ public void onStartPage(PdfWriter writer, Document document) {
+ super.onStartPage(writer, document);
+
+ }
+
+ /**
+ *
+ * TODO 关闭每页的时候,写入页眉,写入'第几页共'这几个字。
+ *
+ * @see com.itextpdf.text.pdf.PdfPageEventHelper#onEndPage(com.itextpdf.text.pdf.PdfWriter,
+ * com.itextpdf.text.Document)
+ */
+ public void onEndPage(PdfWriter writer, Document document) {
+ //是否添加页眉
+ if(StringUtils.isNotBlank(header)){
+ this.addPage(writer, document);
+ }
+ //是否签章
+ if(isSign){
+ this.addSign(writer);
+ }
+
+ }
+
+ //加分页
+ public void addPage(PdfWriter writer, Document document) {
+ //页眉没有数据的话直接return
+ if(StringUtils.isBlank(header)){
+ return;
+ }
+ //生成下划线,使用空格占位
+ ColumnText.showTextAligned(writer.getDirectContent(),
+ Element.ALIGN_LEFT, new Phrase(PDFBuilder.Blank, new Font(PDFBuilder.baseFont, Font.DEFAULTSIZE, Font.UNDERLINE)),
+ document.left(-1), document.top()+10, 0);
+ // 1.写入页眉
+ ColumnText.showTextAligned(writer.getDirectContent(),
+ Element.ALIGN_LEFT, new Phrase(header, fontDetail),
+ document.right()/2+20, document.top()+10 , 0);
+ }
+
+ //加半章图片
+ public void addSign(PdfWriter writer){
+ String psth;
+ if(writer.getPageNumber()%2 == 1){
+ psth = imagepath+"/doc/image/leftZ.png";
+ }else/* if (writer.getPageNumber() == 2)*/{
+ psth = imagepath+"/doc/image/rightZ.png";
+ }/*else{
+ return;
+ }*/
+ // 加半章图片
+ Image image;
+ try {
+ image = Image.getInstance(psth);
+ image.scaleAbsolute(75,150);
+ PdfContentByte content = writer.getDirectContentUnder();
+ content.beginText();
+ image.setAbsolutePosition(450+75,400);
+ content.addImage(image);
+ content.endText();
+ } catch (IOException | DocumentException e) {
+ log.error("加半章图片异常",e);
+ }
+ }
+
+ /**
+ *
+ * TODO 关闭文档时,替换模板,完成整个页眉页脚组件
+ *
+ * @see com.itextpdf.text.pdf.PdfPageEventHelper#onCloseDocument(com.itextpdf.text.pdf.PdfWriter,
+ * com.itextpdf.text.Document)
+ */
+ /* public void onCloseDocument(PdfWriter writer, Document document) {
+ // 7.最后一步了,就是关闭文档的时候,将模板替换成实际的 Y 值,至此,page x of y 制作完毕,完美兼容各种文档size。
+ total.beginText();
+ total.setFontAndSize(bf, presentFontSize);// 生成的模版的字体、颜色
+ String foot2 = " " + (writer.getPageNumber()-1) + " 页";
+ total.showText(foot2);// 模版显示的内容
+ total.endText();
+ total.closePath();
+ }*/
+}
diff --git a/pusong-common/pusong-common-doc/src/main/java/com/pusong/common/doc/util/PdfGenerator.java b/pusong-common/pusong-common-doc/src/main/java/com/pusong/common/doc/util/PdfGenerator.java
new file mode 100644
index 0000000..4e1cbb9
--- /dev/null
+++ b/pusong-common/pusong-common-doc/src/main/java/com/pusong/common/doc/util/PdfGenerator.java
@@ -0,0 +1,118 @@
+package com.pusong.common.doc.util;
+
+import com.alibaba.fastjson.JSON;
+import com.itextpdf.text.Document;
+import com.itextpdf.text.PageSize;
+import com.itextpdf.text.pdf.PdfWriter;
+import com.itextpdf.tool.xml.XMLWorkerHelper;
+import freemarker.template.Configuration;
+import freemarker.template.Template;
+import freemarker.template.TemplateException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.*;
+import java.nio.charset.Charset;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class PdfGenerator {
+
+ private static final Logger log = LoggerFactory.getLogger(PdfGenerator.class);
+ //标题
+ public static final String HEAD = "head";
+ //是否签章
+ public static final String SIGN = "isSign";
+ public static Configuration configuration =null;
+ static {
+ configuration = new Configuration(Configuration.VERSION_2_3_31);
+ // 设置模板目录
+ File file = new File(System.getProperty("user.dir") + "/doc");
+ try {
+ configuration.setDirectoryForTemplateLoading(file);
+ } catch (IOException e) {
+ configuration =null;
+ throw new RuntimeException(e);
+ }
+ }
+ public static void main(String[] args) throws Exception {
+
+
+// // 创建数据模型
+// Map data = new HashMap<>();
+// data.put("text1", "天津浦颂企业管理咨询有限公司");
+// data.put("text2", "");
+// data.put("total", 1000);
+// List> map = new ArrayList<>();
+// Map map1=new HashMap<>();
+// map1.put("id","7894564123");
+// map.add(map1);
+// Map map2 =new HashMap<>();
+// map2.put("id","7894564123");
+// map.add(map2);
+// data.put("items",map);
+//
+// // 设置模板目录
+// Configuration configuration = new Configuration(Configuration.VERSION_2_3_31);
+// configuration.setDirectoryForTemplateLoading(new File(System.getProperty("user.dir")+"\\doc"));
+// // 加载模板
+// Template template = configuration.getTemplate("report.ftl");
+// data.put("imagePath",System.getProperty("user.dir"));
+// // 生成HTML
+// String html = processTemplate(template, data);
+// // 生成PDF
+// generatePdf(html, "D:/王立帅/临时/output.pdf");
+ String str = "{\"business\":[{\"businessAmount\":500,\"businessType\":\"1\",\"contractCode\":\"\",\"detailBos\":[{\"amount\":100,\"amountDesc\":\"备注\",\"businessProject\":\"1\",\"businessProjectLabel\":\"测试1\",\"contractCode\":\"\",\"id\":0}],\"id\":0}],\"contract\":{\"applyDate\":1722355200000,\"companyId\":0,\"contractAmount\":500,\"contractCode\":\"\",\"contractMain\":\"1\",\"contractName\":\"合同名称\",\"customId\":1816388144534081538,\"customManager\":1,\"customScene\":\"\",\"isProxy\":\"\",\"params\":{},\"payModeDesc\":\"支付方式\",\"signDesc\":\"描述描述\"},\"company\":{\"companyAccountBank\":\"银行编码\",\"companyAccountBankAdress\":\"开户行地址\",\"companyAdress\":\"公司地址\",\"companyName\":\"公司名\",\"customId\":0,\"id\":0,\"legalPersonIdcard\":\"法人身份证\",\"legalPersonName\":\"法人姓名\",\"legalPersonPhone\":\"法人手机号\"},\"customer\":{\"black\":\"0\",\"createBy\":1,\"createDept\":103,\"createTime\":1721895655000,\"customLevel\":\"2\",\"customManager\":3,\"customMobile\":\"13511245332\",\"customName\":\"东洲\",\"customSource\":\"抖音\",\"customStatus\":\"1\",\"delFlag\":0,\"id\":1816388144534081538,\"params\":{},\"tenantId\":\"000000\",\"updateBy\":1,\"updateTime\":1723097358000}}";
+ Map map = JSON.parseObject(str, Map.class);
+ map.put(HEAD,"sss");
+ map.put(SIGN,true);
+ makePdf(map,"D:/王立帅/临时/output.pdf","report.ftl");
+ }
+
+
+ public static void makePdf(Map data ,String pdfpath,String templateName) throws Exception{
+ // 加载模板
+ Template template = configuration.getTemplate(templateName);
+ data.put("imagePath",System.getProperty("user.dir"));
+ // 生成HTML
+ String html = processTemplate(template, data);
+ // 生成PDF
+ PDFBuilder builder = new PDFBuilder(data.get(HEAD),data.get(SIGN),System.getProperty("user.dir"));
+ generatePdf(html, pdfpath, builder);
+
+ }
+ private static String processTemplate(Template template, Map data) throws IOException, TemplateException {
+ StringWriter writer = new StringWriter();
+ template.process(data, writer);
+ return writer.toString();
+ }
+
+ private static void generatePdf(String html, String outputPath,PDFBuilder builder) {
+ Document document = null;
+ PdfWriter writer = null;
+ try{
+ document = new Document(PageSize.A4, 36, 36, 50, 50);
+ writer = PdfWriter.getInstance(document, new FileOutputStream(outputPath));
+
+ writer.setPageEvent(builder);
+ document.open();
+ XMLWorkerHelper.getInstance().parseXHtml(writer, document, new ByteArrayInputStream(html.getBytes()),
+ XMLWorkerHelper.class.getResourceAsStream("/default.css"),
+ Charset.forName("UTF-8"),
+ new CustomXMLWorkerFontProvider());
+ }catch (Exception e){
+ log.error("生成pdf异常",e);
+ }finally {
+ if(document != null){
+ document.close();
+ }
+ if(writer != null){
+ writer.close();
+ }
+ }
+
+
+ }
+}
diff --git a/pusong-common/pusong-common-doc/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/pusong-common/pusong-common-doc/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 0000000..b7f3dd6
--- /dev/null
+++ b/pusong-common/pusong-common-doc/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -0,0 +1 @@
+com.pusong.common.doc.config.SpringDocConfig
diff --git a/pusong-common/pusong-common-encrypt/pom.xml b/pusong-common/pusong-common-encrypt/pom.xml
new file mode 100644
index 0000000..eaf2b76
--- /dev/null
+++ b/pusong-common/pusong-common-encrypt/pom.xml
@@ -0,0 +1,54 @@
+
+
+
+ com.pusong
+ pusong-common
+ ${revision}
+
+ 4.0.0
+
+ pusong-common-encrypt
+
+
+ ruoyi-common-encrypt 数据加解密模块
+
+
+
+
+
+ com.pusong
+ pusong-common-core
+
+
+
+ org.bouncycastle
+ bcprov-jdk15to18
+
+
+
+ cn.hutool
+ hutool-crypto
+
+
+
+ org.springframework
+ spring-webmvc
+
+
+
+ com.baomidou
+ mybatis-plus-spring-boot3-starter
+ true
+
+
+ org.mybatis
+ mybatis-spring
+
+
+
+
+
+
+
diff --git a/pusong-common/pusong-common-encrypt/src/main/java/com/pusong/common/encrypt/annotation/ApiEncrypt.java b/pusong-common/pusong-common-encrypt/src/main/java/com/pusong/common/encrypt/annotation/ApiEncrypt.java
new file mode 100644
index 0000000..9717442
--- /dev/null
+++ b/pusong-common/pusong-common-encrypt/src/main/java/com/pusong/common/encrypt/annotation/ApiEncrypt.java
@@ -0,0 +1,20 @@
+package com.pusong.common.encrypt.annotation;
+
+import java.lang.annotation.*;
+
+/**
+ * 强制加密注解
+ *
+ * @author Michelle.Chung
+ */
+@Documented
+@Target({ElementType.METHOD})
+@Retention(RetentionPolicy.RUNTIME)
+public @interface ApiEncrypt {
+
+ /**
+ * 响应加密忽略,默认不加密,为 true 时加密
+ */
+ boolean response() default false;
+
+}
diff --git a/pusong-common/pusong-common-encrypt/src/main/java/com/pusong/common/encrypt/annotation/EncryptField.java b/pusong-common/pusong-common-encrypt/src/main/java/com/pusong/common/encrypt/annotation/EncryptField.java
new file mode 100644
index 0000000..6f7af07
--- /dev/null
+++ b/pusong-common/pusong-common-encrypt/src/main/java/com/pusong/common/encrypt/annotation/EncryptField.java
@@ -0,0 +1,44 @@
+package com.pusong.common.encrypt.annotation;
+
+import com.pusong.common.encrypt.enumd.AlgorithmType;
+import com.pusong.common.encrypt.enumd.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/pusong-common/pusong-common-encrypt/src/main/java/com/pusong/common/encrypt/config/ApiDecryptAutoConfiguration.java b/pusong-common/pusong-common-encrypt/src/main/java/com/pusong/common/encrypt/config/ApiDecryptAutoConfiguration.java
new file mode 100644
index 0000000..691948d
--- /dev/null
+++ b/pusong-common/pusong-common-encrypt/src/main/java/com/pusong/common/encrypt/config/ApiDecryptAutoConfiguration.java
@@ -0,0 +1,32 @@
+package com.pusong.common.encrypt.config;
+
+import com.pusong.common.encrypt.filter.CryptoFilter;
+import jakarta.servlet.DispatcherType;
+import com.pusong.common.encrypt.properties.ApiDecryptProperties;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.boot.web.servlet.FilterRegistrationBean;
+import org.springframework.context.annotation.Bean;
+
+/**
+ * api 解密自动配置
+ *
+ * @author wdhcr
+ */
+@AutoConfiguration
+@EnableConfigurationProperties(ApiDecryptProperties.class)
+@ConditionalOnProperty(value = "api-decrypt.enabled", havingValue = "true")
+public class ApiDecryptAutoConfiguration {
+
+ @Bean
+ public FilterRegistrationBean cryptoFilterRegistration(ApiDecryptProperties properties) {
+ FilterRegistrationBean registration = new FilterRegistrationBean<>();
+ registration.setDispatcherTypes(DispatcherType.REQUEST);
+ registration.setFilter(new CryptoFilter(properties));
+ registration.addUrlPatterns("/*");
+ registration.setName("cryptoFilter");
+ registration.setOrder(FilterRegistrationBean.HIGHEST_PRECEDENCE);
+ return registration;
+ }
+}
diff --git a/pusong-common/pusong-common-encrypt/src/main/java/com/pusong/common/encrypt/config/EncryptorAutoConfiguration.java b/pusong-common/pusong-common-encrypt/src/main/java/com/pusong/common/encrypt/config/EncryptorAutoConfiguration.java
new file mode 100644
index 0000000..dbb9c10
--- /dev/null
+++ b/pusong-common/pusong-common-encrypt/src/main/java/com/pusong/common/encrypt/config/EncryptorAutoConfiguration.java
@@ -0,0 +1,49 @@
+package com.pusong.common.encrypt.config;
+
+import com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration;
+import com.baomidou.mybatisplus.autoconfigure.MybatisPlusProperties;
+import lombok.extern.slf4j.Slf4j;
+import com.pusong.common.encrypt.core.EncryptorManager;
+import com.pusong.common.encrypt.interceptor.MybatisDecryptInterceptor;
+import com.pusong.common.encrypt.interceptor.MybatisEncryptInterceptor;
+import com.pusong.common.encrypt.properties.EncryptorProperties;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+
+/**
+ * 加解密配置
+ *
+ * @author 老马
+ * @version 4.6.0
+ */
+@AutoConfiguration(after = MybatisPlusAutoConfiguration.class)
+@EnableConfigurationProperties(EncryptorProperties.class)
+@ConditionalOnProperty(value = "mybatis-encryptor.enable", havingValue = "true")
+@Slf4j
+public class EncryptorAutoConfiguration {
+
+ @Autowired
+ private EncryptorProperties properties;
+
+ @Bean
+ public EncryptorManager encryptorManager(MybatisPlusProperties mybatisPlusProperties) {
+ return new EncryptorManager(mybatisPlusProperties.getTypeAliasesPackage());
+ }
+
+ @Bean
+ public MybatisEncryptInterceptor mybatisEncryptInterceptor(EncryptorManager encryptorManager) {
+ return new MybatisEncryptInterceptor(encryptorManager, properties);
+ }
+
+ @Bean
+ public MybatisDecryptInterceptor mybatisDecryptInterceptor(EncryptorManager encryptorManager) {
+ return new MybatisDecryptInterceptor(encryptorManager, properties);
+ }
+
+}
+
+
+
diff --git a/pusong-common/pusong-common-encrypt/src/main/java/com/pusong/common/encrypt/core/EncryptContext.java b/pusong-common/pusong-common-encrypt/src/main/java/com/pusong/common/encrypt/core/EncryptContext.java
new file mode 100644
index 0000000..17a6154
--- /dev/null
+++ b/pusong-common/pusong-common-encrypt/src/main/java/com/pusong/common/encrypt/core/EncryptContext.java
@@ -0,0 +1,41 @@
+package com.pusong.common.encrypt.core;
+
+import com.pusong.common.encrypt.enumd.AlgorithmType;
+import com.pusong.common.encrypt.enumd.EncodeType;
+import lombok.Data;
+
+/**
+ * 加密上下文 用于encryptor传递必要的参数。
+ *
+ * @author 老马
+ * @version 4.6.0
+ */
+@Data
+public class EncryptContext {
+
+ /**
+ * 默认算法
+ */
+ private AlgorithmType algorithm;
+
+ /**
+ * 安全秘钥
+ */
+ private String password;
+
+ /**
+ * 公钥
+ */
+ private String publicKey;
+
+ /**
+ * 私钥
+ */
+ private String privateKey;
+
+ /**
+ * 编码方式,base64/hex
+ */
+ private EncodeType encode;
+
+}
diff --git a/pusong-common/pusong-common-encrypt/src/main/java/com/pusong/common/encrypt/core/EncryptorManager.java b/pusong-common/pusong-common-encrypt/src/main/java/com/pusong/common/encrypt/core/EncryptorManager.java
new file mode 100644
index 0000000..18fafcd
--- /dev/null
+++ b/pusong-common/pusong-common-encrypt/src/main/java/com/pusong/common/encrypt/core/EncryptorManager.java
@@ -0,0 +1,158 @@
+package com.pusong.common.encrypt.core;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.core.util.ReflectUtil;
+import lombok.NoArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.ibatis.io.Resources;
+import com.pusong.common.core.utils.StringUtils;
+import com.pusong.common.encrypt.annotation.EncryptField;
+import org.springframework.context.ConfigurableApplicationContext;
+import org.springframework.core.io.Resource;
+import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
+import org.springframework.core.io.support.ResourcePatternResolver;
+import org.springframework.core.type.ClassMetadata;
+import org.springframework.core.type.classreading.CachingMetadataReaderFactory;
+import org.springframework.util.ClassUtils;
+
+import java.lang.reflect.Field;
+import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.stream.Collectors;
+
+/**
+ * 加密管理类
+ *
+ * @author 老马
+ * @version 4.6.0
+ */
+@Slf4j
+@NoArgsConstructor
+public class EncryptorManager {
+
+ /**
+ * 缓存加密器
+ */
+ Map encryptorMap = new ConcurrentHashMap<>();
+
+ /**
+ * 类加密字段缓存
+ */
+ Map, Set> fieldCache = new ConcurrentHashMap<>();
+
+ /**
+ * 构造方法传入类加密字段缓存
+ *
+ * @param typeAliasesPackage 实体类包
+ */
+ public EncryptorManager(String typeAliasesPackage) {
+ scanEncryptClasses(typeAliasesPackage);
+ }
+
+
+ /**
+ * 获取类加密字段缓存
+ */
+ public Set getFieldCache(Class> sourceClazz) {
+ if (ObjectUtil.isNotNull(fieldCache)) {
+ return fieldCache.get(sourceClazz);
+ }
+ return null;
+ }
+
+ /**
+ * 注册加密执行者到缓存
+ *
+ * @param encryptContext 加密执行者需要的相关配置参数
+ */
+ public IEncryptor registAndGetEncryptor(EncryptContext encryptContext) {
+ if (encryptorMap.containsKey(encryptContext)) {
+ return encryptorMap.get(encryptContext);
+ }
+ IEncryptor encryptor = ReflectUtil.newInstance(encryptContext.getAlgorithm().getClazz(), encryptContext);
+ encryptorMap.put(encryptContext, encryptor);
+ return encryptor;
+ }
+
+ /**
+ * 移除缓存中的加密执行者
+ *
+ * @param encryptContext 加密执行者需要的相关配置参数
+ */
+ public void removeEncryptor(EncryptContext encryptContext) {
+ this.encryptorMap.remove(encryptContext);
+ }
+
+ /**
+ * 根据配置进行加密。会进行本地缓存对应的算法和对应的秘钥信息。
+ *
+ * @param value 待加密的值
+ * @param encryptContext 加密相关的配置信息
+ */
+ public String encrypt(String value, EncryptContext encryptContext) {
+ IEncryptor encryptor = this.registAndGetEncryptor(encryptContext);
+ return encryptor.encrypt(value, encryptContext.getEncode());
+ }
+
+ /**
+ * 根据配置进行解密
+ *
+ * @param value 待解密的值
+ * @param encryptContext 加密相关的配置信息
+ */
+ public String decrypt(String value, EncryptContext encryptContext) {
+ IEncryptor encryptor = this.registAndGetEncryptor(encryptContext);
+ return encryptor.decrypt(value);
+ }
+
+ /**
+ * 通过 typeAliasesPackage 设置的扫描包 扫描缓存实体
+ */
+ private void scanEncryptClasses(String typeAliasesPackage) {
+ PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
+ CachingMetadataReaderFactory factory = new CachingMetadataReaderFactory();
+ String[] packagePatternArray = StringUtils.splitPreserveAllTokens(typeAliasesPackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS);
+ String classpath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX;
+ try {
+ for (String packagePattern : packagePatternArray) {
+ String path = ClassUtils.convertClassNameToResourcePath(packagePattern);
+ Resource[] resources = resolver.getResources(classpath + path + "/*.class");
+ for (Resource resource : resources) {
+ ClassMetadata classMetadata = factory.getMetadataReader(resource).getClassMetadata();
+ Class> clazz = Resources.classForName(classMetadata.getClassName());
+ Set encryptFieldSet = getEncryptFieldSetFromClazz(clazz);
+ if (CollUtil.isNotEmpty(encryptFieldSet)) {
+ fieldCache.put(clazz, encryptFieldSet);
+ }
+ }
+ }
+ } catch (Exception e) {
+ log.error("初始化数据安全缓存时出错:{}", e.getMessage());
+ }
+ }
+
+ /**
+ * 获得一个类的加密字段集合
+ */
+ private Set getEncryptFieldSetFromClazz(Class> clazz) {
+ Set fieldSet = new HashSet<>();
+ // 判断clazz如果是接口,内部类,匿名类就直接返回
+ if (clazz.isInterface() || clazz.isMemberClass() || clazz.isAnonymousClass()) {
+ return fieldSet;
+ }
+ while (clazz != null) {
+ Field[] fields = clazz.getDeclaredFields();
+ fieldSet.addAll(Arrays.asList(fields));
+ clazz = clazz.getSuperclass();
+ }
+ fieldSet = fieldSet.stream().filter(field ->
+ field.isAnnotationPresent(EncryptField.class) && field.getType() == String.class)
+ .collect(Collectors.toSet());
+ for (Field field : fieldSet) {
+ field.setAccessible(true);
+ }
+ return fieldSet;
+ }
+
+}
diff --git a/pusong-common/pusong-common-encrypt/src/main/java/com/pusong/common/encrypt/core/IEncryptor.java b/pusong-common/pusong-common-encrypt/src/main/java/com/pusong/common/encrypt/core/IEncryptor.java
new file mode 100644
index 0000000..852eede
--- /dev/null
+++ b/pusong-common/pusong-common-encrypt/src/main/java/com/pusong/common/encrypt/core/IEncryptor.java
@@ -0,0 +1,35 @@
+package com.pusong.common.encrypt.core;
+
+import com.pusong.common.encrypt.enumd.AlgorithmType;
+import com.pusong.common.encrypt.enumd.EncodeType;
+
+/**
+ * 加解者
+ *
+ * @author 老马
+ * @version 4.6.0
+ */
+public interface IEncryptor {
+
+ /**
+ * 获得当前算法
+ */
+ AlgorithmType algorithm();
+
+ /**
+ * 加密
+ *
+ * @param value 待加密字符串
+ * @param encodeType 加密后的编码格式
+ * @return 加密后的字符串
+ */
+ String encrypt(String value, EncodeType encodeType);
+
+ /**
+ * 解密
+ *
+ * @param value 待加密字符串
+ * @return 解密后的字符串
+ */
+ String decrypt(String value);
+}
diff --git a/pusong-common/pusong-common-encrypt/src/main/java/com/pusong/common/encrypt/core/encryptor/AbstractEncryptor.java b/pusong-common/pusong-common-encrypt/src/main/java/com/pusong/common/encrypt/core/encryptor/AbstractEncryptor.java
new file mode 100644
index 0000000..a9987c2
--- /dev/null
+++ b/pusong-common/pusong-common-encrypt/src/main/java/com/pusong/common/encrypt/core/encryptor/AbstractEncryptor.java
@@ -0,0 +1,18 @@
+package com.pusong.common.encrypt.core.encryptor;
+
+import com.pusong.common.encrypt.core.EncryptContext;
+import com.pusong.common.encrypt.core.IEncryptor;
+
+/**
+ * 所有加密执行者的基类
+ *
+ * @author 老马
+ * @version 4.6.0
+ */
+public abstract class AbstractEncryptor implements IEncryptor {
+
+ public AbstractEncryptor(EncryptContext context) {
+ // 用户配置校验与配置注入
+ }
+
+}
diff --git a/pusong-common/pusong-common-encrypt/src/main/java/com/pusong/common/encrypt/core/encryptor/AesEncryptor.java b/pusong-common/pusong-common-encrypt/src/main/java/com/pusong/common/encrypt/core/encryptor/AesEncryptor.java
new file mode 100644
index 0000000..bb05167
--- /dev/null
+++ b/pusong-common/pusong-common-encrypt/src/main/java/com/pusong/common/encrypt/core/encryptor/AesEncryptor.java
@@ -0,0 +1,55 @@
+package com.pusong.common.encrypt.core.encryptor;
+
+import com.pusong.common.encrypt.enumd.AlgorithmType;
+import com.pusong.common.encrypt.enumd.EncodeType;
+import com.pusong.common.encrypt.utils.EncryptUtils;
+import com.pusong.common.encrypt.core.EncryptContext;
+
+/**
+ * AES算法实现
+ *
+ * @author 老马
+ * @version 4.6.0
+ */
+public class AesEncryptor extends AbstractEncryptor {
+
+ private final EncryptContext context;
+
+ public AesEncryptor(EncryptContext context) {
+ super(context);
+ this.context = context;
+ }
+
+ /**
+ * 获得当前算法
+ */
+ @Override
+ public AlgorithmType algorithm() {
+ return AlgorithmType.AES;
+ }
+
+ /**
+ * 加密
+ *
+ * @param value 待加密字符串
+ * @param encodeType 加密后的编码格式
+ */
+ @Override
+ public String encrypt(String value, EncodeType encodeType) {
+ if (encodeType == EncodeType.HEX) {
+ return EncryptUtils.encryptByAesHex(value, context.getPassword());
+ } else {
+ return EncryptUtils.encryptByAes(value, context.getPassword());
+ }
+ }
+
+ /**
+ * 解密
+ *
+ * @param value 待加密字符串
+ */
+ @Override
+ public String decrypt(String value) {
+ return EncryptUtils.decryptByAes(value, context.getPassword());
+ }
+}
diff --git a/pusong-common/pusong-common-encrypt/src/main/java/com/pusong/common/encrypt/core/encryptor/Base64Encryptor.java b/pusong-common/pusong-common-encrypt/src/main/java/com/pusong/common/encrypt/core/encryptor/Base64Encryptor.java
new file mode 100644
index 0000000..fc209a8
--- /dev/null
+++ b/pusong-common/pusong-common-encrypt/src/main/java/com/pusong/common/encrypt/core/encryptor/Base64Encryptor.java
@@ -0,0 +1,48 @@
+package com.pusong.common.encrypt.core.encryptor;
+
+import com.pusong.common.encrypt.enumd.AlgorithmType;
+import com.pusong.common.encrypt.enumd.EncodeType;
+import com.pusong.common.encrypt.utils.EncryptUtils;
+import com.pusong.common.encrypt.core.EncryptContext;
+
+/**
+ * Base64算法实现
+ *
+ * @author 老马
+ * @version 4.6.0
+ */
+public class Base64Encryptor extends AbstractEncryptor {
+
+ public Base64Encryptor(EncryptContext context) {
+ super(context);
+ }
+
+ /**
+ * 获得当前算法
+ */
+ @Override
+ public AlgorithmType algorithm() {
+ return AlgorithmType.BASE64;
+ }
+
+ /**
+ * 加密
+ *
+ * @param value 待加密字符串
+ * @param encodeType 加密后的编码格式
+ */
+ @Override
+ public String encrypt(String value, EncodeType encodeType) {
+ return EncryptUtils.encryptByBase64(value);
+ }
+
+ /**
+ * 解密
+ *
+ * @param value 待加密字符串
+ */
+ @Override
+ public String decrypt(String value) {
+ return EncryptUtils.decryptByBase64(value);
+ }
+}
diff --git a/pusong-common/pusong-common-encrypt/src/main/java/com/pusong/common/encrypt/core/encryptor/RsaEncryptor.java b/pusong-common/pusong-common-encrypt/src/main/java/com/pusong/common/encrypt/core/encryptor/RsaEncryptor.java
new file mode 100644
index 0000000..25f0daa
--- /dev/null
+++ b/pusong-common/pusong-common-encrypt/src/main/java/com/pusong/common/encrypt/core/encryptor/RsaEncryptor.java
@@ -0,0 +1,62 @@
+package com.pusong.common.encrypt.core.encryptor;
+
+import com.pusong.common.core.utils.StringUtils;
+import com.pusong.common.encrypt.enumd.AlgorithmType;
+import com.pusong.common.encrypt.enumd.EncodeType;
+import com.pusong.common.encrypt.utils.EncryptUtils;
+import com.pusong.common.encrypt.core.EncryptContext;
+
+
+/**
+ * RSA算法实现
+ *
+ * @author 老马
+ * @version 4.6.0
+ */
+public class RsaEncryptor extends AbstractEncryptor {
+
+ private final EncryptContext context;
+
+ public RsaEncryptor(EncryptContext context) {
+ super(context);
+ String privateKey = context.getPrivateKey();
+ String publicKey = context.getPublicKey();
+ if (StringUtils.isAnyEmpty(privateKey, publicKey)) {
+ throw new IllegalArgumentException("RSA公私钥均需要提供,公钥加密,私钥解密。");
+ }
+ this.context = context;
+ }
+
+ /**
+ * 获得当前算法
+ */
+ @Override
+ public AlgorithmType algorithm() {
+ return AlgorithmType.RSA;
+ }
+
+ /**
+ * 加密
+ *
+ * @param value 待加密字符串
+ * @param encodeType 加密后的编码格式
+ */
+ @Override
+ public String encrypt(String value, EncodeType encodeType) {
+ if (encodeType == EncodeType.HEX) {
+ return EncryptUtils.encryptByRsaHex(value, context.getPublicKey());
+ } else {
+ return EncryptUtils.encryptByRsa(value, context.getPublicKey());
+ }
+ }
+
+ /**
+ * 解密
+ *
+ * @param value 待加密字符串
+ */
+ @Override
+ public String decrypt(String value) {
+ return EncryptUtils.decryptByRsa(value, context.getPrivateKey());
+ }
+}
diff --git a/pusong-common/pusong-common-encrypt/src/main/java/com/pusong/common/encrypt/core/encryptor/Sm2Encryptor.java b/pusong-common/pusong-common-encrypt/src/main/java/com/pusong/common/encrypt/core/encryptor/Sm2Encryptor.java
new file mode 100644
index 0000000..f21cbb3
--- /dev/null
+++ b/pusong-common/pusong-common-encrypt/src/main/java/com/pusong/common/encrypt/core/encryptor/Sm2Encryptor.java
@@ -0,0 +1,61 @@
+package com.pusong.common.encrypt.core.encryptor;
+
+import com.pusong.common.core.utils.StringUtils;
+import com.pusong.common.encrypt.enumd.AlgorithmType;
+import com.pusong.common.encrypt.enumd.EncodeType;
+import com.pusong.common.encrypt.utils.EncryptUtils;
+import com.pusong.common.encrypt.core.EncryptContext;
+
+/**
+ * sm2算法实现
+ *
+ * @author 老马
+ * @version 4.6.0
+ */
+public class Sm2Encryptor extends AbstractEncryptor {
+
+ private final EncryptContext context;
+
+ public Sm2Encryptor(EncryptContext context) {
+ super(context);
+ String privateKey = context.getPrivateKey();
+ String publicKey = context.getPublicKey();
+ if (StringUtils.isAnyEmpty(privateKey, publicKey)) {
+ throw new IllegalArgumentException("SM2公私钥均需要提供,公钥加密,私钥解密。");
+ }
+ this.context = context;
+ }
+
+ /**
+ * 获得当前算法
+ */
+ @Override
+ public AlgorithmType algorithm() {
+ return AlgorithmType.SM2;
+ }
+
+ /**
+ * 加密
+ *
+ * @param value 待加密字符串
+ * @param encodeType 加密后的编码格式
+ */
+ @Override
+ public String encrypt(String value, EncodeType encodeType) {
+ if (encodeType == EncodeType.HEX) {
+ return EncryptUtils.encryptBySm2Hex(value, context.getPublicKey());
+ } else {
+ return EncryptUtils.encryptBySm2(value, context.getPublicKey());
+ }
+ }
+
+ /**
+ * 解密
+ *
+ * @param value 待加密字符串
+ */
+ @Override
+ public String decrypt(String value) {
+ return EncryptUtils.decryptBySm2(value, context.getPrivateKey());
+ }
+}
diff --git a/pusong-common/pusong-common-encrypt/src/main/java/com/pusong/common/encrypt/core/encryptor/Sm4Encryptor.java b/pusong-common/pusong-common-encrypt/src/main/java/com/pusong/common/encrypt/core/encryptor/Sm4Encryptor.java
new file mode 100644
index 0000000..82064bc
--- /dev/null
+++ b/pusong-common/pusong-common-encrypt/src/main/java/com/pusong/common/encrypt/core/encryptor/Sm4Encryptor.java
@@ -0,0 +1,55 @@
+package com.pusong.common.encrypt.core.encryptor;
+
+import com.pusong.common.encrypt.enumd.AlgorithmType;
+import com.pusong.common.encrypt.enumd.EncodeType;
+import com.pusong.common.encrypt.utils.EncryptUtils;
+import com.pusong.common.encrypt.core.EncryptContext;
+
+/**
+ * sm4算法实现
+ *
+ * @author 老马
+ * @version 4.6.0
+ */
+public class Sm4Encryptor extends AbstractEncryptor {
+
+ private final EncryptContext context;
+
+ public Sm4Encryptor(EncryptContext context) {
+ super(context);
+ this.context = context;
+ }
+
+ /**
+ * 获得当前算法
+ */
+ @Override
+ public AlgorithmType algorithm() {
+ return AlgorithmType.SM4;
+ }
+
+ /**
+ * 加密
+ *
+ * @param value 待加密字符串
+ * @param encodeType 加密后的编码格式
+ */
+ @Override
+ public String encrypt(String value, EncodeType encodeType) {
+ if (encodeType == EncodeType.HEX) {
+ return EncryptUtils.encryptBySm4Hex(value, context.getPassword());
+ } else {
+ return EncryptUtils.encryptBySm4(value, context.getPassword());
+ }
+ }
+
+ /**
+ * 解密
+ *
+ * @param value 待加密字符串
+ */
+ @Override
+ public String decrypt(String value) {
+ return EncryptUtils.decryptBySm4(value, context.getPassword());
+ }
+}
diff --git a/pusong-common/pusong-common-encrypt/src/main/java/com/pusong/common/encrypt/enumd/AlgorithmType.java b/pusong-common/pusong-common-encrypt/src/main/java/com/pusong/common/encrypt/enumd/AlgorithmType.java
new file mode 100644
index 0000000..e786368
--- /dev/null
+++ b/pusong-common/pusong-common-encrypt/src/main/java/com/pusong/common/encrypt/enumd/AlgorithmType.java
@@ -0,0 +1,49 @@
+package com.pusong.common.encrypt.enumd;
+
+import com.pusong.common.encrypt.core.encryptor.*;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import com.pusong.common.encrypt.core.encryptor.*;
+
+/**
+ * 算法名称
+ *
+ * @author 老马
+ * @version 4.6.0
+ */
+@Getter
+@AllArgsConstructor
+public enum AlgorithmType {
+
+ /**
+ * 默认走yml配置
+ */
+ DEFAULT(null),
+
+ /**
+ * base64
+ */
+ BASE64(Base64Encryptor.class),
+
+ /**
+ * aes
+ */
+ AES(AesEncryptor.class),
+
+ /**
+ * rsa
+ */
+ RSA(RsaEncryptor.class),
+
+ /**
+ * sm2
+ */
+ SM2(Sm2Encryptor.class),
+
+ /**
+ * sm4
+ */
+ SM4(Sm4Encryptor.class);
+
+ private final Class extends AbstractEncryptor> clazz;
+}
diff --git a/pusong-common/pusong-common-encrypt/src/main/java/com/pusong/common/encrypt/enumd/EncodeType.java b/pusong-common/pusong-common-encrypt/src/main/java/com/pusong/common/encrypt/enumd/EncodeType.java
new file mode 100644
index 0000000..f7fa027
--- /dev/null
+++ b/pusong-common/pusong-common-encrypt/src/main/java/com/pusong/common/encrypt/enumd/EncodeType.java
@@ -0,0 +1,26 @@
+package com.pusong.common.encrypt.enumd;
+
+/**
+ * 编码类型
+ *
+ * @author 老马
+ * @version 4.6.0
+ */
+public enum EncodeType {
+
+ /**
+ * 默认使用yml配置
+ */
+ DEFAULT,
+
+ /**
+ * base64编码
+ */
+ BASE64,
+
+ /**
+ * 16进制编码
+ */
+ HEX;
+
+}
diff --git a/pusong-common/pusong-common-encrypt/src/main/java/com/pusong/common/encrypt/filter/CryptoFilter.java b/pusong-common/pusong-common-encrypt/src/main/java/com/pusong/common/encrypt/filter/CryptoFilter.java
new file mode 100644
index 0000000..2894cc6
--- /dev/null
+++ b/pusong-common/pusong-common-encrypt/src/main/java/com/pusong/common/encrypt/filter/CryptoFilter.java
@@ -0,0 +1,110 @@
+package com.pusong.common.encrypt.filter;
+
+import cn.hutool.core.util.ObjectUtil;
+import jakarta.servlet.*;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import com.pusong.common.core.constant.HttpStatus;
+import com.pusong.common.core.exception.ServiceException;
+import com.pusong.common.core.utils.SpringUtils;
+import com.pusong.common.core.utils.StringUtils;
+import com.pusong.common.encrypt.annotation.ApiEncrypt;
+import com.pusong.common.encrypt.properties.ApiDecryptProperties;
+import org.springframework.http.HttpMethod;
+import org.springframework.web.method.HandlerMethod;
+import org.springframework.web.servlet.HandlerExceptionResolver;
+import org.springframework.web.servlet.HandlerExecutionChain;
+import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
+
+import java.io.IOException;
+
+
+/**
+ * Crypto 过滤器
+ *
+ * @author wdhcr
+ */
+public class CryptoFilter implements Filter {
+ private final ApiDecryptProperties properties;
+
+ public CryptoFilter(ApiDecryptProperties properties) {
+ this.properties = properties;
+ }
+
+ @Override
+ public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
+ HttpServletRequest servletRequest = (HttpServletRequest) request;
+ HttpServletResponse servletResponse = (HttpServletResponse) response;
+ // 获取加密注解
+ ApiEncrypt apiEncrypt = this.getApiEncryptAnnotation(servletRequest);
+ boolean responseFlag = apiEncrypt != null && apiEncrypt.response();
+ ServletRequest requestWrapper = null;
+ ServletResponse responseWrapper = null;
+ EncryptResponseBodyWrapper responseBodyWrapper = null;
+
+ // 是否为 put 或者 post 请求
+ if (HttpMethod.PUT.matches(servletRequest.getMethod()) || HttpMethod.POST.matches(servletRequest.getMethod())) {
+ // 是否存在加密标头
+ String headerValue = servletRequest.getHeader(properties.getHeaderFlag());
+ if (StringUtils.isNotBlank(headerValue)) {
+ // 请求解密
+ requestWrapper = new DecryptRequestBodyWrapper(servletRequest, properties.getPrivateKey(), properties.getHeaderFlag());
+ } else {
+ // 是否有注解,有就报错,没有放行
+ if (ObjectUtil.isNotNull(apiEncrypt)) {
+ HandlerExceptionResolver exceptionResolver = SpringUtils.getBean("handlerExceptionResolver", HandlerExceptionResolver.class);
+ exceptionResolver.resolveException(
+ servletRequest, servletResponse, null,
+ new ServiceException("没有访问权限,请联系管理员授权", HttpStatus.FORBIDDEN));
+ return;
+ }
+ }
+ }
+
+ // 判断是否响应加密
+ if (responseFlag) {
+ responseBodyWrapper = new EncryptResponseBodyWrapper(servletResponse);
+ responseWrapper = responseBodyWrapper;
+ }
+
+ chain.doFilter(
+ ObjectUtil.defaultIfNull(requestWrapper, request),
+ ObjectUtil.defaultIfNull(responseWrapper, response));
+
+ if (responseFlag) {
+ servletResponse.reset();
+ // 对原始内容加密
+ String encryptContent = responseBodyWrapper.getEncryptContent(
+ servletResponse, properties.getPublicKey(), properties.getHeaderFlag());
+ // 对加密后的内容写出
+ servletResponse.getWriter().write(encryptContent);
+ }
+ }
+
+ /**
+ * 获取 ApiEncrypt 注解
+ */
+ private ApiEncrypt getApiEncryptAnnotation(HttpServletRequest servletRequest) {
+ RequestMappingHandlerMapping handlerMapping = SpringUtils.getBean("requestMappingHandlerMapping", RequestMappingHandlerMapping.class);
+ // 获取注解
+ try {
+ HandlerExecutionChain mappingHandler = handlerMapping.getHandler(servletRequest);
+ if (ObjectUtil.isNotNull(mappingHandler)) {
+ Object handler = mappingHandler.getHandler();
+ if (ObjectUtil.isNotNull(handler)) {
+ // 从handler获取注解
+ if (handler instanceof HandlerMethod handlerMethod) {
+ return handlerMethod.getMethodAnnotation(ApiEncrypt.class);
+ }
+ }
+ }
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ return null;
+ }
+
+ @Override
+ public void destroy() {
+ }
+}
diff --git a/pusong-common/pusong-common-encrypt/src/main/java/com/pusong/common/encrypt/filter/DecryptRequestBodyWrapper.java b/pusong-common/pusong-common-encrypt/src/main/java/com/pusong/common/encrypt/filter/DecryptRequestBodyWrapper.java
new file mode 100644
index 0000000..f85d615
--- /dev/null
+++ b/pusong-common/pusong-common-encrypt/src/main/java/com/pusong/common/encrypt/filter/DecryptRequestBodyWrapper.java
@@ -0,0 +1,94 @@
+package com.pusong.common.encrypt.filter;
+
+import cn.hutool.core.io.IoUtil;
+import com.pusong.common.encrypt.utils.EncryptUtils;
+import jakarta.servlet.ReadListener;
+import jakarta.servlet.ServletInputStream;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletRequestWrapper;
+import com.pusong.common.core.constant.Constants;
+import org.springframework.http.MediaType;
+
+import java.io.BufferedReader;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
+
+/**
+ * 解密请求参数工具类
+ *
+ * @author wdhcr
+ */
+public class DecryptRequestBodyWrapper extends HttpServletRequestWrapper {
+
+ private final byte[] body;
+
+ public DecryptRequestBodyWrapper(HttpServletRequest request, String privateKey, String headerFlag) throws IOException {
+ super(request);
+ // 获取 AES 密码 采用 RSA 加密
+ String headerRsa = request.getHeader(headerFlag);
+ String decryptAes = EncryptUtils.decryptByRsa(headerRsa, privateKey);
+ // 解密 AES 密码
+ String aesPassword = EncryptUtils.decryptByBase64(decryptAes);
+ request.setCharacterEncoding(Constants.UTF8);
+ byte[] readBytes = IoUtil.readBytes(request.getInputStream(), false);
+ String requestBody = new String(readBytes, StandardCharsets.UTF_8);
+ // 解密 body 采用 AES 加密
+ String decryptBody = EncryptUtils.decryptByAes(requestBody, aesPassword);
+ body = decryptBody.getBytes(StandardCharsets.UTF_8);
+ }
+
+ @Override
+ public BufferedReader getReader() {
+ return new BufferedReader(new InputStreamReader(getInputStream()));
+ }
+
+
+ @Override
+ public int getContentLength() {
+ return body.length;
+ }
+
+ @Override
+ public long getContentLengthLong() {
+ return body.length;
+ }
+
+ @Override
+ public String getContentType() {
+ return MediaType.APPLICATION_JSON_VALUE;
+ }
+
+
+ @Override
+ public ServletInputStream getInputStream() {
+ final ByteArrayInputStream bais = new ByteArrayInputStream(body);
+ return new ServletInputStream() {
+ @Override
+ public int read() {
+ return bais.read();
+ }
+
+ @Override
+ public int available() {
+ return body.length;
+ }
+
+ @Override
+ public boolean isFinished() {
+ return false;
+ }
+
+ @Override
+ public boolean isReady() {
+ return false;
+ }
+
+ @Override
+ public void setReadListener(ReadListener readListener) {
+
+ }
+ };
+ }
+}
diff --git a/pusong-common/pusong-common-encrypt/src/main/java/com/pusong/common/encrypt/filter/EncryptResponseBodyWrapper.java b/pusong-common/pusong-common-encrypt/src/main/java/com/pusong/common/encrypt/filter/EncryptResponseBodyWrapper.java
new file mode 100644
index 0000000..3e1c203
--- /dev/null
+++ b/pusong-common/pusong-common-encrypt/src/main/java/com/pusong/common/encrypt/filter/EncryptResponseBodyWrapper.java
@@ -0,0 +1,121 @@
+package com.pusong.common.encrypt.filter;
+
+import cn.hutool.core.util.RandomUtil;
+import com.pusong.common.encrypt.utils.EncryptUtils;
+import jakarta.servlet.ServletOutputStream;
+import jakarta.servlet.WriteListener;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpServletResponseWrapper;
+
+import java.io.*;
+import java.nio.charset.StandardCharsets;
+
+/**
+ * 加密响应参数包装类
+ *
+ * @author Michelle.Chung
+ */
+public class EncryptResponseBodyWrapper extends HttpServletResponseWrapper {
+
+ private final ByteArrayOutputStream byteArrayOutputStream;
+ private final ServletOutputStream servletOutputStream;
+ private final PrintWriter printWriter;
+
+ public EncryptResponseBodyWrapper(HttpServletResponse response) throws IOException {
+ super(response);
+ this.byteArrayOutputStream = new ByteArrayOutputStream();
+ this.servletOutputStream = this.getOutputStream();
+ this.printWriter = new PrintWriter(new OutputStreamWriter(byteArrayOutputStream));
+ }
+
+ @Override
+ public PrintWriter getWriter() {
+ return printWriter;
+ }
+
+ @Override
+ public void flushBuffer() throws IOException {
+ if (servletOutputStream != null) {
+ servletOutputStream.flush();
+ }
+ if (printWriter != null) {
+ printWriter.flush();
+ }
+ }
+
+ @Override
+ public void reset() {
+ byteArrayOutputStream.reset();
+ }
+
+ public byte[] getResponseData() throws IOException {
+ flushBuffer();
+ return byteArrayOutputStream.toByteArray();
+ }
+
+ public String getContent() throws IOException {
+ flushBuffer();
+ return byteArrayOutputStream.toString();
+ }
+
+ /**
+ * 获取加密内容
+ *
+ * @param servletResponse response
+ * @param publicKey RSA公钥 (用于加密 AES 秘钥)
+ * @param headerFlag 请求头标志
+ * @return 加密内容
+ * @throws IOException
+ */
+ public String getEncryptContent(HttpServletResponse servletResponse, String publicKey, String headerFlag) throws IOException {
+ // 生成秘钥
+ String aesPassword = RandomUtil.randomString(32);
+ // 秘钥使用 Base64 编码
+ String encryptAes = EncryptUtils.encryptByBase64(aesPassword);
+ // Rsa 公钥加密 Base64 编码
+ String encryptPassword = EncryptUtils.encryptByRsa(encryptAes, publicKey);
+
+ // 设置响应头
+ servletResponse.addHeader("Access-Control-Expose-Headers", headerFlag);
+ servletResponse.setHeader(headerFlag, encryptPassword);
+ servletResponse.setHeader("Access-Control-Allow-Origin", "*");
+ servletResponse.setHeader("Access-Control-Allow-Methods", "*");
+ servletResponse.setCharacterEncoding(StandardCharsets.UTF_8.toString());
+
+ // 获取原始内容
+ String originalBody = this.getContent();
+ // 对内容进行加密
+ return EncryptUtils.encryptByAes(originalBody, aesPassword);
+ }
+
+ @Override
+ public ServletOutputStream getOutputStream() throws IOException {
+ return new ServletOutputStream() {
+ @Override
+ public boolean isReady() {
+ return false;
+ }
+
+ @Override
+ public void setWriteListener(WriteListener writeListener) {
+
+ }
+
+ @Override
+ public void write(int b) throws IOException {
+ byteArrayOutputStream.write(b);
+ }
+
+ @Override
+ public void write(byte[] b) throws IOException {
+ byteArrayOutputStream.write(b);
+ }
+
+ @Override
+ public void write(byte[] b, int off, int len) throws IOException {
+ byteArrayOutputStream.write(b, off, len);
+ }
+ };
+ }
+
+}
diff --git a/pusong-common/pusong-common-encrypt/src/main/java/com/pusong/common/encrypt/interceptor/MybatisDecryptInterceptor.java b/pusong-common/pusong-common-encrypt/src/main/java/com/pusong/common/encrypt/interceptor/MybatisDecryptInterceptor.java
new file mode 100644
index 0000000..7efd00a
--- /dev/null
+++ b/pusong-common/pusong-common-encrypt/src/main/java/com/pusong/common/encrypt/interceptor/MybatisDecryptInterceptor.java
@@ -0,0 +1,120 @@
+package com.pusong.common.encrypt.interceptor;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.convert.Convert;
+import cn.hutool.core.util.ObjectUtil;
+import com.pusong.common.encrypt.annotation.EncryptField;
+import com.pusong.common.encrypt.core.EncryptContext;
+import com.pusong.common.encrypt.core.EncryptorManager;
+import com.pusong.common.encrypt.enumd.AlgorithmType;
+import com.pusong.common.encrypt.enumd.EncodeType;
+import com.pusong.common.encrypt.properties.EncryptorProperties;
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.ibatis.executor.resultset.ResultSetHandler;
+import org.apache.ibatis.plugin.*;
+import com.pusong.common.core.utils.StringUtils;
+
+import java.lang.reflect.Field;
+import java.sql.Statement;
+import java.util.*;
+
+/**
+ * 出参解密拦截器
+ *
+ * @author 老马
+ * @version 4.6.0
+ */
+@Slf4j
+@Intercepts({@Signature(
+ type = ResultSetHandler.class,
+ method = "handleResultSets",
+ args = {Statement.class})
+})
+@AllArgsConstructor
+public class MybatisDecryptInterceptor implements Interceptor {
+
+ private final EncryptorManager encryptorManager;
+ private final EncryptorProperties defaultProperties;
+
+ @Override
+ public Object intercept(Invocation invocation) throws Throwable {
+ // 获取执行mysql执行结果
+ Object result = invocation.proceed();
+ if (result == null) {
+ return null;
+ }
+ decryptHandler(result);
+ return result;
+ }
+
+ /**
+ * 解密对象
+ *
+ * @param sourceObject 待加密对象
+ */
+ private void decryptHandler(Object sourceObject) {
+ if (ObjectUtil.isNull(sourceObject)) {
+ return;
+ }
+ if (sourceObject instanceof Map, ?> map) {
+ new HashSet<>(map.values()).forEach(this::decryptHandler);
+ return;
+ }
+ if (sourceObject instanceof List> list) {
+ if(CollUtil.isEmpty(list)) {
+ return;
+ }
+ // 判断第一个元素是否含有注解。如果没有直接返回,提高效率
+ Object firstItem = list.get(0);
+ if (ObjectUtil.isNull(firstItem) || CollUtil.isEmpty(encryptorManager.getFieldCache(firstItem.getClass()))) {
+ return;
+ }
+ list.forEach(this::decryptHandler);
+ return;
+ }
+ // 不在缓存中的类,就是没有加密注解的类(当然也有可能是typeAliasesPackage写错)
+ Set fields = encryptorManager.getFieldCache(sourceObject.getClass());
+ if(ObjectUtil.isNull(fields)){
+ return;
+ }
+ try {
+ for (Field field : fields) {
+ field.set(sourceObject, this.decryptField(Convert.toStr(field.get(sourceObject)), field));
+ }
+ } catch (Exception e) {
+ log.error("处理解密字段时出错", e);
+ }
+ }
+
+ /**
+ * 字段值进行加密。通过字段的批注注册新的加密算法
+ *
+ * @param value 待加密的值
+ * @param field 待加密字段
+ * @return 加密后结果
+ */
+ private String decryptField(String value, Field field) {
+ if (ObjectUtil.isNull(value)) {
+ return null;
+ }
+ EncryptField encryptField = field.getAnnotation(EncryptField.class);
+ EncryptContext encryptContext = new EncryptContext();
+ encryptContext.setAlgorithm(encryptField.algorithm() == AlgorithmType.DEFAULT ? defaultProperties.getAlgorithm() : encryptField.algorithm());
+ encryptContext.setEncode(encryptField.encode() == EncodeType.DEFAULT ? defaultProperties.getEncode() : encryptField.encode());
+ encryptContext.setPassword(StringUtils.isBlank(encryptField.password()) ? defaultProperties.getPassword() : encryptField.password());
+ encryptContext.setPrivateKey(StringUtils.isBlank(encryptField.privateKey()) ? defaultProperties.getPrivateKey() : encryptField.privateKey());
+ encryptContext.setPublicKey(StringUtils.isBlank(encryptField.publicKey()) ? defaultProperties.getPublicKey() : encryptField.publicKey());
+ return this.encryptorManager.decrypt(value, encryptContext);
+ }
+
+ @Override
+ public Object plugin(Object target) {
+ return Plugin.wrap(target, this);
+ }
+
+ @Override
+ public void setProperties(Properties properties) {
+
+ }
+}
diff --git a/pusong-common/pusong-common-encrypt/src/main/java/com/pusong/common/encrypt/interceptor/MybatisEncryptInterceptor.java b/pusong-common/pusong-common-encrypt/src/main/java/com/pusong/common/encrypt/interceptor/MybatisEncryptInterceptor.java
new file mode 100644
index 0000000..bd76b5b
--- /dev/null
+++ b/pusong-common/pusong-common-encrypt/src/main/java/com/pusong/common/encrypt/interceptor/MybatisEncryptInterceptor.java
@@ -0,0 +1,124 @@
+package com.pusong.common.encrypt.interceptor;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.convert.Convert;
+import cn.hutool.core.util.ObjectUtil;
+import com.pusong.common.encrypt.annotation.EncryptField;
+import com.pusong.common.encrypt.core.EncryptContext;
+import com.pusong.common.encrypt.core.EncryptorManager;
+import com.pusong.common.encrypt.enumd.AlgorithmType;
+import com.pusong.common.encrypt.enumd.EncodeType;
+import com.pusong.common.encrypt.properties.EncryptorProperties;
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.ibatis.executor.parameter.ParameterHandler;
+import org.apache.ibatis.plugin.Interceptor;
+import org.apache.ibatis.plugin.Intercepts;
+import org.apache.ibatis.plugin.Invocation;
+import org.apache.ibatis.plugin.Signature;
+import com.pusong.common.core.utils.StringUtils;
+
+import java.lang.reflect.Field;
+import java.sql.PreparedStatement;
+import java.util.*;
+
+/**
+ * 入参加密拦截器
+ *
+ * @author 老马
+ * @version 4.6.0
+ */
+@Slf4j
+@Intercepts({@Signature(
+ type = ParameterHandler.class,
+ method = "setParameters",
+ args = {PreparedStatement.class})
+})
+@AllArgsConstructor
+public class MybatisEncryptInterceptor implements Interceptor {
+
+ private final EncryptorManager encryptorManager;
+ private final EncryptorProperties defaultProperties;
+
+ @Override
+ public Object intercept(Invocation invocation) throws Throwable {
+ return invocation;
+ }
+
+ @Override
+ public Object plugin(Object target) {
+ if (target instanceof ParameterHandler parameterHandler) {
+ // 进行加密操作
+ Object parameterObject = parameterHandler.getParameterObject();
+ if (ObjectUtil.isNotNull(parameterObject) && !(parameterObject instanceof String)) {
+ this.encryptHandler(parameterObject);
+ }
+ }
+ return target;
+ }
+
+ /**
+ * 加密对象
+ *
+ * @param sourceObject 待加密对象
+ */
+ private void encryptHandler(Object sourceObject) {
+ if (ObjectUtil.isNull(sourceObject)) {
+ return;
+ }
+ if (sourceObject instanceof Map, ?> map) {
+ new HashSet<>(map.values()).forEach(this::encryptHandler);
+ return;
+ }
+ if (sourceObject instanceof List> list) {
+ if(CollUtil.isEmpty(list)) {
+ return;
+ }
+ // 判断第一个元素是否含有注解。如果没有直接返回,提高效率
+ Object firstItem = list.get(0);
+ if (ObjectUtil.isNull(firstItem) || CollUtil.isEmpty(encryptorManager.getFieldCache(firstItem.getClass()))) {
+ return;
+ }
+ list.forEach(this::encryptHandler);
+ return;
+ }
+ // 不在缓存中的类,就是没有加密注解的类(当然也有可能是typeAliasesPackage写错)
+ Set fields = encryptorManager.getFieldCache(sourceObject.getClass());
+ if(ObjectUtil.isNull(fields)){
+ return;
+ }
+ try {
+ for (Field field : fields) {
+ field.set(sourceObject, this.encryptField(Convert.toStr(field.get(sourceObject)), field));
+ }
+ } catch (Exception e) {
+ log.error("处理加密字段时出错", e);
+ }
+ }
+
+ /**
+ * 字段值进行加密。通过字段的批注注册新的加密算法
+ *
+ * @param value 待加密的值
+ * @param field 待加密字段
+ * @return 加密后结果
+ */
+ private String encryptField(String value, Field field) {
+ if (ObjectUtil.isNull(value)) {
+ return null;
+ }
+ EncryptField encryptField = field.getAnnotation(EncryptField.class);
+ EncryptContext encryptContext = new EncryptContext();
+ encryptContext.setAlgorithm(encryptField.algorithm() == AlgorithmType.DEFAULT ? defaultProperties.getAlgorithm() : encryptField.algorithm());
+ encryptContext.setEncode(encryptField.encode() == EncodeType.DEFAULT ? defaultProperties.getEncode() : encryptField.encode());
+ encryptContext.setPassword(StringUtils.isBlank(encryptField.password()) ? defaultProperties.getPassword() : encryptField.password());
+ encryptContext.setPrivateKey(StringUtils.isBlank(encryptField.privateKey()) ? defaultProperties.getPrivateKey() : encryptField.privateKey());
+ encryptContext.setPublicKey(StringUtils.isBlank(encryptField.publicKey()) ? defaultProperties.getPublicKey() : encryptField.publicKey());
+ return this.encryptorManager.encrypt(value, encryptContext);
+ }
+
+
+ @Override
+ public void setProperties(Properties properties) {
+ }
+}
diff --git a/pusong-common/pusong-common-encrypt/src/main/java/com/pusong/common/encrypt/properties/ApiDecryptProperties.java b/pusong-common/pusong-common-encrypt/src/main/java/com/pusong/common/encrypt/properties/ApiDecryptProperties.java
new file mode 100644
index 0000000..a769827
--- /dev/null
+++ b/pusong-common/pusong-common-encrypt/src/main/java/com/pusong/common/encrypt/properties/ApiDecryptProperties.java
@@ -0,0 +1,34 @@
+package com.pusong.common.encrypt.properties;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+/**
+ * api解密属性配置类
+ * @author wdhcr
+ */
+@Data
+@ConfigurationProperties(prefix = "api-decrypt")
+public class ApiDecryptProperties {
+
+ /**
+ * 加密开关
+ */
+ private Boolean enabled;
+
+ /**
+ * 头部标识
+ */
+ private String headerFlag;
+
+ /**
+ * 响应加密公钥
+ */
+ private String publicKey;
+
+ /**
+ * 请求解密私钥
+ */
+ private String privateKey;
+
+}
diff --git a/pusong-common/pusong-common-encrypt/src/main/java/com/pusong/common/encrypt/properties/EncryptorProperties.java b/pusong-common/pusong-common-encrypt/src/main/java/com/pusong/common/encrypt/properties/EncryptorProperties.java
new file mode 100644
index 0000000..38d2fdf
--- /dev/null
+++ b/pusong-common/pusong-common-encrypt/src/main/java/com/pusong/common/encrypt/properties/EncryptorProperties.java
@@ -0,0 +1,48 @@
+package com.pusong.common.encrypt.properties;
+
+import com.pusong.common.encrypt.enumd.AlgorithmType;
+import com.pusong.common.encrypt.enumd.EncodeType;
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+/**
+ * 加解密属性配置类
+ *
+ * @author 老马
+ * @version 4.6.0
+ */
+@Data
+@ConfigurationProperties(prefix = "mybatis-encryptor")
+public class EncryptorProperties {
+
+ /**
+ * 过滤开关
+ */
+ private Boolean enable;
+
+ /**
+ * 默认算法
+ */
+ private AlgorithmType algorithm;
+
+ /**
+ * 安全秘钥
+ */
+ private String password;
+
+ /**
+ * 公钥
+ */
+ private String publicKey;
+
+ /**
+ * 私钥
+ */
+ private String privateKey;
+
+ /**
+ * 编码方式,base64/hex
+ */
+ private EncodeType encode;
+
+}
diff --git a/pusong-common/pusong-common-encrypt/src/main/java/com/pusong/common/encrypt/utils/EncryptUtils.java b/pusong-common/pusong-common-encrypt/src/main/java/com/pusong/common/encrypt/utils/EncryptUtils.java
new file mode 100644
index 0000000..d36ca03
--- /dev/null
+++ b/pusong-common/pusong-common-encrypt/src/main/java/com/pusong/common/encrypt/utils/EncryptUtils.java
@@ -0,0 +1,311 @@
+package com.pusong.common.encrypt.utils;
+
+import cn.hutool.core.codec.Base64;
+import cn.hutool.core.util.ArrayUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.crypto.SecureUtil;
+import cn.hutool.crypto.SmUtil;
+import cn.hutool.crypto.asymmetric.KeyType;
+import cn.hutool.crypto.asymmetric.RSA;
+import cn.hutool.crypto.asymmetric.SM2;
+
+import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 安全相关工具类
+ *
+ * @author 老马
+ */
+public class EncryptUtils {
+ /**
+ * 公钥
+ */
+ public static final String PUBLIC_KEY = "publicKey";
+ /**
+ * 私钥
+ */
+ public static final String PRIVATE_KEY = "privateKey";
+
+ /**
+ * Base64加密
+ *
+ * @param data 待加密数据
+ * @return 加密后字符串
+ */
+ public static String encryptByBase64(String data) {
+ return Base64.encode(data, StandardCharsets.UTF_8);
+ }
+
+ /**
+ * Base64解密
+ *
+ * @param data 待解密数据
+ * @return 解密后字符串
+ */
+ public static String decryptByBase64(String data) {
+ return Base64.decodeStr(data, StandardCharsets.UTF_8);
+ }
+
+ /**
+ * AES加密
+ *
+ * @param data 待解密数据
+ * @param password 秘钥字符串
+ * @return 加密后字符串, 采用Base64编码
+ */
+ public static String encryptByAes(String data, String password) {
+ if (StrUtil.isBlank(password)) {
+ throw new IllegalArgumentException("AES需要传入秘钥信息");
+ }
+ // aes算法的秘钥要求是16位、24位、32位
+ int[] array = {16, 24, 32};
+ if (!ArrayUtil.contains(array, password.length())) {
+ throw new IllegalArgumentException("AES秘钥长度要求为16位、24位、32位");
+ }
+ return SecureUtil.aes(password.getBytes(StandardCharsets.UTF_8)).encryptBase64(data, StandardCharsets.UTF_8);
+ }
+
+ /**
+ * AES加密
+ *
+ * @param data 待解密数据
+ * @param password 秘钥字符串
+ * @return 加密后字符串, 采用Hex编码
+ */
+ public static String encryptByAesHex(String data, String password) {
+ if (StrUtil.isBlank(password)) {
+ throw new IllegalArgumentException("AES需要传入秘钥信息");
+ }
+ // aes算法的秘钥要求是16位、24位、32位
+ int[] array = {16, 24, 32};
+ if (!ArrayUtil.contains(array, password.length())) {
+ throw new IllegalArgumentException("AES秘钥长度要求为16位、24位、32位");
+ }
+ return SecureUtil.aes(password.getBytes(StandardCharsets.UTF_8)).encryptHex(data, StandardCharsets.UTF_8);
+ }
+
+ /**
+ * AES解密
+ *
+ * @param data 待解密数据
+ * @param password 秘钥字符串
+ * @return 解密后字符串
+ */
+ public static String decryptByAes(String data, String password) {
+ if (StrUtil.isBlank(password)) {
+ throw new IllegalArgumentException("AES需要传入秘钥信息");
+ }
+ // aes算法的秘钥要求是16位、24位、32位
+ int[] array = {16, 24, 32};
+ if (!ArrayUtil.contains(array, password.length())) {
+ throw new IllegalArgumentException("AES秘钥长度要求为16位、24位、32位");
+ }
+ return SecureUtil.aes(password.getBytes(StandardCharsets.UTF_8)).decryptStr(data, StandardCharsets.UTF_8);
+ }
+
+ /**
+ * sm4加密
+ *
+ * @param data 待加密数据
+ * @param password 秘钥字符串
+ * @return 加密后字符串, 采用Base64编码
+ */
+ public static String encryptBySm4(String data, String password) {
+ if (StrUtil.isBlank(password)) {
+ throw new IllegalArgumentException("SM4需要传入秘钥信息");
+ }
+ // sm4算法的秘钥要求是16位长度
+ int sm4PasswordLength = 16;
+ if (sm4PasswordLength != password.length()) {
+ throw new IllegalArgumentException("SM4秘钥长度要求为16位");
+ }
+ return SmUtil.sm4(password.getBytes(StandardCharsets.UTF_8)).encryptBase64(data, StandardCharsets.UTF_8);
+ }
+
+ /**
+ * sm4加密
+ *
+ * @param data 待加密数据
+ * @param password 秘钥字符串
+ * @return 加密后字符串, 采用Base64编码
+ */
+ public static String encryptBySm4Hex(String data, String password) {
+ if (StrUtil.isBlank(password)) {
+ throw new IllegalArgumentException("SM4需要传入秘钥信息");
+ }
+ // sm4算法的秘钥要求是16位长度
+ int sm4PasswordLength = 16;
+ if (sm4PasswordLength != password.length()) {
+ throw new IllegalArgumentException("SM4秘钥长度要求为16位");
+ }
+ return SmUtil.sm4(password.getBytes(StandardCharsets.UTF_8)).encryptHex(data, StandardCharsets.UTF_8);
+ }
+
+ /**
+ * sm4解密
+ *
+ * @param data 待解密数据
+ * @param password 秘钥字符串
+ * @return 解密后字符串
+ */
+ public static String decryptBySm4(String data, String password) {
+ if (StrUtil.isBlank(password)) {
+ throw new IllegalArgumentException("SM4需要传入秘钥信息");
+ }
+ // sm4算法的秘钥要求是16位长度
+ int sm4PasswordLength = 16;
+ if (sm4PasswordLength != password.length()) {
+ throw new IllegalArgumentException("SM4秘钥长度要求为16位");
+ }
+ return SmUtil.sm4(password.getBytes(StandardCharsets.UTF_8)).decryptStr(data, StandardCharsets.UTF_8);
+ }
+
+ /**
+ * 产生sm2加解密需要的公钥和私钥
+ *
+ * @return 公私钥Map
+ */
+ public static Map generateSm2Key() {
+ Map keyMap = new HashMap<>(2);
+ SM2 sm2 = SmUtil.sm2();
+ keyMap.put(PRIVATE_KEY, sm2.getPrivateKeyBase64());
+ keyMap.put(PUBLIC_KEY, sm2.getPublicKeyBase64());
+ return keyMap;
+ }
+
+ /**
+ * sm2公钥加密
+ *
+ * @param data 待加密数据
+ * @param publicKey 公钥
+ * @return 加密后字符串, 采用Base64编码
+ */
+ public static String encryptBySm2(String data, String publicKey) {
+ if (StrUtil.isBlank(publicKey)) {
+ throw new IllegalArgumentException("SM2需要传入公钥进行加密");
+ }
+ SM2 sm2 = SmUtil.sm2(null, publicKey);
+ return sm2.encryptBase64(data, StandardCharsets.UTF_8, KeyType.PublicKey);
+ }
+
+ /**
+ * sm2公钥加密
+ *
+ * @param data 待加密数据
+ * @param publicKey 公钥
+ * @return 加密后字符串, 采用Hex编码
+ */
+ public static String encryptBySm2Hex(String data, String publicKey) {
+ if (StrUtil.isBlank(publicKey)) {
+ throw new IllegalArgumentException("SM2需要传入公钥进行加密");
+ }
+ SM2 sm2 = SmUtil.sm2(null, publicKey);
+ return sm2.encryptHex(data, StandardCharsets.UTF_8, KeyType.PublicKey);
+ }
+
+ /**
+ * sm2私钥解密
+ *
+ * @param data 待加密数据
+ * @param privateKey 私钥
+ * @return 解密后字符串
+ */
+ public static String decryptBySm2(String data, String privateKey) {
+ if (StrUtil.isBlank(privateKey)) {
+ throw new IllegalArgumentException("SM2需要传入私钥进行解密");
+ }
+ SM2 sm2 = SmUtil.sm2(privateKey, null);
+ return sm2.decryptStr(data, KeyType.PrivateKey, StandardCharsets.UTF_8);
+ }
+
+ /**
+ * 产生RSA加解密需要的公钥和私钥
+ *
+ * @return 公私钥Map
+ */
+ public static Map generateRsaKey() {
+ Map keyMap = new HashMap<>(2);
+ RSA rsa = SecureUtil.rsa();
+ keyMap.put(PRIVATE_KEY, rsa.getPrivateKeyBase64());
+ keyMap.put(PUBLIC_KEY, rsa.getPublicKeyBase64());
+ return keyMap;
+ }
+
+ /**
+ * rsa公钥加密
+ *
+ * @param data 待加密数据
+ * @param publicKey 公钥
+ * @return 加密后字符串, 采用Base64编码
+ */
+ public static String encryptByRsa(String data, String publicKey) {
+ if (StrUtil.isBlank(publicKey)) {
+ throw new IllegalArgumentException("RSA需要传入公钥进行加密");
+ }
+ RSA rsa = SecureUtil.rsa(null, publicKey);
+ return rsa.encryptBase64(data, StandardCharsets.UTF_8, KeyType.PublicKey);
+ }
+
+ /**
+ * rsa公钥加密
+ *
+ * @param data 待加密数据
+ * @param publicKey 公钥
+ * @return 加密后字符串, 采用Hex编码
+ */
+ public static String encryptByRsaHex(String data, String publicKey) {
+ if (StrUtil.isBlank(publicKey)) {
+ throw new IllegalArgumentException("RSA需要传入公钥进行加密");
+ }
+ RSA rsa = SecureUtil.rsa(null, publicKey);
+ return rsa.encryptHex(data, StandardCharsets.UTF_8, KeyType.PublicKey);
+ }
+
+ /**
+ * rsa私钥解密
+ *
+ * @param data 待加密数据
+ * @param privateKey 私钥
+ * @return 解密后字符串
+ */
+ public static String decryptByRsa(String data, String privateKey) {
+ if (StrUtil.isBlank(privateKey)) {
+ throw new IllegalArgumentException("RSA需要传入私钥进行解密");
+ }
+ RSA rsa = SecureUtil.rsa(privateKey, null);
+ return rsa.decryptStr(data, KeyType.PrivateKey, StandardCharsets.UTF_8);
+ }
+
+ /**
+ * md5加密
+ *
+ * @param data 待加密数据
+ * @return 加密后字符串, 采用Hex编码
+ */
+ public static String encryptByMd5(String data) {
+ return SecureUtil.md5(data);
+ }
+
+ /**
+ * sha256加密
+ *
+ * @param data 待加密数据
+ * @return 加密后字符串, 采用Hex编码
+ */
+ public static String encryptBySha256(String data) {
+ return SecureUtil.sha256(data);
+ }
+
+ /**
+ * sm3加密
+ *
+ * @param data 待加密数据
+ * @return 加密后字符串, 采用Hex编码
+ */
+ public static String encryptBySm3(String data) {
+ return SmUtil.sm3(data);
+ }
+
+}
diff --git a/pusong-common/pusong-common-encrypt/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/pusong-common/pusong-common-encrypt/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 0000000..3e5cde9
--- /dev/null
+++ b/pusong-common/pusong-common-encrypt/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -0,0 +1,3 @@
+com.pusong.common.encrypt.config.EncryptorAutoConfiguration
+com.pusong.common.encrypt.config.ApiDecryptAutoConfiguration
+
diff --git a/pusong-common/pusong-common-excel/pom.xml b/pusong-common/pusong-common-excel/pom.xml
new file mode 100644
index 0000000..6bcf346
--- /dev/null
+++ b/pusong-common/pusong-common-excel/pom.xml
@@ -0,0 +1,30 @@
+
+
+
+ com.pusong
+ pusong-common
+ ${revision}
+
+ 4.0.0
+
+ pusong-common-excel
+
+
+ ruoyi-common-excel
+
+
+
+
+ com.pusong
+ pusong-common-json
+
+
+
+ com.alibaba
+ easyexcel
+
+
+
+
diff --git a/pusong-common/pusong-common-excel/src/main/java/com/pusong/common/excel/annotation/CellMerge.java b/pusong-common/pusong-common-excel/src/main/java/com/pusong/common/excel/annotation/CellMerge.java
new file mode 100644
index 0000000..6e710ca
--- /dev/null
+++ b/pusong-common/pusong-common-excel/src/main/java/com/pusong/common/excel/annotation/CellMerge.java
@@ -0,0 +1,29 @@
+package com.pusong.common.excel.annotation;
+
+import com.pusong.common.excel.core.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;
+
+ /**
+ * 合并需要依赖的其他字段名称
+ */
+ String[] mergeBy() default {};
+
+}
diff --git a/pusong-common/pusong-common-excel/src/main/java/com/pusong/common/excel/annotation/ExcelDictFormat.java b/pusong-common/pusong-common-excel/src/main/java/com/pusong/common/excel/annotation/ExcelDictFormat.java
new file mode 100644
index 0000000..70f0cd1
--- /dev/null
+++ b/pusong-common/pusong-common-excel/src/main/java/com/pusong/common/excel/annotation/ExcelDictFormat.java
@@ -0,0 +1,32 @@
+package com.pusong.common.excel.annotation;
+
+import com.pusong.common.core.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/pusong-common/pusong-common-excel/src/main/java/com/pusong/common/excel/annotation/ExcelEnumFormat.java b/pusong-common/pusong-common-excel/src/main/java/com/pusong/common/excel/annotation/ExcelEnumFormat.java
new file mode 100644
index 0000000..cab6224
--- /dev/null
+++ b/pusong-common/pusong-common-excel/src/main/java/com/pusong/common/excel/annotation/ExcelEnumFormat.java
@@ -0,0 +1,30 @@
+package com.pusong.common.excel.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/pusong-common/pusong-common-excel/src/main/java/com/pusong/common/excel/convert/ExcelBigNumberConvert.java b/pusong-common/pusong-common-excel/src/main/java/com/pusong/common/excel/convert/ExcelBigNumberConvert.java
new file mode 100644
index 0000000..9ce146d
--- /dev/null
+++ b/pusong-common/pusong-common-excel/src/main/java/com/pusong/common/excel/convert/ExcelBigNumberConvert.java
@@ -0,0 +1,52 @@
+package com.pusong.common.excel.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/pusong-common/pusong-common-excel/src/main/java/com/pusong/common/excel/convert/ExcelDictConvert.java b/pusong-common/pusong-common-excel/src/main/java/com/pusong/common/excel/convert/ExcelDictConvert.java
new file mode 100644
index 0000000..570454e
--- /dev/null
+++ b/pusong-common/pusong-common-excel/src/main/java/com/pusong/common/excel/convert/ExcelDictConvert.java
@@ -0,0 +1,73 @@
+package com.pusong.common.excel.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.pusong.common.excel.annotation.ExcelDictFormat;
+import com.pusong.common.core.service.DictService;
+import com.pusong.common.core.utils.SpringUtils;
+import com.pusong.common.core.utils.StringUtils;
+import com.pusong.common.excel.utils.ExcelUtil;
+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/pusong-common/pusong-common-excel/src/main/java/com/pusong/common/excel/convert/ExcelEnumConvert.java b/pusong-common/pusong-common-excel/src/main/java/com/pusong/common/excel/convert/ExcelEnumConvert.java
new file mode 100644
index 0000000..4c212f3
--- /dev/null
+++ b/pusong-common/pusong-common-excel/src/main/java/com/pusong/common/excel/convert/ExcelEnumConvert.java
@@ -0,0 +1,87 @@
+package com.pusong.common.excel.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.pusong.common.core.utils.reflect.ReflectUtils;
+import com.pusong.common.excel.annotation.ExcelEnumFormat;
+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) {
+ cellData.checkEmpty();
+ // Excel中填入的是枚举中指定的描述
+ Object textValue = switch (cellData.getType()) {
+ case STRING, DIRECT_STRING, RICH_TEXT_STRING -> cellData.getStringValue();
+ case NUMBER -> cellData.getNumberValue();
+ case BOOLEAN -> cellData.getBooleanValue();
+ default -> throw new IllegalArgumentException("单元格类型异常!");
+ };
+ // 如果是空值
+ if (ObjectUtil.isNull(textValue)) {
+ return null;
+ }
+ Map enumCodeToTextMap = beforeConvert(contentProperty);
+ // 从Java输出至Excel是code转text
+ // 因此从Excel转Java应该将text与code对调
+ Map enumTextToCodeMap = new HashMap<>();
+ enumCodeToTextMap.forEach((key, value) -> enumTextToCodeMap.put(value, key));
+ // 应该从text -> code中查找
+ Object codeValue = enumTextToCodeMap.get(textValue);
+ return Convert.convert(contentProperty.getField().getType(), codeValue);
+ }
+
+ @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/pusong-common/pusong-common-excel/src/main/java/com/pusong/common/excel/core/CellMergeStrategy.java b/pusong-common/pusong-common-excel/src/main/java/com/pusong/common/excel/core/CellMergeStrategy.java
new file mode 100644
index 0000000..87051d1
--- /dev/null
+++ b/pusong-common/pusong-common-excel/src/main/java/com/pusong/common/excel/core/CellMergeStrategy.java
@@ -0,0 +1,152 @@
+package com.pusong.common.excel.core;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.ReflectUtil;
+import cn.hutool.core.util.StrUtil;
+import com.alibaba.excel.annotation.ExcelProperty;
+import com.alibaba.excel.metadata.Head;
+import com.alibaba.excel.write.handler.WorkbookWriteHandler;
+import com.alibaba.excel.write.handler.context.WorkbookWriteHandlerContext;
+import com.alibaba.excel.write.merge.AbstractMergeStrategy;
+import com.pusong.common.excel.annotation.CellMerge;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.SneakyThrows;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.poi.ss.usermodel.Cell;
+import org.apache.poi.ss.usermodel.Sheet;
+import org.apache.poi.ss.util.CellRangeAddress;
+import com.pusong.common.core.utils.reflect.ReflectUtils;
+
+import java.lang.reflect.Field;
+import java.util.*;
+
+/**
+ * 列值重复合并策略
+ *
+ * @author Lion Li
+ */
+@Slf4j
+public class CellMergeStrategy extends AbstractMergeStrategy implements WorkbookWriteHandler {
+
+ private final List