commit b236048a24e5c17d7721d047606041a25f43fce5
Author: dev <182542500@qq.com>
Date: Sun Feb 22 12:12:02 2026 +0800
first commit
diff --git a/.DS_Store b/.DS_Store
new file mode 100644
index 0000000..c328ada
Binary files /dev/null and b/.DS_Store differ
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..61a9f09
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,57 @@
+######################################################################
+# Build Tools
+
+.gradle
+/build/
+!gradle/wrapper/gradle-wrapper.jar
+
+target/
+!.mvn/wrapper/maven-wrapper.jar
+
+######################################################################
+# IDE
+
+### STS ###
+.apt_generated
+.classpath
+.factorypath
+.project
+.settings
+.springBeans
+
+### IntelliJ IDEA ###
+.idea
+*.iws
+*.iml
+*.ipr
+
+### JRebel ###
+rebel.xml
+
+### NetBeans ###
+nbproject/private/
+build/*
+nbbuild/
+nbdist/
+.nb-gradle/
+
+### Visual Studio Code ###
+.vscode
+
+### TRAE ###
+.trae
+
+######################################################################
+# Others
+*.log
+*.xml.versionsBackup
+*.swp
+
+!*/build/*.java
+!*/build/*.html
+!*/build/*.xml
+
+.flattened-pom.xml
+.flattened-pom *.xml
+
+logs/
\ No newline at end of file
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..49bf69a
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2025 pengles
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..c001e6a
--- /dev/null
+++ b/README.md
@@ -0,0 +1,687 @@
+# PoJie
+
+
+
+[](https://gitee.com/dromara/RuoYi-Vue-Plus/blob/5.X/LICENSE)
+[](https://spring.io/projects/spring-boot)
+[](https://openjdk.org/projects/jdk/17/)
+[](https://openjdk.org/projects/jdk/21/)
+[](https://baomidou.com/)
+[](https://sa-token.cc/)
+[](https://github.com/langchain4j/langchain4j)
+[](https://spring.io/projects/spring-ai)
+
+
+*集成多种AI框架,提供统一的AI服务管理平台*
+
+[在线预览](#) · [使用文档](#) · [更新日志](#)
+
+
+
+## 📖 平台简介
+
+**PoJie** 是一款基于 **Spring Boot 3.4.7** + **JDK 17/21** 的AI增强企业级快速开发平台,在 RuoYi-Vue-Plus 的基础上深度集成了多种AI框架和服务,打造统一的AI服务管理平台。
+
+### 🎯 核心特色
+
+- 🤖 **统一AI服务**:集成Spring AI、LangChain4j等主流AI框架,提供统一的AI服务接口
+- 🔌 **多Provider支持**:支持OpenAI、ChatGLM、通义千问、智谱AI等多种AI服务提供商
+- 💬 **智能对话**:支持单轮对话、多轮对话、流式响应和记忆管理
+- 📚 **知识库集成**:内置知识库服务,支持RAG(检索增强生成)
+- ⚖️ **智能编排**:多Provider负载均衡、故障转移和智能路由
+- 🛡️ **高可用性**:熔断器保护、限流控制、健康检查和自动重试
+- 📊 **AI监控**:实时性能监控、使用统计分析、成本追踪和质量评估
+- 🔐 **权限管理**:使用Sa-Token实现权限控制,支持多种认证方式
+- 🔗 **第三方登录**:集成JustAuth支持微信、钉钉等多种第三方认证
+- 💾 **数据库支持**:原生支持MySQL、Oracle、PostgreSQL、SQLServer等主流数据库
+- 🚀 **缓存优化**:使用Redisson作为Redis客户端,提供更丰富的分布式功能
+- 📈 **监控体系**:集成Spring Boot Admin,实现全方位监控
+- 🛠️ **代码生成**:提供智能化代码生成工具,支持多数据源
+- 💰 **支付集成**:内置支付宝、微信支付等主流支付方式
+
+## 功能特性
+
+### AI核心模块
+- **统一AI服务**:作为系统的AI大脑,统一管理和编排所有AI Provider
+- **多框架集成**:支持Spring AI、LangChain4j等主流AI框架
+- **智能对话**:单轮对话、多轮对话、流式响应、记忆管理
+- **知识库服务**:内置知识库,支持RAG检索增强生成
+- **AI Provider管理**:支持OpenAI、ChatGLM、通义千问、智谱AI等多种服务商
+- **智能编排**:负载均衡、故障转移、智能路由、并行处理
+- **AI监控**:实时性能监控、使用统计、成本追踪、质量评估
+
+### 系统核心模块
+- **系统管理(ruoyi-system)**:用户、角色、菜单、部门、字典等基础管理功能
+- **移动端模块(ruoyi-app)**:支持Web和移动端用户统一管理,提供移动端专用API
+- **权限控制**:基于Sa-Token的多维度权限管理体系,支持RBAC权限模型
+- **工作流(ruoyi-workflow)**:支持复杂的审批流程管理(Warm-Flow工作流引擎)
+- **文件存储**:集成MinIO实现分布式文件存储,支持多种OSS存储方式
+- **消息推送**:支持WebSocket和SSE实时消息推送
+- **聊天系统(ruoyi-chat)**:完整的聊天功能,支持AI助手集成、会话管理、消息历史
+
+### 业务模块
+- **商城模块(ruoyi-mall)**:完整的电商业务模块
+ - 商品中心:商品、SKU、分类、品牌、属性管理
+ - 交易中心:购物车、订单、售后管理
+ - 营销中心:活动、拼团、秒杀、砍价
+ - 优惠券中心:优惠券发放、核销、过期处理
+ - 统计中心:交易分析、会员分析
+- **支付模块(ruoyi-pay)**:集成支付宝、微信支付等主流支付方式
+- **微信公众号(ruoyi-mp)**:微信公众号开发支持
+
+### 开发工具
+- **代码生成器**:根据表结构一键生成CRUD代码
+- **Excel处理**:基于FastExcel实现高效的Excel操作
+- **日志监控**:完整的操作日志和登录日志记录
+- **接口文档**:使用SpringDoc自动生成API文档
+- **AI开发工具**:LangChain4j集成工具、AI服务测试工具
+
+### 安全特性
+- **数据脱敏**:敏感信息自动脱敏处理
+- **数据加密**:支持多种加密算法保障数据安全
+- **接口安全**:动态AES+RSA加密传输
+- **防重提交**:分布式环境下防重复提交
+- **XSS防护**:内置跨站脚本攻击防护
+- **AI安全**:API密钥安全管理、AI服务访问控制
+
+### 运维支持
+- **多数据源管理**:支持动态切换不同数据库
+- **邮件服务**:集成邮件发送功能
+- **短信服务**:支持多种短信平台
+- **定时任务**:分布式任务调度系统(SnailJob)
+- **系统监控**:实时监控服务状态和性能指标
+- **AI运维**:AI服务健康检查、性能监控、故障告警
+
+## 🏗️ 技术架构
+
+### 核心技术栈
+
+| 分类 | 技术 | 版本 | 说明 |
+|------|------|------|------|
+| **基础框架** | Spring Boot | 3.4.7 | 核心框架 |
+| **开发环境** | JDK | 17/21 | Java开发环境 |
+| **Web容器** | Undertow | - | 高性能Web容器 |
+| **权限认证** | Sa-Token | 1.44.0 | 轻量级权限框架 |
+| **ORM框架** | MyBatis-Plus | 3.5.12 | 持久层框架 |
+| **缓存中间件** | Redisson | 3.50.0 | Redis客户端 |
+| **数据库连接池** | HikariCP | - | 高性能连接池 |
+| **工具类库** | Hutool | 5.8.38 | Java工具包 |
+| **API文档** | SpringDoc | 2.8.8 | OpenAPI 3.0 |
+| **Excel处理** | FastExcel | 1.2.0 | 高性能Excel |
+| **第三方登录** | JustAuth | 1.16.7 | 第三方认证 |
+| **工作流引擎** | Warm-Flow | 1.7.4 | 国产工作流 |
+| **任务调度** | SnailJob | 1.5.0 | 分布式任务 |
+| **对象映射** | MapStruct Plus | 1.4.8 | 对象转换 |
+| **监控管理** | Spring Boot Admin | 3.4.7 | 应用监控 |
+
+### AI技术栈
+
+| 分类 | 技术 | 版本 | 说明 |
+|------|------|------|------|
+| **AI框架** | Spring AI | 1.0.0-M5 | Spring官方AI框架 |
+| **AI框架** | LangChain4j | 0.36.2 | Java版LangChain |
+| **AI服务** | OpenAI | - | GPT系列模型 |
+| **AI服务** | Azure OpenAI | - | Azure托管OpenAI |
+| **AI服务** | ChatGLM | - | 智谱AI对话模型 |
+| **AI服务** | 通义千问 | - | 阿里云大语言模型 |
+| **AI服务** | 智谱AI | - | 智谱AI平台 |
+| **AI平台** | Coze | 0.4.2 | 字节跳动AI Bot |
+| **AI平台** | Dify | 1.1.7 | 开源LLM应用平台 |
+| **HTTP客户端** | OkHttp | 4.12.0 | AI服务HTTP客户端 |
+
+### 数据库支持
+
+| 数据库 | 支持情况 | 说明 |
+|--------|----------|------|
+| MySQL | ✅ 原生支持 | 主要推荐 |
+| Oracle | ✅ 原生支持 | 企业级 |
+| PostgreSQL | ✅ 原生支持 | 开源数据库 |
+| SQL Server | ✅ 原生支持 | 微软数据库 |
+| 达梦数据库 | ✅ 支持 | 国产数据库 |
+| 金仓数据库 | ✅ 支持 | 国产数据库 |
+
+## 🚀 快速开始
+
+### 环境要求
+
+- **JDK**: 17+ (推荐 JDK 21) **⚠️ 必须使用JDK 17+**
+- **Maven**: 3.6+
+- **MySQL**: 5.7+ / 8.0+
+- **Redis**: 5.0+
+- **Node.js**: 16+ (前端开发)
+- **AI服务**: OpenAI API Key 或其他AI服务商密钥
+
+### 安装步骤
+
+#### 1. 克隆项目
+
+```bash
+# 克隆仓库(示例)
+git clone https://gitee.com/pengles/PoJie.git
+cd PoJie
+```
+
+#### 2. 数据库初始化
+
+**方式一:使用 MySQL 命令行**
+
+```sql
+-- 1. 创建数据库
+CREATE DATABASE ruoyi_vue_plus DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
+USE ruoyi_vue_plus;
+
+-- 2. 导入数据库脚本(按顺序执行,路径相对于项目根目录)
+SOURCE script/sql/ry_vue.sql; -- 基础脚本(必须)
+SOURCE script/sql/ry_ai.sql; -- AI模块脚本(如使用AI功能)
+SOURCE script/sql/ry_chat.sql; -- 聊天模块脚本(如使用聊天功能)
+SOURCE script/sql/ry_knowledge.sql; -- 知识库脚本(如使用知识库功能)
+SOURCE script/sql/ry_workflow.sql; -- 工作流脚本(如使用工作流功能)
+SOURCE script/sql/ry_mall.sql; -- 商城脚本(如使用商城功能)
+SOURCE script/sql/ry_pay.sql; -- 支付脚本(如使用支付功能)
+SOURCE script/sql/ry_app.sql; -- 移动端脚本(如使用移动端功能)
+SOURCE script/sql/ry_job.sql; -- 定时任务脚本(如使用定时任务)
+```
+
+**方式二:使用图形化工具(Navicat、DBeaver、DataGrip 等)**
+
+1. 创建数据库 `ruoyi_vue_plus`,字符集选择 `utf8mb4`,排序规则选择 `utf8mb4_general_ci`
+2. 按顺序执行 `script/sql/` 目录下的 SQL 文件:
+ - `ry_vue.sql`(必须,基础脚本)
+ - `ry_ai.sql`(如使用 AI 功能)
+ - `ry_chat.sql`(如使用聊天功能)
+ - `ry_knowledge.sql`(如使用知识库功能)
+ - `ry_workflow.sql`(如使用工作流功能)
+ - `ry_mall.sql`(如使用商城功能)
+ - `ry_pay.sql`(如使用支付功能)
+ - `ry_app.sql`(如使用移动端功能)
+ - `ry_job.sql`(如使用定时任务)
+
+> **注意**:
+> - `ry_vue.sql` 为基础脚本,必须最先执行
+> - 其他脚本根据实际使用的功能模块选择执行
+> - 建议在生产环境执行前先备份数据库
+
+#### 3. 配置AI服务
+
+**重要:配置AI服务API密钥**
+
+```bash
+# 设置环境变量(推荐方式)
+# Windows
+set OPENAI_API_KEY=your-openai-api-key
+set ZHIPU_API_KEY=your-zhipu-api-key
+set QWEN_API_KEY=your-qwen-api-key
+
+# Linux/Mac
+export OPENAI_API_KEY=your-openai-api-key
+export ZHIPU_API_KEY=your-zhipu-api-key
+export QWEN_API_KEY=your-qwen-api-key
+```
+
+#### 4. 修改配置文件
+
+编辑 `ruoyi-server/src/main/resources/application-dev.yml`:
+
+**基础配置(必须)**
+
+```yaml
+spring:
+ # 数据源配置
+ datasource:
+ dynamic:
+ primary: master
+ datasource:
+ master:
+ url: jdbc:mysql://localhost:3306/ruoyi_vue_plus?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true
+ username: root
+ password: your_password
+ driver-class-name: com.mysql.cj.jdbc.Driver
+
+ # Redis配置
+ data:
+ redis:
+ host: localhost
+ port: 6379
+ password: # 如有密码请填写
+ database: 0
+ timeout: 10s
+ lettuce:
+ pool:
+ max-active: 8
+ max-wait: -1ms
+ max-idle: 8
+ min-idle: 0
+```
+
+**AI服务配置(可选,如使用AI功能)**
+
+```yaml
+# AI模块配置(application-ai.yml 或 application-dev.yml)
+ruoyi:
+ ai:
+ enabled: true
+ default-provider: openai # 默认AI提供商:openai、zhipu、qwen、chatglm等
+ providers:
+ openai:
+ enabled: true
+ api-key: ${OPENAI_API_KEY:your-openai-api-key}
+ base-url: https://api.openai.com/v1
+ model: gpt-3.5-turbo
+ temperature: 0.7
+ max-tokens: 2000
+ zhipu:
+ enabled: false
+ api-key: ${ZHIPU_API_KEY:}
+ base-url: https://open.bigmodel.cn/api/paas/v4
+ model: glm-4
+ qwen:
+ enabled: false
+ api-key: ${QWEN_API_KEY:}
+ base-url: https://dashscope.aliyuncs.com/compatible-mode/v1
+ model: qwen-turbo
+```
+
+> **配置说明**:
+> - 数据库密码和Redis密码建议使用环境变量配置
+> - AI服务的API密钥强烈建议使用环境变量配置,避免泄露
+> - 更多配置项请参考 `application.yml` 和 `application-ai.yml` 文件
+> - 各模块的详细配置请参考对应模块的 README 文档
+
+#### 5. 启动应用
+
+**方式一:Maven启动(推荐用于开发)**
+
+```bash
+# 进入项目根目录
+cd PoJie
+
+# 编译项目
+mvn clean compile
+
+# 启动应用
+mvn spring-boot:run -pl ruoyi-server -am
+
+# 或使用简化命令
+mvn -q -pl ruoyi-server -am spring-boot:run -DskipTests
+```
+
+**方式二:IDE启动(推荐用于开发调试)**
+
+1. 使用 IDEA 或 Eclipse 导入 Maven 项目
+2. 找到 `ruoyi-server/src/main/java/org/dromara/RuoYiApplication.java`
+3. 右键运行 `main` 方法
+4. 或配置运行配置,设置工作目录为 `ruoyi-server`
+
+**方式三:JAR包启动(推荐用于生产)**
+
+```bash
+# 进入项目根目录
+cd PoJie
+
+# 打包(跳过测试)
+mvn clean package -DskipTests
+
+# 启动JAR包
+java -jar ruoyi-server/target/ruoyi-server.jar
+
+# 或指定配置文件
+java -jar ruoyi-server/target/ruoyi-server.jar --spring.profiles.active=prod
+
+# 后台启动(Linux)
+nohup java -jar ruoyi-server/target/ruoyi-server.jar > app.log 2>&1 &
+```
+
+**方式四:Docker启动(推荐用于生产)**
+
+```bash
+# 使用Docker Compose启动(包含MySQL、Redis等)
+cd script/docker
+docker-compose up -d
+
+# 构建应用镜像(需要Dockerfile)
+docker build -t pojie:latest .
+docker run -d -p 8080:8080 --name pojie pojie:latest
+```
+
+> **启动注意事项**:
+> - 确保 MySQL 和 Redis 服务已启动
+> - 检查端口 8080 是否被占用
+> - 查看启动日志确认服务启动成功
+> - 如遇到端口冲突,可在配置文件中修改 `server.port`
+
+#### 6. 访问系统
+
+启动成功后,访问以下地址:
+
+| 服务 | 地址 | 说明 |
+|------|------|------|
+| **系统首页** | http://localhost:8080 | 系统入口(需要前端配合) |
+| **API文档(Swagger)** | http://localhost:8080/swagger-ui/index.html | Swagger UI 接口文档 |
+| **API文档(Knife4j)** | http://localhost:8080/doc.html | Knife4j 增强文档(如已集成) |
+| **监控中心** | http://localhost:8080/admin | Spring Boot Admin 监控 |
+| **健康检查** | http://localhost:8080/actuator/health | Spring Boot Actuator |
+| **AI服务健康检查** | http://localhost:8080/ai/health | AI模块健康状态(如启用) |
+| **LangChain4j服务** | http://localhost:8080/ai/langchain4j/health | LangChain4j服务状态(如启用) |
+
+**默认登录账号**:
+- 用户名:`admin`
+- 密码:`admin123`
+
+> **注意**:首次登录后建议立即修改默认密码
+
+#### 7. 验证AI功能(可选)
+
+如果已配置AI服务,可以通过以下方式验证:
+
+1. **测试AI聊天接口**:
+ ```bash
+ curl -X POST http://localhost:8080/ai/chat \
+ -H "Content-Type: application/json" \
+ -H "Authorization: Bearer your-token" \
+ -d '{"message": "你好,请介绍一下自己"}'
+ ```
+
+2. **查看支持的AI模型列表**:
+ ```bash
+ curl http://localhost:8080/ai/models
+ ```
+
+3. **检查AI服务健康状态**:
+ 访问 http://localhost:8080/ai/health
+
+4. **通过API文档测试**:
+ 访问 Swagger UI 或 Knife4j 文档,找到AI相关接口进行测试
+
+## 💡 使用示例
+
+### AI服务开发
+
+#### 统一AI服务接口
+- 提供统一的AI聊天接口,支持多种AI Provider
+- 支持同步和异步(流式)响应模式
+- 集成请求验证和响应封装
+- 支持多模型切换和配置管理
+
+#### LangChain4j服务使用
+- 单轮对话:直接调用AI模型进行问答
+- 带记忆的对话:支持上下文记忆的连续对话
+- 流式对话:实时返回AI生成内容
+- 模型管理:获取和切换支持的AI模型
+
+### 移动端API开发
+- 移动端用户登录和认证
+- 用户信息管理和权限控制
+- 移动端专用的API接口设计
+- 支持多种登录方式(账号密码、第三方登录等)
+
+
+### 分布式文件存储
+- 支持多种文件存储方式(本地、阿里云OSS、腾讯云COS、七牛云等)
+- 提供统一的文件上传和下载接口
+- 支持文件预览和在线编辑
+- 集成文件安全检查和病毒扫描
+
+### 实时消息推送
+- 基于SSE(Server-Sent Events)的实时消息推送
+- 支持用户连接管理和消息广播
+- 集成WebSocket支持双向通信
+- 提供消息队列和持久化存储
+
+### 多数据源支持
+- 支持动态数据源切换和管理
+- 提供主从数据库读写分离
+- 支持分布式事务处理
+- 集成数据源监控和健康检查
+
+### 权限控制
+- 基于Sa-Token的轻量级权限框架
+- 支持RBAC权限模型
+- 提供细粒度的权限控制
+- 集成操作日志和审计功能
+
+## 📁 项目结构
+
+```
+PoJie/
+├── ruoyi-common/ # 公共模块
+│ ├── ruoyi-common-bom/ # BOM依赖管理
+│ ├── ruoyi-common-core/ # 核心工具类
+│ ├── ruoyi-common-doc/ # 接口文档配置(SpringDoc)
+│ ├── ruoyi-common-excel/ # Excel处理(FastExcel)
+│ ├── ruoyi-common-mybatis/ # MyBatis-Plus配置
+│ ├── ruoyi-common-redis/ # Redis配置(Redisson)
+│ ├── ruoyi-common-satoken/ # 权限认证(Sa-Token)
+│ ├── ruoyi-common-oss/ # 对象存储(MinIO/OSS)
+│ ├── ruoyi-common-sms/ # 短信服务
+│ ├── ruoyi-common-mail/ # 邮件服务
+│ ├── ruoyi-common-websocket/ # WebSocket支持
+│ ├── ruoyi-common-sse/ # SSE服务器推送
+│ ├── ruoyi-common-encrypt/ # 数据加密
+│ ├── ruoyi-common-idempotent/ # 幂等性控制
+│ ├── ruoyi-common-ratelimiter/ # 限流控制
+│ ├── ruoyi-common-log/ # 日志记录
+│ ├── ruoyi-common-security/ # 安全配置
+│ ├── ruoyi-common-sensitive/ # 敏感数据脱敏
+│ ├── ruoyi-common-social/ # 第三方登录(JustAuth)
+│ ├── ruoyi-common-translation/ # 数据翻译
+│ ├── ruoyi-common-web/ # Web通用配置
+│ ├── ruoyi-common-json/ # JSON处理
+│ └── ruoyi-common-job/ # 任务调度
+├── ruoyi-modules/ # 业务模块
+│ ├── ruoyi-system/ # 系统管理模块
+│ ├── ruoyi-member/ # 移动端模块
+│ ├── ruoyi-chat/ # 聊天模块
+│ ├── ruoyi-ai/ # AI核心模块
+│ ├── ruoyi-knowledge/ # 知识库模块
+│ ├── ruoyi-workflow/ # 工作流模块(Warm-Flow)
+│ ├── ruoyi-mall/ # 商城模块(聚合模块)
+│ │ ├── ruoyi-mall-product/ # 商品中心
+│ │ ├── ruoyi-mall-trade/ # 交易中心
+│ │ ├── ruoyi-mall-coupon/ # 优惠券中心
+│ │ ├── ruoyi-mall-promotion/ # 营销中心
+│ │ └── ruoyi-mall-statistics/ # 统计中心
+│ ├── ruoyi-pay/ # 支付模块
+│ ├── ruoyi-generator/ # 代码生成器
+│ ├── ruoyi-job/ # 定时任务模块(SnailJob)
+│ └── ruoyi-mp/ # 微信公众号模块
+├── ruoyi-extend/ # 扩展模块
+│ ├── ruoyi-monitor-admin/ # 监控中心(Spring Boot Admin)
+│ └── ruoyi-snailjob-server/ # 分布式任务调度服务端
+├── ruoyi-server/ # 服务启动模块
+│ ├── src/main/java/
+│ │ └── org/dromara/
+│ │ └── RuoYiApplication.java # 启动类
+│ └── src/main/resources/
+│ ├── application.yml # 主配置文件
+│ ├── application-dev.yml # 开发环境配置
+│ ├── application-prod.yml # 生产环境配置
+│ └── application-ai.yml # AI模块配置
+├── script/ # 脚本文件
+│ ├── sql/ # 数据库脚本
+│ │ ├── ry_vue.sql # 基础数据库脚本(必须)
+│ │ ├── ry_ai.sql # AI模块数据库脚本
+│ │ ├── ry_chat.sql # 聊天模块数据库脚本
+│ │ ├── ry_knowledge.sql # 知识库模块数据库脚本
+│ │ ├── ry_workflow.sql # 工作流模块数据库脚本
+│ │ ├── ry_mall.sql # 商城模块数据库脚本
+│ │ ├── ry_pay.sql # 支付模块数据库脚本
+│ │ ├── ry_app.sql # 移动端模块数据库脚本
+│ │ └── ry_job.sql # 定时任务数据库脚本
+│ ├── docker/ # Docker配置
+│ │ ├── docker-compose.yml # Docker Compose配置
+│ │ ├── database.yml # 数据库Docker配置
+│ │ └── redis/ # Redis配置
+│ └── bin/ # 启动脚本
+└── docs/ # 项目文档(可选)
+ ├── AI_INTEGRATION_GUIDE.md # AI集成指南
+ ├── LANGCHAIN4J_INTEGRATION_GUIDE.md # LangChain4j集成指南
+ └── DEPLOYMENT_GUIDE.md # 部署指南
+```
+
+## 🔧 开发指南
+
+### 环境配置
+
+1. **开发环境**
+ - JDK 17+ (必须)
+ - Maven 3.6+
+ - MySQL 8.0+
+ - Redis 6.0+
+ - Node.js 16+
+2. **AI服务配置**
+ - OpenAI API Key (推荐)
+ - 智谱AI API Key (可选)
+ - 通义千问API Key (可选)
+ - Azure OpenAI配置 (可选)
+
+### AI模块开发
+
+#### 1. 添加新的AI Provider
+- 实现AiProvider接口,定义Provider名称和核心方法
+- 添加@Component注解和条件配置注解
+- 实现chat()和streamChat()方法
+- 处理请求参数和响应格式
+
+#### 2. 配置新Provider
+- 在application.yml中添加Provider配置
+- 设置enabled、api-key、base-url、model等参数
+- 支持环境变量和配置文件两种方式
+
+#### 3. LangChain4j扩展开发
+- 注入ChatLanguageModel进行AI对话
+- 实现自定义聊天服务和流式响应
+- 支持记忆管理和上下文处理
+- 集成监控和异常处理机制
+
+### 代码生成器使用
+
+1. 访问系统管理 -> 代码生成
+2. 选择要生成的数据表
+3. 配置生成信息
+4. 下载生成的代码
+
+### 新增业务模块
+
+1. 在 `ruoyi-modules` 下创建新模块
+2. 添加模块依赖到主pom.xml
+3. 配置模块的Controller、Service、Mapper
+4. 添加相应的权限配置
+
+### 代码规范
+
+- 遵循阿里巴巴Java开发手册
+- 使用统一的代码格式化配置
+- 添加必要的单元测试
+- 更新相关文档
+- AI相关代码需要添加详细的使用说明
+
+### 数据库设计
+
+- 统一的表前缀和命名规范
+- 合理的索引设计
+- 数据字典维护
+- AI相关表结构设计规范:
+ - `ai_*` 表前缀用于AI核心功能
+ - `chat_*` 表前缀用于聊天相关
+ - `knowledge_*` 表前缀用于知识库
+
+### API文档规范
+
+使用 `@Operation`、`@Tag`、`@Parameter` 等注解标注API信息:
+
+```java
+@Tag(name = "用户管理")
+@RestController
+public class UserController {
+
+ @Operation(summary = "获取用户信息", description = "根据用户ID获取用户详细信息")
+ @GetMapping("/{id}")
+ public R getUser(@Parameter(description = "用户ID") @PathVariable Long id) {
+ return R.ok(userService.getById(id));
+ }
+}
+```
+
+## 🤝 贡献指南
+
+我们欢迎所有形式的贡献,包括但不限于:
+
+- 🐛 **Bug报告**:发现问题请提交Issue
+- 💡 **功能建议**:有好的想法请告诉我们
+- 📝 **文档改进**:帮助完善文档
+- 🔧 **代码贡献**:提交Pull Request
+- 🤖 **AI Provider集成**:添加新的AI服务提供商
+- 🧠 **AI算法优化**:改进AI服务性能和质量
+- 📚 **知识库功能扩展**:增强RAG和知识管理功能
+
+### 贡献流程
+
+1. Fork 本仓库
+2. 创建特性分支 (`git checkout -b feature/AmazingFeature`)
+3. 提交更改 (`git commit -m 'Add some AmazingFeature'`)
+4. 推送到分支 (`git push origin feature/AmazingFeature`)
+5. 开启 Pull Request
+
+### AI相关贡献指南
+
+#### 新增AI Provider
+
+1. **实现Provider接口**
+ - 继承AiProvider接口并实现所有必要方法
+ - 定义Provider名称和基本配置
+
+2. **添加配置类**
+ - 创建配置属性类,使用@ConfigurationProperties注解
+ - 定义API密钥、基础URL、模型等配置项
+
+3. **编写单元测试**
+ - 为新Provider编写完整的单元测试
+ - 测试聊天功能、流式响应、异常处理等
+
+4. **更新文档**
+ - 在 `docs/AI_INTEGRATION_GUIDE.md` 中添加使用说明
+ - 更新 `README.md` 中的支持列表
+
+#### LangChain4j扩展贡献
+
+1. **扩展现有功能**
+ - 添加新的模型支持
+ - 优化对话记忆管理
+ - 增强流式响应处理
+
+2. **性能优化**
+ - 连接池优化
+ - 缓存策略改进
+ - 异步处理优化
+
+### 代码规范
+
+- 遵循阿里巴巴Java开发手册
+- 使用统一的代码格式化配置
+- 添加必要的单元测试
+- 更新相关文档
+- AI相关代码需要包含详细的JavaDoc注释
+- 提供配置示例和使用说明
+
+## 📞 联系我们
+
+- **项目地址**:https://gitee.com/pengles/PoJie
+- **问题反馈**:https://gitee.com/pengles/PoJie/issues
+- **QQ交流群**:待建立
+- **微信交流群**:待建立
+
+🌟 **开源精神让我们共同进步,感谢每一位开发者的贡献和分享!**
+
+## 📄 许可证
+
+本项目采用 [MIT 许可证](./LICENSE),详见 LICENSE 文件。
+
+---
+
+
+
+**如果这个项目对你有帮助,请给个 ⭐ Star 支持一下!**
+
+Made with ❤️ by PoJie Team
+
+
diff --git a/UPDATE.md b/UPDATE.md
new file mode 100644
index 0000000..91639d1
--- /dev/null
+++ b/UPDATE.md
@@ -0,0 +1,229 @@
+# 项目升级与优化方案(UPDATE)
+
+> **版本**: v1.1.0 (2025-01-XX)
+>
+> **更新日志**:
+> - v1.1.0: 新增商城模块完善章节,记录商城系统模块拆分与功能实现进展
+> - v1.0.0: 初始版本,聚焦认证安全与系统优化
+
+---
+
+## 目标与范围
+- **目标**: 在不改变主业务的前提下,增强认证安全、可扩展性、性能稳定性与可观测能力,同时提升开发者体验与交付效率
+- **范围**: Sys 后台端、App 用户端、MP 公众号、公共组件(认证策略、会话、缓存、日志、CI/CD、文档与测试)、商城系统模块拆分与完善
+
+---
+
+## 总体原则
+- **最小入侵、增量演进**: 不推倒重来,尽量通过服务层与拦截器/过滤器扩展
+- **枚举化与集中化**: 统一策略路由、Key 规范、限流与加密开关
+- **可观测先行**: 指标、告警、健康检查与日志字段统一,便于稳定运营
+- **先方案后实现**: 每个子项包含明确的改动点、验证方法与风险控制
+
+---
+
+## 认证与安全(高优先)
+
+### grantType 枚举化与策略映射
+- **内容**: 用枚举或常量表统一 Sys/App 的 grantType 与策略映射,移除魔法字符串
+- **改动点**: 策略注册与查找入口、客户端配置校验
+- **验证**: 枚举覆盖所有现有策略,错误入参返回一致错误码
+
+### 风险登录与强制改密
+- **内容**: 登录成功后比对 IP 归属地变化(日志/邮件告警);密码超期标记 Session,拦截除改密外所有接口
+- **改动点**: 在登录服务增加策略执行方法;新增 WebMvc 拦截器
+- **验证**: 模拟超期与非超期用户行为;确保拦截仅放行改密与登出
+
+### 验证码策略(保持现状)
+- **内容**: 当前阶段 App 密码策略不启用验证码,保持与现状一致;但保留验证码校验逻辑与开关,后续在前端加密与交互链路完善后可一键开启,与 Sys 保持一致
+- **改动点**: 不改动 App 端验证码逻辑(保留关闭状态);保留校验方法与配置开关(示例:app.auth.captcha.enabled=false)
+- **验证**: 无功能改动;后续启用时按开关置为 true 并执行联调(验证码生成/校验、错误次数累加与锁定、生效范围与白名单)
+- **准备项**: 前端加密到位、验证码生成与传输链路打通、联调用例与回归清单就绪
+
+### 接口加密与防刷
+- **内容**: App 端在前端加密准备就绪后启用 ApiEncrypt;登录/验证码/二维码轮询加限流
+- **改动点**: 控制器方法注解与网关规则
+- **验证**: 压测防刷路径、请求速率下服务稳定性
+
+---
+
+## App 信息补全(高优先)
+- **内容**: 登录后资料未完善仅放行白名单接口(个人信息查询、绑定手机号),其他接口返回 403_INFO_INCOMPLETE
+- **改动点**: 新增 App 过滤器 + 配置项白名单;登录成功后写入 Session 标记
+- **验证**: 未完善资料用户访问白名单成功、非白名单拦截;完善后恢复正常
+
+---
+
+## 会话管理与事件(中优先)
+- **内容**: 完善第 6 章会话 API(活跃列表、终止、心跳续期、SSE 事件);令牌 TTL 与 Redis TTL 同步,避免“僵尸会话”
+- **改动点**: App 会话服务持久化实现;统一 Key 前缀与 TTL
+- **验证**: 多设备并发登录、心跳续期、终止事件推送的端到端验证
+
+---
+
+## 第三方授权与绑定(中优先)
+- **内容**: justauth 登录与绑定流程的鲁棒化(source 与 state 校验、重复绑定幂等);MP 二维码的 scene_str 规范(sys_user:{userId})
+- **改动点**: 社交授权回调控制器、绑定服务的重复校验;MP 配置与二维码接口
+- **验证**: 常见平台的授权成功/失败、重复绑定、解绑与重新绑定
+
+---
+
+## 数据层与性能(中优先)
+
+### MyBatis-Plus 警告修复
+- **内容**: 为缺少 @TableId 的实体(如 AppUserRole)补注解,恢复 ById 系列方法可用
+- **验证**: 编译无警告且 CRUD 正常
+
+### 索引与热点查询优化
+- **内容**: 用户、角色关联、社交绑定等常查表加必要索引;分页与条件查询走服务层统一封装
+- **验证**: 压测主要查询场景,慢查与扫描下降
+
+---
+
+## 可观测与告警(中优先)
+
+### 指标与健康检查
+- **内容**: 暴露登录成功率、锁定次数、验证码命中率、会话活跃数;完善 actuator 健康检查
+- **改动点**: Micrometer 指标埋点;Spring Boot Admin 端点配置
+- **验证**: 仪表板查看与阈值告警验证
+
+### 日志与链路追踪
+- **内容**: 统一日志字段(traceId、userId、clientId、grantType);社交授权与二维码流程关键点日志
+- **验证**: 问题复盘路径明确、跨模块调用链可追踪
+
+---
+
+## 开发者体验与文档(中优先)
+
+### 文档
+- **内容**: 在登录综合指南中加入响应结构说明与“grantType→策略类→入口路径”对照表;升级与优化指南形成统一入口
+- **验证**: 联调工程师可按文档快速定位与调用
+
+### 测试
+- **内容**: 策略单测覆盖验证码开关、停用用户、第三方失败分支;集成测试覆盖 /auth/login、/app/auth/login 的典型入参与响应
+- **验证**: 持续集成跑通、关键路径不回归
+
+---
+
+## 交付与构建(低优先但必要)
+
+### CI/CD 稳定性
+- **内容**: 分模块并行构建与独立重试;对 clean 阶段的目标目录锁定增加占用检测;缓存 Maven 仓库
+- **验证**: 全量构建时间缩短、偶发失败模块可重试成功
+
+### 配置与密钥管理
+- **内容**: 区分 dev/prod 配置;禁止密钥进入日志;构建衍生文件与敏感清单进入 .gitignore
+- **验证**: 审计通过、漏洞扫描无高危项
+
+---
+
+## 风险与回滚
+- **风险**:
+ - 拦截策略误拦截、授权流程改动影响兼容
+ - 商城模块拆分可能影响现有代码调用(已采用渐进式迁移策略,保留原有代码)
+- **回滚**:
+ - 按模块独立开关与配置回退;拦截器与过滤器具备开关与白名单;社交绑定保留幂等校验
+ - 商城模块:保留原有代码在 `ruoyi-mall/src` 目录,新功能在新子模块开发,可平滑回退
+
+---
+
+## 阶段产出与验证
+
+### 阶段 1(高优先):grantType 枚举、风险登录与强制改密、App 信息补全过滤器、验证码一致化
+- **验证**: 编译通过、用例跑通、预览可访问,关键接口行为与文档一致
+
+### 阶段 2(中优先):会话 API、第三方授权鲁棒化、数据层优化、指标与健康检查
+- **验证**: 压测与监控数据达标、授权与绑定流程无异常
+
+### 阶段 3(持续):测试完善、CI/CD 优化、文档与示例更新
+
+### 阶段 4(已完成):商城系统模块拆分与核心功能实现 ✅
+- **验证**: 各子模块功能完整、API接口可用、数据库表结构正确、文档完善
+- **成果**:
+ - 5 个子模块基础结构完整
+ - 商品中心、交易中心、优惠券中心功能完整实现
+ - 营销中心活动管理和秒杀功能实现
+ - 完整的 API 接口和文档
+
+---
+
+## 商城系统模块拆分与完善(高优先,已完成核心功能)✅
+
+### 模块结构重构
+- **内容**: 按照领域驱动设计原则,将 `ruoyi-mall` 模块拆分为 5 个子模块,实现模块化架构
+- **改动点**:
+ - 创建子模块目录结构(product、trade、coupon、promotion、statistics)
+ - 配置各子模块的 Maven 依赖关系
+ - 创建各子模块的 AutoConfiguration 类和 spring.factories
+ - 扩展数据库表结构,支持完整业务场景
+- **验证**: 各子模块可独立编译、自动配置生效、数据库表结构完整
+
+### 商品中心模块 (ruoyi-mall-product) ✅
+- **已完成功能**:
+ - ✅ 商品(SPU)和SKU的完整CRUD操作
+ - ✅ 商品分类管理(三级分类树形结构)
+ - ✅ 品牌管理
+ - ✅ 商品上架/下架、浏览量和销量统计
+- **数据库表**: `mall_product`, `mall_sku`, `mall_category`, `mall_brand`
+- **API接口**: 管理端和移动端完整的商品、分类、品牌接口
+
+### 交易中心模块 (ruoyi-mall-trade) ✅
+- **已完成功能**:
+ - ✅ 购物车管理(添加、删除、数量修改、选中状态)
+ - ✅ 订单管理(创建、查询、取消、状态流转)
+ - ✅ 售后管理(退款、退货、换货申请及审核流程)
+ - ✅ 订单评价(评价提交、审核、回复)
+ - ✅ 订单支付集成(与支付中心集成)
+- **数据库表**: `mall_cart`, `mall_order`, `mall_order_item`, `mall_after_sale`, `mall_order_evaluate`, `mall_order_coupon`
+- **API接口**: 完整的订单、售后、评价管理接口
+
+### 优惠券中心模块 (ruoyi-mall-coupon) ✅
+- **已完成功能**:
+ - ✅ 优惠券管理(满减、折扣、现金券类型)
+ - ✅ 用户优惠券(领券、使用、退还)
+ - ✅ 优惠券过期处理(定时任务方法已实现)
+- **数据库表**: `mall_coupon`, `mall_coupon_user`
+- **API接口**: 完整的优惠券和用户优惠券管理接口
+
+### 营销中心模块 (ruoyi-mall-promotion) ⚠️
+- **已完成功能**:
+ - ✅ 营销活动管理(活动类型、规则配置、时间管理)
+ - ✅ 秒杀功能(秒杀活动创建、库存管理、状态管理)
+- **部分实现**:
+ - ⚠️ 拼团功能(表结构已创建,业务逻辑待实现)
+ - ⚠️ 砍价功能(表结构已创建,业务逻辑待实现)
+- **数据库表**: `mall_promotion`, `mall_seckill`, `mall_group_buy`, `mall_group_buy_order`, `mall_bargain`, `mall_bargain_record`, `mall_bargain_help`
+
+### 统计中心模块 (ruoyi-mall-statistics) ⚠️
+- **状态**: 框架已创建,基础结构完善
+- **待实现**: 交易分析、会员分析、商品分析、营销分析等统计功能
+
+### 文档完善 ✅
+- ✅ 各子模块 README.md 文档完整
+- ✅ 模块结构说明文档(MODULE_STRUCTURE.md)
+- ✅ API接口详细说明
+- ✅ 数据库表结构说明
+
+---
+
+## 行动清单(优先顺序)
+
+### 已完成 ✅
+- ✅ 商城模块结构拆分与基础功能实现
+- ✅ 商品中心模块完整实现(商品、SKU、分类、品牌)
+- ✅ 交易中心模块完整实现(订单、售后、评价)
+- ✅ 优惠券中心模块完整实现
+- ✅ 营销中心模块部分实现(活动管理、秒杀)
+
+### 进行中 / 待完成
+1. 实现风险登录检测与强制改密拦截(Sys)
+2. 统一 App 信息补全过滤器与白名单
+3. 修复 MyBatis-Plus 警告(补充缺失的 @TableId)
+4. 统一 grantType 枚举化与策略映射表
+5. App 启用验证码开关,与 Sys 行为一致
+6. 完善会话指标与健康检查,接入告警
+7. 补充集成测试与 Postman 集合,提高联调效率
+8. CI 并行构建与清理增强,提升交付稳定性
+9. 完善营销中心模块(拼团、砍价功能实现)
+10. 实现统计中心模块数据分析功能
+11. 商城模块代码迁移(将现有代码迁移到新子模块结构,可选)
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..eacda03
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,719 @@
+
+
+ 4.0.0
+
+ org.dromara
+ PoJie
+ ${revision}
+
+ PoJie
+ https://gitee.com/pengles/PoJie
+ PoJie管理系统
+
+
+ 5.4.1
+ 3.4.7
+ UTF-8
+ UTF-8
+ 17
+ 3.5.16
+ 2.8.14
+ 0.15.0
+ 1.3.0
+ 2.3
+ 1.44.0
+ 3.5.12
+ 3.9.1
+ 5.8.40
+ 3.4.7
+ 3.52.0
+ 2.2.7
+ 4.3.1
+ 1.6.0
+ 1.5.0
+ 0.2.0
+ 1.18.36
+ 1.80
+ 1.16.7
+
+ 2.7.0
+
+ 2.28.22
+
+ 3.3.4
+
+ 1.2.83
+
+ 8.7.2-20250603
+
+ 1.7.4
+
+ 4.34.86.ALL
+
+ 4.7.0
+ 4.7.0
+ 0.2.12
+
+ 5.2.4
+
+ 8.0.2
+
+
+ 1.0.1
+ 1.3.0-beta9
+
+ 4.12.0
+
+
+ 3.4.2
+ 3.4.0
+ 3.14.0
+ 3.5.3
+ 1.3.0
+
+ true
+
+
+
+
+ local
+
+
+ local
+ info
+ ruoyi
+ 123456
+
+
+
+ dev
+
+
+ dev
+ info
+ ruoyi
+ 123456
+
+
+
+ true
+
+
+
+ prod
+
+ prod
+ warn
+ ruoyi
+ 123456
+
+
+
+
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-dependencies
+ ${spring-boot.version}
+ pom
+ import
+
+
+
+
+ cn.hutool
+ hutool-bom
+ ${hutool.version}
+ pom
+ import
+
+
+
+
+ org.dromara.warm
+ warm-flow-mybatis-plus-sb3-starter
+ ${warm-flow.version}
+
+
+ org.dromara.warm
+ warm-flow-plugin-ui-sb-web
+ ${warm-flow.version}
+
+
+
+
+ me.zhyd.oauth
+ JustAuth
+ ${justauth.version}
+
+
+
+
+ org.dromara
+ ruoyi-common-bom
+ ${revision}
+ pom
+ import
+
+
+
+ org.springdoc
+ springdoc-openapi-starter-webmvc-api
+ ${springdoc.version}
+
+
+
+ org.springdoc
+ springdoc-openapi-starter-common
+ ${springdoc.version}
+
+
+
+ com.github.therapi
+ therapi-runtime-javadoc
+ ${therapi-javadoc.version}
+
+
+
+ org.projectlombok
+ lombok
+ ${lombok.version}
+
+
+
+ cn.idev.excel
+ fastexcel
+ ${fastexcel.version}
+
+
+
+
+ org.apache.velocity
+ velocity-engine-core
+ ${velocity.version}
+
+
+
+
+ cn.dev33
+ sa-token-spring-boot3-starter
+ ${satoken.version}
+
+
+
+ cn.dev33
+ sa-token-jwt
+ ${satoken.version}
+
+
+ cn.hutool
+ hutool-all
+
+
+
+
+ cn.dev33
+ sa-token-core
+ ${satoken.version}
+
+
+
+
+ com.baomidou
+ dynamic-datasource-spring-boot3-starter
+ ${dynamic-ds.version}
+
+
+
+ org.mybatis
+ mybatis
+ ${mybatis.version}
+
+
+
+ com.baomidou
+ mybatis-plus-spring-boot3-starter
+ ${mybatis-plus.version}
+
+
+
+ com.baomidou
+ mybatis-plus-jsqlparser
+ ${mybatis-plus.version}
+
+
+
+ com.baomidou
+ mybatis-plus-annotation
+ ${mybatis-plus.version}
+
+
+
+
+ p6spy
+ p6spy
+ ${p6spy.version}
+
+
+
+
+ software.amazon.awssdk
+ s3
+ ${aws.sdk.version}
+
+
+
+ software.amazon.awssdk
+ s3-transfer-manager
+ ${aws.sdk.version}
+
+
+
+ software.amazon.awssdk
+ netty-nio-client
+ ${aws.sdk.version}
+
+
+
+ org.dromara.sms4j
+ sms4j-spring-boot-starter
+ ${sms4j.version}
+
+
+
+ de.codecentric
+ spring-boot-admin-starter-server
+ ${spring-boot-admin.version}
+
+
+ de.codecentric
+ spring-boot-admin-starter-client
+ ${spring-boot-admin.version}
+
+
+
+
+ org.redisson
+ redisson-spring-boot-starter
+ ${redisson.version}
+
+
+
+ com.baomidou
+ lock4j-redisson-spring-boot-starter
+ ${lock4j.version}
+
+
+
+
+ com.aizuda
+ snail-job-client-starter
+ ${snailjob.version}
+
+
+ com.aizuda
+ snail-job-client-job-core
+ ${snailjob.version}
+
+
+
+
+ org.bouncycastle
+ bcprov-jdk15to18
+ ${bouncycastle.version}
+
+
+
+ io.github.linpeilie
+ mapstruct-plus-spring-boot-starter
+ ${mapstruct-plus.version}
+
+
+
+
+ org.lionsoul
+ ip2region
+ ${ip2region.version}
+
+
+
+
+ com.github.binarywang
+ weixin-java-miniapp
+ ${weixin-java-miniapp.version}
+
+
+
+
+ com.github.binarywang
+ weixin-java-mp
+ ${weixin-java-mp.version}
+
+
+
+
+ com.github.wechatpay-apiv3
+ wechatpay-java
+ ${wechatpay-java.version}
+
+
+
+ com.alibaba
+ fastjson
+ ${fastjson.version}
+
+
+
+
+ com.alipay.sdk
+ alipay-sdk-java
+ ${alipay-sdk.version}
+
+
+
+
+ org.apache.poi
+ poi
+ ${poi.version}
+
+
+
+ org.apache.poi
+ poi-ooxml
+ ${poi.version}
+
+
+
+ org.apache.poi
+ poi-scratchpad
+ ${poi.version}
+
+
+
+
+ com.itextpdf
+ kernel
+ ${itext.version}
+
+
+
+ com.itextpdf
+ layout
+ ${itext.version}
+
+
+
+
+ org.springframework.ai
+ spring-ai-bom
+ ${spring-ai.pom.version}
+ pom
+ import
+
+
+
+
+ org.springframework.ai
+ spring-ai-core
+
+
+ org.springframework.ai
+ spring-ai-openai-spring-boot-starter
+
+
+
+
+ dev.langchain4j
+ langchain4j-community-bom
+ ${langchain4j.pom.version}
+ pom
+ import
+
+
+
+
+ dev.langchain4j
+ langchain4j
+
+
+ dev.langchain4j
+ langchain4j-spring-boot-starter
+
+
+ dev.langchain4j
+ langchain4j-azure-open-ai
+
+
+ dev.langchain4j
+ langchain4j-zhipu-ai
+
+
+ dev.langchain4j
+ langchain4j-dashscope
+
+
+
+
+ com.squareup.okhttp3
+ okhttp
+ ${okhttp.version}
+
+
+
+ com.squareup.okhttp3
+ okhttp-sse
+ ${okhttp.version}
+
+
+
+ org.dromara
+ ruoyi-ai
+ ${revision}
+
+
+
+ org.dromara
+ ruoyi-system
+ ${revision}
+
+
+
+ org.dromara
+ ruoyi-chat
+ ${revision}
+
+
+
+ org.dromara
+ ruoyi-job
+ ${revision}
+
+
+
+ org.dromara
+ ruoyi-generator
+ ${revision}
+
+
+
+ org.dromara
+ ruoyi-knowledge
+ ${revision}
+
+
+
+
+ org.dromara
+ ruoyi-workflow
+ ${revision}
+
+
+
+
+ org.dromara
+ ruoyi-mp
+ ${revision}
+
+
+
+
+ org.dromara
+ ruoyi-member
+ ${revision}
+
+
+
+
+ org.dromara
+ ruoyi-pay
+ ${revision}
+
+
+
+
+ org.dromara
+ ruoyi-mall
+ ${revision}
+
+
+
+
+
+
+ ruoyi-common
+ ruoyi-extend
+ ruoyi-modules
+ ruoyi-server
+
+ pom
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ ${maven-compiler-plugin.version}
+
+ ${java.version}
+ ${java.version}
+ ${project.build.sourceEncoding}
+
+
+ com.github.therapi
+ therapi-runtime-javadoc-scribe
+ ${therapi-javadoc.version}
+
+
+ org.projectlombok
+ lombok
+ ${lombok.version}
+
+
+ org.springframework.boot
+ spring-boot-configuration-processor
+ ${spring-boot.version}
+
+
+ io.github.linpeilie
+ mapstruct-plus-processor
+ ${mapstruct-plus.version}
+
+
+ org.projectlombok
+ lombok-mapstruct-binding
+ ${mapstruct-plus.lombok.version}
+
+
+
+ -parameters
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ ${maven-surefire-plugin.version}
+
+ -Dfile.encoding=UTF-8
+
+ ${profiles.active}
+
+ exclude
+
+
+
+
+ org.codehaus.mojo
+ flatten-maven-plugin
+ ${flatten-maven-plugin.version}
+
+ true
+ resolveCiFriendliesOnly
+
+
+
+ flatten
+ process-resources
+
+ flatten
+
+
+
+ flatten.clean
+ clean
+
+ clean
+
+
+
+
+
+
+
+ src/main/resources
+
+ false
+
+
+ src/main/resources
+
+
+ application*
+ bootstrap*
+ banner*
+
+
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ central
+ Maven Central
+ https://repo1.maven.org/maven2
+
+ true
+
+
+
+ public
+ huawei nexus
+ https://mirrors.huaweicloud.com/repository/maven/
+
+ true
+
+
+
+ spring-milestones
+ Spring Milestones
+ https://repo.spring.io/milestone
+
+ false
+
+
+
+ spring-snapshots
+ Spring Snapshots
+ https://repo.spring.io/snapshot
+
+ false
+
+
+
+
+
+
+ public
+ huawei nexus
+ https://mirrors.huaweicloud.com/repository/maven/
+
+ true
+
+
+ false
+
+
+
+
+
+
diff --git a/ruoyi-common/.DS_Store b/ruoyi-common/.DS_Store
new file mode 100644
index 0000000..f689641
Binary files /dev/null and b/ruoyi-common/.DS_Store differ
diff --git a/ruoyi-common/pom.xml b/ruoyi-common/pom.xml
new file mode 100644
index 0000000..b92f4c0
--- /dev/null
+++ b/ruoyi-common/pom.xml
@@ -0,0 +1,45 @@
+
+
+
+ org.dromara
+ PoJie
+ ${revision}
+
+ 4.0.0
+
+
+ ruoyi-common-bom
+ ruoyi-common-social
+ ruoyi-common-core
+ ruoyi-common-doc
+ ruoyi-common-excel
+ ruoyi-common-idempotent
+ ruoyi-common-job
+ ruoyi-common-log
+ ruoyi-common-mail
+ ruoyi-common-mybatis
+ ruoyi-common-oss
+ ruoyi-common-ratelimiter
+ ruoyi-common-redis
+ ruoyi-common-satoken
+ ruoyi-common-security
+ ruoyi-common-sms
+ ruoyi-common-web
+ ruoyi-common-translation
+ ruoyi-common-json
+ ruoyi-common-encrypt
+ ruoyi-common-sensitive
+ ruoyi-common-websocket
+ ruoyi-common-sse
+
+
+ ruoyi-common
+ pom
+
+
+ common 通用模块
+
+
+
diff --git a/ruoyi-common/ruoyi-common-bom/pom.xml b/ruoyi-common/ruoyi-common-bom/pom.xml
new file mode 100644
index 0000000..94a0908
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-bom/pom.xml
@@ -0,0 +1,178 @@
+
+
+ 4.0.0
+
+ org.dromara
+ ruoyi-common-bom
+ ${revision}
+ pom
+
+
+ ruoyi-common-bom common依赖项
+
+
+
+ 5.4.1
+
+
+
+
+
+
+ org.dromara
+ ruoyi-common-core
+ ${revision}
+
+
+
+
+ org.dromara
+ ruoyi-common-doc
+ ${revision}
+
+
+
+
+ org.dromara
+ ruoyi-common-excel
+ ${revision}
+
+
+
+
+ org.dromara
+ ruoyi-common-idempotent
+ ${revision}
+
+
+
+
+ org.dromara
+ ruoyi-common-job
+ ${revision}
+
+
+
+
+ org.dromara
+ ruoyi-common-log
+ ${revision}
+
+
+
+
+ org.dromara
+ ruoyi-common-mail
+ ${revision}
+
+
+
+
+ org.dromara
+ ruoyi-common-mybatis
+ ${revision}
+
+
+
+
+ org.dromara
+ ruoyi-common-oss
+ ${revision}
+
+
+
+
+ org.dromara
+ ruoyi-common-ratelimiter
+ ${revision}
+
+
+
+
+ org.dromara
+ ruoyi-common-redis
+ ${revision}
+
+
+
+
+ org.dromara
+ ruoyi-common-satoken
+ ${revision}
+
+
+
+
+ org.dromara
+ ruoyi-common-security
+ ${revision}
+
+
+
+
+ org.dromara
+ ruoyi-common-sms
+ ${revision}
+
+
+
+ org.dromara
+ ruoyi-common-social
+ ${revision}
+
+
+
+
+ org.dromara
+ ruoyi-common-web
+ ${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-websocket
+ ${revision}
+
+
+
+
+ org.dromara
+ ruoyi-common-sse
+ ${revision}
+
+
+
+
+
+
diff --git a/ruoyi-common/ruoyi-common-core/pom.xml b/ruoyi-common/ruoyi-common-core/pom.xml
new file mode 100644
index 0000000..d74f44e
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/pom.xml
@@ -0,0 +1,111 @@
+
+
+
+ org.dromara
+ ruoyi-common
+ ${revision}
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+ 16
+ 16
+
+
+
+
+ 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
+
+
+
+ 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..bcf5d41
--- /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 pengles
+ */
+@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..6427ad5
--- /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 pengles
+ */
+@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..d0cfbaf
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/config/ThreadPoolConfig.java
@@ -0,0 +1,87 @@
+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.config.properties.ThreadPoolProperties;
+import org.dromara.common.core.utils.SpringUtils;
+import org.dromara.common.core.utils.Threads;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.core.task.VirtualThreadTaskExecutor;
+import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
+
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+import java.util.concurrent.ThreadPoolExecutor;
+
+/**
+ * 线程池配置
+ *
+ * @author pengles
+ **/
+@Slf4j
+@AutoConfiguration
+@EnableConfigurationProperties(ThreadPoolProperties.class)
+public class ThreadPoolConfig {
+
+ /**
+ * 核心线程数 = cpu 核心数 + 1
+ */
+ private final int core = Runtime.getRuntime().availableProcessors() + 1;
+
+ private ScheduledExecutorService scheduledExecutorService;
+
+ @Bean(name = "threadPoolTaskExecutor")
+ @ConditionalOnProperty(prefix = "thread-pool", name = "enabled", havingValue = "true")
+ public ThreadPoolTaskExecutor threadPoolTaskExecutor(ThreadPoolProperties threadPoolProperties) {
+ ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
+ executor.setCorePoolSize(core);
+ executor.setMaxPoolSize(core * 2);
+ executor.setQueueCapacity(threadPoolProperties.getQueueCapacity());
+ executor.setKeepAliveSeconds(threadPoolProperties.getKeepAliveSeconds());
+ executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
+ return executor;
+ }
+
+ /**
+ * 执行周期性或定时任务
+ */
+ @Bean(name = "scheduledExecutorService")
+ protected ScheduledExecutorService scheduledExecutorService() {
+ // 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..2b8343d
--- /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 pengles
+ */
+@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/config/properties/ThreadPoolProperties.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/config/properties/ThreadPoolProperties.java
new file mode 100644
index 0000000..334b242
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/config/properties/ThreadPoolProperties.java
@@ -0,0 +1,30 @@
+package org.dromara.common.core.config.properties;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+/**
+ * 线程池 配置属性
+ *
+ * @author pengles
+ */
+@Data
+@ConfigurationProperties(prefix = "thread-pool")
+public class ThreadPoolProperties {
+
+ /**
+ * 是否开启线程池
+ */
+ private boolean enabled;
+
+ /**
+ * 队列最大长度
+ */
+ private int queueCapacity;
+
+ /**
+ * 线程池维护线程所允许的空闲时间
+ */
+ private int keepAliveSeconds;
+
+}
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..fc246cc
--- /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 pengles
+ */
+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..279ebb6
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/CacheNames.java
@@ -0,0 +1,83 @@
+package org.dromara.common.core.constant;
+
+/**
+ * 缓存组名称常量
+ *
+ * key 格式为 cacheNames#ttl#maxIdleTime#maxSize
+ *
+ * ttl 过期时间 如果设置为0则不过期 默认为0
+ * maxIdleTime 最大空闲时间 根据LRU算法清理空闲数据 如果设置为0则不检测 默认为0
+ * maxSize 组最大长度 根据LRU算法清理溢出数据 如果设置为0则无限长 默认为0
+ *
+ * 例子: test#60s、test#0#60s、test#0#1m#1000、test#1h#0#500
+ *
+ * @author pengles
+ */
+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_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..f47654c
--- /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 pengles
+ */
+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..65868c0
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/GlobalConstants.java
@@ -0,0 +1,34 @@
+package org.dromara.common.core.constant;
+
+/**
+ * 全局的key常量 (业务无关的key)
+ *
+ * @author pengles
+ */
+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..70c3383
--- /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 pengles
+ */
+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..0220b46
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/SystemConstants.java
@@ -0,0 +1,95 @@
+package org.dromara.common.core.constant;
+
+/**
+ * 系统常量信息
+ *
+ * @author pengles
+ */
+public interface SystemConstants {
+
+ /**
+ * 正常状态
+ */
+ String NORMAL = "0";
+
+ /**
+ * 异常状态
+ */
+ String DISABLE = "1";
+
+ /**
+ * 是否为系统默认(是)
+ */
+ String YES = "Y";
+
+ /**
+ * 是否为系统默认(否)
+ */
+ String NO = "N";
+
+ /**
+ * 是否菜单外链(是)
+ */
+ 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";
+
+ /**
+ * 超级管理员角色 roleKey
+ */
+ String SUPER_ADMIN_ROLE_KEY = "superadmin";
+
+ /**
+ * 管理员角色 roleKey (保留 roleKey)
+ */
+ String ADMIN_ROLE_KEY = "admin";
+
+ /**
+ * 超级管理员ID
+ */
+ Long SUPER_ADMIN_ID = 1L;
+
+ /**
+ * 根部门祖级列表
+ */
+ String ROOT_DEPT_ANCESTORS = "0";
+
+ /**
+ * 默认部门 ID
+ */
+ Long DEFAULT_DEPT_ID = 100L;
+
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/UserConstants.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/UserConstants.java
new file mode 100644
index 0000000..8323bb8
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/UserConstants.java
@@ -0,0 +1,118 @@
+package org.dromara.common.core.constant;
+
+/**
+ * 用户常量信息
+ *
+ * @author pengles
+ */
+public interface UserConstants {
+
+ /**
+ * 平台内系统用户的唯一标志
+ */
+ String SYS_USER = "SYS_USER";
+
+ /**
+ * App用户类型
+ */
+ String APP_USER_TYPE = "APP_USER";
+
+ /**
+ * 正常状态
+ */
+ String NORMAL = "0";
+
+ /**
+ * 异常状态
+ */
+ String EXCEPTION = "1";
+
+ /**
+ * 用户封禁状态
+ */
+ String USER_DISABLE = "1";
+
+ /**
+ * 角色封禁状态
+ */
+ String ROLE_DISABLE = "1";
+
+ /**
+ * 部门正常状态
+ */
+ String DEPT_NORMAL = "0";
+
+ /**
+ * 部门停用状态
+ */
+ String DEPT_DISABLE = "1";
+
+ /**
+ * 字典正常状态
+ */
+ String DICT_NORMAL = "0";
+
+ /**
+ * 是否为系统默认(是)
+ */
+ String YES = "Y";
+
+ /**
+ * 是否菜单外链(是)
+ */
+ String YES_FRAME = "0";
+
+ /**
+ * 是否菜单外链(否)
+ */
+ String NO_FRAME = "1";
+
+ /**
+ * 菜单类型(目录)
+ */
+ String 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";
+
+ /**
+ * 校验返回结果码
+ */
+ String UNIQUE = "0";
+ String NOT_UNIQUE = "1";
+
+ /**
+ * 用户名长度限制
+ */
+ int USERNAME_MIN_LENGTH = 2;
+ int USERNAME_MAX_LENGTH = 20;
+
+ /**
+ * 密码长度限制
+ */
+ int PASSWORD_MIN_LENGTH = 5;
+ int PASSWORD_MAX_LENGTH = 20;
+
+}
\ No newline at end of file
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..81a287f
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/R.java
@@ -0,0 +1,122 @@
+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 pengles
+ */
+@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(Boolean success) {
+ return restResult(null, success ? SUCCESS : FAIL, success ? "操作成功" : "操作失败");
+ }
+
+ public static R ok(Boolean success, String msg) {
+ return restResult(null, success ? SUCCESS : FAIL, msg);
+ }
+
+ public static R ok(Boolean success, T data) {
+ return restResult(data, success ? SUCCESS : FAIL, 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/dto/CompleteTaskDTO.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/dto/CompleteTaskDTO.java
new file mode 100644
index 0000000..2e63f8a
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/dto/CompleteTaskDTO.java
@@ -0,0 +1,71 @@
+package org.dromara.common.core.domain.dto;
+
+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 CompleteTaskDTO 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-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/dto/DeptDTO.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/dto/DeptDTO.java
new file mode 100644
index 0000000..7b748b0
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/dto/DeptDTO.java
@@ -0,0 +1,36 @@
+package org.dromara.common.core.domain.dto;
+
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * 部门
+ *
+ * @author AprilWind
+ */
+@Data
+@NoArgsConstructor
+public class DeptDTO implements Serializable {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 部门ID
+ */
+ private Long deptId;
+
+ /**
+ * 父部门ID
+ */
+ private Long parentId;
+
+ /**
+ * 部门名称
+ */
+ private String deptName;
+
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/dto/DictDataDTO.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/dto/DictDataDTO.java
new file mode 100644
index 0000000..dff1a75
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/dto/DictDataDTO.java
@@ -0,0 +1,41 @@
+package org.dromara.common.core.domain.dto;
+
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * 字典数据DTO
+ *
+ * @author AprilWind
+ */
+@Data
+@NoArgsConstructor
+public class DictDataDTO implements Serializable {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 字典标签
+ */
+ private String dictLabel;
+
+ /**
+ * 字典键值
+ */
+ private String dictValue;
+
+ /**
+ * 是否默认(Y是 N否)
+ */
+ private String isDefault;
+
+ /**
+ * 备注
+ */
+ private String remark;
+
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/dto/DictTypeDTO.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/dto/DictTypeDTO.java
new file mode 100644
index 0000000..43ab142
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/dto/DictTypeDTO.java
@@ -0,0 +1,41 @@
+package org.dromara.common.core.domain.dto;
+
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * 字典类型DTO
+ *
+ * @author AprilWind
+ */
+@Data
+@NoArgsConstructor
+public class DictTypeDTO implements Serializable {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 字典主键
+ */
+ private Long dictId;
+
+ /**
+ * 字典名称
+ */
+ private String dictName;
+
+ /**
+ * 字典类型
+ */
+ private String dictType;
+
+ /**
+ * 备注
+ */
+ private String remark;
+
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/dto/FlowCopyDTO.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/dto/FlowCopyDTO.java
new file mode 100644
index 0000000..2f20b21
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/dto/FlowCopyDTO.java
@@ -0,0 +1,30 @@
+package org.dromara.common.core.domain.dto;
+
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+
+/**
+ * 抄送
+ *
+ * @author may
+ */
+@Data
+public class FlowCopyDTO implements Serializable {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 用户id
+ */
+ private Long userId;
+
+ /**
+ * 用户名称
+ */
+ private String userName;
+
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/dto/OssDTO.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/dto/OssDTO.java
new file mode 100644
index 0000000..823af0c
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/dto/OssDTO.java
@@ -0,0 +1,46 @@
+package org.dromara.common.core.domain.dto;
+
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * OSS对象
+ *
+ * @author pengles
+ */
+@Data
+@NoArgsConstructor
+public class OssDTO implements Serializable {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 对象存储主键
+ */
+ private Long ossId;
+
+ /**
+ * 文件名
+ */
+ private String fileName;
+
+ /**
+ * 原名
+ */
+ private String originalName;
+
+ /**
+ * 文件后缀名
+ */
+ private String fileSuffix;
+
+ /**
+ * URL地址
+ */
+ private String url;
+
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/dto/PostDTO.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/dto/PostDTO.java
new file mode 100644
index 0000000..7536ee3
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/dto/PostDTO.java
@@ -0,0 +1,46 @@
+package org.dromara.common.core.domain.dto;
+
+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-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/dto/RoleDTO.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/dto/RoleDTO.java
new file mode 100644
index 0000000..70e81de
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/dto/RoleDTO.java
@@ -0,0 +1,42 @@
+package org.dromara.common.core.domain.dto;
+
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * 角色
+ *
+ * @author pengles
+ */
+
+@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-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/dto/StartProcessDTO.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/dto/StartProcessDTO.java
new file mode 100644
index 0000000..3934ada
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/dto/StartProcessDTO.java
@@ -0,0 +1,45 @@
+package org.dromara.common.core.domain.dto;
+
+
+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 StartProcessDTO 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-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/dto/StartProcessReturnDTO.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/dto/StartProcessReturnDTO.java
new file mode 100644
index 0000000..8a4bda3
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/dto/StartProcessReturnDTO.java
@@ -0,0 +1,30 @@
+package org.dromara.common.core.domain.dto;
+
+
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * 启动流程返回对象
+ *
+ * @author pengles
+ */
+@Data
+public class StartProcessReturnDTO implements Serializable {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 流程实例id
+ */
+ private Long processInstanceId;
+
+ /**
+ * 任务id
+ */
+ private Long taskId;
+
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/dto/TaskAssigneeDTO.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/dto/TaskAssigneeDTO.java
new file mode 100644
index 0000000..85893e1
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/dto/TaskAssigneeDTO.java
@@ -0,0 +1,101 @@
+package org.dromara.common.core.domain.dto;
+
+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 TaskAssigneeDTO implements Serializable {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 总大小
+ */
+ private Long total = 0L;
+
+ /**
+ *
+ */
+ private List list;
+
+ public TaskAssigneeDTO(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 {
+
+ /**
+ * 主键
+ */
+ private String storageId;
+
+ /**
+ * 权限编码
+ */
+ private String handlerCode;
+
+ /**
+ * 权限名称
+ */
+ private String handlerName;
+
+ /**
+ * 权限分组
+ */
+ private String groupName;
+
+ /**
+ * 创建时间
+ */
+ private Date createTime;
+ }
+
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/dto/UserDTO.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/dto/UserDTO.java
new file mode 100644
index 0000000..2e5dbc8
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/dto/UserDTO.java
@@ -0,0 +1,73 @@
+package org.dromara.common.core.domain.dto;
+
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.Date;
+
+
+/**
+ * 用户
+ *
+ * @author pengles
+ */
+@Data
+@NoArgsConstructor
+public class UserDTO implements Serializable {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 用户ID
+ */
+ private Long userId;
+
+ /**
+ * 部门ID
+ */
+ private Long deptId;
+
+ /**
+ * 用户账号
+ */
+ private String userName;
+
+ /**
+ * 用户昵称
+ */
+ private String nickName;
+
+ /**
+ * 用户类型(sys_user系统用户)
+ */
+ private String userType;
+
+ /**
+ * 用户邮箱
+ */
+ private String email;
+
+ /**
+ * 手机号码
+ */
+ private String phonenumber;
+
+ /**
+ * 用户性别(0男 1女 2未知)
+ */
+ private String sex;
+
+ /**
+ * 帐号状态(0正常 1停用)
+ */
+ private String status;
+
+ /**
+ * 创建时间
+ */
+ private Date createTime;
+
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/dto/UserOnlineDTO.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/dto/UserOnlineDTO.java
new file mode 100644
index 0000000..b74905c
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/dto/UserOnlineDTO.java
@@ -0,0 +1,72 @@
+package org.dromara.common.core.domain.dto;
+
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * 当前在线会话
+ *
+ * @author pengles
+ */
+
+@Data
+@NoArgsConstructor
+public class UserOnlineDTO implements Serializable {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 会话编号
+ */
+ private String tokenId;
+
+ /**
+ * 部门名称
+ */
+ private String deptName;
+
+ /**
+ * 用户名称
+ */
+ private String userName;
+
+ /**
+ * 客户端
+ */
+ private String clientKey;
+
+ /**
+ * 设备类型
+ */
+ private String deviceType;
+
+ /**
+ * 登录IP地址
+ */
+ private String ipaddr;
+
+ /**
+ * 登录地址
+ */
+ private String loginLocation;
+
+ /**
+ * 浏览器类型
+ */
+ private String browser;
+
+ /**
+ * 操作系统
+ */
+ private String os;
+
+ /**
+ * 登录时间
+ */
+ private Long loginTime;
+
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/event/ProcessDeleteEvent.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/event/ProcessDeleteEvent.java
new file mode 100644
index 0000000..03e7867
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/event/ProcessDeleteEvent.java
@@ -0,0 +1,29 @@
+package org.dromara.common.core.domain.event;
+
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * 删除流程监听
+ *
+ * @author AprilWind
+ */
+@Data
+public class ProcessDeleteEvent implements Serializable {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 流程定义编码
+ */
+ private String flowCode;
+
+ /**
+ * 业务id
+ */
+ private String businessId;
+
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/event/ProcessEvent.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/event/ProcessEvent.java
new file mode 100644
index 0000000..1973d21
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/event/ProcessEvent.java
@@ -0,0 +1,60 @@
+package org.dromara.common.core.domain.event;
+
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.Map;
+
+/**
+ * 总体流程监听
+ *
+ * @author may
+ */
+@Data
+public class ProcessEvent implements Serializable {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 流程定义编码
+ */
+ 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;
+
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/event/ProcessTaskEvent.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/event/ProcessTaskEvent.java
new file mode 100644
index 0000000..b13f157
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/event/ProcessTaskEvent.java
@@ -0,0 +1,54 @@
+package org.dromara.common.core.domain.event;
+
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * 流程任务监听
+ *
+ * @author may
+ */
+@Data
+public class ProcessTaskEvent implements Serializable {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 流程定义编码
+ */
+ 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;
+
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/model/EmailLoginBody.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/model/EmailLoginBody.java
new file mode 100644
index 0000000..9dc69a3
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/model/EmailLoginBody.java
@@ -0,0 +1,31 @@
+package org.dromara.common.core.domain.model;
+
+import jakarta.validation.constraints.Email;
+import jakarta.validation.constraints.NotBlank;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 邮件登录对象
+ *
+ * @author pengles
+ */
+
+@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-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..41e5de4
--- /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 java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * 用户登录对象
+ *
+ * @author pengles
+ */
+
+@Data
+public class LoginBody implements Serializable {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 客户端id
+ */
+ @NotBlank(message = "{auth.clientid.not.blank}")
+ private String clientId;
+
+ /**
+ * 授权类型
+ */
+ @NotBlank(message = "{auth.grant.type.not.blank}")
+ private String grantType;
+
+ /**
+ * 验证码
+ */
+ private String code;
+
+ /**
+ * 唯一标识
+ */
+ private String uuid;
+
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/model/LoginUser.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/model/LoginUser.java
new file mode 100644
index 0000000..84cf224
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/model/LoginUser.java
@@ -0,0 +1,143 @@
+package org.dromara.common.core.domain.model;
+
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.dromara.common.core.domain.dto.PostDTO;
+import org.dromara.common.core.domain.dto.RoleDTO;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * 登录用户身份权限
+ *
+ * @author pengles
+ */
+@Data
+@NoArgsConstructor
+public class LoginUser implements Serializable {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 用户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 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-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/model/PasswordLoginBody.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/model/PasswordLoginBody.java
new file mode 100644
index 0000000..40f2aa5
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/model/PasswordLoginBody.java
@@ -0,0 +1,31 @@
+package org.dromara.common.core.domain.model;
+
+import jakarta.validation.constraints.NotBlank;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.hibernate.validator.constraints.Length;
+
+/**
+ * 密码登录对象
+ *
+ * @author pengles
+ */
+@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-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/model/RegisterBody.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/model/RegisterBody.java
new file mode 100644
index 0000000..d37174a
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/model/RegisterBody.java
@@ -0,0 +1,33 @@
+package org.dromara.common.core.domain.model;
+
+import jakarta.validation.constraints.NotBlank;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.hibernate.validator.constraints.Length;
+
+/**
+ * 用户注册对象
+ *
+ * @author pengles
+ */
+@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-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/model/SmsLoginBody.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/model/SmsLoginBody.java
new file mode 100644
index 0000000..28bff61
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/model/SmsLoginBody.java
@@ -0,0 +1,29 @@
+package org.dromara.common.core.domain.model;
+
+import jakarta.validation.constraints.NotBlank;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 短信登录对象
+ *
+ * @author pengles
+ */
+
+@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-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/model/SocialLoginBody.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/model/SocialLoginBody.java
new file mode 100644
index 0000000..dabf4a6
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/model/SocialLoginBody.java
@@ -0,0 +1,35 @@
+package org.dromara.common.core.domain.model;
+
+import jakarta.validation.constraints.NotBlank;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 三方登录对象
+ *
+ * @author pengles
+ */
+
+@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-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/model/TaskAssigneeBody.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/model/TaskAssigneeBody.java
new file mode 100644
index 0000000..0cbed2f
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/model/TaskAssigneeBody.java
@@ -0,0 +1,56 @@
+package org.dromara.common.core.domain.model;
+
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * 任务受让人
+ *
+ * @author AprilWind
+ */
+@Data
+@NoArgsConstructor
+public class TaskAssigneeBody 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-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/model/XcxLoginBody.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/model/XcxLoginBody.java
new file mode 100644
index 0000000..01fbd53
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/model/XcxLoginBody.java
@@ -0,0 +1,28 @@
+package org.dromara.common.core.domain.model;
+
+import jakarta.validation.constraints.NotBlank;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 三方登录对象
+ *
+ * @author pengles
+ */
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class XcxLoginBody extends LoginBody {
+
+ /**
+ * 小程序id(多个小程序时使用)
+ */
+ private String appid;
+
+ /**
+ * 小程序code
+ */
+ @NotBlank(message = "{xcx.code.not.blank}")
+ private String code;
+
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/model/XcxLoginUser.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/model/XcxLoginUser.java
new file mode 100644
index 0000000..f741d1c
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/model/XcxLoginUser.java
@@ -0,0 +1,32 @@
+package org.dromara.common.core.domain.model;
+
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+
+import java.io.Serial;
+
+/**
+ * 小程序登录用户身份权限
+ *
+ * @author pengles
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@NoArgsConstructor
+public class XcxLoginUser extends LoginUser {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * openid
+ */
+ private String openid;
+
+ /**
+ * unionId
+ */
+ private String unionId;
+
+}
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..0fefa6b
--- /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 pengles
+ */
+@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..af69d08
--- /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 pengles
+ */
+@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..e8547e7
--- /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 pengles
+ */
+@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..0cc5a4d
--- /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 pengles
+ */
+@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..27b1022
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/exception/ServiceException.java
@@ -0,0 +1,67 @@
+package org.dromara.common.core.exception;
+
+import cn.hutool.core.text.StrFormatter;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+
+import java.io.Serial;
+
+/**
+ * 业务异常(支持占位符 {} )
+ *
+ * @author pengles
+ */
+@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 ServiceException(String message, Object... args) {
+ this.message = StrFormatter.format(message, args);
+ }
+
+ @Override
+ public String getMessage() {
+ return message;
+ }
+
+ public ServiceException setMessage(String message) {
+ this.message = message;
+ return this;
+ }
+
+ public ServiceException setDetailMessage(String detailMessage) {
+ this.detailMessage = detailMessage;
+ return this;
+ }
+}
diff --git a/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..2b97014
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/exception/base/BaseException.java
@@ -0,0 +1,74 @@
+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 pengles
+ */
+@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..7110be1
--- /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 pengles
+ */
+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..d6269a1
--- /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 pengles
+ */
+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..e990a92
--- /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 pengles
+ */
+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..af64b41
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/exception/user/CaptchaException.java
@@ -0,0 +1,18 @@
+package org.dromara.common.core.exception.user;
+
+import java.io.Serial;
+
+/**
+ * 验证码错误异常类
+ *
+ * @author pengles
+ */
+public class CaptchaException extends UserException {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ public CaptchaException() {
+ super("user.jcaptcha.error");
+ }
+}
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..6168782
--- /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 pengles
+ */
+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..9a806c0
--- /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 pengles
+ */
+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..5135a81
--- /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 pengles
+ */
+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/ConfigService.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/service/ConfigService.java
new file mode 100644
index 0000000..37cbbcf
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/service/ConfigService.java
@@ -0,0 +1,18 @@
+package org.dromara.common.core.service;
+
+/**
+ * 通用 参数配置服务
+ *
+ * @author pengles
+ */
+public interface ConfigService {
+
+ /**
+ * 根据参数 key 获取参数值
+ *
+ * @param configKey 参数 key
+ * @return 参数值
+ */
+ String getConfigValue(String configKey);
+
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/service/DeptService.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/service/DeptService.java
new file mode 100644
index 0000000..f477caf
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/service/DeptService.java
@@ -0,0 +1,37 @@
+package org.dromara.common.core.service;
+
+import org.dromara.common.core.domain.dto.DeptDTO;
+
+import java.util.List;
+
+/**
+ * 通用 部门服务
+ *
+ * @author pengles
+ */
+public interface DeptService {
+
+ /**
+ * 通过部门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-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..2e67183
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/service/DictService.java
@@ -0,0 +1,87 @@
+package org.dromara.common.core.service;
+
+import org.dromara.common.core.domain.dto.DictDataDTO;
+import org.dromara.common.core.domain.dto.DictTypeDTO;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 通用 字典服务
+ *
+ * @author pengles
+ */
+public interface DictService {
+
+ /**
+ * 分隔符
+ */
+ String SEPARATOR = ",";
+
+ /**
+ * 根据字典类型和字典值获取字典标签
+ *
+ * @param dictType 字典类型
+ * @param dictValue 字典值
+ * @return 字典标签
+ */
+ default String getDictLabel(String dictType, String dictValue) {
+ return getDictLabel(dictType, dictValue, SEPARATOR);
+ }
+
+ /**
+ * 根据字典类型和字典标签获取字典值
+ *
+ * @param dictType 字典类型
+ * @param dictLabel 字典标签
+ * @return 字典值
+ */
+ default String getDictValue(String dictType, String dictLabel) {
+ return getDictValue(dictType, dictLabel, SEPARATOR);
+ }
+
+ /**
+ * 根据字典类型和字典值获取字典标签
+ *
+ * @param dictType 字典类型
+ * @param dictValue 字典值
+ * @param separator 分隔符
+ * @return 字典标签
+ */
+ String getDictLabel(String dictType, String dictValue, String separator);
+
+ /**
+ * 根据字典类型和字典标签获取字典值
+ *
+ * @param dictType 字典类型
+ * @param dictLabel 字典标签
+ * @param separator 分隔符
+ * @return 字典值
+ */
+ String getDictValue(String dictType, String dictLabel, String separator);
+
+ /**
+ * 获取字典下所有的字典值与标签
+ *
+ * @param dictType 字典类型
+ * @return dictValue为key,dictLabel为值组成的Map
+ */
+ Map getAllDictByDictType(String dictType);
+
+ /**
+ * 根据字典类型查询详细信息
+ *
+ * @param dictType 字典类型
+ * @return 字典类型详细信息
+ */
+ DictTypeDTO getDictType(String dictType);
+
+ /**
+ * 根据字典类型查询字典数据列表
+ *
+ * @param dictType 字典类型
+ * @return 字典数据列表
+ */
+ List getDictData(String dictType);
+
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/service/OssService.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/service/OssService.java
new file mode 100644
index 0000000..703ebf3
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/service/OssService.java
@@ -0,0 +1,29 @@
+package org.dromara.common.core.service;
+
+import org.dromara.common.core.domain.dto.OssDTO;
+
+import java.util.List;
+
+/**
+ * 通用 OSS服务
+ *
+ * @author pengles
+ */
+public interface OssService {
+
+ /**
+ * 通过ossId查询对应的url
+ *
+ * @param ossIds ossId串逗号分隔
+ * @return url串逗号分隔
+ */
+ String selectUrlByIds(String ossIds);
+
+ /**
+ * 通过ossId查询列表
+ *
+ * @param ossIds ossId串逗号分隔
+ * @return 列表
+ */
+ List selectByIds(String ossIds);
+}
diff --git a/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..893f0a9
--- /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 pengles
+ */
+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/service/PostService.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/service/PostService.java
new file mode 100644
index 0000000..41d4e83
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/service/PostService.java
@@ -0,0 +1,10 @@
+package org.dromara.common.core.service;
+
+/**
+ * 通用 岗位服务
+ *
+ * @author AprilWind
+ */
+public interface PostService {
+
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/service/RoleService.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/service/RoleService.java
new file mode 100644
index 0000000..ba62c82
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/service/RoleService.java
@@ -0,0 +1,10 @@
+package org.dromara.common.core.service;
+
+/**
+ * 通用 角色服务
+ *
+ * @author AprilWind
+ */
+public interface RoleService {
+
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/service/TaskAssigneeService.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/service/TaskAssigneeService.java
new file mode 100644
index 0000000..1c1f8a0
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/service/TaskAssigneeService.java
@@ -0,0 +1,45 @@
+package org.dromara.common.core.service;
+
+import org.dromara.common.core.domain.dto.TaskAssigneeDTO;
+import org.dromara.common.core.domain.model.TaskAssigneeBody;
+
+/**
+ * 工作流设计器获取任务执行人
+ *
+ * @author pengles
+ */
+public interface TaskAssigneeService {
+
+ /**
+ * 查询角色并返回任务指派的列表,支持分页
+ *
+ * @param taskQuery 查询条件
+ * @return 办理人
+ */
+ TaskAssigneeDTO selectRolesByTaskAssigneeList(TaskAssigneeBody taskQuery);
+
+ /**
+ * 查询岗位并返回任务指派的列表,支持分页
+ *
+ * @param taskQuery 查询条件
+ * @return 办理人
+ */
+ TaskAssigneeDTO selectPostsByTaskAssigneeList(TaskAssigneeBody taskQuery);
+
+ /**
+ * 查询部门并返回任务指派的列表,支持分页
+ *
+ * @param taskQuery 查询条件
+ * @return 办理人
+ */
+ TaskAssigneeDTO selectDeptsByTaskAssigneeList(TaskAssigneeBody taskQuery);
+
+ /**
+ * 查询用户并返回任务指派的列表,支持分页
+ *
+ * @param taskQuery 查询条件
+ * @return 办理人
+ */
+ TaskAssigneeDTO selectUsersByTaskAssigneeList(TaskAssigneeBody taskQuery);
+
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/service/UserService.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/service/UserService.java
new file mode 100644
index 0000000..991928b
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/service/UserService.java
@@ -0,0 +1,127 @@
+package org.dromara.common.core.service;
+
+import org.dromara.common.core.domain.dto.UserDTO;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 通用 用户服务
+ *
+ * @author pengles
+ */
+public interface UserService {
+
+ /**
+ * 通过用户ID查询用户账户
+ *
+ * @param userId 用户ID
+ * @return 用户账户
+ */
+ String selectUserNameById(Long userId);
+
+ /**
+ * 通过用户ID查询用户账户
+ *
+ * @param userId 用户ID
+ * @return 用户名称
+ */
+ String selectNicknameById(Long userId);
+
+ /**
+ * 通过用户ID查询用户账户
+ *
+ * @param userIds 用户ID 多个用逗号隔开
+ * @return 用户名称
+ */
+ String selectNicknameByIds(String userIds);
+
+ /**
+ * 通过用户ID查询用户手机号
+ *
+ * @param userId 用户id
+ * @return 用户手机号
+ */
+ String selectPhonenumberById(Long userId);
+
+ /**
+ * 通过用户ID查询用户邮箱
+ *
+ * @param userId 用户id
+ * @return 用户邮箱
+ */
+ String selectEmailById(Long userId);
+
+ /**
+ * 通过用户ID查询用户列表
+ *
+ * @param userIds 用户ids
+ * @return 用户列表
+ */
+ List selectListByIds(List userIds);
+
+ /**
+ * 通过角色ID查询用户ID
+ *
+ * @param roleIds 角色ids
+ * @return 用户ids
+ */
+ List selectUserIdsByRoleIds(List roleIds);
+
+ /**
+ * 通过角色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-common/ruoyi-common-core/src/main/java/org/dromara/common/core/service/WorkflowService.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/service/WorkflowService.java
new file mode 100644
index 0000000..23dedca
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/service/WorkflowService.java
@@ -0,0 +1,88 @@
+package org.dromara.common.core.service;
+
+import org.dromara.common.core.domain.dto.CompleteTaskDTO;
+import org.dromara.common.core.domain.dto.StartProcessDTO;
+import org.dromara.common.core.domain.dto.StartProcessReturnDTO;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 通用 工作流服务
+ *
+ * @author may
+ */
+public interface WorkflowService {
+
+ /**
+ * 运行中的实例 删除程实例,删除历史记录,删除业务与流程关联信息
+ *
+ * @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 startProcess 参数
+ * @return 结果
+ */
+ StartProcessReturnDTO startWorkFlow(StartProcessDTO startProcess);
+
+ /**
+ * 办理任务
+ * 系统后台发起审批 无用户信息 需要忽略权限
+ * completeTask.getVariables().put("ignore", true);
+ *
+ * @param completeTask 参数
+ */
+ boolean completeTask(CompleteTaskDTO completeTask);
+
+ /**
+ * 办理任务
+ *
+ * @param taskId 任务ID
+ * @param message 办理意见
+ */
+ boolean completeTask(Long taskId, String message);
+}
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..897554b
--- /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 pengles
+ */
+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..8cc77d9
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/MapstructUtils.java
@@ -0,0 +1,93 @@
+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 pengles
+ */
+@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..17ea377
--- /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 pengles
+ */
+@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..439c2af
--- /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 pengles
+ */
+@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..f831e3c
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/SpringUtils.java
@@ -0,0 +1,67 @@
+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 pengles
+ */
+@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..859e045
--- /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 pengles
+ */
+@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..2637119
--- /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 pengles
+ */
+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..69d54be
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/Threads.java
@@ -0,0 +1,63 @@
+package org.dromara.common.core.utils;
+
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+
+import java.util.concurrent.*;
+
+/**
+ * 线程相关工具类.
+ *
+ * @author pengles
+ */
+@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..c07dae9
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/TreeBuildUtils.java
@@ -0,0 +1,123 @@
+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.Set;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/**
+ * 扩展 hutool TreeUtil 封装系统树构建
+ *
+ * @author pengles
+ */
+@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 List> build(List list, K parentId, NodeParser nodeParser) {
+ if (CollUtil.isEmpty(list)) {
+ return CollUtil.newArrayList();
+ }
+ return TreeUtil.build(list, parentId, DEFAULT_CONFIG, nodeParser);
+ }
+
+ /**
+ * 构建多根节点的树结构(支持多个顶级节点)
+ *
+ * @param list 原始数据列表
+ * @param getId 获取节点 ID 的方法引用,例如:node -> node.getId()
+ * @param getParentId 获取节点父级 ID 的方法引用,例如:node -> node.getParentId()
+ * @param parser 树节点属性映射器,用于将原始节点 T 转为 Tree 节点
+ * @param 原始数据类型(如实体类、DTO 等)
+ * @param 节点 ID 类型(如 Long、String)
+ * @return 构建完成的树形结构(可能包含多个顶级根节点)
+ */
+ public static List> buildMultiRoot(List list, Function getId, Function getParentId, NodeParser parser) {
+ if (CollUtil.isEmpty(list)) {
+ return CollUtil.newArrayList();
+ }
+
+ Set rootParentIds = StreamUtils.toSet(list, getParentId);
+ rootParentIds.removeAll(StreamUtils.toSet(list, getId));
+
+ // 构建每一个根 parentId 下的树,并合并成最终结果列表
+ return rootParentIds.stream()
+ .flatMap(rootParentId -> TreeUtil.build(list, rootParentId, parser).stream())
+ .collect(Collectors.toList());
+ }
+
+ /**
+ * 获取节点列表中所有节点的叶子节点
+ *
+ * @param 节点ID的类型
+ * @param nodes 节点列表
+ * @return 包含所有叶子节点的列表
+ */
+ public static List> getLeafNodes(List> nodes) {
+ if (CollUtil.isEmpty(nodes)) {
+ return CollUtil.newArrayList();
+ }
+ return nodes.stream()
+ .flatMap(TreeBuildUtils::extractLeafNodes)
+ .collect(Collectors.toList());
+ }
+
+ /**
+ * 获取指定节点下的所有叶子节点
+ *
+ * @param 节点ID的类型
+ * @param node 要查找叶子节点的根节点
+ * @return 包含所有叶子节点的列表
+ */
+ private static Stream> extractLeafNodes(Tree node) {
+ if (!node.hasChild()) {
+ return Stream.of(node);
+ } else {
+ // 递归调用,获取所有子节点的叶子节点
+ return node.getChildren().stream()
+ .flatMap(TreeBuildUtils::extractLeafNodes);
+ }
+ }
+
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/ValidatorUtils.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/ValidatorUtils.java
new file mode 100644
index 0000000..66db70e
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/ValidatorUtils.java
@@ -0,0 +1,35 @@
+package org.dromara.common.core.utils;
+
+import jakarta.validation.ConstraintViolation;
+import jakarta.validation.ConstraintViolationException;
+import jakarta.validation.Validator;
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+
+import java.util.Set;
+
+/**
+ * Validator 校验框架工具
+ *
+ * @author pengles
+ */
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public class ValidatorUtils {
+
+ private static final Validator VALID = SpringUtils.getBean(Validator.class);
+
+ /**
+ * 对给定对象进行参数校验,并根据指定的校验组进行校验
+ *
+ * @param object 要进行校验的对象
+ * @param groups 校验组
+ * @throws ConstraintViolationException 如果校验不通过,则抛出参数校验异常
+ */
+ public static void validate(T object, Class>... groups) {
+ Set> validate = VALID.validate(object, groups);
+ if (!validate.isEmpty()) {
+ throw new ConstraintViolationException("参数校验异常", validate);
+ }
+ }
+
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/file/FileUtils.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/file/FileUtils.java
new file mode 100644
index 0000000..48cffad
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/file/FileUtils.java
@@ -0,0 +1,43 @@
+package org.dromara.common.core.utils.file;
+
+import cn.hutool.core.io.FileUtil;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+
+/**
+ * 文件处理工具类
+ *
+ * @author pengles
+ */
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public class FileUtils extends FileUtil {
+
+ /**
+ * 下载文件名重新编码
+ *
+ * @param response 响应对象
+ * @param realFileName 真实文件名
+ */
+ public static void setAttachmentResponseHeader(HttpServletResponse response, String realFileName) {
+ String percentEncodedFileName = percentEncode(realFileName);
+ String contentDispositionValue = "attachment; filename=%s;filename*=utf-8''%s".formatted(percentEncodedFileName, percentEncodedFileName);
+ response.addHeader("Access-Control-Expose-Headers", "Content-Disposition,download-filename");
+ response.setHeader("Content-disposition", contentDispositionValue);
+ response.setHeader("download-filename", percentEncodedFileName);
+ }
+
+ /**
+ * 百分号编码工具方法
+ *
+ * @param s 需要百分号编码的字符串
+ * @return 百分号编码后的字符串
+ */
+ public static String percentEncode(String s) {
+ String encode = URLEncoder.encode(s, StandardCharsets.UTF_8);
+ return encode.replaceAll("\\+", "%20");
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/file/MimeTypeUtils.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/file/MimeTypeUtils.java
new file mode 100644
index 0000000..4f61ded
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/file/MimeTypeUtils.java
@@ -0,0 +1,40 @@
+package org.dromara.common.core.utils.file;
+
+/**
+ * 媒体类型工具类
+ *
+ * @author pengles
+ */
+public class MimeTypeUtils {
+ public static final String IMAGE_PNG = "image/png";
+
+ public static final String IMAGE_JPG = "image/jpg";
+
+ public static final String IMAGE_JPEG = "image/jpeg";
+
+ public static final String IMAGE_BMP = "image/bmp";
+
+ public static final String IMAGE_GIF = "image/gif";
+
+ public static final String[] IMAGE_EXTENSION = {"bmp", "gif", "jpg", "jpeg", "png"};
+
+ public static final String[] FLASH_EXTENSION = {"swf", "flv"};
+
+ public static final String[] MEDIA_EXTENSION = {"swf", "flv", "mp3", "wav", "wma", "wmv", "mid", "avi", "mpg",
+ "asf", "rm", "rmvb"};
+
+ public static final String[] VIDEO_EXTENSION = {"mp4", "avi", "rmvb"};
+
+ public static final String[] DEFAULT_ALLOWED_EXTENSION = {
+ // 图片
+ "bmp", "gif", "jpg", "jpeg", "png",
+ // word excel powerpoint
+ "doc", "docx", "xls", "xlsx", "ppt", "pptx", "html", "htm", "txt",
+ // 压缩文件
+ "rar", "zip", "gz", "bz2",
+ // 视频格式
+ "mp4", "avi", "rmvb",
+ // pdf
+ "pdf"};
+
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/ip/AddressUtils.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/ip/AddressUtils.java
new file mode 100644
index 0000000..72027fe
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/ip/AddressUtils.java
@@ -0,0 +1,70 @@
+package org.dromara.common.core.utils.ip;
+
+import cn.hutool.http.HtmlUtil;
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.dromara.common.core.utils.NetUtils;
+import org.dromara.common.core.utils.StringUtils;
+
+/**
+ * 获取地址类
+ *
+ * @author pengles
+ */
+@Slf4j
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public class AddressUtils {
+
+ // 未知IP
+ public static final String UNKNOWN_IP = "XX XX";
+ // 内网地址
+ public static final String LOCAL_ADDRESS = "内网IP";
+ // 未知地址
+ public static final String UNKNOWN_ADDRESS = "未知";
+
+ public static String getRealAddressByIP(String ip) {
+ // 处理空串并过滤HTML标签
+ ip = HtmlUtil.cleanHtmlTag(StringUtils.blankToDefault(ip,""));
+ // 判断是否为IPv4
+ if (NetUtils.isIPv4(ip)) {
+ return resolverIPv4Region(ip);
+ }
+ // 判断是否为IPv6
+ if (NetUtils.isIPv6(ip)) {
+ return resolverIPv6Region(ip);
+ }
+ // 如果不是IPv4或IPv6,则返回未知IP
+ return UNKNOWN_IP;
+ }
+
+ /**
+ * 根据IPv4地址查询IP归属行政区域
+ * @param ip ipv4地址
+ * @return 归属行政区域
+ */
+ private static String resolverIPv4Region(String ip){
+ // 内网不查询
+ if (NetUtils.isInnerIP(ip)) {
+ return LOCAL_ADDRESS;
+ }
+ return RegionUtils.getCityInfo(ip);
+ }
+
+ /**
+ * 根据IPv6地址查询IP归属行政区域
+ * @param ip ipv6地址
+ * @return 归属行政区域
+ */
+ private static String resolverIPv6Region(String ip){
+ // 内网不查询
+ if (NetUtils.isInnerIPv6(ip)) {
+ return LOCAL_ADDRESS;
+ }
+ log.warn("ip2region不支持IPV6地址解析:{}", ip);
+ // 不支持IPv6,不再进行没有必要的IP地址信息的解析,直接返回
+ // 如有需要,可自行实现IPv6地址信息解析逻辑,并在这里返回
+ return UNKNOWN_ADDRESS;
+ }
+
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/ip/RegionUtils.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/ip/RegionUtils.java
new file mode 100644
index 0000000..c9e8678
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/ip/RegionUtils.java
@@ -0,0 +1,51 @@
+package org.dromara.common.core.utils.ip;
+
+import cn.hutool.core.io.resource.NoResourceException;
+import cn.hutool.core.io.resource.ResourceUtil;
+import lombok.extern.slf4j.Slf4j;
+import org.dromara.common.core.exception.ServiceException;
+import org.dromara.common.core.utils.StringUtils;
+import org.lionsoul.ip2region.xdb.Searcher;
+
+/**
+ * 根据ip地址定位工具类,离线方式
+ * 参考地址:集成 ip2region 实现离线IP地址定位库
+ *
+ * @author lishuyan
+ */
+@Slf4j
+public class RegionUtils {
+
+ // IP地址库文件名称
+ public static final String IP_XDB_FILENAME = "ip2region.xdb";
+
+ private static final Searcher SEARCHER;
+
+ static {
+ try {
+ // 1、将 ip2region 数据库文件 xdb 从 ClassPath 加载到内存。
+ // 2、基于加载到内存的 xdb 数据创建一个 Searcher 查询对象。
+ SEARCHER = Searcher.newWithBuffer(ResourceUtil.readBytes(IP_XDB_FILENAME));
+ log.info("RegionUtils初始化成功,加载IP地址库数据成功!");
+ } catch (NoResourceException e) {
+ throw new ServiceException("RegionUtils初始化失败,原因:IP地址库数据不存在!");
+ } catch (Exception e) {
+ throw new ServiceException("RegionUtils初始化失败,原因:" + e.getMessage());
+ }
+ }
+
+ /**
+ * 根据IP地址离线获取城市
+ */
+ public static String getCityInfo(String ip) {
+ try {
+ // 3、执行查询
+ String region = SEARCHER.search(StringUtils.trim(ip));
+ return region.replace("0|", "").replace("|0", "");
+ } catch (Exception e) {
+ log.error("IP地址离线获取城市异常 {}", ip);
+ return "未知";
+ }
+ }
+
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/reflect/ReflectUtils.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/reflect/ReflectUtils.java
new file mode 100644
index 0000000..fcfcef7
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/reflect/ReflectUtils.java
@@ -0,0 +1,56 @@
+package org.dromara.common.core.utils.reflect;
+
+import cn.hutool.core.util.ReflectUtil;
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+import org.dromara.common.core.utils.StringUtils;
+
+import java.lang.reflect.Method;
+
+/**
+ * 反射工具类. 提供调用getter/setter方法, 访问私有变量, 调用私有方法, 获取泛型类型Class, 被AOP过的真实类等工具函数.
+ *
+ * @author pengles
+ */
+@SuppressWarnings("rawtypes")
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public class ReflectUtils extends ReflectUtil {
+
+ private static final String SETTER_PREFIX = "set";
+
+ private static final String GETTER_PREFIX = "get";
+
+ /**
+ * 调用Getter方法.
+ * 支持多级,如:对象名.对象名.方法
+ */
+ @SuppressWarnings("unchecked")
+ public static E invokeGetter(Object obj, String propertyName) {
+ Object object = obj;
+ for (String name : StringUtils.split(propertyName, ".")) {
+ String getterMethodName = GETTER_PREFIX + StringUtils.capitalize(name);
+ object = invoke(object, getterMethodName);
+ }
+ return (E) object;
+ }
+
+ /**
+ * 调用Setter方法, 仅匹配方法名。
+ * 支持多级,如:对象名.对象名.方法
+ */
+ public static void invokeSetter(Object obj, String propertyName, E value) {
+ Object object = obj;
+ String[] names = StringUtils.split(propertyName, ".");
+ for (int i = 0; i < names.length; i++) {
+ if (i < names.length - 1) {
+ String getterMethodName = GETTER_PREFIX + StringUtils.capitalize(names[i]);
+ object = invoke(object, getterMethodName);
+ } else {
+ String setterMethodName = SETTER_PREFIX + StringUtils.capitalize(names[i]);
+ Method method = getMethodByName(object.getClass(), setterMethodName);
+ invoke(object, method, value);
+ }
+ }
+ }
+
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/regex/RegexUtils.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/regex/RegexUtils.java
new file mode 100644
index 0000000..6dde129
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/regex/RegexUtils.java
@@ -0,0 +1,31 @@
+package org.dromara.common.core.utils.regex;
+
+
+import cn.hutool.core.util.ReUtil;
+import org.dromara.common.core.constant.RegexConstants;
+
+/**
+ * 正则相关工具类
+ *
+ * @author Feng
+ */
+public final class RegexUtils extends ReUtil {
+
+ /**
+ * 从输入字符串中提取匹配的部分,如果没有匹配则返回默认值
+ *
+ * @param input 要提取的输入字符串
+ * @param regex 用于匹配的正则表达式,可以使用 {@link RegexConstants} 中定义的常量
+ * @param defaultInput 如果没有匹配时返回的默认值
+ * @return 如果找到匹配的部分,则返回匹配的部分,否则返回默认值
+ */
+ public static String extractFromString(String input, String regex, String defaultInput) {
+ try {
+ String str = ReUtil.get(regex, input, 1);
+ return str == null ? defaultInput : str;
+ } catch (Exception e) {
+ return defaultInput;
+ }
+ }
+
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/regex/RegexValidator.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/regex/RegexValidator.java
new file mode 100644
index 0000000..c0dda20
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/regex/RegexValidator.java
@@ -0,0 +1,105 @@
+package org.dromara.common.core.utils.regex;
+
+import cn.hutool.core.exceptions.ValidateException;
+import cn.hutool.core.lang.Validator;
+import org.dromara.common.core.factory.RegexPatternPoolFactory;
+
+import java.util.regex.Pattern;
+
+/**
+ * 正则字段校验器
+ * 主要验证字段非空、是否为满足指定格式等
+ *
+ * @author Feng
+ */
+public class RegexValidator extends Validator {
+
+ /**
+ * 字典类型必须以字母开头,且只能为(小写字母,数字,下滑线)
+ */
+ public static final Pattern DICTIONARY_TYPE = RegexPatternPoolFactory.DICTIONARY_TYPE;
+
+ /**
+ * 身份证号码(后6位)
+ */
+ public static final Pattern ID_CARD_LAST_6 = RegexPatternPoolFactory.ID_CARD_LAST_6;
+
+ /**
+ * QQ号码
+ */
+ public static final Pattern QQ_NUMBER = RegexPatternPoolFactory.QQ_NUMBER;
+
+ /**
+ * 邮政编码
+ */
+ public static final Pattern POSTAL_CODE = RegexPatternPoolFactory.POSTAL_CODE;
+
+ /**
+ * 注册账号
+ */
+ public static final Pattern ACCOUNT = RegexPatternPoolFactory.ACCOUNT;
+
+ /**
+ * 密码:包含至少8个字符,包括大写字母、小写字母、数字和特殊字符
+ */
+ public static final Pattern PASSWORD = RegexPatternPoolFactory.PASSWORD;
+
+ /**
+ * 通用状态(0表示正常,1表示停用)
+ */
+ public static final Pattern STATUS = RegexPatternPoolFactory.STATUS;
+
+
+ /**
+ * 检查输入的账号是否匹配预定义的规则
+ *
+ * @param value 要验证的账号
+ * @return 如果账号符合规则,返回 true;否则,返回 false。
+ */
+ public static boolean isAccount(CharSequence value) {
+ return isMatchRegex(ACCOUNT, value);
+ }
+
+ /**
+ * 验证输入的账号是否符合规则,如果不符合,则抛出 ValidateException 异常
+ *
+ * @param value 要验证的账号
+ * @param errorMsg 验证失败时抛出的异常消息
+ * @param CharSequence 的子类型
+ * @return 如果验证通过,返回输入的账号
+ * @throws ValidateException 如果验证失败
+ */
+ public static T validateAccount(T value, String errorMsg) throws ValidateException {
+ if (!isAccount(value)) {
+ throw new ValidateException(errorMsg);
+ }
+ return value;
+ }
+
+ /**
+ * 检查输入的状态是否匹配预定义的规则
+ *
+ * @param value 要验证的状态
+ * @return 如果状态符合规则,返回 true;否则,返回 false。
+ */
+ public static boolean isStatus(CharSequence value) {
+ return isMatchRegex(STATUS, value);
+ }
+
+ /**
+ * 验证输入的状态是否符合规则,如果不符合,则抛出 ValidateException 异常
+ *
+ * @param value 要验证的状态
+ * @param errorMsg 验证失败时抛出的异常消息
+ * @param CharSequence 的子类型
+ * @return 如果验证通过,返回输入的状态
+ * @throws ValidateException 如果验证失败
+ */
+ public static T validateStatus(T value, String errorMsg) throws ValidateException {
+ if (!isStatus(value)) {
+ throw new ValidateException(errorMsg);
+ }
+ return value;
+ }
+
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/sql/SqlUtil.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/sql/SqlUtil.java
new file mode 100644
index 0000000..5c83c6b
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/sql/SqlUtil.java
@@ -0,0 +1,56 @@
+package org.dromara.common.core.utils.sql;
+
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+import org.dromara.common.core.utils.StringUtils;
+
+/**
+ * sql操作工具类
+ *
+ * @author pengles
+ */
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public class SqlUtil {
+
+ /**
+ * 定义常用的 sql关键字
+ */
+ public static String SQL_REGEX = "\u000B|and |extractvalue|updatexml|sleep|exec |insert |select |delete |update |drop |count |chr |mid |master |truncate |char |declare |or |union |like |+|/*|user()";
+
+ /**
+ * 仅支持字母、数字、下划线、空格、逗号、小数点(支持多个字段排序)
+ */
+ public static final String SQL_PATTERN = "[a-zA-Z0-9_\\ \\,\\.]+";
+
+ /**
+ * 检查字符,防止注入绕过
+ */
+ public static String escapeOrderBySql(String value) {
+ if (StringUtils.isNotEmpty(value) && !isValidOrderBySql(value)) {
+ throw new IllegalArgumentException("参数不符合规范,不能进行查询");
+ }
+ return value;
+ }
+
+ /**
+ * 验证 order by 语法是否符合规范
+ */
+ public static boolean isValidOrderBySql(String value) {
+ return value.matches(SQL_PATTERN);
+ }
+
+ /**
+ * SQL关键字检查
+ */
+ public static void filterKeyword(String value) {
+ if (StringUtils.isEmpty(value)) {
+ return;
+ }
+ String[] sqlKeywords = StringUtils.split(SQL_REGEX, "\\|");
+ for (String sqlKeyword : sqlKeywords) {
+ if (StringUtils.indexOfIgnoreCase(value, sqlKeyword) > -1) {
+ throw new IllegalArgumentException("参数存在SQL注入风险");
+ }
+ }
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/validate/AddGroup.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/validate/AddGroup.java
new file mode 100644
index 0000000..4512276
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/validate/AddGroup.java
@@ -0,0 +1,9 @@
+package org.dromara.common.core.validate;
+
+/**
+ * 校验分组 add
+ *
+ * @author pengles
+ */
+public interface AddGroup {
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/validate/EditGroup.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/validate/EditGroup.java
new file mode 100644
index 0000000..365df79
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/validate/EditGroup.java
@@ -0,0 +1,9 @@
+package org.dromara.common.core.validate;
+
+/**
+ * 校验分组 edit
+ *
+ * @author pengles
+ */
+public interface EditGroup {
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/validate/QueryGroup.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/validate/QueryGroup.java
new file mode 100644
index 0000000..f1367b4
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/validate/QueryGroup.java
@@ -0,0 +1,9 @@
+package org.dromara.common.core.validate;
+
+/**
+ * 校验分组 query
+ *
+ * @author pengles
+ */
+public interface QueryGroup {
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/validate/dicts/DictPattern.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/validate/dicts/DictPattern.java
new file mode 100644
index 0000000..73fc4c4
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/validate/dicts/DictPattern.java
@@ -0,0 +1,40 @@
+package org.dromara.common.core.validate.dicts;
+
+import jakarta.validation.Constraint;
+import jakarta.validation.Payload;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * 字典项校验注解
+ *
+ * @author AprilWind
+ */
+@Constraint(validatedBy = DictPatternValidator.class)
+@Target({ElementType.FIELD, ElementType.PARAMETER})
+@Retention(RetentionPolicy.RUNTIME)
+public @interface DictPattern {
+
+ /**
+ * 字典类型,如 "sys_user_sex"
+ */
+ String dictType();
+
+ /**
+ * 分隔符
+ */
+ String separator();
+
+ /**
+ * 默认校验失败提示信息
+ */
+ String message() default "字典值无效";
+
+ Class>[] groups() default {};
+
+ Class extends Payload>[] payload() default {};
+
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/validate/dicts/DictPatternValidator.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/validate/dicts/DictPatternValidator.java
new file mode 100644
index 0000000..558a343
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/validate/dicts/DictPatternValidator.java
@@ -0,0 +1,55 @@
+package org.dromara.common.core.validate.dicts;
+
+import jakarta.validation.ConstraintValidator;
+import jakarta.validation.ConstraintValidatorContext;
+import org.dromara.common.core.service.DictService;
+import org.dromara.common.core.utils.SpringUtils;
+import org.dromara.common.core.utils.StringUtils;
+
+/**
+ * 自定义字典值校验器
+ *
+ * @author AprilWind
+ */
+public class DictPatternValidator implements ConstraintValidator {
+
+ /**
+ * 字典类型
+ */
+ private String dictType;
+
+ /**
+ * 分隔符
+ */
+ private String separator = ",";
+
+ /**
+ * 初始化校验器,提取注解上的字典类型
+ *
+ * @param annotation 注解实例
+ */
+ @Override
+ public void initialize(DictPattern annotation) {
+ this.dictType = annotation.dictType();
+ if (StringUtils.isNotBlank(annotation.separator())) {
+ this.separator = annotation.separator();
+ }
+ }
+
+ /**
+ * 校验字段值是否为指定字典类型中的合法值
+ *
+ * @param value 被校验的字段值
+ * @param context 校验上下文(可用于构建错误信息)
+ * @return true 表示校验通过(合法字典值),false 表示不通过
+ */
+ @Override
+ public boolean isValid(String value, ConstraintValidatorContext context) {
+ if (StringUtils.isBlank(dictType) || StringUtils.isBlank(value)) {
+ return false;
+ }
+ String dictLabel = SpringUtils.getBean(DictService.class).getDictLabel(dictType, value, separator);
+ return StringUtils.isNotBlank(dictLabel);
+ }
+
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/validate/enumd/EnumPattern.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/validate/enumd/EnumPattern.java
new file mode 100644
index 0000000..b1ae504
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/validate/enumd/EnumPattern.java
@@ -0,0 +1,51 @@
+package org.dromara.common.core.validate.enumd;
+
+import jakarta.validation.Constraint;
+import jakarta.validation.Payload;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.Repeatable;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+import static java.lang.annotation.ElementType.*;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+/**
+ * 自定义枚举校验
+ *
+ * @author 秋辞未寒
+ * @date 2024-12-09
+ */
+@Documented
+@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})
+@Retention(RUNTIME)
+@Repeatable(EnumPattern.List.class) // 允许在同一元素上多次使用该注解
+@Constraint(validatedBy = {EnumPatternValidator.class})
+public @interface EnumPattern {
+
+ /**
+ * 需要校验的枚举类型
+ */
+ Class extends Enum>> type();
+
+ /**
+ * 枚举类型校验值字段名称
+ * 需确保该字段实现了 getter 方法
+ */
+ String fieldName();
+
+ String message() default "输入值不在枚举范围内";
+
+ Class>[] groups() default {};
+
+ Class extends Payload>[] payload() default {};
+
+ @Documented
+ @Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})
+ @Retention(RUNTIME)
+ @interface List {
+ EnumPattern[] value();
+ }
+
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/validate/enumd/EnumPatternValidator.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/validate/enumd/EnumPatternValidator.java
new file mode 100644
index 0000000..e63f44a
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/validate/enumd/EnumPatternValidator.java
@@ -0,0 +1,37 @@
+package org.dromara.common.core.validate.enumd;
+
+import jakarta.validation.ConstraintValidator;
+import jakarta.validation.ConstraintValidatorContext;
+import org.dromara.common.core.utils.StringUtils;
+import org.dromara.common.core.utils.reflect.ReflectUtils;
+
+/**
+ * 自定义枚举校验注解实现
+ *
+ * @author 秋辞未寒
+ * @date 2024-12-09
+ */
+public class EnumPatternValidator implements ConstraintValidator {
+
+ private EnumPattern annotation;
+
+ @Override
+ public void initialize(EnumPattern annotation) {
+ ConstraintValidator.super.initialize(annotation);
+ this.annotation = annotation;
+ }
+
+ @Override
+ public boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext) {
+ if (StringUtils.isNotBlank(value)) {
+ String fieldName = annotation.fieldName();
+ for (Object e : annotation.type().getEnumConstants()) {
+ if (value.equals(ReflectUtils.invokeGetter(e, fieldName))) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/xss/Xss.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/xss/Xss.java
new file mode 100644
index 0000000..8f75ade
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/xss/Xss.java
@@ -0,0 +1,27 @@
+package org.dromara.common.core.xss;
+
+import jakarta.validation.Constraint;
+import jakarta.validation.Payload;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * 自定义xss校验注解
+ *
+ * @author pengles
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(value = {ElementType.METHOD, ElementType.FIELD, ElementType.CONSTRUCTOR, ElementType.PARAMETER})
+@Constraint(validatedBy = {XssValidator.class})
+public @interface Xss {
+
+ String message() default "不允许任何脚本运行";
+
+ Class>[] groups() default {};
+
+ Class extends Payload>[] payload() default {};
+
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/xss/XssValidator.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/xss/XssValidator.java
new file mode 100644
index 0000000..155759f
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/xss/XssValidator.java
@@ -0,0 +1,20 @@
+package org.dromara.common.core.xss;
+
+import cn.hutool.core.util.ReUtil;
+import cn.hutool.http.HtmlUtil;
+import jakarta.validation.ConstraintValidator;
+import jakarta.validation.ConstraintValidatorContext;
+
+/**
+ * 自定义xss校验注解实现
+ *
+ * @author pengles
+ */
+public class XssValidator implements ConstraintValidator {
+
+ @Override
+ public boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext) {
+ return !ReUtil.contains(HtmlUtil.RE_HTML_MARK, value);
+ }
+
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/ruoyi-common/ruoyi-common-core/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 0000000..43c7fcf
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -0,0 +1,5 @@
+org.dromara.common.core.config.ApplicationConfig
+org.dromara.common.core.config.AsyncConfig
+org.dromara.common.core.config.ThreadPoolConfig
+org.dromara.common.core.config.ValidatorConfig
+org.dromara.common.core.utils.SpringUtils
diff --git a/ruoyi-common/ruoyi-common-doc/pom.xml b/ruoyi-common/ruoyi-common-doc/pom.xml
new file mode 100644
index 0000000..bd50bf7
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-doc/pom.xml
@@ -0,0 +1,52 @@
+
+
+
+ org.dromara
+ ruoyi-common
+ ${revision}
+
+ 4.0.0
+
+ ruoyi-common-doc
+
+
+ ruoyi-common-doc 系统接口
+
+
+
+
+ org.dromara
+ ruoyi-common-core
+
+
+
+ org.springdoc
+ springdoc-openapi-starter-webmvc-api
+
+
+ org.springdoc
+ springdoc-openapi-starter-common
+
+
+
+
+
+ org.springdoc
+ springdoc-openapi-starter-common
+
+
+
+ com.github.therapi
+ therapi-runtime-javadoc
+
+
+
+ com.fasterxml.jackson.module
+ jackson-module-kotlin
+
+
+
+
+
diff --git a/ruoyi-common/ruoyi-common-doc/src/main/java/org/dromara/common/doc/config/SpringDocConfig.java b/ruoyi-common/ruoyi-common-doc/src/main/java/org/dromara/common/doc/config/SpringDocConfig.java
new file mode 100644
index 0000000..8dee85d
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-doc/src/main/java/org/dromara/common/doc/config/SpringDocConfig.java
@@ -0,0 +1,126 @@
+package org.dromara.common.doc.config;
+
+import io.swagger.v3.oas.models.OpenAPI;
+import io.swagger.v3.oas.models.Paths;
+import io.swagger.v3.oas.models.info.Info;
+import io.swagger.v3.oas.models.security.SecurityRequirement;
+import lombok.RequiredArgsConstructor;
+import org.dromara.common.core.utils.StringUtils;
+import org.dromara.common.doc.config.properties.SpringDocProperties;
+import org.dromara.common.doc.handler.OpenApiHandler;
+import org.springdoc.core.configuration.SpringDocConfiguration;
+import org.springdoc.core.customizers.OpenApiBuilderCustomizer;
+import org.springdoc.core.customizers.OpenApiCustomizer;
+import org.springdoc.core.customizers.ServerBaseUrlCustomizer;
+import org.springdoc.core.properties.SpringDocConfigProperties;
+import org.springdoc.core.providers.JavadocProvider;
+import org.springdoc.core.service.OpenAPIService;
+import org.springdoc.core.service.SecurityService;
+import org.springdoc.core.utils.PropertyResolverUtils;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.autoconfigure.web.ServerProperties;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+
+/**
+ * 接口文档配置
+ *
+ * @author pengles
+ */
+@RequiredArgsConstructor
+@AutoConfiguration(before = SpringDocConfiguration.class)
+@EnableConfigurationProperties(SpringDocProperties.class)
+@ConditionalOnProperty(name = "springdoc.api-docs.enabled", havingValue = "true", matchIfMissing = true)
+public class SpringDocConfig {
+
+ private final ServerProperties serverProperties;
+
+ @Bean
+ @ConditionalOnMissingBean(OpenAPI.class)
+ public OpenAPI openApi(SpringDocProperties properties) {
+ OpenAPI openApi = new OpenAPI();
+ // 文档基本信息
+ SpringDocProperties.InfoProperties infoProperties = properties.getInfo();
+ Info info = convertInfo(infoProperties);
+ openApi.info(info);
+ // 扩展文档信息
+ openApi.externalDocs(properties.getExternalDocs());
+ openApi.tags(properties.getTags());
+ openApi.paths(properties.getPaths());
+ openApi.components(properties.getComponents());
+ Set keySet = properties.getComponents().getSecuritySchemes().keySet();
+ List list = new ArrayList<>();
+ SecurityRequirement securityRequirement = new SecurityRequirement();
+ keySet.forEach(securityRequirement::addList);
+ list.add(securityRequirement);
+ openApi.security(list);
+
+ return openApi;
+ }
+
+ private Info convertInfo(SpringDocProperties.InfoProperties infoProperties) {
+ Info info = new Info();
+ info.setTitle(infoProperties.getTitle());
+ info.setDescription(infoProperties.getDescription());
+ info.setContact(infoProperties.getContact());
+ info.setLicense(infoProperties.getLicense());
+ info.setVersion(infoProperties.getVersion());
+ return info;
+ }
+
+ /**
+ * 自定义 openapi 处理器
+ */
+ @Bean
+ public OpenAPIService openApiBuilder(Optional openAPI,
+ SecurityService securityParser,
+ SpringDocConfigProperties springDocConfigProperties, PropertyResolverUtils propertyResolverUtils,
+ Optional> openApiBuilderCustomisers,
+ Optional> serverBaseUrlCustomisers, Optional javadocProvider) {
+ return new OpenApiHandler(openAPI, securityParser, springDocConfigProperties, propertyResolverUtils, openApiBuilderCustomisers, serverBaseUrlCustomisers, javadocProvider);
+ }
+
+ /**
+ * 对已经生成好的 OpenApi 进行自定义操作
+ */
+ @Bean
+ public OpenApiCustomizer openApiCustomizer() {
+ String contextPath = serverProperties.getServlet().getContextPath();
+ String finalContextPath;
+ if (StringUtils.isBlank(contextPath) || "/".equals(contextPath)) {
+ finalContextPath = "";
+ } else {
+ finalContextPath = contextPath;
+ }
+ // 对所有路径增加前置上下文路径
+ return openApi -> {
+ Paths oldPaths = openApi.getPaths();
+ if (oldPaths instanceof PlusPaths) {
+ return;
+ }
+ PlusPaths newPaths = new PlusPaths();
+ oldPaths.forEach((k, v) -> newPaths.addPathItem(finalContextPath + k, v));
+ openApi.setPaths(newPaths);
+ };
+ }
+
+ /**
+ * 单独使用一个类便于判断 解决springdoc路径拼接重复问题
+ *
+ * @author pengles
+ */
+ static class PlusPaths extends Paths {
+
+ public PlusPaths() {
+ super();
+ }
+ }
+
+}
diff --git a/ruoyi-common/ruoyi-common-doc/src/main/java/org/dromara/common/doc/config/properties/SpringDocProperties.java b/ruoyi-common/ruoyi-common-doc/src/main/java/org/dromara/common/doc/config/properties/SpringDocProperties.java
new file mode 100644
index 0000000..bc4089c
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-doc/src/main/java/org/dromara/common/doc/config/properties/SpringDocProperties.java
@@ -0,0 +1,94 @@
+package org.dromara.common.doc.config.properties;
+
+import io.swagger.v3.oas.models.Components;
+import io.swagger.v3.oas.models.ExternalDocumentation;
+import io.swagger.v3.oas.models.Paths;
+import io.swagger.v3.oas.models.info.Contact;
+import io.swagger.v3.oas.models.info.License;
+import io.swagger.v3.oas.models.tags.Tag;
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.boot.context.properties.NestedConfigurationProperty;
+
+import java.util.List;
+
+/**
+ * swagger 配置属性
+ *
+ * @author pengles
+ */
+@Data
+@ConfigurationProperties(prefix = "springdoc")
+public class SpringDocProperties {
+
+ /**
+ * 文档基本信息
+ */
+ @NestedConfigurationProperty
+ private InfoProperties info = new InfoProperties();
+
+ /**
+ * 扩展文档地址
+ */
+ @NestedConfigurationProperty
+ private ExternalDocumentation externalDocs;
+
+ /**
+ * 标签
+ */
+ private List tags = null;
+
+ /**
+ * 路径
+ */
+ @NestedConfigurationProperty
+ private Paths paths = null;
+
+ /**
+ * 组件
+ */
+ @NestedConfigurationProperty
+ private Components components = null;
+
+ /**
+ *
+ * 文档的基础属性信息
+ *
+ *
+ * @see io.swagger.v3.oas.models.info.Info
+ *
+ * 为了 springboot 自动生产配置提示信息,所以这里复制一个类出来
+ */
+ @Data
+ public static class InfoProperties {
+
+ /**
+ * 标题
+ */
+ private String title = null;
+
+ /**
+ * 描述
+ */
+ private String description = null;
+
+ /**
+ * 联系人信息
+ */
+ @NestedConfigurationProperty
+ private Contact contact = null;
+
+ /**
+ * 许可证
+ */
+ @NestedConfigurationProperty
+ private License license = null;
+
+ /**
+ * 版本
+ */
+ private String version = null;
+
+ }
+
+}
diff --git a/ruoyi-common/ruoyi-common-doc/src/main/java/org/dromara/common/doc/handler/OpenApiHandler.java b/ruoyi-common/ruoyi-common-doc/src/main/java/org/dromara/common/doc/handler/OpenApiHandler.java
new file mode 100644
index 0000000..56b7369
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-doc/src/main/java/org/dromara/common/doc/handler/OpenApiHandler.java
@@ -0,0 +1,253 @@
+package org.dromara.common.doc.handler;
+
+import cn.hutool.core.io.IoUtil;
+import io.swagger.v3.core.jackson.TypeNameResolver;
+import io.swagger.v3.core.util.AnnotationsUtils;
+import io.swagger.v3.oas.annotations.tags.Tags;
+import io.swagger.v3.oas.models.Components;
+import io.swagger.v3.oas.models.OpenAPI;
+import io.swagger.v3.oas.models.Operation;
+import io.swagger.v3.oas.models.Paths;
+import io.swagger.v3.oas.models.tags.Tag;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.dromara.common.core.utils.StreamUtils;
+import org.springdoc.core.customizers.OpenApiBuilderCustomizer;
+import org.springdoc.core.customizers.ServerBaseUrlCustomizer;
+import org.springdoc.core.properties.SpringDocConfigProperties;
+import org.springdoc.core.providers.JavadocProvider;
+import org.springdoc.core.service.OpenAPIService;
+import org.springdoc.core.service.SecurityService;
+import org.springdoc.core.utils.PropertyResolverUtils;
+import org.springframework.context.ApplicationContext;
+import org.springframework.core.annotation.AnnotatedElementUtils;
+import org.springframework.util.CollectionUtils;
+import org.springframework.web.method.HandlerMethod;
+
+import java.io.StringReader;
+import java.lang.reflect.Method;
+import java.util.*;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/**
+ * 自定义 openapi 处理器
+ * 对源码功能进行修改 增强使用
+ */
+@Slf4j
+@SuppressWarnings("all")
+public class OpenApiHandler extends OpenAPIService {
+
+ /**
+ * The Basic error controller.
+ */
+ private static Class> basicErrorController;
+
+ /**
+ * The Security parser.
+ */
+ private final SecurityService securityParser;
+
+ /**
+ * The Mappings map.
+ */
+ private final Map mappingsMap = new HashMap<>();
+
+ /**
+ * The Springdoc tags.
+ */
+ private final Map springdocTags = new HashMap<>();
+
+ /**
+ * The Open api builder customisers.
+ */
+ private final Optional