first commit

This commit is contained in:
dev
2026-02-22 12:12:02 +08:00
commit b236048a24
1467 changed files with 160403 additions and 0 deletions
Vendored
BIN
View File
Binary file not shown.
+57
View File
@@ -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/
+21
View File
@@ -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.
+687
View File
@@ -0,0 +1,687 @@
# PoJie
<div align="center">
[![License](https://img.shields.io/badge/License-MIT-blue.svg)](https://gitee.com/dromara/RuoYi-Vue-Plus/blob/5.X/LICENSE)
[![Spring Boot](https://img.shields.io/badge/Spring%20Boot-3.4.7-brightgreen.svg)](https://spring.io/projects/spring-boot)
[![JDK-17](https://img.shields.io/badge/JDK-17-green.svg)](https://openjdk.org/projects/jdk/17/)
[![JDK-21](https://img.shields.io/badge/JDK-21-green.svg)](https://openjdk.org/projects/jdk/21/)
[![MyBatis-Plus](https://img.shields.io/badge/MyBatis--Plus-3.5.12-blue.svg)](https://baomidou.com/)
[![Sa-Token](https://img.shields.io/badge/Sa--Token-1.44.0-orange.svg)](https://sa-token.cc/)
[![LangChain4j](https://img.shields.io/badge/LangChain4j-0.36.2-purple.svg)](https://github.com/langchain4j/langchain4j)
[![Spring AI](https://img.shields.io/badge/Spring%20AI-1.0.0--M5-green.svg)](https://spring.io/projects/spring-ai)
*集成多种AI框架,提供统一的AI服务管理平台*
[在线预览](#) · [使用文档](#) · [更新日志](#)
</div>
## 📖 平台简介
**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、七牛云等)
- 提供统一的文件上传和下载接口
- 支持文件预览和在线编辑
- 集成文件安全检查和病毒扫描
### 实时消息推送
- 基于SSEServer-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<UserVo> 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 文件。
---
<div align="center">
**如果这个项目对你有帮助,请给个 ⭐ Star 支持一下!**
Made with ❤️ by PoJie Team
</div>
+229
View File
@@ -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. 商城模块代码迁移(将现有代码迁移到新子模块结构,可选)
+719
View File
@@ -0,0 +1,719 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.dromara</groupId>
<artifactId>PoJie</artifactId>
<version>${revision}</version>
<name>PoJie</name>
<url>https://gitee.com/pengles/PoJie</url>
<description>PoJie管理系统</description>
<properties>
<revision>5.4.1</revision>
<spring-boot.version>3.4.7</spring-boot.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>17</java.version>
<mybatis.version>3.5.16</mybatis.version>
<springdoc.version>2.8.14</springdoc.version>
<therapi-javadoc.version>0.15.0</therapi-javadoc.version>
<fastexcel.version>1.3.0</fastexcel.version>
<velocity.version>2.3</velocity.version>
<satoken.version>1.44.0</satoken.version>
<mybatis-plus.version>3.5.12</mybatis-plus.version>
<p6spy.version>3.9.1</p6spy.version>
<hutool.version>5.8.40</hutool.version>
<spring-boot-admin.version>3.4.7</spring-boot-admin.version>
<redisson.version>3.52.0</redisson.version>
<lock4j.version>2.2.7</lock4j.version>
<dynamic-ds.version>4.3.1</dynamic-ds.version>
<snailjob.version>1.6.0</snailjob.version>
<mapstruct-plus.version>1.5.0</mapstruct-plus.version>
<mapstruct-plus.lombok.version>0.2.0</mapstruct-plus.lombok.version>
<lombok.version>1.18.36</lombok.version>
<bouncycastle.version>1.80</bouncycastle.version>
<justauth.version>1.16.7</justauth.version>
<!-- 离线IP地址定位库 -->
<ip2region.version>2.7.0</ip2region.version>
<!-- OSS 配置 -->
<aws.sdk.version>2.28.22</aws.sdk.version>
<!-- SMS 配置 -->
<sms4j.version>3.3.4</sms4j.version>
<!-- 限制框架中的fastjson版本 -->
<fastjson.version>1.2.83</fastjson.version>
<!-- 面向运行时的D-ORM依赖 -->
<anyline.version>8.7.2-20250603</anyline.version>
<!-- 工作流配置 -->
<warm-flow.version>1.7.4</warm-flow.version>
<!-- 支付宝SDK配置 -->
<alipay-sdk.version>4.34.86.ALL</alipay-sdk.version>
<!-- 微信相关配置 -->
<weixin-java-mp.version>4.7.0</weixin-java-mp.version>
<weixin-java-miniapp.version>4.7.0</weixin-java-miniapp.version>
<wechatpay-java.version>0.2.12</wechatpay-java.version>
<!-- Apache POI 配置 -->
<poi.version>5.2.4</poi.version>
<!-- iText PDF 配置 -->
<itext.version>8.0.2</itext.version>
<!-- AI 相关配置 -->
<spring-ai.pom.version>1.0.1</spring-ai.pom.version>
<langchain4j.pom.version>1.3.0-beta9</langchain4j.pom.version>
<okhttp.version>4.12.0</okhttp.version>
<!-- 插件版本 -->
<maven-jar-plugin.version>3.4.2</maven-jar-plugin.version>
<maven-war-plugin.version>3.4.0</maven-war-plugin.version>
<maven-compiler-plugin.version>3.14.0</maven-compiler-plugin.version>
<maven-surefire-plugin.version>3.5.3</maven-surefire-plugin.version>
<flatten-maven-plugin.version>1.3.0</flatten-maven-plugin.version>
<!-- 打包默认跳过测试 -->
<skipTests>true</skipTests>
</properties>
<profiles>
<profile>
<id>local</id>
<properties>
<!-- 环境标识,需要与配置文件的名称相对应 -->
<profiles.active>local</profiles.active>
<logging.level>info</logging.level>
<monitor.username>ruoyi</monitor.username>
<monitor.password>123456</monitor.password>
</properties>
</profile>
<profile>
<id>dev</id>
<properties>
<!-- 环境标识,需要与配置文件的名称相对应 -->
<profiles.active>dev</profiles.active>
<logging.level>info</logging.level>
<monitor.username>ruoyi</monitor.username>
<monitor.password>123456</monitor.password>
</properties>
<activation>
<!-- 默认环境 -->
<activeByDefault>true</activeByDefault>
</activation>
</profile>
<profile>
<id>prod</id>
<properties>
<profiles.active>prod</profiles.active>
<logging.level>warn</logging.level>
<monitor.username>ruoyi</monitor.username>
<monitor.password>123456</monitor.password>
</properties>
</profile>
</profiles>
<!-- 依赖声明 -->
<dependencyManagement>
<dependencies>
<!-- SpringBoot的依赖配置-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- hutool 的依赖配置-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-bom</artifactId>
<version>${hutool.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- Warm-Flow国产工作流引擎, 在线文档:http://warm-flow.cn/ -->
<dependency>
<groupId>org.dromara.warm</groupId>
<artifactId>warm-flow-mybatis-plus-sb3-starter</artifactId>
<version>${warm-flow.version}</version>
</dependency>
<dependency>
<groupId>org.dromara.warm</groupId>
<artifactId>warm-flow-plugin-ui-sb-web</artifactId>
<version>${warm-flow.version}</version>
</dependency>
<!-- JustAuth 的依赖配置-->
<dependency>
<groupId>me.zhyd.oauth</groupId>
<artifactId>JustAuth</artifactId>
<version>${justauth.version}</version>
</dependency>
<!-- common 的依赖配置-->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-bom</artifactId>
<version>${revision}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-api</artifactId>
<version>${springdoc.version}</version>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-common</artifactId>
<version>${springdoc.version}</version>
</dependency>
<dependency>
<groupId>com.github.therapi</groupId>
<artifactId>therapi-runtime-javadoc</artifactId>
<version>${therapi-javadoc.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</dependency>
<dependency>
<groupId>cn.idev.excel</groupId>
<artifactId>fastexcel</artifactId>
<version>${fastexcel.version}</version>
</dependency>
<!-- velocity代码生成使用模板 -->
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity-engine-core</artifactId>
<version>${velocity.version}</version>
</dependency>
<!-- Sa-Token 权限认证, 在线文档:http://sa-token.dev33.cn/ -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot3-starter</artifactId>
<version>${satoken.version}</version>
</dependency>
<!-- Sa-Token 整合 jwt -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-jwt</artifactId>
<version>${satoken.version}</version>
<exclusions>
<exclusion>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-core</artifactId>
<version>${satoken.version}</version>
</dependency>
<!-- dynamic-datasource 多数据源-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot3-starter</artifactId>
<version>${dynamic-ds.version}</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>${mybatis.version}</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-jsqlparser</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-annotation</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<!-- sql性能分析插件 -->
<dependency>
<groupId>p6spy</groupId>
<artifactId>p6spy</artifactId>
<version>${p6spy.version}</version>
</dependency>
<!-- AWS SDK for Java 2.x -->
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>s3</artifactId>
<version>${aws.sdk.version}</version>
</dependency>
<!-- 基于 AWS CRT 的 S3 客户端的性能增强的 S3 传输管理器 -->
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>s3-transfer-manager</artifactId>
<version>${aws.sdk.version}</version>
</dependency>
<!-- 将基于 Netty 的 HTTP 客户端从类路径中移除 -->
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>netty-nio-client</artifactId>
<version>${aws.sdk.version}</version>
</dependency>
<!--短信sms4j-->
<dependency>
<groupId>org.dromara.sms4j</groupId>
<artifactId>sms4j-spring-boot-starter</artifactId>
<version>${sms4j.version}</version>
</dependency>
<dependency>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-starter-server</artifactId>
<version>${spring-boot-admin.version}</version>
</dependency>
<dependency>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-starter-client</artifactId>
<version>${spring-boot-admin.version}</version>
</dependency>
<!--redisson-->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>${redisson.version}</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>lock4j-redisson-spring-boot-starter</artifactId>
<version>${lock4j.version}</version>
</dependency>
<!-- SnailJob Client -->
<dependency>
<groupId>com.aizuda</groupId>
<artifactId>snail-job-client-starter</artifactId>
<version>${snailjob.version}</version>
</dependency>
<dependency>
<groupId>com.aizuda</groupId>
<artifactId>snail-job-client-job-core</artifactId>
<version>${snailjob.version}</version>
</dependency>
<!-- 加密包引入 -->
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15to18</artifactId>
<version>${bouncycastle.version}</version>
</dependency>
<dependency>
<groupId>io.github.linpeilie</groupId>
<artifactId>mapstruct-plus-spring-boot-starter</artifactId>
<version>${mapstruct-plus.version}</version>
</dependency>
<!-- 离线IP地址定位库 ip2region -->
<dependency>
<groupId>org.lionsoul</groupId>
<artifactId>ip2region</artifactId>
<version>${ip2region.version}</version>
</dependency>
<!-- 微信小程序 SDK -->
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>weixin-java-miniapp</artifactId>
<version>${weixin-java-miniapp.version}</version>
</dependency>
<!-- 微信公众号 SDK -->
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>weixin-java-mp</artifactId>
<version>${weixin-java-mp.version}</version>
</dependency>
<!-- 微信支付 SDK -->
<dependency>
<groupId>com.github.wechatpay-apiv3</groupId>
<artifactId>wechatpay-java</artifactId>
<version>${wechatpay-java.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>${fastjson.version}</version>
</dependency>
<!-- 支付宝SDK -->
<dependency>
<groupId>com.alipay.sdk</groupId>
<artifactId>alipay-sdk-java</artifactId>
<version>${alipay-sdk.version}</version>
</dependency>
<!-- Apache POI for document processing -->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>${poi.version}</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>${poi.version}</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-scratchpad</artifactId>
<version>${poi.version}</version>
</dependency>
<!-- iText for PDF processing -->
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>kernel</artifactId>
<version>${itext.version}</version>
</dependency>
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>layout</artifactId>
<version>${itext.version}</version>
</dependency>
<!-- Spring AI BOM -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>${spring-ai.pom.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- Spring AI Dependencies -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-core</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-openai-spring-boot-starter</artifactId>
</dependency>
<!-- LangChain4j BOM -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-community-bom</artifactId>
<version>${langchain4j.pom.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- LangChain4j Dependencies -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j</artifactId>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-azure-open-ai</artifactId>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-zhipu-ai</artifactId>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-dashscope</artifactId>
</dependency>
<!-- HTTP Client Dependencies -->
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>${okhttp.version}</version>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp-sse</artifactId>
<version>${okhttp.version}</version>
</dependency>
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-ai</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-system</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-chat</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-job</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-generator</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-knowledge</artifactId>
<version>${revision}</version>
</dependency>
<!-- 工作流模块 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-workflow</artifactId>
<version>${revision}</version>
</dependency>
<!-- 微信公众号模块 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-mp</artifactId>
<version>${revision}</version>
</dependency>
<!-- 会员模块 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-member</artifactId>
<version>${revision}</version>
</dependency>
<!-- 支付模块 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-pay</artifactId>
<version>${revision}</version>
</dependency>
<!-- 商城模块 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-mall</artifactId>
<version>${revision}</version>
</dependency>
</dependencies>
</dependencyManagement>
<modules>
<module>ruoyi-common</module>
<module>ruoyi-extend</module>
<module>ruoyi-modules</module>
<module>ruoyi-server</module>
</modules>
<packaging>pom</packaging>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>${maven-compiler-plugin.version}</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
<encoding>${project.build.sourceEncoding}</encoding>
<annotationProcessorPaths>
<path>
<groupId>com.github.therapi</groupId>
<artifactId>therapi-runtime-javadoc-scribe</artifactId>
<version>${therapi-javadoc.version}</version>
</path>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
<path>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<version>${spring-boot.version}</version>
</path>
<path>
<groupId>io.github.linpeilie</groupId>
<artifactId>mapstruct-plus-processor</artifactId>
<version>${mapstruct-plus.version}</version>
</path>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok-mapstruct-binding</artifactId>
<version>${mapstruct-plus.lombok.version}</version>
</path>
</annotationProcessorPaths>
<compilerArgs>
<arg>-parameters</arg>
</compilerArgs>
</configuration>
</plugin>
<!-- 单元测试使用 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>${maven-surefire-plugin.version}</version>
<configuration>
<argLine>-Dfile.encoding=UTF-8</argLine>
<!-- 根据打包环境执行对应的@Tag测试方法 -->
<groups>${profiles.active}</groups>
<!-- 排除标签 -->
<excludedGroups>exclude</excludedGroups>
</configuration>
</plugin>
<!-- 统一版本号管理 -->
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>flatten-maven-plugin</artifactId>
<version>${flatten-maven-plugin.version}</version>
<configuration>
<updatePomFile>true</updatePomFile>
<flattenMode>resolveCiFriendliesOnly</flattenMode>
</configuration>
<executions>
<execution>
<id>flatten</id>
<phase>process-resources</phase>
<goals>
<goal>flatten</goal>
</goals>
</execution>
<execution>
<id>flatten.clean</id>
<phase>clean</phase>
<goals>
<goal>clean</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
<resources>
<resource>
<directory>src/main/resources</directory>
<!-- 关闭过滤 -->
<filtering>false</filtering>
</resource>
<resource>
<directory>src/main/resources</directory>
<!-- 引入所有 匹配文件进行过滤 -->
<includes>
<include>application*</include>
<include>bootstrap*</include>
<include>banner*</include>
</includes>
<!-- 启用过滤 即该资源中的变量将会被过滤器中的值替换 -->
<filtering>true</filtering>
</resource>
</resources>
</build>
<repositories>
<!-- <repository>-->
<!-- <id>aliyun-spring</id>-->
<!-- <name>Aliyun Spring Repository</name>-->
<!-- <url>https://maven.aliyun.com/repository/spring</url>-->
<!-- <releases>-->
<!-- <enabled>true</enabled>-->
<!-- </releases>-->
<!-- <snapshots>-->
<!-- <enabled>true</enabled>-->
<!-- </snapshots>-->
<!-- </repository>-->
<repository>
<id>central</id>
<name>Maven Central</name>
<url>https://repo1.maven.org/maven2</url>
<releases>
<enabled>true</enabled>
</releases>
</repository>
<repository>
<id>public</id>
<name>huawei nexus</name>
<url>https://mirrors.huaweicloud.com/repository/maven/</url>
<releases>
<enabled>true</enabled>
</releases>
</repository>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
<repository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<releases>
<enabled>false</enabled>
</releases>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>public</id>
<name>huawei nexus</name>
<url>https://mirrors.huaweicloud.com/repository/maven/</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>false</enabled>
</snapshots>
</pluginRepository>
</pluginRepositories>
</project>
BIN
View File
Binary file not shown.
+45
View File
@@ -0,0 +1,45 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>org.dromara</groupId>
<artifactId>PoJie</artifactId>
<version>${revision}</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<modules>
<module>ruoyi-common-bom</module>
<module>ruoyi-common-social</module>
<module>ruoyi-common-core</module>
<module>ruoyi-common-doc</module>
<module>ruoyi-common-excel</module>
<module>ruoyi-common-idempotent</module>
<module>ruoyi-common-job</module>
<module>ruoyi-common-log</module>
<module>ruoyi-common-mail</module>
<module>ruoyi-common-mybatis</module>
<module>ruoyi-common-oss</module>
<module>ruoyi-common-ratelimiter</module>
<module>ruoyi-common-redis</module>
<module>ruoyi-common-satoken</module>
<module>ruoyi-common-security</module>
<module>ruoyi-common-sms</module>
<module>ruoyi-common-web</module>
<module>ruoyi-common-translation</module>
<module>ruoyi-common-json</module>
<module>ruoyi-common-encrypt</module>
<module>ruoyi-common-sensitive</module>
<module>ruoyi-common-websocket</module>
<module>ruoyi-common-sse</module>
</modules>
<artifactId>ruoyi-common</artifactId>
<packaging>pom</packaging>
<description>
common 通用模块
</description>
</project>
+178
View File
@@ -0,0 +1,178 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-bom</artifactId>
<version>${revision}</version>
<packaging>pom</packaging>
<description>
ruoyi-common-bom common依赖项
</description>
<properties>
<revision>5.4.1</revision>
</properties>
<dependencyManagement>
<dependencies>
<!-- 核心模块 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-core</artifactId>
<version>${revision}</version>
</dependency>
<!-- 接口模块 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-doc</artifactId>
<version>${revision}</version>
</dependency>
<!-- excel -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-excel</artifactId>
<version>${revision}</version>
</dependency>
<!-- 幂等 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-idempotent</artifactId>
<version>${revision}</version>
</dependency>
<!-- 调度模块 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-job</artifactId>
<version>${revision}</version>
</dependency>
<!-- 日志记录 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-log</artifactId>
<version>${revision}</version>
</dependency>
<!-- 邮件服务 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-mail</artifactId>
<version>${revision}</version>
</dependency>
<!-- 数据库服务 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-mybatis</artifactId>
<version>${revision}</version>
</dependency>
<!-- OSS -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-oss</artifactId>
<version>${revision}</version>
</dependency>
<!-- 限流 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-ratelimiter</artifactId>
<version>${revision}</version>
</dependency>
<!-- 缓存服务 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-redis</artifactId>
<version>${revision}</version>
</dependency>
<!-- satoken -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-satoken</artifactId>
<version>${revision}</version>
</dependency>
<!-- 安全模块 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-security</artifactId>
<version>${revision}</version>
</dependency>
<!-- 短信模块 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-sms</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-social</artifactId>
<version>${revision}</version>
</dependency>
<!-- web服务 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-web</artifactId>
<version>${revision}</version>
</dependency>
<!-- 翻译模块 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-translation</artifactId>
<version>${revision}</version>
</dependency>
<!-- 脱敏模块 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-sensitive</artifactId>
<version>${revision}</version>
</dependency>
<!-- 序列化模块 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-json</artifactId>
<version>${revision}</version>
</dependency>
<!-- 数据库加解密模块 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-encrypt</artifactId>
<version>${revision}</version>
</dependency>
<!-- WebSocket模块 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-websocket</artifactId>
<version>${revision}</version>
</dependency>
<!-- SSE模块 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-sse</artifactId>
<version>${revision}</version>
</dependency>
</dependencies>
</dependencyManagement>
</project>
+111
View File
@@ -0,0 +1,111 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common</artifactId>
<version>${revision}</version>
</parent>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>16</source>
<target>16</target>
</configuration>
</plugin>
</plugins>
</build>
<modelVersion>4.0.0</modelVersion>
<artifactId>ruoyi-common-core</artifactId>
<description>
ruoyi-common-core 核心模块
</description>
<dependencies>
<!-- Spring框架基本的核心工具 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
</dependency>
<!-- SpringWeb模块 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
</dependency>
<!-- 自定义验证注解 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!--常用工具类 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<!-- servlet包 -->
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-core</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-http</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-extra</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!-- 自动生成YML配置关联JSON文件 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-properties-migrator</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.github.linpeilie</groupId>
<artifactId>mapstruct-plus-spring-boot-starter</artifactId>
</dependency>
<!-- 离线IP地址定位库 -->
<dependency>
<groupId>org.lionsoul</groupId>
<artifactId>ip2region</artifactId>
</dependency>
</dependencies>
</project>
@@ -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 {
}
@@ -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;
/**
* 异步配置
* <p>
* 如果未使用虚拟线程则生效
*
* @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());
};
}
}
@@ -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);
}
}
}
@@ -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();
}
}
}
@@ -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;
}
@@ -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:";
}
@@ -0,0 +1,83 @@
package org.dromara.common.core.constant;
/**
* 缓存组名称常量
* <p>
* key 格式为 cacheNames#ttl#maxIdleTime#maxSize
* <p>
* ttl 过期时间 如果设置为0则不过期 默认为0
* maxIdleTime 最大空闲时间 根据LRU算法清理空闲数据 如果设置为0则不检测 默认为0
* maxSize 组最大长度 根据LRU算法清理溢出数据 如果设置为0则无限长 默认为0
* <p>
* 例子: 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";
}
@@ -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;
}
@@ -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:";
}
@@ -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;
}
@@ -0,0 +1,59 @@
package org.dromara.common.core.constant;
import cn.hutool.core.lang.RegexPool;
/**
* 常用正则表达式字符串
* <p>
* 常用正则表达式集合,更多正则见: 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]$";
}
@@ -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;
}
@@ -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;
}
@@ -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<T> 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 <T> R<T> ok() {
return restResult(null, SUCCESS, "操作成功");
}
public static <T> R<T> ok(T data) {
return restResult(data, SUCCESS, "操作成功");
}
public static <T> R<T> ok(Boolean success) {
return restResult(null, success ? SUCCESS : FAIL, success ? "操作成功" : "操作失败");
}
public static <T> R<T> ok(Boolean success, String msg) {
return restResult(null, success ? SUCCESS : FAIL, msg);
}
public static <T> R<T> ok(Boolean success, T data) {
return restResult(data, success ? SUCCESS : FAIL, success ? "操作成功" : "操作失败");
}
public static <T> R<T> ok(String msg) {
return restResult(null, SUCCESS, msg);
}
public static <T> R<T> ok(String msg, T data) {
return restResult(data, SUCCESS, msg);
}
public static <T> R<T> fail() {
return restResult(null, FAIL, "操作失败");
}
public static <T> R<T> fail(String msg) {
return restResult(null, FAIL, msg);
}
public static <T> R<T> fail(T data) {
return restResult(data, FAIL, "操作失败");
}
public static <T> R<T> fail(String msg, T data) {
return restResult(data, FAIL, msg);
}
public static <T> R<T> fail(int code, String msg) {
return restResult(null, code, msg);
}
/**
* 返回警告消息
*
* @param msg 返回内容
* @return 警告消息
*/
public static <T> R<T> warn(String msg) {
return restResult(null, HttpStatus.WARN, msg);
}
/**
* 返回警告消息
*
* @param msg 返回内容
* @param data 数据对象
* @return 警告消息
*/
public static <T> R<T> warn(String msg, T data) {
return restResult(data, HttpStatus.WARN, msg);
}
private static <T> R<T> restResult(T data, int code, String msg) {
R<T> r = new R<>();
r.setCode(code);
r.setData(data);
r.setMsg(msg);
return r;
}
public static <T> Boolean isError(R<T> ret) {
return !isSuccess(ret);
}
public static <T> Boolean isSuccess(R<T> ret) {
return R.SUCCESS == ret.getCode();
}
}
@@ -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<FlowCopyDTO> flowCopyList;
/**
* 消息类型
*/
private List<String> messageType;
/**
* 办理意见
*/
private String message;
/**
* 消息通知
*/
private String notice;
/**
* 流程变量
*/
private Map<String, Object> variables;
/**
* 扩展变量(此处为逗号分隔的ossId)
*/
private String ext;
public Map<String, Object> getVariables() {
if (variables == null) {
return new HashMap<>(16);
}
variables.entrySet().removeIf(entry -> Objects.isNull(entry.getValue()));
return variables;
}
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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<String, Object> variables;
public Map<String, Object> getVariables() {
if (variables == null) {
return new HashMap<>(16);
}
variables.entrySet().removeIf(entry -> Objects.isNull(entry.getValue()));
return variables;
}
}
@@ -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;
}
@@ -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<TaskHandler> list;
public TaskAssigneeDTO(Long total, List<TaskHandler> list) {
this.total = total;
this.list = list;
}
/**
* 将源列表转换为 TaskHandler 列表
*
* @param <T> 通用类型
* @param sourceList 待转换的源列表
* @param storageId 提取 storageId 的函数
* @param handlerCode 提取 handlerCode 的函数
* @param handlerName 提取 handlerName 的函数
* @param groupName 提取 groupName 的函数
* @param createTimeMapper 提取 createTime 的函数
* @return 转换后的 TaskHandler 列表
*/
public static <T> List<TaskHandler> convertToHandlerList(
List<T> sourceList,
Function<T, Long> storageId,
Function<T, String> handlerCode,
Function<T, String> handlerName,
Function<T, Long> groupName,
Function<T, Date> 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;
}
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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<String, Object> params;
/**
* 当为true时为申请人节点办理
*/
private Boolean submit;
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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<String> menuPermission;
/**
* 角色权限
*/
private Set<String> rolePermission;
/**
* 用户名
*/
private String username;
/**
* 用户昵称
*/
private String nickname;
/**
* 角色对象
*/
private List<RoleDTO> roles;
/**
* 岗位对象
*/
private List<PostDTO> 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;
}
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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<String, BusinessStatusEnum> 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<String> runningStatus() {
return Arrays.asList(DRAFT.status, WAITING.status, BACK.status, CANCEL.status);
}
/**
* 获取结束实例的状态列表
*
* @return 包含结束实例状态的不可变列表
* (包含 FINISH、INVALID 和 TERMINATION 状态)
*/
public static List<String> 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("流程状态为空!");
}
}
}
@@ -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;
}
@@ -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);
}
}
@@ -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;
}
@@ -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;
}
@@ -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);
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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);
}
}
@@ -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});
}
}
@@ -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});
}
}
@@ -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");
}
}
@@ -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");
}
}
@@ -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);
}
}
@@ -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;
/**
* 正则表达式模式池工厂
* <p>初始化的时候将正则表达式加入缓存池当中</p>
* <p>提高正则表达式的性能,避免重复编译相同的正则表达式</p>
*
* @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);
}
@@ -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);
}
}
@@ -0,0 +1,18 @@
package org.dromara.common.core.service;
/**
* 通用 参数配置服务
*
* @author pengles
*/
public interface ConfigService {
/**
* 根据参数 key 获取参数值
*
* @param configKey 参数 key
* @return 参数值
*/
String getConfigValue(String configKey);
}
@@ -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<DeptDTO> selectDeptsByList();
}
@@ -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为keydictLabel为值组成的Map
*/
Map<String, String> getAllDictByDictType(String dictType);
/**
* 根据字典类型查询详细信息
*
* @param dictType 字典类型
* @return 字典类型详细信息
*/
DictTypeDTO getDictType(String dictType);
/**
* 根据字典类型查询字典数据列表
*
* @param dictType 字典类型
* @return 字典数据列表
*/
List<DictDataDTO> getDictData(String dictType);
}
@@ -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<OssDTO> selectByIds(String ossIds);
}
@@ -0,0 +1,28 @@
package org.dromara.common.core.service;
import java.util.Set;
/**
* 用户权限处理
*
* @author pengles
*/
public interface PermissionService {
/**
* 获取角色数据权限
*
* @param userId 用户id
* @return 角色权限信息
*/
Set<String> getRolePermission(Long userId);
/**
* 获取菜单数据权限
*
* @param userId 用户id
* @return 菜单权限信息
*/
Set<String> getMenuPermission(Long userId);
}
@@ -0,0 +1,10 @@
package org.dromara.common.core.service;
/**
* 通用 岗位服务
*
* @author AprilWind
*/
public interface PostService {
}
@@ -0,0 +1,10 @@
package org.dromara.common.core.service;
/**
* 通用 角色服务
*
* @author AprilWind
*/
public interface RoleService {
}
@@ -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);
}
@@ -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<UserDTO> selectListByIds(List<Long> userIds);
/**
* 通过角色ID查询用户ID
*
* @param roleIds 角色ids
* @return 用户ids
*/
List<Long> selectUserIdsByRoleIds(List<Long> roleIds);
/**
* 通过角色ID查询用户
*
* @param roleIds 角色ids
* @return 用户
*/
List<UserDTO> selectUsersByRoleIds(List<Long> roleIds);
/**
* 通过部门ID查询用户
*
* @param deptIds 部门ids
* @return 用户
*/
List<UserDTO> selectUsersByDeptIds(List<Long> deptIds);
/**
* 通过岗位ID查询用户
*
* @param postIds 岗位ids
* @return 用户
*/
List<UserDTO> selectUsersByPostIds(List<Long> postIds);
/**
* 根据用户 ID 列表查询用户名称映射关系
*
* @param userIds 用户 ID 列表
* @return Map其中 key 为用户 IDvalue 为对应的用户名称
*/
Map<Long, String> selectUserNamesByIds(List<Long> userIds);
/**
* 根据角色 ID 列表查询角色名称映射关系
*
* @param roleIds 角色 ID 列表
* @return Map其中 key 为角色 IDvalue 为对应的角色名称
*/
Map<Long, String> selectRoleNamesByIds(List<Long> roleIds);
/**
* 根据部门 ID 列表查询部门名称映射关系
*
* @param deptIds 部门 ID 列表
* @return Map其中 key 为部门 IDvalue 为对应的部门名称
*/
Map<Long, String> selectDeptNamesByIds(List<Long> deptIds);
/**
* 根据岗位 ID 列表查询岗位名称映射关系
*
* @param postIds 岗位 ID 列表
* @return Map其中 key 为岗位 IDvalue 为对应的岗位名称
*/
Map<Long, String> selectPostNamesByIds(List<Long> postIds);
}
@@ -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<Long> 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<String, Object> variable);
/**
* 获取流程变量
*
* @param instanceId 流程实例id
*/
Map<String, Object> 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);
}
@@ -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 所需返回的时间单位DAYSHOURSMINUTESSECONDSMILLISECONDSMICROSECONDSNANOSECONDS
* @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());
}
}
}
@@ -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 工具类
* <p>参考文档<a href="https://mapstruct.plus/introduction/quick-start.html">mapstruct-plus</a></p>
*
*
* @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 <T, V> V convert(T source, Class<V> 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 <T, V> 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 <T, V> List<V> convert(List<T> sourceList, Class<V> 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> T convert(Map<String, Object> map, Class<T> beanClass) {
if (MapUtil.isEmpty(map)) {
return null;
}
if (ObjectUtil.isNull(beanClass)) {
return null;
}
return CONVERTER.convert(map, beanClass);
}
}
@@ -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;
}
}
}
@@ -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地址是否为内网地址
* <br><br>
* 以下地址将归类为本地地址如有业务场景有需要请根据需求自行处理
* <pre>
* 通配符地址 0:0:0:0:0:0:0:0
* 链路本地地址 fe80::/10
* 唯一本地地址 fec0::/10
* 环回地址 ::1
* </pre>
*
* @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);
}
}
@@ -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 <T, E> E notNullGetter(T obj, Function<T, E> func) {
if (isNotNull(obj) && isNotNull(func)) {
return func.apply(obj);
}
return null;
}
/**
* 如果对象不为空则获取对象中的某个字段否则返回默认值
*
* @param obj 对象
* @param func 获取方法
* @param defaultValue 默认值
* @return 对象字段
*/
public static <T, E> E notNullGetter(T obj, Function<T, E> func, E defaultValue) {
if (isNotNull(obj) && isNotNull(func)) {
return func.apply(obj);
}
return defaultValue;
}
/**
* 如果值不为空则返回值否则返回默认值
*
* @param obj 对象
* @param defaultValue 默认值
* @return 对象字段
*/
public static <T> T notNull(T obj, T defaultValue) {
if (isNotNull(obj)) {
return obj;
}
return defaultValue;
}
}
@@ -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<String, String[]> getParams(ServletRequest request) {
final Map<String, String[]> map = request.getParameterMap();
return Collections.unmodifiableMap(map);
}
/**
* 获取所有请求参数 Map 的形式返回值为字符串形式的拼接
*
* @param request 请求对象{@link ServletRequest}
* @return 请求参数的 Map键为参数名值为拼接后的字符串
*/
public static Map<String, String> getParamMap(ServletRequest request) {
Map<String, String> params = new HashMap<>();
for (Map.Entry<String, String[]> 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 对象
* <p>
* 如果当前请求已经关联了一个会话即已经存在有效的 session ID
* 则返回该会话对象如果没有关联会话则会创建一个新的会话对象并返回
* <p>
* 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<String, String> getHeaders(HttpServletRequest request) {
Map<String, String> map = new LinkedCaseInsensitiveMap<>();
Enumeration<String> 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);
}
}
@@ -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> 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));
}
}
@@ -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 <E> List<E> filter(Collection<E> collection, Predicate<E> 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> E findFirst(Collection<E> collection, Predicate<E> function) {
if (CollUtil.isEmpty(collection)) {
return null;
}
return collection.stream().filter(function).findFirst().orElse(null);
}
/**
* 找到流中任意一个满足条件的元素
*
* @param collection 需要查询的集合
* @param function 过滤方法
* @return 找到符合条件的任意一个元素没有则返回null
*/
public static <E> Optional<E> findAny(Collection<E> collection, Predicate<E> function) {
if (CollUtil.isEmpty(collection)) {
return Optional.empty();
}
return collection.stream().filter(function).findAny();
}
/**
* 将collection拼接
*
* @param collection 需要转化的集合
* @param function 拼接方法
* @return 拼接后的list
*/
public static <E> String join(Collection<E> collection, Function<E, String> function) {
return join(collection, function, StringUtils.SEPARATOR);
}
/**
* 将collection拼接
*
* @param collection 需要转化的集合
* @param function 拼接方法
* @param delimiter 拼接符
* @return 拼接后的list
*/
public static <E> String join(Collection<E> collection, Function<E, String> 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 <E> List<E> sorted(Collection<E> collection, Comparator<E> comparing) {
if (CollUtil.isEmpty(collection)) {
return CollUtil.newArrayList();
}
// 注意此处不要使用 .toList() 新语法 因为返回的是不可变List 会导致序列化问题
return collection.stream().filter(Objects::nonNull).sorted(comparing).collect(Collectors.toList());
}
/**
* 将collection转化为类型不变的map<br>
* <B>{@code Collection<V> ----> Map<K,V>}</B>
*
* @param collection 需要转化的集合
* @param key V类型转化为K类型的lambda方法
* @param <V> collection中的泛型
* @param <K> map中的key类型
* @return 转化后的map
*/
public static <V, K> Map<K, V> toIdentityMap(Collection<V> collection, Function<V, K> 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的泛型不同)<br>
* <B>{@code Collection<E> -----> Map<K,V> }</B>
*
* @param collection 需要转化的集合
* @param key E类型转化为K类型的lambda方法
* @param value E类型转化为V类型的lambda方法
* @param <E> collection中的泛型
* @param <K> map中的key类型
* @param <V> map中的value类型
* @return 转化后的map
*/
public static <E, K, V> Map<K, V> toMap(Collection<E> collection, Function<E, K> key, Function<E, V> 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<br>
* <B>{@code Collection<E> -------> Map<K,List<E>> } </B>
*
* @param collection 需要分类的集合
* @param key 分类的规则
* @param <E> collection中的泛型
* @param <K> map中的key类型
* @return 分类后的map
*/
public static <E, K> Map<K, List<E>> groupByKey(Collection<E> collection, Function<E, K> 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<br>
* <B>{@code Collection<E> ---> Map<T,Map<U,List<E>>> } </B>
*
* @param collection 需要分类的集合
* @param key1 第一个分类的规则
* @param key2 第二个分类的规则
* @param <E> 集合元素类型
* @param <K> 第一个map中的key类型
* @param <U> 第二个map中的key类型
* @return 分类后的map
*/
public static <E, K, U> Map<K, Map<U, List<E>>> groupBy2Key(Collection<E> collection, Function<E, K> key1, Function<E, U> 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<br>
* <B>{@code Collection<E> ---> Map<T,Map<U,E>> } </B>
*
* @param collection 需要分类的集合
* @param key1 第一个分类的规则
* @param key2 第二个分类的规则
* @param <T> 第一个map中的key类型
* @param <U> 第二个map中的key类型
* @param <E> collection中的泛型
* @return 分类后的map
*/
public static <E, T, U> Map<T, Map<U, E>> group2Map(Collection<E> collection, Function<E, T> key1, Function<E, U> 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集合但是两者的泛型不同<br>
* <B>{@code Collection<E> ------> List<T> } </B>
*
* @param collection 需要转化的集合
* @param function collection中的泛型转化为list泛型的lambda表达式
* @param <E> collection中的泛型
* @param <T> List中的泛型
* @return 转化后的list
*/
public static <E, T> List<T> toList(Collection<E> collection, Function<E, T> function) {
if (CollUtil.isEmpty(collection)) {
return CollUtil.newArrayList();
}
return collection
.stream()
.map(function)
.filter(Objects::nonNull)
// 注意此处不要使用 .toList() 新语法 因为返回的是不可变List 会导致序列化问题
.collect(Collectors.toList());
}
/**
* 将collection转化为Set集合但是两者的泛型不同<br>
* <B>{@code Collection<E> ------> Set<T> } </B>
*
* @param collection 需要转化的集合
* @param function collection中的泛型转化为set泛型的lambda表达式
* @param <E> collection中的泛型
* @param <T> Set中的泛型
* @return 转化后的Set
*/
public static <E, T> Set<T> toSet(Collection<E> collection, Function<E, T> 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 <K> map中的key类型
* @param <X> 第一个 map的value类型
* @param <Y> 第二个 map的value类型
* @param <V> 最终map的value类型
* @return 合并后的map
*/
public static <K, X, Y, V> Map<K, V> merge(Map<K, X> map1, Map<K, Y> map2, BiFunction<X, Y, V> 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<K> key = new HashSet<>();
key.addAll(map1.keySet());
key.addAll(map2.keySet());
Map<K, V> 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;
}
}
@@ -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);
}
/**
* 格式化文本, {} 表示占位符<br>
* 此方法只是简单将占位符 {} 按照顺序替换为参数<br>
* 如果想输出 {} 使用 \\转义 { 即可如果想输出 {} 之前的 \ 使用双转义符 \\\\ 即可<br>
* <br>
* 通常使用format("this is {} for {}", "a", "b") -> this is a for b<br>
* 转义{} format("this is \\{} for {}", "a", "b") -> this is {} for a<br>
* 转义\ format("this is \\\\{} for {}", "a", "b") -> this is \a for b<br>
*
* @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<String> 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<String> str2List(String str, String sep, boolean filterBlank, boolean trim) {
List<String> 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<String> 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<String> splitList(String str) {
return splitTo(str, Convert::toStr);
}
/**
* 切分字符串
*
* @param str 被切分的字符串
* @param separator 分隔符
* @return 分割后的数据列表
*/
public static List<String> splitList(String str, String separator) {
return splitTo(str, separator, Convert::toStr);
}
/**
* 切分字符串自定义转换(分隔符默认逗号)
*
* @param str 被切分的字符串
* @param mapper 自定义转换
* @return 分割后的数据列表
*/
public static <T> List<T> splitTo(String str, Function<? super Object, T> mapper) {
return splitTo(str, SEPARATOR, mapper);
}
/**
* 切分字符串自定义转换
*
* @param str 被切分的字符串
* @param separator 分隔符
* @param mapper 自定义转换
* @return 分割后的数据列表
*/
public static <T> List<T> 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;
}
}
}
@@ -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);
}
}
}
@@ -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 <T> 输入节点的类型
* @param <K> 节点ID的类型
* @param list 节点列表其中包含了要构建树形结构的所有节点
* @param nodeParser 解析器用于将输入节点转换为树节点
* @return 构建好的树形结构列表
*/
public static <T, K> List<Tree<K>> build(List<T> list, NodeParser<T, K> 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 <T> 输入节点的类型
* @param <K> 节点ID的类型
* @param parentId 顶级节点
* @param list 节点列表其中包含了要构建树形结构的所有节点
* @param nodeParser 解析器用于将输入节点转换为树节点
* @return 构建好的树形结构列表
*/
public static <T, K> List<Tree<K>> build(List<T> list, K parentId, NodeParser<T, K> 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 <T> 原始数据类型如实体类DTO
* @param <K> 节点 ID 类型 LongString
* @return 构建完成的树形结构可能包含多个顶级根节点
*/
public static <T, K> List<Tree<K>> buildMultiRoot(List<T> list, Function<T, K> getId, Function<T, K> getParentId, NodeParser<T, K> parser) {
if (CollUtil.isEmpty(list)) {
return CollUtil.newArrayList();
}
Set<K> 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 <K> 节点ID的类型
* @param nodes 节点列表
* @return 包含所有叶子节点的列表
*/
public static <K> List<Tree<K>> getLeafNodes(List<Tree<K>> nodes) {
if (CollUtil.isEmpty(nodes)) {
return CollUtil.newArrayList();
}
return nodes.stream()
.flatMap(TreeBuildUtils::extractLeafNodes)
.collect(Collectors.toList());
}
/**
* 获取指定节点下的所有叶子节点
*
* @param <K> 节点ID的类型
* @param node 要查找叶子节点的根节点
* @return 包含所有叶子节点的列表
*/
private static <K> Stream<Tree<K>> extractLeafNodes(Tree<K> node) {
if (!node.hasChild()) {
return Stream.of(node);
} else {
// 递归调用获取所有子节点的叶子节点
return node.getChildren().stream()
.flatMap(TreeBuildUtils::extractLeafNodes);
}
}
}
@@ -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 <T> void validate(T object, Class<?>... groups) {
Set<ConstraintViolation<T>> validate = VALID.validate(object, groups);
if (!validate.isEmpty()) {
throw new ConstraintViolationException("参数校验异常", validate);
}
}
}
@@ -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");
}
}
@@ -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"};
}
@@ -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;
}
}
@@ -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地址定位工具类离线方式
* 参考地址<a href="https://gitee.com/lionsoul/ip2region/tree/master/binding/java">集成 ip2region 实现离线IP地址定位库</a>
*
* @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 "未知";
}
}
}
@@ -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> 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 <E> 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);
}
}
}
}
@@ -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;
}
}
}
@@ -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 <T> CharSequence 的子类型
* @return 如果验证通过返回输入的账号
* @throws ValidateException 如果验证失败
*/
public static <T extends CharSequence> 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 <T> CharSequence 的子类型
* @return 如果验证通过返回输入的状态
* @throws ValidateException 如果验证失败
*/
public static <T extends CharSequence> T validateStatus(T value, String errorMsg) throws ValidateException {
if (!isStatus(value)) {
throw new ValidateException(errorMsg);
}
return value;
}
}
@@ -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注入风险");
}
}
}
}
@@ -0,0 +1,9 @@
package org.dromara.common.core.validate;
/**
* 校验分组 add
*
* @author pengles
*/
public interface AddGroup {
}
@@ -0,0 +1,9 @@
package org.dromara.common.core.validate;
/**
* 校验分组 edit
*
* @author pengles
*/
public interface EditGroup {
}
@@ -0,0 +1,9 @@
package org.dromara.common.core.validate;
/**
* 校验分组 query
*
* @author pengles
*/
public interface QueryGroup {
}

Some files were not shown because too many files have changed in this diff Show More