phones);
+
+}
diff --git a/ruoyi-api/ruoyi-api-resource/src/main/java/org/dromara/resource/api/domain/RemoteFile.java b/ruoyi-api/ruoyi-api-resource/src/main/java/org/dromara/resource/api/domain/RemoteFile.java
new file mode 100644
index 0000000..7140fe6
--- /dev/null
+++ b/ruoyi-api/ruoyi-api-resource/src/main/java/org/dromara/resource/api/domain/RemoteFile.java
@@ -0,0 +1,44 @@
+package org.dromara.resource.api.domain;
+
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * 文件信息
+ *
+ * @author ruoyi
+ */
+@Data
+public class RemoteFile implements Serializable {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * oss主键
+ */
+ private Long ossId;
+
+ /**
+ * 文件名称
+ */
+ private String name;
+
+ /**
+ * 文件地址
+ */
+ private String url;
+
+ /**
+ * 原名
+ */
+ private String originalName;
+
+ /**
+ * 文件后缀名
+ */
+ private String fileSuffix;
+
+}
diff --git a/ruoyi-api/ruoyi-api-resource/src/main/java/org/dromara/resource/api/domain/RemoteSms.java b/ruoyi-api/ruoyi-api-resource/src/main/java/org/dromara/resource/api/domain/RemoteSms.java
new file mode 100644
index 0000000..3e35d5b
--- /dev/null
+++ b/ruoyi-api/ruoyi-api-resource/src/main/java/org/dromara/resource/api/domain/RemoteSms.java
@@ -0,0 +1,36 @@
+package org.dromara.resource.api.domain;
+
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * 文件信息
+ *
+ * @author ruoyi
+ */
+@Data
+public class RemoteSms implements Serializable {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 是否成功
+ */
+ private Boolean success;
+
+ /**
+ * 配置标识名 如未配置取对应渠道名例如 Alibaba
+ */
+ private String configId;
+
+ /**
+ * 厂商原返回体
+ *
+ * 可自行转换为 SDK 对应的 SendSmsResponse
+ */
+ private String response;
+
+}
diff --git a/ruoyi-api/ruoyi-api-system/pom.xml b/ruoyi-api/ruoyi-api-system/pom.xml
new file mode 100644
index 0000000..02958f3
--- /dev/null
+++ b/ruoyi-api/ruoyi-api-system/pom.xml
@@ -0,0 +1,33 @@
+
+
+
+ org.dromara
+ ruoyi-api
+ ${revision}
+
+ 4.0.0
+
+ ruoyi-api-system
+
+
+ ruoyi-api-system系统接口模块
+
+
+
+
+
+
+ org.dromara
+ ruoyi-common-core
+
+
+
+ org.dromara
+ ruoyi-common-excel
+
+
+
+
+
diff --git a/ruoyi-api/ruoyi-api-system/src/main/java/org/dromara/system/api/RemoteClientService.java b/ruoyi-api/ruoyi-api-system/src/main/java/org/dromara/system/api/RemoteClientService.java
new file mode 100644
index 0000000..89c3068
--- /dev/null
+++ b/ruoyi-api/ruoyi-api-system/src/main/java/org/dromara/system/api/RemoteClientService.java
@@ -0,0 +1,20 @@
+package org.dromara.system.api;
+
+import org.dromara.system.api.domain.vo.RemoteClientVo;
+
+/**
+ * 客户端服务
+ *
+ * @author Michelle.Chung
+ */
+public interface RemoteClientService {
+
+ /**
+ * 根据客户端id获取客户端详情
+ *
+ * @param clientId 客户端id
+ * @return 客户端对象
+ */
+ RemoteClientVo queryByClientId(String clientId);
+
+}
diff --git a/ruoyi-api/ruoyi-api-system/src/main/java/org/dromara/system/api/RemoteConfigService.java b/ruoyi-api/ruoyi-api-system/src/main/java/org/dromara/system/api/RemoteConfigService.java
new file mode 100644
index 0000000..4c4502e
--- /dev/null
+++ b/ruoyi-api/ruoyi-api-system/src/main/java/org/dromara/system/api/RemoteConfigService.java
@@ -0,0 +1,17 @@
+package org.dromara.system.api;
+
+/**
+ * 配置服务
+ *
+ * @author Michelle.Chung
+ */
+public interface RemoteConfigService {
+
+ /**
+ * 获取注册开关
+ * @param tenantId 租户id
+ * @return true开启,false关闭
+ */
+ boolean selectRegisterEnabled(String tenantId);
+
+}
diff --git a/ruoyi-api/ruoyi-api-system/src/main/java/org/dromara/system/api/RemoteDataScopeService.java b/ruoyi-api/ruoyi-api-system/src/main/java/org/dromara/system/api/RemoteDataScopeService.java
new file mode 100644
index 0000000..d6a4005
--- /dev/null
+++ b/ruoyi-api/ruoyi-api-system/src/main/java/org/dromara/system/api/RemoteDataScopeService.java
@@ -0,0 +1,26 @@
+package org.dromara.system.api;
+
+/**
+ * 数据权限服务
+ *
+ * @author Lion Li
+ */
+public interface RemoteDataScopeService {
+
+ /**
+ * 获取角色自定义权限语句
+ *
+ * @param roleId 角色ID
+ * @return 返回角色的自定义权限语句,如果没有找到则返回 null
+ */
+ String getRoleCustom(Long roleId);
+
+ /**
+ * 获取部门和下级权限语句
+ *
+ * @param deptId 部门ID
+ * @return 返回部门及其下级的权限语句,如果没有找到则返回 null
+ */
+ String getDeptAndChild(Long deptId);
+
+}
diff --git a/ruoyi-api/ruoyi-api-system/src/main/java/org/dromara/system/api/RemoteDeptService.java b/ruoyi-api/ruoyi-api-system/src/main/java/org/dromara/system/api/RemoteDeptService.java
new file mode 100644
index 0000000..2ece928
--- /dev/null
+++ b/ruoyi-api/ruoyi-api-system/src/main/java/org/dromara/system/api/RemoteDeptService.java
@@ -0,0 +1,37 @@
+package org.dromara.system.api;
+
+import org.dromara.system.api.domain.vo.RemoteDeptVo;
+
+import java.util.List;
+
+/**
+ * 部门服务
+ *
+ * @author Lion Li
+ */
+public interface RemoteDeptService {
+
+ /**
+ * 通过部门ID查询部门名称
+ *
+ * @param deptIds 部门ID串逗号分隔
+ * @return 部门名称串逗号分隔
+ */
+ String selectDeptNameByIds(String deptIds);
+
+ /**
+ * 根据部门ID查询部门负责人
+ *
+ * @param deptId 部门ID,用于指定需要查询的部门
+ * @return 返回该部门的负责人ID
+ */
+ Long selectDeptLeaderById(Long deptId);
+
+ /**
+ * 查询部门
+ *
+ * @return 部门列表
+ */
+ List selectDeptsByList();
+
+}
diff --git a/ruoyi-api/ruoyi-api-system/src/main/java/org/dromara/system/api/RemoteDictService.java b/ruoyi-api/ruoyi-api-system/src/main/java/org/dromara/system/api/RemoteDictService.java
new file mode 100644
index 0000000..d5b6727
--- /dev/null
+++ b/ruoyi-api/ruoyi-api-system/src/main/java/org/dromara/system/api/RemoteDictService.java
@@ -0,0 +1,31 @@
+package org.dromara.system.api;
+
+import org.dromara.system.api.domain.vo.RemoteDictDataVo;
+import org.dromara.system.api.domain.vo.RemoteDictTypeVo;
+
+import java.util.List;
+
+/**
+ * 字典服务
+ *
+ * @author Lion Li
+ */
+public interface RemoteDictService {
+
+ /**
+ * 根据字典类型查询信息
+ *
+ * @param dictType 字典类型
+ * @return 字典类型
+ */
+ RemoteDictTypeVo selectDictTypeByType(String dictType);
+
+ /**
+ * 根据字典类型查询字典数据
+ *
+ * @param dictType 字典类型
+ * @return 字典数据集合信息
+ */
+ List selectDictDataByType(String dictType);
+
+}
diff --git a/ruoyi-api/ruoyi-api-system/src/main/java/org/dromara/system/api/RemoteLogService.java b/ruoyi-api/ruoyi-api-system/src/main/java/org/dromara/system/api/RemoteLogService.java
new file mode 100644
index 0000000..6b35118
--- /dev/null
+++ b/ruoyi-api/ruoyi-api-system/src/main/java/org/dromara/system/api/RemoteLogService.java
@@ -0,0 +1,27 @@
+package org.dromara.system.api;
+
+import org.dromara.system.api.domain.bo.RemoteLogininforBo;
+import org.dromara.system.api.domain.bo.RemoteOperLogBo;
+
+/**
+ * 日志服务
+ *
+ * @author Lion Li
+ */
+public interface RemoteLogService {
+
+ /**
+ * 保存系统日志
+ *
+ * @param sysOperLog 日志实体
+ */
+ void saveLog(RemoteOperLogBo sysOperLog);
+
+ /**
+ * 保存访问记录
+ *
+ * @param sysLogininfor 访问实体
+ */
+ void saveLogininfor(RemoteLogininforBo sysLogininfor);
+
+}
diff --git a/ruoyi-api/ruoyi-api-system/src/main/java/org/dromara/system/api/RemotePermissionService.java b/ruoyi-api/ruoyi-api-system/src/main/java/org/dromara/system/api/RemotePermissionService.java
new file mode 100644
index 0000000..2a815a2
--- /dev/null
+++ b/ruoyi-api/ruoyi-api-system/src/main/java/org/dromara/system/api/RemotePermissionService.java
@@ -0,0 +1,28 @@
+package org.dromara.system.api;
+
+import java.util.Set;
+
+/**
+ * 用户权限处理
+ *
+ * @author Lion Li
+ */
+public interface RemotePermissionService {
+
+ /**
+ * 获取角色数据权限
+ *
+ * @param userId 用户id
+ * @return 角色权限信息
+ */
+ Set getRolePermission(Long userId);
+
+ /**
+ * 获取菜单数据权限
+ *
+ * @param userId 用户id
+ * @return 菜单权限信息
+ */
+ Set getMenuPermission(Long userId);
+
+}
diff --git a/ruoyi-api/ruoyi-api-system/src/main/java/org/dromara/system/api/RemoteSocialService.java b/ruoyi-api/ruoyi-api-system/src/main/java/org/dromara/system/api/RemoteSocialService.java
new file mode 100644
index 0000000..0a54df5
--- /dev/null
+++ b/ruoyi-api/ruoyi-api-system/src/main/java/org/dromara/system/api/RemoteSocialService.java
@@ -0,0 +1,52 @@
+package org.dromara.system.api;
+
+import org.dromara.system.api.domain.bo.RemoteSocialBo;
+import org.dromara.system.api.domain.vo.RemoteSocialVo;
+
+import java.util.List;
+
+/**
+ * 社会化关系服务
+ *
+ * @author Michelle.Chung
+ */
+public interface RemoteSocialService {
+
+ /**
+ * 根据 authId 查询用户授权信息
+ *
+ * @param authId 认证id
+ * @return 授权信息
+ */
+ List selectByAuthId(String authId);
+
+ /**
+ * 查询列表
+ *
+ * @param bo 社会化关系业务对象
+ */
+ List queryList(RemoteSocialBo bo);
+
+ /**
+ * 保存社会化关系
+ *
+ * @param bo 社会化关系业务对象
+ */
+ void insertByBo(RemoteSocialBo bo);
+
+ /**
+ * 更新社会化关系
+ *
+ * @param bo 社会化关系业务对象
+ */
+ void updateByBo(RemoteSocialBo bo);
+
+ /**
+ * 删除社会化关系
+ *
+ * @param socialId 社会化关系ID
+ * @return 结果
+ */
+ Boolean deleteWithValidById(Long socialId);
+
+}
diff --git a/ruoyi-api/ruoyi-api-system/src/main/java/org/dromara/system/api/RemoteTaskAssigneeService.java b/ruoyi-api/ruoyi-api-system/src/main/java/org/dromara/system/api/RemoteTaskAssigneeService.java
new file mode 100644
index 0000000..df06407
--- /dev/null
+++ b/ruoyi-api/ruoyi-api-system/src/main/java/org/dromara/system/api/RemoteTaskAssigneeService.java
@@ -0,0 +1,45 @@
+package org.dromara.system.api;
+
+import org.dromara.system.api.domain.bo.RemoteTaskAssigneeBo;
+import org.dromara.system.api.domain.vo.RemoteTaskAssigneeVo;
+
+/**
+ * 工作流设计器获取任务执行人
+ *
+ * @author Lion Li
+ */
+public interface RemoteTaskAssigneeService {
+
+ /**
+ * 查询角色并返回任务指派的列表,支持分页
+ *
+ * @param taskQuery 查询条件
+ * @return 办理人
+ */
+ RemoteTaskAssigneeVo selectRolesByTaskAssigneeList(RemoteTaskAssigneeBo taskQuery);
+
+ /**
+ * 查询岗位并返回任务指派的列表,支持分页
+ *
+ * @param taskQuery 查询条件
+ * @return 办理人
+ */
+ RemoteTaskAssigneeVo selectPostsByTaskAssigneeList(RemoteTaskAssigneeBo taskQuery);
+
+ /**
+ * 查询部门并返回任务指派的列表,支持分页
+ *
+ * @param taskQuery 查询条件
+ * @return 办理人
+ */
+ RemoteTaskAssigneeVo selectDeptsByTaskAssigneeList(RemoteTaskAssigneeBo taskQuery);
+
+ /**
+ * 查询用户并返回任务指派的列表,支持分页
+ *
+ * @param taskQuery 查询条件
+ * @return 办理人
+ */
+ RemoteTaskAssigneeVo selectUsersByTaskAssigneeList(RemoteTaskAssigneeBo taskQuery);
+
+}
diff --git a/ruoyi-api/ruoyi-api-system/src/main/java/org/dromara/system/api/RemoteTenantService.java b/ruoyi-api/ruoyi-api-system/src/main/java/org/dromara/system/api/RemoteTenantService.java
new file mode 100644
index 0000000..2626abe
--- /dev/null
+++ b/ruoyi-api/ruoyi-api-system/src/main/java/org/dromara/system/api/RemoteTenantService.java
@@ -0,0 +1,28 @@
+package org.dromara.system.api;
+
+
+import org.dromara.system.api.domain.vo.RemoteTenantVo;
+
+import java.util.List;
+
+/**
+ * 租户服务
+ *
+ * @author zhujie
+ */
+public interface RemoteTenantService {
+
+ /**
+ * 根据租户id获取租户详情
+ * @param tenantId 租户id
+ * @return 结果
+ */
+ RemoteTenantVo queryByTenantId(String tenantId);
+
+ /**
+ * 获取租户列表
+ * @return 结果
+ */
+ List queryList();
+
+}
diff --git a/ruoyi-api/ruoyi-api-system/src/main/java/org/dromara/system/api/RemoteUserService.java b/ruoyi-api/ruoyi-api-system/src/main/java/org/dromara/system/api/RemoteUserService.java
new file mode 100644
index 0000000..3e8b548
--- /dev/null
+++ b/ruoyi-api/ruoyi-api-system/src/main/java/org/dromara/system/api/RemoteUserService.java
@@ -0,0 +1,192 @@
+package org.dromara.system.api;
+
+import org.dromara.common.core.exception.ServiceException;
+import org.dromara.common.core.exception.user.UserException;
+import org.dromara.system.api.domain.bo.RemoteUserBo;
+import org.dromara.system.api.domain.vo.RemoteUserVo;
+import org.dromara.system.api.model.LoginUser;
+import org.dromara.system.api.model.XcxLoginUser;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 用户服务
+ *
+ * @author Lion Li
+ */
+public interface RemoteUserService {
+
+ /**
+ * 通过用户名查询用户信息
+ *
+ * @param username 用户名
+ * @param tenantId 租户id
+ * @return 结果
+ */
+ LoginUser getUserInfo(String username, String tenantId) throws UserException;
+
+ /**
+ * 通过用户id查询用户信息
+ *
+ * @param userId 用户id
+ * @param tenantId 租户id
+ * @return 结果
+ */
+ LoginUser getUserInfo(Long userId, String tenantId) throws UserException;
+
+ /**
+ * 通过手机号查询用户信息
+ *
+ * @param phonenumber 手机号
+ * @param tenantId 租户id
+ * @return 结果
+ */
+ LoginUser getUserInfoByPhonenumber(String phonenumber, String tenantId) throws UserException;
+
+ /**
+ * 通过邮箱查询用户信息
+ *
+ * @param email 邮箱
+ * @param tenantId 租户id
+ * @return 结果
+ */
+ LoginUser getUserInfoByEmail(String email, String tenantId) throws UserException;
+
+ /**
+ * 通过openid查询用户信息
+ *
+ * @param openid openid
+ * @return 结果
+ */
+ XcxLoginUser getUserInfoByOpenid(String openid) throws UserException;
+
+ /**
+ * 注册用户信息
+ *
+ * @param remoteUserBo 用户信息
+ * @return 结果
+ */
+ Boolean registerUserInfo(RemoteUserBo remoteUserBo) throws UserException, ServiceException;
+
+ /**
+ * 通过userId查询用户账户
+ *
+ * @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);
+
+ /**
+ * 更新用户信息
+ *
+ * @param userId 用户ID
+ * @param ip IP地址
+ */
+ void recordLoginInfo(Long userId, String ip);
+
+ /**
+ * 通过用户ID查询用户列表
+ *
+ * @param userIds 用户ids
+ * @return 用户列表
+ */
+ List selectListByIds(List userIds);
+
+ /**
+ * 通过角色ID查询用户ID
+ *
+ * @param roleIds 角色ids
+ * @return 用户ids
+ */
+ List selectUserIdsByRoleIds(List roleIds);
+
+ /**
+ * 通过角色ID查询用户
+ *
+ * @param roleIds 角色ids
+ * @return 用户
+ */
+ List selectUsersByRoleIds(List roleIds);
+
+ /**
+ * 通过部门ID查询用户
+ *
+ * @param deptIds 部门ids
+ * @return 用户
+ */
+ List selectUsersByDeptIds(List deptIds);
+
+ /**
+ * 通过岗位ID查询用户
+ *
+ * @param postIds 岗位ids
+ * @return 用户
+ */
+ List selectUsersByPostIds(List postIds);
+
+ /**
+ * 根据用户 ID 列表查询用户名称映射关系
+ *
+ * @param userIds 用户 ID 列表
+ * @return Map,其中 key 为用户 ID,value 为对应的用户名称
+ */
+ Map selectUserNamesByIds(List userIds);
+
+ /**
+ * 根据角色 ID 列表查询角色名称映射关系
+ *
+ * @param roleIds 角色 ID 列表
+ * @return Map,其中 key 为角色 ID,value 为对应的角色名称
+ */
+ Map selectRoleNamesByIds(List roleIds);
+
+ /**
+ * 根据部门 ID 列表查询部门名称映射关系
+ *
+ * @param deptIds 部门 ID 列表
+ * @return Map,其中 key 为部门 ID,value 为对应的部门名称
+ */
+ Map selectDeptNamesByIds(List deptIds);
+
+ /**
+ * 根据岗位 ID 列表查询岗位名称映射关系
+ *
+ * @param postIds 岗位 ID 列表
+ * @return Map,其中 key 为岗位 ID,value 为对应的岗位名称
+ */
+ Map selectPostNamesByIds(List postIds);
+
+}
diff --git a/ruoyi-api/ruoyi-api-system/src/main/java/org/dromara/system/api/domain/SysUserOnline.java b/ruoyi-api/ruoyi-api-system/src/main/java/org/dromara/system/api/domain/SysUserOnline.java
new file mode 100644
index 0000000..25c985a
--- /dev/null
+++ b/ruoyi-api/ruoyi-api-system/src/main/java/org/dromara/system/api/domain/SysUserOnline.java
@@ -0,0 +1,71 @@
+package org.dromara.system.api.domain;
+
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * 当前在线会话
+ *
+ * @author Lion Li
+ */
+@Data
+@NoArgsConstructor
+public class SysUserOnline 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/ruoyi-api/ruoyi-api-system/src/main/java/org/dromara/system/api/domain/bo/RemoteLogininforBo.java b/ruoyi-api/ruoyi-api-system/src/main/java/org/dromara/system/api/domain/bo/RemoteLogininforBo.java
new file mode 100644
index 0000000..d8c48a9
--- /dev/null
+++ b/ruoyi-api/ruoyi-api-system/src/main/java/org/dromara/system/api/domain/bo/RemoteLogininforBo.java
@@ -0,0 +1,89 @@
+package org.dromara.system.api.domain.bo;
+
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 系统访问记录表 sys_logininfor
+ *
+ * @author Lion Li
+ */
+@Data
+@NoArgsConstructor
+public class RemoteLogininforBo implements Serializable {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 访问ID
+ */
+ private Long infoId;
+
+ /**
+ * 租户编号
+ */
+ private String tenantId;
+
+ /**
+ * 用户账号
+ */
+ private String userName;
+
+ /**
+ * 客户端
+ */
+ private String clientKey;
+
+ /**
+ * 设备类型
+ */
+ private String deviceType;
+
+ /**
+ * 登录IP地址
+ */
+ private String ipaddr;
+
+ /**
+ * 登录地点
+ */
+ private String loginLocation;
+
+ /**
+ * 浏览器类型
+ */
+ private String browser;
+
+ /**
+ * 操作系统
+ */
+ private String os;
+
+ /**
+ * 登录状态(0成功 1失败)
+ */
+ private String status;
+
+ /**
+ * 提示消息
+ */
+ private String msg;
+
+ /**
+ * 访问时间
+ */
+ private Date loginTime;
+
+ /**
+ * 请求参数
+ */
+ private Map params = new HashMap<>();
+
+}
diff --git a/ruoyi-api/ruoyi-api-system/src/main/java/org/dromara/system/api/domain/bo/RemoteOperLogBo.java b/ruoyi-api/ruoyi-api-system/src/main/java/org/dromara/system/api/domain/bo/RemoteOperLogBo.java
new file mode 100644
index 0000000..5ed441b
--- /dev/null
+++ b/ruoyi-api/ruoyi-api-system/src/main/java/org/dromara/system/api/domain/bo/RemoteOperLogBo.java
@@ -0,0 +1,119 @@
+package org.dromara.system.api.domain.bo;
+
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 操作日志记录表 oper_log
+ *
+ * @author Lion Li
+ */
+@Data
+@NoArgsConstructor
+public class RemoteOperLogBo implements Serializable {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 日志主键
+ */
+ private Long operId;
+
+ /**
+ * 租户编号
+ */
+ private String tenantId;
+
+ /**
+ * 模块标题
+ */
+ private String title;
+
+ /**
+ * 业务类型(0其它 1新增 2修改 3删除)
+ */
+ private Integer businessType;
+
+ /**
+ * 方法名称
+ */
+ private String method;
+
+ /**
+ * 请求方式
+ */
+ private String requestMethod;
+
+ /**
+ * 操作类别(0其它 1后台用户 2手机端用户)
+ */
+ private Integer operatorType;
+
+ /**
+ * 操作人员
+ */
+ private String operName;
+
+ /**
+ * 部门名称
+ */
+ private String deptName;
+
+ /**
+ * 请求URL
+ */
+ private String operUrl;
+
+ /**
+ * 主机地址
+ */
+ private String operIp;
+
+ /**
+ * 操作地点
+ */
+ private String operLocation;
+
+ /**
+ * 请求参数
+ */
+ private String operParam;
+
+ /**
+ * 返回参数
+ */
+ private String jsonResult;
+
+ /**
+ * 操作状态(0正常 1异常)
+ */
+ private Integer status;
+
+ /**
+ * 错误消息
+ */
+ private String errorMsg;
+
+ /**
+ * 操作时间
+ */
+ private Date operTime;
+
+ /**
+ * 消耗时间
+ */
+ private Long costTime;
+
+ /**
+ * 请求参数
+ */
+ private Map params = new HashMap<>();
+
+}
diff --git a/ruoyi-api/ruoyi-api-system/src/main/java/org/dromara/system/api/domain/bo/RemoteSocialBo.java b/ruoyi-api/ruoyi-api-system/src/main/java/org/dromara/system/api/domain/bo/RemoteSocialBo.java
new file mode 100644
index 0000000..78daf2f
--- /dev/null
+++ b/ruoyi-api/ruoyi-api-system/src/main/java/org/dromara/system/api/domain/bo/RemoteSocialBo.java
@@ -0,0 +1,129 @@
+package org.dromara.system.api.domain.bo;
+
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * 社会化关系业务对象 sys_social
+ *
+ * @author Michelle.Chung
+ */
+@Data
+@NoArgsConstructor
+public class RemoteSocialBo implements Serializable {
+ @Serial
+ private static final long serialVersionUID = 1L;
+ /**
+ * 主键
+ */
+ private Long id;
+
+ /**
+ * 的唯一ID
+ */
+ private String authId;
+
+ /**
+ * 用户来源
+ */
+ private String source;
+
+ /**
+ * 用户的授权令牌
+ */
+ private String accessToken;
+
+ /**
+ * 用户的授权令牌的有效期,部分平台可能没有
+ */
+ private int expireIn;
+
+ /**
+ * 刷新令牌,部分平台可能没有
+ */
+ private String refreshToken;
+
+ /**
+ * 平台唯一id
+ */
+ private String openId;
+
+ /**
+ * 用户的 ID
+ */
+ private Long userId;
+
+ /**
+ * 平台的授权信息,部分平台可能没有
+ */
+ private String accessCode;
+
+ /**
+ * 用户的 unionid
+ */
+ private String unionId;
+
+ /**
+ * 授予的权限,部分平台可能没有
+ */
+ private String scope;
+
+ /**
+ * 授权的第三方账号
+ */
+ private String userName;
+
+ /**
+ * 授权的第三方昵称
+ */
+ private String nickName;
+
+ /**
+ * 授权的第三方邮箱
+ */
+ private String email;
+
+ /**
+ * 授权的第三方头像地址
+ */
+ private String avatar;
+
+ /**
+ * 个别平台的授权信息,部分平台可能没有
+ */
+ private String tokenType;
+
+ /**
+ * id token,部分平台可能没有
+ */
+ private String idToken;
+
+ /**
+ * 小米平台用户的附带属性,部分平台可能没有
+ */
+ private String macAlgorithm;
+
+ /**
+ * 小米平台用户的附带属性,部分平台可能没有
+ */
+ private String macKey;
+
+ /**
+ * 用户的授权code,部分平台可能没有
+ */
+ private String code;
+
+ /**
+ * Twitter平台用户的附带属性,部分平台可能没有
+ */
+ private String oauthToken;
+
+ /**
+ * Twitter平台用户的附带属性,部分平台可能没有
+ */
+ private String oauthTokenSecret;
+
+}
diff --git a/ruoyi-api/ruoyi-api-system/src/main/java/org/dromara/system/api/domain/bo/RemoteTaskAssigneeBo.java b/ruoyi-api/ruoyi-api-system/src/main/java/org/dromara/system/api/domain/bo/RemoteTaskAssigneeBo.java
new file mode 100644
index 0000000..91ae441
--- /dev/null
+++ b/ruoyi-api/ruoyi-api-system/src/main/java/org/dromara/system/api/domain/bo/RemoteTaskAssigneeBo.java
@@ -0,0 +1,56 @@
+package org.dromara.system.api.domain.bo;
+
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * 任务受让人
+ *
+ * @author AprilWind
+ */
+@Data
+@NoArgsConstructor
+public class RemoteTaskAssigneeBo implements Serializable {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 权限编码
+ */
+ private String handlerCode;
+
+ /**
+ * 权限名称
+ */
+ private String handlerName;
+
+ /**
+ * 权限分组
+ */
+ private String groupId;
+
+ /**
+ * 开始时间
+ */
+ private String beginTime;
+
+ /**
+ * 结束时间
+ */
+ private String endTime;
+
+ /**
+ * 当前页
+ */
+ private Integer pageNum = 1;
+
+ /**
+ * 每页显示条数
+ */
+ private Integer pageSize = 10;
+
+}
diff --git a/ruoyi-api/ruoyi-api-system/src/main/java/org/dromara/system/api/domain/bo/RemoteUserBo.java b/ruoyi-api/ruoyi-api-system/src/main/java/org/dromara/system/api/domain/bo/RemoteUserBo.java
new file mode 100644
index 0000000..64f0cf7
--- /dev/null
+++ b/ruoyi-api/ruoyi-api-system/src/main/java/org/dromara/system/api/domain/bo/RemoteUserBo.java
@@ -0,0 +1,124 @@
+package org.dromara.system.api.domain.bo;
+
+
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.dromara.common.core.constant.SystemConstants;
+import org.dromara.common.core.xss.Xss;
+
+import jakarta.validation.constraints.Email;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.Size;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * 用户信息业务对象 sys_user
+ *
+ * @author Michelle.Chung
+ */
+@Data
+@NoArgsConstructor
+public class RemoteUserBo implements Serializable {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 用户ID
+ */
+ private Long userId;
+
+ /**
+ * 租户ID
+ */
+ private String tenantId;
+
+ /**
+ * 部门ID
+ */
+ private Long deptId;
+
+ /**
+ * 用户账号
+ */
+ @Xss(message = "用户账号不能包含脚本字符")
+ @NotBlank(message = "用户账号不能为空")
+ @Size(min = 0, max = 30, message = "用户账号长度不能超过{max}个字符")
+ private String userName;
+
+ /**
+ * 用户昵称
+ */
+ @Xss(message = "用户昵称不能包含脚本字符")
+ @Size(min = 0, max = 30, message = "用户昵称长度不能超过{max}个字符")
+ private String nickName;
+
+ /**
+ * 用户类型(sys_user系统用户)
+ */
+ private String userType;
+
+ /**
+ * 用户邮箱
+ */
+ @Email(message = "邮箱格式不正确")
+ @Size(min = 0, max = 50, message = "邮箱长度不能超过{max}个字符")
+ private String email;
+
+ /**
+ * 手机号码
+ */
+ private String phonenumber;
+
+ /**
+ * 用户性别(0男 1女 2未知)
+ */
+ private String sex;
+
+ /**
+ * 头像地址
+ */
+ private Long avatar;
+
+ /**
+ * 密码
+ */
+ private String password;
+
+ /**
+ * 帐号状态(0正常 1停用)
+ */
+ private String status;
+
+ /**
+ * 最后登录IP
+ */
+ private String loginIp;
+
+ /**
+ * 最后登录时间
+ */
+ private Date loginDate;
+
+ /**
+ * 备注
+ */
+ private String remark;
+
+ /**
+ * 数据权限 当前角色ID
+ */
+ private Long roleId;
+
+ public RemoteUserBo(Long userId) {
+ this.userId = userId;
+ }
+
+ public boolean isSuperAdmin() {
+ return SystemConstants.SUPER_ADMIN_ID.equals(this.userId);
+ }
+
+}
diff --git a/ruoyi-api/ruoyi-api-system/src/main/java/org/dromara/system/api/domain/vo/RemoteClientVo.java b/ruoyi-api/ruoyi-api-system/src/main/java/org/dromara/system/api/domain/vo/RemoteClientVo.java
new file mode 100644
index 0000000..e141f2b
--- /dev/null
+++ b/ruoyi-api/ruoyi-api-system/src/main/java/org/dromara/system/api/domain/vo/RemoteClientVo.java
@@ -0,0 +1,70 @@
+package org.dromara.system.api.domain.vo;
+
+import lombok.Data;
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.List;
+
+
+/**
+ * 授权管理视图对象 sys_client
+ *
+ * @author Michelle.Chung
+ */
+@Data
+public class RemoteClientVo implements Serializable {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * id
+ */
+ private Long id;
+
+ /**
+ * 客户端id
+ */
+ private String clientId;
+
+ /**
+ * 客户端key
+ */
+ private String clientKey;
+
+ /**
+ * 客户端秘钥
+ */
+ private String clientSecret;
+
+ /**
+ * 授权类型
+ */
+ private List grantTypeList;
+
+ /**
+ * 授权类型
+ */
+ private String grantType;
+
+ /**
+ * 设备类型
+ */
+ private String deviceType;
+
+ /**
+ * token活跃超时时间
+ */
+ private Long activeTimeout;
+
+ /**
+ * token固定超时时间
+ */
+ private Long timeout;
+
+ /**
+ * 状态(0正常 1停用)
+ */
+ private String status;
+
+}
diff --git a/ruoyi-api/ruoyi-api-system/src/main/java/org/dromara/system/api/domain/vo/RemoteDeptVo.java b/ruoyi-api/ruoyi-api-system/src/main/java/org/dromara/system/api/domain/vo/RemoteDeptVo.java
new file mode 100644
index 0000000..e02b565
--- /dev/null
+++ b/ruoyi-api/ruoyi-api-system/src/main/java/org/dromara/system/api/domain/vo/RemoteDeptVo.java
@@ -0,0 +1,37 @@
+package org.dromara.system.api.domain.vo;
+
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * 部门
+ *
+ * @author AprilWind
+ */
+
+@Data
+@NoArgsConstructor
+public class RemoteDeptVo implements Serializable {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 部门ID
+ */
+ private Long deptId;
+
+ /**
+ * 父部门ID
+ */
+ private Long parentId;
+
+ /**
+ * 部门名称
+ */
+ private String deptName;
+
+}
diff --git a/ruoyi-api/ruoyi-api-system/src/main/java/org/dromara/system/api/domain/vo/RemoteDictDataVo.java b/ruoyi-api/ruoyi-api-system/src/main/java/org/dromara/system/api/domain/vo/RemoteDictDataVo.java
new file mode 100644
index 0000000..294d3e3
--- /dev/null
+++ b/ruoyi-api/ruoyi-api-system/src/main/java/org/dromara/system/api/domain/vo/RemoteDictDataVo.java
@@ -0,0 +1,76 @@
+package org.dromara.system.api.domain.vo;
+
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.Date;
+
+
+/**
+ * 字典数据视图对象 sys_dict_data
+ *
+ * @author Michelle.Chung
+ */
+@Data
+public class RemoteDictDataVo implements Serializable {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 字典编码
+ */
+ private Long dictCode;
+
+ /**
+ * 字典排序
+ */
+ private Integer dictSort;
+
+ /**
+ * 字典标签
+ */
+ private String dictLabel;
+
+ /**
+ * 字典键值
+ */
+ private String dictValue;
+
+ /**
+ * 字典类型
+ */
+ private String dictType;
+
+ /**
+ * 样式属性(其他样式扩展)
+ */
+ private String cssClass;
+
+ /**
+ * 表格回显样式
+ */
+ private String listClass;
+
+ /**
+ * 是否默认(Y是 N否)
+ */
+ private String isDefault;
+
+ /**
+ * 状态(0正常 1停用)
+ */
+ private String status;
+
+ /**
+ * 备注
+ */
+ private String remark;
+
+ /**
+ * 创建时间
+ */
+ private Date createTime;
+
+}
diff --git a/ruoyi-api/ruoyi-api-system/src/main/java/org/dromara/system/api/domain/vo/RemoteDictTypeVo.java b/ruoyi-api/ruoyi-api-system/src/main/java/org/dromara/system/api/domain/vo/RemoteDictTypeVo.java
new file mode 100644
index 0000000..5b9b099
--- /dev/null
+++ b/ruoyi-api/ruoyi-api-system/src/main/java/org/dromara/system/api/domain/vo/RemoteDictTypeVo.java
@@ -0,0 +1,46 @@
+package org.dromara.system.api.domain.vo;
+
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.Date;
+
+
+/**
+ * 字典类型视图对象 sys_dict_type
+ *
+ * @author Michelle.Chung
+ */
+@Data
+public class RemoteDictTypeVo implements Serializable {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 字典主键
+ */
+ private Long dictId;
+
+ /**
+ * 字典名称
+ */
+ private String dictName;
+
+ /**
+ * 字典类型
+ */
+ private String dictType;
+
+ /**
+ * 备注
+ */
+ private String remark;
+
+ /**
+ * 创建时间
+ */
+ private Date createTime;
+
+}
diff --git a/ruoyi-api/ruoyi-api-system/src/main/java/org/dromara/system/api/domain/vo/RemoteSocialVo.java b/ruoyi-api/ruoyi-api-system/src/main/java/org/dromara/system/api/domain/vo/RemoteSocialVo.java
new file mode 100644
index 0000000..b3272f5
--- /dev/null
+++ b/ruoyi-api/ruoyi-api-system/src/main/java/org/dromara/system/api/domain/vo/RemoteSocialVo.java
@@ -0,0 +1,135 @@
+package org.dromara.system.api.domain.vo;
+
+import lombok.Data;
+import java.io.Serial;
+import java.io.Serializable;
+
+
+/**
+ * 社会化关系视图对象 sys_social
+ *
+ * @author thiszhc
+ */
+@Data
+public class RemoteSocialVo implements Serializable {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 主键
+ */
+ private Long id;
+
+ /**
+ * 用户ID
+ */
+ private Long userId;
+
+ /**
+ * 租户ID
+ */
+ private String tenantId;
+
+ /**
+ * 认证唯一ID
+ */
+ private String authId;
+
+ /**
+ * 用户来源
+ */
+ private String source;
+
+ /**
+ * 用户的授权令牌
+ */
+ private String accessToken;
+
+ /**
+ * 用户的授权令牌的有效期,部分平台可能没有
+ */
+ private int expireIn;
+
+ /**
+ * 刷新令牌,部分平台可能没有
+ */
+ private String refreshToken;
+
+ /**
+ * 用户的 open id
+ */
+ private String openId;
+
+ /**
+ * 授权的第三方账号
+ */
+ private String userName;
+
+ /**
+ * 授权的第三方昵称
+ */
+ private String nickName;
+
+ /**
+ * 授权的第三方邮箱
+ */
+ private String email;
+
+ /**
+ * 授权的第三方头像地址
+ */
+ private String avatar;
+
+
+ /**
+ * 平台的授权信息,部分平台可能没有
+ */
+ private String accessCode;
+
+ /**
+ * 用户的 unionid
+ */
+ private String unionId;
+
+ /**
+ * 授予的权限,部分平台可能没有
+ */
+ private String scope;
+
+ /**
+ * 个别平台的授权信息,部分平台可能没有
+ */
+ private String tokenType;
+
+ /**
+ * id token,部分平台可能没有
+ */
+ private String idToken;
+
+ /**
+ * 小米平台用户的附带属性,部分平台可能没有
+ */
+ private String macAlgorithm;
+
+ /**
+ * 小米平台用户的附带属性,部分平台可能没有
+ */
+ private String macKey;
+
+ /**
+ * 用户的授权code,部分平台可能没有
+ */
+ private String code;
+
+ /**
+ * Twitter平台用户的附带属性,部分平台可能没有
+ */
+ private String oauthToken;
+
+ /**
+ * Twitter平台用户的附带属性,部分平台可能没有
+ */
+ private String oauthTokenSecret;
+
+}
diff --git a/ruoyi-api/ruoyi-api-system/src/main/java/org/dromara/system/api/domain/vo/RemoteTaskAssigneeVo.java b/ruoyi-api/ruoyi-api-system/src/main/java/org/dromara/system/api/domain/vo/RemoteTaskAssigneeVo.java
new file mode 100644
index 0000000..5f59d8c
--- /dev/null
+++ b/ruoyi-api/ruoyi-api-system/src/main/java/org/dromara/system/api/domain/vo/RemoteTaskAssigneeVo.java
@@ -0,0 +1,104 @@
+package org.dromara.system.api.domain.vo;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.Date;
+import java.util.List;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+/**
+ * 任务受让人
+ *
+ * @author AprilWind
+ */
+@Data
+@NoArgsConstructor
+public class RemoteTaskAssigneeVo implements Serializable {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 总大小
+ */
+ private Long total = 0L;
+
+ /**
+ *
+ */
+ private List list;
+
+ public RemoteTaskAssigneeVo(Long total, List list) {
+ this.total = total;
+ this.list = list;
+ }
+
+ /**
+ * 将源列表转换为 TaskHandler 列表
+ *
+ * @param 通用类型
+ * @param sourceList 待转换的源列表
+ * @param storageId 提取 storageId 的函数
+ * @param handlerCode 提取 handlerCode 的函数
+ * @param handlerName 提取 handlerName 的函数
+ * @param groupName 提取 groupName 的函数
+ * @param createTimeMapper 提取 createTime 的函数
+ * @return 转换后的 TaskHandler 列表
+ */
+ public static List convertToHandlerList(
+ List sourceList,
+ Function storageId,
+ Function handlerCode,
+ Function handlerName,
+ Function groupName,
+ Function createTimeMapper) {
+ return sourceList.stream()
+ .map(item -> new TaskHandler(
+ String.valueOf(storageId.apply(item)),
+ handlerCode.apply(item),
+ handlerName.apply(item),
+ groupName != null ? String.valueOf(groupName.apply(item)) : null,
+ createTimeMapper.apply(item)
+ )).collect(Collectors.toList());
+ }
+
+ @Data
+ @NoArgsConstructor
+ @AllArgsConstructor
+ public static class TaskHandler implements Serializable {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 主键
+ */
+ private String storageId;
+
+ /**
+ * 权限编码
+ */
+ private String handlerCode;
+
+ /**
+ * 权限名称
+ */
+ private String handlerName;
+
+ /**
+ * 权限分组
+ */
+ private String groupName;
+
+ /**
+ * 创建时间
+ */
+ private Date createTime;
+ }
+
+}
diff --git a/ruoyi-api/ruoyi-api-system/src/main/java/org/dromara/system/api/domain/vo/RemoteTenantVo.java b/ruoyi-api/ruoyi-api-system/src/main/java/org/dromara/system/api/domain/vo/RemoteTenantVo.java
new file mode 100644
index 0000000..b9387f9
--- /dev/null
+++ b/ruoyi-api/ruoyi-api-system/src/main/java/org/dromara/system/api/domain/vo/RemoteTenantVo.java
@@ -0,0 +1,91 @@
+package org.dromara.system.api.domain.vo;
+
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.Date;
+
+
+/**
+ * 租户视图对象
+ *
+ * @author zhujie
+ */
+@Data
+public class RemoteTenantVo implements Serializable {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * id
+ */
+ private Long id;
+
+ /**
+ * 租户编号
+ */
+ private String tenantId;
+
+ /**
+ * 联系人
+ */
+ private String contactUserName;
+
+ /**
+ * 联系电话
+ */
+ private String contactPhone;
+
+ /**
+ * 企业名称
+ */
+ private String companyName;
+
+ /**
+ * 统一社会信用代码
+ */
+ private String licenseNumber;
+
+ /**
+ * 地址
+ */
+ private String address;
+
+ /**
+ * 域名
+ */
+ private String domain;
+
+ /**
+ * 企业简介
+ */
+ private String intro;
+
+ /**
+ * 备注
+ */
+ private String remark;
+
+ /**
+ * 租户套餐编号
+ */
+ private Long packageId;
+
+ /**
+ * 过期时间
+ */
+ private Date expireTime;
+
+ /**
+ * 用户数量(-1不限制)
+ */
+ private Long accountCount;
+
+ /**
+ * 租户状态(0正常 1停用)
+ */
+ private String status;
+
+}
diff --git a/ruoyi-api/ruoyi-api-system/src/main/java/org/dromara/system/api/domain/vo/RemoteUserVo.java b/ruoyi-api/ruoyi-api-system/src/main/java/org/dromara/system/api/domain/vo/RemoteUserVo.java
new file mode 100644
index 0000000..fb7f589
--- /dev/null
+++ b/ruoyi-api/ruoyi-api-system/src/main/java/org/dromara/system/api/domain/vo/RemoteUserVo.java
@@ -0,0 +1,73 @@
+package org.dromara.system.api.domain.vo;
+
+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 RemoteUserVo 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/ruoyi-api/ruoyi-api-system/src/main/java/org/dromara/system/api/model/LoginUser.java b/ruoyi-api/ruoyi-api-system/src/main/java/org/dromara/system/api/model/LoginUser.java
new file mode 100644
index 0000000..010ec42
--- /dev/null
+++ b/ruoyi-api/ruoyi-api-system/src/main/java/org/dromara/system/api/model/LoginUser.java
@@ -0,0 +1,151 @@
+package org.dromara.system.api.model;
+
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * 用户信息
+ *
+ * @author ruoyi
+ */
+@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 password;
+
+ /**
+ * 角色对象
+ */
+ private List roles;
+
+ /**
+ * 岗位对象
+ */
+ private List posts;
+
+ /**
+ * 数据权限 当前角色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/ruoyi-api/ruoyi-api-system/src/main/java/org/dromara/system/api/model/PostDTO.java b/ruoyi-api/ruoyi-api-system/src/main/java/org/dromara/system/api/model/PostDTO.java
new file mode 100644
index 0000000..87b3c5a
--- /dev/null
+++ b/ruoyi-api/ruoyi-api-system/src/main/java/org/dromara/system/api/model/PostDTO.java
@@ -0,0 +1,46 @@
+package org.dromara.system.api.model;
+
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * 岗位
+ *
+ * @author AprilWind
+ */
+@Data
+@NoArgsConstructor
+public class PostDTO implements Serializable {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 岗位ID
+ */
+ private Long postId;
+
+ /**
+ * 部门id
+ */
+ private Long deptId;
+
+ /**
+ * 岗位编码
+ */
+ private String postCode;
+
+ /**
+ * 岗位名称
+ */
+ private String postName;
+
+ /**
+ * 岗位类别编码
+ */
+ private String postCategory;
+
+}
diff --git a/ruoyi-api/ruoyi-api-system/src/main/java/org/dromara/system/api/model/RoleDTO.java b/ruoyi-api/ruoyi-api-system/src/main/java/org/dromara/system/api/model/RoleDTO.java
new file mode 100644
index 0000000..4bfd096
--- /dev/null
+++ b/ruoyi-api/ruoyi-api-system/src/main/java/org/dromara/system/api/model/RoleDTO.java
@@ -0,0 +1,42 @@
+package org.dromara.system.api.model;
+
+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:仅本人数据权限 6:部门及以下或本人数据权限)
+ */
+ private String dataScope;
+
+}
diff --git a/ruoyi-api/ruoyi-api-system/src/main/java/org/dromara/system/api/model/XcxLoginUser.java b/ruoyi-api/ruoyi-api-system/src/main/java/org/dromara/system/api/model/XcxLoginUser.java
new file mode 100644
index 0000000..3982e2e
--- /dev/null
+++ b/ruoyi-api/ruoyi-api-system/src/main/java/org/dromara/system/api/model/XcxLoginUser.java
@@ -0,0 +1,27 @@
+package org.dromara.system.api.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/ruoyi-api/ruoyi-api-workflow/pom.xml b/ruoyi-api/ruoyi-api-workflow/pom.xml
new file mode 100644
index 0000000..2d648e1
--- /dev/null
+++ b/ruoyi-api/ruoyi-api-workflow/pom.xml
@@ -0,0 +1,35 @@
+
+
+
+ org.dromara
+ ruoyi-api
+ ${revision}
+
+ 4.0.0
+
+ ruoyi-api-workflow
+
+
+ ruoyi-api-workflow 工作流接口模块
+
+
+
+
+
+
+ org.dromara
+ ruoyi-common-core
+
+
+
+
+ org.dromara
+ ruoyi-common-bus
+ true
+
+
+
+
+
diff --git a/ruoyi-api/ruoyi-api-workflow/src/main/java/org/dromara/workflow/api/RemoteWorkflowService.java b/ruoyi-api/ruoyi-api-workflow/src/main/java/org/dromara/workflow/api/RemoteWorkflowService.java
new file mode 100644
index 0000000..f533381
--- /dev/null
+++ b/ruoyi-api/ruoyi-api-workflow/src/main/java/org/dromara/workflow/api/RemoteWorkflowService.java
@@ -0,0 +1,88 @@
+package org.dromara.workflow.api;
+
+import org.dromara.workflow.api.domain.RemoteCompleteTask;
+import org.dromara.workflow.api.domain.RemoteStartProcess;
+import org.dromara.workflow.api.domain.RemoteStartProcessReturn;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 通用 工作流服务
+ *
+ * @Author ZETA
+ * @Date 2024/6/3
+ */
+public interface RemoteWorkflowService {
+
+ /**
+ * 运行中的实例 删除程实例,删除历史记录,删除业务与流程关联信息
+ *
+ * @param businessIds 业务id
+ * @return 结果
+ */
+ boolean deleteInstance(List businessIds);
+
+ /**
+ * 获取当前流程状态
+ *
+ * @param taskId 任务id
+ * @return 状态
+ */
+ String getBusinessStatusByTaskId(Long taskId);
+
+ /**
+ * 获取当前流程状态
+ *
+ * @param businessId 业务id
+ * @return 状态
+ */
+ String getBusinessStatus(String businessId);
+
+ /**
+ * 设置流程变量
+ *
+ * @param instanceId 流程实例id
+ * @param variable 流程变量
+ */
+ void setVariable(Long instanceId, Map variable);
+
+ /**
+ * 获取流程变量
+ *
+ * @param instanceId 流程实例id
+ */
+ Map instanceVariable(Long instanceId);
+
+ /**
+ * 按照业务id查询流程实例id
+ *
+ * @param businessId 业务id
+ * @return 结果
+ */
+ Long getInstanceIdByBusinessId(String businessId);
+
+ /**
+ * 新增租户流程定义
+ *
+ * @param tenantId 租户id
+ */
+ void syncDef(String tenantId);
+
+ /**
+ * 启动流程
+ *
+ * @param startProcess 参数
+ * @return 结果
+ */
+ RemoteStartProcessReturn startWorkFlow(RemoteStartProcess startProcess);
+
+ /**
+ * 办理任务
+ *
+ * @param completeTask 参数
+ * @return 结果
+ */
+ boolean completeTask(RemoteCompleteTask completeTask);
+
+}
diff --git a/ruoyi-api/ruoyi-api-workflow/src/main/java/org/dromara/workflow/api/RemoteWorkflowServiceMock.java b/ruoyi-api/ruoyi-api-workflow/src/main/java/org/dromara/workflow/api/RemoteWorkflowServiceMock.java
new file mode 100644
index 0000000..7eeb82c
--- /dev/null
+++ b/ruoyi-api/ruoyi-api-workflow/src/main/java/org/dromara/workflow/api/RemoteWorkflowServiceMock.java
@@ -0,0 +1,70 @@
+package org.dromara.workflow.api;
+
+import lombok.extern.slf4j.Slf4j;
+import org.dromara.workflow.api.domain.RemoteCompleteTask;
+import org.dromara.workflow.api.domain.RemoteStartProcess;
+import org.dromara.workflow.api.domain.RemoteStartProcessReturn;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 工作流服务(降级处理)
+ *
+ * @author Lion Li
+ */
+@Slf4j
+public class RemoteWorkflowServiceMock implements RemoteWorkflowService {
+
+ @Override
+ public boolean deleteInstance(List businessIds) {
+ log.warn("服务调用异常 -> 降级处理");
+ return false;
+ }
+
+ @Override
+ public String getBusinessStatusByTaskId(Long taskId) {
+ log.warn("服务调用异常 -> 降级处理");
+ return null;
+ }
+
+ @Override
+ public String getBusinessStatus(String businessId) {
+ log.warn("服务调用异常 -> 降级处理");
+ return null;
+ }
+
+ @Override
+ public void setVariable(Long instanceId, Map variable) {
+ log.warn("服务调用异常 -> 降级处理");
+ }
+
+ @Override
+ public Map instanceVariable(Long instanceId) {
+ log.warn("服务调用异常 -> 降级处理");
+ return null;
+ }
+
+ @Override
+ public Long getInstanceIdByBusinessId(String businessId) {
+ log.warn("服务调用异常 -> 降级处理");
+ return null;
+ }
+
+ @Override
+ public void syncDef(String tenantId) {
+ log.warn("服务调用异常 -> 降级处理");
+ }
+
+ @Override
+ public RemoteStartProcessReturn startWorkFlow(RemoteStartProcess startProcess) {
+ log.warn("服务调用异常 -> 降级处理");
+ return null;
+ }
+
+ @Override
+ public boolean completeTask(RemoteCompleteTask completeTask) {
+ log.warn("服务调用异常 -> 降级处理");
+ return false;
+ }
+}
diff --git a/ruoyi-api/ruoyi-api-workflow/src/main/java/org/dromara/workflow/api/domain/RemoteCompleteTask.java b/ruoyi-api/ruoyi-api-workflow/src/main/java/org/dromara/workflow/api/domain/RemoteCompleteTask.java
new file mode 100644
index 0000000..aa21a3f
--- /dev/null
+++ b/ruoyi-api/ruoyi-api-workflow/src/main/java/org/dromara/workflow/api/domain/RemoteCompleteTask.java
@@ -0,0 +1,71 @@
+package org.dromara.workflow.api.domain;
+
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * 办理任务请求对象
+ *
+ * @author may
+ */
+@Data
+public class RemoteCompleteTask implements Serializable {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 任务id
+ */
+ private Long taskId;
+
+ /**
+ * 附件id
+ */
+ private String fileId;
+
+ /**
+ * 抄送人员
+ */
+ private List flowCopyList;
+
+ /**
+ * 消息类型
+ */
+ private List messageType;
+
+ /**
+ * 办理意见
+ */
+ private String message;
+
+ /**
+ * 消息通知
+ */
+ private String notice;
+
+ /**
+ * 流程变量
+ */
+ private Map variables;
+
+ /**
+ * 扩展变量(此处为逗号分隔的ossId)
+ */
+ private String ext;
+
+ public Map getVariables() {
+ if (variables == null) {
+ return new HashMap<>(16);
+ }
+ variables.entrySet().removeIf(entry -> Objects.isNull(entry.getValue()));
+ return variables;
+ }
+
+}
diff --git a/ruoyi-api/ruoyi-api-workflow/src/main/java/org/dromara/workflow/api/domain/RemoteFlowCopy.java b/ruoyi-api/ruoyi-api-workflow/src/main/java/org/dromara/workflow/api/domain/RemoteFlowCopy.java
new file mode 100644
index 0000000..135b70f
--- /dev/null
+++ b/ruoyi-api/ruoyi-api-workflow/src/main/java/org/dromara/workflow/api/domain/RemoteFlowCopy.java
@@ -0,0 +1,30 @@
+package org.dromara.workflow.api.domain;
+
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+
+/**
+ * 抄送
+ *
+ * @author may
+ */
+@Data
+public class RemoteFlowCopy implements Serializable {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 用户id
+ */
+ private Long userId;
+
+ /**
+ * 用户名称
+ */
+ private String userName;
+
+}
diff --git a/ruoyi-api/ruoyi-api-workflow/src/main/java/org/dromara/workflow/api/domain/RemoteStartProcess.java b/ruoyi-api/ruoyi-api-workflow/src/main/java/org/dromara/workflow/api/domain/RemoteStartProcess.java
new file mode 100644
index 0000000..f7f12a5
--- /dev/null
+++ b/ruoyi-api/ruoyi-api-workflow/src/main/java/org/dromara/workflow/api/domain/RemoteStartProcess.java
@@ -0,0 +1,45 @@
+package org.dromara.workflow.api.domain;
+
+
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * 启动流程对象
+ *
+ * @author may
+ */
+@Data
+public class RemoteStartProcess implements Serializable {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 业务唯一值id
+ */
+ private String businessId;
+
+ /**
+ * 流程定义编码
+ */
+ private String flowCode;
+
+ /**
+ * 流程变量,前端会提交一个元素{'entity': {业务详情数据对象}}
+ */
+ private Map variables;
+
+ public Map getVariables() {
+ if (variables == null) {
+ return new HashMap<>(16);
+ }
+ variables.entrySet().removeIf(entry -> Objects.isNull(entry.getValue()));
+ return variables;
+ }
+}
diff --git a/ruoyi-api/ruoyi-api-workflow/src/main/java/org/dromara/workflow/api/domain/RemoteStartProcessReturn.java b/ruoyi-api/ruoyi-api-workflow/src/main/java/org/dromara/workflow/api/domain/RemoteStartProcessReturn.java
new file mode 100644
index 0000000..c989bc2
--- /dev/null
+++ b/ruoyi-api/ruoyi-api-workflow/src/main/java/org/dromara/workflow/api/domain/RemoteStartProcessReturn.java
@@ -0,0 +1,30 @@
+package org.dromara.workflow.api.domain;
+
+
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * 启动流程返回对象
+ *
+ * @author Lion Li
+ */
+@Data
+public class RemoteStartProcessReturn implements Serializable {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 流程实例id
+ */
+ private Long processInstanceId;
+
+ /**
+ * 任务id
+ */
+ private Long taskId;
+
+}
diff --git a/ruoyi-api/ruoyi-api-workflow/src/main/java/org/dromara/workflow/api/event/ProcessDeleteEvent.java b/ruoyi-api/ruoyi-api-workflow/src/main/java/org/dromara/workflow/api/event/ProcessDeleteEvent.java
new file mode 100644
index 0000000..4e3ead9
--- /dev/null
+++ b/ruoyi-api/ruoyi-api-workflow/src/main/java/org/dromara/workflow/api/event/ProcessDeleteEvent.java
@@ -0,0 +1,41 @@
+package org.dromara.workflow.api.event;
+
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.dromara.common.core.utils.SpringUtils;
+import org.springframework.cloud.bus.event.RemoteApplicationEvent;
+
+import java.io.Serial;
+
+/**
+ * 删除流程监听
+ *
+ * @author AprilWind
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class ProcessDeleteEvent extends RemoteApplicationEvent {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 租户ID
+ */
+ private String tenantId;
+
+ /**
+ * 流程定义编码
+ */
+ private String flowCode;
+
+ /**
+ * 业务id
+ */
+ private String businessId;
+
+ public ProcessDeleteEvent() {
+ super(new Object(), SpringUtils.getApplicationName(), DEFAULT_DESTINATION_FACTORY.getDestination(null));
+ }
+
+}
diff --git a/ruoyi-api/ruoyi-api-workflow/src/main/java/org/dromara/workflow/api/event/ProcessEvent.java b/ruoyi-api/ruoyi-api-workflow/src/main/java/org/dromara/workflow/api/event/ProcessEvent.java
new file mode 100644
index 0000000..937a931
--- /dev/null
+++ b/ruoyi-api/ruoyi-api-workflow/src/main/java/org/dromara/workflow/api/event/ProcessEvent.java
@@ -0,0 +1,72 @@
+package org.dromara.workflow.api.event;
+
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.dromara.common.core.utils.SpringUtils;
+import org.springframework.cloud.bus.event.RemoteApplicationEvent;
+
+import java.io.Serial;
+import java.util.Map;
+
+/**
+ * 总体流程监听
+ *
+ * @author may
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class ProcessEvent extends RemoteApplicationEvent {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 租户ID
+ */
+ private String tenantId;
+
+ /**
+ * 流程定义编码
+ */
+ private String flowCode;
+
+ /**
+ * 业务id
+ */
+ private String businessId;
+
+ /**
+ * 节点类型(0开始节点 1中间节点 2结束节点 3互斥网关 4并行网关)
+ */
+ private Integer nodeType;
+
+ /**
+ * 流程节点编码
+ */
+ private String nodeCode;
+
+ /**
+ * 流程节点名称
+ */
+ private String nodeName;
+
+ /**
+ * 流程状态
+ */
+ private String status;
+
+ /**
+ * 办理参数
+ */
+ private Map params;
+
+ /**
+ * 当为true时为申请人节点办理
+ */
+ private Boolean submit;
+
+ public ProcessEvent() {
+ super(new Object(), SpringUtils.getApplicationName(), DEFAULT_DESTINATION_FACTORY.getDestination(null));
+ }
+
+}
diff --git a/ruoyi-api/ruoyi-api-workflow/src/main/java/org/dromara/workflow/api/event/ProcessTaskEvent.java b/ruoyi-api/ruoyi-api-workflow/src/main/java/org/dromara/workflow/api/event/ProcessTaskEvent.java
new file mode 100644
index 0000000..f53c8de
--- /dev/null
+++ b/ruoyi-api/ruoyi-api-workflow/src/main/java/org/dromara/workflow/api/event/ProcessTaskEvent.java
@@ -0,0 +1,65 @@
+package org.dromara.workflow.api.event;
+
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.dromara.common.core.utils.SpringUtils;
+import org.springframework.cloud.bus.event.RemoteApplicationEvent;
+
+import java.io.Serial;
+
+/**
+ * 流程任务监听
+ *
+ * @author may
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class ProcessTaskEvent extends RemoteApplicationEvent {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 租户ID
+ */
+ private String tenantId;
+
+ /**
+ * 流程定义编码
+ */
+ private String flowCode;
+
+ /**
+ * 节点类型(0开始节点 1中间节点 2结束节点 3互斥网关 4并行网关)
+ */
+ private Integer nodeType;
+
+ /**
+ * 流程节点编码
+ */
+ private String nodeCode;
+
+ /**
+ * 流程节点名称
+ */
+ private String nodeName;
+
+ /**
+ * 任务id
+ */
+ private Long taskId;
+
+ /**
+ * 业务id
+ */
+ private String businessId;
+
+ /**
+ * 流程状态
+ */
+ private String status;
+
+ public ProcessTaskEvent() {
+ super(new Object(), SpringUtils.getApplicationName(), DEFAULT_DESTINATION_FACTORY.getDestination(null));
+ }
+}
diff --git a/ruoyi-auth/Dockerfile b/ruoyi-auth/Dockerfile
new file mode 100644
index 0000000..c24c9e8
--- /dev/null
+++ b/ruoyi-auth/Dockerfile
@@ -0,0 +1,27 @@
+# 贝尔实验室 Spring 官方推荐镜像 JDK下载地址 https://bell-sw.com/pages/downloads/
+FROM bellsoft/liberica-openjdk-rocky:17.0.15-cds
+#FROM bellsoft/liberica-openjdk-rocky:21.0.7-cds
+#FROM findepi/graalvm:java17-native
+
+LABEL maintainer="Lion Li"
+
+RUN mkdir -p /ruoyi/auth/logs \
+ /ruoyi/auth/temp \
+ /ruoyi/skywalking/agent
+
+WORKDIR /ruoyi/auth
+
+ENV SERVER_PORT=9210 LANG=C.UTF-8 LC_ALL=C.UTF-8 JAVA_OPTS=""
+
+EXPOSE ${SERVER_PORT}
+
+ADD ./target/ruoyi-auth.jar ./app.jar
+
+SHELL ["/bin/bash", "-c"]
+
+ENTRYPOINT java -Djava.security.egd=file:/dev/./urandom -Dserver.port=${SERVER_PORT} \
+ #-Dskywalking.agent.service_name=ruoyi-auth \
+ #-javaagent:/ruoyi/skywalking/agent/skywalking-agent.jar \
+ -XX:+HeapDumpOnOutOfMemoryError -XX:+UseZGC ${JAVA_OPTS} \
+ -jar app.jar
+
diff --git a/ruoyi-auth/pom.xml b/ruoyi-auth/pom.xml
new file mode 100644
index 0000000..6b685a9
--- /dev/null
+++ b/ruoyi-auth/pom.xml
@@ -0,0 +1,139 @@
+
+
+ org.dromara
+ xny-cloud
+ ${revision}
+
+ 4.0.0
+
+ ruoyi-auth
+
+
+ ruoyi-auth 认证授权中心
+
+
+
+
+
+ org.dromara
+ ruoyi-common-nacos
+
+
+
+ cn.hutool
+ hutool-captcha
+
+
+
+ org.dromara
+ ruoyi-common-sentinel
+
+
+
+
+ org.dromara
+ ruoyi-common-security
+
+
+
+ org.dromara
+ ruoyi-common-social
+
+
+
+
+ org.dromara
+ ruoyi-common-log
+
+
+
+ org.dromara
+ ruoyi-common-doc
+
+
+
+ org.dromara
+ ruoyi-common-web
+
+
+
+ org.dromara
+ ruoyi-common-ratelimiter
+
+
+
+ org.dromara
+ ruoyi-common-encrypt
+
+
+
+ org.dromara
+ ruoyi-common-dubbo
+
+
+
+ org.dromara
+ ruoyi-common-seata
+
+
+ org.dromara
+ ruoyi-common-tenant
+
+
+
+ org.dromara
+ ruoyi-common-service-impl
+
+
+
+ org.dromara
+ ruoyi-api-resource
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ${project.artifactId}
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+ ${spring-boot.version}
+
+
+
+ repackage
+
+
+
+
+
+
+
+
diff --git a/ruoyi-auth/src/main/java/org/dromara/auth/RuoYiAuthApplication.java b/ruoyi-auth/src/main/java/org/dromara/auth/RuoYiAuthApplication.java
new file mode 100644
index 0000000..12d4d63
--- /dev/null
+++ b/ruoyi-auth/src/main/java/org/dromara/auth/RuoYiAuthApplication.java
@@ -0,0 +1,23 @@
+package org.dromara.auth;
+
+import org.apache.dubbo.config.spring.context.annotation.EnableDubbo;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
+import org.springframework.boot.context.metrics.buffering.BufferingApplicationStartup;
+
+/**
+ * 认证授权中心
+ *
+ * @author ruoyi
+ */
+@EnableDubbo
+@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
+public class RuoYiAuthApplication {
+ public static void main(String[] args) {
+ SpringApplication application = new SpringApplication(RuoYiAuthApplication.class);
+ application.setApplicationStartup(new BufferingApplicationStartup(2048));
+ application.run(args);
+ System.out.println("(♥◠‿◠)ノ゙ 认证授权中心启动成功 ლ(´ڡ`ლ)゙ ");
+ }
+}
diff --git a/ruoyi-auth/src/main/java/org/dromara/auth/captcha/UnsignedMathGenerator.java b/ruoyi-auth/src/main/java/org/dromara/auth/captcha/UnsignedMathGenerator.java
new file mode 100644
index 0000000..feb4cdf
--- /dev/null
+++ b/ruoyi-auth/src/main/java/org/dromara/auth/captcha/UnsignedMathGenerator.java
@@ -0,0 +1,88 @@
+package org.dromara.auth.captcha;
+
+import cn.hutool.captcha.generator.CodeGenerator;
+import cn.hutool.core.math.Calculator;
+import cn.hutool.core.util.CharUtil;
+import cn.hutool.core.util.RandomUtil;
+import org.dromara.common.core.utils.StringUtils;
+
+import java.io.Serial;
+
+/**
+ * 无符号计算生成器
+ *
+ * @author Lion Li
+ */
+public class UnsignedMathGenerator implements CodeGenerator {
+
+ @Serial
+ private static final long serialVersionUID = -5514819971774091076L;
+
+ private static final String OPERATORS = "+-*";
+
+ /**
+ * 参与计算数字最大长度
+ */
+ private final int numberLength;
+
+ /**
+ * 构造
+ */
+ public UnsignedMathGenerator() {
+ this(2);
+ }
+
+ /**
+ * 构造
+ *
+ * @param numberLength 参与计算最大数字位数
+ */
+ public UnsignedMathGenerator(int numberLength) {
+ this.numberLength = numberLength;
+ }
+
+ @Override
+ public String generate() {
+ final int limit = getLimit();
+ int a = RandomUtil.randomInt(limit);
+ int b = RandomUtil.randomInt(limit);
+ String max = Integer.toString(Math.max(a,b));
+ String min = Integer.toString(Math.min(a,b));
+ max = StringUtils.rightPad(max, this.numberLength, CharUtil.SPACE);
+ min = StringUtils.rightPad(min, this.numberLength, CharUtil.SPACE);
+
+ return max + RandomUtil.randomChar(OPERATORS) + min + '=';
+ }
+
+ @Override
+ public boolean verify(String code, String userInputCode) {
+ int result;
+ try {
+ result = Integer.parseInt(userInputCode);
+ } catch (NumberFormatException e) {
+ // 用户输入非数字
+ return false;
+ }
+
+ final int calculateResult = (int) Calculator.conversion(code);
+ return result == calculateResult;
+ }
+
+ /**
+ * 获取验证码长度
+ *
+ * @return 验证码长度
+ */
+ public int getLength() {
+ return this.numberLength * 2 + 2;
+ }
+
+ /**
+ * 根据长度获取参与计算数字最大值
+ *
+ * @return 最大值
+ */
+ private int getLimit() {
+ return Integer.parseInt("1" + StringUtils.repeat('0', this.numberLength));
+ }
+}
diff --git a/ruoyi-auth/src/main/java/org/dromara/auth/config/CaptchaConfig.java b/ruoyi-auth/src/main/java/org/dromara/auth/config/CaptchaConfig.java
new file mode 100644
index 0000000..e9804a0
--- /dev/null
+++ b/ruoyi-auth/src/main/java/org/dromara/auth/config/CaptchaConfig.java
@@ -0,0 +1,62 @@
+package org.dromara.auth.config;
+
+import cn.hutool.captcha.CaptchaUtil;
+import cn.hutool.captcha.CircleCaptcha;
+import cn.hutool.captcha.LineCaptcha;
+import cn.hutool.captcha.ShearCaptcha;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Lazy;
+
+import java.awt.*;
+
+/**
+ * 验证码配置
+ *
+ * @author Lion Li
+ */
+@Configuration
+public class CaptchaConfig {
+
+ private static final int WIDTH = 160;
+ private static final int HEIGHT = 60;
+ private static final Color BACKGROUND = Color.LIGHT_GRAY;
+ private static final Font FONT = new Font("Arial", Font.BOLD, 48);
+
+ /**
+ * 圆圈干扰验证码
+ */
+ @Lazy
+ @Bean
+ public CircleCaptcha circleCaptcha() {
+ CircleCaptcha captcha = CaptchaUtil.createCircleCaptcha(WIDTH, HEIGHT);
+ captcha.setBackground(BACKGROUND);
+ captcha.setFont(FONT);
+ return captcha;
+ }
+
+ /**
+ * 线段干扰的验证码
+ */
+ @Lazy
+ @Bean
+ public LineCaptcha lineCaptcha() {
+ LineCaptcha captcha = CaptchaUtil.createLineCaptcha(WIDTH, HEIGHT);
+ captcha.setBackground(BACKGROUND);
+ captcha.setFont(FONT);
+ return captcha;
+ }
+
+ /**
+ * 扭曲干扰验证码
+ */
+ @Lazy
+ @Bean
+ public ShearCaptcha shearCaptcha() {
+ ShearCaptcha captcha = CaptchaUtil.createShearCaptcha(WIDTH, HEIGHT);
+ captcha.setBackground(BACKGROUND);
+ captcha.setFont(FONT);
+ return captcha;
+ }
+
+}
diff --git a/ruoyi-auth/src/main/java/org/dromara/auth/controller/CaptchaController.java b/ruoyi-auth/src/main/java/org/dromara/auth/controller/CaptchaController.java
new file mode 100644
index 0000000..3a2476a
--- /dev/null
+++ b/ruoyi-auth/src/main/java/org/dromara/auth/controller/CaptchaController.java
@@ -0,0 +1,87 @@
+package org.dromara.auth.controller;
+
+import cn.hutool.captcha.AbstractCaptcha;
+import cn.hutool.captcha.generator.CodeGenerator;
+import cn.hutool.core.util.IdUtil;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.dromara.auth.domain.vo.CaptchaVo;
+import org.dromara.auth.enums.CaptchaType;
+import org.dromara.auth.properties.CaptchaProperties;
+import org.dromara.common.core.constant.Constants;
+import org.dromara.common.core.constant.GlobalConstants;
+import org.dromara.common.core.domain.R;
+import org.dromara.common.core.utils.SpringUtils;
+import org.dromara.common.core.utils.StringUtils;
+import org.dromara.common.core.utils.reflect.ReflectUtils;
+import org.dromara.common.ratelimiter.annotation.RateLimiter;
+import org.dromara.common.ratelimiter.enums.LimitType;
+import org.dromara.common.redis.utils.RedisUtils;
+import org.springframework.expression.Expression;
+import org.springframework.expression.ExpressionParser;
+import org.springframework.expression.spel.standard.SpelExpressionParser;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.time.Duration;
+
+/**
+ * 验证码操作处理
+ *
+ * @author Lion Li
+ */
+@Slf4j
+@Validated
+@RequiredArgsConstructor
+@RestController
+public class CaptchaController {
+
+ private final CaptchaProperties captchaProperties;
+
+ /**
+ * 生成验证码
+ */
+ @GetMapping("/code")
+ public R getCode() {
+ CaptchaVo captchaVo = new CaptchaVo();
+ boolean captchaEnabled = captchaProperties.getEnabled();
+ if (!captchaEnabled) {
+ captchaVo.setCaptchaEnabled(false);
+ return R.ok(captchaVo);
+ }
+ return R.ok(SpringUtils.getAopProxy(this).getCodeImpl());
+ }
+
+ /**
+ * 生成验证码
+ * 独立方法避免验证码关闭之后仍然走限流
+ */
+ @RateLimiter(time = 60, count = 10, limitType = LimitType.IP)
+ public CaptchaVo getCodeImpl() {
+ // 保存验证码信息
+ String uuid = IdUtil.simpleUUID();
+ String verifyKey = GlobalConstants.CAPTCHA_CODE_KEY + uuid;
+ // 生成验证码
+ CaptchaType captchaType = captchaProperties.getType();
+ boolean isMath = CaptchaType.MATH == captchaType;
+ Integer length = isMath ? captchaProperties.getNumberLength() : captchaProperties.getCharLength();
+ CodeGenerator codeGenerator = ReflectUtils.newInstance(captchaType.getClazz(), length);
+ AbstractCaptcha captcha = SpringUtils.getBean(captchaProperties.getCategory().getClazz());
+ captcha.setGenerator(codeGenerator);
+ captcha.createCode();
+ // 如果是数学验证码,使用SpEL表达式处理验证码结果
+ String code = captcha.getCode();
+ if (isMath) {
+ ExpressionParser parser = new SpelExpressionParser();
+ Expression exp = parser.parseExpression(StringUtils.remove(code, "="));
+ code = exp.getValue(String.class);
+ }
+ RedisUtils.setCacheObject(verifyKey, code, Duration.ofMinutes(Constants.CAPTCHA_EXPIRATION));
+ CaptchaVo captchaVo = new CaptchaVo();
+ captchaVo.setUuid(uuid);
+ captchaVo.setImg(captcha.getImageBase64());
+ return captchaVo;
+ }
+
+}
diff --git a/ruoyi-auth/src/main/java/org/dromara/auth/controller/TokenController.java b/ruoyi-auth/src/main/java/org/dromara/auth/controller/TokenController.java
new file mode 100644
index 0000000..416f7a4
--- /dev/null
+++ b/ruoyi-auth/src/main/java/org/dromara/auth/controller/TokenController.java
@@ -0,0 +1,231 @@
+package org.dromara.auth.controller;
+
+import cn.dev33.satoken.exception.NotLoginException;
+import cn.hutool.core.codec.Base64;
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.ObjectUtil;
+import jakarta.servlet.http.HttpServletRequest;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import me.zhyd.oauth.model.AuthResponse;
+import me.zhyd.oauth.model.AuthUser;
+import me.zhyd.oauth.request.AuthRequest;
+import me.zhyd.oauth.utils.AuthStateUtils;
+import org.apache.dubbo.config.annotation.DubboReference;
+import org.dromara.auth.domain.vo.LoginTenantVo;
+import org.dromara.auth.domain.vo.LoginVo;
+import org.dromara.auth.domain.vo.TenantListVo;
+import org.dromara.auth.form.RegisterBody;
+import org.dromara.auth.form.SocialLoginBody;
+import org.dromara.auth.service.IAuthStrategy;
+import org.dromara.auth.service.SysLoginService;
+import org.dromara.common.core.constant.SystemConstants;
+import org.dromara.common.core.domain.R;
+import org.dromara.common.core.domain.model.LoginBody;
+import org.dromara.common.core.utils.*;
+import org.dromara.common.encrypt.annotation.ApiEncrypt;
+import org.dromara.common.json.utils.JsonUtils;
+import org.dromara.common.satoken.utils.LoginHelper;
+import org.dromara.common.social.config.properties.SocialLoginConfigProperties;
+import org.dromara.common.social.config.properties.SocialProperties;
+import org.dromara.common.social.utils.SocialUtils;
+import org.dromara.common.tenant.helper.TenantHelper;
+import org.dromara.resource.api.RemoteMessageService;
+import org.dromara.system.api.RemoteClientService;
+import org.dromara.system.api.RemoteConfigService;
+import org.dromara.system.api.RemoteSocialService;
+import org.dromara.system.api.RemoteTenantService;
+import org.dromara.system.api.domain.vo.RemoteClientVo;
+import org.dromara.system.api.domain.vo.RemoteTenantVo;
+import org.springframework.web.bind.annotation.*;
+
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * token 控制
+ *
+ * @author Lion Li
+ */
+@Slf4j
+@RequiredArgsConstructor
+@RestController
+public class TokenController {
+
+ private final SocialProperties socialProperties;
+ private final SysLoginService sysLoginService;
+ private final ScheduledExecutorService scheduledExecutorService;
+
+ @DubboReference
+ private final RemoteConfigService remoteConfigService;
+ @DubboReference
+ private final RemoteTenantService remoteTenantService;
+ @DubboReference
+ private final RemoteClientService remoteClientService;
+ @DubboReference
+ private final RemoteSocialService remoteSocialService;
+ @DubboReference(stub = "true")
+ private final RemoteMessageService remoteMessageService;
+
+ /**
+ * 登录方法
+ *
+ * @param body 登录信息
+ * @return 结果
+ */
+ @ApiEncrypt
+ @PostMapping("/login")
+ public R login(@RequestBody String body) {
+ LoginBody loginBody = JsonUtils.parseObject(body, LoginBody.class);
+ ValidatorUtils.validate(loginBody);
+ // 授权类型和客户端id
+ String clientId = loginBody.getClientId();
+ String grantType = loginBody.getGrantType();
+ RemoteClientVo clientVo = remoteClientService.queryByClientId(clientId);
+
+ // 查询不到 client 或 client 内不包含 grantType
+ if (ObjectUtil.isNull(clientVo) || !StringUtils.contains(clientVo.getGrantType(), grantType)) {
+ log.info("客户端id: {} 认证类型:{} 异常!.", clientId, grantType);
+ return R.fail(MessageUtils.message("auth.grant.type.error"));
+ } else if (!SystemConstants.NORMAL.equals(clientVo.getStatus())) {
+ return R.fail(MessageUtils.message("auth.grant.type.blocked"));
+ }
+ // 校验租户
+ sysLoginService.checkTenant(loginBody.getTenantId());
+ // 登录
+ LoginVo loginVo = IAuthStrategy.login(body, clientVo, grantType);
+
+ Long userId = LoginHelper.getUserId();
+ scheduledExecutorService.schedule(() -> {
+ remoteMessageService.publishMessage(List.of(userId), "欢迎登录RuoYi-Cloud-Plus微服务管理系统");
+ }, 5, TimeUnit.SECONDS);
+ return R.ok(loginVo);
+ }
+
+ /**
+ * 第三方登录请求
+ *
+ * @param source 登录来源
+ * @return 结果
+ */
+ @GetMapping("/binding/{source}")
+ public R authBinding(@PathVariable("source") String source,
+ @RequestParam String tenantId, @RequestParam String domain) {
+ SocialLoginConfigProperties obj = socialProperties.getType().get(source);
+ if (ObjectUtil.isNull(obj)) {
+ return R.fail(source + "平台账号暂不支持");
+ }
+ AuthRequest authRequest = SocialUtils.getAuthRequest(source, socialProperties);
+ Map map = new HashMap<>();
+ map.put("tenantId", tenantId);
+ map.put("domain", domain);
+ map.put("state", AuthStateUtils.createState());
+ String authorizeUrl = authRequest.authorize(Base64.encode(JsonUtils.toJsonString(map), StandardCharsets.UTF_8));
+ return R.ok("操作成功", authorizeUrl);
+ }
+
+ /**
+ * 第三方登录回调业务处理 绑定授权
+ *
+ * @param loginBody 请求体
+ * @return 结果
+ */
+ @PostMapping("/social/callback")
+ public R socialCallback(@RequestBody SocialLoginBody loginBody) {
+ // 获取第三方登录信息
+ AuthResponse response = SocialUtils.loginAuth(
+ loginBody.getSource(), loginBody.getSocialCode(),
+ loginBody.getSocialState(), socialProperties);
+ AuthUser authUserData = response.getData();
+ // 判断授权响应是否成功
+ if (!response.ok()) {
+ return R.fail(response.getMsg());
+ }
+ sysLoginService.socialRegister(authUserData);
+ return R.ok();
+ }
+
+
+ /**
+ * 取消授权
+ *
+ * @param socialId socialId
+ */
+ @DeleteMapping(value = "/unlock/{socialId}")
+ public R unlockSocial(@PathVariable Long socialId) {
+ Boolean rows = remoteSocialService.deleteWithValidById(socialId);
+ return rows ? R.ok() : R.fail("取消授权失败");
+ }
+
+ /**
+ * 登出方法
+ */
+ @PostMapping("logout")
+ public R logout() {
+ sysLoginService.logout();
+ return R.ok();
+ }
+
+ /**
+ * 用户注册
+ */
+ @ApiEncrypt
+ @PostMapping("register")
+ public R register(@RequestBody RegisterBody registerBody) {
+ if (!remoteConfigService.selectRegisterEnabled(registerBody.getTenantId())) {
+ return R.fail("当前系统没有开启注册功能!");
+ }
+ // 用户注册
+ sysLoginService.register(registerBody);
+ return R.ok();
+ }
+
+ /**
+ * 登录页面租户下拉框
+ *
+ * @return 租户列表
+ */
+ @GetMapping("/tenant/list")
+ public R tenantList(HttpServletRequest request) throws Exception {
+ // 返回对象
+ LoginTenantVo result = new LoginTenantVo();
+ boolean enable = TenantHelper.isEnable();
+ result.setTenantEnabled(enable);
+ // 如果未开启租户这直接返回
+ if (!enable) {
+ return R.ok(result);
+ }
+
+ List tenantList = remoteTenantService.queryList();
+ List voList = MapstructUtils.convert(tenantList, TenantListVo.class);
+ try {
+ // 如果只超管返回所有租户
+ if (LoginHelper.isSuperAdmin()) {
+ result.setVoList(voList);
+ return R.ok(result);
+ }
+ } catch (NotLoginException ignored) {
+ }
+
+ // 获取域名
+ String host;
+ String referer = request.getHeader("referer");
+ if (StringUtils.isNotBlank(referer)) {
+ // 这里从referer中取值是为了本地使用hosts添加虚拟域名,方便本地环境调试
+ host = referer.split("//")[1].split("/")[0];
+ } else {
+ host = new URL(request.getRequestURL().toString()).getHost();
+ }
+ // 根据域名进行筛选
+ List list = StreamUtils.filter(voList, vo ->
+ StringUtils.equalsIgnoreCase(vo.getDomain(), host));
+ result.setVoList(CollUtil.isNotEmpty(list) ? list : voList);
+ return R.ok(result);
+ }
+
+}
diff --git a/ruoyi-auth/src/main/java/org/dromara/auth/domain/convert/TenantVoConvert.java b/ruoyi-auth/src/main/java/org/dromara/auth/domain/convert/TenantVoConvert.java
new file mode 100644
index 0000000..7888f2c
--- /dev/null
+++ b/ruoyi-auth/src/main/java/org/dromara/auth/domain/convert/TenantVoConvert.java
@@ -0,0 +1,16 @@
+package org.dromara.auth.domain.convert;
+
+import io.github.linpeilie.BaseMapper;
+import org.dromara.auth.domain.vo.TenantListVo;
+import org.dromara.system.api.domain.vo.RemoteTenantVo;
+import org.mapstruct.Mapper;
+import org.mapstruct.MappingConstants;
+
+/**
+ * 租户vo转换器
+ * @author zhujie
+ */
+@Mapper(componentModel = MappingConstants.ComponentModel.SPRING)
+public interface TenantVoConvert extends BaseMapper {
+
+}
diff --git a/ruoyi-auth/src/main/java/org/dromara/auth/domain/vo/CaptchaVo.java b/ruoyi-auth/src/main/java/org/dromara/auth/domain/vo/CaptchaVo.java
new file mode 100644
index 0000000..2a4c0bd
--- /dev/null
+++ b/ruoyi-auth/src/main/java/org/dromara/auth/domain/vo/CaptchaVo.java
@@ -0,0 +1,25 @@
+package org.dromara.auth.domain.vo;
+
+import lombok.Data;
+
+/**
+ * 验证码信息
+ *
+ * @author Michelle.Chung
+ */
+@Data
+public class CaptchaVo {
+
+ /**
+ * 是否开启验证码
+ */
+ private Boolean captchaEnabled = true;
+
+ private String uuid;
+
+ /**
+ * 验证码图片
+ */
+ private String img;
+
+}
diff --git a/ruoyi-auth/src/main/java/org/dromara/auth/domain/vo/LoginTenantVo.java b/ruoyi-auth/src/main/java/org/dromara/auth/domain/vo/LoginTenantVo.java
new file mode 100644
index 0000000..fcfdcc3
--- /dev/null
+++ b/ruoyi-auth/src/main/java/org/dromara/auth/domain/vo/LoginTenantVo.java
@@ -0,0 +1,25 @@
+package org.dromara.auth.domain.vo;
+
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * 登录租户对象
+ *
+ * @author Michelle.Chung
+ */
+@Data
+public class LoginTenantVo {
+
+ /**
+ * 租户开关
+ */
+ private Boolean tenantEnabled;
+
+ /**
+ * 租户对象列表
+ */
+ private List voList;
+
+}
diff --git a/ruoyi-auth/src/main/java/org/dromara/auth/domain/vo/LoginVo.java b/ruoyi-auth/src/main/java/org/dromara/auth/domain/vo/LoginVo.java
new file mode 100644
index 0000000..e4bea14
--- /dev/null
+++ b/ruoyi-auth/src/main/java/org/dromara/auth/domain/vo/LoginVo.java
@@ -0,0 +1,54 @@
+package org.dromara.auth.domain.vo;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+/**
+ * 登录验证信息
+ *
+ * @author Michelle.Chung
+ */
+@Data
+public class LoginVo {
+
+ /**
+ * 授权令牌
+ */
+ @JsonProperty("access_token")
+ private String accessToken;
+
+ /**
+ * 刷新令牌
+ */
+ @JsonProperty("refresh_token")
+ private String refreshToken;
+
+ /**
+ * 授权令牌 access_token 的有效期
+ */
+ @JsonProperty("expire_in")
+ private Long expireIn;
+
+ /**
+ * 刷新令牌 refresh_token 的有效期
+ */
+ @JsonProperty("refresh_expire_in")
+ private Long refreshExpireIn;
+
+ /**
+ * 应用id
+ */
+ @JsonProperty("client_id")
+ private String clientId;
+
+ /**
+ * 令牌权限
+ */
+ private String scope;
+
+ /**
+ * 用户 openid
+ */
+ private String openid;
+
+}
diff --git a/ruoyi-auth/src/main/java/org/dromara/auth/domain/vo/TenantListVo.java b/ruoyi-auth/src/main/java/org/dromara/auth/domain/vo/TenantListVo.java
new file mode 100644
index 0000000..523e0f8
--- /dev/null
+++ b/ruoyi-auth/src/main/java/org/dromara/auth/domain/vo/TenantListVo.java
@@ -0,0 +1,28 @@
+package org.dromara.auth.domain.vo;
+
+import lombok.Data;
+
+/**
+ * 租户列表
+ *
+ * @author zhujie
+ */
+@Data
+public class TenantListVo {
+
+ /**
+ * 租户编号
+ */
+ private String tenantId;
+
+ /**
+ * 企业名称
+ */
+ private String companyName;
+
+ /**
+ * 域名
+ */
+ private String domain;
+
+}
diff --git a/ruoyi-auth/src/main/java/org/dromara/auth/enums/CaptchaCategory.java b/ruoyi-auth/src/main/java/org/dromara/auth/enums/CaptchaCategory.java
new file mode 100644
index 0000000..b387aed
--- /dev/null
+++ b/ruoyi-auth/src/main/java/org/dromara/auth/enums/CaptchaCategory.java
@@ -0,0 +1,35 @@
+package org.dromara.auth.enums;
+
+import cn.hutool.captcha.AbstractCaptcha;
+import cn.hutool.captcha.CircleCaptcha;
+import cn.hutool.captcha.LineCaptcha;
+import cn.hutool.captcha.ShearCaptcha;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * 验证码类别
+ *
+ * @author Lion Li
+ */
+@Getter
+@AllArgsConstructor
+public enum CaptchaCategory {
+
+ /**
+ * 线段干扰
+ */
+ LINE(LineCaptcha.class),
+
+ /**
+ * 圆圈干扰
+ */
+ CIRCLE(CircleCaptcha.class),
+
+ /**
+ * 扭曲干扰
+ */
+ SHEAR(ShearCaptcha.class);
+
+ private final Class extends AbstractCaptcha> clazz;
+}
diff --git a/ruoyi-auth/src/main/java/org/dromara/auth/enums/CaptchaType.java b/ruoyi-auth/src/main/java/org/dromara/auth/enums/CaptchaType.java
new file mode 100644
index 0000000..b663345
--- /dev/null
+++ b/ruoyi-auth/src/main/java/org/dromara/auth/enums/CaptchaType.java
@@ -0,0 +1,29 @@
+package org.dromara.auth.enums;
+
+import cn.hutool.captcha.generator.CodeGenerator;
+import cn.hutool.captcha.generator.RandomGenerator;
+import org.dromara.auth.captcha.UnsignedMathGenerator;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * 验证码类型
+ *
+ * @author Lion Li
+ */
+@Getter
+@AllArgsConstructor
+public enum CaptchaType {
+
+ /**
+ * 数字
+ */
+ MATH(UnsignedMathGenerator.class),
+
+ /**
+ * 字符
+ */
+ CHAR(RandomGenerator.class);
+
+ private final Class extends CodeGenerator> clazz;
+}
diff --git a/ruoyi-auth/src/main/java/org/dromara/auth/form/EmailLoginBody.java b/ruoyi-auth/src/main/java/org/dromara/auth/form/EmailLoginBody.java
new file mode 100644
index 0000000..5c4fc72
--- /dev/null
+++ b/ruoyi-auth/src/main/java/org/dromara/auth/form/EmailLoginBody.java
@@ -0,0 +1,31 @@
+package org.dromara.auth.form;
+
+import jakarta.validation.constraints.Email;
+import jakarta.validation.constraints.NotBlank;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.dromara.common.core.domain.model.LoginBody;
+
+/**
+ * 邮件登录对象
+ *
+ * @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/ruoyi-auth/src/main/java/org/dromara/auth/form/PasswordLoginBody.java b/ruoyi-auth/src/main/java/org/dromara/auth/form/PasswordLoginBody.java
new file mode 100644
index 0000000..31086ab
--- /dev/null
+++ b/ruoyi-auth/src/main/java/org/dromara/auth/form/PasswordLoginBody.java
@@ -0,0 +1,32 @@
+package org.dromara.auth.form;
+
+import jakarta.validation.constraints.NotBlank;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.dromara.common.core.domain.model.LoginBody;
+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 = 2, max = 30, message = "{user.username.length.valid}")
+ private String username;
+
+ /**
+ * 用户密码
+ */
+ @NotBlank(message = "{user.password.not.blank}")
+ @Length(min = 5, max = 30, message = "{user.password.length.valid}")
+ private String password;
+
+}
diff --git a/ruoyi-auth/src/main/java/org/dromara/auth/form/RegisterBody.java b/ruoyi-auth/src/main/java/org/dromara/auth/form/RegisterBody.java
new file mode 100644
index 0000000..8b0a047
--- /dev/null
+++ b/ruoyi-auth/src/main/java/org/dromara/auth/form/RegisterBody.java
@@ -0,0 +1,37 @@
+package org.dromara.auth.form;
+
+import jakarta.validation.constraints.NotBlank;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.dromara.common.core.domain.model.LoginBody;
+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 = 2, max = 30, message = "{user.username.length.valid}")
+ private String username;
+
+ /**
+ * 用户密码
+ */
+ @NotBlank(message = "{user.password.not.blank}")
+ @Length(min = 5, max = 30, message = "{user.password.length.valid}")
+ private String password;
+
+ /**
+ * 用户类型
+ */
+ private String userType;
+
+}
diff --git a/ruoyi-auth/src/main/java/org/dromara/auth/form/SmsLoginBody.java b/ruoyi-auth/src/main/java/org/dromara/auth/form/SmsLoginBody.java
new file mode 100644
index 0000000..7271b63
--- /dev/null
+++ b/ruoyi-auth/src/main/java/org/dromara/auth/form/SmsLoginBody.java
@@ -0,0 +1,29 @@
+package org.dromara.auth.form;
+
+import jakarta.validation.constraints.NotBlank;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.dromara.common.core.domain.model.LoginBody;
+
+/**
+ * 短信登录对象
+ *
+ * @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/ruoyi-auth/src/main/java/org/dromara/auth/form/SocialLoginBody.java b/ruoyi-auth/src/main/java/org/dromara/auth/form/SocialLoginBody.java
new file mode 100644
index 0000000..faf8aa6
--- /dev/null
+++ b/ruoyi-auth/src/main/java/org/dromara/auth/form/SocialLoginBody.java
@@ -0,0 +1,35 @@
+package org.dromara.auth.form;
+
+import jakarta.validation.constraints.NotBlank;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.dromara.common.core.domain.model.LoginBody;
+
+/**
+ * 三方登录对象
+ *
+ * @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/ruoyi-auth/src/main/java/org/dromara/auth/form/XcxLoginBody.java b/ruoyi-auth/src/main/java/org/dromara/auth/form/XcxLoginBody.java
new file mode 100644
index 0000000..3c8a7f5
--- /dev/null
+++ b/ruoyi-auth/src/main/java/org/dromara/auth/form/XcxLoginBody.java
@@ -0,0 +1,28 @@
+package org.dromara.auth.form;
+
+import jakarta.validation.constraints.NotBlank;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.dromara.common.core.domain.model.LoginBody;
+
+/**
+ * 三方登录对象
+ *
+ * @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/ruoyi-auth/src/main/java/org/dromara/auth/listener/UserActionListener.java b/ruoyi-auth/src/main/java/org/dromara/auth/listener/UserActionListener.java
new file mode 100644
index 0000000..4715399
--- /dev/null
+++ b/ruoyi-auth/src/main/java/org/dromara/auth/listener/UserActionListener.java
@@ -0,0 +1,168 @@
+package org.dromara.auth.listener;
+
+import cn.dev33.satoken.listener.SaTokenListener;
+import cn.dev33.satoken.stp.StpUtil;
+import cn.dev33.satoken.stp.parameter.SaLoginParameter;
+import cn.hutool.core.convert.Convert;
+import cn.hutool.http.useragent.UserAgent;
+import cn.hutool.http.useragent.UserAgentUtil;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.dubbo.config.annotation.DubboReference;
+import org.dromara.common.core.constant.CacheConstants;
+import org.dromara.common.core.constant.Constants;
+import org.dromara.common.core.utils.MessageUtils;
+import org.dromara.common.core.utils.ServletUtils;
+import org.dromara.common.core.utils.SpringUtils;
+import org.dromara.common.core.utils.ip.AddressUtils;
+import org.dromara.common.log.event.LogininforEvent;
+import org.dromara.common.redis.utils.RedisUtils;
+import org.dromara.common.satoken.utils.LoginHelper;
+import org.dromara.common.tenant.helper.TenantHelper;
+import org.dromara.resource.api.RemoteMessageService;
+import org.dromara.system.api.RemoteUserService;
+import org.dromara.system.api.domain.SysUserOnline;
+import org.springframework.stereotype.Component;
+
+import java.time.Duration;
+
+/**
+ * 用户行为 侦听器的实现
+ *
+ * @author Lion Li
+ */
+@RequiredArgsConstructor
+@Component
+@Slf4j
+public class UserActionListener implements SaTokenListener {
+
+ @DubboReference
+ private RemoteUserService remoteUserService;
+ @DubboReference
+ private RemoteMessageService remoteMessageService;
+
+ /**
+ * 每次登录时触发
+ */
+ @Override
+ public void doLogin(String loginType, Object loginId, String tokenValue, SaLoginParameter loginParameter) {
+ UserAgent userAgent = UserAgentUtil.parse(ServletUtils.getRequest().getHeader("User-Agent"));
+ String ip = ServletUtils.getClientIP();
+ SysUserOnline userOnline = new SysUserOnline();
+ userOnline.setIpaddr(ip);
+ userOnline.setLoginLocation(AddressUtils.getRealAddressByIP(ip));
+ userOnline.setBrowser(userAgent.getBrowser().getName());
+ userOnline.setOs(userAgent.getOs().getName());
+ userOnline.setLoginTime(System.currentTimeMillis());
+ userOnline.setTokenId(tokenValue);
+ String username = (String) loginParameter.getExtra(LoginHelper.USER_NAME_KEY);
+ String tenantId = (String) loginParameter.getExtra(LoginHelper.TENANT_KEY);
+ userOnline.setUserName(username);
+ userOnline.setClientKey((String) loginParameter.getExtra(LoginHelper.CLIENT_KEY));
+ userOnline.setDeviceType(loginParameter.getDeviceType());
+ userOnline.setDeptName((String) loginParameter.getExtra(LoginHelper.DEPT_NAME_KEY));
+ TenantHelper.dynamic(tenantId, () -> {
+ if (loginParameter.getTimeout() == -1) {
+ RedisUtils.setCacheObject(CacheConstants.ONLINE_TOKEN_KEY + tokenValue, userOnline);
+ } else {
+ RedisUtils.setCacheObject(CacheConstants.ONLINE_TOKEN_KEY + tokenValue, userOnline, Duration.ofSeconds(loginParameter.getTimeout()));
+ }
+ });
+ // 记录登录日志
+ LogininforEvent logininforEvent = new LogininforEvent();
+ logininforEvent.setTenantId(tenantId);
+ logininforEvent.setUsername(username);
+ logininforEvent.setStatus(Constants.LOGIN_SUCCESS);
+ logininforEvent.setMessage(MessageUtils.message("user.login.success"));
+ SpringUtils.context().publishEvent(logininforEvent);
+ // 更新登录信息
+ remoteUserService.recordLoginInfo((Long) loginParameter.getExtra(LoginHelper.USER_KEY), ip);
+ log.info("user doLogin, useId:{}, token:{}", loginId, tokenValue);
+ }
+
+ /**
+ * 每次注销时触发
+ */
+ @Override
+ public void doLogout(String loginType, Object loginId, String tokenValue) {
+ String tenantId = Convert.toStr(StpUtil.getExtra(tokenValue, LoginHelper.TENANT_KEY));
+ TenantHelper.dynamic(tenantId, () -> {
+ RedisUtils.deleteObject(CacheConstants.ONLINE_TOKEN_KEY + tokenValue);
+ });
+ log.info("user doLogout, useId:{}, token:{}", loginId, tokenValue);
+ }
+
+ /**
+ * 每次被踢下线时触发
+ */
+ @Override
+ public void doKickout(String loginType, Object loginId, String tokenValue) {
+ String tenantId = Convert.toStr(StpUtil.getExtra(tokenValue, LoginHelper.TENANT_KEY));
+ TenantHelper.dynamic(tenantId, () -> {
+ RedisUtils.deleteObject(CacheConstants.ONLINE_TOKEN_KEY + tokenValue);
+ });
+ log.info("user doLogoutByLoginId, useId:{}, token:{}", loginId, tokenValue);
+ }
+
+ /**
+ * 每次被顶下线时触发
+ */
+ @Override
+ public void doReplaced(String loginType, Object loginId, String tokenValue) {
+ String tenantId = Convert.toStr(StpUtil.getExtra(tokenValue, LoginHelper.TENANT_KEY));
+ TenantHelper.dynamic(tenantId, () -> {
+ RedisUtils.deleteObject(CacheConstants.ONLINE_TOKEN_KEY + tokenValue);
+ });
+ log.info("user doReplaced, useId:{}, token:{}", loginId, tokenValue);
+ }
+
+ /**
+ * 每次被封禁时触发
+ */
+ @Override
+ public void doDisable(String loginType, Object loginId, String service, int level, long disableTime) {
+ }
+
+ /**
+ * 每次被解封时触发
+ */
+ @Override
+ public void doUntieDisable(String loginType, Object loginId, String service) {
+ }
+
+ /**
+ * 每次打开二级认证时触发
+ */
+ @Override
+ public void doOpenSafe(String loginType, String tokenValue, String service, long safeTime) {
+ }
+
+ /**
+ * 每次创建Session时触发
+ */
+ @Override
+ public void doCloseSafe(String loginType, String tokenValue, String service) {
+ }
+
+ /**
+ * 每次创建Session时触发
+ */
+ @Override
+ public void doCreateSession(String id) {
+ }
+
+ /**
+ * 每次注销Session时触发
+ */
+ @Override
+ public void doLogoutSession(String id) {
+ }
+
+ /**
+ * 每次Token续期时触发
+ */
+ @Override
+ public void doRenewTimeout(String loginType, Object loginId, String tokenValue, long timeout) {
+ }
+
+}
diff --git a/ruoyi-auth/src/main/java/org/dromara/auth/properties/CaptchaProperties.java b/ruoyi-auth/src/main/java/org/dromara/auth/properties/CaptchaProperties.java
new file mode 100644
index 0000000..3f05d9b
--- /dev/null
+++ b/ruoyi-auth/src/main/java/org/dromara/auth/properties/CaptchaProperties.java
@@ -0,0 +1,46 @@
+package org.dromara.auth.properties;
+
+import org.dromara.auth.enums.CaptchaCategory;
+import org.dromara.auth.enums.CaptchaType;
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.cloud.context.config.annotation.RefreshScope;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * 验证码配置
+ *
+ * @author ruoyi
+ */
+@Data
+@Configuration
+@RefreshScope
+@ConfigurationProperties(prefix = "security.captcha")
+public class CaptchaProperties {
+
+ /**
+ * 验证码类型
+ */
+ private CaptchaType type;
+
+ /**
+ * 验证码类别
+ */
+ private CaptchaCategory category;
+
+ /**
+ * 数字验证码位数
+ */
+ private Integer numberLength;
+
+ /**
+ * 字符验证码长度
+ */
+ private Integer charLength;
+
+ /**
+ * 验证码开关
+ */
+ private Boolean enabled;
+
+}
diff --git a/ruoyi-auth/src/main/java/org/dromara/auth/properties/UserPasswordProperties.java b/ruoyi-auth/src/main/java/org/dromara/auth/properties/UserPasswordProperties.java
new file mode 100644
index 0000000..5960d71
--- /dev/null
+++ b/ruoyi-auth/src/main/java/org/dromara/auth/properties/UserPasswordProperties.java
@@ -0,0 +1,29 @@
+package org.dromara.auth.properties;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.cloud.context.config.annotation.RefreshScope;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * 用户密码配置
+ *
+ * @author Lion Li
+ */
+@Data
+@Configuration
+@RefreshScope
+@ConfigurationProperties(prefix = "user.password")
+public class UserPasswordProperties {
+
+ /**
+ * 密码最大错误次数
+ */
+ private Integer maxRetryCount;
+
+ /**
+ * 密码锁定时间(默认10分钟)
+ */
+ private Integer lockTime;
+
+}
diff --git a/ruoyi-auth/src/main/java/org/dromara/auth/service/IAuthStrategy.java b/ruoyi-auth/src/main/java/org/dromara/auth/service/IAuthStrategy.java
new file mode 100644
index 0000000..28307c3
--- /dev/null
+++ b/ruoyi-auth/src/main/java/org/dromara/auth/service/IAuthStrategy.java
@@ -0,0 +1,44 @@
+package org.dromara.auth.service;
+
+import org.dromara.auth.domain.vo.LoginVo;
+import org.dromara.common.core.exception.ServiceException;
+import org.dromara.common.core.utils.SpringUtils;
+import org.dromara.system.api.domain.vo.RemoteClientVo;
+
+/**
+ * 授权策略
+ *
+ * @author Michelle.Chung
+ */
+public interface IAuthStrategy {
+
+ String BASE_NAME = "AuthStrategy";
+
+ /**
+ * 登录
+ *
+ * @param body 登录对象
+ * @param client 授权管理视图对象
+ * @param grantType 授权类型
+ * @return 登录验证信息
+ */
+ static LoginVo login(String body, RemoteClientVo client, String grantType) {
+ // 授权类型和客户端id
+ String beanName = grantType + BASE_NAME;
+ if (!SpringUtils.containsBean(beanName)) {
+ throw new ServiceException("授权类型不正确!");
+ }
+ IAuthStrategy instance = SpringUtils.getBean(beanName);
+ return instance.login(body, client);
+ }
+
+ /**
+ * 登录
+ *
+ * @param body 登录对象
+ * @param client 授权管理视图对象
+ * @return 登录验证信息
+ */
+ LoginVo login(String body, RemoteClientVo client);
+
+}
diff --git a/ruoyi-auth/src/main/java/org/dromara/auth/service/SysLoginService.java b/ruoyi-auth/src/main/java/org/dromara/auth/service/SysLoginService.java
new file mode 100644
index 0000000..59af7cc
--- /dev/null
+++ b/ruoyi-auth/src/main/java/org/dromara/auth/service/SysLoginService.java
@@ -0,0 +1,265 @@
+package org.dromara.auth.service;
+
+import cn.dev33.satoken.exception.NotLoginException;
+import cn.hutool.crypto.digest.BCrypt;
+import cn.dev33.satoken.stp.StpUtil;
+import cn.hutool.core.bean.BeanUtil;
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.ObjectUtil;
+import com.baomidou.lock.annotation.Lock4j;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import me.zhyd.oauth.model.AuthUser;
+import org.apache.dubbo.config.annotation.DubboReference;
+import org.dromara.auth.form.RegisterBody;
+import org.dromara.auth.properties.CaptchaProperties;
+import org.dromara.auth.properties.UserPasswordProperties;
+import org.dromara.common.core.constant.*;
+import org.dromara.common.core.enums.LoginType;
+import org.dromara.common.core.enums.UserType;
+import org.dromara.common.core.exception.ServiceException;
+import org.dromara.common.core.exception.user.CaptchaException;
+import org.dromara.common.core.exception.user.CaptchaExpireException;
+import org.dromara.common.core.exception.user.UserException;
+import org.dromara.common.core.utils.MessageUtils;
+import org.dromara.common.core.utils.SpringUtils;
+import org.dromara.common.core.utils.StringUtils;
+import org.dromara.common.log.event.LogininforEvent;
+import org.dromara.common.redis.utils.RedisUtils;
+import org.dromara.common.satoken.utils.LoginHelper;
+import org.dromara.common.tenant.exception.TenantException;
+import org.dromara.common.tenant.helper.TenantHelper;
+import org.dromara.system.api.RemoteSocialService;
+import org.dromara.system.api.RemoteTenantService;
+import org.dromara.system.api.RemoteUserService;
+import org.dromara.system.api.domain.bo.RemoteSocialBo;
+import org.dromara.system.api.domain.bo.RemoteUserBo;
+import org.dromara.system.api.domain.vo.RemoteSocialVo;
+import org.dromara.system.api.domain.vo.RemoteTenantVo;
+import org.dromara.system.api.model.LoginUser;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.time.Duration;
+import java.util.Date;
+import java.util.List;
+import java.util.function.Supplier;
+
+/**
+ * 登录校验方法
+ *
+ * @author ruoyi
+ */
+@RequiredArgsConstructor
+@Service
+@Slf4j
+public class SysLoginService {
+
+ @DubboReference
+ private RemoteUserService remoteUserService;
+ @DubboReference
+ private RemoteTenantService remoteTenantService;
+ @DubboReference
+ private RemoteSocialService remoteSocialService;
+
+ @Autowired
+ private UserPasswordProperties userPasswordProperties;
+ @Autowired
+ private final CaptchaProperties captchaProperties;
+
+ /**
+ * 绑定第三方用户
+ *
+ * @param authUserData 授权响应实体
+ */
+ @Lock4j
+ public void socialRegister(AuthUser authUserData) {
+ String authId = authUserData.getSource() + authUserData.getUuid();
+ // 第三方用户信息
+ RemoteSocialBo bo = BeanUtil.toBean(authUserData, RemoteSocialBo.class);
+ BeanUtil.copyProperties(authUserData.getToken(), bo);
+ Long userId = LoginHelper.getUserId();
+ bo.setUserId(userId);
+ bo.setAuthId(authId);
+ bo.setOpenId(authUserData.getUuid());
+ bo.setUserName(authUserData.getUsername());
+ bo.setNickName(authUserData.getNickname());
+ List checkList = remoteSocialService.selectByAuthId(authId);
+ if (CollUtil.isNotEmpty(checkList)) {
+ throw new ServiceException("此三方账号已经被绑定!");
+ }
+ // 查询是否已经绑定用户
+ RemoteSocialBo params = new RemoteSocialBo();
+ params.setUserId(userId);
+ params.setSource(bo.getSource());
+ List list = remoteSocialService.queryList(params);
+ if (CollUtil.isEmpty(list)) {
+ // 没有绑定用户, 新增用户信息
+ remoteSocialService.insertByBo(bo);
+ } else {
+ // 更新用户信息
+ bo.setId(list.get(0).getId());
+ remoteSocialService.updateByBo(bo);
+ // 如果要绑定的平台账号已经被绑定过了 是否抛异常自行决断
+ // throw new ServiceException("此平台账号已经被绑定!");
+ }
+ }
+
+ /**
+ * 退出登录
+ */
+ public void logout() {
+ try {
+ LoginUser loginUser = LoginHelper.getLoginUser();
+ if (ObjectUtil.isNull(loginUser)) {
+ return;
+ }
+ if (TenantHelper.isEnable() && LoginHelper.isSuperAdmin()) {
+ // 超级管理员 登出清除动态租户
+ TenantHelper.clearDynamic();
+ }
+ recordLogininfor(loginUser.getTenantId(), loginUser.getUsername(), Constants.LOGOUT, MessageUtils.message("user.logout.success"));
+ } catch (NotLoginException ignored) {
+ } finally {
+ try {
+ StpUtil.logout();
+ } catch (NotLoginException ignored) {
+ }
+ }
+ }
+
+ /**
+ * 注册
+ */
+ public void register(RegisterBody registerBody) {
+ String tenantId = registerBody.getTenantId();
+ String username = registerBody.getUsername();
+ String password = registerBody.getPassword();
+ // 校验用户类型是否存在
+ String userType = UserType.getUserType(registerBody.getUserType()).getUserType();
+
+ boolean captchaEnabled = captchaProperties.getEnabled();
+ // 验证码开关
+ if (captchaEnabled) {
+ validateCaptcha(tenantId, username, registerBody.getCode(), registerBody.getUuid());
+ }
+
+ // 注册用户信息
+ RemoteUserBo remoteUserBo = new RemoteUserBo();
+ remoteUserBo.setTenantId(tenantId);
+ remoteUserBo.setUserName(username);
+ remoteUserBo.setNickName(username);
+ remoteUserBo.setPassword(BCrypt.hashpw(password));
+ remoteUserBo.setUserType(userType);
+
+ boolean regFlag = remoteUserService.registerUserInfo(remoteUserBo);
+ if (!regFlag) {
+ throw new UserException("user.register.error");
+ }
+ recordLogininfor(tenantId, username, Constants.REGISTER, MessageUtils.message("user.register.success"));
+ }
+
+ /**
+ * 校验验证码
+ *
+ * @param username 用户名
+ * @param code 验证码
+ * @param uuid 唯一标识
+ */
+ public void validateCaptcha(String tenantId, String username, String code, String uuid) {
+ String verifyKey = GlobalConstants.CAPTCHA_CODE_KEY + StringUtils.blankToDefault(uuid, "");
+ String captcha = RedisUtils.getCacheObject(verifyKey);
+ RedisUtils.deleteObject(verifyKey);
+ if (captcha == null) {
+ recordLogininfor(tenantId, username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire"));
+ throw new CaptchaExpireException();
+ }
+ if (!code.equalsIgnoreCase(captcha)) {
+ recordLogininfor(tenantId, username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.error"));
+ throw new CaptchaException();
+ }
+ }
+
+ /**
+ * 记录登录信息
+ *
+ * @param username 用户名
+ * @param status 状态
+ * @param message 消息内容
+ * @return
+ */
+ public void recordLogininfor(String tenantId, String username, String status, String message) {
+ // 封装对象
+ LogininforEvent logininforEvent = new LogininforEvent();
+ logininforEvent.setTenantId(tenantId);
+ logininforEvent.setUsername(username);
+ logininforEvent.setStatus(status);
+ logininforEvent.setMessage(message);
+ SpringUtils.context().publishEvent(logininforEvent);
+ }
+
+ /**
+ * 登录校验
+ */
+ public void checkLogin(LoginType loginType, String tenantId, String username, Supplier supplier) {
+ String errorKey = CacheConstants.PWD_ERR_CNT_KEY + username;
+ String loginFail = Constants.LOGIN_FAIL;
+ Integer maxRetryCount = userPasswordProperties.getMaxRetryCount();
+ Integer lockTime = userPasswordProperties.getLockTime();
+
+ // 获取用户登录错误次数,默认为0 (可自定义限制策略 例如: key + username + ip)
+ int errorNumber = ObjectUtil.defaultIfNull(RedisUtils.getCacheObject(errorKey), 0);
+ // 锁定时间内登录 则踢出
+ if (errorNumber >= maxRetryCount) {
+ recordLogininfor(tenantId, username, loginFail, MessageUtils.message(loginType.getRetryLimitExceed(), maxRetryCount, lockTime));
+ throw new UserException(loginType.getRetryLimitExceed(), maxRetryCount, lockTime);
+ }
+
+ if (supplier.get()) {
+ // 错误次数递增
+ errorNumber++;
+ RedisUtils.setCacheObject(errorKey, errorNumber, Duration.ofMinutes(lockTime));
+ // 达到规定错误次数 则锁定登录
+ if (errorNumber >= maxRetryCount) {
+ recordLogininfor(tenantId, username, loginFail, MessageUtils.message(loginType.getRetryLimitExceed(), maxRetryCount, lockTime));
+ throw new UserException(loginType.getRetryLimitExceed(), maxRetryCount, lockTime);
+ } else {
+ // 未达到规定错误次数
+ recordLogininfor(tenantId, username, loginFail, MessageUtils.message(loginType.getRetryLimitCount(), errorNumber));
+ throw new UserException(loginType.getRetryLimitCount(), errorNumber);
+ }
+ }
+
+ // 登录成功 清空错误次数
+ RedisUtils.deleteObject(errorKey);
+ }
+
+ /**
+ * 校验租户
+ *
+ * @param tenantId 租户ID
+ */
+ public void checkTenant(String tenantId) {
+ if (!TenantHelper.isEnable()) {
+ return;
+ }
+ if (StringUtils.isBlank(tenantId)) {
+ throw new TenantException("tenant.number.not.blank");
+ }
+ if (TenantConstants.DEFAULT_TENANT_ID.equals(tenantId)) {
+ return;
+ }
+ RemoteTenantVo tenant = remoteTenantService.queryByTenantId(tenantId);
+ if (ObjectUtil.isNull(tenant)) {
+ log.info("登录租户:{} 不存在.", tenantId);
+ throw new TenantException("tenant.not.exists");
+ } else if (SystemConstants.DISABLE.equals(tenant.getStatus())) {
+ log.info("登录租户:{} 已被停用.", tenantId);
+ throw new TenantException("tenant.blocked");
+ } else if (ObjectUtil.isNotNull(tenant.getExpireTime())
+ && new Date().after(tenant.getExpireTime())) {
+ log.info("登录租户:{} 已超过有效期.", tenantId);
+ throw new TenantException("tenant.expired");
+ }
+ }
+}
diff --git a/ruoyi-auth/src/main/java/org/dromara/auth/service/impl/EmailAuthStrategy.java b/ruoyi-auth/src/main/java/org/dromara/auth/service/impl/EmailAuthStrategy.java
new file mode 100644
index 0000000..c1403b6
--- /dev/null
+++ b/ruoyi-auth/src/main/java/org/dromara/auth/service/impl/EmailAuthStrategy.java
@@ -0,0 +1,86 @@
+package org.dromara.auth.service.impl;
+
+import cn.dev33.satoken.stp.StpUtil;
+import cn.dev33.satoken.stp.parameter.SaLoginParameter;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.dubbo.config.annotation.DubboReference;
+import org.dromara.auth.domain.vo.LoginVo;
+import org.dromara.auth.form.EmailLoginBody;
+import org.dromara.auth.service.IAuthStrategy;
+import org.dromara.auth.service.SysLoginService;
+import org.dromara.common.core.constant.Constants;
+import org.dromara.common.core.constant.GlobalConstants;
+import org.dromara.common.core.enums.LoginType;
+import org.dromara.common.core.exception.user.CaptchaExpireException;
+import org.dromara.common.core.utils.MessageUtils;
+import org.dromara.common.core.utils.StringUtils;
+import org.dromara.common.core.utils.ValidatorUtils;
+import org.dromara.common.json.utils.JsonUtils;
+import org.dromara.common.redis.utils.RedisUtils;
+import org.dromara.common.satoken.utils.LoginHelper;
+import org.dromara.common.tenant.helper.TenantHelper;
+import org.dromara.system.api.RemoteUserService;
+import org.dromara.system.api.domain.vo.RemoteClientVo;
+import org.dromara.system.api.model.LoginUser;
+import org.springframework.stereotype.Service;
+
+/**
+ * 邮件认证策略
+ *
+ * @author Michelle.Chung
+ */
+@Slf4j
+@Service("email" + IAuthStrategy.BASE_NAME)
+@RequiredArgsConstructor
+public class EmailAuthStrategy implements IAuthStrategy {
+
+ private final SysLoginService loginService;
+
+ @DubboReference
+ private RemoteUserService remoteUserService;
+
+ @Override
+ public LoginVo login(String body, RemoteClientVo client) {
+ EmailLoginBody loginBody = JsonUtils.parseObject(body, EmailLoginBody.class);
+ ValidatorUtils.validate(loginBody);
+ String tenantId = loginBody.getTenantId();
+ String email = loginBody.getEmail();
+ String emailCode = loginBody.getEmailCode();
+ LoginUser loginUser = TenantHelper.dynamic(tenantId, () -> {
+ LoginUser user = remoteUserService.getUserInfoByEmail(email, tenantId);
+ loginService.checkLogin(LoginType.EMAIL, tenantId, user.getUsername(), () -> !validateEmailCode(tenantId, email, emailCode));
+ return user;
+ });
+ loginUser.setClientKey(client.getClientKey());
+ loginUser.setDeviceType(client.getDeviceType());
+ SaLoginParameter model = new SaLoginParameter();
+ model.setDeviceType(client.getDeviceType());
+ // 自定义分配 不同用户体系 不同 token 授权时间 不设置默认走全局 yml 配置
+ // 例如: 后台用户30分钟过期 app用户1天过期
+ model.setTimeout(client.getTimeout());
+ model.setActiveTimeout(client.getActiveTimeout());
+ model.setExtra(LoginHelper.CLIENT_KEY, client.getClientId());
+ // 生成token
+ LoginHelper.login(loginUser, model);
+
+ LoginVo loginVo = new LoginVo();
+ loginVo.setAccessToken(StpUtil.getTokenValue());
+ loginVo.setExpireIn(StpUtil.getTokenTimeout());
+ loginVo.setClientId(client.getClientId());
+ return loginVo;
+ }
+
+ /**
+ * 校验邮箱验证码
+ */
+ private boolean validateEmailCode(String tenantId, String email, String emailCode) {
+ String code = RedisUtils.getCacheObject(GlobalConstants.CAPTCHA_CODE_KEY + email);
+ if (StringUtils.isBlank(code)) {
+ loginService.recordLogininfor(tenantId, email, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire"));
+ throw new CaptchaExpireException();
+ }
+ return code.equals(emailCode);
+ }
+
+}
diff --git a/ruoyi-auth/src/main/java/org/dromara/auth/service/impl/PasswordAuthStrategy.java b/ruoyi-auth/src/main/java/org/dromara/auth/service/impl/PasswordAuthStrategy.java
new file mode 100644
index 0000000..53476f9
--- /dev/null
+++ b/ruoyi-auth/src/main/java/org/dromara/auth/service/impl/PasswordAuthStrategy.java
@@ -0,0 +1,107 @@
+package org.dromara.auth.service.impl;
+
+import cn.hutool.crypto.digest.BCrypt;
+import cn.dev33.satoken.stp.StpUtil;
+import cn.dev33.satoken.stp.parameter.SaLoginParameter;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.dubbo.config.annotation.DubboReference;
+import org.dromara.auth.domain.vo.LoginVo;
+import org.dromara.auth.form.PasswordLoginBody;
+import org.dromara.auth.properties.CaptchaProperties;
+import org.dromara.auth.service.IAuthStrategy;
+import org.dromara.auth.service.SysLoginService;
+import org.dromara.common.core.constant.Constants;
+import org.dromara.common.core.constant.GlobalConstants;
+import org.dromara.common.core.enums.LoginType;
+import org.dromara.common.core.exception.user.CaptchaException;
+import org.dromara.common.core.exception.user.CaptchaExpireException;
+import org.dromara.common.core.utils.MessageUtils;
+import org.dromara.common.core.utils.StringUtils;
+import org.dromara.common.core.utils.ValidatorUtils;
+import org.dromara.common.json.utils.JsonUtils;
+import org.dromara.common.redis.utils.RedisUtils;
+import org.dromara.common.satoken.utils.LoginHelper;
+import org.dromara.common.tenant.helper.TenantHelper;
+import org.dromara.system.api.RemoteUserService;
+import org.dromara.system.api.domain.vo.RemoteClientVo;
+import org.dromara.system.api.model.LoginUser;
+import org.springframework.stereotype.Service;
+
+/**
+ * 密码认证策略
+ *
+ * @author Michelle.Chung
+ */
+@Slf4j
+@Service("password" + IAuthStrategy.BASE_NAME)
+@RequiredArgsConstructor
+public class PasswordAuthStrategy implements IAuthStrategy {
+
+ private final CaptchaProperties captchaProperties;
+
+ private final SysLoginService loginService;
+
+ @DubboReference
+ private RemoteUserService remoteUserService;
+
+ @Override
+ public LoginVo login(String body, RemoteClientVo client) {
+ PasswordLoginBody loginBody = JsonUtils.parseObject(body, PasswordLoginBody.class);
+ ValidatorUtils.validate(loginBody);
+ String tenantId = loginBody.getTenantId();
+ String username = loginBody.getUsername();
+ String password = loginBody.getPassword();
+ String code = loginBody.getCode();
+ String uuid = loginBody.getUuid();
+
+ // 验证码开关
+ if (captchaProperties.getEnabled()) {
+ validateCaptcha(tenantId, username, code, uuid);
+ }
+ LoginUser loginUser = TenantHelper.dynamic(tenantId, () -> {
+ LoginUser user = remoteUserService.getUserInfo(username, tenantId);
+ loginService.checkLogin(LoginType.PASSWORD, tenantId, username, () -> !BCrypt.checkpw(password, user.getPassword()));
+ return user;
+ });
+ loginUser.setClientKey(client.getClientKey());
+ loginUser.setDeviceType(client.getDeviceType());
+ SaLoginParameter model = new SaLoginParameter();
+ model.setDeviceType(client.getDeviceType());
+ // 自定义分配 不同用户体系 不同 token 授权时间 不设置默认走全局 yml 配置
+ // 例如: 后台用户30分钟过期 app用户1天过期
+ model.setTimeout(client.getTimeout());
+ model.setActiveTimeout(client.getActiveTimeout());
+ model.setExtra(LoginHelper.CLIENT_KEY, client.getClientId());
+ // 生成token
+ LoginHelper.login(loginUser, model);
+
+ LoginVo loginVo = new LoginVo();
+ loginVo.setAccessToken(StpUtil.getTokenValue());
+ loginVo.setExpireIn(StpUtil.getTokenTimeout());
+ loginVo.setClientId(client.getClientId());
+ return loginVo;
+ }
+
+ /**
+ * 校验验证码
+ *
+ * @param username 用户名
+ * @param code 验证码
+ * @param uuid 唯一标识
+ */
+ private void validateCaptcha(String tenantId, String username, String code, String uuid) {
+ String verifyKey = GlobalConstants.CAPTCHA_CODE_KEY + StringUtils.blankToDefault(uuid, "");
+ String captcha = RedisUtils.getCacheObject(verifyKey);
+ RedisUtils.deleteObject(verifyKey);
+ if (captcha == null) {
+ loginService.recordLogininfor(tenantId, username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire"));
+ throw new CaptchaExpireException();
+ }
+ if (!code.equalsIgnoreCase(captcha)) {
+ loginService.recordLogininfor(tenantId, username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.error"));
+ throw new CaptchaException();
+ }
+ }
+
+}
diff --git a/ruoyi-auth/src/main/java/org/dromara/auth/service/impl/SmsAuthStrategy.java b/ruoyi-auth/src/main/java/org/dromara/auth/service/impl/SmsAuthStrategy.java
new file mode 100644
index 0000000..641cc91
--- /dev/null
+++ b/ruoyi-auth/src/main/java/org/dromara/auth/service/impl/SmsAuthStrategy.java
@@ -0,0 +1,86 @@
+package org.dromara.auth.service.impl;
+
+import cn.dev33.satoken.stp.StpUtil;
+import cn.dev33.satoken.stp.parameter.SaLoginParameter;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.dubbo.config.annotation.DubboReference;
+import org.dromara.auth.domain.vo.LoginVo;
+import org.dromara.auth.form.SmsLoginBody;
+import org.dromara.auth.service.IAuthStrategy;
+import org.dromara.auth.service.SysLoginService;
+import org.dromara.common.core.constant.Constants;
+import org.dromara.common.core.constant.GlobalConstants;
+import org.dromara.common.core.enums.LoginType;
+import org.dromara.common.core.exception.user.CaptchaExpireException;
+import org.dromara.common.core.utils.MessageUtils;
+import org.dromara.common.core.utils.StringUtils;
+import org.dromara.common.core.utils.ValidatorUtils;
+import org.dromara.common.json.utils.JsonUtils;
+import org.dromara.common.redis.utils.RedisUtils;
+import org.dromara.common.satoken.utils.LoginHelper;
+import org.dromara.common.tenant.helper.TenantHelper;
+import org.dromara.system.api.RemoteUserService;
+import org.dromara.system.api.domain.vo.RemoteClientVo;
+import org.dromara.system.api.model.LoginUser;
+import org.springframework.stereotype.Service;
+
+/**
+ * 短信认证策略
+ *
+ * @author Michelle.Chung
+ */
+@Slf4j
+@Service("sms" + IAuthStrategy.BASE_NAME)
+@RequiredArgsConstructor
+public class SmsAuthStrategy implements IAuthStrategy {
+
+ private final SysLoginService loginService;
+
+ @DubboReference
+ private RemoteUserService remoteUserService;
+
+ @Override
+ public LoginVo login(String body, RemoteClientVo client) {
+ SmsLoginBody loginBody = JsonUtils.parseObject(body, SmsLoginBody.class);
+ ValidatorUtils.validate(loginBody);
+ String tenantId = loginBody.getTenantId();
+ String phonenumber = loginBody.getPhonenumber();
+ String smsCode = loginBody.getSmsCode();
+ LoginUser loginUser = TenantHelper.dynamic(tenantId, () -> {
+ LoginUser user = remoteUserService.getUserInfoByPhonenumber(phonenumber, tenantId);
+ loginService.checkLogin(LoginType.SMS, tenantId, user.getUsername(), () -> !validateSmsCode(tenantId, phonenumber, smsCode));
+ return user;
+ });
+ loginUser.setClientKey(client.getClientKey());
+ loginUser.setDeviceType(client.getDeviceType());
+ SaLoginParameter model = new SaLoginParameter();
+ model.setDeviceType(client.getDeviceType());
+ // 自定义分配 不同用户体系 不同 token 授权时间 不设置默认走全局 yml 配置
+ // 例如: 后台用户30分钟过期 app用户1天过期
+ model.setTimeout(client.getTimeout());
+ model.setActiveTimeout(client.getActiveTimeout());
+ model.setExtra(LoginHelper.CLIENT_KEY, client.getClientId());
+ // 生成token
+ LoginHelper.login(loginUser, model);
+
+ LoginVo loginVo = new LoginVo();
+ loginVo.setAccessToken(StpUtil.getTokenValue());
+ loginVo.setExpireIn(StpUtil.getTokenTimeout());
+ loginVo.setClientId(client.getClientId());
+ return loginVo;
+ }
+
+ /**
+ * 校验短信验证码
+ */
+ private boolean validateSmsCode(String tenantId, String phonenumber, String smsCode) {
+ String code = RedisUtils.getCacheObject(GlobalConstants.CAPTCHA_CODE_KEY + phonenumber);
+ if (StringUtils.isBlank(code)) {
+ loginService.recordLogininfor(tenantId, phonenumber, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire"));
+ throw new CaptchaExpireException();
+ }
+ return code.equals(smsCode);
+ }
+
+}
diff --git a/ruoyi-auth/src/main/java/org/dromara/auth/service/impl/SocialAuthStrategy.java b/ruoyi-auth/src/main/java/org/dromara/auth/service/impl/SocialAuthStrategy.java
new file mode 100644
index 0000000..074f02e
--- /dev/null
+++ b/ruoyi-auth/src/main/java/org/dromara/auth/service/impl/SocialAuthStrategy.java
@@ -0,0 +1,102 @@
+package org.dromara.auth.service.impl;
+
+import cn.dev33.satoken.stp.StpUtil;
+import cn.dev33.satoken.stp.parameter.SaLoginParameter;
+import cn.hutool.core.collection.CollUtil;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import me.zhyd.oauth.model.AuthResponse;
+import me.zhyd.oauth.model.AuthUser;
+import org.apache.dubbo.config.annotation.DubboReference;
+import org.dromara.auth.domain.vo.LoginVo;
+import org.dromara.auth.form.SocialLoginBody;
+import org.dromara.auth.service.IAuthStrategy;
+import org.dromara.common.core.exception.ServiceException;
+import org.dromara.common.core.utils.StreamUtils;
+import org.dromara.common.core.utils.ValidatorUtils;
+import org.dromara.common.json.utils.JsonUtils;
+import org.dromara.common.satoken.utils.LoginHelper;
+import org.dromara.common.social.config.properties.SocialProperties;
+import org.dromara.common.social.utils.SocialUtils;
+import org.dromara.common.tenant.helper.TenantHelper;
+import org.dromara.system.api.RemoteSocialService;
+import org.dromara.system.api.RemoteUserService;
+import org.dromara.system.api.domain.vo.RemoteClientVo;
+import org.dromara.system.api.domain.vo.RemoteSocialVo;
+import org.dromara.system.api.model.LoginUser;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * 第三方授权策略
+ *
+ * @author thiszhc is 三三
+ */
+@Slf4j
+@Service("social" + IAuthStrategy.BASE_NAME)
+@RequiredArgsConstructor
+public class SocialAuthStrategy implements IAuthStrategy {
+
+ private final SocialProperties socialProperties;
+
+ @DubboReference
+ private RemoteSocialService remoteSocialService;
+ @DubboReference
+ private RemoteUserService remoteUserService;
+
+ /**
+ * 登录-第三方授权登录
+ *
+ * @param body 登录信息
+ * @param client 客户端信息
+ */
+ @Override
+ public LoginVo login(String body, RemoteClientVo client) {
+ SocialLoginBody loginBody = JsonUtils.parseObject(body, SocialLoginBody.class);
+ ValidatorUtils.validate(loginBody);
+ AuthResponse response = SocialUtils.loginAuth(
+ loginBody.getSource(), loginBody.getSocialCode(),
+ loginBody.getSocialState(), socialProperties);
+ if (!response.ok()) {
+ throw new ServiceException(response.getMsg());
+ }
+ AuthUser authUserData = response.getData();
+
+ List list = remoteSocialService.selectByAuthId(authUserData.getSource() + authUserData.getUuid());
+ if (CollUtil.isEmpty(list)) {
+ throw new ServiceException("你还没有绑定第三方账号,绑定后才可以登录!");
+ }
+ RemoteSocialVo socialVo;
+ if (TenantHelper.isEnable()) {
+ Optional opt = StreamUtils.findAny(list, x -> x.getTenantId().equals(loginBody.getTenantId()));
+ if (opt.isEmpty()) {
+ throw new ServiceException("对不起,你没有权限登录当前租户!");
+ }
+ socialVo = opt.get();
+ } else {
+ socialVo = list.get(0);
+ }
+
+ LoginUser loginUser = remoteUserService.getUserInfo(socialVo.getUserId(), socialVo.getTenantId());
+ loginUser.setClientKey(client.getClientKey());
+ loginUser.setDeviceType(client.getDeviceType());
+ SaLoginParameter model = new SaLoginParameter();
+ model.setDeviceType(client.getDeviceType());
+ // 自定义分配 不同用户体系 不同 token 授权时间 不设置默认走全局 yml 配置
+ // 例如: 后台用户30分钟过期 app用户1天过期
+ model.setTimeout(client.getTimeout());
+ model.setActiveTimeout(client.getActiveTimeout());
+ model.setExtra(LoginHelper.CLIENT_KEY, client.getClientId());
+ // 生成token
+ LoginHelper.login(loginUser, model);
+
+ LoginVo loginVo = new LoginVo();
+ loginVo.setAccessToken(StpUtil.getTokenValue());
+ loginVo.setExpireIn(StpUtil.getTokenTimeout());
+ loginVo.setClientId(client.getClientId());
+ return loginVo;
+ }
+
+}
diff --git a/ruoyi-auth/src/main/java/org/dromara/auth/service/impl/XcxAuthStrategy.java b/ruoyi-auth/src/main/java/org/dromara/auth/service/impl/XcxAuthStrategy.java
new file mode 100644
index 0000000..f3248f9
--- /dev/null
+++ b/ruoyi-auth/src/main/java/org/dromara/auth/service/impl/XcxAuthStrategy.java
@@ -0,0 +1,91 @@
+package org.dromara.auth.service.impl;
+
+import cn.dev33.satoken.stp.StpUtil;
+import cn.dev33.satoken.stp.parameter.SaLoginParameter;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import me.zhyd.oauth.config.AuthConfig;
+import me.zhyd.oauth.model.AuthCallback;
+import me.zhyd.oauth.model.AuthResponse;
+import me.zhyd.oauth.model.AuthToken;
+import me.zhyd.oauth.model.AuthUser;
+import me.zhyd.oauth.request.AuthRequest;
+import me.zhyd.oauth.request.AuthWechatMiniProgramRequest;
+import org.apache.dubbo.config.annotation.DubboReference;
+import org.dromara.auth.domain.vo.LoginVo;
+import org.dromara.auth.form.XcxLoginBody;
+import org.dromara.auth.service.IAuthStrategy;
+import org.dromara.auth.service.SysLoginService;
+import org.dromara.common.core.exception.ServiceException;
+import org.dromara.common.core.utils.ValidatorUtils;
+import org.dromara.common.json.utils.JsonUtils;
+import org.dromara.common.satoken.utils.LoginHelper;
+import org.dromara.system.api.RemoteUserService;
+import org.dromara.system.api.domain.vo.RemoteClientVo;
+import org.dromara.system.api.model.XcxLoginUser;
+import org.springframework.stereotype.Service;
+
+/**
+ * 邮件认证策略
+ *
+ * @author Michelle.Chung
+ */
+@Slf4j
+@Service("xcx" + IAuthStrategy.BASE_NAME)
+@RequiredArgsConstructor
+public class XcxAuthStrategy implements IAuthStrategy {
+
+ private final SysLoginService loginService;
+
+ @DubboReference
+ private RemoteUserService remoteUserService;
+
+ @Override
+ public LoginVo login(String body, RemoteClientVo client) {
+ XcxLoginBody loginBody = JsonUtils.parseObject(body, XcxLoginBody.class);
+ ValidatorUtils.validate(loginBody);
+ // xcxCode 为 小程序调用 wx.login 授权后获取
+ String xcxCode = loginBody.getXcxCode();
+ // 多个小程序识别使用
+ String appid = loginBody.getAppid();
+
+ // 校验 appid + appsrcret + xcxCode 调用登录凭证校验接口 获取 session_key 与 openid
+ AuthRequest authRequest = new AuthWechatMiniProgramRequest(AuthConfig.builder()
+ .clientId(appid).clientSecret("自行填写密钥 可根据不同appid填入不同密钥")
+ .ignoreCheckRedirectUri(true).ignoreCheckState(true).build());
+ AuthCallback authCallback = new AuthCallback();
+ authCallback.setCode(xcxCode);
+ AuthResponse resp = authRequest.login(authCallback);
+ String openid, unionId;
+ if (resp.ok()) {
+ AuthToken token = resp.getData().getToken();
+ openid = token.getOpenId();
+ // 微信小程序只有关联到微信开放平台下之后才能获取到 unionId,因此unionId不一定能返回。
+ unionId = token.getUnionId();
+ } else {
+ throw new ServiceException(resp.getMsg());
+ }
+ // todo getUserInfoByOpenid 方法内部查询逻辑需要自行根据业务实现
+ XcxLoginUser loginUser = remoteUserService.getUserInfoByOpenid(openid);
+ loginUser.setClientKey(client.getClientKey());
+ loginUser.setDeviceType(client.getDeviceType());
+
+ SaLoginParameter model = new SaLoginParameter();
+ model.setDeviceType(client.getDeviceType());
+ // 自定义分配 不同用户体系 不同 token 授权时间 不设置默认走全局 yml 配置
+ // 例如: 后台用户30分钟过期 app用户1天过期
+ model.setTimeout(client.getTimeout());
+ model.setActiveTimeout(client.getActiveTimeout());
+ model.setExtra(LoginHelper.CLIENT_KEY, client.getClientId());
+ // 生成token
+ LoginHelper.login(loginUser, model);
+
+ LoginVo loginVo = new LoginVo();
+ loginVo.setAccessToken(StpUtil.getTokenValue());
+ loginVo.setExpireIn(StpUtil.getTokenTimeout());
+ loginVo.setClientId(client.getClientId());
+ loginVo.setOpenid(openid);
+ return loginVo;
+ }
+
+}
diff --git a/ruoyi-auth/src/main/resources/application.yml b/ruoyi-auth/src/main/resources/application.yml
new file mode 100644
index 0000000..90c0535
--- /dev/null
+++ b/ruoyi-auth/src/main/resources/application.yml
@@ -0,0 +1,33 @@
+# Tomcat
+server:
+ port: 9210
+
+# Spring
+spring:
+ application:
+ # 应用名称
+ name: ruoyi-auth
+ profiles:
+ # 环境配置
+ active: @profiles.active@
+
+--- # nacos 配置
+spring:
+ cloud:
+ nacos:
+ # nacos 服务地址
+ server-addr: @nacos.server@
+ username: @nacos.username@
+ password: @nacos.password@
+ discovery:
+ # 注册组
+ group: @nacos.discovery.group@
+ namespace: ${spring.profiles.active}
+ config:
+ # 配置组
+ group: @nacos.config.group@
+ namespace: ${spring.profiles.active}
+ config:
+ import:
+ - optional:nacos:application-common.yml
+ - optional:nacos:${spring.application.name}.yml
diff --git a/ruoyi-auth/src/main/resources/banner.txt b/ruoyi-auth/src/main/resources/banner.txt
new file mode 100644
index 0000000..97c5c27
--- /dev/null
+++ b/ruoyi-auth/src/main/resources/banner.txt
@@ -0,0 +1,10 @@
+Spring Boot Version: ${spring-boot.version}
+Spring Application Name: ${spring.application.name}
+ _ _ _
+ (_) | | | |
+ _ __ _ _ ___ _ _ _ ______ __ _ _ _ | |_ | |__
+| '__|| | | | / _ \ | | | || ||______| / _` || | | || __|| '_ \
+| | | |_| || (_) || |_| || | | (_| || |_| || |_ | | | |
+|_| \__,_| \___/ \__, ||_| \__,_| \__,_| \__||_| |_|
+ __/ |
+ |___/
\ No newline at end of file
diff --git a/ruoyi-auth/src/main/resources/logback-plus.xml b/ruoyi-auth/src/main/resources/logback-plus.xml
new file mode 100644
index 0000000..680b0af
--- /dev/null
+++ b/ruoyi-auth/src/main/resources/logback-plus.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+ ${console.log.pattern}
+ utf-8
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ruoyi-common/pom.xml b/ruoyi-common/pom.xml
new file mode 100644
index 0000000..ca68699
--- /dev/null
+++ b/ruoyi-common/pom.xml
@@ -0,0 +1,57 @@
+
+
+
+ org.dromara
+ xny-cloud
+ ${revision}
+
+ 4.0.0
+
+
+ ruoyi-common-bom
+ ruoyi-common-alibaba-bom
+ ruoyi-common-log
+ ruoyi-common-service-impl
+ ruoyi-common-excel
+ ruoyi-common-core
+ ruoyi-common-redis
+ ruoyi-common-doc
+ ruoyi-common-security
+ ruoyi-common-satoken
+ ruoyi-common-web
+ ruoyi-common-mybatis
+ ruoyi-common-job
+ ruoyi-common-dubbo
+ ruoyi-common-seata
+ ruoyi-common-loadbalancer
+ ruoyi-common-oss
+ ruoyi-common-ratelimiter
+ ruoyi-common-idempotent
+ ruoyi-common-mail
+ ruoyi-common-sms
+ ruoyi-common-logstash
+ ruoyi-common-elasticsearch
+ ruoyi-common-sentinel
+ ruoyi-common-skylog
+ ruoyi-common-prometheus
+ ruoyi-common-translation
+ ruoyi-common-sensitive
+ ruoyi-common-json
+ ruoyi-common-encrypt
+ ruoyi-common-tenant
+ ruoyi-common-websocket
+ ruoyi-common-social
+ ruoyi-common-nacos
+ ruoyi-common-bus
+ ruoyi-common-sse
+
+
+ ruoyi-common
+ pom
+
+
+ ruoyi-common通用模块
+
+
+
diff --git a/ruoyi-common/ruoyi-common-alibaba-bom/pom.xml b/ruoyi-common/ruoyi-common-alibaba-bom/pom.xml
new file mode 100644
index 0000000..51a935a
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-alibaba-bom/pom.xml
@@ -0,0 +1,181 @@
+
+
+ 4.0.0
+
+ org.dromara
+ ruoyi-common-alibaba-bom
+ ${revision}
+ pom
+
+
+ ruoyi-common-alibaba-bom alibaba依赖项
+
+
+
+ 2.4.1
+ 2023.0.3.3
+ 1.8.8
+ 2.4.0
+ 2.5.1
+ 3.3.5
+ 3.3.1
+
+
+
+
+ com.alibaba.cloud
+ spring-cloud-alibaba-dependencies
+ ${spring-cloud-alibaba.version}
+ pom
+ import
+
+
+ com.alibaba.nacos
+ nacos-client
+ ${nacos.client.version}
+
+
+ com.alibaba.csp
+ sentinel-core
+ ${sentinel.version}
+
+
+ com.alibaba.csp
+ sentinel-parameter-flow-control
+ ${sentinel.version}
+
+
+ com.alibaba.csp
+ sentinel-datasource-extension
+ ${sentinel.version}
+
+
+ com.alibaba.csp
+ sentinel-datasource-apollo
+ ${sentinel.version}
+
+
+ com.alibaba.csp
+ sentinel-datasource-zookeeper
+ ${sentinel.version}
+
+
+ com.alibaba.csp
+ sentinel-datasource-nacos
+ ${sentinel.version}
+
+
+ com.alibaba.csp
+ sentinel-datasource-redis
+ ${sentinel.version}
+
+
+ com.alibaba.csp
+ sentinel-datasource-consul
+ ${sentinel.version}
+
+
+ com.alibaba.csp
+ sentinel-web-servlet
+ ${sentinel.version}
+
+
+ com.alibaba.csp
+ sentinel-spring-cloud-gateway-adapter
+ ${sentinel.version}
+
+
+ com.alibaba.csp
+ sentinel-transport-simple-http
+ ${sentinel.version}
+
+
+ com.alibaba.csp
+ sentinel-annotation-aspectj
+ ${sentinel.version}
+
+
+ com.alibaba.csp
+ sentinel-reactor-adapter
+ ${sentinel.version}
+
+
+ com.alibaba.csp
+ sentinel-cluster-server-default
+ ${sentinel.version}
+
+
+ com.alibaba.csp
+ sentinel-cluster-client-default
+ ${sentinel.version}
+
+
+ com.alibaba.csp
+ sentinel-spring-webflux-adapter
+ ${sentinel.version}
+
+
+ com.alibaba.csp
+ sentinel-api-gateway-adapter-common
+ ${sentinel.version}
+
+
+ com.alibaba.csp
+ sentinel-spring-webmvc-v6x-adapter
+ ${sentinel.version}
+
+
+ com.alibaba.csp
+ sentinel-dubbo-adapter
+ ${sentinel.version}
+
+
+ com.alibaba.csp
+ sentinel-apache-dubbo-adapter
+ ${sentinel.version}
+
+
+ com.alibaba.csp
+ sentinel-apache-dubbo3-adapter
+ ${sentinel.version}
+
+
+ org.apache.seata
+ seata-spring-boot-starter
+ ${seata.version}
+
+
+ org.apache.seata
+ seata-all
+ ${seata.version}
+
+
+
+
+ org.apache.dubbo
+ dubbo-spring-boot-starter
+ ${dubbo.version}
+
+
+
+ org.apache.dubbo
+ dubbo-spring-boot-actuator
+ ${dubbo.version}
+
+
+
+ org.apache.dubbo
+ dubbo
+ ${dubbo.version}
+
+
+
+ org.apache.dubbo.extensions
+ dubbo-metadata-report-redis
+ ${dubbo-extensions.version}
+
+
+
+
diff --git a/ruoyi-common/ruoyi-common-bom/pom.xml b/ruoyi-common/ruoyi-common-bom/pom.xml
new file mode 100644
index 0000000..449a1a7
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-bom/pom.xml
@@ -0,0 +1,262 @@
+
+
+ 4.0.0
+
+ org.dromara
+ ruoyi-common-bom
+ ${revision}
+ pom
+
+
+ ruoyi-common-bom common依赖项
+
+
+
+ 2.4.1
+
+
+
+
+
+
+ org.dromara
+ ruoyi-common-core
+ ${revision}
+
+
+
+
+ org.dromara
+ ruoyi-common-doc
+ ${revision}
+
+
+
+
+ org.dromara
+ ruoyi-common-security
+ ${revision}
+
+
+
+
+ org.dromara
+ ruoyi-common-satoken
+ ${revision}
+
+
+
+
+ org.dromara
+ ruoyi-common-log
+ ${revision}
+
+
+
+
+ org.dromara
+ ruoyi-common-service-impl
+ ${revision}
+
+
+
+
+ org.dromara
+ ruoyi-common-excel
+ ${revision}
+
+
+
+
+ org.dromara
+ ruoyi-common-redis
+ ${revision}
+
+
+
+
+ org.dromara
+ ruoyi-common-web
+ ${revision}
+
+
+
+
+ org.dromara
+ ruoyi-common-mybatis
+ ${revision}
+
+
+
+
+ org.dromara
+ ruoyi-common-job
+ ${revision}
+
+
+
+
+ org.dromara
+ ruoyi-common-dubbo
+ ${revision}
+
+
+
+
+ org.dromara
+ ruoyi-common-seata
+ ${revision}
+
+
+
+
+ org.dromara
+ ruoyi-common-loadbalancer
+ ${revision}
+
+
+
+
+ org.dromara
+ ruoyi-common-oss
+ ${revision}
+
+
+
+
+ org.dromara
+ ruoyi-common-ratelimiter
+ ${revision}
+
+
+
+
+ org.dromara
+ ruoyi-common-idempotent
+ ${revision}
+
+
+
+
+ org.dromara
+ ruoyi-common-mail
+ ${revision}
+
+
+
+
+ org.dromara
+ ruoyi-common-sms
+ ${revision}
+
+
+
+
+ org.dromara
+ ruoyi-common-logstash
+ ${revision}
+
+
+
+
+ org.dromara
+ ruoyi-common-elasticsearch
+ ${revision}
+
+
+
+
+ org.dromara
+ ruoyi-common-sentinel
+ ${revision}
+
+
+
+
+ org.dromara
+ ruoyi-common-skylog
+ ${revision}
+
+
+
+
+ org.dromara
+ ruoyi-common-prometheus
+ ${revision}
+
+
+
+
+ org.dromara
+ ruoyi-common-translation
+ ${revision}
+
+
+
+
+ org.dromara
+ ruoyi-common-sensitive
+ ${revision}
+
+
+
+
+ org.dromara
+ ruoyi-common-json
+ ${revision}
+
+
+
+
+ org.dromara
+ ruoyi-common-encrypt
+ ${revision}
+
+
+
+
+ org.dromara
+ ruoyi-common-tenant
+ ${revision}
+
+
+
+
+ org.dromara
+ ruoyi-common-websocket
+ ${revision}
+
+
+
+
+ org.dromara
+ ruoyi-common-social
+ ${revision}
+
+
+
+
+ org.dromara
+ ruoyi-common-nacos
+ ${revision}
+
+
+
+
+ org.dromara
+ ruoyi-common-bus
+ ${revision}
+
+
+
+
+ org.dromara
+ ruoyi-common-sse
+ ${revision}
+
+
+
+
+
diff --git a/ruoyi-common/ruoyi-common-bus/pom.xml b/ruoyi-common/ruoyi-common-bus/pom.xml
new file mode 100644
index 0000000..e0256f8
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-bus/pom.xml
@@ -0,0 +1,35 @@
+
+
+
+ org.dromara
+ ruoyi-common
+ ${revision}
+
+ 4.0.0
+
+ ruoyi-common-bus
+
+
+ ruoyi-common-bus 消息总线模块
+
+
+
+
+
+ org.springframework.cloud
+ spring-cloud-starter-bus-amqp
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ruoyi-common/ruoyi-common-bus/src/main/java/org/dromara/common/bus/config/BusCustomConfiguration.java b/ruoyi-common/ruoyi-common-bus/src/main/java/org/dromara/common/bus/config/BusCustomConfiguration.java
new file mode 100644
index 0000000..68423b4
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-bus/src/main/java/org/dromara/common/bus/config/BusCustomConfiguration.java
@@ -0,0 +1,15 @@
+package org.dromara.common.bus.config;
+
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.cloud.bus.jackson.RemoteApplicationEventScan;
+
+/**
+ * bus 配置
+ *
+ * @author Lion Li
+ */
+@AutoConfiguration
+@RemoteApplicationEventScan(basePackages = "${spring.cloud.bus.base-packages}")
+public class BusCustomConfiguration {
+
+}
diff --git a/ruoyi-common/ruoyi-common-bus/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/ruoyi-common/ruoyi-common-bus/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 0000000..3b5376e
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-bus/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -0,0 +1 @@
+org.dromara.common.bus.config.BusCustomConfiguration
diff --git a/ruoyi-common/ruoyi-common-core/pom.xml b/ruoyi-common/ruoyi-common-core/pom.xml
new file mode 100644
index 0000000..01d876e
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/pom.xml
@@ -0,0 +1,104 @@
+
+
+
+ org.dromara
+ ruoyi-common
+ ${revision}
+
+ 4.0.0
+
+ ruoyi-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
+
+
+
+ io.swagger.core.v3
+ swagger-annotations
+
+
+
+ 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
+
+
+
+
+
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/config/ApplicationConfig.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/config/ApplicationConfig.java
new file mode 100644
index 0000000..d9f70e4
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/config/ApplicationConfig.java
@@ -0,0 +1,17 @@
+package org.dromara.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/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/config/AsyncConfig.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/config/AsyncConfig.java
new file mode 100644
index 0000000..cd01e33
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/config/AsyncConfig.java
@@ -0,0 +1,52 @@
+package org.dromara.common.core.config;
+
+import cn.hutool.core.util.ArrayUtil;
+import org.dromara.common.core.exception.ServiceException;
+import org.dromara.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/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/config/ThreadPoolConfig.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/config/ThreadPoolConfig.java
new file mode 100644
index 0000000..2c38129
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/config/ThreadPoolConfig.java
@@ -0,0 +1,70 @@
+package org.dromara.common.core.config;
+
+import jakarta.annotation.PreDestroy;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.concurrent.BasicThreadFactory;
+import org.dromara.common.core.utils.SpringUtils;
+import org.dromara.common.core.utils.Threads;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.context.annotation.Bean;
+import org.springframework.core.task.VirtualThreadTaskExecutor;
+
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+import java.util.concurrent.ThreadPoolExecutor;
+
+/**
+ * 线程池配置
+ *
+ * @author Lion Li
+ **/
+@Slf4j
+@AutoConfiguration
+public class ThreadPoolConfig {
+
+ /**
+ * 核心线程数 = cpu 核心数 + 1
+ */
+ private final int core = Runtime.getRuntime().availableProcessors() + 1;
+
+ private ScheduledExecutorService scheduledExecutorService;
+
+ /**
+ * 执行周期性或定时任务
+ */
+ @Bean(name = "scheduledExecutorService")
+ protected ScheduledExecutorService scheduledExecutorService() {
+ // daemon 必须为 true
+ BasicThreadFactory.Builder builder = new BasicThreadFactory.Builder().daemon(true);
+ if (SpringUtils.isVirtual()) {
+ builder.namingPattern("virtual-schedule-pool-%d").wrappedFactory(new VirtualThreadTaskExecutor().getVirtualThreadFactory());
+ } else {
+ builder.namingPattern("schedule-pool-%d");
+ }
+ ScheduledThreadPoolExecutor scheduledThreadPoolExecutor = new ScheduledThreadPoolExecutor(core,
+ builder.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/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/config/ValidatorConfig.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/config/ValidatorConfig.java
new file mode 100644
index 0000000..ddcd836
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/config/ValidatorConfig.java
@@ -0,0 +1,41 @@
+package org.dromara.common.core.config;
+
+import jakarta.validation.Validator;
+import org.hibernate.validator.HibernateValidator;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration;
+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(before = ValidationAutoConfiguration.class)
+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();
+ // 设置快速失败模式(fail-fast),即校验过程中一旦遇到失败,立即停止并返回错误
+ properties.setProperty("hibernate.validator.fail_fast", "true");
+ factoryBean.setValidationProperties(properties);
+ // 加载配置
+ factoryBean.afterPropertiesSet();
+ return factoryBean.getValidator();
+ }
+ }
+
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/CacheConstants.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/CacheConstants.java
new file mode 100644
index 0000000..ceb8370
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/CacheConstants.java
@@ -0,0 +1,30 @@
+package org.dromara.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:";
+
+ /**
+ * 登录账户密码错误次数 redis key
+ */
+ String PWD_ERR_CNT_KEY = "pwd_err_cnt:";
+
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/CacheNames.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/CacheNames.java
new file mode 100644
index 0000000..c38f39b
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/CacheNames.java
@@ -0,0 +1,89 @@
+package org.dromara.common.core.constant;
+
+/**
+ * 缓存组名称常量
+ *
+ * key 格式为 cacheNames#ttl#maxIdleTime#maxSize#local
+ *
+ * ttl 过期时间 如果设置为0则不过期 默认为0
+ * maxIdleTime 最大空闲时间 根据LRU算法清理空闲数据 如果设置为0则不检测 默认为0
+ * maxSize 组最大长度 根据LRU算法清理溢出数据 如果设置为0则无限长 默认为0
+ * local 默认开启本地缓存为1 关闭本地缓存为0
+ *
+ * 例子: test#60s、test#0#60s、test#0#1m#1000、test#1h#0#500、test#1h#0#500#0
+ *
+ * @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_DICT_TYPE = "sys_dict_type";
+
+ /**
+ * 租户
+ */
+ 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";
+
+ /**
+ * 角色自定义权限
+ */
+ String SYS_ROLE_CUSTOM = "sys_role_custom#30d";
+
+ /**
+ * 部门及以下权限
+ */
+ String SYS_DEPT_AND_CHILD = "sys_dept_and_child#30d";
+
+ /**
+ * OSS配置
+ */
+ String SYS_OSS_CONFIG = GlobalConstants.GLOBAL_REDIS_KEY + "sys_oss_config";
+
+ /**
+ * 在线用户
+ */
+ String ONLINE_TOKEN = "online_tokens";
+
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/Constants.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/Constants.java
new file mode 100644
index 0000000..d031921
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/Constants.java
@@ -0,0 +1,76 @@
+package org.dromara.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;
+
+ /**
+ * 顶级部门id
+ */
+ Long TOP_PARENT_ID = 0L;
+
+}
+
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/GlobalConstants.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/GlobalConstants.java
new file mode 100644
index 0000000..73f62a3
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/GlobalConstants.java
@@ -0,0 +1,35 @@
+package org.dromara.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 SOCIAL_AUTH_CODE_KEY = GLOBAL_REDIS_KEY + "social_auth_codes:";
+
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/HttpStatus.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/HttpStatus.java
new file mode 100644
index 0000000..85566e8
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/HttpStatus.java
@@ -0,0 +1,93 @@
+package org.dromara.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/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/RegexConstants.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/RegexConstants.java
new file mode 100644
index 0000000..f1e04f7
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/RegexConstants.java
@@ -0,0 +1,59 @@
+package org.dromara.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_]*$";
+
+ /**
+ * 权限标识必须符合以下格式:
+ * 1. 标准格式:xxx:yyy:zzz
+ * - 第一部分(xxx):只能包含字母、数字和下划线(_),不能使用 `*`
+ * - 第二部分(yyy):可以包含字母、数字、下划线(_)和 `*`
+ * - 第三部分(zzz):可以包含字母、数字、下划线(_)和 `*`
+ * 2. 允许空字符串(""),表示没有权限标识
+ */
+ 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/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/SystemConstants.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/SystemConstants.java
new file mode 100644
index 0000000..b2e025c
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/SystemConstants.java
@@ -0,0 +1,74 @@
+package org.dromara.common.core.constant;
+
+/**
+ * 用户常量信息
+ *
+ * @author Lion Li
+ */
+public interface SystemConstants {
+
+ /**
+ * 正常状态
+ */
+ String NORMAL = "0";
+
+ /**
+ * 异常状态
+ */
+ String DISABLE = "1";
+
+ /**
+ * 是否为系统默认(是)
+ */
+ String YES = "Y";
+
+ /**
+ * 是否菜单外链(是)
+ */
+ String YES_FRAME = "0";
+
+ /**
+ * 是否菜单外链(否)
+ */
+ String NO_FRAME = "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";
+
+ /**
+ * 超级管理员ID
+ */
+ Long SUPER_ADMIN_ID = 1L;
+
+ /**
+ * 根部门祖级列表
+ */
+ String ROOT_DEPT_ANCESTORS = "0";
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/TenantConstants.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/TenantConstants.java
new file mode 100644
index 0000000..33ce0cf
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/TenantConstants.java
@@ -0,0 +1,35 @@
+package org.dromara.common.core.constant;
+
+/**
+ * 租户常量信息
+ *
+ * @author Lion Li
+ */
+public interface TenantConstants {
+
+ /**
+ * 超级管理员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/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/R.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/R.java
new file mode 100644
index 0000000..625f58b
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/R.java
@@ -0,0 +1,120 @@
+package org.dromara.common.core.domain;
+
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.dromara.common.core.constant.HttpStatus;
+
+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/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/model/LoginBody.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/model/LoginBody.java
new file mode 100644
index 0000000..ee612fd
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/model/LoginBody.java
@@ -0,0 +1,43 @@
+package org.dromara.common.core.domain.model;
+
+import jakarta.validation.constraints.NotBlank;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * 用户登录对象
+ *
+ * @author Lion Li
+ */
+@Data
+@NoArgsConstructor
+public class LoginBody {
+
+ /**
+ * 客户端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/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/enums/BusinessStatusEnum.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/enums/BusinessStatusEnum.java
new file mode 100644
index 0000000..c1660ee
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/enums/BusinessStatusEnum.java
@@ -0,0 +1,215 @@
+package org.dromara.common.core.enums;
+
+import cn.hutool.core.util.StrUtil;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import org.dromara.common.core.exception.ServiceException;
+import org.dromara.common.core.utils.StringUtils;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+/**
+ * 业务状态枚举
+ *
+ * @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;
+
+ private static final Map STATUS_MAP = Arrays.stream(BusinessStatusEnum.values())
+ .collect(Collectors.toConcurrentMap(BusinessStatusEnum::getStatus, Function.identity()));
+
+ /**
+ * 根据状态获取对应的 BusinessStatusEnum 枚举
+ *
+ * @param status 业务状态码
+ * @return 对应的 BusinessStatusEnum 枚举,如果找不到则返回 null
+ */
+ public static BusinessStatusEnum getByStatus(String status) {
+ // 使用 STATUS_MAP 获取对应的枚举,若找不到则返回 null
+ return STATUS_MAP.get(status);
+ }
+
+ /**
+ * 根据状态获取对应的业务状态描述信息
+ *
+ * @param status 业务状态码
+ * @return 返回业务状态描述,若状态码为空或未找到对应的枚举,返回空字符串
+ */
+ public static String findByStatus(String status) {
+ if (StringUtils.isBlank(status)) {
+ return StrUtil.EMPTY;
+ }
+ BusinessStatusEnum statusEnum = STATUS_MAP.get(status);
+ return (statusEnum != null) ? statusEnum.getDesc() : StrUtil.EMPTY;
+ }
+
+ /**
+ * 判断是否为指定的状态之一:草稿、已撤销或已退回
+ *
+ * @param status 要检查的状态
+ * @return 如果状态为草稿、已撤销或已退回之一,则返回 true;否则返回 false
+ */
+ public static boolean isDraftOrCancelOrBack(String status) {
+ return DRAFT.status.equals(status) || CANCEL.status.equals(status) || BACK.status.equals(status);
+ }
+
+ /**
+ * 判断是否为撤销,退回,作废,终止
+ *
+ * @param status status
+ * @return 结果
+ */
+ public static boolean initialState(String status) {
+ return CANCEL.status.equals(status) || BACK.status.equals(status) || INVALID.status.equals(status) || TERMINATION.status.equals(status);
+ }
+
+ /**
+ * 获取运行中的实例状态列表
+ *
+ * @return 包含运行中实例状态的不可变列表
+ * (包含 DRAFT、WAITING、BACK 和 CANCEL 状态)
+ */
+ public static List runningStatus() {
+ return Arrays.asList(DRAFT.status, WAITING.status, BACK.status, CANCEL.status);
+ }
+
+ /**
+ * 获取结束实例的状态列表
+ *
+ * @return 包含结束实例状态的不可变列表
+ * (包含 FINISH、INVALID 和 TERMINATION 状态)
+ */
+ public static List finishStatus() {
+ return Arrays.asList(FINISH.status, INVALID.status, TERMINATION.status);
+ }
+
+ /**
+ * 启动流程校验
+ *
+ * @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/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/enums/DeviceType.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/enums/DeviceType.java
new file mode 100644
index 0000000..1667ac7
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/enums/DeviceType.java
@@ -0,0 +1,39 @@
+package org.dromara.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");
+
+ /**
+ * 设备标识
+ */
+ private final String device;
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/enums/FormatsType.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/enums/FormatsType.java
new file mode 100644
index 0000000..8d4b6d9
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/enums/FormatsType.java
@@ -0,0 +1,146 @@
+package org.dromara.common.core.enums;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import org.dromara.common.core.utils.StringUtils;
+
+/*
+ * 日期格式
+ * "yyyy":4位数的年份,例如:2023年表示为"2023"。
+ * "yy":2位数的年份,例如:2023年表示为"23"。
+ * "MM":2位数的月份,取值范围为01到12,例如:7月表示为"07"。
+ * "M":不带前导零的月份,取值范围为1到12,例如:7月表示为"7"。
+ * "dd":2位数的日期,取值范围为01到31,例如:22日表示为"22"。
+ * "d":不带前导零的日期,取值范围为1到31,例如:22日表示为"22"。
+ * "EEEE":星期的全名,例如:星期三表示为"Wednesday"。
+ * "E":星期的缩写,例如:星期三表示为"Wed"。
+ * "DDD" 或 "D":一年中的第几天,取值范围为001到366,例如:第200天表示为"200"。
+ * 时间格式
+ * "HH":24小时制的小时数,取值范围为00到23,例如:下午5点表示为"17"。
+ * "hh":12小时制的小时数,取值范围为01到12,例如:下午5点表示为"05"。
+ * "mm":分钟数,取值范围为00到59,例如:30分钟表示为"30"。
+ * "ss":秒数,取值范围为00到59,例如:45秒表示为"45"。
+ * "SSS":毫秒数,取值范围为000到999,例如:123毫秒表示为"123"。
+ */
+
+/**
+ * 日期格式与时间格式枚举
+ */
+@Getter
+@AllArgsConstructor
+public enum FormatsType {
+
+ /**
+ * 例如:2023年表示为"23"
+ */
+ YY("yy"),
+
+ /**
+ * 例如:2023年表示为"2023"
+ */
+ YYYY("yyyy"),
+
+ /**
+ * 例例如,2023年7月可以表示为 "2023-07"
+ */
+ YYYY_MM("yyyy-MM"),
+
+ /**
+ * 例如,日期 "2023年7月22日" 可以表示为 "2023-07-22"
+ */
+ YYYY_MM_DD("yyyy-MM-dd"),
+
+ /**
+ * 例如,当前时间如果是 "2023年7月22日下午3点30分",则可以表示为 "2023-07-22 15:30"
+ */
+ YYYY_MM_DD_HH_MM("yyyy-MM-dd HH:mm"),
+
+ /**
+ * 例如,当前时间如果是 "2023年7月22日下午3点30分45秒",则可以表示为 "2023-07-22 15:30:45"
+ */
+ YYYY_MM_DD_HH_MM_SS("yyyy-MM-dd HH:mm:ss"),
+
+ /**
+ * 例如:下午3点30分45秒,表示为 "15:30:45"
+ */
+ HH_MM_SS("HH:mm:ss"),
+
+ /**
+ * 例例如,2023年7月可以表示为 "2023/07"
+ */
+ YYYY_MM_SLASH("yyyy/MM"),
+
+ /**
+ * 例如,日期 "2023年7月22日" 可以表示为 "2023/07/22"
+ */
+ YYYY_MM_DD_SLASH("yyyy/MM/dd"),
+
+ /**
+ * 例如,当前时间如果是 "2023年7月22日下午3点30分45秒",则可以表示为 "2023/07/22 15:30:45"
+ */
+ YYYY_MM_DD_HH_MM_SLASH("yyyy/MM/dd HH:mm"),
+
+ /**
+ * 例如,当前时间如果是 "2023年7月22日下午3点30分45秒",则可以表示为 "2023/07/22 15:30:45"
+ */
+ YYYY_MM_DD_HH_MM_SS_SLASH("yyyy/MM/dd HH:mm:ss"),
+
+ /**
+ * 例例如,2023年7月可以表示为 "2023.07"
+ */
+ YYYY_MM_DOT("yyyy.MM"),
+
+ /**
+ * 例如,日期 "2023年7月22日" 可以表示为 "2023.07.22"
+ */
+ YYYY_MM_DD_DOT("yyyy.MM.dd"),
+
+ /**
+ * 例如,当前时间如果是 "2023年7月22日下午3点30分",则可以表示为 "2023.07.22 15:30"
+ */
+ YYYY_MM_DD_HH_MM_DOT("yyyy.MM.dd HH:mm"),
+
+ /**
+ * 例如,当前时间如果是 "2023年7月22日下午3点30分45秒",则可以表示为 "2023.07.22 15:30:45"
+ */
+ YYYY_MM_DD_HH_MM_SS_DOT("yyyy.MM.dd HH:mm:ss"),
+
+ /**
+ * 例如,2023年7月可以表示为 "202307"
+ */
+ YYYYMM("yyyyMM"),
+
+ /**
+ * 例如,2023年7月22日可以表示为 "20230722"
+ */
+ YYYYMMDD("yyyyMMdd"),
+
+ /**
+ * 例如,2023年7月22日下午3点可以表示为 "2023072215"
+ */
+ YYYYMMDDHH("yyyyMMddHH"),
+
+ /**
+ * 例如,2023年7月22日下午3点30分可以表示为 "202307221530"
+ */
+ YYYYMMDDHHMM("yyyyMMddHHmm"),
+
+ /**
+ * 例如,2023年7月22日下午3点30分45秒可以表示为 "20230722153045"
+ */
+ YYYYMMDDHHMMSS("yyyyMMddHHmmss");
+
+ /**
+ * 时间格式
+ */
+ private final String timeFormat;
+
+ public static FormatsType getFormatsType(String str) {
+ for (FormatsType value : values()) {
+ if (StringUtils.contains(str, value.getTimeFormat())) {
+ return value;
+ }
+ }
+ throw new RuntimeException("'FormatsType' not found By " + str);
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/enums/LoginType.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/enums/LoginType.java
new file mode 100644
index 0000000..f9cac66
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/enums/LoginType.java
@@ -0,0 +1,44 @@
+package org.dromara.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/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/enums/UserStatus.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/enums/UserStatus.java
new file mode 100644
index 0000000..be7e44d
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/enums/UserStatus.java
@@ -0,0 +1,30 @@
+package org.dromara.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/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/enums/UserType.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/enums/UserType.java
new file mode 100644
index 0000000..636988f
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/enums/UserType.java
@@ -0,0 +1,39 @@
+package org.dromara.common.core.enums;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import org.dromara.common.core.utils.StringUtils;
+
+/**
+ * 用户类型
+ *
+ * @author Lion Li
+ */
+@Getter
+@AllArgsConstructor
+public enum UserType {
+
+ /**
+ * 后台系统用户
+ */
+ SYS_USER("sys_user"),
+
+ /**
+ * 移动客户端用户
+ */
+ APP_USER("app_user");
+
+ /**
+ * 用户类型标识(用于 token、权限识别等)
+ */
+ 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/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/exception/ServiceException.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/exception/ServiceException.java
new file mode 100644
index 0000000..4fb097a
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/exception/ServiceException.java
@@ -0,0 +1,70 @@
+package org.dromara.common.core.exception;
+
+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 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;
+ }
+
+ public String getDetailMessage() {
+ return detailMessage;
+ }
+
+ @Override
+ public String getMessage() {
+ return message;
+ }
+
+ public Integer getCode() {
+ return code;
+ }
+
+ public ServiceException setMessage(String message) {
+ this.message = message;
+ return this;
+ }
+
+ public ServiceException setDetailMessage(String detailMessage) {
+ this.detailMessage = detailMessage;
+ return this;
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/exception/SseException.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/exception/SseException.java
new file mode 100644
index 0000000..a76e16d
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/exception/SseException.java
@@ -0,0 +1,62 @@
+package org.dromara.common.core.exception;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+
+import java.io.Serial;
+
+/**
+ * sse 特制异常
+ *
+ * @author LionLi
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@NoArgsConstructor
+@AllArgsConstructor
+public final class SseException extends RuntimeException {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 错误码
+ */
+ private Integer code;
+
+ /**
+ * 错误提示
+ */
+ private String message;
+
+ /**
+ * 错误明细,内部调试错误
+ */
+ private String detailMessage;
+
+ public SseException(String message) {
+ this.message = message;
+ }
+
+ public SseException(String message, Integer code) {
+ this.message = message;
+ this.code = code;
+ }
+
+ @Override
+ public String getMessage() {
+ return message;
+ }
+
+ public SseException setMessage(String message) {
+ this.message = message;
+ return this;
+ }
+
+ public SseException setDetailMessage(String detailMessage) {
+ this.detailMessage = detailMessage;
+ return this;
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/exception/base/BaseException.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/exception/base/BaseException.java
new file mode 100644
index 0000000..8df8acf
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/exception/base/BaseException.java
@@ -0,0 +1,73 @@
+package org.dromara.common.core.exception.base;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+import org.dromara.common.core.utils.MessageUtils;
+import org.dromara.common.core.utils.StringUtils;
+
+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/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/exception/file/FileException.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/exception/file/FileException.java
new file mode 100644
index 0000000..d374fc0
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/exception/file/FileException.java
@@ -0,0 +1,21 @@
+package org.dromara.common.core.exception.file;
+
+import org.dromara.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/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/exception/file/FileNameLengthLimitExceededException.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/exception/file/FileNameLengthLimitExceededException.java
new file mode 100644
index 0000000..af98124
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/exception/file/FileNameLengthLimitExceededException.java
@@ -0,0 +1,18 @@
+package org.dromara.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/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/exception/file/FileSizeLimitExceededException.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/exception/file/FileSizeLimitExceededException.java
new file mode 100644
index 0000000..1eb8d40
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/exception/file/FileSizeLimitExceededException.java
@@ -0,0 +1,18 @@
+package org.dromara.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/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/exception/user/CaptchaException.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/exception/user/CaptchaException.java
new file mode 100644
index 0000000..a18e581
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/exception/user/CaptchaException.java
@@ -0,0 +1,21 @@
+package org.dromara.common.core.exception.user;
+
+import java.io.Serial;
+
+/**
+ * 验证码错误异常类
+ *
+ * @author Lion Li
+ */
+public class CaptchaException extends UserException {
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ public CaptchaException() {
+ super("user.jcaptcha.error");
+ }
+
+ public CaptchaException(String msg) {
+ super(msg);
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/exception/user/CaptchaExpireException.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/exception/user/CaptchaExpireException.java
new file mode 100644
index 0000000..f4b8cac
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/exception/user/CaptchaExpireException.java
@@ -0,0 +1,18 @@
+package org.dromara.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/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/exception/user/UserException.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/exception/user/UserException.java
new file mode 100644
index 0000000..024fed6
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/exception/user/UserException.java
@@ -0,0 +1,20 @@
+package org.dromara.common.core.exception.user;
+
+import org.dromara.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/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/factory/RegexPatternPoolFactory.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/factory/RegexPatternPoolFactory.java
new file mode 100644
index 0000000..fd907d2
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/factory/RegexPatternPoolFactory.java
@@ -0,0 +1,52 @@
+package org.dromara.common.core.factory;
+
+import cn.hutool.core.lang.PatternPool;
+import org.dromara.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/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/factory/YmlPropertySourceFactory.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/factory/YmlPropertySourceFactory.java
new file mode 100644
index 0000000..af61b90
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/factory/YmlPropertySourceFactory.java
@@ -0,0 +1,31 @@
+package org.dromara.common.core.factory;
+
+import org.dromara.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/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/service/DictService.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/service/DictService.java
new file mode 100644
index 0000000..9f2632f
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/service/DictService.java
@@ -0,0 +1,63 @@
+package org.dromara.common.core.service;
+
+import org.dromara.common.core.utils.StringUtils;
+
+import java.util.Map;
+
+/**
+ * 字典服务服务
+ *
+ * @author Lion Li
+ */
+public interface DictService {
+
+ /**
+ * 根据字典类型和字典值获取字典标签
+ *
+ * @param dictType 字典类型
+ * @param dictValue 字典值
+ * @return 字典标签
+ */
+ default String getDictLabel(String dictType, String dictValue) {
+ return getDictLabel(dictType, dictValue, StringUtils.SEPARATOR);
+ }
+
+ /**
+ * 根据字典类型和字典标签获取字典值
+ *
+ * @param dictType 字典类型
+ * @param dictLabel 字典标签
+ * @return 字典值
+ */
+ default String getDictValue(String dictType, String dictLabel) {
+ return getDictValue(dictType, dictLabel, StringUtils.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/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/service/PermissionService.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/service/PermissionService.java
new file mode 100644
index 0000000..d7db79a
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/service/PermissionService.java
@@ -0,0 +1,28 @@
+package org.dromara.common.core.service;
+
+import java.util.Set;
+
+/**
+ * 用户权限处理
+ *
+ * @author Lion Li
+ */
+public interface PermissionService {
+
+ /**
+ * 获取角色数据权限
+ *
+ * @param userId 用户id
+ * @return 角色权限信息
+ */
+ Set getRolePermission(Long userId);
+
+ /**
+ * 获取菜单数据权限
+ *
+ * @param userId 用户id
+ * @return 菜单权限信息
+ */
+ Set getMenuPermission(Long userId);
+
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/DateUtils.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/DateUtils.java
new file mode 100644
index 0000000..b52d95e
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/DateUtils.java
@@ -0,0 +1,300 @@
+package org.dromara.common.core.utils;
+
+import org.apache.commons.lang3.time.DateFormatUtils;
+import org.dromara.common.core.enums.FormatsType;
+import org.dromara.common.core.exception.ServiceException;
+
+import java.lang.management.ManagementFactory;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.time.*;
+import java.util.Date;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 时间工具类
+ *
+ * @author ruoyi
+ */
+public class DateUtils extends org.apache.commons.lang3.time.DateUtils {
+ 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"};
+
+ @Deprecated
+ private DateUtils() {
+ }
+
+ /**
+ * 获取当前日期和时间
+ *
+ * @return 当前日期和时间的 Date 对象表示
+ */
+ public static Date getNowDate() {
+ return new Date();
+ }
+
+ /**
+ * 获取当前日期的字符串表示,格式为YYYY-MM-DD
+ *
+ * @return 当前日期的字符串表示
+ */
+ public static String getDate() {
+ return dateTimeNow(FormatsType.YYYY_MM_DD);
+ }
+
+ /**
+ * 获取当前日期的字符串表示,格式为yyyyMMdd
+ *
+ * @return 当前日期的字符串表示
+ */
+ public static String getCurrentDate() {
+ return DateFormatUtils.format(new Date(), FormatsType.YYYYMMDD.getTimeFormat());
+ }
+
+ /**
+ * 获取当前日期的路径格式字符串,格式为"yyyy/MM/dd"
+ *
+ * @return 当前日期的路径格式字符串
+ */
+ public static String datePath() {
+ Date now = new Date();
+ return DateFormatUtils.format(now, FormatsType.YYYY_MM_DD_SLASH.getTimeFormat());
+ }
+
+ /**
+ * 获取当前时间的字符串表示,格式为YYYY-MM-DD HH:MM:SS
+ *
+ * @return 当前时间的字符串表示
+ */
+ public static String getTime() {
+ return dateTimeNow(FormatsType.YYYY_MM_DD_HH_MM_SS);
+ }
+
+ /**
+ * 获取当前时间的字符串表示,格式为 "HH:MM:SS"
+ *
+ * @return 当前时间的字符串表示,格式为 "HH:MM:SS"
+ */
+ public static String getTimeWithHourMinuteSecond() {
+ return dateTimeNow(FormatsType.HH_MM_SS);
+ }
+
+ /**
+ * 获取当前日期和时间的字符串表示,格式为YYYYMMDDHHMMSS
+ *
+ * @return 当前日期和时间的字符串表示
+ */
+ public static String dateTimeNow() {
+ return dateTimeNow(FormatsType.YYYYMMDDHHMMSS);
+ }
+
+ /**
+ * 获取当前日期和时间的指定格式的字符串表示
+ *
+ * @param format 日期时间格式,例如"YYYY-MM-DD HH:MM:SS"
+ * @return 当前日期和时间的字符串表示
+ */
+ public static String dateTimeNow(final FormatsType format) {
+ return parseDateToStr(format, new Date());
+ }
+
+ /**
+ * 将指定日期格式化为 YYYY-MM-DD 格式的字符串
+ *
+ * @param date 要格式化的日期对象
+ * @return 格式化后的日期字符串
+ */
+ public static String formatDate(final Date date) {
+ return parseDateToStr(FormatsType.YYYY_MM_DD, date);
+ }
+
+ /**
+ * 将指定日期格式化为 YYYY-MM-DD HH:MM:SS 格式的字符串
+ *
+ * @param date 要格式化的日期对象
+ * @return 格式化后的日期时间字符串
+ */
+ public static String formatDateTime(final Date date) {
+ return parseDateToStr(FormatsType.YYYY_MM_DD_HH_MM_SS, date);
+ }
+
+ /**
+ * 将指定日期按照指定格式进行格式化
+ *
+ * @param format 要使用的日期时间格式,例如"YYYY-MM-DD HH:MM:SS"
+ * @param date 要格式化的日期对象
+ * @return 格式化后的日期时间字符串
+ */
+ public static String parseDateToStr(final FormatsType format, final Date date) {
+ return new SimpleDateFormat(format.getTimeFormat()).format(date);
+ }
+
+ /**
+ * 将指定格式的日期时间字符串转换为 Date 对象
+ *
+ * @param format 要解析的日期时间格式,例如"YYYY-MM-DD HH:MM:SS"
+ * @param ts 要解析的日期时间字符串
+ * @return 解析后的 Date 对象
+ * @throws RuntimeException 如果解析过程中发生异常
+ */
+ public static Date parseDateTime(final FormatsType format, final String ts) {
+ try {
+ return new SimpleDateFormat(format.getTimeFormat()).parse(ts);
+ } catch (ParseException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * 将对象转换为日期对象
+ *
+ * @param str 要转换的对象,通常是字符串
+ * @return 转换后的日期对象,如果转换失败或输入为null,则返回null
+ */
+ public static Date parseDate(Object str) {
+ if (str == null) {
+ return null;
+ }
+ try {
+ return parseDate(str.toString(), PARSE_PATTERNS);
+ } catch (ParseException e) {
+ return null;
+ }
+ }
+
+ /**
+ * 获取服务器启动时间
+ *
+ * @return 服务器启动时间的 Date 对象表示
+ */
+ public static Date getServerStartDate() {
+ long time = ManagementFactory.getRuntimeMXBean().getStartTime();
+ return new Date(time);
+ }
+
+ /**
+ * 计算两个时间之间的时间差,并以指定单位返回(绝对值)
+ *
+ * @param start 起始时间
+ * @param end 结束时间
+ * @param unit 所需返回的时间单位(DAYS、HOURS、MINUTES、SECONDS、MILLISECONDS、MICROSECONDS、NANOSECONDS)
+ * @return 时间差的绝对值,以指定单位表示
+ */
+ public static long difference(Date start, Date end, TimeUnit unit) {
+ // 计算时间差,单位为毫秒,取绝对值避免负数
+ long diffInMillis = Math.abs(end.getTime() - start.getTime());
+
+ // 根据目标单位转换时间差
+ return switch (unit) {
+ case DAYS -> diffInMillis / TimeUnit.DAYS.toMillis(1);
+ case HOURS -> diffInMillis / TimeUnit.HOURS.toMillis(1);
+ case MINUTES -> diffInMillis / TimeUnit.MINUTES.toMillis(1);
+ case SECONDS -> diffInMillis / TimeUnit.SECONDS.toMillis(1);
+ case MILLISECONDS -> diffInMillis;
+ case MICROSECONDS -> TimeUnit.MILLISECONDS.toMicros(diffInMillis);
+ case NANOSECONDS -> TimeUnit.MILLISECONDS.toNanos(diffInMillis);
+ };
+ }
+
+ /**
+ * 计算两个日期之间的时间差,并以天、小时和分钟的格式返回
+ *
+ * @param endDate 结束日期
+ * @param nowDate 当前日期
+ * @return 表示时间差的字符串,格式为"天 小时 分钟"
+ */
+ public static String getDatePoor(Date endDate, Date nowDate) {
+ long diffInMillis = endDate.getTime() - nowDate.getTime();
+ long day = TimeUnit.MILLISECONDS.toDays(diffInMillis);
+ long hour = TimeUnit.MILLISECONDS.toHours(diffInMillis) % 24;
+ long min = TimeUnit.MILLISECONDS.toMinutes(diffInMillis) % 60;
+ return String.format("%d天 %d小时 %d分钟", day, hour, min);
+ }
+
+ /**
+ * 计算两个时间点的差值(天、小时、分钟、秒),当值为0时不显示该单位
+ *
+ * @param endDate 结束时间
+ * @param nowDate 当前时间
+ * @return 时间差字符串,格式为 "x天 x小时 x分钟 x秒",若为 0 则不显示
+ */
+ public static String getTimeDifference(Date endDate, Date nowDate) {
+ long diffInMillis = endDate.getTime() - nowDate.getTime();
+ long day = TimeUnit.MILLISECONDS.toDays(diffInMillis);
+ long hour = TimeUnit.MILLISECONDS.toHours(diffInMillis) % 24;
+ long min = TimeUnit.MILLISECONDS.toMinutes(diffInMillis) % 60;
+ long sec = TimeUnit.MILLISECONDS.toSeconds(diffInMillis) % 60;
+ // 构建时间差字符串,条件是值不为0才显示
+ StringBuilder result = new StringBuilder();
+ if (day > 0) {
+ result.append(String.format("%d天 ", day));
+ }
+ if (hour > 0) {
+ result.append(String.format("%d小时 ", hour));
+ }
+ if (min > 0) {
+ result.append(String.format("%d分钟 ", min));
+ }
+ if (sec > 0) {
+ result.append(String.format("%d秒", sec));
+ }
+ return result.length() > 0 ? result.toString().trim() : "0秒";
+ }
+
+ /**
+ * 将 LocalDateTime 对象转换为 Date 对象
+ *
+ * @param temporalAccessor 要转换的 LocalDateTime 对象
+ * @return 转换后的 Date 对象
+ */
+ public static Date toDate(LocalDateTime temporalAccessor) {
+ ZonedDateTime zdt = temporalAccessor.atZone(ZoneId.systemDefault());
+ return Date.from(zdt.toInstant());
+ }
+
+ /**
+ * 将 LocalDate 对象转换为 Date 对象
+ *
+ * @param temporalAccessor 要转换的 LocalDate 对象
+ * @return 转换后的 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());
+ }
+
+ /**
+ * 校验日期范围
+ *
+ * @param startDate 开始日期
+ * @param endDate 结束日期
+ * @param maxValue 最大时间跨度的限制值
+ * @param unit 时间跨度的单位,可选择 "DAYS"、"HOURS" 或 "MINUTES"
+ */
+ public static void validateDateRange(Date startDate, Date endDate, int maxValue, TimeUnit unit) {
+ // 校验结束日期不能早于开始日期
+ if (endDate.before(startDate)) {
+ throw new ServiceException("结束日期不能早于开始日期");
+ }
+
+ // 计算时间跨度
+ long diffInMillis = endDate.getTime() - startDate.getTime();
+
+ // 根据单位转换时间跨度
+ long diff = switch (unit) {
+ case DAYS -> TimeUnit.MILLISECONDS.toDays(diffInMillis);
+ case HOURS -> TimeUnit.MILLISECONDS.toHours(diffInMillis);
+ case MINUTES -> TimeUnit.MILLISECONDS.toMinutes(diffInMillis);
+ default -> throw new IllegalArgumentException("不支持的时间单位");
+ };
+
+ // 校验时间跨度不超过最大限制
+ if (diff > maxValue) {
+ throw new ServiceException("最大时间跨度为 " + maxValue + " " + unit.toString().toLowerCase());
+ }
+ }
+
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/MapstructUtils.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/MapstructUtils.java
new file mode 100644
index 0000000..a3fcb8e
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/MapstructUtils.java
@@ -0,0 +1,92 @@
+package org.dromara.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/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/MessageUtils.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/MessageUtils.java
new file mode 100644
index 0000000..48dfc08
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/MessageUtils.java
@@ -0,0 +1,33 @@
+package org.dromara.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/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/NetUtils.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/NetUtils.java
new file mode 100644
index 0000000..72fdf40
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/NetUtils.java
@@ -0,0 +1,84 @@
+package org.dromara.common.core.utils;
+
+import cn.hutool.core.lang.PatternPool;
+import cn.hutool.core.net.NetUtil;
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.dromara.common.core.utils.regex.RegexUtils;
+
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+
+/**
+ * 增强网络相关工具类
+ *
+ * @author 秋辞未寒
+ */
+@Slf4j
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public class NetUtils extends NetUtil {
+
+ /**
+ * 判断是否为IPv6地址
+ *
+ * @param ip IP地址
+ * @return 是否为IPv6地址
+ */
+ public static boolean isIPv6(String ip) {
+ try {
+ // 判断是否为IPv6地址
+ return InetAddress.getByName(ip) instanceof Inet6Address;
+ } catch (UnknownHostException e) {
+ return false;
+ }
+ }
+
+ /**
+ * 判断IPv6地址是否为内网地址
+ *
+ * 以下地址将归类为本地地址,如有业务场景有需要,请根据需求自行处理:
+ *
+ * 通配符地址 0:0:0:0:0:0:0:0
+ * 链路本地地址 fe80::/10
+ * 唯一本地地址 fec0::/10
+ * 环回地址 ::1
+ *
+ *
+ * @param ip IP地址
+ * @return 是否为内网地址
+ */
+ public static boolean isInnerIPv6(String ip) {
+ try {
+ // 判断是否为IPv6地址
+ if (InetAddress.getByName(ip) instanceof Inet6Address inet6Address) {
+ // isAnyLocalAddress 判断是否为通配符地址,通常不会将其视为内网地址,根据业务场景自行处理判断
+ // isLinkLocalAddress 判断是否为链路本地地址,通常不算内网地址,是否划分归属于内网需要根据业务场景自行处理判断
+ // isLoopbackAddress 判断是否为环回地址,与IPv4的 127.0.0.1 同理,用于表示本机
+ // isSiteLocalAddress 判断是否为本地站点地址,IPv6唯一本地地址(Unique Local Addresses,简称ULA)
+ if (inet6Address.isAnyLocalAddress()
+ || inet6Address.isLinkLocalAddress()
+ || inet6Address.isLoopbackAddress()
+ || inet6Address.isSiteLocalAddress()) {
+ return true;
+ }
+ }
+ } catch (UnknownHostException e) {
+ // 注意,isInnerIPv6方法和isIPv6方法的适用范围不同,所以此处不能忽略其异常信息。
+ throw new IllegalArgumentException("Invalid IPv6 address!", e);
+ }
+ return false;
+ }
+
+ /**
+ * 判断是否为IPv4地址
+ *
+ * @param ip IP地址
+ * @return 是否为IPv4地址
+ */
+ public static boolean isIPv4(String ip) {
+ return RegexUtils.isMatch(PatternPool.IPV4, ip);
+ }
+
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/ObjectUtils.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/ObjectUtils.java
new file mode 100644
index 0000000..199fd82
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/ObjectUtils.java
@@ -0,0 +1,60 @@
+package org.dromara.common.core.utils;
+
+import cn.hutool.core.util.ObjectUtil;
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+
+import java.util.function.Function;
+
+/**
+ * 对象工具类
+ *
+ * @author 秋辞未寒
+ */
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public class ObjectUtils extends ObjectUtil {
+
+ /**
+ * 如果对象不为空,则获取对象中的某个字段 ObjectUtils.notNullGetter(user, User::getName);
+ *
+ * @param obj 对象
+ * @param func 获取方法
+ * @return 对象字段
+ */
+ public static E notNullGetter(T obj, Function func) {
+ if (isNotNull(obj) && isNotNull(func)) {
+ return func.apply(obj);
+ }
+ return null;
+ }
+
+ /**
+ * 如果对象不为空,则获取对象中的某个字段,否则返回默认值
+ *
+ * @param obj 对象
+ * @param func 获取方法
+ * @param defaultValue 默认值
+ * @return 对象字段
+ */
+ public static E notNullGetter(T obj, Function func, E defaultValue) {
+ if (isNotNull(obj) && isNotNull(func)) {
+ return func.apply(obj);
+ }
+ return defaultValue;
+ }
+
+ /**
+ * 如果值不为空,则返回值,否则返回默认值
+ *
+ * @param obj 对象
+ * @param defaultValue 默认值
+ * @return 对象字段
+ */
+ public static T notNull(T obj, T defaultValue) {
+ if (isNotNull(obj)) {
+ return obj;
+ }
+ return defaultValue;
+ }
+
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/ServletUtils.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/ServletUtils.java
new file mode 100644
index 0000000..bd1aab8
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/ServletUtils.java
@@ -0,0 +1,289 @@
+package org.dromara.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 类型的请求参数
+ *
+ * @param name 参数名
+ * @return 参数值
+ */
+ public static String getParameter(String name) {
+ return getRequest().getParameter(name);
+ }
+
+ /**
+ * 获取指定名称的 String 类型的请求参数,若参数不存在,则返回默认值
+ *
+ * @param name 参数名
+ * @param defaultValue 默认值
+ * @return 参数值或默认值
+ */
+ public static String getParameter(String name, String defaultValue) {
+ return Convert.toStr(getRequest().getParameter(name), defaultValue);
+ }
+
+ /**
+ * 获取指定名称的 Integer 类型的请求参数
+ *
+ * @param name 参数名
+ * @return 参数值
+ */
+ public static Integer getParameterToInt(String name) {
+ return Convert.toInt(getRequest().getParameter(name));
+ }
+
+ /**
+ * 获取指定名称的 Integer 类型的请求参数,若参数不存在,则返回默认值
+ *
+ * @param name 参数名
+ * @param defaultValue 默认值
+ * @return 参数值或默认值
+ */
+ public static Integer getParameterToInt(String name, Integer defaultValue) {
+ return Convert.toInt(getRequest().getParameter(name), defaultValue);
+ }
+
+ /**
+ * 获取指定名称的 Boolean 类型的请求参数
+ *
+ * @param name 参数名
+ * @return 参数值
+ */
+ public static Boolean getParameterToBool(String name) {
+ return Convert.toBool(getRequest().getParameter(name));
+ }
+
+ /**
+ * 获取指定名称的 Boolean 类型的请求参数,若参数不存在,则返回默认值
+ *
+ * @param name 参数名
+ * @param defaultValue 默认值
+ * @return 参数值或默认值
+ */
+ public static Boolean getParameterToBool(String name, Boolean defaultValue) {
+ return Convert.toBool(getRequest().getParameter(name), defaultValue);
+ }
+
+ /**
+ * 获取所有请求参数(以 Map 的形式返回)
+ *
+ * @param request 请求对象{@link ServletRequest}
+ * @return 请求参数的 Map,键为参数名,值为参数值数组
+ */
+ public static Map getParams(ServletRequest request) {
+ final Map map = request.getParameterMap();
+ return Collections.unmodifiableMap(map);
+ }
+
+ /**
+ * 获取所有请求参数(以 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;
+ }
+
+ /**
+ * 获取当前 HTTP 请求对象
+ *
+ * @return 当前 HTTP 请求对象
+ */
+ public static HttpServletRequest getRequest() {
+ try {
+ return getRequestAttributes().getRequest();
+ } catch (Exception e) {
+ return null;
+ }
+ }
+
+ /**
+ * 获取当前 HTTP 响应对象
+ *
+ * @return 当前 HTTP 响应对象
+ */
+ public static HttpServletResponse getResponse() {
+ try {
+ return getRequestAttributes().getResponse();
+ } catch (Exception e) {
+ return null;
+ }
+ }
+
+ /**
+ * 获取当前请求的 HttpSession 对象
+ *
+ * 如果当前请求已经关联了一个会话(即已经存在有效的 session ID),
+ * 则返回该会话对象;如果没有关联会话,则会创建一个新的会话对象并返回。
+ *
+ * HttpSession 用于存储会话级别的数据,如用户登录信息、购物车内容等,
+ * 可以在多个请求之间共享会话数据
+ *
+ * @return 当前请求的 HttpSession 对象
+ */
+ public static HttpSession getSession() {
+ return getRequest().getSession();
+ }
+
+ /**
+ * 获取当前请求的请求属性
+ *
+ * @return {@link ServletRequestAttributes} 请求属性对象
+ */
+ public static ServletRequestAttributes getRequestAttributes() {
+ try {
+ RequestAttributes attributes = RequestContextHolder.getRequestAttributes();
+ return (ServletRequestAttributes) attributes;
+ } catch (Exception e) {
+ return null;
+ }
+ }
+
+ /**
+ * 获取指定请求头的值,如果头部为空则返回空字符串
+ *
+ * @param request 请求对象
+ * @param name 头部名称
+ * @return 头部值
+ */
+ public static String getHeader(HttpServletRequest request, String name) {
+ String value = request.getHeader(name);
+ if (StringUtils.isEmpty(value)) {
+ return StringUtils.EMPTY;
+ }
+ return urlDecode(value);
+ }
+
+ /**
+ * 获取所有请求头的 Map,键为头部名称,值为头部值
+ *
+ * @param request 请求对象
+ * @return 请求头的 Map
+ */
+ 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;
+ }
+
+ /**
+ * 将字符串渲染到客户端(以 JSON 格式返回)
+ *
+ * @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 请求对象
+ * @return 是否为 Ajax 请求
+ */
+ public static boolean isAjaxRequest(HttpServletRequest request) {
+
+ // 判断 Accept 头部是否包含 application/json
+ String accept = request.getHeader("accept");
+ if (accept != null && accept.contains(MediaType.APPLICATION_JSON_VALUE)) {
+ return true;
+ }
+
+ // 判断 X-Requested-With 头部是否包含 XMLHttpRequest
+ String xRequestedWith = request.getHeader("X-Requested-With");
+ if (xRequestedWith != null && xRequestedWith.contains("XMLHttpRequest")) {
+ return true;
+ }
+
+ // 判断 URI 后缀是否为 .json 或 .xml
+ String uri = request.getRequestURI();
+ if (StringUtils.equalsAnyIgnoreCase(uri, ".json", ".xml")) {
+ return true;
+ }
+
+ // 判断请求参数 __ajax 是否为 json 或 xml
+ String ajax = request.getParameter("__ajax");
+ return StringUtils.equalsAnyIgnoreCase(ajax, "json", "xml");
+ }
+
+ /**
+ * 获取客户端 IP 地址
+ *
+ * @return 客户端 IP 地址
+ */
+ public static String getClientIP() {
+ return getClientIP(getRequest());
+ }
+
+ /**
+ * 对内容进行 URL 编码
+ *
+ * @param str 内容
+ * @return 编码后的内容
+ */
+ public static String urlEncode(String str) {
+ return URLEncoder.encode(str, StandardCharsets.UTF_8);
+ }
+
+ /**
+ * 对内容进行 URL 解码
+ *
+ * @param str 内容
+ * @return 解码后的内容
+ */
+ public static String urlDecode(String str) {
+ return URLDecoder.decode(str, StandardCharsets.UTF_8);
+ }
+
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/SpringUtils.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/SpringUtils.java
new file mode 100644
index 0000000..5ff751d
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/SpringUtils.java
@@ -0,0 +1,66 @@
+package org.dromara.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/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/StreamUtils.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/StreamUtils.java
new file mode 100644
index 0000000..f9e53a5
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/StreamUtils.java
@@ -0,0 +1,282 @@
+package org.dromara.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());
+ }
+
+ /**
+ * 找到流中满足条件的第一个元素
+ *
+ * @param collection 需要查询的集合
+ * @param function 过滤方法
+ * @return 找到符合条件的第一个元素,没有则返回null
+ */
+ public static E findFirst(Collection collection, Predicate function) {
+ if (CollUtil.isEmpty(collection)) {
+ return null;
+ }
+ return collection.stream().filter(function).findFirst().orElse(null);
+ }
+
+ /**
+ * 找到流中任意一个满足条件的元素
+ *
+ * @param collection 需要查询的集合
+ * @param function 过滤方法
+ * @return 找到符合条件的任意一个元素,没有则返回null
+ */
+ public static Optional findAny(Collection collection, Predicate function) {
+ if (CollUtil.isEmpty(collection)) {
+ return Optional.empty();
+ }
+ return collection.stream().filter(function).findAny();
+ }
+
+ /**
+ * 将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/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/StringUtils.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/StringUtils.java
new file mode 100644
index 0000000..7165734
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/StringUtils.java
@@ -0,0 +1,365 @@
+package org.dromara.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 org.springframework.util.AntPathMatcher;
+
+import java.nio.charset.Charset;
+import java.util.*;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+/**
+ * 字符串工具类
+ *
+ * @author Lion Li
+ */
+public class StringUtils extends org.apache.commons.lang3.StringUtils {
+
+ public static final String SEPARATOR = ",";
+
+ public static final String SLASH = "/";
+
+ @Deprecated
+ private StringUtils() {
+ }
+
+ /**
+ * 获取参数不为空值
+ *
+ * @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)
+ .filter(Objects::nonNull)
+ .collect(Collectors.toList());
+ }
+
+ /**
+ * 不区分大小写检查 CharSequence 是否以指定的前缀开头。
+ *
+ * @param str 要检查的 CharSequence 可能为 null
+ * @param prefixs 要查找的前缀可能为 null
+ * @return 是否包含
+ */
+ public static boolean startWithAnyIgnoreCase(CharSequence str, CharSequence... prefixs) {
+ // 判断是否是以指定字符串开头
+ for (CharSequence prefix : prefixs) {
+ if (StringUtils.startsWithIgnoreCase(str, prefix)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * 将字符串从源字符集转换为目标字符集
+ *
+ * @param input 原始字符串
+ * @param fromCharset 源字符集
+ * @param toCharset 目标字符集
+ * @return 转换后的字符串
+ */
+ public static String convert(String input, Charset fromCharset, Charset toCharset) {
+ if (isBlank(input)) {
+ return input;
+ }
+ try {
+ // 从源字符集获取字节
+ byte[] bytes = input.getBytes(fromCharset);
+ // 使用目标字符集解码
+ return new String(bytes, toCharset);
+ } catch (Exception e) {
+ return input;
+ }
+ }
+
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/Threads.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/Threads.java
new file mode 100644
index 0000000..86eebe2
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/Threads.java
@@ -0,0 +1,64 @@
+package org.dromara.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 {
+
+ /**
+ * 停止线程池
+ * 先使用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/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/TreeBuildUtils.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/TreeBuildUtils.java
new file mode 100644
index 0000000..1f2b99c
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/TreeBuildUtils.java
@@ -0,0 +1,131 @@
+package org.dromara.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 lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+import org.dromara.common.core.utils.reflect.ReflectUtils;
+
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/**
+ * 扩展 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");
+
+ /**
+ * 构建树形结构
+ *
+ * @param 输入节点的类型
+ * @param 节点ID的类型
+ * @param list 节点列表,其中包含了要构建树形结构的所有节点
+ * @param nodeParser 解析器,用于将输入节点转换为树节点
+ * @return 构建好的树形结构列表
+ */
+ public static List> build(List list, NodeParser nodeParser) {
+ if (CollUtil.isEmpty(list)) {
+ return CollUtil.newArrayList();
+ }
+ K k = ReflectUtils.invokeGetter(list.get(0), "parentId");
+ return TreeUtil.build(list, k, DEFAULT_CONFIG, nodeParser);
+ }
+
+ /**
+ * 构建树形结构
+ *
+ * @param 输入节点的类型
+ * @param 节点ID的类型
+ * @param parentId 顶级节点
+ * @param list 节点列表,其中包含了要构建树形结构的所有节点
+ * @param nodeParser 解析器,用于将输入节点转换为树节点
+ * @return 构建好的树形结构列表
+ */
+ public static