Compare commits

..

86 Commits

Author SHA1 Message Date
657b84d3cf Merge pull request #8007 from EightMonth/springboot3_sas
修复 CVE-2023-6378
2025-03-25 16:02:45 +08:00
2021bf39f8 修复 CVE-2023-6378 2025-03-25 16:00:14 +08:00
fdeb37c3d0 Merge pull request #8005 from EightMonth/springboot3_sas
jeewx-api修改成weixin4j
2025-03-25 14:19:33 +08:00
f9123208e1 jeewx-api修改成weixin4j 2025-03-25 14:15:05 +08:00
accb8f2f9f Merge pull request #7950 from EightMonth/springboot3_sas
解决严重bug,War包方式部署,服务启动报错
2025-03-12 15:28:44 +08:00
c643994546 解决严重bug,War包方式部署,服务启动报错 2025-03-12 14:16:50 +08:00
6934a0adee Merge pull request #7875 from EightMonth/springboot3_sas
修复 #7613
2025-03-03 17:05:12 +08:00
93e32a7177 Merge branch 'springboot3' into springboot3_sas 2025-03-03 14:30:40 +08:00
c9f5bb4409 【3.7.3 版本合并升级】
Merge remote-tracking branch 'origin/springboot3' into springboot3_sas

# Conflicts:
#	jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/system/util/JwtUtil.java
#	jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/shiro/ShiroConfig.java
#	jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/shiro/ShiroRealm.java
#	jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/service/impl/SysTenantPackServiceImpl.java
#	jeecgboot-vue3/package.json
2025-02-24 18:24:51 +08:00
10b68858d6 Merge pull request #7796 from EightMonth/springboot3_sas
固定vue-router版本号
2025-02-11 14:49:17 +08:00
da72e8f9c5 Update package.json 2025-02-11 14:27:40 +08:00
73e86686dc 固定vue-router版本号 2025-02-11 09:25:59 +08:00
f43d0d486b Merge pull request #7378 from EightMonth/springboot3_sas
分布式事务demo修复
2024-11-20 17:42:55 +08:00
65bde3331b 修复#7459,修复sas分支 @ignoreauth注解无效 2024-11-20 11:08:53 +08:00
b60942aa86 分布式事务demo修复 2024-10-24 10:48:00 +08:00
197b267e71 分布式事务demo修复 2024-10-24 09:19:49 +08:00
79f7134bd5 【springboot3分支 issues/7353】The bean 'dataSource', defined in class path resource #7353 2024-10-19 19:30:05 +08:00
6d432bc186 Merge pull request #7332 from EightMonth/springboot3_sas
修复redis序列化认证信息问题
2024-10-11 16:33:11 +08:00
415307eb9f 修复redis序列化认证信息问题 2024-10-11 16:27:28 +08:00
48e20b2af5 Merge pull request #7329 from EightMonth/springboot3_sas
Revert "修复redis序列化认证信息问题"
2024-10-11 15:03:12 +08:00
b7924b9ca8 Revert "修复redis序列化认证信息问题"
This reverts commit 4aa88189ed.
2024-10-11 14:59:11 +08:00
a10a2e0a9d Merge pull request #7327 from EightMonth/springboot3_sas
修复redis序列化认证信息问题
2024-10-11 13:44:54 +08:00
4aa88189ed 修复redis序列化认证信息问题 2024-10-11 11:43:05 +08:00
fdb05443c2 【版本合并】springboot3 sas升级到3.7.1 2024-10-10 22:48:10 +08:00
65d737db6d Merge remote-tracking branch 'origin/springboot3_sas' into springboot3_sas 2024-08-21 15:22:11 +08:00
f04f7f9abf Merge pull request #6899 from EightMonth/springboot3_sas
缩短token长度,适配主分支前端页面登录
2024-08-21 15:14:01 +08:00
935e118d15 sas分支前端同步master分支 2024-08-21 12:46:58 +08:00
e218367332 登出及强退逻辑完善 2024-08-21 11:22:13 +08:00
3a3f3cf367 升级积木报表到v1.8.0 2024-08-20 16:41:14 +08:00
0e762b4157 缩短token长度,适配主分支前端页面登录 2024-07-15 17:11:05 +08:00
f4712baa39 Merge pull request #6879 from EightMonth/springboot3_sas
修复访问仪表盘401
2024-07-10 16:23:48 +08:00
7d8b653d6e 修复访问仪表盘401 2024-07-10 16:22:10 +08:00
cf7f3f94be Merge pull request #6878 from EightMonth/springboot3_sas
修复访问仪表盘401问题
2024-07-10 16:03:19 +08:00
49f63b92ac 修复访问仪表盘401问题 2024-07-10 16:01:57 +08:00
5670a15b20 Merge remote-tracking branch 'origin/springboot3' into springboot3_sas 2024-07-08 12:28:17 +08:00
9e9ef20b7c 改法不兼容,暂时先还原 2024-07-03 14:35:44 +08:00
0c034031d1 Merge branch 'springboot3_sas' of https://github.com/jeecgboot/jeecg-boot into springboot3_sas 2024-07-03 12:23:13 +08:00
491a038b5a 增加仪表盘的请求排除 2024-07-03 12:22:43 +08:00
8a4fcb0023 Merge remote-tracking branch 'origin/springboot3' into springboot3_sas 2024-07-03 12:02:32 +08:00
e93dcc1a7e Merge pull request #6660 from EightMonth/springboot3_sas
修复redis反序列化时间问题
2024-06-27 18:33:16 +08:00
383cbf250f 修复redis反序列化时间问题 2024-06-27 18:09:54 +08:00
9fe1450ac9 Merge pull request #6646 from EightMonth/springboot3_sas
sas实现免登录注解
2024-06-25 17:38:48 +08:00
88b9b12998 sas实现免登录注解 2024-06-25 17:32:57 +08:00
9e25566271 【重要更新】vue3前端为了适配springboot3 sas分支(Spring Authorization Server)特定修改 2024-06-23 11:25:55 +08:00
8e54e06978 Merge remote-tracking branch 'origin/springboot3' into springboot3_sas 2024-06-23 11:23:34 +08:00
e5c082ae13 版本合并前端和后端放到一起 2024-06-23 10:35:53 +08:00
96ab98ac3e Merge remote-tracking branch 'origin/springboot3' into springboot3_sas
# Conflicts:
#	jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/exception/JeecgCaptchaException.java
#	jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/shiro/JwtToken.java
#	jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/shiro/ShiroConfig.java
#	jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/shiro/ShiroRealm.java
#	jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/shiro/filters/CustomShiroFilterFactoryBean.java
#	jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/shiro/filters/JwtFilter.java
#	jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/shiro/filters/ResourceCheckFilter.java
#	jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/vo/Shiro.java
2024-06-23 10:29:34 +08:00
1632c241ee 修改nacos命名空间springboot3的id也是springboot3,方便理解 2024-06-22 23:23:55 +08:00
e9d05b0e75 访问积木报表提示token无权限,原因TokenUtils.getLoginUser方法报错 2024-06-22 22:59:34 +08:00
6ade7e22f8 积木报表扩展实现类放开注释 2024-06-22 20:06:13 +08:00
43d47c08cb 【jeecgboot 3.7.0里程碑版本发布——合并springboot3sas分支】
Merge remote-tracking branch 'origin/springboot3' into springboot3_sas

# Conflicts:
#	.gitignore
#	db/tables_nacos.sql
#	jeecg-boot-base-core/pom.xml
#	jeecg-boot-base-core/src/main/java/org/jeecg/common/aspect/AutoLogAspect.java
#	jeecg-boot-base-core/src/main/java/org/jeecg/common/exception/JeecgBootExceptionHandler.java
#	jeecg-boot-base-core/src/main/java/org/jeecg/config/DruidWallConfigRegister.java
#	jeecg-boot-base-core/src/main/java/org/jeecg/config/shiro/ShiroConfig.java
#	jeecg-boot-base-core/src/main/java/org/jeecg/config/shiro/ShiroRealm.java
#	jeecg-boot-base-core/src/main/java/org/jeecg/config/shiro/filters/JwtFilter.java
#	jeecg-boot-base-core/src/main/java/org/jeecg/modules/base/service/impl/BaseCommonServiceImpl.java
#	jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/controller/LoginController.java
#	jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/controller/SysAnnouncementController.java
#	jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/controller/SysDepartController.java
#	jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/controller/SysDepartRoleController.java
#	jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/controller/SysDictController.java
#	jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/controller/SysPermissionController.java
#	jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/controller/SysTableWhiteListController.java
#	jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/controller/SysTenantController.java
#	jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/controller/SysUserController.java
#	jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/service/impl/SysAnnouncementServiceImpl.java
#	jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/service/impl/SysDepartServiceImpl.java
#	jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/service/impl/SysTenantPackServiceImpl.java
#	jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/service/impl/SysTenantServiceImpl.java
#	jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/service/impl/SysUserDepartServiceImpl.java
#	jeecg-module-system/jeecg-system-start/src/main/resources/application-dev.yml
#	jeecg-module-system/jeecg-system-start/src/main/resources/application-prod.yml
#	jeecg-module-system/jeecg-system-start/src/main/resources/application-test.yml
#	pom.xml
2024-06-22 19:55:35 +08:00
e616c5d8fe Merge pull request #6243 from EightMonth/springboot3_sas
sas兼容shiro处理
2024-06-20 16:09:29 +08:00
cddf23c787 sas 兼容shiro处理 2024-05-31 14:29:21 +08:00
70a37309dd sas兼容shiro处理 2024-05-22 18:08:31 +08:00
48555b5219 Merge pull request #6201 from EightMonth/springboot3_sas
增加bug修复注释
2024-04-30 14:25:10 +08:00
06d58f202f 增加bug修复注释 2024-04-30 14:07:59 +08:00
628870af9b Merge pull request #6199 from EightMonth/springboot3_sas
修复#6168\#6169\websocket连接问题
2024-04-30 14:02:22 +08:00
b46a6438e6 修复#6168\#6169\websocket连接问题 2024-04-30 11:47:51 +08:00
5488f99723 Merge pull request #6194 from EightMonth/springboot3_sas
修复#6150,同时修复online表单无法加载问题
2024-04-29 17:34:06 +08:00
6bc1fe8d21 修复#6150,同时修复online表单无法加载问题 2024-04-29 17:27:25 +08:00
7cac16320c Merge pull request #6182 from EightMonth/springboot3_sas
修复Online同步数据库
2024-04-26 12:59:43 +08:00
24dbd1db39 修复Online同步数据库 2024-04-26 12:53:22 +08:00
46b026b989 Merge pull request #6092 from EightMonth/springboot3_sas
升级druid v1.2.22版本兼容处理
2024-04-08 15:26:41 +08:00
94c45f5e0f 升级druid v1.2.22版本兼容处理 2024-04-08 14:11:39 +08:00
8950e19d4e Merge pull request #6037 from EightMonth/springboot3_sas
修复 #5936
2024-03-25 15:00:06 +08:00
99eb88f71c 修复 #5936 2024-03-25 14:44:23 +08:00
824d7839d8 Merge pull request #6014 from EightMonth/springboot3_sas
支持手动生成token
2024-03-22 16:18:29 +08:00
c88f9d95d4 支持手动生成token 2024-03-19 17:16:12 +08:00
beb0bc2f64 Merge pull request #5995 from EightMonth/springboot3_sas
移除权限不足异常堆栈,权限加载加入缓存
2024-03-15 17:06:05 +08:00
f741db874c 移除权限不足异常堆栈,权限加载加入缓存 2024-03-15 13:55:58 +08:00
d684c09392 Merge pull request #5965 from EightMonth/springboot3_sas
新增token校验、客户端便捷工具类、修复登录缺乏租户信息、强退功能失效
2024-03-12 14:27:11 +08:00
364be22dd0 新增token校验、客户端便捷工具类、修复登录缺乏租户信息、强退功能失效 2024-03-08 16:30:23 +08:00
20efa3bf9a Merge pull request #5934 from EightMonth/springboot3_sas
修正springboot3 sas默认配置
2024-03-01 16:23:13 +08:00
c7977dda3d 添加nacos sql自动创建nacos库 2024-03-01 16:17:30 +08:00
c27c5a9a9b 梳理服务配置信息 2024-03-01 15:49:28 +08:00
0ab280f812 添加springboot3的配置变更 2024-03-01 14:59:40 +08:00
c3066dac17 修改认证异常类拦截 2024-03-01 14:51:16 +08:00
b650d512b3 修复 #5903 ,完善在微服务的sas认证 2024-03-01 14:51:09 +08:00
925ec9447d Merge pull request #5819 from EightMonth/springboot3_sas
打通三方登录&移除shiro
2024-01-17 13:55:58 +08:00
411a73c1bf 移除shiro 2024-01-16 19:49:42 +08:00
84077e6e24 移除shiro 2024-01-16 19:49:15 +08:00
184cf97304 打通三方登录 2024-01-16 19:09:56 +08:00
5f425b49b2 Merge pull request #5761 from EightMonth/springboot3_sas
升级 spring authorization server
2024-01-12 09:40:48 +08:00
3ac8ee304a 完全替换shiro权限注解,新增手机登录、APP登录 2024-01-12 09:26:30 +08:00
0faac01bb7 sas升级脚本 2024-01-04 11:27:33 +08:00
74d88a8fcc springboot sas升级 2024-01-04 11:27:23 +08:00
1448 changed files with 23369 additions and 76365 deletions

View File

@ -10,9 +10,6 @@ assignees: getActivity
##### 版本号:
##### 分支:
##### 问题描述:

View File

@ -6,12 +6,10 @@ assignees: getActivity
---
##### 版本号:
##### 分支:
##### 问题描述:

3
.gitignore vendored
View File

@ -13,5 +13,4 @@ os_del.cmd
os_del_doc.cmd
.svn
derby.log
.cursor
.history
*.log

View File

@ -1,177 +0,0 @@
AIGC应用平台介绍
===============
一个全栈式 AI 开发平台,旨在帮助开发者快速构建和部署个性化的 AI 应用。
> JDK说明AI流程编排引擎暂时不支持jdk21所以目前只能使用jdk8或者jdk17启动项目。
JeecgBoot平台的AIGC功能模块是一套类似`Dify``AIGC应用开发平台`+`知识库问答`是一款基于LLM大语言模型AI应用平台和 RAG 的知识库问答系统。
其直观的界面结合了 AI 流程编排、RAG 管道、知识库管理、模型管理、对接向量库、实时运行可观察等让您可以快速从原型到生产拥有AI服务能力。
### AI视频介绍
[![](https://jeecgos.oss-cn-beijing.aliyuncs.com/files/jeecg_aivideo.png)](https://www.bilibili.com/video/BV1zmd7YFE4w)
##### 功能大模块
- AI应用开发平台
- AI知识库系统
- AI大模型管理
- AI流程编排
- AI对话支持图片
- AI对话助手(智能问答)
- AI建表Online表单
- AI写文章CMS
- AI表单字段建议表单设计器
#### Dify `VS` JEECG AI
> JEECG AI与Dify相比在多个方面展现出显著的优势特别是在文档处理、格式和图片保持方面。以下是一些具体的优点
> - Markdown文档库导入
> JEECG AI允许用户直接导入整个Markdown文档库这不仅保留markdown格式还支持图片的导入确保文档内容的完整性和可视化效果。
> - 对话回复格式美观:
> 在对话过程中JEECG AI能够保持回复内容的原格式也不丢失图片使得输出的文章更加美观不会出现格式错乱的情况还支持图片的渲染。
> - PDF文档导入与格式转换
> JEECG AI在处理PDF文档时能够更好地保持原始格式和图片确保转换后的内容与原始文档一致。这个功能在许多AI产品中表现不佳而JEECG AI在这方面做出了显著的优化
| 功能 | Dify | Jeecg AI |
|------------|------------------|-----------------------------------------|
| AI工作流 | 有 | 有 |
| RAG 管道向量搜索 | 有 | 有 |
| AI模型管理 | 有 | 有 |
| AI应用管理 | 有 | 有 |
| AI知识库 | 有 | 有 |
| 产品方向 | 一款独立的 LLM 应用开发平台 | 低代码与AIGC应用二者结合的平台 |
| 业务集成 | 业务集成能力弱 | 更方便与业务系统集成,调用系统接口和逻辑更加方便 |
| AI业务流 | 侧重AI逻辑流程 | AI流程编排作为低代码的业务引擎用户可以通过AI流程配置各种业务流和AI流程 |
| 实现语言 | python + react | JAVA + vue3 |
| 上传markdown文档库(支持图片) | 不支持 | 支持 |
| AI对话支持发图和展示图片 | 支持 | 支持 |
### 技术文档
- [AIGC开发文档](https://help.jeecg.com/aigc)
- [安装向量库 pgvector](https://help.jeecg.com/aigc/config)
## 功能特点
- AI流程: 提供强大的AI流程设计器引擎支持编排 AI 工作过程,满足复杂业务场景,支持画布上构建和实时运行查看 AI流程运行情况。
- AI流程即服务: 通过AI流程编排你需要的智能体结合AI+自定义开发节点 实现功能性 API让你瞬间拥有各种智能体API。
- AI助手对话功能: 集成 ChatGPT、Deepseek、智普、私有大模型 等 AI 模型,提供智能对话和生成式 AI 功能,深度与知识库结合提供更精准的知识。
- RAG 功能: 涵盖从文档摄入到检索的所有内容,支持从 PDF、PPT 和其他常见文档格式中提取文本支持检索增强生成RAG将未训练数据与 AI 模型集成,提升智能交互能力。
- AI 知识库: 通过导入文档或已有问答对进行训练,让 AI 模型能根据文档以交互式对话方式回答问题。
- 模型管理支持对接各种大模型包括本地私有大模型Deepseek/ Llama 3 / Qwen 2 等)、国内公共大模型(通义千问 / 腾讯混元 / 字节豆包 / 百度千帆 / 智谱 AI / Kimi 等和国外公共大模型OpenAI / Claude / Gemini 等);
- 无缝嵌入Iframe一键嵌入,支持将AI聊天助手快速嵌入到第三方系统让系统快速拥有智能问答能力提高用户满意度。
#### 在线体验
- JeecgBoot演示 https://boot3.jeecg.com
- 敲敲云在线搭建AI知识库https://app.qiaoqiaoyun.com
## 技术交流
- 开发文档https://help.jeecg.com/aigc
- QQ群964611995、716488839(满)
## 功能列表
- AI应用管理(普通应用、高级流程应用)
- AI模型管理
- AI知识库
- AI应用平台(普通、对接AI流程)
- AI流程编排
- AI聊天支持嵌入第三方
- AI向量库对接
## 支持AI模型
| AI大模型 | 支持 |
|---------------| --- |
| DeepSeek | √ |
| ChatGTP | √ |
| Qwq | √ |
| 智库 | √ |
| Ollama本地搭建大模型 | √ |
| 等等。。 | √ |
## AIGC能做什么
AIGC模块是一个基于AI的自动化流程编排工具和聊天应用搭建平台它可以帮助用户快速生成AI流程接口和聊天应用提高效率。
以下是一些具体的应用场景和示例:
- 你可能需要一个翻译接口可以通过AI流程编排搭建出来。
- 你可能需要一个接口转换工具可以通过AI流程编排搭建出来。比如jimureport所需要接口返回格式与你的系统不同你通过AI接口实现自动转换
- 你可能需要一个聊天机器人可以通过AI流程编排搭建出来。
- 你可能需要一个自动化流程可以通过AI流程编排搭建出来。
- 你可能需要一个自动化处理文件的流程可以通过AI流程结合python脚本实现操作电脑文件等。
## AI应用平台功能展示
AI模型列表
![](https://oscimg.oschina.net/oscnet//a5fb3e0d69ca1706b0de221535c7acaa.png)
选择AI模型配置你的参数
![](https://oscimg.oschina.net/oscnet//1f941472758a5fc227f54f2683953b8e.png)
AI知识库支持手工录入文本导入pdf\\word\\excel等文档支持问答对训练
![](https://oscimg.oschina.net/oscnet//150bb33f48d6c8e2ae059e2a58f4200b.png)
![](https://oscimg.oschina.net/oscnet//032d16c915b0f79318935484c81df260.png)
AI流程提供强大的AI流程设计器引擎支持编排 AI 工作过程,满足复杂业务场景,支持画布上构建和实时运行查看 AI流程运行情况。
![](https://oscimg.oschina.net/oscnet//f40f9aa275cd4aea94e1c209513151e2.png)
目前支持的节点有开始、结束、AI知识库节点、AI节点、分类节点、分支节点、JAVA节点、脚本节点、子流程节点、http请求节点、直接回复节点等节点
![](https://oscimg.oschina.net/oscnet//6d86480ab1bbfab5b2e6992b416b2152.png)
节点项配置
![](https://oscimg.oschina.net/oscnet//90a5f76b6b4fc406e2e2b87245b35459.png)
在线运行看结果
![](https://oscimg.oschina.net/oscnet//bc9817a7bbd94936a5a3e885abe3cb38.png)
AI应用配置支持AI流程配置和简单的AI配置
![](https://oscimg.oschina.net/oscnet//a853d9be4d3756806799ad025e722df8.png)![](https://oscimg.oschina.net/oscnet//d3bcbf5977c6fb75a8f996e1e40590be.png)
可以关联多个知识库右侧是AI智能回复你可以搭建自己的智能体比如搭建一个 “诗词达人” “翻译助手”
![](https://oscimg.oschina.net/oscnet//c26a848136be3e22ec1e0651e78976c2.png)
可以将创建的聊天应用,集成到第三方系统中
![](https://oscimg.oschina.net/oscnet//39c6f589ef46f0454b229915ffa263f4.png)

View File

@ -7,12 +7,12 @@
JEECG BOOT AI Low Code Platform
===============
Current version: 3.9.0 (Release date: 2025-12-01)
Current version: 3.7.3 (Release date: 2025-02-10)
[![AUR](https://img.shields.io/badge/license-Apache%20License%202.0-blue.svg)](https://github.com/zhangdaiscott/jeecg-boot/blob/master/LICENSE)
[![](https://img.shields.io/badge/Author-guojusoft-orange.svg)](http://www.jeecg.com)
[![](https://img.shields.io/badge/version-3.9.0-brightgreen.svg)](https://github.com/zhangdaiscott/jeecg-boot)
[![](https://img.shields.io/badge/version-3.7.3-brightgreen.svg)](https://github.com/zhangdaiscott/jeecg-boot)
[![GitHub stars](https://img.shields.io/github/stars/zhangdaiscott/jeecg-boot.svg?style=social&label=Stars)](https://github.com/zhangdaiscott/jeecg-boot)
[![GitHub forks](https://img.shields.io/github/forks/zhangdaiscott/jeecg-boot.svg?style=social&label=Fork)](https://github.com/zhangdaiscott/jeecg-boot)
@ -21,7 +21,7 @@ Current version: 3.9.0 (Release date: 2025-12-01)
Project introduction
-----------------------------------
<h3 align="center">Java AI Low Code Platform</h3>
<h3 align="center">Java AI Low Code Platform for Enterprise web applications</h3>
JeecgBoot is a `AI low code platform` based on code `generators`! Front and back end separation architecture SpringBoot2.x, SpringCloud, Ant Design&Vue, Mybatis plus, Shiro, JWT, support for microservices. The powerful code generator makes the front and back end of the code generation, low code development! JeecgBoot leads a new low-code development paradigm (OnlineCoding-> Code Generator -> Manual MERGE) that helps resolve 70% of the duplication in Java projects and makes development more business-focused. Not only can quickly improve efficiency, save research and development costs, but also do not lose flexibility!
@ -37,7 +37,7 @@ AI Empowering Low-Code: Currently, JeecgBoot supports AI large models such as Ch
Technical support
-----------------------------------
Problems or bugs in use can be found in [Making on the Issues](https://github.com/jeecgboot/JeecgBoot/issues/new?template=bug_report.md)
Problems or bugs in use can be found in [Making on the Issues](https://github.com/jeecgboot/JeecgBoot/issues/new)
##### Project description
@ -49,11 +49,6 @@ Problems or bugs in use can be found in [Making on the Issues](https://github.co
| `jeecg-uniapp` | [APP development framework, a code multi terminal adaptation, and support APP, small program, H5](https://github.com/jeecgboot/jeecg-uniapp) |
### Video Introduction
[![](https://upload.jeecg.com/jeecg/qiaoqiaoyunsite/jeecgvideo02.png)](https://www.bilibili.com/video/BV1Nk4y1o7Qc)
Download other source code
-----------------------------------
@ -69,13 +64,9 @@ Jeecg-Boot AI low code platform can be applied in the development of any J2EE pr
Starts the project
-----------------------------------
> Default account password admin/123456
- [IDEA Quick start](https://help.jeecg.com/java/setup/idea/startup.html)
- [Docker Quick start](https://help.jeecg.com/java/docker/quick.html)
- [Development Environment setup](https://help.jeecg.com/java/setup/tools)
- [IDEA Quick start(single model)](https://help.jeecg.com/java/setup/idea/startup)
- [Docker Quick start(single model)](https://help.jeecg.com/java/docker/quick)
- [IDEA Quick start(microservices model)](https://help.jeecg.com/java/springcloud/switchcloud/monomer)
- [Docker Quick start(microservices model)](https://help.jeecg.com/java/docker/quickcloud)
Technical documentation
@ -83,9 +74,9 @@ Technical documentation
- Website [http://www.jeecg.com](http://www.jeecg.com)
- Demo [OnlineDemo](http://boot3.jeecg.com) | [APP](http://jeecg.com/appIndex)
- Doc [DocumentCenter](http://help.jeecg.com) | [AI Config](https://help.jeecg.com/java/ai/aichat)
- Doc [DocumentCenter](http://help.jeecg.com) | [AI Config](https://help.jeecg.com/java/ai/aichat.html)
- Newbie guide [Quick start](http://www.jeecg.com/doc/quickstart) | [Q&A ](http://www.jeecg.com/doc/qa) | [1 minute experience](https://my.oschina.net/jeecg/blog/3083313)
- QQ group 964611995、⑩716488839(满)、⑨808791225(满)
- QQ group ⑩716488839、⑨808791225、⑧825232878、⑦791696430、⑥730954414(full)、683903138(full)、⑤860162132(full)、④774126647(full)、③816531124(full)、②769925425(full)、①284271917(full)
@ -185,7 +176,7 @@ Technical Architecture:
#### Development Environment
- Language: Java Default Jdk17(support jdk8、jdk21)
- Language: Java 8+ (17)
- IDE(JAVA) : IDEA (lombok plug-in must be installed)
@ -202,17 +193,17 @@ Technical Architecture:
- Basic framework: Spring Boot 2.7.18
- Microservice framework: Spring Cloud Alibaba 2021.0.6.2
- Microservice framework: Spring Cloud Alibaba 2021.0.1.0
- Persistence layer framework: MybatisPlus 3.5.3.2
- Report tool: JimuReport 1.9.5
- Report tool: JimuReport 1.9.3
- Security framework: Apache Shiro 1.13.0, Jwt 4.5.0
- Security framework: Apache Shiro 1.12.0, Jwt 3.11.0
- Microservice technology stack: Spring Cloud Alibaba, Nacos, Gateway, Sentinel, Skywalking
- Database connection pool: Alibaba Druid 1.1.24
- Database connection pool: Alibaba Druid 1.1.22
- Log printing: logback
@ -251,16 +242,8 @@ Technical Architecture:
| --- | --- |
| DeepSeek | √ |
| ChatGPT | √ |
| Qwq | √ |
| 智库 | √ |
| Ollama本地搭建大模型 | √ |
| 等等。。 | √ |
AI Config https://help.jeecg.com/java/ai/aichat
AI APP: https://help.jeecg.com/aigc
AI Config https://help.jeecg.com/java/ai/aichat.html
## Microservice solutions
@ -272,7 +255,7 @@ AI APP: https://help.jeecg.com/aigc
- 6. Distributed files Minio and Alioss √
- 7. Unified permission control
- 8. Service monitoring SpringBootAdmin√
- 9. link tracking Skywalking [reference document](https://help.jeecg.com/java/springcloud/super/skywarking)
- 9. link tracking Skywalking [reference document](https://help.jeecg.com/java/springcloud/super/skywarking.html)
- 10. Messaging middleware RabbitMQ √
- 11. Distributed task xxl-job √
- 12. Distributed Transaction Seata
@ -289,8 +272,8 @@ AI APP: https://help.jeecg.com/aigc
![功能蓝图](https://jeecgos.oss-cn-beijing.aliyuncs.com/upload/test/Jeecg-Boot-lantu202005_1590912449914.jpg "在这里输入图片标题")
### quick start
- Microservice Development [Monomer upgrade to microservice](https://help.jeecg.com/java/springcloud/switchcloud/monomer)
- [Docker starts the micro-service background](https://help.jeecg.com/java/docker/springcloud)
- Microservice Development [Monomer upgrade to microservice](https://help.jeecg.com/java/springcloud/switchcloud/monomer.html)
- [Docker starts the micro-service background](https://help.jeecg.com/java/docker/springcloud.html)
### Effect of system

View File

@ -1,126 +0,0 @@
JeecgBoot低代码平台(商业版介绍)
===============
项目介绍
-----------------------------------
<h3 align="center">企业级AI低代码平台</h3>
JeecgBoot是一款集成AI应用的基于BPM流程的低代码平台旨在帮助企业快速实现低代码开发和构建个性化AI应用支持MCP和插件实现聊天式业务操作如 “一句话创建用户”)!
前后端分离架构Ant Design&Vue3SpringBootSpringCloud AlibabaMybatis-plusShiro。强大的代码生成器让前后端代码一键生成无需写任何代码 引领AI低代码开发模式: AI生成->OnlineCoding-> 代码生成-> 手工MERGE 帮助Java项目解决80%的重复工作让开发更多关注业务提高效率、节省成本同时又不失灵活性低代码能力Online表单、表单设计、流程设计、Online报表、大屏/仪表盘设计、报表设计; AI应用平台功能AI知识库问答、AI模型管理、AI流程编排、AI聊天等支持含ChatGPT、DeepSeek、Ollama等多种AI大模型
JeecgBoot 提供了一系列 `低代码能力`,实现`真正的零代码`在线开发Online表单开发、Online报表、复杂报表设计、打印设计、在线图表设计、仪表盘设计、大屏设计、移动图表能力、表单设计器、在线设计流程、流程自动化配置、插件能力可插拔
`AI赋能低代码:` 目前提供了AI应用、AI模型管理、AI流程编排、AI对话助手AI建表、AI写文章、AI知识库问答、AI字段建议等功能;支持各种AI大模型ChatGPT、DeepSeek、Ollama、智普、千问等.
`JEECG宗旨是:` 简单功能由OnlineCoding配置实现做到`零代码开发`复杂功能由代码生成器生成进行手工Merge 实现`低代码开发`,既保证了`智能`又兼顾`灵活`;实现了低代码开发的同时又支持灵活编码,解决了当前低代码产品普遍不灵活的弊端!
`JEECG业务流程:` 采用工作流来实现、扩展出任务接口,供开发编写业务逻辑,表单提供多种解决方案: 表单设计器、online配置表单、编码表单。同时实现了流程与表单的分离设计松耦合、并支持任务节点灵活配置既保证了公司流程的保密性又减少了开发人员的工作量。
#### JeecgBoot商业版与同类产品区别
-----------------------------------
- 灵活性jeecgboot基于开源技术栈设计初考虑到可插拔性和集成灵活性确保平台的智能性与灵活性避免因平台过于庞大而导致的扩展困难。
- 流程管理:支持一个表单挂接多个流程,同时一个流程可以连接多个表单,增强了流程的灵活性和复杂性管理。
- 符合中国国情的流程针对中国市场的特定需求jeecgboot能够实现各种符合中国国情的业务流程。
- 强大的表单设计器jeecgboot的表单设计器与敲敲云共享具备高质量和智能化的特点能够满足零代码应用的需求业内同类产品中不多见。
- 报表功能:自主研发的报表工具,拥有独立知识产权,功能上比业内老牌产品如帆软更智能,操作简便。
- BI产品整合提供大屏、仪表盘、门户等功能完美解决这些需求并支持移动面板的设计与渲染。
- 自主研发的模块jeecgboot的所有模块均为自主研发具有独立的知识产权。
- 颗粒度和功能细致在功能细致度和颗粒度上jeecgboot远超同类产品尤其在零代码能力方面表现突出。
- 零代码应用管理最新版支持与敲敲云的零代码应用管理能力的集成使得jeecgboot既具备低代码又具备零代码的应用能力业内独一无二。
- 强大的代码生成器作为开源代码生成器的先锋jeecgboot在代码生成的智能化和在线低代码与代码生成的结合方面优势明显。
- 精细化权限管理提供行级和列级的数据权限控制满足企业在ERP和OA领域对权限管理的严格需求。
- 多平台支持的APP目前采用uniapp3实现支持小程序、H5、App及鸿蒙、鸿蒙Next、Electron桌面应用等多种终端。
> 综上所述jeecgboot不仅在功能上具备丰富性和灵活性还在技术架构、权限管理和用户体验等方面展现出明显的优势是一个综合性能强大的低代码平台。
商业版演示
-----------------------------------
JeecgBoot vs 敲敲云
> - JeecgBoot是低代码产品拥有系列低代码能力比如流程设计、表单设计、大屏设计代码生成器适合半开发模式开发+低代码结合),也可以集成零代码应用管理模块.
> - 敲敲云是零代码产品完全不写代码通过配置搭建业务系统其在jeecgboot基础上研发而成删除了online、代码生成、OA等需要编码功能只保留应用管理功能和聊天、日程、文件三个OA组件.
- JeecgBoot低代码 https://boot3.jeecg.com
- 敲敲云零代码https://app.qiaoqiaoyun.com
- APP演示(多端): http://jeecg.com/appIndex
### 流程视频介绍
[![](https://jeecgos.oss-cn-beijing.aliyuncs.com/files/flow_video.png)](https://www.bilibili.com/video/BV1Nk4y1o7Qc)
### 商业版功能简述
> 详细的功能介绍,[请联系官方](https://jeecg.com/vip)
```
│─更多商业功能
│ ├─流程设计器
│ ├─简流设计器(类钉钉版)
│ ├─门户设计NEW
│ ├─表单设计器
│ ├─大屏设计器
│ └─我的任务
│ └─历史流程
│ └─历史流程
│ └─流程实例管理
│ └─流程监听管理
│ └─流程表达式
│ └─我发起的流程
│ └─我的抄送
│ └─流程委派、抄送、跳转
│ └─OA办公组件
│ └─零代码应用管理(无需编码,在线搭建应用系统)
│ ├─积木报表企业版含jimureport、jimubi
│ ├─AI流程设计器源码
│ ├─Online全模块功能和源码
│ ├─AI写文章CMS
│ ├─AI表单字段建议表单设计器
│ ├─OA办公协同组件
│ ├─在线聊天功能
│ ├─设计表单移动适配
│ ├─设计表单支持外部填报
│ ├─设计表单AI字段建议
│ ├─设计表单视图功能(支持多种类型含日历、表格、看板、甘特图)
│ └─。。。
```
##### 流程设计
![](https://oscimg.oschina.net/oscnet/up-981ce174e4fbb48c8a2ce4ccfd7372e2994.png)
![](https://oscimg.oschina.net/oscnet/up-1dc0d052149ec675f3e4fad632b82b48add.png)
![](https://oscimg.oschina.net/oscnet/up-de31bc2f9d9b8332c554b0954cc73d79593.png)
![输入图片说明](https://static.oschina.net/uploads/img/201907/05165142_yyQ7.png "在这里输入图片标题")
![输入图片说明](https://static.oschina.net/uploads/img/201904/14160917_9Ftz.png "在这里输入图片标题")
![输入图片说明](https://static.oschina.net/uploads/img/201904/14160633_u59G.png "在这里输入图片标题")
##### 表单设计器
![](https://oscimg.oschina.net/oscnet/up-5f8cb657615714b02190b355e59f60c5937.png)
![](https://oscimg.oschina.net/oscnet/up-d9659b2f324e33218476ec98c9b400e6508.png)
![](https://oscimg.oschina.net/oscnet/up-4868615395272d3206dbb960ade02dbc291.png)

426
README.md
View File

@ -2,13 +2,12 @@
JeecgBoot AI低代码平台
===============
当前最新版本: 3.9.0发布日期2025-12-01
当前最新版本: 3.7.3发布日期2025-02-10
[![AUR](https://img.shields.io/badge/license-Apache%20License%202.0-blue.svg)](https://github.com/jeecgboot/JeecgBoot/blob/master/LICENSE)
[![](https://img.shields.io/badge/Author-北京国炬软件-orange.svg)](https://jeecg.com)
[![](https://img.shields.io/badge/blog-技术博客-orange.svg)](https://jeecg.blog.csdn.net)
[![](https://img.shields.io/badge/version-3.9.0-brightgreen.svg)](https://github.com/jeecgboot/JeecgBoot)
[![](https://img.shields.io/badge/Author-北京国炬软件-orange.svg)](http://guojusoft.com)
[![](https://img.shields.io/badge/version-3.7.3-brightgreen.svg)](https://github.com/jeecgboot/JeecgBoot)
[![GitHub stars](https://img.shields.io/github/stars/zhangdaiscott/jeecg-boot.svg?style=social&label=Stars)](https://github.com/jeecgboot/JeecgBoot)
[![GitHub forks](https://img.shields.io/github/forks/zhangdaiscott/jeecg-boot.svg?style=social&label=Fork)](https://github.com/jeecgboot/JeecgBoot)
@ -17,52 +16,45 @@ JeecgBoot AI低代码平台
项目介绍
-----------------------------------
<h3 align="center">企业级AI低代码平台</h3>
<h3 align="center">Java AI Low Code Platform for Enterprise web applications</h3>
JeecgBoot 是一款基于BPM流程和代码生成的AI低代码平台助力企业快速实现低代码开发和构建AI应用支持MCP和插件实现聊天式业务操作如 “一句话创建用户”)!
JeecgBoot 是一款基于`BPM``代码生成器`的 AI低代码平台前后端分离架构 SpringBoot2.x/3.xSpringCloudAnt Design Vue3Mybatis-plusShiroJWT支持微服务、多租户;支持 AI 大模型 DeepSeek 和 ChatGPT、Ollama本地模型; 强大的代码生成器让前后端代码一键生成,无需写任何代码! JeecgBoot 引领 AI 低代码开发模式(AI生成-> OnlineCoding-> 代码生成器-> 手工MERGE) 帮助解决Java项目80%的重复工作让开发更多关注业务。既能快速提高效率节省成本同时又不失灵活性AIGC能力AI对话助手、AI建表、AI写文章、AI流程编排、AI知识库问答等等.
采用前后端分离架构Ant Design&Vue3SpringBoot3SpringCloud AlibabaMybatis-plus强大代码生成器实现前后端一键生成无需手写代码。
平台引领AI低代码开发模式AI生成→在线编码→代码生成→手工合并解决Java项目80%重复工作,提升效率,节省成本,兼顾灵活性。
具备强大且颗粒化的权限控制支持按钮权限和数据权限设置满足大型业务系统需求。功能涵盖在线表单、表单设计、流程设计、门户设计、报表与大屏设计、OA办公、AI应用、AI知识库、大模型管理、AI流程编排、AI聊天支持ChatGPT、DeepSeek、Ollama等多种AI大模型。
`AI赋能报表:` 积木报表是一款自主研发的强大开源企业级Web报表与大屏工具。它通过零编码的拖拽式操作赋能用户如同搭积木般轻松构建各类复杂报表和数据大屏全面满足企业数据可视化与分析需求助力企业级数据产品的高效打造与应用。
`AI赋能低代码:` 提供完善成熟的AI应用平台涵盖AI应用管理、AI模型管理、智能对话助手、知识库问答、流程编排与设计器、AI建表等多项功能。平台兼容多种主流大模型包括ChatGPT、DeepSeek、Ollama、智普、千问等助力企业高效构建智能化应用推动低代码开发与AI深度融合。
`JEECG宗旨是:` JEECG旨在通过OnlineCoding平台实现简单功能的零代码快速搭建同时针对复杂功能采用代码生成器生成代码并手工合并打造智能且灵活的低代码开发模式有效解决了当前低代码产品普遍缺乏灵活性的问题提升开发效率的同时兼顾系统的扩展性和定制化能力。
`JEECG业务流程:` JEECG业务流程采用BPM工作流引擎实现业务审批扩展任务接口供开发人员编写业务逻辑表单提供表单设计器、在线配置表单和编码表单等多种解决方案。通过流程与表单的分离设计松耦合及任务节点的灵活配置既保障了企业流程的安全性与保密性又大幅降低了开发人员的工作量。
JeecgBoot 提供了一系列 `AI能力` `低代码模块`,实现在线开发`真正的零代码`Online表单开发、Online报表、报表配置能力、在线图表设计、仪表盘设计、大屏设计、移动配置能力、表单设计器、在线设计流程、流程自动化配置、插件能力可插拔、AI对话助手AI建表、AI写文章、AI流程编排、AI知识库问答、AI赋能低代码等等
`JEECG宗旨是:` 简单功能由OnlineCoding配置实现做到`零代码开发`复杂功能由代码生成器生成进行手工Merge 实现`低代码开发`,既保证了`智能`又兼顾`灵活`;实现了低代码开发的同时又支持灵活编码,解决了当前低代码产品普遍不灵活的弊端!
`JEECG业务流程:` 采用工作流来实现、扩展出任务接口,供开发编写业务逻辑,表单提供多种解决方案: 表单设计器、online配置表单、编码表单。同时实现了流程与表单的分离设计松耦合、并支持任务节点灵活配置既保证了公司流程的保密性又减少了开发人员的工作量。
`AI赋能低代码:` 目前JeecgBoot支持AI大模型`ChatGPT``DeepSeek`,现在最新版默认使用`DeepSeek`速度更快质量更高。目前提供了AI对话助手、AI建表、AI报表、AI写文章、AI流程编排、AI知识库问答等功能。
适用项目
-----------------------------------
JeecgBoot低代码平台兼容所有J2EE项目开发支持信创国产化,特别适用于SAAS、企业信息管理系统MIS、内部办公系统OA、企业资源计划系统ERP、客户关系管理系统CRM及AI知识库等场景。其半智能手工Merge开发式,可显著提升70%以上的开发效率极大降低开发成本。同时JeecgBoot还是一款全栈式AI开发平台助力企业快速构建和部署个性化AI应用。
JeecgBoot AI低代码平台,可以应用在任何J2EE项目开发,支持信创国产化(默认适配达梦和人大金仓)。尤其适合SAAS项目、企业信息管理系统MIS、内部办公系统OA、企业资源计划系统ERP、客户关系管理系统CRM等,其半智能手工Merge开发式,可显著提高开发效率70%以上,极大降低开发成本
**信创兼容说明**
- 操作系统:国产麒麟、银河麒麟等国产系统几乎都是基于 Linux 内核,因此它们具有良好的兼容性。
- 数据库达梦、人大金仓、TiDB
- 中间件:东方通 TongWeb、TongRDS宝兰德 AppServer、CacheDB, [信创配置文档](https://help.jeecg.com/java/tongweb-deploy/)
#### 项目说明
| 项目名 | 说明 |
|--------------------|------------------------|
| `jeecg-boot` | 后端源码JAVASpringBoot微服务架构 |
| `jeecgboot-vue3` | 前端源码VUE3vue3+vite6+ts最新技术栈 |
| `jeecg-uniapp` | [配套APP框架](https://github.com/jeecgboot/jeecg-uniapp) 适配多个终端支持APP、小程序、H5 |
版本说明
技术文档
-----------------------------------
|下载 | SpringBoot3.5 + Shiro |SpringBoot3.5+ SpringAuthorizationServer | SpringBoot3.5 + Sa-Token | SpringBoot2.7(JDK17/JDK8) |
|------|---------------------------------------------------------|----------------------------|-------------------|--------------------------------------------|
| Github | [`main`](https://github.com/jeecgboot/JeecgBoot) | [`springboot3_sas`](https://github.com/jeecgboot/JeecgBoot/tree/springboot3_sas) 分支 | [`springboot3-satoken`](https://github.com/jeecgboot/JeecgBoot/tree/springboot3-satoken) 分支|[`springboot2`](https://github.com/jeecgboot/JeecgBoot/tree/springboot2) 分支|
| Gitee | [`main`](https://github.com/jeecgboot/JeecgBoot) | [`springboot3_sas`](https://gitee.com/jeecg/JeecgBoot/tree/springboot3_sas) 分支| [`springboot3-satoken`](https://gitee.com/jeecg/JeecgBoot/tree/springboot3-satoken) 分支|[`springboot2`](https://github.com/jeecgboot/JeecgBoot/tree/springboot2) 分支 |
- `jeecg-boot` 是后端JAVA源码项目Springboot3+Shiro+Mybatis+SpringCloudAlibaba支持单体和微服务切换.
- `jeecgboot-vue3` 是前端VUE3源码项目vue3+vite6+ts最新技术栈.
- `JeecgUniapp` 是[配套APP框架](https://github.com/jeecgboot/JeecgUniapp) 适配多个终端支持APP、小程序、H5、鸿蒙、鸿蒙Next.
- `jeecg-boot-starter` 是[jeecg-boot对应的底层封装starter](https://github.com/jeecgboot/jeecg-boot-starter) 微服务启动、xxljob、分布式锁starter、rabbitmq、分布式事务、分库分表shardingsphere等.
- 参考 [文档](https://help.jeecg.com/ui/2dev/mini) 可以删除不需要的demo制作一个精简版本
- 官方网站: [http://www.jeecg.com](http://www.jeecg.com)
- 在线演示 [在线演示](http://boot3.jeecg.com) | [APP演示](http://jeecg.com/appIndex)
- 快速体验: [一分钟体验低代码](https://jeecg.blog.csdn.net/article/details/106079007?spm=1001.2014.3001.5502 "一分钟体验零代码") | [在线体验零代码](https://app.qiaoqiaoyun.com/myapps/index "在线体验零代码")
- 开发文档: [文档中心](https://help.jeecg.com) | [AI集成配置(支持DeepSeek)](https://help.jeecg.com/java/ai/aichat.html)
- 反馈问题: [在Github上提Issues](https://github.com/jeecgboot/JeecgBoot/issues/new)
- 新手指南: [快速入门](http://www.jeecg.com/doc/quickstart) | [入门视频](http://jeecg.com/doc/video)
- QQ交流群 ⑩716488839、⑨808791225(满)、其他(满)
@ -70,126 +62,69 @@ JeecgBoot低代码平台兼容所有J2EE项目开发支持信创国产化
启动项目
-----------------------------------
> 默认账号密码: admin/123456
- [开发环境搭建](https://help.jeecg.com/java/setup/tools)
- [IDEA启动前后端(单体模式)](https://help.jeecg.com/java/setup/idea/startup)
- [Docker一键启动(单体模式)](https://help.jeecg.com/java/docker/quick)
- [IDEA启动前后端(微服务方式)](https://help.jeecg.com/java/springcloud/switchcloud/monomer)
- [Docker一键启动(微服务方式)](https://help.jeecg.com/java/docker/quickcloud)
- [IDEA启动前后端项目](https://help.jeecg.com/java/setup/idea/startup.html)
- [Docker一键启动前后端](https://help.jeecg.com/java/docker/quick.html)
技术文档
AIGC功能清单
-----------------------------------
- 官方网站: [http://www.jeecg.com](http://www.jeecg.com)
- 在线演示: [平台演示](https://boot3.jeecg.com) | [APP演示](https://jeecg.com/appIndex)
- 入门指南: [快速入门](http://www.jeecg.com/doc/quickstart) | [代码生成使用](https://help.jeecg.com/java/codegen/online) | [开发文档](https://help.jeecg.com) | [AI应用手册](https://help.jeecg.com/aigc) | [视频教程](http://jeecg.com/doc/video)
- AI编程实战视频: [JEECG低代码与Cursor+GitHub Copilot实现AI高效编程实战](https://www.bilibili.com/video/BV11XyaBVEoH)
- 技术支持: [反馈问题](https://github.com/jeecgboot/JeecgBoot/issues/new?template=bug_report.md) | [低代码体验一分钟](https://jeecg.blog.csdn.net/article/details/106079007)
- QQ交流群 964611995、⑩716488839(满)、⑨808791225(满)、其他(满)
- AI对聊天助手
- AI建表Online表单
- AI写文章CMS
- AI表单建议(表单设计器)
- AI流程编排研发中
- AI知识库问答系统研发中
- AI应用开发平台研发中
- AI聊天窗口支持嵌入第三方研发中
AI 应用平台介绍
-----------------------------------
一个全栈式 AI 开发平台,旨在帮助开发者快速构建和部署个性化的 AI 应用。
JeecgBoot平台提供了一套完善的AI应用管理系统模块是一套类似`Dify``AIGC应用开发平台`+`知识库问答`是一款基于LLM大语言模型AI应用平台和 RAG 的知识库问答系统。
其直观的界面结合了 AI 流程编排、RAG 管道、知识库管理、模型管理、对接向量库、实时运行可观察等让您可以快速从原型到生产拥有AI服务能力。
- [详细专题介绍,请点击查看](README-AI.md)
- AI视频介绍
[![](https://jeecgos.oss-cn-beijing.aliyuncs.com/files/jeecg_aivideo.png)](https://www.bilibili.com/video/BV1zmd7YFE4w)
为什么选择JeecgBoot?
-----------------------------------
- 1.采用最新主流前后分离框架Spring Boot3 + MyBatis + Shiro/SpringAuthorizationServer + Ant Design4 + Vue3容易上手代码生成器依赖性低灵活的扩展能力可快速实现二次开发。
- 2.前端大版本换代,最新版采用 Vue3.0 + TypeScript + Vite6 + Ant Design Vue4 等新技术方案。
- 3.支持微服务Spring Cloud AlibabaNacos、Gateway、Sentinel、Skywalking提供简易机制支持单体和微服务自由切换这样可以满足各类项目需求
- 4.开发效率高支持在线建表和AI建表提供强大代码生成器单表、树列表、一对多、一对一等数据模型增删改查功能一键生成菜单配置直接使用。
- 5.代码生成器提供强大模板机制,支持自定义模板,目前提供四套风格模板(单表两套、树模型一套、一对多三套)。
- 6.提供强大的报表和大屏可视化工具,支持丰富的数据源连接,能够通过拖拉拽方式快速制作报表、大屏和门户设计;支持多种图表类型:柱形图、折线图、散点图、饼图、环形图、面积图、漏斗图、进度图、仪表盘、雷达图、地图等。
- 7.低代码能力在线表单无需编码通过在线配置表单实现表单的增删改查支持单表、树、一对多、一对一等模型实现人人皆可编码在线配置零代码开发、所见即所得支持23种类控件。
- 8.低代码能力:在线报表、在线图表(无需编码,通过在线配置方式,实现数据报表和图形报表,可以快速抽取数据,减轻开发压力,实现人人皆可编码)。
- 9.Online支持在线增强开发提供在线代码编辑器支持代码高亮、代码提示等功能支持多种语言Java、SQL、JavaScript等
- 10.封装完善的用户、角色、菜单、组织机构、数据字典、在线定时任务等基础功能,支持访问授权、按钮权限、数据权限等功能。
- 11.前端UI提供丰富的组件库支持各种常用组件如表格、树形控件、下拉框、日期选择器等满足各种复杂的业务需求 [UI组件库文档](https://help.jeecg.com/category/ui%E7%BB%84%E4%BB%B6%E5%BA%93)。
- 12.提供APP配套框架一份多代码多终端适配一份代码多终端适配小程序、H5、安卓、iOS、鸿蒙Next。
- 13.新版APP框架采用Uniapp、Vue3.0、Vite、Wot-design-uni、TypeScript等最新技术栈包括二次封装组件、路由拦截、请求拦截等功能。实现了与JeecgBoot完美对接目前已经实现登录、用户信息、通讯录、公告、移动首页、九宫格、聊天、Online表单、仪表盘等功能提供了丰富的组件。
- 14.提供了一套成熟的AI应用平台功能从AI模型、知识库到AI应用搭建助力企业快速落地AI服务加速智能化升级。
- 15.AI能力目前JeecgBoot支持AI大模型chatgpt和deepseek现在最新版默认使用deepseek速度更快质量更高。目前提供了AI对话助手、AI知识库、AI应用、AI建表、AI报表等功能。
- 16.提供新行编辑表格JVXETable轻松满足各种复杂ERP布局拥有更高的性能、更灵活的扩展、更强大的功能。
- 17.平台首页风格,提供多种组合模式,支持自定义风格;支持门户设计,支持自定义首页。
- 18.常用共通封装各种工具类定时任务、短信接口、邮件发送、Excel导入导出等基本满足80%项目需求。
- 19.简易Excel导入导出支持单表导出和一对多表模式导出生成的代码自带导入导出功能。
- 20.集成智能报表工具报表打印、图像报表和数据导出非常方便可极其方便地生成PDF、Excel、Word等报表。
- 21.采用前后分离技术页面UI风格精美针对常用组件做了封装时间、行表格控件、截取显示控件、报表组件、编辑器等。
- 22.查询过滤器查询功能自动生成后台动态拼SQL追加查询条件支持多种匹配方式全匹配/模糊查询/包含查询/不匹配查询)。
- 23.数据权限(精细化数据权限控制,控制到行级、列表级、表单字段级,实现不同人看不同数据,不同人对同一个页面操作不同字段)。
- 24.接口安全机制可细化控制接口授权非常简便实现不同客户端只看自己数据等控制也提供了基于AK和SK认证鉴权的OpenAPI功能。
- 25.活跃的社区支持;近年来,随着网络威胁的日益增加,团队在安全和漏洞管理方面积累了丰富的经验,能够为企业提供全面的安全解决方案。
- 26.权限控制采用RBACRole-Based Access Control基于角色的访问控制
- 27.页面校验自动生成(必须输入、数字校验、金额校验、时间空间等)。
- 28.支持SaaS服务模式提供SaaS多租户架构方案。
- 29.分布式文件服务集成MinIO、阿里OSS等优秀的第三方提供便捷的文件上传与管理同时也支持本地存储。
- 30.主流数据库兼容一套代码完全兼容MySQL、PostgreSQL、Oracle、SQL Server、MariaDB、达梦、人大金仓等主流数据库。
- 31.集成工作流Flowable并实现了只需在页面配置流程转向可极大简化BPM工作流的开发用BPM的流程设计器画出了流程走向一个工作流基本就完成了只需写很少量的Java代码。
- 32.低代码能力在线流程设计采用开源Flowable流程引擎实现在线画流程、自定义表单、表单挂靠、业务流转。
- 33.多数据源:极其简易的使用方式,在线配置数据源配置,便捷地从其他数据抓取数据。
- 34.提供单点登录CAS集成方案项目中已经提供完善的对接代码。
- 35.低代码能力表单设计器支持用户自定义表单布局支持单表、一对多表单支持select、radio、checkbox、textarea、date、popup、列表、宏等控件。
- 36.专业接口对接机制统一采用RESTful接口方式集成Swagger-UI在线接口文档JWT token安全验证方便客户端对接。
- 37.高级组合查询功能,在线配置支持主子表关联查询,可保存查询历史。
- 38.提供各种系统监控实时跟踪系统运行情况监控Redis、Tomcat、JVM、服务器信息、请求追踪、SQL监控
- 39.消息中心支持短信、邮件、微信推送等集成WebSocket消息通知机制。
- 40.支持多语言,提供国际化方案。
- 41.数据变更记录日志,可记录数据每次变更内容,通过版本对比功能查看历史变化。
- 42.提供简单易用的打印插件支持谷歌、火狐、IE11+等各种浏览器。
- 43.后端采用Maven分模块开发方式前端支持菜单动态路由。
- 44.提供丰富的示例代码,涵盖了常用的业务场景,便于学习和参考。
<b>关注公众号了解官方动态</b>
![公众号](https://jeecg.com/images/jeecg/qrcode_jeecgboot.jpg "在这里输入图片标题")
技术架构:
-----------------------------------
#### 前端
- 前端环境要求Node.js要求`Node 20+` 版本以上、pnpm 要求`9+` 版本以上
` ( Vite 不再支持已结束生命周期EOL的 Node.js 18。现在需要使用 Node.js 20.19+ 或 22.12+)`
- 依赖管理node、npm、pnpm
- 前端IDE建议IDEA、WebStorm、Vscode
- 采用 Vue3.0+TypeScript+Vite6+Ant-Design-Vue4等新技术方案包括二次封装组件、utils、hooks、动态菜单、权限校验、按钮级别权限控制等功能
- 最新技术栈Vue3.0 + TypeScript + Vite6 + ant-design-vue4 + pinia + echarts + unocss + vxe-table + qiankun + es6
#### 后端
- IDE建议 IDEA (必须安装lombok插件 )
- 语言Java 默认jdk17(jdk21、jdk24)
- 语言Java 8+ (支持17)
- 依赖管理Maven
- 基础框架Spring Boot 3.5.5
- 微服务框架: Spring Cloud Alibaba 2023.0.3.3
- 持久层框架MybatisPlus 3.5.12
- 报表工具: JimuReport 2.1.3
- 安全框架Apache Shiro 2.0.4Jwt 4.5.0
- 基础框架Spring Boot 2.7.18
- 微服务框架: Spring Cloud Alibaba 2021.0.1.0
- 持久层框架MybatisPlus 3.5.3.2
- 报表工具: JimuReport 1.9.3
- 安全框架Apache Shiro 1.12.0Jwt 3.11.0
- 微服务技术栈Spring Cloud Alibaba、Nacos、Gateway、Sentinel、Skywalking
- 数据库连接池阿里巴巴Druid 1.2.24
- AI大模型支持 `ChatGPT` `DeepSeek` `千问`等各种常规模式
- 数据库连接池阿里巴巴Druid 1.1.22
- AI大模型支持 `ChatGPT` `DeepSeek`切换
- 日志打印logback
- 缓存Redis
- 其他autopoi, fastjsonpoiSwagger-uiquartz, lombok简化代码等。
- 默认提供MySQL5.7+数据库脚本
- 默认数据库脚本:MySQL5.7+
- [其他数据库,需要自己转](https://my.oschina.net/jeecg/blog/4905722)
#### 数据库支持
> jeecgboot平台支持以下数据库默认我们只提供mysql脚本其他数据库可以参考[转库文档](https://my.oschina.net/jeecg/blog/4905722)自己转。
#### 前端
- 前端IDE建议WebStorm、Vscode
- 采用 Vue3.0+TypeScript+Vite6+Ant-Design-Vue等新技术方案包括二次封装组件、utils、hooks、动态菜单、权限校验、按钮级别权限控制等功能
- 最新技术栈Vue3.0 + TypeScript + Vite6 + ant-design-vue4 + pinia + echarts + unocss + vxe-table + qiankun + es6
- 依赖管理node、npm、pnpm
#### 前端环境要求
* 本地环境安装 `Node.js 、npm 、pnpm`
* pnpm 要求`9+` 版本以上
* Node.js 版本建议`v20.15.0`,要求`Node 20+` 版本以上
` ( 因为Vite6 需要 Node.js 18 / 20+ )`
#### 支持库
| 数据库 | 支持 |
| --- | --- |
@ -198,16 +133,25 @@ JeecgBoot平台提供了一套完善的AI应用管理系统模块是一套类
| Sqlserver2017 | √ |
| PostgreSQL | √ |
| MariaDB | √ |
| MariaDB | √ |
| 达梦 | √ |
| 人大金仓 | √ |
| TiDB | √ |
| kingbase8 | √ |
#### 支持AI大模型
| AI大模型 | 支持 |
| --- | --- |
| DeepSeek | √ |
| ChatGTP | √ |
| Ollama本地搭建大模型 | √ |
AI集成文档 https://help.jeecg.com/java/ai/aichat.html
## 微服务解决方案
- 1、服务注册和发现 Nacos √
- 2、统一配置中心 Nacos √
- 3、路由网关 gateway(三种加载方式) √
@ -216,7 +160,7 @@ JeecgBoot平台提供了一套完善的AI应用管理系统模块是一套类
- 6、分布式文件 Minio、阿里OSS √
- 7、统一权限控制 JWT + Shiro √
- 8、服务监控 SpringBootAdmin√
- 9、链路跟踪 Skywalking [参考文档](https://help.jeecg.com/java/springcloud/super/skywarking)
- 9、链路跟踪 Skywalking [参考文档](https://help.jeecg.com/java/springcloud/super/skywarking.html)
- 10、消息中间件 RabbitMQ √
- 11、分布式任务 xxl-job √
- 12、分布式事务 Seata
@ -225,23 +169,62 @@ JeecgBoot平台提供了一套完善的AI应用管理系统模块是一套类
- 15、CAS 单点登录 √
- 16、路由限流 √
#### 微服务方式启动
- [单体快速切换微服务](https://help.jeecg.com/java/springcloud/switchcloud/monomer.html)
- [Docker一键启动微服务前后端](https://help.jeecg.com/java/docker/quickcloud.html)
#### 微服务架构图
![微服务架构图](https://jeecgos.oss-cn-beijing.aliyuncs.com/files/jeecgboot_springcloud2022.png "在这里输入图片标题")
开源版与企业版区别?
为什么选择JeecgBoot?
-----------------------------------
- JeecgBoot开源版采用 [Apache-2.0 license](LICENSE) 协议附加补充条款:允许商用使用,不会造成侵权行为,允许基于本平台软件开展业务系统开发(但在任何情况下,您不得使用本软件开发可能被认为与本软件竞争的软件).
- 商业版与开源版主要区别在于商业版提供了技术支持 和 更多的企业级功能(例如Online图表、流程监控、流程设计、流程审批、表单设计器、表单视图、积木报表企业版、OA办公、商业APP、零代码应用、Online模块源码等功能). [更多商业功能介绍,点击查看](README-Enterprise.md)
- JeecgBoot未来发展方向是零代码平台的建设也就是团队的另外一款产品 [敲敲云零代码](https://www.qiaoqiaoyun.com) 无需编码即可通过拖拽快速搭建企业级应用与JeecgBoot低代码平台形成互补满足从简单业务到复杂系统的全场景开发需求目前已经开源[欢迎下载](https://qiaoqiaoyun.com/downloadCode)
* 1.采用最新主流前后分离框架Springboot+Mybatis+antd+vue3容易上手; 代码生成器依赖性低,灵活的扩展能力,可快速实现二次开发;
* 2.支持微服务SpringCloud Alibaba(Nacos、Gateway、Sentinel、Skywalking),提供切换机制支持单体和微服务自由切换
* 3.开发效率高,采用代码生成器单表、树列表、一对多、一对一等数据模型增删改查功能一键生成菜单配置直接使用引入AI能力支持自动建表等功能
* 4.代码生成器提供强大模板机制,支持自定义模板,目前提供四套风格模板(单表两套、树模型一套、一对多三套)
* 5.代码生成器非常智能在线业务建模、在线配置、所见即所得支持23种类控件一键生成前后端代码大幅度提升开发效率不再为重复工作发愁。
* 6.AI能力目前JeecgBoot支持AI大模型chatgpt和deepseek现在最新版默认使用deepseek速度更快质量更高。目前提供了AI对话助手、AI建表、AI报表等功能。
* 6.低代码能力Online在线表单无需编码通过在线配置表单实现表单的增删改查支持单表、树、一对多、一对一等模型实现人人皆可编码
* 7.低代码能力Online在线报表、Online在线图表无需编码通过在线配置方式实现数据报表和图形报表可以快速抽取数据减轻开发压力实现人人皆可编码
* 9.封装完善的用户、角色、菜单、组织机构、数据字典、在线定时任务等基础功能,支持访问授权、按钮权限、数据权限等功能
* 10.常用共通封装,各种工具类(定时任务,短信接口,邮件发送,Excel导入导出等),基本满足80%项目需求
* 11.简易Excel导入导出支持单表导出和一对多表模式导出生成的代码自带导入导出功能
* 12.集成简易报表工具图像报表和数据导出非常方便可极其方便的生成图形报表、pdf、excel、word等报表
* 13.采用前后分离技术页面UI风格精美针对常用组件做了封装时间、行表格控件、截取显示控件、报表组件编辑器等等
* 14.查询过滤器查询功能自动生成后台动态拼SQL追加查询条件支持多种匹配方式全匹配/模糊查询/包含查询/不匹配查询);
* 15.数据权限(精细化数据权限控制,控制到行级,列表级,表单字段级,实现不同人看不同数据,不同人对同一个页面操作不同字段
* 16.页面校验自动生成(必须输入、数字校验、金额校验、时间空间等);
* 17.支持SAAS服务模式提供SaaS多租户架构方案。
* 18.分布式文件服务集成minio、阿里OSS等优秀的第三方提供便捷的文件上传与管理同时也支持本地存储。
* 19.主流数据库兼容一套代码完全兼容Mysql、Postgresql、Oracle、Sqlserver、MariaDB、达梦、人大金仓等主流数据库。
* 20.集成工作流flowable并实现了只需在页面配置流程转向可极大的简化bpm工作流的开发用bpm的流程设计器画出了流程走向一个工作流基本就完成了只需写很少量的java代码
* 21.低代码能力在线流程设计采用开源flowable流程引擎实现在线画流程,自定义表单,表单挂靠,业务流转
* 22.多数据源:及其简易的使用方式,在线配置数据源配置,便捷的从其他数据抓取数据;
* 23.提供单点登录CAS集成方案项目中已经提供完善的对接代码
* 24.低代码能力表单设计器支持用户自定义表单布局支持单表一对多表单、支持select、radio、checkbox、textarea、date、popup、列表、宏等控件
* 25.专业接口对接机制统一采用restful接口方式集成swagger-ui在线接口文档Jwt token安全验证方便客户端对接
* 26.接口安全机制,可细化控制接口授权,非常简便实现不同客户端只看自己数据等控制
* 27.高级组合查询功能,在线配置支持主子表关联查询,可保存查询历史
* 28.提供各种系统监控,实时跟踪系统运行情况(监控 Redis、Tomcat、jvm、服务器信息、请求追踪、SQL监控
* 29.消息中心(支持短信、邮件、微信推送等等)
* 30.集成Websocket消息通知机制
* 31.移动自适应效果优秀提供APP发布方案
* 32.支持多语言,提供国际化方案;
* 33.数据变更记录日志,可记录数据每次变更内容,通过版本对比功能查看历史变化
* 34.平台UI强大实现了移动自适应
* 35.平台首页风格,提供多种组合模式,支持自定义风格
* 36.提供简单易用的打印插件支持谷歌、火狐、IE11+ 等各种浏览器
* 37.示例代码丰富,提供很多学习案例参考
* 38.采用maven分模块开发方式
* 39.支持菜单动态路由
* 40.权限控制采用 RBACRole-Based Access Control基于角色的访问控制
* 41.提供新行编辑表格JVXETable轻松满足各种复杂ERP布局拥有更高的性能、更灵活的扩展、更强大的功能
* 42.提供仪表盘设计器,类大屏设计支持移动端,免费的数据可视化设计工具,支持丰富的数据源连接,能够通过拖拉拽方式快速制作图表和门户设计;目前支持多种图表类型:柱形图、折线图、散点图、饼图、环形图、面积图、漏斗图、进度图、仪表盘、雷达图、地图等等;
### Jeecg Boot 产品功能蓝图
@ -249,19 +232,47 @@ JeecgBoot平台提供了一套完善的AI应用管理系统模块是一套类
#### 系统功能架构图
![](https://oscimg.oschina.net/oscnet/up-1569487b95a07dbc3599fb1349a2e3aaae1.png)
### 分支说明
> 主干master更稳定如果你对最新技术栈无要求建议采用主干
#### springboot3分支
- 源码地址https://github.com/jeecgboot/JeecgBoot/tree/springboot3
- 架构说明升级Spring Boot3 & JDK 17 + Undertow + springdoc + fastjson2
#### springboot3_sas分支
- 源码地址https://github.com/jeecgboot/JeecgBoot/tree/springboot3_sas
- 架构说明在springboot3分支基础上采用SpringAuthorizationServer替换Shiro
### 开源版功能清单
### 功能模块
```
├─AI开发
│ ├─支持AI大模型ChatGPT和DeepSeek
│ ├─AI对话助手
│ ├─AI建表
│ ├─AI写文章
│ ├─AI流程编排研发中
│ ├─AI知识库问答系统研发中
│ ├─AI应用开发平台研发中
│ ├─AI聊天窗口支持嵌入第三方研发中
├─Online在线开发(低代码)
│ ├─Online在线表单
│ ├─Online代码生成器
│ ├─Online在线报表
│ ├─仪表盘设计器
│ ├─系统编码规则
│ ├─系统校验规则
├─积木报表设计器
│ ├─打印设计器
│ ├─数据报表设计
│ ├─图形报表设计支持echart
├─系统管理
│ ├─用户管理
│ ├─角色管理
│ ├─菜单管理
│ ├─首页配置
│ ├─权限设置(支持按钮权限、数据权限)
│ ├─表单权限(控制字段禁用、隐藏)
│ ├─部门管理
@ -272,36 +283,7 @@ JeecgBoot平台提供了一套完善的AI应用管理系统模块是一套类
│ └─职务管理
│ └─通讯录
│ ├─多数据源管理
├─白名单管理
│ ├─第三方配置(对接钉钉和企业微信)
│ └─多租户管理(租户管理、租户角色、我的租户、租户默认套餐管理)
├─Online在线开发(低代码)
│ ├─Online在线表单
│ ├─Online代码生成器
│ ├─Online在线报表
│ ├─仪表盘设计器
│ ├─系统编码规则
│ ├─系统校验规则
│ ├─APP版本管理
├─AI应用平台
│ ├─AI知识库问答系统
│ ├─AI大模型管理
│ ├─AI流程编排
│ ├─AI流程设计器
│ ├─AI对话支持图片
│ ├─AI对话助手(智能问答)
│ ├─AI建表Online表单
│ ├─AI聊天窗口支持嵌入第三方
│ ├─AI聊天窗口支持移动端
│ ├─支持常见大模型ChatGPT和DeepSeek、ollama等等
│ ├─AI OCR示例
├─数据可视化
│ ├─报表设计器(支持打印设计)
│ ├─大屏设和仪表盘设计
├─OpenAPI基于AK和SK认证鉴权
│ ├─接口管理
│ ├─接口授权
│ ├─接口文档
└─多租户管理(租户管理、租户角色、我的租户)
├─消息中心
│ ├─消息管理
│ ├─模板管理
@ -313,12 +295,8 @@ JeecgBoot平台提供了一套完善的AI应用管理系统模块是一套类
│ ├─高级查询器(弹窗自动组合查询条件)
│ ├─Excel导入导出工具集成支持单表一对多 导入导出)
│ ├─平台移动自适应支持
│ ├─提供新版uniapp3的代码生成器模板
├─系统监控
│ ├─Gateway路由网关
│ ├─基于AK和SK认证鉴权OpenAPI功能
│ ├─定时任务
│ ├─数据源管理
│ ├─性能扫描监控
│ │ ├─监控 Redis
│ │ ├─Tomcat
@ -326,11 +304,13 @@ JeecgBoot平台提供了一套完善的AI应用管理系统模块是一套类
│ │ ├─服务器信息
│ │ ├─请求追踪
│ │ ├─磁盘监控
│ ├─定时任务
│ ├─系统日志
│ ├─消息中心(支持短信、邮件、微信推送等等)
│ ├─数据日志(记录数据快照,可对比快照,查看数据变更情况)
│ ├─系统通知
│ ├─SQL监控
│ ├─在线用户
│ ├─swagger-ui(在线接口文档)
│─报表示例
│ ├─曲线图
│ └─饼状图
@ -395,16 +375,46 @@ JeecgBoot平台提供了一套完善的AI应用管理系统模块是一套类
│ ├─提供单点登录CAS集成方案
│ ├─提供APP发布方案
│ ├─集成Websocket消息通知机制
├─支持electron桌面应用打包(支持windows、linux、macOS三大平台)
│ ├─docker容器支持
│ ├─提供移动APP框架及源码Uniapp3版本支持H5、小程序、APP、鸿蒙Next
│ ├─提供移动APP低代码设计(Online表单、仪表盘)
─更多商业功能
│ ├─流程设计器
│ ├─表单设计器
│ ├─大屏设计器
│ └─我的任务
│ └─历史流程
│ └─历史流程
│ └─流程实例管理
│ └─流程监听管理
│ └─流程表达式
│ └─我发起的流程
│ └─我的抄送
│ └─流程委派、抄送、跳转
│ └─OA办公组件
│ └─。。。
```
### 系统效果
##### AI功能
AI聊天助手
![](https://oscimg.oschina.net/oscnet//65298d5710b4e6039a5f802b5f8505c5.png)
AI建表
![](https://oscimg.oschina.net/oscnet/up-381423599f219a67def45dfd9a99df8ef3f.png)
![](https://oscimg.oschina.net/oscnet/up-1508c2b0708c365605f68893044ee11f20d.png)
AI写文章
![](https://oscimg.oschina.net/oscnet/up-e3ee5b1fe497308805aa5e324b72994af79.png)
##### PC端
![](https://oscimg.oschina.net/oscnet/up-000530d95df337b43089ac77e562494f454.png)
@ -424,22 +434,6 @@ JeecgBoot平台提供了一套完善的AI应用管理系统模块是一套类
![](https://oscimg.oschina.net/oscnet/up-16c07e000278329b69b228ae3189814b8e9.png)
##### AI功能
AI聊天助手
![](https://oscimg.oschina.net/oscnet//65298d5710b4e6039a5f802b5f8505c5.png)
AI建表
![](https://oscimg.oschina.net/oscnet/up-381423599f219a67def45dfd9a99df8ef3f.png)
![](https://oscimg.oschina.net/oscnet/up-1508c2b0708c365605f68893044ee11f20d.png)
AI写文章
![](https://oscimg.oschina.net/oscnet/up-e3ee5b1fe497308805aa5e324b72994af79.png)
##### 仪表盘设计器
@ -506,6 +500,28 @@ AI写文章
![](https://oscimg.oschina.net/oscnet/up-6b81781b43086819049c4421206810667c5.png)
##### 流程设计
![](https://oscimg.oschina.net/oscnet/up-981ce174e4fbb48c8a2ce4ccfd7372e2994.png)
![](https://oscimg.oschina.net/oscnet/up-1dc0d052149ec675f3e4fad632b82b48add.png)
![](https://oscimg.oschina.net/oscnet/up-de31bc2f9d9b8332c554b0954cc73d79593.png)
![输入图片说明](https://static.oschina.net/uploads/img/201907/05165142_yyQ7.png "在这里输入图片标题")
![输入图片说明](https://static.oschina.net/uploads/img/201904/14160917_9Ftz.png "在这里输入图片标题")
![输入图片说明](https://static.oschina.net/uploads/img/201904/14160633_u59G.png "在这里输入图片标题")
##### 表单设计器
![](https://oscimg.oschina.net/oscnet/up-5f8cb657615714b02190b355e59f60c5937.png)
![](https://oscimg.oschina.net/oscnet/up-d9659b2f324e33218476ec98c9b400e6508.png)
![](https://oscimg.oschina.net/oscnet/up-4868615395272d3206dbb960ade02dbc291.png)

View File

@ -1,216 +0,0 @@
import os
import subprocess
import re
import sys
from typing import Tuple, Optional
def run_command(cmd: str) -> Tuple[int, str]:
"""执行命令并返回退出码和输出"""
try:
result = subprocess.run(cmd, shell=True, check=False,
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
text=True)
return result.returncode, result.stdout.strip()
except Exception as e:
return -1, str(e)
def check_java() -> bool:
"""检查JDK 17+是否安装"""
print("\n检查JDK 17+...")
rc, output = run_command("java -version 2>&1")
if rc != 0:
print("❌ 未检测到Java请安装JDK 17+")
return False
version_pattern = r'"(\d+)(?:\.\d+)*(?:_\d+)?'
match = re.search(version_pattern, output)
if not match:
print("❌ 无法解析Java版本")
return False
version = int(match.group(1))
if version >= 17:
print(f"✅ JDK版本 {version} (满足17+要求)")
return True
else:
print(f"❌ JDK版本 {version} (需要17+)")
return False
def check_maven() -> bool:
"""检查Maven是否安装"""
print("\n检查Maven...")
rc, output = run_command("mvn -v")
if rc == 0:
print("✅ Maven已安装")
return True
else:
print("❌ Maven未安装")
return False
def check_node() -> bool:
"""检查Node.js 20+是否安装"""
print("\n检查Node.js 20+...")
rc, output = run_command("node -v")
if rc != 0:
print("❌ Node.js未安装")
return False
version_pattern = r'v(\d+)\.\d+\.\d+'
match = re.search(version_pattern, output)
if not match:
print("❌ 无法解析Node.js版本")
return False
version = int(match.group(1))
if version >= 20:
print(f"✅ Node.js版本 {version} (满足20+要求)")
return True
else:
print(f"❌ Node.js版本 {version} (需要20+)")
return False
def check_pnpm() -> bool:
"""检查PNPM 9+是否安装"""
print("\n检查PNPM 9+...")
rc, output = run_command("pnpm -v")
if rc != 0:
print("❌ PNPM未安装")
return False
try:
# 处理可能的版本号格式v9.0.0 或 9.0.0 或 9
version_str = output.strip().lstrip('v').split('.')[0]
version = int(version_str)
if version >= 9:
print(f"✅ PNPM版本 {output.strip()} (满足9+要求)")
return True
else:
print(f"❌ PNPM版本 {output.strip()} (需要9+)")
return False
except (ValueError, IndexError):
print(f"❌ 无法解析PNPM版本: {output.strip()}")
return False
def check_redis_connection() -> bool:
"""检查Redis连接"""
print("\n检查Redis连接...")
print("⚠️ 请确保已配置Redis连接信息并在jeecg-boot项目中正确配置")
print("⚠️ 此检查需要根据实际项目配置进行验证")
print("⚠️ 配置文件位置: jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/application-dev.yml")
return True
def check_mysql_connection() -> bool:
"""检查MySQL连接"""
print("\n检查MySQL连接...")
print("⚠️ 请确保已配置MySQL连接信息并在jeecg-boot项目中正确配置")
print("⚠️ 此检查需要根据实际项目配置进行验证")
print("⚠️ 配置文件位置: jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/application-dev.yml")
return True
def print_mysql_config():
"""打印MySQL配置并提示需要修改的位置"""
print("\nMySQL配置参考 (请检查以下配置是否正确):")
print("""
spring.datasource.dynamic.datasource:
master:
url: jdbc:mysql://127.0.0.1:3306/jeecg-boot?characterEncoding=UTF-8&useUnicode=true&useSSL=false&tinyInt1isBit=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai
username: root # ← 可能需要修改
password: root # ← 可能需要修改
driver-class-name: com.mysql.cj.jdbc.Driver
""")
def check_ai_vector_db() -> bool:
"""检查AI向量库(pgvector)配置"""
print("\n检查AI知识库向量库配置...")
print("⚠️ 如果需要使用AI知识库功能请配置pgvector向量库")
print("⚠️ 配置文件位置: jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/application-dev.yml")
print("\n配置参考:")
print("""
jeecg.ai-rag:
embed-store:
host: 127.0.0.1 # ← 可能需要修改
port: 5432 # ← 可能需要修改
database: postgres # ← 可能需要修改
user: postgres # ← 可能需要修改
password: postgres # ← 可能需要修改
table: embeddings # ← 可能需要修改
""")
print("⚠️ 注意: 请确保已安装PostgreSQL并添加pgvector扩展docker安装参考https://help.jeecg.com/aigc/config")
return True
def check_ai_config() -> bool:
"""检查AI账号配置"""
print("\n检查AI功能配置...")
print("⚠️ 如果需要使用AI聊天功能请配置AI账号信息")
print("⚠️ 配置文件位置: jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/application-dev.yml")
print("\n配置参考:")
print("""
jeecg:
# AI集成
ai-chat:
enabled: true # ← 启用AI功能
model: deepseek-chat # ← 模型名称
apiKey: ?? # ← 必须修改为您的API Key
apiHost: https://api.deepseek.com/v1 # ← API地址
timeout: 60 # ← 超时时间(秒)
""")
print("⚠️ 注意: 请确保已获取有效的API Key并正确配置AI账号注册获取参考 https://help.jeecg.com/java/deepSeekSupport")
return True
def print_redis_config():
"""打印Redis配置并提示需要修改的位置"""
print("\nRedis配置参考 (请检查以下配置是否正确):")
print("""
spring.redis:
database: 0
host: 127.0.0.1 # ← 可能需要修改
port: 6379 # ← 可能需要修改
password: '' # ← 如果需要密码请修改
""")
def main():
print("="*50)
print("JeecgBoot 运行环境检查脚本")
print("="*50)
all_checks_passed = True
# 检查各项依赖
if not check_java():
all_checks_passed = False
if not check_maven():
all_checks_passed = False
if not check_node():
all_checks_passed = False
if not check_pnpm():
all_checks_passed = False
# 数据库提示
print("="*50)
check_redis_connection()
print_redis_config()
print("="*50)
check_mysql_connection()
print_mysql_config()
print("="*50)
check_ai_config()
print("="*50)
check_ai_vector_db()
print("\n" + "="*50)
if all_checks_passed:
print("✅ 所有基础环境检查通过")
print("⚠️ 注意: 请确保Redis和MySQL、AI账号、向量库pgvector 已正确配置并连接成功")
else:
print("❌ 部分环境检查未通过,请根据上述提示解决问题")
print("="*50)
if __name__ == "__main__":
main()
input("\n按回车键退出...") # 等待用户输入

View File

@ -18,33 +18,20 @@ services:
--max_allowed_packet=128M
--default-authentication-plugin=caching_sha2_password
ports:
- 13306:3306
- 3306:3306
networks:
- jeecg-boot
jeecg-boot-redis:
image: registry.cn-hangzhou.aliyuncs.com/jeecgdocker/redis:5.0
# ports:
# - 6379:6379
ports:
- 6379:6379
restart: always
hostname: jeecg-boot-redis
container_name: jeecg-boot-redis
networks:
- jeecg-boot
jeecg-boot-pgvector:
image: registry.cn-hangzhou.aliyuncs.com/jeecgdocker/pgvector
container_name: jeecg-boot-pgvector
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: vector_db
ports:
- 5432:5432
restart: always
networks:
- jeecg-boot
jeecg-boot-nacos:
restart: always
build:
@ -109,37 +96,31 @@ services:
# environment:
# RABBITMQ_DEFAULT_USER: guest
# RABBITMQ_DEFAULT_PASS: guest
# jeecg-boot-sentinel:
# restart: on-failure
# build:
# context: ./jeecg-visual/jeecg-cloud-sentinel
# ports:
# - 9000:9000
# depends_on:
# - jeecg-boot-nacos
# - jeecg-boot-demo
# - jeecg-boot-system
# - jeecg-boot-gateway
# container_name: jeecg-boot-sentinel
# hostname: jeecg-boot-sentinel
#
# jeecg-boot-xxljob:
# build:
# context: ./jeecg-visual/jeecg-cloud-xxljob
# ports:
# - 9080:9080
# container_name: jeecg-boot-xxljob
# hostname: jeecg-boot-xxljob
jeecg-boot-sentinel:
restart: on-failure
build:
context: ./jeecg-boot/jeecg-server-cloud/jeecg-visual/jeecg-cloud-sentinel
ports:
- 9000:9000
depends_on:
- jeecg-boot-nacos
- jeecg-boot-demo
- jeecg-boot-system
- jeecg-boot-gateway
container_name: jeecg-boot-sentinel
hostname: jeecg-boot-sentinel
networks:
- jeecg-boot
jeecg-boot-xxljob:
build:
context: ./jeecg-boot/jeecg-server-cloud/jeecg-visual/jeecg-cloud-xxljob
ports:
- 9080:9080
container_name: jeecg-boot-xxljob
hostname: jeecg-boot-xxljob
networks:
- jeecg-boot
jeecg-vue:
build:
context: ./jeecgboot-vue3
dockerfile: Dockerfile.cloud
container_name: jeecgboot-vue3-nginx
image: jeecgboot-vue3
depends_on:

View File

@ -18,33 +18,20 @@ services:
--max_allowed_packet=128M
--default-authentication-plugin=caching_sha2_password
ports:
- 13306:3306
- 3306:3306
networks:
- jeecg-boot
jeecg-boot-redis:
image: registry.cn-hangzhou.aliyuncs.com/jeecgdocker/redis:5.0
# ports:
# - 3792:6379
ports:
- 6379:6379
restart: always
hostname: jeecg-boot-redis
container_name: jeecg-boot-redis
networks:
- jeecg-boot
jeecg-boot-pgvector:
image: registry.cn-hangzhou.aliyuncs.com/jeecgdocker/pgvector
container_name: jeecg-boot-pgvector
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: vector_db
ports:
- 5432:5432
restart: always
networks:
- jeecg-boot
jeecg-boot-system:
build:
context: ./jeecg-boot/jeecg-module-system/jeecg-system-start

View File

@ -13,3 +13,4 @@ os_del.cmd
os_del_doc.cmd
.svn
derby.log
*.log

View File

@ -2,12 +2,12 @@
JeecgBoot 低代码开发平台
===============
当前最新版本: 3.9.0发布日期2025-12-01
当前最新版本: 3.7.3发布日期2025-02-10
[![AUR](https://img.shields.io/badge/license-Apache%20License%202.0-blue.svg)](https://github.com/zhangdaiscott/jeecg-boot/blob/master/LICENSE)
[![](https://img.shields.io/badge/Author-北京国炬软件-orange.svg)](http://jeecg.com/aboutusIndex)
[![](https://img.shields.io/badge/version-3.9.0-brightgreen.svg)](https://github.com/zhangdaiscott/jeecg-boot)
[![](https://img.shields.io/badge/version-3.7.3-brightgreen.svg)](https://github.com/zhangdaiscott/jeecg-boot)
[![GitHub stars](https://img.shields.io/github/stars/zhangdaiscott/jeecg-boot.svg?style=social&label=Stars)](https://github.com/zhangdaiscott/jeecg-boot)
[![GitHub forks](https://img.shields.io/github/forks/zhangdaiscott/jeecg-boot.svg?style=social&label=Fork)](https://github.com/zhangdaiscott/jeecg-boot)
@ -16,127 +16,43 @@ JeecgBoot 低代码开发平台
项目介绍
-----------------------------------
<h3 align="center">企业级AI低代码平台</h3>
JeecgBoot 是一款基于BPM流程和代码生成的AI低代码平台助力企业快速实现低代码开发和构建AI应用。
采用前后端分离架构Ant Design&Vue3SpringBoot3SpringCloud AlibabaMybatis-plus强大代码生成器实现前后端一键生成无需手写代码。
平台引领AI低代码开发模式AI生成→在线编码→代码生成→手工合并解决Java项目80%重复工作,提升效率,节省成本,兼顾灵活性。
具备强大且颗粒化的权限控制支持按钮权限和数据权限设置满足大型业务系统需求。功能涵盖在线表单、表单设计、流程设计、门户设计、报表与大屏设计、OA办公、AI应用、AI知识库、大模型管理、AI流程编排、AI聊天支持ChatGPT、DeepSeek、Ollama等多种AI大模型。
`AI赋能报表:` 积木报表是一款自主研发的强大开源企业级Web报表与大屏工具。它通过零编码的拖拽式操作赋能用户如同搭积木般轻松构建各类复杂报表和数据大屏全面满足企业数据可视化与分析需求助力企业级数据产品的高效打造与应用。
`AI赋能低代码:` 提供完善成熟的AI应用平台涵盖AI应用管理、AI模型管理、智能对话助手、知识库问答、流程编排与设计器、AI建表等多项功能。平台兼容多种主流大模型包括ChatGPT、DeepSeek、Ollama、智普、千问等助力企业高效构建智能化应用推动低代码开发与AI深度融合。
`JEECG宗旨是:` JEECG旨在通过OnlineCoding平台实现简单功能的零代码快速搭建同时针对复杂功能采用代码生成器生成代码并手工合并打造智能且灵活的低代码开发模式有效解决了当前低代码产品普遍缺乏灵活性的问题提升开发效率的同时兼顾系统的扩展性和定制化能力。
`JEECG业务流程:` JEECG业务流程采用BPM工作流引擎实现业务审批扩展任务接口供开发人员编写业务逻辑表单提供表单设计器、在线配置表单和编码表单等多种解决方案。通过流程与表单的分离设计松耦合及任务节点的灵活配置既保障了企业流程的安全性与保密性又大幅降低了开发人员的工作量。
适用项目
-----------------------------------
JeecgBoot低代码平台兼容所有J2EE项目开发支持信创国产化特别适用于SAAS、企业信息管理系统MIS、内部办公系统OA、企业资源计划系统ERP、客户关系管理系统CRM及AI知识库等场景。其半智能手工Merge开发模式可显著提升70%以上的开发效率极大降低开发成本。同时JeecgBoot还是一款全栈式AI开发平台助力企业快速构建和部署个性化AI应用。。
**信创兼容说明**
- 操作系统:国产麒麟、银河麒麟等国产系统几乎都是基于 Linux 内核,因此它们具有良好的兼容性。
- 数据库达梦、人大金仓、TiDB
- 中间件:东方通 TongWeb、TongRDS宝兰德 AppServer、CacheDB, [信创配置文档](https://help.jeecg.com/java/tongweb-deploy/)
<h3 align="center">Java Low Code Platform for Enterprise web applications</h3>
JeecgBoot 是一款基于代码生成器的`低代码开发平台`!前后端分离架构 SpringBoot2.x和3.xSpringCloudAnt Design Vue3Mybatis-plusShiroJWT支持微服务。强大的代码生成器让前后端代码一键生成实现低代码开发! JeecgBoot 引领新的低代码开发模式(OnlineCoding-> 代码生成器-> 手工MERGE) 帮助解决Java项目70%的重复工作,让开发更多关注业务。既能快速提高效率,节省研发成本,同时又不失灵活性!
#### 项目说明
| 项目名 | 说明 |
|--------------------|------------------------------------|
| `jeecg-boot` | 后端源码JAVASpringBoot3微服务架构) |
| `jeecgboot-vue3` | 前端源码VUE3vue3+vite6+antd4+ts最新技术栈 |
| 项目名 | 说明 |
|--------------------|------------------------|
| `jeecg-boot` | 后端源码JAVASpringBoot微服务架构 |
| `jeecgboot-vue3` | 前端源码VUE3vue3+vite5+ts最新技术栈 |
启动项目
-----------------------------------
> 默认账号密码: admin/123456
- [开发环境搭建](https://help.jeecg.com/java/setup/tools)
- [IDEA启动前后端(单体模式)](https://help.jeecg.com/java/setup/idea/startup)
- [Docker一键启动(单体模式)](https://help.jeecg.com/java/docker/quick)
- [IDEA启动前后端(微服务方式)](https://help.jeecg.com/java/springcloud/switchcloud/monomer)
- [Docker一键启动(微服务方式)](https://help.jeecg.com/java/docker/quickcloud)
技术文档
-----------------------------------
- 官方网站: [http://www.jeecg.com](http://www.jeecg.com)
- 在线演示 [平台演示](https://boot3.jeecg.com) | [APP演示](https://jeecg.com/appIndex)
- 入门指南: [快速入门](http://www.jeecg.com/doc/quickstart) | [代码生成使用](https://help.jeecg.com/java/codegen/online) | [开发文档](https://help.jeecg.com) | [AI应用手册](https://help.jeecg.com/aigc) | [视频教程](http://jeecg.com/doc/video)
- 技术支持 [反馈问题](https://github.com/jeecgboot/JeecgBoot/issues/new?template=bug_report.md) | [低代码体验一分钟](https://jeecg.blog.csdn.net/article/details/106079007)
- QQ交流群 964611995、⑩716488839(满)、⑨808791225(满)、其他(满)
- 新手指南 [快速入门](http://www.jeecg.com/doc/quickstart)
- QQ交流群 ⑩716488839、⑨808791225、其他(满)
- 在线演示 [在线演示](http://boot3.jeecg.com) | [APP演示](http://jeecg.com/appIndex)
> 演示系统的登录账号密码,请点击 [获取账号密码](http://jeecg.com/doc/demo) 获取
AI 应用平台介绍
启动项目
-----------------------------------
一个全栈式 AI 开发平台,旨在帮助开发者快速构建和部署个性化的 AI 应用。
JeecgBoot平台提供了一套完善的AI应用管理系统模块是一套类似`Dify``AIGC应用开发平台`+`知识库问答`是一款基于LLM大语言模型AI应用平台和 RAG 的知识库问答系统。
其直观的界面结合了 AI 流程编排、RAG 管道、知识库管理、模型管理、对接向量库、实时运行可观察等让您可以快速从原型到生产拥有AI服务能力。
- [详细专题介绍,请点击查看](README-AI.md)
- AI视频介绍
[![](https://jeecgos.oss-cn-beijing.aliyuncs.com/files/jeecg_aivideo.png)](https://www.bilibili.com/video/BV1zmd7YFE4w)
- [IDEA启动前后端项目](https://help.jeecg.com/java/setup/idea/startup.html)
- [Docker一键启动前后端](https://help.jeecg.com/java/docker/quick.html)
为什么选择JeecgBoot?
微服务启动
-----------------------------------
- 1.采用最新主流前后分离框架Spring Boot3 + MyBatis + Shiro/SpringAuthorizationServer + Ant Design4 + Vue3容易上手代码生成器依赖性低灵活的扩展能力可快速实现二次开发。
- 2.前端大版本换代,最新版采用 Vue3.0 + TypeScript + Vite6 + Ant Design Vue4 等新技术方案。
- 3.支持微服务Spring Cloud AlibabaNacos、Gateway、Sentinel、Skywalking提供简易机制支持单体和微服务自由切换这样可以满足各类项目需求
- 4.开发效率高支持在线建表和AI建表提供强大代码生成器单表、树列表、一对多、一对一等数据模型增删改查功能一键生成菜单配置直接使用。
- 5.代码生成器提供强大模板机制,支持自定义模板,目前提供四套风格模板(单表两套、树模型一套、一对多三套)。
- 6.提供强大的报表和大屏可视化工具,支持丰富的数据源连接,能够通过拖拉拽方式快速制作报表、大屏和门户设计;支持多种图表类型:柱形图、折线图、散点图、饼图、环形图、面积图、漏斗图、进度图、仪表盘、雷达图、地图等。
- 7.低代码能力在线表单无需编码通过在线配置表单实现表单的增删改查支持单表、树、一对多、一对一等模型实现人人皆可编码在线配置零代码开发、所见即所得支持23种类控件。
- 8.低代码能力:在线报表、在线图表(无需编码,通过在线配置方式,实现数据报表和图形报表,可以快速抽取数据,减轻开发压力,实现人人皆可编码)。
- 9.Online支持在线增强开发提供在线代码编辑器支持代码高亮、代码提示等功能支持多种语言Java、SQL、JavaScript等
- 10.封装完善的用户、角色、菜单、组织机构、数据字典、在线定时任务等基础功能,支持访问授权、按钮权限、数据权限等功能。
- 11.前端UI提供丰富的组件库支持各种常用组件如表格、树形控件、下拉框、日期选择器等满足各种复杂的业务需求 [UI组件库文档](https://help.jeecg.com/category/ui%E7%BB%84%E4%BB%B6%E5%BA%93)。
- 12.提供APP配套框架一份多代码多终端适配一份代码多终端适配小程序、H5、安卓、iOS、鸿蒙Next。
- 13.新版APP框架采用Uniapp、Vue3.0、Vite、Wot-design-uni、TypeScript等最新技术栈包括二次封装组件、路由拦截、请求拦截等功能。实现了与JeecgBoot完美对接目前已经实现登录、用户信息、通讯录、公告、移动首页、九宫格、聊天、Online表单、仪表盘等功能提供了丰富的组件。
- 14.提供了一套成熟的AI应用平台功能从AI模型、知识库到AI应用搭建助力企业快速落地AI服务加速智能化升级。
- 15.AI能力目前JeecgBoot支持AI大模型chatgpt和deepseek现在最新版默认使用deepseek速度更快质量更高。目前提供了AI对话助手、AI知识库、AI应用、AI建表、AI报表等功能。
- 16.提供新行编辑表格JVXETable轻松满足各种复杂ERP布局拥有更高的性能、更灵活的扩展、更强大的功能。
- 17.平台首页风格,提供多种组合模式,支持自定义风格;支持门户设计,支持自定义首页。
- 18.常用共通封装各种工具类定时任务、短信接口、邮件发送、Excel导入导出等基本满足80%项目需求。
- 19.简易Excel导入导出支持单表导出和一对多表模式导出生成的代码自带导入导出功能。
- 20.集成智能报表工具报表打印、图像报表和数据导出非常方便可极其方便地生成PDF、Excel、Word等报表。
- 21.采用前后分离技术页面UI风格精美针对常用组件做了封装时间、行表格控件、截取显示控件、报表组件、编辑器等。
- 22.查询过滤器查询功能自动生成后台动态拼SQL追加查询条件支持多种匹配方式全匹配/模糊查询/包含查询/不匹配查询)。
- 23.数据权限(精细化数据权限控制,控制到行级、列表级、表单字段级,实现不同人看不同数据,不同人对同一个页面操作不同字段)。
- 24.接口安全机制可细化控制接口授权非常简便实现不同客户端只看自己数据等控制也提供了基于AK和SK认证鉴权的OpenAPI功能。
- 25.活跃的社区支持;近年来,随着网络威胁的日益增加,团队在安全和漏洞管理方面积累了丰富的经验,能够为企业提供全面的安全解决方案。
- 26.权限控制采用RBACRole-Based Access Control基于角色的访问控制
- 27.页面校验自动生成(必须输入、数字校验、金额校验、时间空间等)。
- 28.支持SaaS服务模式提供SaaS多租户架构方案。
- 29.分布式文件服务集成MinIO、阿里OSS等优秀的第三方提供便捷的文件上传与管理同时也支持本地存储。
- 30.主流数据库兼容一套代码完全兼容MySQL、PostgreSQL、Oracle、SQL Server、MariaDB、达梦、人大金仓等主流数据库。
- 31.集成工作流Flowable并实现了只需在页面配置流程转向可极大简化BPM工作流的开发用BPM的流程设计器画出了流程走向一个工作流基本就完成了只需写很少量的Java代码。
- 32.低代码能力在线流程设计采用开源Flowable流程引擎实现在线画流程、自定义表单、表单挂靠、业务流转。
- 33.多数据源:极其简易的使用方式,在线配置数据源配置,便捷地从其他数据抓取数据。
- 34.提供单点登录CAS集成方案项目中已经提供完善的对接代码。
- 35.低代码能力表单设计器支持用户自定义表单布局支持单表、一对多表单支持select、radio、checkbox、textarea、date、popup、列表、宏等控件。
- 36.专业接口对接机制统一采用RESTful接口方式集成Swagger-UI在线接口文档JWT token安全验证方便客户端对接。
- 37.高级组合查询功能,在线配置支持主子表关联查询,可保存查询历史。
- 38.提供各种系统监控实时跟踪系统运行情况监控Redis、Tomcat、JVM、服务器信息、请求追踪、SQL监控
- 39.消息中心支持短信、邮件、微信推送等集成WebSocket消息通知机制。
- 40.支持多语言,提供国际化方案。
- 41.数据变更记录日志,可记录数据每次变更内容,通过版本对比功能查看历史变化。
- 42.提供简单易用的打印插件支持谷歌、火狐、IE11+等各种浏览器。
- 43.后端采用Maven分模块开发方式前端支持菜单动态路由。
- 44.提供丰富的示例代码,涵盖了常用的业务场景,便于学习和参考。
- [单体快速切换微服务](https://help.jeecg.com/java/springcloud/switchcloud/monomer.html)
- [Docker启动微服务后台](https://help.jeecg.com/java/docker/springcloud.html)
技术架构:
@ -145,33 +61,28 @@ JeecgBoot平台提供了一套完善的AI应用管理系统模块是一套类
#### 后端
- IDE建议 IDEA (必须安装lombok插件 )
- 语言Java 默认jdk17(jdk21、jdk24)
- 语言Java 8+ (支持17)
- 依赖管理Maven
- 基础框架Spring Boot 3.5.5
- 微服务框架: Spring Cloud Alibaba 2023.0.3.3
- 持久层框架MybatisPlus 3.5.12
- 报表工具: JimuReport 2.1.3
- 安全框架Apache Shiro 2.0.4Jwt 4.5.0
- 基础框架Spring Boot 2.7.18
- 微服务框架: Spring Cloud Alibaba 2021.0.1.0
- 持久层框架MybatisPlus 3.5.3.2
- 报表工具: JimuReport 1.8.1
- 安全框架Apache Shiro 1.12.0Jwt 3.11.0
- 微服务技术栈Spring Cloud Alibaba、Nacos、Gateway、Sentinel、Skywalking
- 数据库连接池阿里巴巴Druid 1.2.24
- AI大模型支持 `ChatGPT` `DeepSeek` `千问`等各种常规模式
- 数据库连接池阿里巴巴Druid 1.1.22
- 日志打印logback
- 缓存Redis
- 其他autopoi, fastjsonpoiSwagger-uiquartz, lombok简化代码等。
- 默认提供MySQL5.7+数据库脚本
- 默认数据库脚本:MySQL5.7+
- [其他数据库,需要自己转](https://my.oschina.net/jeecg/blog/4905722)
#### 前端
- 前端环境要求Node.js要求`Node 20+` 版本以上、pnpm 要求`9+` 版本以上
` ( Vite 不再支持已结束生命周期EOL的 Node.js 18。现在需要使用 Node.js 20.19+ 或 22.12+)`
- 前端IDE建议WebStorm、Vscode
- 采用 Vue3.0+TypeScript+Vite+Ant-Design-Vue等新技术方案包括二次封装组件、utils、hooks、动态菜单、权限校验、按钮级别权限控制等功能
- 最新技术栈Vue3.0 + TypeScript + Vite5 + ant-design-vue4 + pinia + echarts + unocss + vxe-table + qiankun + es6
- 依赖管理node、npm、pnpm
- 前端IDE建议IDEA、WebStorm、Vscode
- 采用 Vue3.0+TypeScript+Vite6+Ant-Design-Vue4等新技术方案包括二次封装组件、utils、hooks、动态菜单、权限校验、按钮级别权限控制等功能
- 最新技术栈Vue3.0 + TypeScript + Vite6 + ant-design-vue4 + pinia + echarts + unocss + vxe-table + qiankun + es6
@ -202,7 +113,7 @@ JeecgBoot平台提供了一套完善的AI应用管理系统模块是一套类
- 6、分布式文件 Minio、阿里OSS √
- 7、统一权限控制 JWT + Shiro √
- 8、服务监控 SpringBootAdmin√
- 9、链路跟踪 Skywalking [参考文档](https://help.jeecg.com/java/springcloud/super/skywarking)
- 9、链路跟踪 Skywalking [参考文档](https://help.jeecg.com/java/springcloud/super/skywarking.html)
- 10、消息中间件 RabbitMQ √
- 11、分布式任务 xxl-job √
- 12、分布式事务 Seata

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,5 +1,5 @@
#
# XXL-JOB v2.4.0
# XXL-JOB v2.2.0
# Copyright (c) 2015-present, xuxueli.
CREATE database if NOT EXISTS `xxl_job` default character set utf8mb4 collate utf8mb4_general_ci;

View File

@ -1,5 +0,0 @@
oracle导出编码 export NLS_LANG=AMERICAN_AMERICA.ZHS16GBK
导出用户: jeecgbootos
导入命令: imp scott/tiger@orcl file=jeecgboot-oracle11g.dmp

View File

@ -0,0 +1,45 @@
CREATE TABLE `oauth2_registered_client` (
`id` varchar(100) NOT NULL,
`client_id` varchar(100) NOT NULL,
`client_id_issued_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`client_secret` varchar(200) DEFAULT NULL,
`client_secret_expires_at` timestamp NULL DEFAULT NULL,
`client_name` varchar(200) NOT NULL,
`client_authentication_methods` varchar(1000) NOT NULL,
`authorization_grant_types` varchar(1000) NOT NULL,
`redirect_uris` varchar(1000) DEFAULT NULL,
`post_logout_redirect_uris` varchar(1000) DEFAULT NULL,
`scopes` varchar(1000) NOT NULL,
`client_settings` varchar(2000) NOT NULL,
`token_settings` varchar(2000) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
INSERT INTO `oauth2_registered_client`
(`id`,
`client_id`,
`client_id_issued_at`,
`client_secret`,
`client_secret_expires_at`,
`client_name`,
`client_authentication_methods`,
`authorization_grant_types`,
`redirect_uris`,
`post_logout_redirect_uris`,
`scopes`,
`client_settings`,
`token_settings`)
VALUES
('3eacac0e-0de9-4727-9a64-6bdd4be2ee1f',
'jeecg-client',
now(),
'secret',
null,
'3eacac0e-0de9-4727-9a64-6bdd4be2ee1f',
'client_secret_basic',
'refresh_token,authorization_code,password,app,phone,social',
'http://127.0.0.1:8080/jeecg-',
'http://127.0.0.1:8080/',
'*',
'{"@class":"java.util.Collections$UnmodifiableMap","settings.client.require-proof-key":false,"settings.client.require-authorization-consent":true}',
'{"@class":"java.util.Collections$UnmodifiableMap","settings.token.reuse-refresh-tokens":true,"settings.token.id-token-signature-algorithm":["org.springframework.security.oauth2.jose.jws.SignatureAlgorithm","RS256"],"settings.token.access-token-time-to-live":["java.time.Duration",300000.000000000],"settings.token.access-token-format":{"@class":"org.springframework.security.oauth2.server.authorization.settings.OAuth2TokenFormat","value":"self-contained"},"settings.token.refresh-token-time-to-live":["java.time.Duration",3600.000000000],"settings.token.authorization-code-time-to-live":["java.time.Duration",300000.000000000],"settings.token.device-code-time-to-live":["java.time.Duration",300000.000000000]}');

View File

@ -18,52 +18,24 @@ services:
--max_allowed_packet=128M
--default-authentication-plugin=caching_sha2_password
ports:
- 13306:3306
- 3306:3306
networks:
- jeecg-boot
jeecg-boot-redis:
image: registry.cn-hangzhou.aliyuncs.com/jeecgdocker/redis:5.0
# ports:
# - 6379:6379
ports:
- 6379:6379
restart: always
hostname: jeecg-boot-redis
container_name: jeecg-boot-redis
networks:
- jeecg-boot
jeecg-boot-pgvector:
image: registry.cn-hangzhou.aliyuncs.com/jeecgdocker/pgvector
container_name: jeecg-boot-pgvector
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: vector_db
# ports:
# - 5432:5432
restart: always
networks:
- jeecg-boot
# jeecg-boot-rabbitmq:
# image: rabbitmq:3.7.7-management
## ports:
## - 5672:5672
## - 15672:15672
# restart: always
# container_name: jeecg-boot-rabbitmq
# hostname: jeecg-boot-rabbitmq
# environment:
# RABBITMQ_DEFAULT_USER: guest
# RABBITMQ_DEFAULT_PASS: guest
# networks:
# - jeecg-boot
jeecg-boot-system:
build:
context: ./jeecg-module-system/jeecg-system-start
restart: on-failure
mac_address: 02:42:ac:11:00:02
depends_on:
- jeecg-boot-mysql
- jeecg-boot-redis
@ -74,8 +46,6 @@ services:
- 8080:8080
networks:
- jeecg-boot
volumes:
- ./config:/jeecg-boot/config
networks:
jeecg-boot:

View File

@ -2,13 +2,17 @@
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.jeecgframework.boot3</groupId>
<groupId>org.jeecgframework.boot</groupId>
<artifactId>jeecg-boot-parent</artifactId>
<version>3.9.0</version>
<version>3.7.3</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>jeecg-boot-base-core</artifactId>
<properties>
<spring-boot.version>3.1.5</spring-boot.version>
</properties>
<repositories>
<repository>
<id>aliyun</id>
@ -42,19 +46,23 @@
<dependencies>
<!--jeecg-tools-->
<dependency>
<groupId>org.jeecgframework.boot3</groupId>
<artifactId>jeecg-boot-common</artifactId>
<exclusions>
<exclusion>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
</exclusion>
</exclusions>
<groupId>org.jeecgframework.boot</groupId>
<artifactId>jeecg-boot-common3</artifactId>
</dependency>
<!--集成springmvc框架并实现自动配置 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-undertow</artifactId>
</dependency>
<!-- websocket -->
<dependency>
@ -104,19 +112,9 @@
<!-- mybatis-plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-jsqlparser-4.9</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<!-- minidao -->
<dependency>
<groupId>org.jeecgframework.boot3</groupId>
<artifactId>minidao-spring-boot-starter-jsqlparser-4.9</artifactId>
</dependency>
<!-- druid -->
<dependency>
@ -143,7 +141,7 @@
<!-- sqlserver-->
<dependency>
<groupId>com.microsoft.sqlserver</groupId>
<artifactId>mssql-jdbc</artifactId>
<artifactId>sqljdbc4</artifactId>
<version>${sqljdbc4.version}</version>
<scope>runtime</scope>
</dependency>
@ -165,13 +163,13 @@
<dependency>
<groupId>org.jeecgframework</groupId>
<artifactId>kingbase8</artifactId>
<version>${kingbase8.version}</version>
<version>9.0.0</version>
<scope>runtime</scope>
</dependency>
<!--达梦数据库驱动 版本号1-3-26-2023.07.26-197096-20046-ENT -->
<dependency>
<groupId>com.dameng</groupId>
<artifactId>DmJdbcDriver18</artifactId>
<artifactId>Dm8JdbcDriver18</artifactId>
<version>${dm8.version}</version>
</dependency>
<dependency>
@ -179,6 +177,8 @@
<artifactId>DmDialect-for-hibernate5.0</artifactId>
<version>${dm8.version}</version>
</dependency>
<!-- Quartz定时任务 -->
<dependency>
<groupId>org.springframework.boot</groupId>
@ -192,87 +192,26 @@
<version>${java-jwt.version}</version>
</dependency>
<!--shiro-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-starter</artifactId>
<classifier>jakarta</classifier>
<version>${shiro.version}</version>
<exclusions>
<exclusion>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<classifier>jakarta</classifier>
<version>${shiro.version}</version>
<!-- 排除仍使用了javax.servlet的依赖 -->
<exclusions>
<exclusion>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
</exclusion>
<exclusion>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-web</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- 引入适配jakarta的依赖包 -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<classifier>jakarta</classifier>
<version>${shiro.version}</version>
<exclusions>
<exclusion>
<groupId>commons-beanutils</groupId>
<artifactId>commons-beanutils</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-web</artifactId>
<classifier>jakarta</classifier>
<version>${shiro.version}</version>
<exclusions>
<exclusion>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- shiro-redis -->
<dependency>
<groupId>org.crazycake</groupId>
<artifactId>shiro-redis</artifactId>
<version>${shiro-redis.version}</version>
<exclusions>
<exclusion>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
</exclusion>
<exclusion>
<artifactId>checkstyle</artifactId>
<groupId>com.puppycrawl.tools</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-openapi3-ui</artifactId>
<version>${knife4j-spring-boot-starter.version}</version>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-authorization-server</artifactId>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.7.0</version>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
<!-- 添加spring security cas支持 -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-cas</artifactId>
</dependency>
<!-- knife4j -->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
<version>${knife4j-spring-boot-starter.version}</version>
</dependency>
<!-- 代码生成器 -->
@ -281,22 +220,23 @@
<groupId>org.jeecgframework.boot</groupId>
<artifactId>codegenerate</artifactId>
<version>${codegenerate.version}</version>
<exclusions>
<exclusion>
<artifactId>commons-io</artifactId>
<groupId>commons-io</groupId>
</exclusion>
<exclusion>
<artifactId>mysql-connector-java</artifactId>
<groupId>mysql</groupId>
</exclusion>
</exclusions>
</dependency>
<!-- AutoPoi Excel工具类-->
<dependency>
<groupId>org.jeecgframework</groupId>
<artifactId>autopoi-spring-boot-3-starter</artifactId>
<groupId>org.jeecgframework.boot3</groupId>
<artifactId>autopoi-web</artifactId>
<version>${autopoi-web.version}</version>
<exclusions>
<exclusion>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
</exclusion>
<exclusion>
<artifactId>xercesImpl</artifactId>
<groupId>xerces</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>xerces</groupId>
@ -309,20 +249,6 @@
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<exclusions>
<exclusion>
<artifactId>checker-qual</artifactId>
<groupId>org.checkerframework</groupId>
</exclusion>
<exclusion>
<groupId>com.google.errorprone</groupId>
<artifactId>error_prone_annotations</artifactId>
</exclusion>
<exclusion>
<groupId>org.apache.commons</groupId>
<artifactId>commons-compress</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- 阿里云短信 -->
@ -374,26 +300,11 @@
<groupId>cn.hutool</groupId>
<artifactId>hutool-crypto</artifactId>
</dependency>
<!-- chatgpt -->
<dependency>
<groupId>org.jeecgframework.boot3</groupId>
<artifactId>jeecg-boot-starter-chatgpt</artifactId>
<groupId>org.jeecgframework.boot</groupId>
<artifactId>jeecg-boot-starter3-chatgpt</artifactId>
</dependency>
<!-- 腾讯云 -->
<dependency>
<groupId>com.tencentcloudapi</groupId>
<artifactId>tencentcloud-sdk-java-sms</artifactId>
<version>${tencentcloud-sdk-java-sms.version}</version>
<exclusions>
<exclusion>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
</exclusion>
<exclusion>
<groupId>com.squareup.okio</groupId>
<artifactId>okio</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,21 @@
package org.apache.shiro;
import org.apache.shiro.subject.Subject;
/**
* 兼容处理Online功能使用处理请勿修改
* @author eightmonth@qq.com
* @date 2024/4/29 14:05
*/
public class SecurityUtils {
public static Subject getSubject() {
return new Subject() {
@Override
public Object getPrincipal() {
return Subject.super.getPrincipal();
}
};
}
}

View File

@ -0,0 +1,14 @@
package org.apache.shiro.subject;
import org.jeecg.config.security.utils.SecureUtil;
/**
* 兼容处理Online功能使用处理请勿修改
* @author eightmonth@qq.com
* @date 2024/4/29 14:18
*/
public interface Subject {
default Object getPrincipal() {
return SecureUtil.currentUser();
}
}

View File

@ -1,6 +1,6 @@
package org.jeecg.common.api;
import org.jeecg.common.api.dto.AiragFlowDTO;
import com.alibaba.fastjson.JSONObject;
import org.jeecg.common.system.vo.*;
import java.util.List;
@ -65,6 +65,13 @@ public interface CommonAPI {
*/
public String getUserIdByName(String username);
/**
* 5根据用户手机号查询用户信息
* @param username
* @return
*/
public LoginUser getUserByPhone(String phone);
/**
* 6字典表的 翻译
@ -132,6 +139,7 @@ public interface CommonAPI {
*/
Map<String, List<DictModel>> translateManyDict(String dictCodes, String keys);
//update-begin---author:chenrui ---date:20231221 for[issues/#5643]解决分布式下表字典跨库无法查询问题------------
/**
* 15 字典表的 翻译,可批量
* @param table
@ -142,16 +150,33 @@ public interface CommonAPI {
* @return
*/
List<DictModel> translateDictFromTableByKeys(String table, String text, String code, String keys, String dataSource);
//update-end---author:chenrui ---date:20231221 for[issues/#5643]解决分布式下表字典跨库无法查询问题------------
/**
* 16 运行AIRag流程
* for [QQYUN-13634]在baseapi里面封装方法方便其他模块调用
*
* @param airagFlowDTO
* @return 流程执行结果,可能是String或者Map
* @author chenrui
* @date 2025/9/2 11:43
* 登录加载系统字典
* @return
*/
Object runAiragFlow(AiragFlowDTO airagFlowDTO);
Map<String,List<DictModel>> queryAllDictItems();
/**
* 查询SysDepart集合
* @param userId
* @return
*/
List<SysDepartModel> queryUserDeparts(String userId);
/**
* 根据用户名设置部门ID
* @param username
* @param orgCode
*/
void updateUserDepart(String username,String orgCode,Integer loginTenantId);
/**
* 设置登录租户
* @param username
* @return
*/
JSONObject setLoginTenant(String username);
}

View File

@ -1,36 +0,0 @@
package org.jeecg.common.api.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.util.Map;
/**
* 调用AI流程入参
* for [QQYUN-13634]在baseapi里面封装方法方便其他模块调用
* @author chenrui
* @date 2025/9/2 14:11
*/
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Data
public class AiragFlowDTO implements Serializable {
private static final long serialVersionUID = 7431775881170684867L;
/**
* 流程id
*/
private String flowId;
/**
* 输入参数
*/
private Map<String, Object> inputParams;
}

View File

@ -1,48 +0,0 @@
package org.jeecg.common.api.dto;
import lombok.Data;
import java.io.Serializable;
import java.util.Date;
/**
* 流程审批意见DTO
* @author scott
* @date 2025-01-29
*/
@Data
public class ApprovalCommentDTO implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 任务ID
*/
private String taskId;
/**
* 任务名称
*/
private String taskName;
/**
* 审批人ID
*/
private String approverId;
/**
* 审批人姓名
*/
private String approverName;
/**
* 审批意见
*/
private String approvalComment;
/**
* 审批时间
*/
private Date approvalTime;
}

View File

@ -30,10 +30,12 @@ public class OnlineAuthDTO implements Serializable {
*/
private String onlineFormUrl;
//update-begin---author:chenrui ---date:20240123 for[QQYUN-7992]【online】工单申请下的online表单未配置online表单开发菜单操作报错无权限------------
/**
* online工单的地址
*/
private String onlineWorkOrderUrl;
//update-end---author:chenrui ---date:20240123 for[QQYUN-7992]【online】工单申请下的online表单未配置online表单开发菜单操作报错无权限------------
public OnlineAuthDTO(){

View File

@ -1,55 +0,0 @@
package org.jeecg.common.api.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.util.List;
import java.util.Map;
/**
* 移动端消息推送
* @author liusq
* @date 2025/11/12 14:11
*/
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Data
public class PushMessageDTO implements Serializable {
private static final long serialVersionUID = 7431775881170684867L;
/**
* 消息标题
*/
private String title;
/**
* 消息内容
*/
private String content;
/**
* 推送形式all全推送 single单用户推送
*/
private String pushType;
/**
* 用户名usernameList
*/
List<String> usernames;
/**
* 用户名idList
*/
List<String> userIds;
/**
* 消息附加参数
*/
Map<String,Object> payload;
}

View File

@ -91,12 +91,6 @@ public class MessageDTO implements Serializable {
private Boolean isTimeJob = false;
//---【邮件相关参数】-------------------------------------------------------------
/**
* 枚举org.jeecg.common.constant.enums.NoticeTypeEnum
* 通知类型(system:系统消息、file:知识库、flow:流程、plan:日程计划、meeting:会议)
*/
private String noticeType;
public MessageDTO(){
}

View File

@ -1,7 +1,6 @@
package org.jeecg.common.api.vo;
import com.fasterxml.jackson.annotation.JsonIgnore;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import org.jeecg.common.constant.CommonConstant;

View File

@ -1,5 +1,6 @@
package org.jeecg.common.aspect;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.serializer.PropertyFilter;
import org.apache.shiro.SecurityUtils;
@ -15,12 +16,14 @@ import org.jeecg.common.aspect.annotation.AutoLog;
import org.jeecg.common.constant.CommonConstant;
import org.jeecg.common.constant.enums.ModuleType;
import org.jeecg.common.constant.enums.OperateTypeEnum;
import org.jeecg.config.security.utils.SecureUtil;
import org.jeecg.modules.base.service.BaseCommonService;
import org.jeecg.common.system.vo.LoginUser;
import org.jeecg.common.util.IpUtils;
import org.jeecg.common.util.SpringContextUtils;
import org.jeecg.common.util.oConvertUtils;
import org.springframework.core.StandardReflectionParameterNameDiscoverer;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.validation.BindingResult;
import org.springframework.web.multipart.MultipartFile;
@ -100,7 +103,7 @@ public class AutoLogAspect {
//设置IP地址
dto.setIp(IpUtils.getIpAddr(request));
//获取登录用户信息
LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal();
LoginUser sysUser = SecureUtil.currentUser();
if(sysUser!=null){
dto.setUserid(sysUser.getUsername());
dto.setUsername(sysUser.getRealname());
@ -121,8 +124,9 @@ public class AutoLogAspect {
if (operateType > 0) {
return operateType;
}
// 代码逻辑说明: 阿里云代码扫描规范(不允许任何魔法值出现在代码中)------------
//update-begin---author:wangshuai ---date:20220331 for阿里云代码扫描规范(不允许任何魔法值出现在代码中)------------
return OperateTypeEnum.getTypeByMethodName(methodName);
//update-end---author:wangshuai ---date:20220331 for阿里云代码扫描规范(不允许任何魔法值出现在代码中)------------
}
/**
@ -142,15 +146,14 @@ public class AutoLogAspect {
// https://my.oschina.net/mengzhang6/blog/2395893
Object[] arguments = new Object[paramsArray.length];
for (int i = 0; i < paramsArray.length; i++) {
if (paramsArray[i] instanceof BindingResult || paramsArray[i] instanceof ServletRequest || paramsArray[i] instanceof ServletResponse || paramsArray[i] instanceof MultipartFile || paramsArray[i] instanceof MultipartFile[]) {
if (paramsArray[i] instanceof BindingResult || paramsArray[i] instanceof ServletRequest || paramsArray[i] instanceof ServletResponse || paramsArray[i] instanceof MultipartFile) {
//ServletRequest不能序列化从入参里排除否则报异常java.lang.IllegalStateException: It is illegal to call this method if the current request is not in asynchronous mode (i.e. isAsyncStarted() returns false)
//ServletResponse不能序列化 从入参里排除否则报异常java.lang.IllegalStateException: getOutputStream() has already been called for this response
//MultipartFile和MultipartFile[]不能序列化,从入参里排除
continue;
}
arguments[i] = paramsArray[i];
}
// 代码逻辑说明: 日志数据太长的直接过滤掉
//update-begin-author:taoyan date:20200724 for:日志数据太长的直接过滤掉
PropertyFilter profilter = new PropertyFilter() {
@Override
public boolean apply(Object o, String name, Object value) {
@ -165,13 +168,14 @@ public class AutoLogAspect {
}
};
params = JSONObject.toJSONString(arguments, profilter);
//update-end-author:taoyan date:20200724 for:日志数据太长的直接过滤掉
} else {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
// 请求的方法参数值
Object[] args = joinPoint.getArgs();
// 请求的方法参数名称
StandardReflectionParameterNameDiscoverer u= new StandardReflectionParameterNameDiscoverer();
StandardReflectionParameterNameDiscoverer u=new StandardReflectionParameterNameDiscoverer();
String[] paramNames = u.getParameterNames(method);
if (args != null && paramNames != null) {
for (int i = 0; i < args.length; i++) {

View File

@ -105,24 +105,29 @@ public class DictAspect {
Map<String, List<String>> dataListMap = new HashMap<>(5);
//取出结果集
List<Object> records=((IPage) ((Result) result).getResult()).getRecords();
// 代码逻辑说明: 【VUEN-1230】 判断是否含有字典注解,没有注解返回-----
//update-begin--Author:zyf -- Date:20220606 ----for【VUEN-1230】 判断是否含有字典注解,没有注解返回-----
Boolean hasDict= checkHasDict(records);
if(!hasDict){
return result;
}
log.debug(" __ 进入字典翻译切面 DictAspect —— " );
//update-end--Author:zyf -- Date:20220606 ----for【VUEN-1230】 判断是否含有字典注解,没有注解返回-----
for (Object record : records) {
String json="{}";
try {
//update-begin--Author:zyf -- Date:20220531 ----for【issues/#3629】 DictAspect Jackson序列化报错-----
//解决@JsonFormat注解解析不了的问题详见SysAnnouncement类的@JsonFormat
json = objectMapper.writeValueAsString(record);
//update-end--Author:zyf -- Date:20220531 ----for【issues/#3629】 DictAspect Jackson序列化报错-----
} catch (JsonProcessingException e) {
log.error("json解析失败"+e.getMessage(),e);
}
// 代码逻辑说明: 【issues/3303】restcontroller返回json数据后key顺序错乱 -----
//update-begin--Author:scott -- Date:20211223 ----for【issues/3303】restcontroller返回json数据后key顺序错乱 -----
JSONObject item = JSONObject.parseObject(json, Feature.OrderedField);
//update-end--Author:scott -- Date:20211223 ----for【issues/3303】restcontroller返回json数据后key顺序错乱 -----
//update-begin--Author:scott -- Date:20190603 ----for解决继承实体字段无法翻译问题------
//for (Field field : record.getClass().getDeclaredFields()) {
// 遍历所有字段把字典Code取出来放到 map 里
for (Field field : oConvertUtils.getAllFields(record)) {
@ -130,6 +135,7 @@ public class DictAspect {
if (oConvertUtils.isEmpty(value)) {
continue;
}
//update-end--Author:scott -- Date:20190603 ----for解决继承实体字段无法翻译问题------
if (field.getAnnotation(Dict.class) != null) {
if (!dictFieldList.contains(field)) {
dictFieldList.add(field);
@ -137,22 +143,26 @@ public class DictAspect {
String code = field.getAnnotation(Dict.class).dicCode();
String text = field.getAnnotation(Dict.class).dicText();
String table = field.getAnnotation(Dict.class).dictTable();
// 代码逻辑说明: [issues/#5643]解决分布式下表字典跨库无法查询问题------------
//update-begin---author:chenrui ---date:20231221 for[issues/#5643]解决分布式下表字典跨库无法查询问题------------
String dataSource = field.getAnnotation(Dict.class).ds();
//update-end---author:chenrui ---date:20231221 for[issues/#5643]解决分布式下表字典跨库无法查询问题------------
List<String> dataList;
String dictCode = code;
if (!StringUtils.isEmpty(table)) {
// 代码逻辑说明: [issues/#5643]解决分布式下表字典跨库无法查询问题------------
//update-begin---author:chenrui ---date:20231221 for[issues/#5643]解决分布式下表字典跨库无法查询问题------------
dictCode = String.format("%s,%s,%s,%s", table, text, code, dataSource);
//update-end---author:chenrui ---date:20231221 for[issues/#5643]解决分布式下表字典跨库无法查询问题------------
}
dataList = dataListMap.computeIfAbsent(dictCode, k -> new ArrayList<>());
this.listAddAllDeduplicate(dataList, Arrays.asList(value.split(",")));
}
//date类型默认转换string格式化日期
//update-begin--Author:zyf -- Date:20220531 ----for【issues/#3629】 DictAspect Jackson序列化报错-----
//if (JAVA_UTIL_DATE.equals(field.getType().getName())&&field.getAnnotation(JsonFormat.class)==null&&item.get(field.getName())!=null){
//SimpleDateFormat aDate=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
// item.put(field.getName(), aDate.format(new Date((Long) item.get(field.getName()))));
//}
//update-end--Author:zyf -- Date:20220531 ----for【issues/#3629】 DictAspect Jackson序列化报错-----
}
items.add(item);
}
@ -166,12 +176,15 @@ public class DictAspect {
String code = field.getAnnotation(Dict.class).dicCode();
String text = field.getAnnotation(Dict.class).dicText();
String table = field.getAnnotation(Dict.class).dictTable();
//update-begin---author:chenrui ---date:20231221 for[issues/#5643]解决分布式下表字典跨库无法查询问题------------
// 自定义的字典表数据源
String dataSource = field.getAnnotation(Dict.class).ds();
//update-end---author:chenrui ---date:20231221 for[issues/#5643]解决分布式下表字典跨库无法查询问题------------
String fieldDictCode = code;
if (!StringUtils.isEmpty(table)) {
// 代码逻辑说明: [issues/#5643]解决分布式下表字典跨库无法查询问题------------
//update-begin---author:chenrui ---date:20231221 for[issues/#5643]解决分布式下表字典跨库无法查询问题------------
fieldDictCode = String.format("%s,%s,%s,%s", table, text, code, dataSource);
//update-end---author:chenrui ---date:20231221 for[issues/#5643]解决分布式下表字典跨库无法查询问题------------
}
String value = record.getString(field.getName());
@ -273,20 +286,25 @@ public class DictAspect {
String[] arr = dictCode.split(",");
String table = arr[0], text = arr[1], code = arr[2];
String values = String.join(",", needTranslDataTable);
//update-begin---author:chenrui ---date:20231221 for[issues/#5643]解决分布式下表字典跨库无法查询问题------------
// 自定义的数据源
String dataSource = null;
if (arr.length > 3) {
dataSource = arr[3];
}
//update-end---author:chenrui ---date:20231221 for[issues/#5643]解决分布式下表字典跨库无法查询问题------------
log.debug("translateDictFromTableByKeys.dictCode:" + dictCode);
log.debug("translateDictFromTableByKeys.values:" + values);
//update-begin---author:chenrui ---date:20231221 for[issues/#5643]解决分布式下表字典跨库无法查询问题------------
// 代码逻辑说明: 微服务下为空报错没有参数需要传递空字符串---
//update-begin---author:wangshuai---date:2024-01-09---for:微服务下为空报错没有参数需要传递空字符串---
if(null == dataSource){
dataSource = "";
}
//update-end---author:wangshuai---date:2024-01-09---for:微服务下为空报错没有参数需要传递空字符串---
List<DictModel> texts = commonApi.translateDictFromTableByKeys(table, text, code, values, dataSource);
//update-end---author:chenrui ---date:20231221 for[issues/#5643]解决分布式下表字典跨库无法查询问题------------
log.debug("translateDictFromTableByKeys.result:" + texts);
List<DictModel> list = translText.computeIfAbsent(dictCode, k -> new ArrayList<>());
list.addAll(texts);
@ -295,8 +313,10 @@ public class DictAspect {
for (DictModel dict : texts) {
String redisKey = String.format("sys:cache:dictTable::SimpleKey [%s,%s]", dictCode, dict.getValue());
try {
// update-begin-author:taoyan date:20211012 for: 字典表翻译注解缓存未更新 issues/3061
// 保留5分钟
redisTemplate.opsForValue().set(redisKey, dict.getText(), 300, TimeUnit.SECONDS);
// update-end-author:taoyan date:20211012 for: 字典表翻译注解缓存未更新 issues/3061
} catch (Exception e) {
log.warn(e.getMessage(), e);
}
@ -380,7 +400,7 @@ public class DictAspect {
if (k.trim().length() == 0) {
continue; //跳过循环
}
// 代码逻辑说明: !56 优化微服务应用下存在表字段需要字典翻译时加载缓慢问题-----
//update-begin--Author:scott -- Date:20210531 ----for !56 优化微服务应用下存在表字段需要字典翻译时加载缓慢问题-----
if (!StringUtils.isEmpty(table)){
log.debug("--DictAspect------dicTable="+ table+" ,dicText= "+text+" ,dicCode="+code);
String keyString = String.format("sys:cache:dictTable::SimpleKey [%s,%s,%s,%s]",table,text,code,k.trim());
@ -405,6 +425,7 @@ public class DictAspect {
tmpValue = commonApi.translateDict(code, k.trim());
}
}
//update-end--Author:scott -- Date:20210531 ----for !56 优化微服务应用下存在表字段需要字典翻译时加载缓慢问题-----
if (tmpValue != null) {
if (!"".equals(textValue.toString())) {

View File

@ -57,6 +57,7 @@ public class PermissionDataAspect {
String requestMethod = request.getMethod();
String requestPath = request.getRequestURI().substring(request.getContextPath().length());
requestPath = filterUrl(requestPath);
//update-begin-author:taoyan date:20211027 for:JTC-132【online报表权限】online报表带参数的菜单配置数据权限无效
//先判断是否online报表请求
if(requestPath.indexOf(UrlMatchEnum.CGREPORT_DATA.getMatchUrl())>=0 || requestPath.indexOf(UrlMatchEnum.CGREPORT_ONLY_DATA.getMatchUrl())>=0){
// 获取地址栏参数
@ -65,6 +66,7 @@ public class PermissionDataAspect {
requestPath+="?"+urlParamString;
}
}
//update-end-author:taoyan date:20211027 for:JTC-132【online报表权限】online报表带参数的菜单配置数据权限无效
log.debug("拦截请求 >> {} ; 请求类型 >> {} . ", requestPath, requestMethod);
String username = JwtUtil.getUserNameByToken(request);
//查询数据权限信息

View File

@ -41,6 +41,7 @@ public @interface Dict {
String dictTable() default "";
//update-begin---author:chenrui ---date:20231221 for[issues/#5643]解决分布式下表字典跨库无法查询问题------------
/**
* 方法描述: 数据字典表所在数据源名称
* 作 者: chenrui
@ -49,4 +50,5 @@ public @interface Dict {
* @return 返回类型: String
*/
String ds() default "";
//update-end---author:chenrui ---date:20231221 for[issues/#5643]解决分布式下表字典跨库无法查询问题------------
}

View File

@ -90,24 +90,7 @@ public interface CommonConstant {
/** 登录用户Shiro权限缓存KEY前缀 */
public static String PREFIX_USER_SHIRO_CACHE = "shiro:cache:org.jeecg.config.shiro.ShiroRealm.authorizationCache:";
/** 登录用户Token令牌缓存KEY前缀 */
String PREFIX_USER_TOKEN = "prefix_user_token:";
/** 登录用户Token令牌作废提示信息比如 “不允许同一账号多地同时登录,会往这个变量存提示信息” */
String PREFIX_USER_TOKEN_ERROR_MSG = "prefix_user_token:error:msg_";
/**============================== 【是否允许同一账号多地同时登录】登录客户端类型常量 ==============================*/
/** 客户端类型PC端 */
String CLIENT_TYPE_PC = "PC";
/** 客户端类型APP端 */
String CLIENT_TYPE_APP = "APP";
/** 客户端类型:手机号登录 */
String CLIENT_TYPE_PHONE = "PHONE";
String PREFIX_USER_TOKEN_PC = "prefix_user_token:single_login:pc:";
/** 单点登录用户在APP端的Token缓存KEY前缀 (username -> token) */
String PREFIX_USER_TOKEN_APP = "prefix_user_token:single_login:app:";
/** 单点登录用户在手机号登录的Token缓存KEY前缀 (username -> token) */
String PREFIX_USER_TOKEN_PHONE = "prefix_user_token:single_login:phone:";
/**============================== 【是否允许同一账号多地同时登录】登录客户端类型常量 ==============================*/
String PREFIX_USER_TOKEN = "token::jeecg-client::";
// /** Token缓存时间3600秒即一小时 */
// int TOKEN_EXPIRE_TIME = 3600;
@ -320,15 +303,6 @@ public interface CommonConstant {
*/
String SYS_USER_ID_MAPPING_CACHE = "sys:cache:user:id_mapping";
/**
* 系统角色管理员编码
*/
String SYS_ROLE_ADMIN = "admin";
/**
* 考勤补卡业务状态 0处理中
*/
String SIGN_PATCH_BIZ_STATUS_0 = "0";
/**
* 考勤补卡业务状态 1同意 2不同意
*/
@ -454,11 +428,6 @@ public interface CommonConstant {
*/
String NOTICE_MSG_BUS_TYPE = "NOTICE_MSG_BUS_TYPE";
/**
* 通知类型,用于区分来源 file 知识 flow 流程 plan 日程 system 系统消息
*/
String NOTICE_TYPE = "noticeType";
/**
* 邮箱消息中地址登录时地址后携带的token,需要替换成真实的token值
*/
@ -612,6 +581,7 @@ public interface CommonConstant {
String ORDER_TYPE_DESC = "DESC";
//update-begin---author:scott ---date:2023-09-10 for积木报表常量----
/**
* 报表允许设计开发的角色
*/
@ -626,7 +596,9 @@ public interface CommonConstant {
* 数据隔离模式: 按照租户隔离
*/
public static final String SAAS_MODE_TENANT = "tenant";
//update-end---author:scott ---date::2023-09-10 for积木报表常量----
//update-begin---author:wangshuai---date:2024-04-07---for:修改手机号常量---
/**
* 修改手机号短信验证码redis-key的前缀
*/
@ -651,94 +623,10 @@ public interface CommonConstant {
* 修改手机号
*/
String UPDATE_PHONE = "updatePhone";
//update-end---author:wangshuai---date:2024-04-07---for:修改手机号常量---
/**
* 修改手机号验证码请求次数超出
*/
Integer PHONE_SMS_FAIL_CODE = 40002;
/**
* 自定义首页关联关系(ROLE:表示角色 USER:表示用户 DEFAULT:默认首页)
*
*/
String HOME_RELATION_ROLE = "ROLE";
String HOME_RELATION_USER = "USER";
String HOME_RELATION_DEFAULT = "DEFAULT";
/**
* 是否置顶(0否 1是)
*/
Integer IZ_TOP_1 = 1;
Integer IZ_TOP_0 = 0;
//关注流程缓存前缀
String FLOW_FOCUS_NOTICE_PREFIX = "flow:runtimeData:focus:notice:";
//任务缓办时间缓存前缀
String FLOW_TASK_DELAY_PREFIX = "flow:runtimeData:task:delay:";
/**
* 用户代理类型离职quit 代理agent
*/
String USER_AGENT_TYPE_QUIT = "quit";
String USER_AGENT_TYPE_AGENT = "agent";
/**
* 督办流程首节点任务taskKey
*/
String SUPERVISE_FIRST_TASK_KEY = "Task_1bhxpt0";
/**
* wps模板预览数据缓存前缀
*/
String EOA_WPS_TEMPLATE_VIEW_DATA ="eoa:wps:templateViewData:";
/**
* wps模板预览版本号缓存前缀
*/
String EOA_WPS_TEMPLATE_VIEW_VERSION ="eoa:wps:templateViewVersion:";
/**
* 表单设计器oa新增字段
* x_oa_timeout_date:逾期时间
* x_oa_archive_status:归档状态
*/
String X_OA_TIMEOUT_DATE ="x_oa_timeout_date";
String X_OA_ARCHIVE_STATUS ="x_oa_archive_status";
/**
* 流程状态
* 待提交: 1
* 处理中: 2
* 已完成: 3
* 已作废: 4
* 已挂起: 5
*/
String BPM_STATUS_1 ="1";
String BPM_STATUS_2 ="2";
String BPM_STATUS_3 ="3";
String BPM_STATUS_4 ="4";
String BPM_STATUS_5 ="5";
/**
* 默认租户产品包
*/
String TENANT_PACK_DEFAULT = "default";
/**
* 部门名称redisKey(全路径)
*/
String DEPART_NAME_REDIS_KEY_PRE = "sys:cache:departPathName:";
/**
* 默认用户排序值
*/
Integer DEFAULT_USER_SORT = 1000;
/**
* 发送短信方式:腾讯
*/
String SMS_SEND_TYPE_TENCENT = "tencent";
/**
* 发送短信方式:阿里云
*/
String SMS_SEND_TYPE_ALI_YUN = "aliyun";
}

View File

@ -4,20 +4,6 @@ package org.jeecg.common.constant;
* @author: jeecg-boot
*/
public interface DataBaseConstant {
/**
* 内置的系统变量键列表
*/
public static final String[] SYSTEM_KEYS = {
DataBaseConstant.SYS_ORG_CODE, DataBaseConstant.SYS_ORG_CODE_TABLE, DataBaseConstant.SYS_MULTI_ORG_CODE,
DataBaseConstant.SYS_MULTI_ORG_CODE_TABLE, DataBaseConstant.SYS_ORG_ID, DataBaseConstant.SYS_ORG_ID_TABLE,
DataBaseConstant.SYS_ROLE_CODE, DataBaseConstant.SYS_ROLE_CODE_TABLE, DataBaseConstant.SYS_USER_CODE,
DataBaseConstant.SYS_USER_CODE_TABLE, DataBaseConstant.SYS_USER_ID, DataBaseConstant.SYS_USER_ID_TABLE,
DataBaseConstant.SYS_USER_NAME, DataBaseConstant.SYS_USER_NAME_TABLE, DataBaseConstant.SYS_DATE,
DataBaseConstant.SYS_DATE_TABLE, DataBaseConstant.SYS_TIME, DataBaseConstant.SYS_TIME_TABLE,
DataBaseConstant.SYS_BASE_PATH
};
//*********数据库类型****************************************
/**MYSQL数据库*/

View File

@ -1,15 +0,0 @@
package org.jeecg.common.constant;
/**
* @Description: 密码常量类
*
* @author: wangshuai
* @date: 2025/8/27 20:10
*/
public interface PasswordConstant {
/**
* 导入用户默认密码
*/
String DEFAULT_PASSWORD = "123456";
}

View File

@ -39,18 +39,20 @@ public class ProvinceCityArea {
this.initAreaList();
if(areaList!=null && areaList.size()>0){
for(int i=areaList.size()-1;i>=0;i--){
// 代码逻辑说明: VUEN-1088 online 导入 省市区导入后 导入数据错乱 北京市/市辖区/西城区-->山西省/晋城市/城区
//update-begin-author:taoyan date:2022-5-24 for:VUEN-1088 online 导入 省市区导入后 导入数据错乱 北京市/市辖区/西城区-->山西省/晋城市/城区
String areaText = areaList.get(i).getText();
String cityText = areaList.get(i).getAheadText();
if(text.indexOf(areaText)>=0 && (cityText!=null && text.indexOf(cityText)>=0)){
return areaList.get(i).getId();
}
//update-end-author:taoyan date:2022-5-24 for:VUEN-1088 online 导入 省市区导入后 导入数据错乱 北京市/市辖区/西城区-->山西省/晋城市/城区
}
}
}
return null;
}
// update-begin-author:sunjianlei date:20220121 for:【JTC-704】数据导入错误 省市区组件,文件中为北京市,导入后,导为了山西省
/**
* 获取省市区code精准匹配
* @param texts 文本数组,省,市,区
@ -115,10 +117,11 @@ public class ProvinceCityArea {
}
return null;
}
// update-end-author:sunjianlei date:20220121 for:【JTC-704】数据导入错误 省市区组件,文件中为北京市,导入后,导为了山西省
public void getAreaByCode(String code,List<String> ls){
for(Area area: areaList){
if(null != area && area.getId().equals(code)){
if(area.getId().equals(code)){
String pid = area.getPid();
ls.add(0,area.getText());
getAreaByCode(pid,ls);
@ -151,8 +154,9 @@ public class ProvinceCityArea {
for(String areaKey:areaJson.keySet()){
//System.out.println("········"+areaKey);
Area area = new Area(areaKey,areaJson.getString(areaKey),cityKey);
// 代码逻辑说明: VUEN-1088 online 导入 省市区导入后 导入数据错乱 北京市/市辖区/西城区-->山西省/晋城市/城区
//update-begin-author:taoyan date:2022-5-24 for:VUEN-1088 online 导入 省市区导入后 导入数据错乱 北京市/市辖区/西城区-->山西省/晋城市/城区
area.setAheadText(cityJson.getString(cityKey));
//update-end-author:taoyan date:2022-5-24 for:VUEN-1088 online 导入 省市区导入后 导入数据错乱 北京市/市辖区/西城区-->山西省/晋城市/城区
this.areaList.add(area);
}
}

View File

@ -1,97 +0,0 @@
package org.jeecg.common.constant.enums;
import org.jeecg.common.util.oConvertUtils;
/**
* @Description: 部门类型枚举类
*
* @author: wangshuai
* @date: 2025/8/19 21:37
*/
public enum DepartCategoryEnum {
DEPART_CATEGORY_COMPANY("部门类型:公司","公司","1"),
DEPART_CATEGORY_DEPART("部门类型:部门","部门","2"),
DEPART_CATEGORY_POST("部门类型:岗位","岗位","3"),
DEPART_CATEGORY_SUB_COMPANY("部门类型:子公司","子公司","4");
DepartCategoryEnum(String described, String name, String value) {
this.value = value;
this.name = name;
this.described = described;
}
/**
* 描述
*/
private String described;
/**
* 值
*/
private String value;
/**
* 名称
*/
private String name;
public String getDescribed() {
return described;
}
public void setDescribed(String described) {
this.described = described;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
/**
* 根据值获取名称
*
* @param value
* @return
*/
public static String getNameByValue(String value){
if (oConvertUtils.isEmpty(value)) {
return null;
}
for (DepartCategoryEnum val : values()) {
if (val.getValue().equals(value)) {
return val.getName();
}
}
return value;
}
/**
* 根据名称获取值
*
* @param name
* @return
*/
public static String getValueByName(String name){
if (oConvertUtils.isEmpty(name)) {
return null;
}
for (DepartCategoryEnum val : values()) {
if (val.getName().equals(name)) {
return val.getValue();
}
}
return name;
}
}

View File

@ -9,14 +9,14 @@ import org.apache.commons.lang3.StringUtils;
public enum DySmsEnum {
/**登录短信模板编码*/
LOGIN_TEMPLATE_CODE("SMS_175435174","敲敲云","code"),
LOGIN_TEMPLATE_CODE("SMS_175435174","敲敲云","code"),
/**忘记密码短信模板编码*/
FORGET_PASSWORD_TEMPLATE_CODE("SMS_175435174","敲敲云","code"),
/**修改密码短信模板编码*/
CHANGE_PASSWORD_TEMPLATE_CODE("SMS_465391221","敲敲云","code"),
/**注册账号短信模板编码*/
REGISTER_TEMPLATE_CODE("SMS_175430166","敲敲云","code");
FORGET_PASSWORD_TEMPLATE_CODE("SMS_175435174","敲敲云","code"),
/**修改密码短信模板编码*/
CHANGE_PASSWORD_TEMPLATE_CODE("SMS_465391221","敲敲云","code"),
/**注册账号短信模板编码*/
REGISTER_TEMPLATE_CODE("SMS_175430166","敲敲云","code");
/**
* 短信模板编码
*/

View File

@ -13,10 +13,6 @@ public enum EmailTemplateEnum {
* 流程催办
*/
BPM_CUIBAN_EMAIL("bpm_cuiban_email", "/templates/email/bpm_cuiban_email.ftl"),
/**
* 流程抄送
*/
BPM_CC_EMAIL("bpm_cc_email", "/templates/email/bpm_cc_email.ftl"),
/**
* 流程新任务
*/

View File

@ -8,30 +8,21 @@ import java.util.List;
/**
* 消息类型
*
* @author: jeecg-boot
*/
@EnumDict("messageType")
public enum MessageTypeEnum {
/**
* 系统消息
*/
XT("system", "系统消息"),
/**
* 邮件消息
*/
YJ("email", "邮件消息"),
/**
* 钉钉消息
*/
/** 系统消息 */
XT("system", "系统消息"),
/** 邮件消息 */
YJ("email", "邮件消息"),
/** 钉钉消息 */
DD("dingtalk", "钉钉消息"),
/**
* 企业微信
*/
/** 企业微信 */
QYWX("wechat_enterprise", "企业微信");
MessageTypeEnum(String type, String note) {
MessageTypeEnum(String type, String note){
this.type = type;
this.note = note;
}
@ -65,13 +56,12 @@ public enum MessageTypeEnum {
/**
* 获取字典数据
*
* @return
*/
public static List<DictModel> getDictList() {
public static List<DictModel> getDictList(){
List<DictModel> list = new ArrayList<>();
DictModel dictModel = null;
for (MessageTypeEnum e : MessageTypeEnum.values()) {
for(MessageTypeEnum e: MessageTypeEnum.values()){
dictModel = new DictModel();
dictModel.setValue(e.getType());
dictModel.setText(e.getNote());

View File

@ -1,88 +0,0 @@
package org.jeecg.common.constant.enums;
/**
* @Description: 文件类型枚举类
*
* @author: wangshuai
* @date: 2025/6/26 17:29
*/
public enum NoticeTypeEnum {
//VUE3专用
NOTICE_TYPE_FILE("知识库消息","file"),
NOTICE_TYPE_FLOW("工作流消息","flow"),
NOTICE_TYPE_PLAN("日程消息","plan"),
//暂时没用到
NOTICE_TYPE_MEETING("会议消息","meeting"),
NOTICE_TYPE_SYSTEM("系统消息","system"),
/**
* 协同工作
* for [JHHB-136]【vue3】协同工作系统消息需要添加一个类型
*/
NOTICE_TYPE_COLLABORATION("协同工作", "collab"),
/**
* 督办
*/
NOTICE_TYPE_SUPERVISE("督办管理", "supe"),
/**
* 考勤
*/
NOTICE_TYPE_ATTENDANCE("考勤消息", "attendance");
/**
* 文件类型名称
*/
private String name;
/**
* 文件类型值
*/
private String value;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
NoticeTypeEnum(String name, String value) {
this.name = name;
this.value = value;
}
/**
* 获取聊天通知类型
*
* @param value
* @return
*/
public static String getChatNoticeType(String value){
return value + "Notice";
}
/**
* 获取通知名称
*
* @param value
* @return
*/
public static String getNoticeNameByValue(String value){
value = value.replace("Notice","");
for (NoticeTypeEnum e : NoticeTypeEnum.values()) {
if (e.getValue().equals(value)) {
return e.getName();
}
}
return "系统消息";
}
}

View File

@ -1,180 +0,0 @@
package org.jeecg.common.constant.enums;
import java.util.Arrays;
import java.util.List;
/**
* 职级枚举类
*
* 注意此枚举仅适用于天津临港控股OA项目,职级的名称和等级均为写死(需要与数据库配置一致)
* @date 2025-08-26
* @author scott
*/
public enum PositionLevelEnum {
// 领导层级等级1-3
CHAIRMAN("董事长", 1, PositionType.LEADER),
GENERAL_MANAGER("总经理", 2, PositionType.LEADER),
VICE_GENERAL_MANAGER("副总经理", 3, PositionType.LEADER),
// 职员层级等级4-6
MINISTER("部长", 4, PositionType.STAFF),
VICE_MINISTER("副部长", 5, PositionType.STAFF),
STAFF("职员", 6, PositionType.STAFF);
private final String name;
private final int level;
private final PositionType type;
PositionLevelEnum(String name, int level, PositionType type) {
this.name = name;
this.level = level;
this.type = type;
}
public String getName() {
return name;
}
public int getLevel() {
return level;
}
public PositionType getType() {
return type;
}
/**
* 职级类型枚举
*/
public enum PositionType {
STAFF("职员层级"),
LEADER("领导层级");
private final String desc;
PositionType(String desc) {
this.desc = desc;
}
public String getDesc() {
return desc;
}
}
/**
* 根据职级名称获取枚举
* @param name 职级名称
* @return 职级枚举
*/
public static PositionLevelEnum getByName(String name) {
for (PositionLevelEnum position : values()) {
if (position.getName().equals(name)) {
return position;
}
}
return null;
}
/**
* 根据职级等级获取枚举
* @param level 职级等级
* @return 职级枚举
*/
public static PositionLevelEnum getByLevel(int level) {
for (PositionLevelEnum position : values()) {
if (position.getLevel() == level) {
return position;
}
}
return null;
}
/**
* 根据职级名称判断是否为职员层级
* @param name 职级名称
* @return true-职员层级false-非职员层级
*/
public static boolean isStaffLevel(String name) {
PositionLevelEnum position = getByName(name);
return position != null && position.getType() == PositionType.STAFF;
}
/**
* 根据职级名称判断是否为领导层级
* @param name 职级名称
* @return true-领导层级false-非领导层级
*/
public static boolean isLeaderLevel(String name) {
PositionLevelEnum position = getByName(name);
return position != null && position.getType() == PositionType.LEADER;
}
/**
* 比较两个职级的等级高低
* @param name1 职级名称1
* @param name2 职级名称2
* @return 正数表示name1等级更高负数表示name2等级更高0表示等级相同
*/
public static int compareLevel(String name1, String name2) {
PositionLevelEnum pos1 = getByName(name1);
PositionLevelEnum pos2 = getByName(name2);
if (pos1 == null || pos2 == null) {
return 0;
}
// 等级数字越小代表职级越高
return pos2.getLevel() - pos1.getLevel();
}
/**
* 判断是否为更高等级
* @param currentName 当前职级名称
* @param targetName 目标职级名称
* @return true-目标职级更高false-目标职级不高于当前职级
*/
public static boolean isHigherLevel(String currentName, String targetName) {
return compareLevel(targetName, currentName) > 0;
}
/**
* 获取所有职员层级名称
* @return 职员层级名称列表
*/
public static List<String> getStaffLevelNames() {
return Arrays.asList(MINISTER.getName(), VICE_MINISTER.getName(), STAFF.getName());
}
/**
* 获取所有领导层级名称
* @return 领导层级名称列表
*/
public static List<String> getLeaderLevelNames() {
return Arrays.asList(CHAIRMAN.getName(), GENERAL_MANAGER.getName(), VICE_GENERAL_MANAGER.getName());
}
/**
* 获取所有职级名称(按等级排序)
* @return 所有职级名称列表
*/
public static List<String> getAllPositionNames() {
return Arrays.asList(
CHAIRMAN.getName(), GENERAL_MANAGER.getName(), VICE_GENERAL_MANAGER.getName(),
MINISTER.getName(), VICE_MINISTER.getName(), STAFF.getName()
);
}
/**
* 获取指定等级范围的职级
* @param minLevel 最小等级
* @param maxLevel 最大等级
* @return 职级名称列表
*/
public static List<String> getPositionsByLevelRange(int minLevel, int maxLevel) {
return Arrays.stream(values())
.filter(p -> p.getLevel() >= minLevel && p.getLevel() <= maxLevel)
.map(PositionLevelEnum::getName)
.collect(java.util.stream.Collectors.toList());
}
}

View File

@ -23,25 +23,7 @@ public enum SysAnnmentTypeEnum {
/**
* 邀请用户跳转到个人设置
*/
TENANT_INVITE("tenant_invite", "url", "/system/usersetting"),
/**
* 协同工作-待办通知
* for [JHHB-136]【vue3】协同工作系统消息需要添加一个类型
*/
EOA_CO_NOTIFY("eoa_co_notify", "url", "/collaboration/pending"),
/**
* 协同工作-催办通知
* for [JHHB-136]【vue3】协同工作系统消息需要添加一个类型
*/
EOA_CO_REMIND("eoa_co_remind", "url", "/collaboration/pending"),
/**
* 督办管理-催办
*/
EOA_SUP_REMIND("eoa_sup_remind", "url", "/superivse/list"),
/**
* 督办管理-通知
*/
EOA_SUP_NOTIFY("eoa_sup_notify", "url", "/superivse/list");
TENANT_INVITE("tenant_invite", "url", "/system/usersetting");
/**
* 业务类型(email:邮件 bpm:流程)

View File

@ -1,82 +0,0 @@
package org.jeecg.common.constant.enums;
import org.jeecg.common.util.oConvertUtils;
/**
* UniPush 消息推送枚举
* @author: jeecg-boot
*/
public enum UniPushTypeEnum {
/**
* 聊天
*/
CHAT("chat", "聊天消息", "收到%s发来的聊天消息"),
/**
* 流程跳转到我的任务
*/
BPM("bpm_task", "待办任务", "收到%s待办任务"),
/**
* 流程抄送任务
*/
BPM_VIEW("bpm_cc", "知会任务", "收到%s知会任务"),
/**
* 系统消息
*/
SYS_MSG("system", "系统消息", "收到一条系统通告");
/**
* 业务类型(chat:聊天 bpm_task:流程 bpm_cc:流程抄送)
*/
private String type;
/**
* 消息标题
*/
private String title;
/**
* 消息内容
*/
private String content;
UniPushTypeEnum(String type, String title, String content) {
this.type = type;
this.title = title;
this.content = content;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public String getTitle() {
return title ;
}
public void setTitle(String openType) {
this.title = openType;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public static UniPushTypeEnum getByType(String type) {
if (oConvertUtils.isEmpty(type)) {
return null;
}
for (UniPushTypeEnum val : values()) {
if (val.getType().equals(type)) {
return val;
}
}
return null;
}
}

View File

@ -73,7 +73,7 @@ public class SensitiveDataAspect {
SensitiveInfoUtil.handleNestedObject(result, entity, isEncode);
}
long endTime=System.currentTimeMillis();
log.debug((isEncode ? "加密操作," : "解密操作,") + "Aspect程序耗时" + (endTime - startTime) + "ms");
log.info((isEncode ? "加密操作," : "解密操作,") + "Aspect程序耗时" + (endTime - startTime) + "ms");
return result;
}

View File

@ -387,7 +387,7 @@ public class JeecgElasticsearchTemplate {
data.remove("id");
bodySb.append(data.toJSONString()).append("\n");
}
//System.out.println("+-+-+-: bodySb.toString(): " + bodySb.toString());
System.out.println("+-+-+-: bodySb.toString(): " + bodySb.toString());
HttpHeaders headers = RestUtil.getHeaderApplicationJson();
RestUtil.request(url, HttpMethod.PUT, headers, null, bodySb, JSONObject.class);
return true;

View File

@ -1,21 +0,0 @@
package org.jeecg.common.exception;
/**
* jeecgboot断言异常
* for [QQYUN-10990]AIRAG
* @author chenrui
* @date 2025/2/14 14:31
*/
public class JeecgBootAssertException extends JeecgBootException {
private static final long serialVersionUID = 1L;
public JeecgBootAssertException(String message) {
super(message);
}
public JeecgBootAssertException(String message, int errCode) {
super(message, errCode);
}
}

View File

@ -6,8 +6,6 @@ import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authz.AuthorizationException;
import org.apache.shiro.authz.UnauthorizedException;
import org.jeecg.common.api.dto.LogDTO;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.constant.CommonConstant;
@ -24,19 +22,17 @@ import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.data.redis.connection.PoolException;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.util.CollectionUtils;
import org.springframework.validation.ObjectError;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.multipart.MaxUploadSizeExceededException;
import org.springframework.web.multipart.MultipartException;
import org.springframework.web.servlet.NoHandlerFoundException;
import java.util.Map;
import java.util.stream.Collectors;
/**
* 异常处理器
@ -47,17 +43,28 @@ import java.util.stream.Collectors;
@RestControllerAdvice
@Slf4j
public class JeecgBootExceptionHandler {
@Resource
@Resource
BaseCommonService baseCommonService;
/**
* 验证码错误异常
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result<?> handleValidationExceptions(MethodArgumentNotValidException e) {
@ExceptionHandler(JeecgCaptchaException.class)
@ResponseStatus(HttpStatus.OK)
public Result<?> handleJeecgCaptchaException(JeecgCaptchaException e) {
log.error(e.getMessage(), e);
addSysLog(e);
return Result.error("校验失败!" + e.getBindingResult().getAllErrors().stream().map(ObjectError::getDefaultMessage).collect(Collectors.joining(",")));
return Result.error(e.getCode(), e.getMessage());
}
@ExceptionHandler(AuthenticationException.class)
@ResponseStatus(HttpStatus.OK)
public Result<?> handleJeecgCaptchaException(AuthenticationException e) {
log.error(e.getMessage(), e);
return Result.error(401, e.getMessage());
}
/**
* 处理自定义异常
*/
@ -112,8 +119,8 @@ public class JeecgBootExceptionHandler {
return Result.error("数据库中已存在该记录");
}
@ExceptionHandler({UnauthorizedException.class, AuthorizationException.class})
public Result<?> handleAuthorizationException(AuthorizationException e){
@ExceptionHandler(AccessDeniedException.class)
public Result<?> handleAuthorizationException(AccessDeniedException e){
log.error(e.getMessage(), e);
return Result.noauth("没有权限,请联系管理员分配权限!");
}
@ -121,12 +128,13 @@ public class JeecgBootExceptionHandler {
@ExceptionHandler(Exception.class)
public Result<?> handleException(Exception e){
log.error(e.getMessage(), e);
// 代码逻辑说明: 处理Sentinel限流自定义异常
//update-begin---author:zyf ---date:20220411 for处理Sentinel限流自定义异常
Throwable throwable = e.getCause();
SentinelErrorInfoEnum errorInfoEnum = SentinelErrorInfoEnum.getErrorByException(throwable);
if (ObjectUtil.isNotEmpty(errorInfoEnum)) {
return Result.error(errorInfoEnum.getError());
}
//update-end---author:zyf ---date:20220411 for处理Sentinel限流自定义异常
addSysLog(e);
return Result.error("操作失败,"+e.getMessage());
}
@ -166,27 +174,6 @@ public class JeecgBootExceptionHandler {
return Result.error("文件大小超出10MB限制, 请压缩或降低文件质量! ");
}
/**
* 处理文件过大异常.
* jdk17中的MultipartException异常类已经被拆分成了MultipartException和MaxUploadSizeExceededException
* for [QQYUN-11716]上传大图片失败没有精确提示
* @param e
* @return
* @author chenrui
* @date 2025/4/8 16:13
*/
@ExceptionHandler(MultipartException.class)
public Result<?> handleMaxUploadSizeExceededException(MultipartException e) {
Throwable cause = e.getCause();
if (cause instanceof IllegalStateException) {
log.error("文件大小超出限制: {}", cause.getMessage(), e);
addSysLog(e);
return Result.error("文件大小超出限制, 请压缩或降低文件质量!");
} else {
return handleException(e);
}
}
@ExceptionHandler(DataIntegrityViolationException.class)
public Result<?> handleDataIntegrityViolationException(DataIntegrityViolationException e) {
log.error(e.getMessage(), e);
@ -223,6 +210,7 @@ public class JeecgBootExceptionHandler {
return Result.error("校验失败存在SQL注入风险" + msg);
}
//update-begin---author:chenrui ---date:20240423 for[QQYUN-8732]把错误的日志都抓取了 方便后续处理,单独弄个日志类型------------
/**
* 添加异常新系统日志
* @param e 异常
@ -242,12 +230,9 @@ public class JeecgBootExceptionHandler {
}
if (null != request) {
//请求的参数
if (!isTooBigException(e)) {
// 文件上传过大异常时不能获取参数,否则会报错
Map<String, String[]> parameterMap = request.getParameterMap();
if(!CollectionUtils.isEmpty(parameterMap)) {
log.setMethod(oConvertUtils.mapToString(request.getParameterMap()));
}
Map<String, String[]> parameterMap = request.getParameterMap();
if(!CollectionUtils.isEmpty(parameterMap)){
log.setMethod(oConvertUtils.mapToString(request.getParameterMap()));
}
// 请求地址
log.setRequestUrl(request.getRequestURI());
@ -272,27 +257,6 @@ public class JeecgBootExceptionHandler {
baseCommonService.addLog(log);
}
/**
* 是否文件过大异常
* for [QQYUN-11716]上传大图片失败没有精确提示
* @param e
* @return
* @author chenrui
* @date 2025/4/8 20:21
*/
private static boolean isTooBigException(Throwable e) {
boolean isTooBigException = false;
if(e instanceof MultipartException){
Throwable cause = e.getCause();
if (cause instanceof IllegalStateException){
isTooBigException = true;
}
}
if(e instanceof MaxUploadSizeExceededException){
isTooBigException = true;
}
return isTooBigException;
}
//update-end---author:chenrui ---date:20240423 for[QQYUN-8732]把错误的日志都抓取了 方便后续处理,单独弄个日志类型------------
}

View File

@ -0,0 +1,28 @@
package org.jeecg.common.exception;
import lombok.Data;
/**
* @author kezhijie@wuhandsj.com
* @date 2024/1/2 11:38
*/
@Data
public class JeecgCaptchaException extends RuntimeException{
private Integer code;
private static final long serialVersionUID = -9093410345065209053L;
public JeecgCaptchaException(Integer code, String message) {
super(message);
this.code = code;
}
public JeecgCaptchaException(String message, Throwable cause) {
super(message, cause);
}
public JeecgCaptchaException(Throwable cause) {
super(cause);
}
}

View File

@ -4,11 +4,6 @@ import java.lang.annotation.*;
/**
* 将枚举类转化成字典数据
*
* <<使用说明>>
* 1. 枚举类需以 `Enum` 结尾,并且在类上添加 `@EnumDict` 注解。
* 2. 需要手动将枚举类所在包路径** 添加到 `org.jeecg.common.system.util.ResourceUtil.BASE_SCAN_PACKAGES` 配置数组中。
*
* @Author taoYan
* @Date 2022/7/8 10:34
**/

View File

@ -1,25 +1,26 @@
package org.jeecg.common.system.base.controller;
import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.beanutils.PropertyUtils;
import org.apache.shiro.SecurityUtils;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.system.query.QueryGenerator;
import org.jeecg.common.system.vo.LoginUser;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.config.JeecgBaseConfig;
import org.jeecg.config.security.utils.SecureUtil;
import org.jeecgframework.poi.excel.ExcelImportUtil;
import org.jeecgframework.poi.excel.def.NormalExcelConstants;
import org.jeecgframework.poi.excel.entity.ExportParams;
import org.jeecgframework.poi.excel.entity.ImportParams;
import org.jeecgframework.poi.excel.entity.enmus.ExcelType;
import org.jeecgframework.poi.excel.view.JeecgEntityExcelView;
import org.jeecgframework.poi.handler.inter.IExcelExportServer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.MultipartHttpServletRequest;
import org.springframework.web.servlet.ModelAndView;
@ -52,7 +53,7 @@ public class JeecgController<T, S extends IService<T>> {
protected ModelAndView exportXls(HttpServletRequest request, T object, Class<T> clazz, String title) {
// Step.1 组装查询条件
QueryWrapper<T> queryWrapper = QueryGenerator.initQueryWrapper(object, request.getParameterMap());
LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal();
LoginUser sysUser = SecureUtil.currentUser();
// 过滤选中数据
String selections = request.getParameter("selections");
@ -68,16 +69,12 @@ public class JeecgController<T, S extends IService<T>> {
//此处设置的filename无效 ,前端会重更新设置一下
mv.addObject(NormalExcelConstants.FILE_NAME, title);
mv.addObject(NormalExcelConstants.CLASS, clazz);
// 代码逻辑说明: 【QQYUN-13930】统一改成导出xlsx格式---
ExportParams exportParams=new ExportParams(title + "报表", "导出人:" + sysUser.getRealname(), title, ExcelType.XSSF);
//update-begin--Author:liusq Date:20210126 for图片导出报错ImageBasePath未设置--------------------
ExportParams exportParams=new ExportParams(title + "报表", "导出人:" + sysUser.getRealname(), title);
exportParams.setImageBasePath(jeecgBaseConfig.getPath().getUpload());
//update-end--Author:liusq Date:20210126 for图片导出报错ImageBasePath未设置----------------------
mv.addObject(NormalExcelConstants.PARAMS,exportParams);
mv.addObject(NormalExcelConstants.DATA_LIST, exportList);
// 代码逻辑说明: 【issues/9052】BasicTable列表页导出excel可以指定列---
String exportFields = request.getParameter(NormalExcelConstants.EXPORT_FIELDS);
if(oConvertUtils.isNotEmpty(exportFields)){
mv.addObject(NormalExcelConstants.EXPORT_FIELDS, exportFields);
}
return mv;
}
/**
@ -94,16 +91,18 @@ public class JeecgController<T, S extends IService<T>> {
protected ModelAndView exportXlsSheet(HttpServletRequest request, T object, Class<T> clazz, String title,String exportFields,Integer pageNum) {
// Step.1 组装查询条件
QueryWrapper<T> queryWrapper = QueryGenerator.initQueryWrapper(object, request.getParameterMap());
LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal();
LoginUser sysUser = SecureUtil.currentUser();
// Step.2 计算分页sheet数据
double total = service.count();
int count = (int)Math.ceil(total/pageNum);
//update-begin-author:liusq---date:20220629--for: 多sheet导出根据选择导出写法调整 ---
// Step.3 过滤选中数据
String selections = request.getParameter("selections");
if (oConvertUtils.isNotEmpty(selections)) {
List<String> selectionList = Arrays.asList(selections.split(","));
queryWrapper.in("id",selectionList);
}
//update-end-author:liusq---date:20220629--for: 多sheet导出根据选择导出写法调整 ---
// Step.4 多sheet处理
List<Map<String, Object>> listMap = new ArrayList<Map<String, Object>>();
for (int i = 1; i <=count ; i++) {
@ -130,53 +129,6 @@ public class JeecgController<T, S extends IService<T>> {
return mv;
}
/**
* 大数据导出
* @param request
* @param object
* @param clazz
* @param title
* @param pageSize 每次查询的数据量
* @return
* @author chenrui
* @date 2025/8/11 16:11
*/
protected ModelAndView exportXlsForBigData(HttpServletRequest request, T object, Class<T> clazz, String title,Integer pageSize) {
// 组装查询条件
QueryWrapper<T> queryWrapper = QueryGenerator.initQueryWrapper(object, request.getParameterMap());
LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal();
// 计算分页数
double total = service.count();
int count = (int) Math.ceil(total / pageSize);
// 过滤选中数据
String selections = request.getParameter("selections");
if (oConvertUtils.isNotEmpty(selections)) {
List<String> selectionList = Arrays.asList(selections.split(","));
queryWrapper.in("id", selectionList);
}
// 定义IExcelExportServer
IExcelExportServer excelExportServer = (queryParams, pageNum) -> {
if (pageNum > count) {
return null;
}
Page<T> page = new Page<T>(pageNum, pageSize);
IPage<T> pageList = service.page(page, (QueryWrapper<T>) queryParams);
return new ArrayList<>(pageList.getRecords());
};
// AutoPoi 导出Excel
ModelAndView mv = new ModelAndView(new JeecgEntityExcelView());
//此处设置的filename无效 ,前端会重更新设置一下
mv.addObject(NormalExcelConstants.FILE_NAME, title);
mv.addObject(NormalExcelConstants.CLASS, clazz);
ExportParams exportParams = new ExportParams(title + "报表", "导出人:" + sysUser.getRealname(), title, jeecgBaseConfig.getPath().getUpload());
mv.addObject(NormalExcelConstants.PARAMS, exportParams);
mv.addObject(NormalExcelConstants.EXPORT_SERVER, excelExportServer);
mv.addObject(NormalExcelConstants.QUERY_PARAMS, queryWrapper);
return mv;
}
/**
* 根据权限导出excel传入导出字段参数
@ -222,15 +174,16 @@ public class JeecgController<T, S extends IService<T>> {
params.setNeedSave(true);
try {
List<T> list = ExcelImportUtil.importExcel(file.getInputStream(), clazz, params);
// 代码逻辑说明: 批量插入数据
//update-begin-author:taoyan date:20190528 for:批量插入数据
long start = System.currentTimeMillis();
service.saveBatch(list);
//400条 saveBatch消耗时间1592毫秒 循环插入消耗时间1947毫秒
//1200条 saveBatch消耗时间3687毫秒 循环插入消耗时间5212毫秒
log.info("消耗时间" + (System.currentTimeMillis() - start) + "毫秒");
//update-end-author:taoyan date:20190528 for:批量插入数据
return Result.ok("文件导入成功!数据行数:" + list.size());
} catch (Exception e) {
// 代码逻辑说明: 导入数据重复增加提示
//update-begin-author:taoyan date:20211124 for: 导入数据重复增加提示
String msg = e.getMessage();
log.error(msg, e);
if(msg!=null && msg.indexOf("Duplicate entry")>=0){
@ -238,6 +191,7 @@ public class JeecgController<T, S extends IService<T>> {
}else{
return Result.error("文件导入失败:" + e.getMessage());
}
//update-end-author:taoyan date:20211124 for: 导入数据重复增加提示
} finally {
try {
file.getInputStream().close();

View File

@ -9,10 +9,10 @@ import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import io.swagger.v3.oas.annotations.media.Schema;
/**
* @Description: Entity基类

View File

@ -11,7 +11,6 @@ import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import com.baomidou.mybatisplus.core.metadata.OrderItem;
import org.apache.commons.beanutils.PropertyUtils;
import org.jeecg.common.constant.CommonConstant;
import org.jeecg.common.constant.DataBaseConstant;
@ -97,6 +96,7 @@ public class QueryGenerator {
return queryWrapper;
}
//update-begin---author:chenrui ---date:20240527 for[TV360X-378]增加自定义字段查询规则功能------------
/**
* 获取查询条件构造器QueryWrapper实例 通用查询条件已被封装完成
* @param searchObj 查询实体
@ -111,6 +111,7 @@ public class QueryGenerator {
log.debug("---查询条件构造器初始化完成,耗时:"+(System.currentTimeMillis()-start)+"毫秒----");
return queryWrapper;
}
//update-end---author:chenrui ---date:20240527 for[TV360X-378]增加自定义字段查询规则功能------------
/**
* 组装Mybatis Plus 查询条件
@ -140,6 +141,7 @@ public class QueryGenerator {
}
String name, type, column;
// update-begin--Author:taoyan Date:20200923 forissues/1671 如果字段加注解了@TableField(exist = false),不走DB查询-------
//定义实体字段和数据库字段名称的映射 高级查询中 只能获取实体字段 如果设置TableField注解 那么查询条件会出问题
Map<String,String> fieldColumnMap = new HashMap<>(5);
for (int i = 0; i < origDescriptors.length; i++) {
@ -185,7 +187,7 @@ public class QueryGenerator {
queryWrapper.and(j -> j.like(field,vals[0]));
}
}else {
// 代码逻辑说明: [TV360X-378]增加自定义字段查询规则功能------------
//update-begin---author:chenrui ---date:20240527 for[TV360X-378]增加自定义字段查询规则功能------------
QueryRuleEnum rule;
if(null != customRuleMap && customRuleMap.containsKey(name)) {
// 有自定义规则,使用自定义规则.
@ -194,6 +196,7 @@ public class QueryGenerator {
//根据参数值带什么关键字符串判断走什么类型的查询
rule = convert2Rule(value);
}
//update-end---author:chenrui ---date:20240527 for[TV360X-378]增加自定义字段查询规则功能------------
value = replaceValue(rule,value);
// add -begin 添加判断为字符串时设为全模糊查询
//if( (rule==null || QueryRuleEnum.EQ.equals(rule)) && "class java.lang.String".equals(type)) {
@ -213,6 +216,7 @@ public class QueryGenerator {
//高级查询
doSuperQuery(queryWrapper, parameterMap, fieldColumnMap);
// update-end--Author:taoyan Date:20200923 forissues/1671 如果字段加注解了@TableField(exist = false),不走DB查询-------
}
@ -253,74 +257,15 @@ public class QueryGenerator {
if(parameterMap!=null&& parameterMap.containsKey(ORDER_TYPE)) {
order = parameterMap.get(ORDER_TYPE)[0];
}
if(oConvertUtils.isNotEmpty(column)){
log.debug("单字段排序规则>> column:" + column + ",排序方式:" + order);
}
log.debug("排序规则>>列:" + column + ",排序方式:" + order);
// 1. 列表多字段排序优先
if(parameterMap!=null&& parameterMap.containsKey("sortInfoString")) {
// 多字段排序
String sortInfoString = parameterMap.get("sortInfoString")[0];
log.debug("多字段排序规则>> sortInfoString:" + sortInfoString);
List<OrderItem> orderItemList = SqlConcatUtil.getQueryConditionOrders(column, order, sortInfoString);
log.debug(orderItemList.toString());
if (orderItemList != null && !orderItemList.isEmpty()) {
for (OrderItem item : orderItemList) {
// 一、获取排序数据库字段
String columnName = item.getColumn();
// 1.字典字段,去掉字典翻译文本后缀
if(columnName.endsWith(CommonConstant.DICT_TEXT_SUFFIX)) {
columnName = columnName.substring(0, column.lastIndexOf(CommonConstant.DICT_TEXT_SUFFIX));
}
// 2.实体驼峰字段转为数据库字段
columnName = SqlInjectionUtil.getSqlInjectSortField(columnName);
// 二、设置字段排序规则
if (item.isAsc()) {
queryWrapper.orderByAsc(columnName);
} else {
queryWrapper.orderByDesc(columnName);
}
}
}
return;
}
// 2. 列表单字段默认排序
if(oConvertUtils.isEmpty(column) && parameterMap!=null&& parameterMap.containsKey("defSortString")) {
// 多字段排序
String sortInfoString = parameterMap.get("defSortString")[0];
log.info("默认多字段排序规则>> defSortString:" + sortInfoString);
List<OrderItem> orderItemList = SqlConcatUtil.getQueryConditionOrders(column, order, sortInfoString);
log.info(orderItemList.toString());
if (orderItemList != null && !orderItemList.isEmpty()) {
for (OrderItem item : orderItemList) {
// 一、获取排序数据库字段
String columnName = item.getColumn();
// 1.字典字段,去掉字典翻译文本后缀
if(columnName.endsWith(CommonConstant.DICT_TEXT_SUFFIX)) {
columnName = columnName.substring(0, column.lastIndexOf(CommonConstant.DICT_TEXT_SUFFIX));
}
// 2.实体驼峰字段转为数据库字段
columnName = SqlInjectionUtil.getSqlInjectSortField(columnName);
// 二、设置字段排序规则
if (item.isAsc()) {
queryWrapper.orderByAsc(columnName);
} else {
queryWrapper.orderByDesc(columnName);
}
}
}
return;
}
//update-begin-author:scott date:2022-11-07 for:避免用户自定义表无默认字段{创建时间},导致排序报错
//TODO 避免用户自定义表无默认字段创建时间,导致排序报错
if(DataBaseConstant.CREATE_TIME.equals(column) && !fieldColumnMap.containsKey(DataBaseConstant.CREATE_TIME)){
column = "id";
log.warn("检测到实体里没有字段createTime改成采用ID排序");
}
//update-end-author:scott date:2022-11-07 for:避免用户自定义表无默认字段{创建时间},导致排序报错
if (oConvertUtils.isNotEmpty(column) && oConvertUtils.isNotEmpty(order)) {
//字典字段,去掉字典翻译文本后缀
@ -328,12 +273,15 @@ public class QueryGenerator {
column = column.substring(0, column.lastIndexOf(CommonConstant.DICT_TEXT_SUFFIX));
}
//update-begin-author:taoyan date:2022-5-16 for: issues/3676 获取系统用户列表时使用SQL注入生效
//判断column是不是当前实体的
log.debug("当前字段有:"+ allFields);
if (!allColumnExist(column, allFields)) {
throw new JeecgBootException("请注意,将要排序的列字段不存在:" + column);
}
//update-end-author:taoyan date:2022-5-16 for: issues/3676 获取系统用户列表时使用SQL注入生效
//update-begin-author:scott date:2022-10-10 for:【jeecg-boot/issues/I5FJU6】doMultiFieldsOrder() 多字段排序方法存在问题
//多字段排序方法没有读取 MybatisPlus 注解 @TableField 里 value 的值
if (column.contains(",")) {
List<String> columnList = Arrays.asList(column.split(","));
@ -344,10 +292,12 @@ public class QueryGenerator {
}else{
column = fieldColumnMap.get(column);
}
//update-end-author:scott date:2022-10-10 for:【jeecg-boot/issues/I5FJU6】doMultiFieldsOrder() 多字段排序方法存在问题
//SQL注入check
SqlInjectionUtil.filterContentMulti(column);
//update-begin--Author:scott Date:20210531 for36 多条件排序无效问题修正-------
// 排序规则修改
// 将现有排序 _ 前端传递排序条件{....,column: 'column1,column2',order: 'desc'} 翻译成sql "column1,column2 desc"
// 修改为 _ 前端传递排序条件{....,column: 'column1,column2',order: 'desc'} 翻译成sql "column1 desc,column2 desc"
@ -356,9 +306,11 @@ public class QueryGenerator {
} else {
queryWrapper.orderByDesc(SqlInjectionUtil.getSqlInjectSortFields(column.split(",")));
}
//update-end--Author:scott Date:20210531 for36 多条件排序无效问题修正-------
}
}
//update-begin-author:taoyan date:2022-5-23 for: issues/3676 获取系统用户列表时使用SQL注入生效
/**
* 多字段排序 判断所传字段是否存在
* @return
@ -378,6 +330,7 @@ public class QueryGenerator {
}
return exist;
}
//update-end-author:taoyan date:2022-5-23 for: issues/3676 获取系统用户列表时使用SQL注入生效
/**
* 高级查询
@ -390,46 +343,42 @@ public class QueryGenerator {
String superQueryParams = parameterMap.get(SUPER_QUERY_PARAMS)[0];
String superQueryMatchType = parameterMap.get(SUPER_QUERY_MATCH_TYPE) != null ? parameterMap.get(SUPER_QUERY_MATCH_TYPE)[0] : MatchTypeEnum.AND.getValue();
MatchTypeEnum matchType = MatchTypeEnum.getByValue(superQueryMatchType);
// 代码逻辑说明: 高级查询的条件要用括号括起来,防止和用户的其他条件冲突 -------
// update-begin--Author:sunjianlei Date:20200325 for高级查询的条件要用括号括起来,防止和用户的其他条件冲突 -------
try {
superQueryParams = URLDecoder.decode(superQueryParams, "UTF-8");
List<QueryCondition> conditions = JSON.parseArray(superQueryParams, QueryCondition.class);
if (conditions == null || conditions.size() == 0) {
return;
}
// 代码逻辑说明: 【JTC-573】 过滤空条件查询,防止 sql 拼接多余的 and
// update-begin-author:sunjianlei date:20220119 for: 【JTC-573】 过滤空条件查询,防止 sql 拼接多余的 and
List<QueryCondition> filterConditions = conditions.stream().filter(
rule -> (oConvertUtils.isNotEmpty(rule.getField())
&& oConvertUtils.isNotEmpty(rule.getRule())
&& oConvertUtils.isNotEmpty(rule.getVal())
)
|| "empty".equals(rule.getRule())
rule -> oConvertUtils.isNotEmpty(rule.getField())
&& oConvertUtils.isNotEmpty(rule.getRule())
&& oConvertUtils.isNotEmpty(rule.getVal())
).collect(Collectors.toList());
if (filterConditions.size() == 0) {
return;
}
// update-end-author:sunjianlei date:20220119 for: 【JTC-573】 过滤空条件查询,防止 sql 拼接多余的 and
log.debug("---高级查询参数-->" + filterConditions);
queryWrapper.and(andWrapper -> {
for (int i = 0; i < filterConditions.size(); i++) {
QueryCondition rule = filterConditions.get(i);
if (
(
oConvertUtils.isNotEmpty(rule.getField()) && oConvertUtils.isNotEmpty(rule.getRule()) && oConvertUtils.isNotEmpty(rule.getVal())
)
|| "empty".equals(rule.getRule())
) {
if (oConvertUtils.isNotEmpty(rule.getField())
&& oConvertUtils.isNotEmpty(rule.getRule())
&& oConvertUtils.isNotEmpty(rule.getVal())) {
log.debug("SuperQuery ==> " + rule.toString());
// 代码逻辑说明: 【高级查询】 oracle 日期等于查询报错
//update-begin-author:taoyan date:20201228 for: 【高级查询】 oracle 日期等于查询报错
Object queryValue = rule.getVal();
if("date".equals(rule.getType())){
queryValue = DateUtils.str2Date(rule.getVal(),DateUtils.date_sdf.get());
}else if("datetime".equals(rule.getType())){
queryValue = DateUtils.str2Date(rule.getVal(), DateUtils.datetimeFormat.get());
}
// 代码逻辑说明: 【/issues/I3VR8E】高级查询没有类型转换查询参数都是字符串类型 ----
// update-begin--author:sunjianlei date:20210702 for【/issues/I3VR8E】高级查询没有类型转换查询参数都是字符串类型 ----
String dbType = rule.getDbType();
if (oConvertUtils.isNotEmpty(dbType)) {
try {
@ -462,8 +411,9 @@ public class QueryGenerator {
log.error("高级查询值转换失败:", e);
}
}
// 代码逻辑说明: 【/issues/I3VR8E】高级查询没有类型转换查询参数都是字符串类型 ----
// update-begin--author:sunjianlei date:20210702 for【/issues/I3VR8E】高级查询没有类型转换查询参数都是字符串类型 ----
addEasyQuery(andWrapper, fieldColumnMap.get(rule.getField()), QueryRuleEnum.getByValue(rule.getRule()), queryValue);
//update-end-author:taoyan date:20201228 for: 【高级查询】 oracle 日期等于查询报错
// 如果拼接方式是OR就拼接OR
if (MatchTypeEnum.OR == matchType && i < (filterConditions.size() - 1)) {
@ -479,6 +429,7 @@ public class QueryGenerator {
log.error("--高级查询拼接失败:" + e.getMessage());
e.printStackTrace();
}
// update-end--Author:sunjianlei Date:20200325 for高级查询的条件要用括号括起来防止和用户的其他条件冲突 -------
}
//log.info(" superQuery getCustomSqlSegment: "+ queryWrapper.getCustomSqlSegment());
}
@ -490,7 +441,7 @@ public class QueryGenerator {
*/
public static QueryRuleEnum convert2Rule(Object value) {
// 避免空数据
// 代码逻辑说明: 查询条件输入空格导致return null后续判断导致抛出null异常
// update-begin-author:taoyan date:20210629 for: 查询条件输入空格导致return null后续判断导致抛出null异常
if (value == null) {
return QueryRuleEnum.EQ;
}
@ -498,8 +449,10 @@ public class QueryGenerator {
if (val.length() == 0) {
return QueryRuleEnum.EQ;
}
// update-end-author:taoyan date:20210629 for: 查询条件输入空格导致return null后续判断导致抛出null异常
QueryRuleEnum rule =null;
//update-begin--Author:scott Date:20190724 forinitQueryWrapper组装sql查询条件错误 #284-------------------
//TODO 此处规则,只适用于 le lt ge gt
// step 2 .>= =<
int length2 = 2;
@ -515,12 +468,14 @@ public class QueryGenerator {
rule = QueryRuleEnum.getByValue(val.substring(0, 1));
}
}
//update-end--Author:scott Date:20190724 forinitQueryWrapper组装sql查询条件错误 #284---------------------
// step 3 like
// 代码逻辑说明: /issues/3382 默认带*就走模糊,但是如果只有一个*,那么走等于查询
//update-begin-author:taoyan for: /issues/3382 默认带*就走模糊,但是如果只有一个*,那么走等于查询
if(rule == null && val.equals(STAR)){
rule = QueryRuleEnum.EQ;
}
//update-end-author:taoyan for: /issues/3382 默认带*就走模糊,但是如果只有一个*,那么走等于查询
if (rule == null && val.contains(STAR)) {
if (val.startsWith(STAR) && val.endsWith(STAR)) {
rule = QueryRuleEnum.LIKE;
@ -545,10 +500,12 @@ public class QueryGenerator {
rule = QueryRuleEnum.EQ_WITH_ADD;
}
//update-begin--Author:taoyan Date:20201229 forinitQueryWrapper组装sql查询条件错误 #284---------------------
//特殊处理Oracle的表达式to_date('xxx','yyyy-MM-dd')含有逗号会被识别为in查询转为等于查询
if(rule == QueryRuleEnum.IN && val.indexOf(YYYY_MM_DD)>=0 && val.indexOf(TO_DATE)>=0){
rule = QueryRuleEnum.EQ;
}
//update-end--Author:taoyan Date:20201229 forinitQueryWrapper组装sql查询条件错误 #284---------------------
return rule != null ? rule : QueryRuleEnum.EQ;
}
@ -568,10 +525,11 @@ public class QueryGenerator {
return value;
}
String val = (value + "").toString().trim();
// 代码逻辑说明: 查询条件的值为等号(=bug #3443
//update-begin-author:taoyan date:20220302 for: 查询条件的值为等号(=bug #3443
if(QueryRuleEnum.EQ.getValue().equals(val)){
return val;
}
//update-end-author:taoyan date:20220302 for: 查询条件的值为等号(=bug #3443
if (rule == QueryRuleEnum.LIKE) {
value = val.substring(1, val.length() - 1);
//mysql 模糊查询之特殊字符下划线 _、\
@ -589,19 +547,21 @@ public class QueryGenerator {
} else if (rule == QueryRuleEnum.EQ_WITH_ADD) {
value = val.replaceAll("\\+\\+", COMMA);
}else {
// 代码逻辑说明: initQueryWrapper组装sql查询条件错误 #284-------------------
//update-begin--Author:scott Date:20190724 forinitQueryWrapper组装sql查询条件错误 #284-------------------
if(val.startsWith(rule.getValue())){
//TODO 此处逻辑应该注释掉-> 如果查询内容中带有查询匹配规则符号,就会被截取的(比如:>=您好)
value = val.replaceFirst(rule.getValue(),"");
}else if(val.startsWith(rule.getCondition()+QUERY_SEPARATE_KEYWORD)){
value = val.replaceFirst(rule.getCondition()+QUERY_SEPARATE_KEYWORD,"").trim();
}
//update-end--Author:scott Date:20190724 forinitQueryWrapper组装sql查询条件错误 #284-------------------
}
return value;
}
private static void addQueryByRule(QueryWrapper<?> queryWrapper,String name,String type,String value,QueryRuleEnum rule) throws ParseException {
if(oConvertUtils.isNotEmpty(value)) {
//update-begin--Author:sunjianlei Date:20220104 for【JTC-409】修复逗号分割情况下没有转换类型导致类型严格的数据库查询报错 -------------------
// 针对数字类型字段,多值查询
if(value.contains(COMMA)){
Object[] temp = Arrays.stream(value.split(COMMA)).map(v -> {
@ -617,6 +577,7 @@ public class QueryGenerator {
}
Object temp = QueryGenerator.parseByType(value, type, rule);
addEasyQuery(queryWrapper, name, rule, temp);
//update-end--Author:sunjianlei Date:20220104 for【JTC-409】修复逗号分割情况下没有转换类型导致类型严格的数据库查询报错 -------------------
}
}
@ -693,11 +654,7 @@ public class QueryGenerator {
* @param value 查询条件值
*/
public static void addEasyQuery(QueryWrapper<?> queryWrapper, String name, QueryRuleEnum rule, Object value) {
if (
(
name==null || value == null || rule == null || oConvertUtils.isEmpty(value)
)
&& !QueryRuleEnum.EMPTY.equals(rule)) {
if (name==null || value == null || rule == null || oConvertUtils.isEmpty(value)) {
return;
}
name = oConvertUtils.camelToUnderline(name);
@ -709,9 +666,6 @@ public class QueryGenerator {
case GE:
queryWrapper.ge(name, value);
break;
case EMPTY:
queryWrapper.isNull(name);
break;
case LT:
queryWrapper.lt(name, value);
break;
@ -731,12 +685,13 @@ public class QueryGenerator {
}else if(value instanceof String[]) {
queryWrapper.in(name, (Object[]) value);
}
// 代码逻辑说明: 【bug】in 类型多值查询 不适配postgresql #1671
//update-begin-author:taoyan date:20200909 for:【bug】in 类型多值查询 不适配postgresql #1671
else if(value.getClass().isArray()) {
queryWrapper.in(name, (Object[])value);
}else {
queryWrapper.in(name, value);
}
//update-end-author:taoyan date:20200909 for:【bug】in 类型多值查询 不适配postgresql #1671
break;
case LIKE:
queryWrapper.like(name, value);
@ -753,7 +708,7 @@ public class QueryGenerator {
case NOT_RIGHT_LIKE:
queryWrapper.notLikeRight(name, value);
break;
// 代码逻辑说明: [TV360X-378]下拉多框根据条件查询不出来:增加自定义字段查询规则功能------------
//update-begin---author:chenrui ---date:20240527 for[TV360X-378]下拉多框根据条件查询不出来:增加自定义字段查询规则功能------------
case LIKE_WITH_OR:
final String nameFinal = name;
Object[] vals;
@ -762,7 +717,7 @@ public class QueryGenerator {
} else if (value instanceof String[]) {
vals = (Object[]) value;
}
// 代码逻辑说明: 【bug】in 类型多值查询 不适配postgresql #1671
//update-begin-author:taoyan date:20200909 for:【bug】in 类型多值查询 不适配postgresql #1671
else if (value.getClass().isArray()) {
vals = (Object[]) value;
} else {
@ -777,6 +732,7 @@ public class QueryGenerator {
}
});
break;
//update-end---author:chenrui ---date:20240527 for[TV360X-378]下拉多框根据条件查询不出来:增加自定义字段查询规则功能------------
default:
log.info("--查询规则未匹配到---");
break;
@ -790,8 +746,10 @@ public class QueryGenerator {
private static boolean judgedIsUselessField(String name) {
return "class".equals(name) || "ids".equals(name)
|| "page".equals(name) || "rows".equals(name)
//// update-begin--author:sunjianlei date:20240808 for【TV360X-2009】取消过滤 sort、order 字段,防止前端排序报错 ------
//// https://github.com/jeecgboot/JeecgBoot/issues/6937
// || "sort".equals(name) || "order".equals(name)
//// update-end----author:sunjianlei date:20240808 for【TV360X-2009】取消过滤 sort、order 字段,防止前端排序报错 ------
;
}
@ -804,12 +762,13 @@ public class QueryGenerator {
public static Map<String, SysPermissionDataRuleModel> getRuleMap() {
Map<String, SysPermissionDataRuleModel> ruleMap = new HashMap<>(5);
List<SysPermissionDataRuleModel> list = null;
// 代码逻辑说明: QQYUN-5441 【简流】获取多个用户/部门/角色 设置部门查询 报错
//update-begin-author:taoyan date:2023-6-1 for:QQYUN-5441 【简流】获取多个用户/部门/角色 设置部门查询 报错
try {
list = JeecgDataAutorUtils.loadDataSearchConditon();
}catch (Exception e){
log.error("根据request对象获取权限数据失败可能是定时任务中执行的。", e);
}
//update-end-author:taoyan date:2023-6-1 for:QQYUN-5441 【简流】获取多个用户/部门/角色 设置部门查询 报错
if(list != null&&list.size()>0){
if(list.get(0)==null){
return ruleMap;
@ -846,8 +805,9 @@ public class QueryGenerator {
addEasyQuery(queryWrapper, name, rule, DateUtils.str2Date(dateStr,DateUtils.datetimeFormat.get()));
}
}else {
// 代码逻辑说明: [issues/7481]多租户模式下 数据权限使用变量:#{tenant_id} 报错------------
//update-begin---author:chenrui ---date:20241125 for[issues/7481]多租户模式下 数据权限使用变量:#{tenant_id} 报错------------
addEasyQuery(queryWrapper, name, rule, NumberUtils.parseNumber(converRuleValue(dataRule.getRuleValue()), propertyType));
//update-end---author:chenrui ---date:20241125 for[issues/7481]多租户模式下 数据权限使用变量:#{tenant_id} 报错------------
}
}
}
@ -901,8 +861,9 @@ public class QueryGenerator {
return null;
}
Set<String> varParams = new HashSet<String>();
// 代码逻辑说明: [QQYUN-10785]数据权限,查看自己拥有部门的权限中存在问题 #7288------------
//update-begin---author:chenrui ---date:20250108 for[QQYUN-10785]数据权限,查看自己拥有部门的权限中存在问题 #7288------------
String regex = "#\\{\\[*\\w+]*}";
//update-end---author:chenrui ---date:20250108 for[QQYUN-10785]数据权限,查看自己拥有部门的权限中存在问题 #7288------------
Pattern p = Pattern.compile(regex);
Matcher m = p.matcher(sql);
@ -958,8 +919,9 @@ public class QueryGenerator {
Class propType = origDescriptors[i].getPropertyType();
boolean isString = propType.equals(String.class);
Object value;
// 代码逻辑说明: [TV360X-539]数据权限,配置日期等于条件时后端报转换错误------------
//update-begin---author:chenrui ---date:20240527 for[TV360X-539]数据权限,配置日期等于条件时后端报转换错误------------
if(isString || Date.class.equals(propType)) {
//update-end---author:chenrui ---date:20240527 for[TV360X-539]数据权限,配置日期等于条件时后端报转换错误------------
value = converRuleValue(dataRule.getRuleValue());
}else {
value = NumberUtils.parseNumber(dataRule.getRuleValue(),propType);

View File

@ -1,5 +1,7 @@
package org.jeecg.common.system.util;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
@ -10,9 +12,9 @@ import com.google.common.base.Joiner;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Date;
import java.util.Objects;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
@ -20,7 +22,7 @@ import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.SecurityUtils;
import org.jeecg.common.api.CommonAPI;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.constant.CommonConstant;
import org.jeecg.common.constant.DataBaseConstant;
@ -32,6 +34,22 @@ import org.jeecg.common.system.vo.SysUserCacheInfo;
import org.jeecg.common.util.DateUtils;
import org.jeecg.common.util.SpringContextUtils;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.config.security.self.SelfAuthenticationProvider;
import org.jeecg.config.security.self.SelfAuthenticationToken;
import org.jeecg.config.security.utils.SecureUtil;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.core.*;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AccessTokenAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.context.AuthorizationServerContextHolder;
import org.springframework.security.oauth2.server.authorization.token.DefaultOAuth2TokenContext;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenContext;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
/**
* @Author Scott
@ -41,36 +59,36 @@ import org.jeecg.common.util.oConvertUtils;
@Slf4j
public class JwtUtil {
/**PC端Token有效期为7天Token在reids中缓存时间为两倍*/
public static final long EXPIRE_TIME = (7 * 12) * 60 * 60 * 1000L;
/**APP端Token有效期为30天Token在reids中缓存时间为两倍*/
public static final long APP_EXPIRE_TIME = (30 * 12) * 60 * 60 * 1000L;
/**Token有效期为7天Token在reids中缓存时间为两倍*/
public static final long EXPIRE_TIME = (7 * 12) * 60 * 60 * 1000;
static final String WELL_NUMBER = SymbolConstant.WELL_NUMBER + SymbolConstant.LEFT_CURLY_BRACKET;
public static final String DEFAULT_CLIENT = "jeecg-client";
/**
*
* @param response
* @param code
* @param errorMsg
*/
public static void responseError(HttpServletResponse response, Integer code, String errorMsg) {
try {
Result jsonResult = new Result(code, errorMsg);
jsonResult.setSuccess(false);
// 设置响应头和内容类型
response.setStatus(code);
response.setHeader("Content-type", "text/html;charset=UTF-8");
response.setContentType("application/json;charset=UTF-8");
// 使用 ObjectMapper 序列化为 JSON 字符串
ObjectMapper objectMapper = new ObjectMapper();
String json = objectMapper.writeValueAsString(jsonResult);
response.getWriter().write(json);
response.getWriter().flush();
} catch (IOException e) {
public static void responseError(ServletResponse response, Integer code, String errorMsg) {
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
// issues/I4YH95浏览器显示乱码问题
httpServletResponse.setHeader("Content-type", "text/html;charset=UTF-8");
Result jsonResult = new Result(code, errorMsg);
jsonResult.setSuccess(false);
OutputStream os = null;
try {
os = httpServletResponse.getOutputStream();
httpServletResponse.setCharacterEncoding("UTF-8");
httpServletResponse.setStatus(code);
os.write(new ObjectMapper().writeValueAsString(jsonResult).getBytes("UTF-8"));
os.flush();
os.close();
} catch (IOException e) {
log.error(e.getMessage(), e);
}
}
}
}
/**
* 校验token是否正确
@ -82,13 +100,12 @@ public class JwtUtil {
public static boolean verify(String token, String username, String secret) {
try {
// 根据密码生成JWT效验器
Algorithm algorithm = Algorithm.HMAC256(secret);
JWTVerifier verifier = JWT.require(algorithm).withClaim("username", username).build();
JwtDecoder jwtDecoder = SpringContextUtils.getBean(JwtDecoder.class);
// 效验TOKEN
DecodedJWT jwt = verifier.verify(token);
jwtDecoder.decode(token);
return true;
} catch (Exception e) {
log.warn("Token验证失败" + e.getMessage(),e);
log.error(e.getMessage(), e);
return false;
}
}
@ -103,88 +120,32 @@ public class JwtUtil {
DecodedJWT jwt = JWT.decode(token);
return jwt.getClaim("username").asString();
} catch (JWTDecodeException e) {
log.error(e.getMessage(), e);
log.warn(e.getMessage(), e);
return null;
}
}
/**
* 生成签名,5min后过期
* 生成token
*
* @param username 用户名
* @param secret 用户的密码
* @return 加密的token
* @deprecated 请使用sign(String username, String secret, String clientType)方法代替
*/
@Deprecated
public static String sign(String username, String secret) {
Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
Algorithm algorithm = Algorithm.HMAC256(secret);
// 附带username信息
return JWT.create().withClaim("username", username).withExpiresAt(date).sign(algorithm);
Map<String, Object> additionalParameter = new HashMap<>();
additionalParameter.put("username", username);
}
RegisteredClientRepository registeredClientRepository = SpringContextUtils.getBean(RegisteredClientRepository.class);
SelfAuthenticationProvider selfAuthenticationProvider = SpringContextUtils.getBean(SelfAuthenticationProvider.class);
OAuth2ClientAuthenticationToken client = new OAuth2ClientAuthenticationToken(Objects.requireNonNull(registeredClientRepository.findByClientId("jeecg-client")), ClientAuthenticationMethod.CLIENT_SECRET_BASIC, null);
client.setAuthenticated(true);
SelfAuthenticationToken selfAuthenticationToken = new SelfAuthenticationToken(client, additionalParameter);
selfAuthenticationToken.setAuthenticated(true);
OAuth2AccessTokenAuthenticationToken accessToken = (OAuth2AccessTokenAuthenticationToken) selfAuthenticationProvider.authenticate(selfAuthenticationToken);
return accessToken.getAccessToken().getTokenValue();
/**
* 生成签名,5min后过期
*
* @param username 用户名
* @param secret 用户的密码
* @param expireTime 过期时间
* @return 加密的token
* @deprecated 请使用sign(String username, String secret, String clientType)方法代替
*/
@Deprecated
public static String sign(String username, String secret, Long expireTime) {
Date date = new Date(System.currentTimeMillis() + expireTime);
Algorithm algorithm = Algorithm.HMAC256(secret);
// 附带username信息
return JWT.create().withClaim("username", username).withExpiresAt(date).sign(algorithm);
}
/**
* 生成签名,根据客户端类型自动选择过期时间
* for [JHHB-1030]【鉴权】移动端用户token到期后续期时间变成pc端时长
*
* @param username 用户名
* @param secret 用户的密码
* @param clientType 客户端类型PC或APP
* @return 加密的token
*/
public static String sign(String username, String secret, String clientType) {
// 根据客户端类型选择对应的过期时间
long expireTime = CommonConstant.CLIENT_TYPE_APP.equalsIgnoreCase(clientType)
? APP_EXPIRE_TIME
: EXPIRE_TIME;
Date date = new Date(System.currentTimeMillis() + expireTime);
Algorithm algorithm = Algorithm.HMAC256(secret);
// 附带username和clientType信息
return JWT.create()
.withClaim("username", username)
.withClaim("clientType", clientType)
.withExpiresAt(date)
.sign(algorithm);
}
/**
* 从token中获取客户端类型
* for [JHHB-1030]【鉴权】移动端用户token到期后续期时间变成pc端时长
*
* @param token JWT token
* @return 客户端类型如果不存在则返回PC兼容旧token
*/
public static String getClientType(String token) {
try {
DecodedJWT jwt = JWT.decode(token);
String clientType = jwt.getClaim("clientType").asString();
// 如果clientType为空返回默认值PC兼容旧token
return oConvertUtils.isNotEmpty(clientType) ? clientType : CommonConstant.CLIENT_TYPE_PC;
} catch (JWTDecodeException e) {
log.warn("解析token中的clientType失败使用默认值PC" + e.getMessage());
return CommonConstant.CLIENT_TYPE_PC;
}
}
/**
@ -248,7 +209,7 @@ public class JwtUtil {
//2.通过shiro获取登录用户信息
LoginUser sysUser = null;
try {
sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal();
sysUser = SecureUtil.currentUser();
} catch (Exception e) {
log.warn("SecurityUtils.getSubject() 获取用户信息异常:" + e.getMessage());
}
@ -266,14 +227,14 @@ public class JwtUtil {
} else {
key = key;
}
//update-begin---author:chenrui ---date:20250107 for[QQYUN-10785]数据权限,查看自己拥有部门的权限中存在问题 #7288------------
// 是否存在字符串标志
boolean multiStr;
boolean multiStr = false;
if(oConvertUtils.isNotEmpty(key) && key.trim().matches("^\\[\\w+]$")){
key = key.substring(1,key.length()-1);
multiStr = true;
} else {
multiStr = false;
}
}
//update-end---author:chenrui ---date:20250107 for[QQYUN-10785]数据权限,查看自己拥有部门的权限中存在问题 #7288------------
//替换为当前系统时间(年月日)
if (key.equals(DataBaseConstant.SYS_DATE)|| key.toLowerCase().equals(DataBaseConstant.SYS_DATE_TABLE)) {
returnValue = DateUtils.formatDate();
@ -342,25 +303,22 @@ public class JwtUtil {
if(user==null){
//TODO 暂时使用用户登录部门,存在逻辑缺陷,不是用户所拥有的部门
returnValue = sysUser.getOrgCode();
// 代码逻辑说明: [QQYUN-10785]数据权限,查看自己拥有部门的权限中存在问题 #7288------------
//update-begin---author:chenrui ---date:20250107 for[QQYUN-10785]数据权限,查看自己拥有部门的权限中存在问题 #7288------------
returnValue = multiStr ? "'" + returnValue + "'" : returnValue;
//update-end---author:chenrui ---date:20250107 for[QQYUN-10785]数据权限,查看自己拥有部门的权限中存在问题 #7288------------
}else{
if(user.isOneDepart()) {
returnValue = user.getSysMultiOrgCode().get(0);
// 代码逻辑说明: [QQYUN-10785]数据权限,查看自己拥有部门的权限中存在问题 #7288------------
//update-begin---author:chenrui ---date:20250107 for[QQYUN-10785]数据权限,查看自己拥有部门的权限中存在问题 #7288------------
returnValue = multiStr ? "'" + returnValue + "'" : returnValue;
//update-end---author:chenrui ---date:20250107 for[QQYUN-10785]数据权限,查看自己拥有部门的权限中存在问题 #7288------------
}else {
// 代码逻辑说明: [QQYUN-10785]数据权限,查看自己拥有部门的权限中存在问题 #7288------------
//update-begin---author:chenrui ---date:20250107 for[QQYUN-10785]数据权限,查看自己拥有部门的权限中存在问题 #7288------------
returnValue = user.getSysMultiOrgCode().stream()
.filter(Objects::nonNull)
.map(orgCode -> {
if (multiStr) {
return "'" + orgCode + "'";
} else {
return orgCode;
}
})
.map(orgCode -> "'" + orgCode + "'")
.collect(Collectors.joining(", "));
//update-end---author:chenrui ---date:20250107 for[QQYUN-10785]数据权限,查看自己拥有部门的权限中存在问题 #7288------------
}
}
}
@ -374,7 +332,7 @@ public class JwtUtil {
}
}
// 代码逻辑说明: 多租户ID作为系统变量
//update-begin-author:taoyan date:20210330 for:多租户ID作为系统变量
else if (key.equals(TenantConstant.TENANT_ID) || key.toLowerCase().equals(TenantConstant.TENANT_ID_TABLE)){
try {
returnValue = SpringContextUtils.getHttpServletRequest().getHeader(CommonConstant.TENANT_ID);
@ -382,6 +340,7 @@ public class JwtUtil {
log.warn("获取系统租户异常:" + e.getMessage());
}
}
//update-end-author:taoyan date:20210330 for:多租户ID作为系统变量
if(returnValue!=null){returnValue = returnValue + moshi;}
return returnValue;
}

View File

@ -13,33 +13,31 @@ import org.springframework.core.type.classreading.CachingMetadataReaderFactory;
import org.springframework.core.type.classreading.MetadataReader;
import org.springframework.core.type.classreading.MetadataReaderFactory;
import org.springframework.util.ClassUtils;
import java.lang.reflect.Method;
import java.util.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 枚举字典数据 资源加载工具类
*
* 资源加载工具类
* @Author taoYan
* @Date 2022/7/8 10:40
**/
@Slf4j
public class ResourceUtil {
/**
* 多个包扫描根路径
*
* 之所以让用户手工配置扫描路径,是为了避免不必要的类加载开销,提升启动性能。
* 请务必将所有枚举类所在包路径添加到此配置中。
*/
private final static String[] BASE_SCAN_PACKAGES = {
"org.jeecg.common.constant.enums",
"org.jeecg.modules.message.enums"
};
/**
* 枚举字典数据
*/
private final static Map<String, List<DictModel>> enumDictData = new HashMap<>(5);
/**
* 所有java类
*/
private final static String CLASS_PATTERN="/**/*.class";
/**
* 所有枚举java类
*/
@ -47,9 +45,9 @@ public class ResourceUtil {
private final static String CLASS_ENUM_PATTERN="/**/*Enum.class";
/**
* 初始化状态标识
* 包路径 org.jeecg
*/
private static volatile boolean initialized = false;
private final static String BASE_PACKAGE = "org.jeecg";
/**
* 枚举类中获取字典数据的方法名
@ -57,135 +55,59 @@ public class ResourceUtil {
private final static String METHOD_NAME = "getDictList";
/**
* 获取枚举字典数据
* 获取枚举类对应的字典数据 SysDictServiceImpl#queryAllDictItems()
*
* @return 枚举字典数据
* @return
*/
public static Map<String, List<DictModel>> getEnumDictData() {
if (!initialized) {
synchronized (ResourceUtil.class) {
if (!initialized) {
long startTime = System.currentTimeMillis();
log.debug("【枚举字典加载】开始初始化枚举字典数据...");
initEnumDictData();
initialized = true;
long endTime = System.currentTimeMillis();
log.debug("【枚举字典加载】枚举字典数据初始化完成,共加载 {} 个字典,总耗时: {}ms", enumDictData.size(), endTime - startTime);
public static Map<String, List<DictModel>> getEnumDictData(){
if(enumDictData.keySet().size()>0){
return enumDictData;
}
ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();
String pattern = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX + ClassUtils.convertClassNameToResourcePath(BASE_PACKAGE) + CLASS_ENUM_PATTERN;
try {
Resource[] resources = resourcePatternResolver.getResources(pattern);
MetadataReaderFactory readerFactory = new CachingMetadataReaderFactory(resourcePatternResolver);
for (Resource resource : resources) {
MetadataReader reader = readerFactory.getMetadataReader(resource);
String classname = reader.getClassMetadata().getClassName();
Class<?> clazz = Class.forName(classname);
EnumDict enumDict = clazz.getAnnotation(EnumDict.class);
if (enumDict != null) {
EnumDict annotation = clazz.getAnnotation(EnumDict.class);
String key = annotation.value();
if(oConvertUtils.isNotEmpty(key)){
List<DictModel> list = (List<DictModel>) clazz.getDeclaredMethod(METHOD_NAME).invoke(null);
enumDictData.put(key, list);
}
}
}
}catch (Exception e){
log.error("获取枚举类字典数据异常", e.getMessage());
// e.printStackTrace();
}
return enumDictData;
}
/**
* 使用多包路径扫描方式初始化枚举字典数据
*/
private static void initEnumDictData() {
ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();
long scanStartTime = System.currentTimeMillis();
List<Resource> allResources = new ArrayList<>();
// 扫描多个包路径
for (String basePackage : BASE_SCAN_PACKAGES) {
String pattern = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX + ClassUtils.convertClassNameToResourcePath(basePackage) + CLASS_ENUM_PATTERN;
try {
Resource[] resources = resourcePatternResolver.getResources(pattern);
allResources.addAll(Arrays.asList(resources));
log.debug("【枚举字典加载】扫描包 {} 找到 {} 个枚举类文件", basePackage, resources.length);
} catch (Exception e) {
log.warn("【枚举字典加载】扫描包 {} 时出现异常: {}", basePackage, e.getMessage());
}
}
long scanEndTime = System.currentTimeMillis();
log.debug("【枚举字典加载】文件扫描完成,总共找到 {} 个枚举类文件,扫描耗时: {}ms", allResources.size(), scanEndTime - scanStartTime);
MetadataReaderFactory readerFactory = new CachingMetadataReaderFactory(resourcePatternResolver);
long processStartTime = System.currentTimeMillis();
int processedCount = 0;
for (Resource resource : allResources) {
try {
MetadataReader reader = readerFactory.getMetadataReader(resource);
String classname = reader.getClassMetadata().getClassName();
// 提前检查是否有@EnumDict注解避免不必要的Class.forName
if (hasEnumDictAnnotation(reader)) {
processEnumClass(classname);
processedCount++;
}
} catch (Exception e) {
log.debug("处理资源异常: {} - {}", resource.getFilename(), e.getMessage());
}
}
long processEndTime = System.currentTimeMillis();
log.debug("【枚举字典加载】处理完成,实际处理 {} 个带注解的枚举类,处理耗时: {}ms", processedCount, processEndTime - processStartTime);
}
/**
* 检查类是否有EnumDict注解通过元数据避免类加载
*/
private static boolean hasEnumDictAnnotation(MetadataReader reader) {
try {
return reader.getAnnotationMetadata().hasAnnotation(EnumDict.class.getName());
} catch (Exception e) {
return false;
}
}
/**
* 处理单个枚举类
*/
private static void processEnumClass(String classname) {
try {
Class<?> clazz = Class.forName(classname);
EnumDict enumDict = clazz.getAnnotation(EnumDict.class);
if (enumDict != null) {
String key = enumDict.value();
if (oConvertUtils.isNotEmpty(key)) {
Method method = clazz.getDeclaredMethod(METHOD_NAME);
List<DictModel> list = (List<DictModel>) method.invoke(null);
enumDictData.put(key, list);
log.debug("成功加载枚举字典: {} -> {}", key, classname);
}
}
} catch (Exception e) {
log.debug("处理枚举类异常: {} - {}", classname, e.getMessage());
}
}
/**
* 用于后端字典翻译 SysDictServiceImpl#queryManyDictByKeys(java.util.List, java.util.List)
*
* @param dictCodeList 字典编码列表
* @param keys 键值列表
* @return 字典数据映射
* @param dictCodeList
* @param keys
* @return
*/
public static Map<String, List<DictModel>> queryManyDictByKeys(List<String> dictCodeList, List<String> keys) {
Map<String, List<DictModel>> enumDict = getEnumDictData();
public static Map<String, List<DictModel>> queryManyDictByKeys(List<String> dictCodeList, List<String> keys){
if(enumDictData.keySet().size()==0){
getEnumDictData();
}
Map<String, List<DictModel>> map = new HashMap<>();
// 使用更高效的查找方式
Set<String> dictCodeSet = new HashSet<>(dictCodeList);
Set<String> keySet = new HashSet<>(keys);
for (String code : enumDict.keySet()) {
if (dictCodeSet.contains(code)) {
List<DictModel> dictItemList = enumDict.get(code);
for (DictModel dm : dictItemList) {
for (String code : enumDictData.keySet()) {
if(dictCodeList.indexOf(code)>=0){
List<DictModel> dictItemList = enumDictData.get(code);
for(DictModel dm: dictItemList){
String value = dm.getValue();
if (keySet.contains(value)) {
if(keys.indexOf(value)>=0){
List<DictModel> list = new ArrayList<>();
list.add(new DictModel(value, dm.getText()));
map.put(code, list);
map.put(code,list);
break;
}
}
@ -193,5 +115,22 @@ public class ResourceUtil {
}
return map;
}
}
/**
* 获取实现类
*
* @param classPath
*/
public static Object getImplementationClass(String classPath){
try {
Class<?> aClass = Class.forName(classPath);
return SpringContextUtils.getBean(aClass);
} catch (ClassNotFoundException e) {
log.error("类没有找到",e);
return null;
} catch (NoSuchBeanDefinitionException e){
log.error(classPath + "没有实现",e);
return null;
}
}
}

View File

@ -1,22 +1,13 @@
package org.jeecg.common.system.util;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.metadata.OrderItem;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.constant.CommonConstant;
import org.jeecg.common.constant.DataBaseConstant;
import org.jeecg.common.constant.SymbolConstant;
import org.jeecg.common.exception.JeecgBootException;
import org.jeecg.common.system.query.QueryGenerator;
import org.jeecg.common.system.query.QueryRuleEnum;
import org.jeecg.common.util.CommonUtils;
import org.jeecg.common.util.oConvertUtils;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
/**
@ -150,7 +141,7 @@ public class SqlConcatUtil {
}
private static String getInConditionValue(Object value,boolean isString) {
// 代码逻辑说明: 查询条件如果输入,导致sql报错
//update-begin-author:taoyan date:20210628 for: 查询条件如果输入,导致sql报错
String[] temp = value.toString().split(",");
if(temp.length==0){
return "('')";
@ -168,6 +159,7 @@ public class SqlConcatUtil {
}else {
return "("+value.toString()+")";
}
//update-end-author:taoyan date:20210628 for: 查询条件如果输入,导致sql报错
}
/**
@ -214,6 +206,7 @@ public class SqlConcatUtil {
}
}else {
//update-begin-author:taoyan date:2022-6-30 for: issues/3810 数据权限规则问题
// 走到这里说明 value不带有任何模糊查询的标识(*或者%)
if (ruleEnum == QueryRuleEnum.LEFT_LIKE) {
if (DataBaseConstant.DB_TYPE_SQLSERVER.equals(getDbType())) {
@ -234,6 +227,7 @@ public class SqlConcatUtil {
return "'%" + str + "%'";
}
}
//update-end-author:taoyan date:2022-6-30 for: issues/3810 数据权限规则问题
}
}
@ -245,47 +239,5 @@ public class SqlConcatUtil {
private static String getDbType() {
return CommonUtils.getDatabaseType();
}
/**
* 获取前端传过来的 "多字段排序信息: sortInfoString"
* @return
*/
public static List<OrderItem> getQueryConditionOrders(String column, String order, String queryInfoString){
List<OrderItem> list = new ArrayList<>();
if(oConvertUtils.isEmpty(queryInfoString)){
//默认以创建时间倒序查询
if(CommonConstant.ORDER_TYPE_DESC.equalsIgnoreCase(order)){
list.add(OrderItem.desc(column));
}else{
list.add(OrderItem.asc(column));
}
}else{
// 【TV360X-967】URL解码微服务下需要
if (queryInfoString.contains("%22column%22")) {
log.info("queryInfoString 原生 = {}", queryInfoString);
try {
queryInfoString = URLDecoder.decode(queryInfoString, "UTF-8");
} catch (UnsupportedEncodingException e) {
throw new JeecgBootException(e);
}
log.info("queryInfoString 解码 = {}", queryInfoString);
}
JSONArray array = JSONArray.parseArray(queryInfoString);
Iterator it = array.iterator();
while(it.hasNext()){
JSONObject json = (JSONObject)it.next();
String tempColumn = json.getString("column");
if(oConvertUtils.isNotEmpty(tempColumn)){
String tempOrder = json.getString("order");
if(CommonConstant.ORDER_TYPE_DESC.equalsIgnoreCase(tempOrder)){
list.add(OrderItem.desc(tempColumn));
}else{
list.add(OrderItem.asc(tempColumn));
}
}
}
}
return list;
}
}

View File

@ -1,13 +1,18 @@
package org.jeecg.common.system.vo;
import com.alibaba.fastjson2.JSON;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import org.jeecg.common.desensitization.annotation.SensitiveField;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.io.Serializable;
import java.util.Date;
import java.util.Set;
/**
* <p>
@ -20,8 +25,10 @@ import java.util.Date;
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
public class LoginUser {
public class LoginUser implements Serializable {
private static final long serialVersionUID = -7143159031677245866L;
/**
* 登录人id
*/
@ -68,12 +75,6 @@ public class LoginUser {
@SensitiveField
private String avatar;
/**
* 工号
*/
@SensitiveField
private String workNo;
/**
* 生日
*/
@ -144,8 +145,29 @@ public class LoginUser {
/**设备id uniapp推送用*/
private String clientId;
/**
* 主岗位
*/
private String mainDepPostId;
@SensitiveField
private String salt;
@Override
public String toString() {
// 重新构建对象过滤一些敏感字段
LoginUser loginUser = new LoginUser();
loginUser.setId(id);
loginUser.setUsername(username);
loginUser.setRealname(realname);
loginUser.setOrgCode(orgCode);
loginUser.setSex(sex);
loginUser.setEmail(email);
loginUser.setPhone(phone);
loginUser.setDelFlag(delFlag);
loginUser.setStatus(status);
loginUser.setActivitiSync(activitiSync);
loginUser.setUserIdentity(userIdentity);
loginUser.setDepartIds(departIds);
loginUser.setPost(post);
loginUser.setTelephone(telephone);
loginUser.setRelTenantIds(relTenantIds);
loginUser.setClientId(clientId);
return JSON.toJSONString(loginUser);
}
}

View File

@ -1,239 +0,0 @@
package org.jeecg.common.util;
import org.jeecg.common.exception.JeecgBootAssertException;
/**
* 断言检查工具
* for for [QQYUN-10990]AIRAG
* @author chenrui
* @date 2017-06-22 10:05:56
*/
public class AssertUtils {
/**
* 确保对象为空,如果不为空抛出异常
*
* @param msg
* @param obj
* @throws JeecgBootAssertException
* @author chenrui
* @date 2017-06-22 10:05:56
*/
public static void assertEmpty(String msg, Object obj) {
if (oConvertUtils.isObjectNotEmpty(obj)) {
throw new JeecgBootAssertException(msg);
}
}
/**
* 确保对象不为空,如果为空抛出异常
*
* @param msg
* @param obj
* @throws JeecgBootAssertException
* @author chenrui
* @date 2017-06-22 10:05:56
*/
public static void assertNotEmpty(String msg, Object obj) {
if (oConvertUtils.isObjectEmpty(obj)) {
throw new JeecgBootAssertException(msg);
}
}
/**
* 验证对象是否相同
*
* @param message
* @param expected
* @param actual
* @author chenrui
* @date 2018/9/12 15:45
*/
public static void assertEquals(String message, Object expected,
Object actual) {
if (oConvertUtils.isEqual(expected, actual)) {
return;
}
throw new JeecgBootAssertException(message);
}
/**
* 验证不相同
*
* @param message
* @param expected
* @param actual
* @author chenrui
* @date 2018/9/12 15:45
*/
public static void assertNotEquals(String message, Object expected,
Object actual) {
if (oConvertUtils.isEqual(expected, actual)) {
throw new JeecgBootAssertException(message);
}
}
/**
* 验证是否相等
*
* @param message
* @param expected
* @param actual
* @author chenrui
* @date 2018/9/12 15:45
*/
public static void assertSame(String message, Object expected,
Object actual) {
if (expected == actual) {
return;
}
throw new JeecgBootAssertException(message);
}
/**
* 验证不相等
*
* @param message
* @param unexpected
* @param actual
* @author chenrui
* @date 2018/9/12 15:45
*/
public static void assertNotSame(String message, Object unexpected,
Object actual) {
if (unexpected == actual) {
throw new JeecgBootAssertException(message);
}
}
/**
* 验证是否为真
*
* @param message
* @param condition
*/
public static void assertTrue(String message, boolean condition) {
if (!condition) {
throw new JeecgBootAssertException(message);
}
}
/**
* 验证 condition是否为false
*
* @param message
* @param condition
*/
public static void assertFalse(String message, boolean condition) {
assertTrue(message, !condition);
}
/**
* 验证是否存在
*
* @param message
* @param obj
* @param objs
* @param <T>
* @throws JeecgBootAssertException
* @author chenrui
* @date 2018/1/31 22:14
*/
public static <T> void assertIn(String message, T obj, T... objs) {
assertNotEmpty(message, obj);
assertNotEmpty(message, objs);
if (!oConvertUtils.isIn(obj, objs)) {
throw new JeecgBootAssertException(message);
}
}
/**
* 验证是否不存在
*
* @param message
* @param obj
* @param objs
* @param <T>
* @throws JeecgBootAssertException
* @author chenrui
* @date 2018/1/31 22:14
*/
public static <T> void assertNotIn(String message, T obj, T... objs) {
assertNotEmpty(message, obj);
assertNotEmpty(message, objs);
if (oConvertUtils.isIn(obj, objs)) {
throw new JeecgBootAssertException(message);
}
}
/**
* 确保src大于des
*
* @param message
* @param src
* @param des
* @author chenrui
* @date 2018/9/19 15:30
*/
public static void assertGt(String message, Number src, Number des) {
if (oConvertUtils.isGt(src, des)) {
return;
}
throw new JeecgBootAssertException(message);
}
/**
* 确保src大于等于des
*
* @param message
* @param src
* @param des
* @author chenrui
* @date 2018/9/19 15:30
*/
public static void assertGe(String message, Number src, Number des) {
if (oConvertUtils.isGe(src, des)) {
return;
}
throw new JeecgBootAssertException(message);
}
/**
* 确保src小于des
*
* @param message
* @param src
* @param des
* @author chenrui
* @date 2018/9/19 15:30
*/
public static void assertLt(String message, Number src, Number des) {
if (oConvertUtils.isGe(src, des)) {
throw new JeecgBootAssertException(message);
}
}
/**
* 确保src小于等于des
*
* @param message
* @param src
* @param des
* @author chenrui
* @date 2018/9/19 15:30
*/
public static void assertLe(String message, Number src, Number des) {
if (oConvertUtils.isGt(src, des)) {
throw new JeecgBootAssertException(message);
}
}
}

View File

@ -25,7 +25,6 @@ import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.SQLException;
@ -153,9 +152,9 @@ public class CommonUtils {
*/
public static String uploadLocal(MultipartFile mf,String bizPath,String uploadpath){
try {
// 文件安全校验,防止上传漏洞文件
SsrfFileTypeFilter.checkUploadFileType(mf, bizPath);
//update-begin-author:liusq date:20210809 for: 过滤上传文件类型
SsrfFileTypeFilter.checkUploadFileType(mf);
//update-end-author:liusq date:20210809 for: 过滤上传文件类型
String fileName = null;
File file = new File(uploadpath + File.separator + bizPath + File.separator );
if (!file.exists()) {
@ -164,10 +163,6 @@ public class CommonUtils {
}
// 获取文件名
String orgName = mf.getOriginalFilename();
// 无中文情况下进行转码
if (orgName != null && !CommonUtils.ifContainChinese(orgName)) {
orgName = new String(orgName.getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8);
}
orgName = CommonUtils.getFileName(orgName);
if(orgName.indexOf(SymbolConstant.SPOT)!=-1){
fileName = orgName.substring(0, orgName.lastIndexOf(".")) + "_" + System.currentTimeMillis() + orgName.substring(orgName.lastIndexOf("."));
@ -247,10 +242,6 @@ public class CommonUtils {
try {
DataSource dataSource = SpringContextUtils.getApplicationContext().getBean(DataSource.class);
dbTypeEnum = JdbcUtils.getDbType(dataSource.getConnection().getMetaData().getURL());
//【采用SQL_SERVER2005引擎】QQYUN-13298 解决升级mybatisPlus后SqlServer分页使用OFFSET无排序字段报错问题
if (dbTypeEnum == DbType.SQL_SERVER) {
dbTypeEnum = DbType.SQL_SERVER2005;
}
return dbTypeEnum;
} catch (SQLException e) {
log.warn(e.getMessage(), e);

View File

@ -13,8 +13,6 @@ import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.temporal.ChronoUnit;
import java.util.List;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
@ -816,44 +814,4 @@ public class DateUtils extends PropertyEditorSupport {
return calendar1.get(Calendar.YEAR) == calendar2.get(Calendar.YEAR);
}
/**
* 获取两个日期之间的所有日期列表,包含开始和结束日期
*
* @param begin
* @param end
* @return
*/
public static List<Date> getDateRangeList(Date begin, Date end) {
List<Date> dateList = new ArrayList<>();
if (begin == null || end == null) {
return dateList;
}
// 清除时间部分,只比较日期
Calendar beginCal = Calendar.getInstance();
beginCal.setTime(begin);
beginCal.set(Calendar.HOUR_OF_DAY, 0);
beginCal.set(Calendar.MINUTE, 0);
beginCal.set(Calendar.SECOND, 0);
beginCal.set(Calendar.MILLISECOND, 0);
Calendar endCal = Calendar.getInstance();
endCal.setTime(end);
endCal.set(Calendar.HOUR_OF_DAY, 0);
endCal.set(Calendar.MINUTE, 0);
endCal.set(Calendar.SECOND, 0);
endCal.set(Calendar.MILLISECOND, 0);
if (endCal.before(beginCal)) {
return dateList;
}
dateList.add(beginCal.getTime());
while (beginCal.before(endCal)) {
beginCal.add(Calendar.DAY_OF_YEAR, 1);
dateList.add(beginCal.getTime());
}
return dateList;
}
}

View File

@ -10,9 +10,7 @@ import com.aliyuncs.exceptions.ClientException;
import com.aliyuncs.profile.DefaultProfile;
import com.aliyuncs.profile.IClientProfile;
import org.apache.commons.lang3.StringUtils;
import org.jeecg.common.constant.CommonConstant;
import org.jeecg.common.constant.enums.DySmsEnum;
import org.jeecg.config.JeecgBaseConfig;
import org.jeecg.config.JeecgSmsTemplateConfig;
import org.jeecg.config.StaticConfig;
import org.slf4j.Logger;
@ -63,21 +61,17 @@ public class DySmsHelper {
public static boolean sendSms(String phone, JSONObject templateParamJson, DySmsEnum dySmsEnum) throws ClientException {
JeecgBaseConfig config = SpringContextUtils.getBean(JeecgBaseConfig.class);
String smsSendType = config.getSmsSendType();
if(oConvertUtils.isNotEmpty(smsSendType) && CommonConstant.SMS_SEND_TYPE_TENCENT.equals(smsSendType)){
return TencentSms.sendTencentSms(phone, templateParamJson, config.getTencent(), dySmsEnum);
}
//可自助调整超时时间
//可自助调整超时时间
System.setProperty("sun.net.client.defaultConnectTimeout", "10000");
System.setProperty("sun.net.client.defaultReadTimeout", "10000");
// 代码逻辑说明: 配置类数据获取
//update-begin-authortaoyan date:20200811 for:配置类数据获取
StaticConfig staticConfig = SpringContextUtils.getBean(StaticConfig.class);
//logger.info("阿里大鱼短信秘钥 accessKeyId" + staticConfig.getAccessKeyId());
//logger.info("阿里大鱼短信秘钥 accessKeySecret"+ staticConfig.getAccessKeySecret());
setAccessKeyId(staticConfig.getAccessKeyId());
setAccessKeySecret(staticConfig.getAccessKeySecret());
//update-end-authortaoyan date:20200811 for:配置类数据获取
//初始化acsClient,暂不支持region化
IClientProfile profile = DefaultProfile.getProfile("cn-hangzhou", accessKeyId, accessKeySecret);
@ -87,7 +81,7 @@ public class DySmsHelper {
//验证json参数
validateParam(templateParamJson,dySmsEnum);
// 代码逻辑说明: 【QQYUN-9422】短信模板管理阿里云---
//update-begin---author:wangshuai---date:2024-11-05---for:【QQYUN-9422】短信模板管理阿里云---
String templateCode = dySmsEnum.getTemplateCode();
JeecgSmsTemplateConfig baseConfig = SpringContextUtils.getBean(JeecgSmsTemplateConfig.class);
if(baseConfig != null && CollectionUtil.isNotEmpty(baseConfig.getTemplateCode())){
@ -103,6 +97,7 @@ public class DySmsHelper {
logger.info("yml中读取签名名称{}",baseConfig.getSignature());
signName = baseConfig.getSignature();
}
//update-end---author:wangshuai---date:2024-11-05---for:【QQYUN-9422】短信模板管理阿里云---
//组装请求对象-具体描述见控制台-文档部分内容
SendSmsRequest request = new SendSmsRequest();

View File

@ -1,22 +1,14 @@
package org.jeecg.common.util;
import cn.hutool.core.io.IoUtil;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.io.IOUtils;
import org.jeecg.common.constant.CommonConstant;
import org.jeecg.common.exception.JeecgBootException;
import org.jeecg.common.util.filter.SsrfFileTypeFilter;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.List;
import java.util.zip.ZipEntry;
@ -211,150 +203,4 @@ public class FileDownloadUtils {
dir.mkdirs();
}
}
/**
* 下载单个文件到ZIP流
* 核心功能获取文件流写入ZIP条目
* @param fileUrl 文件URL可以是HTTP URL或本地路径
* @param fileName ZIP内的文件名
* @param zous ZIP输出流
*/
public static void downLoadSingleFile(String fileUrl, String fileName, String uploadUrl,ZipArchiveOutputStream zous) {
InputStream inputStream = null;
try {
// 创建ZIP条目每个文件在ZIP中都是一个独立条目
ZipArchiveEntry entry = new ZipArchiveEntry(fileName);
zous.putArchiveEntry(entry);
// 获取文件输入流:区分普通文件和快捷方式
if (fileUrl.endsWith(".url")) {
// 处理快捷方式:生成.url文件内容
inputStream = FileDownloadUtils.createInternetShortcut(fileName, fileUrl, "");
} else {
// 普通文件下载从URL或本地路径获取流
inputStream = getDownInputStream(fileUrl,uploadUrl);
}
if (inputStream != null) {
// 将文件流写入ZIP
IOUtils.copy(inputStream, zous);
}
// 关闭当前ZIP条目
zous.closeArchiveEntry();
} catch (IOException e) {
log.error("文件下载失败: {}", e);
} finally {
// 确保输入流关闭
IoUtil.close(inputStream);
}
}
/**
* 获取下载文件输入流
* 功能根据URL类型HTTP或本地获取文件流
* @param fileUrl 文件URL支持HTTP和本地路径
* @return 文件输入流失败返回null
*/
public static InputStream getDownInputStream(String fileUrl, String uploadUrl) {
try {
// 处理HTTP URL通过网络下载
if (oConvertUtils.isNotEmpty(fileUrl) && fileUrl.startsWith(CommonConstant.STR_HTTP)) {
URL url = new URL(fileUrl);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setConnectTimeout(5000); // 连接超时5秒
connection.setReadTimeout(30000); // 读取超时30秒
return connection.getInputStream();
} else {
// 处理本地文件:直接读取文件系统
String downloadFilePath = uploadUrl + File.separator + fileUrl;
// 安全检查:防止下载危险文件类型
SsrfFileTypeFilter.checkDownloadFileType(downloadFilePath);
return new BufferedInputStream(new FileInputStream(downloadFilePath));
}
} catch (IOException e) {
// 异常时返回null上层会处理空流情况
return null;
}
}
/**
* 获取文件扩展名
* 功能:从文件名中提取扩展名
* @param fileName 文件名
* @return 文件扩展名(不含点),如"txt"、"png"
*/
public static String getFileExtension(String fileName) {
int dotIndex = fileName.lastIndexOf('.');
return (dotIndex == -1) ? "" : fileName.substring(dotIndex + 1);
}
/**
* 创建快捷方式(.url文件内容
* 功能生成Internet快捷方式文件内容
* @param name 快捷方式名称
* @param url 目标URL地址
* @param icon 图标路径(可选)
* @return 包含.url文件内容的输入流
*/
public static InputStream createInternetShortcut(String name, String url, String icon) {
StringWriter sw = new StringWriter();
try {
// 按照Windows快捷方式格式写入内容
sw.write("[InternetShortcut]\n");
sw.write("URL=" + url + "\n");
if (oConvertUtils.isNotEmpty(icon)) {
sw.write("IconFile=" + icon + "\n");
}
// 将字符串内容转换为输入流
return new ByteArrayInputStream(sw.toString().getBytes(StandardCharsets.UTF_8));
} finally {
IoUtil.close(sw);
}
}
/**
* 从URL中提取文件名
* 功能从HTTP URL或本地路径中提取纯文件名
* @param fileUrl 文件URL
* @return 文件名(不含路径)
*/
public static String getFileNameFromUrl(String fileUrl) {
try {
// 处理HTTP URL从路径部分提取文件名
if (fileUrl.startsWith(CommonConstant.STR_HTTP)) {
URL url = new URL(fileUrl);
String path = url.getPath();
return path.substring(path.lastIndexOf('/') + 1);
}
// 处理本地文件路径:从文件路径提取文件名
return fileUrl.substring(fileUrl.lastIndexOf(File.separator) + 1);
} catch (Exception e) {
// 如果解析失败,使用时间戳作为文件名
return "file_" + System.currentTimeMillis();
}
}
/**
* 生成ZIP中的文件名
* 功能:避免文件名冲突,为多个文件添加序号
* @param fileUrl 文件URL用于提取原始文件名
* @param index 文件序号从0开始
* @param total 文件总数
* @return 处理后的文件名(带序号)
*/
public static String generateFileName(String fileUrl, int index, int total) {
// 从URL中提取原始文件名
String originalFileName = getFileNameFromUrl(fileUrl);
// 如果只有一个文件,直接使用原始文件名
if (total == 1) {
return originalFileName;
}
// 多个文件时,使用序号+原始文件名
String extension = getFileExtension(originalFileName);
String nameWithoutExtension = originalFileName.replace("." + extension, "");
return String.format("%s_%d.%s", nameWithoutExtension, index + 1, extension);
}
}

View File

@ -38,12 +38,14 @@ public class HTMLUtils {
* @return
*/
public static String parseMarkdown(String markdownContent) {
//update-begin---author:wangshuai---date:2024-06-26---for:【TV360X-1344】JDK17 邮箱发送失败,需要换写法---
/*PegDownProcessor pdp = new PegDownProcessor();
return pdp.markdownToHtml(markdownContent);*/
Parser parser = Parser.builder().build();
Node document = parser.parse(markdownContent);
HtmlRenderer renderer = HtmlRenderer.builder().build();
return renderer.render(document);
//update-end---author:wangshuai---date:2024-06-26---for:【TV360X-1344】JDK17 邮箱发送失败,需要换写法---
}
}

View File

@ -55,11 +55,13 @@ public class MinioUtil {
*/
public static String upload(MultipartFile file, String bizPath, String customBucket) throws Exception {
String fileUrl = "";
// 业务路径过滤,防止攻击
//update-begin-author:wangshuai date:20201012 for: 过滤上传文件夹名特殊字符,防止攻击
bizPath = StrAttackFilter.filter(bizPath);
//update-end-author:wangshuai date:20201012 for: 过滤上传文件夹名特殊字符,防止攻击
// 文件安全校验,防止上传漏洞文件
SsrfFileTypeFilter.checkUploadFileType(file, bizPath);
//update-begin-author:liusq date:20210809 for: 过滤上传文件类型
SsrfFileTypeFilter.checkUploadFileType(file);
//update-end-author:liusq date:20210809 for: 过滤上传文件类型
String newBucket = bucketName;
if(oConvertUtils.isNotEmpty(customBucket)){
@ -161,10 +163,11 @@ public class MinioUtil {
public static String getObjectUrl(String bucketName, String objectName, Integer expires) {
initMinio(minioUrl, minioName,minioPass);
try{
// 代码逻辑说明: 获取文件外链报错提示method不能为空导致文件下载和预览失败----
//update-begin---author:liusq Date:20220121 for获取文件外链报错提示method不能为空导致文件下载和预览失败----
GetPresignedObjectUrlArgs objectArgs = GetPresignedObjectUrlArgs.builder().object(objectName)
.bucket(bucketName)
.expiry(expires).method(Method.GET).build();
//update-begin---author:liusq Date:20220121 for获取文件外链报错提示method不能为空导致文件下载和预览失败----
String url = minioClient.getPresignedObjectUrl(objectArgs);
return URLDecoder.decode(url,"UTF-8");
}catch (Exception e){

View File

@ -1,78 +0,0 @@
package org.jeecg.common.util;
import org.apache.commons.fileupload.FileItem;
import org.springframework.web.multipart.MultipartFile;
import java.io.*;
/**
* @date 2025-09-04
* @author scott
*
* 升级springboot3 无法使用 CommonsMultipartFile
* 自定义 MultipartFile 实现类,支持从 FileItem 构造
*/
public class MyCommonsMultipartFile implements MultipartFile {
private final byte[] fileContent;
private final String fileName;
private final String contentType;
// 新增构造方法,支持 FileItem 参数
public MyCommonsMultipartFile(FileItem fileItem) throws IOException {
this.fileName = fileItem.getName();
this.contentType = fileItem.getContentType();
try (InputStream inputStream = fileItem.getInputStream()) {
this.fileContent = inputStream.readAllBytes();
}
}
// 现有构造方法
public MyCommonsMultipartFile(InputStream inputStream, String fileName, String contentType) throws IOException {
this.fileName = fileName;
this.contentType = contentType;
this.fileContent = inputStream.readAllBytes();
}
@Override
public String getName() {
return fileName;
}
@Override
public String getOriginalFilename() {
return fileName;
}
@Override
public String getContentType() {
return contentType;
}
@Override
public boolean isEmpty() {
return fileContent.length == 0;
}
@Override
public long getSize() {
return fileContent.length;
}
@Override
public byte[] getBytes() {
return fileContent;
}
@Override
public InputStream getInputStream() {
return new ByteArrayInputStream(fileContent);
}
@Override
public void transferTo(File dest) throws IOException {
try (OutputStream os = new FileOutputStream(dest)) {
os.write(fileContent);
}
}
}

View File

@ -99,8 +99,9 @@ public class PasswordUtil {
Cipher cipher = Cipher.getInstance(ALGORITHM);
cipher.init(Cipher.ENCRYPT_MODE, key, parameterSpec);
// 代码逻辑说明: 中文作为用户名时加密的密码windows和linux会得到不同的结果 gitee/issues/IZUD7
//update-begin-author:sccott date:20180815 for:中文作为用户名时加密的密码windows和linux会得到不同的结果 gitee/issues/IZUD7
encipheredData = cipher.doFinal(plaintext.getBytes("utf-8"));
//update-end-author:sccott date:20180815 for:中文作为用户名时加密的密码windows和linux会得到不同的结果 gitee/issues/IZUD7
} catch (Exception e) {
}
return bytesToHexString(encipheredData);

View File

@ -2,7 +2,6 @@ package org.jeecg.common.util;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
import java.io.BufferedWriter;
@ -17,7 +16,6 @@ import java.util.List;
*/
@Slf4j
@Component
@Lazy(false)
public class PmsUtil {

View File

@ -4,7 +4,7 @@ import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.http.*;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.web.client.RestTemplate;
@ -56,19 +56,12 @@ public class RestUtil {
private final static RestTemplate RT;
static {
// 解决[issues/8859]online表单java增强失效------------
// 使用 Apache HttpClient 避免 JDK HttpURLConnection 的 too many bytes written 问题
HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory();
SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
requestFactory.setConnectTimeout(30000);
requestFactory.setReadTimeout(30000);
RT = new RestTemplate(requestFactory);
// 解决乱码问题(替换 StringHttpMessageConverter 为 UTF-8
for (int i = 0; i < RT.getMessageConverters().size(); i++) {
if (RT.getMessageConverters().get(i) instanceof StringHttpMessageConverter) {
RT.getMessageConverters().set(i, new StringHttpMessageConverter(StandardCharsets.UTF_8));
break;
}
}
// 解决乱码问题
RT.getMessageConverters().set(1, new StringHttpMessageConverter(StandardCharsets.UTF_8));
}
public static RestTemplate getRestTemplate() {
@ -223,94 +216,11 @@ public class RestUtil {
if (variables != null && !variables.isEmpty()) {
url += ("?" + asUrlVariables(variables));
}
// 解决[issues/8951]从jeecgboot 3.8.2 升级到 3.8.3 在线表单java增强功能报错------------
// Content-Length 强制设置(解决可能出现的截断问题)
if (StringUtils.isNotEmpty(body)) {
int contentLength = body.getBytes(StandardCharsets.UTF_8).length;
String current = headers.getFirst(HttpHeaders.CONTENT_LENGTH);
if (current == null || !current.equals(String.valueOf(contentLength))) {
headers.setContentLength(contentLength);
log.info(" RestUtil --- request --- 修正/设置 Content-Length = " + contentLength + (current!=null?" (原值="+current+")":""));
}
}
// 发送请求
HttpEntity<String> request = new HttpEntity<>(body, headers);
return RT.exchange(url, method, request, responseType);
}
/**
* 发送请求(支持自定义超时时间)
*
* @param url 请求地址
* @param method 请求方式
* @param headers 请求头 可空
* @param variables 请求url参数 可空
* @param params 请求body参数 可空
* @param responseType 返回类型
* @param timeout 超时时间毫秒如果为0或负数则使用默认超时
* @return ResponseEntity<responseType>
*/
public static <T> ResponseEntity<T> request(String url, HttpMethod method, HttpHeaders headers,
JSONObject variables, Object params, Class<T> responseType, int timeout) {
log.info(" RestUtil --- request --- url = "+ url + ", timeout = " + timeout);
if (StringUtils.isEmpty(url)) {
throw new RuntimeException("url 不能为空");
}
if (method == null) {
throw new RuntimeException("method 不能为空");
}
if (headers == null) {
headers = new HttpHeaders();
}
// 创建自定义RestTemplate如果需要设置超时
RestTemplate restTemplate = RT;
if (timeout > 0) {
// 代码逻辑说明: [issues/8859]online表单java增强失效------------
HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory();
requestFactory.setConnectTimeout(timeout);
requestFactory.setReadTimeout(timeout);
restTemplate = new RestTemplate(requestFactory);
// 解决乱码问题(替换 StringHttpMessageConverter 为 UTF-8
for (int i = 0; i < restTemplate.getMessageConverters().size(); i++) {
if (restTemplate.getMessageConverters().get(i) instanceof StringHttpMessageConverter) {
restTemplate.getMessageConverters().set(i, new StringHttpMessageConverter(StandardCharsets.UTF_8));
break;
}
}
}
// 请求体
String body = "";
if (params != null) {
if (params instanceof JSONObject) {
body = ((JSONObject) params).toJSONString();
} else {
body = params.toString();
}
}
// 拼接 url 参数
if (variables != null && !variables.isEmpty()) {
url += ("?" + asUrlVariables(variables));
}
// Content-Length 强制设置(解决可能出现的截断问题)
if (StringUtils.isNotEmpty(body) && !headers.containsKey(HttpHeaders.CONTENT_LENGTH)) {
int contentLength = body.getBytes(StandardCharsets.UTF_8).length;
String current = headers.getFirst(HttpHeaders.CONTENT_LENGTH);
if (current == null || !current.equals(String.valueOf(contentLength))) {
headers.setContentLength(contentLength);
log.info(" RestUtil --- request(timeout) --- 修正/设置 Content-Length = " + contentLength + (current!=null?" (原值="+current+")":""));
}
}
// 发送请求
HttpEntity<String> request = new HttpEntity<>(body, headers);
return restTemplate.exchange(url, method, request, responseType);
}
/**
* 获取JSON请求头
*/

View File

@ -1,37 +0,0 @@
package org.jeecg.common.util;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.ThreadContext;
import java.util.concurrent.*;
/**
* @date 2025-09-04
* @author scott
*
* @Description: 支持shiro的API获取当前登录人方法的线程池
*/
public class ShiroThreadPoolExecutor extends ThreadPoolExecutor {
public ShiroThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
}
@Override
public void execute(Runnable command) {
Subject subject = SecurityUtils.getSubject();
SecurityManager securityManager = SecurityUtils.getSecurityManager();
super.execute(() -> {
try {
ThreadContext.bind(securityManager);
ThreadContext.bind(subject);
command.run();
} finally {
ThreadContext.unbindSubject();
ThreadContext.unbindSecurityManager();
}
});
}
}

View File

@ -8,7 +8,6 @@ import org.jeecg.common.constant.ServiceNameConstants;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
@ -17,7 +16,6 @@ import org.springframework.web.context.request.ServletRequestAttributes;
* @Description: spring上下文工具类
* @author: jeecg-boot
*/
@Lazy(false)
@Component
public class SpringContextUtils implements ApplicationContextAware {

View File

@ -16,13 +16,7 @@ import java.util.regex.Pattern;
* @author zhoujf
*/
@Slf4j
public class SqlInjectionUtil {
/**
* sql注入黑名单数据库名
*/
public final static String XSS_STR_TABLE = "peformance_schema|information_schema";
public class SqlInjectionUtil {
/**
* 默认—sql注入关键词
*/
@ -173,28 +167,7 @@ public class SqlInjectionUtil {
}
return false;
}
/**
* 判断是否存在SQL注入关键词字符串
*
* @param keyword
* @return
*/
@SuppressWarnings("AlibabaUndefineMagicConstant")
private static boolean isExistSqlInjectTableKeyword(String sql, String keyword) {
// 需要匹配的sql注入关键词
String[] matchingTexts = new String[]{"`" + keyword, "(" + keyword, "(`" + keyword};
for (String matchingText : matchingTexts) {
String[] checkTexts = new String[]{" " + matchingText, "from" + matchingText};
for (String checkText : checkTexts) {
if (sql.contains(checkText)) {
return true;
}
}
}
return false;
}
/**
* sql注入过滤处理遇到注入关键字抛异常
*
@ -235,14 +208,6 @@ public class SqlInjectionUtil {
throw new JeecgSqlInjectionException(SqlInjectionUtil.SQL_INJECTION_TIP + value);
}
}
String[] xssTableArr = XSS_STR_TABLE.split("\\|");
for (String xssTableStr : xssTableArr) {
if (isExistSqlInjectTableKeyword(value, xssTableStr)) {
log.error(SqlInjectionUtil.SQL_INJECTION_KEYWORD_TIP, xssTableStr);
log.error(SqlInjectionUtil.SQL_INJECTION_TIP_VARIABLE, value);
throw new JeecgSqlInjectionException(SqlInjectionUtil.SQL_INJECTION_TIP + value);
}
}
// 三、SQL注入检测存在绕过风险 (正则校验)
for (String regularOriginal : XSS_REGULAR_STR_ARRAY) {
@ -279,14 +244,6 @@ public class SqlInjectionUtil {
throw new JeecgSqlInjectionException(SqlInjectionUtil.SQL_INJECTION_TIP + value);
}
}
String[] xssTableArr = XSS_STR_TABLE.split("\\|");
for (String xssTableStr : xssTableArr) {
if (isExistSqlInjectTableKeyword(value, xssTableStr)) {
log.error(SqlInjectionUtil.SQL_INJECTION_KEYWORD_TIP, xssTableStr);
log.error(SqlInjectionUtil.SQL_INJECTION_TIP_VARIABLE, value);
throw new JeecgSqlInjectionException(SqlInjectionUtil.SQL_INJECTION_TIP + value);
}
}
// 三、SQL注入检测存在绕过风险 (正则校验)
for (String regularOriginal : XSS_REGULAR_STR_ARRAY) {
@ -335,12 +292,13 @@ public class SqlInjectionUtil {
return table;
}
// 代码逻辑说明: 表单设计器列表翻译存在表名带条件,导致翻译出问题----
//update-begin---author:scott ---date:2024-05-28 for表单设计器列表翻译存在表名带条件,导致翻译出问题----
int index = table.toLowerCase().indexOf(" where ");
if (index != -1) {
table = table.substring(0, index);
log.info("截掉where之后的新表名" + table);
}
//update-end---author:scott ---date::2024-05-28 for表单设计器列表翻译存在表名带条件导致翻译出问题----
table = table.trim();
/**

View File

@ -1,184 +0,0 @@
package org.jeecg.common.util;
import com.alibaba.fastjson.JSONObject;
import com.tencentcloudapi.common.Credential;
import com.tencentcloudapi.common.exception.TencentCloudSDKException;
import com.tencentcloudapi.common.profile.ClientProfile;
import com.tencentcloudapi.common.profile.HttpProfile;
import com.tencentcloudapi.sms.v20210111.SmsClient;
import com.tencentcloudapi.sms.v20210111.models.SendSmsRequest;
import com.tencentcloudapi.sms.v20210111.models.SendSmsResponse;
import com.tencentcloudapi.sms.v20210111.models.SendStatus;
import org.apache.commons.lang3.StringUtils;
import org.jeecg.common.constant.enums.DySmsEnum;
import org.jeecg.config.JeecgSmsTemplateConfig;
import org.jeecg.config.tencent.JeecgTencent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* @Description: 腾讯发送短信
* @author: wangshuai
* @date: 2025/11/4 19:27
*/
public class TencentSms {
private final static Logger logger = LoggerFactory.getLogger(TencentSms.class);
/**
* 发送腾讯短信
*
* @param phone
* @param templateParamJson
* @param tencent
* @param dySmsEnum
* @return
*/
public static boolean sendTencentSms(String phone, JSONObject templateParamJson, JeecgTencent tencent, DySmsEnum dySmsEnum) {
//获取客户端链接
SmsClient client = getSmsClient(tencent);
//构建腾讯云短信发送请求
SendSmsRequest req = buildSendSmsRequest(phone, templateParamJson, dySmsEnum, tencent);
try {
//发送短信
SendSmsResponse resp = client.SendSms(req);
// 处理响应
SendStatus[] statusSet = resp.getSendStatusSet();
if (statusSet != null && statusSet.length > 0) {
SendStatus status = statusSet[0];
if ("Ok".equals(status.getCode())) {
logger.info("短信发送成功,手机号:{}", phone);
return true;
} else {
logger.error("短信发送失败,手机号:{},错误码:{},错误信息:{}",
phone, status.getCode(), status.getMessage());
}
}
} catch (TencentCloudSDKException e) {
logger.error("短信发送失败{}", e.getMessage());
}
return false;
}
/**
* 获取sms客户端
*
* @param tencent 腾讯云配置
* @return SmsClient对象
*/
private static SmsClient getSmsClient(JeecgTencent tencent) {
Credential cred = new Credential(tencent.getSecretId(), tencent.getSecretKey());
// 实例化一个http选项可选的没有特殊需求可以跳过
HttpProfile httpProfile = new HttpProfile();
//指定接入地域域名*/
httpProfile.setEndpoint(tencent.getEndpoint());
//实例化一个客户端配置对象
ClientProfile clientProfile = new ClientProfile();
clientProfile.setHttpProfile(httpProfile);
//实例化要请求产品的client对象第二个参数是地域信息
return new SmsClient(cred, tencent.getRegion(), clientProfile);
}
/**
* 构建腾讯云短信发送请求
*
* @param phone 手机号码
* @param templateParamJson 模板参数JSON对象
* @param dySmsEnum 短信枚举配置
* @param tencent 腾讯云配置
* @return 构建好的SendSmsRequest对象
*/
private static SendSmsRequest buildSendSmsRequest(
String phone,
JSONObject templateParamJson,
DySmsEnum dySmsEnum,
JeecgTencent tencent) {
SendSmsRequest req = new SendSmsRequest();
// 1. 设置短信应用ID
String sdkAppId = tencent.getSdkAppId();
req.setSmsSdkAppId(sdkAppId);
// 2. 设置短信签名
String signName = getSmsSignName(dySmsEnum);
req.setSignName(signName);
// 3. 设置模板ID
String templateId = getSmsTemplateId(dySmsEnum);
req.setTemplateId(templateId);
// 4. 设置模板参数
String[] templateParams = extractTemplateParams(templateParamJson);
req.setTemplateParamSet(templateParams);
// 5. 设置手机号码
String[] phoneNumberSet = { phone };
req.setPhoneNumberSet(phoneNumberSet);
logger.debug("构建短信请求完成 - 应用ID: {}, 签名: {}, 模板ID: {}, 手机号: {}",
sdkAppId, signName, templateId, phone);
return req;
}
/**
* 获取短信签名名称
*
* @param dySmsEnum 腾讯云对象
*/
private static String getSmsSignName(DySmsEnum dySmsEnum) {
JeecgSmsTemplateConfig baseConfig = SpringContextUtils.getBean(JeecgSmsTemplateConfig.class);
String signName = dySmsEnum.getSignName();
if (StringUtils.isNotEmpty(baseConfig.getSignature())) {
logger.debug("yml中读取签名名称: {}", baseConfig.getSignature());
signName = baseConfig.getSignature();
}
return signName;
}
/**
* 获取短信模板ID
*
* @param dySmsEnum 腾讯云对象
*/
private static String getSmsTemplateId(DySmsEnum dySmsEnum) {
JeecgSmsTemplateConfig baseConfig = SpringContextUtils.getBean(JeecgSmsTemplateConfig.class);
String templateCode = dySmsEnum.getTemplateCode();
if (StringUtils.isNotEmpty(baseConfig.getSignature())) {
Map<String, String> smsTemplate = baseConfig.getTemplateCode();
if (smsTemplate.containsKey(templateCode) &&
StringUtils.isNotEmpty(smsTemplate.get(templateCode))) {
templateCode = smsTemplate.get(templateCode);
logger.debug("yml中读取短信模板ID: {}", templateCode);
}
}
return templateCode;
}
/**
* 从JSONObject中提取模板参数按原始顺序
*
* @param templateParamJson 模板参数
*/
private static String[] extractTemplateParams(JSONObject templateParamJson) {
if (templateParamJson == null || templateParamJson.isEmpty()) {
return new String[0];
}
List<String> params = new ArrayList<>();
for (String key : templateParamJson.keySet()) {
Object value = templateParamJson.get(key);
if (value != null) {
params.add(value.toString());
} else {
// 处理null值
params.add("");
}
}
logger.debug("提取模板参数: {}", params);
return params.toArray(new String[0]);
}
}

View File

@ -1,5 +1,6 @@
package org.jeecg.common.util;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.jeecg.common.api.CommonAPI;
@ -11,8 +12,6 @@ import org.jeecg.common.exception.JeecgBoot401Exception;
import org.jeecg.common.system.util.JwtUtil;
import org.jeecg.common.system.vo.LoginUser;
import jakarta.servlet.http.HttpServletRequest;
/**
* @Author scott
* @Date 2019/9/23 14:12
@ -65,10 +64,6 @@ public class TokenUtils {
if (tenantId == null) {
tenantId = oConvertUtils.getString(request.getHeader(CommonConstant.TENANT_ID));
}
if (oConvertUtils.isNotEmpty(tenantId) && "undefined".equals(tenantId)) {
return null;
}
return tenantId;
}
@ -110,8 +105,8 @@ public class TokenUtils {
}
// 查询用户信息
LoginUser user = TokenUtils.getLoginUser(username, commonApi, redisUtil);
//LoginUser user = commonApi.getUserByName(username);
//LoginUser user = TokenUtils.getLoginUser(username, commonApi, redisUtil);
LoginUser user = commonApi.getUserByName(username);
if (user == null) {
throw new JeecgBoot401Exception("用户不存在!");
}
@ -121,9 +116,7 @@ public class TokenUtils {
}
// 校验token是否超时失效 & 或者账号密码是否错误
if (!jwtTokenRefresh(token, username, user.getPassword(), redisUtil)) {
// 用户登录Token过期提示信息
String userLoginTokenErrorMsg = oConvertUtils.getString(redisUtil.get(CommonConstant.PREFIX_USER_TOKEN_ERROR_MSG + token));
throw new JeecgBoot401Exception(oConvertUtils.isEmpty(userLoginTokenErrorMsg)? CommonConstant.TOKEN_IS_INVALID_MSG: userLoginTokenErrorMsg);
throw new JeecgBoot401Exception(CommonConstant.TOKEN_IS_INVALID_MSG);
}
return true;
}
@ -141,15 +134,10 @@ public class TokenUtils {
if (oConvertUtils.isNotEmpty(cacheToken)) {
// 校验token有效性
if (!JwtUtil.verify(cacheToken, userName, passWord)) {
// 从token中解析客户端类型保持续期时使用相同的客户端类型
String clientType = JwtUtil.getClientType(token);
String newAuthorization = JwtUtil.sign(userName, passWord, clientType);
// 根据客户端类型设置对应的缓存有效时间
long expireTime = CommonConstant.CLIENT_TYPE_APP.equalsIgnoreCase(clientType)
? JwtUtil.APP_EXPIRE_TIME * 2 / 1000
: JwtUtil.EXPIRE_TIME * 2 / 1000;
String newAuthorization = JwtUtil.sign(userName, passWord);
// 设置Toekn缓存有效时间
redisUtil.set(CommonConstant.PREFIX_USER_TOKEN + token, newAuthorization);
redisUtil.expire(CommonConstant.PREFIX_USER_TOKEN + token, expireTime);
redisUtil.expire(CommonConstant.PREFIX_USER_TOKEN + token, JwtUtil.EXPIRE_TIME * 2 / 1000);
}
return true;
}
@ -169,10 +157,11 @@ public class TokenUtils {
//【重要】此处通过redis原生获取缓存用户是为了解决微服务下system服务挂了其他服务互调不通问题---
if (redisUtil.hasKey(loginUserKey)) {
try {
loginUser = (LoginUser) redisUtil.get(loginUserKey);
Object obj = redisUtil.get(loginUserKey);
loginUser = (LoginUser) obj;
//解密用户
SensitiveInfoUtil.handlerObject(loginUser, false);
} catch (IllegalAccessException e) {
} catch (Exception e) {
e.printStackTrace();
}
} else {

View File

@ -68,13 +68,6 @@ public class DbTypeUtils {
return dbTypeIf(dbType, DbType.ORACLE, DbType.ORACLE_12C, DbType.DM);
}
/**
* 是否是达梦
*/
public static boolean dbTypeIsDm(DbType dbType) {
return dbTypeIf(dbType, DbType.DM);
}
public static boolean dbTypeIsSqlServer(DbType dbType) {
return dbTypeIf(dbType, DbType.SQL_SERVER, DbType.SQL_SERVER2005);
}

View File

@ -54,10 +54,11 @@ public class FreemarkerParseFactory {
//classic_compatible设置解决报空指针错误
SQL_CONFIG.setClassicCompatible(true);
// 解决freemarker模板注入问题 禁止解析ObjectConstructorExecute和freemarker.template.utility.JythonRuntime。
//update-begin-author:taoyan date:2022-8-10 for: freemarker模板注入问题 禁止解析ObjectConstructorExecute和freemarker.template.utility.JythonRuntime。
//https://ackcent.com/in-depth-freemarker-template-injection/
TPL_CONFIG.setNewBuiltinClassResolver(TemplateClassResolver.SAFER_RESOLVER);
SQL_CONFIG.setNewBuiltinClassResolver(TemplateClassResolver.SAFER_RESOLVER);
//update-end-author:taoyan date:2022-8-10 for: freemarker模板注入问题 禁止解析ObjectConstructorExecute和freemarker.template.utility.JythonRuntime。
}
/**
@ -72,12 +73,13 @@ public class FreemarkerParseFactory {
return false;
}
} catch (Exception e) {
// 代码逻辑说明: 解决问题 - 错误提示sql文件不存在实际问题是sql freemarker用法错误-----
//update-begin--Author:scott Date:20180320 for解决问题 - 错误提示sql文件不存在实际问题是sql freemarker用法错误-----
if (e instanceof ParseException) {
log.error(e.getMessage(), e.fillInStackTrace());
throw new Exception(e);
}
log.debug("----isExistTemplate----" + e.toString());
//update-end--Author:scott Date:20180320 for解决问题 - 错误提示sql文件不存在实际问题是sql freemarker用法错误------
return false;
}
return true;

View File

@ -1,107 +1,123 @@
package org.jeecg.common.util.encryption;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.lang.codec.Base64;
import org.jeecg.common.util.oConvertUtils;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
/**
* AES 工具 (兼容历史 NoPadding + 新 PKCS5Padding)
* @Description: AES 加密
* @author: jeecg-boot
* @date: 2022/3/30 11:48
*/
@Slf4j
public class AesEncryptUtil {
private static final String KEY = EncryptedString.key;
private static final String IV = EncryptedString.iv;
/**
* 使用AES-128-CBC加密模式 key和iv可以相同
*/
private static String KEY = EncryptedString.key;
private static String IV = EncryptedString.iv;
/* -------- 新版CBC + PKCS5Padding (与前端 CryptoJS Pkcs7 兼容) -------- */
private static String decryptPkcs5(String cipherBase64) throws Exception {
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
SecretKeySpec ks = new SecretKeySpec(KEY.getBytes(StandardCharsets.UTF_8), "AES");
IvParameterSpec ivSpec = new IvParameterSpec(IV.getBytes(StandardCharsets.UTF_8));
cipher.init(Cipher.DECRYPT_MODE, ks, ivSpec);
byte[] plain = cipher.doFinal(Base64.decode(cipherBase64));
return new String(plain, StandardCharsets.UTF_8);
}
/**
* 加密方法
* @param data 要加密的数据
* @param key 加密key
* @param iv 加密iv
* @return 加密的结果
* @throws Exception
*/
public static String encrypt(String data, String key, String iv) throws Exception {
try {
/* -------- 旧版CBC + NoPadding (手工补 0) -------- */
private static String decryptLegacyNoPadding(String cipherBase64) throws Exception {
Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
SecretKeySpec ks = new SecretKeySpec(KEY.getBytes(StandardCharsets.UTF_8), "AES");
IvParameterSpec ivSpec = new IvParameterSpec(IV.getBytes(StandardCharsets.UTF_8));
cipher.init(Cipher.DECRYPT_MODE, ks, ivSpec);
byte[] data = cipher.doFinal(Base64.decode(cipherBase64));
return new String(data, StandardCharsets.UTF_8)
.replace("\u0000",""); // 旧填充 0
}
/* -------- 兼容入口:登录使用 -------- */
public static String resolvePassword(String input){
if(oConvertUtils.isEmpty(input)){
return input;
}
// 1. 先尝试新版
try{
String p = decryptPkcs5(input);
return clean(p);
}catch(Exception ignore){
//log.debug("【AES解密】Password not AES PKCS5 cipher, try legacy.");
}
// 2. 回退旧版
try{
String legacy = decryptLegacyNoPadding(input);
return clean(legacy);
}catch(Exception e){
log.debug("【AES解密】Password not AES cipher, raw used.");
}
// 3. 视为明文
return input;
}
/* -------- 可选:统一清理尾部不可见控制字符 -------- */
private static String clean(String s){
if(s==null) return null;
// 去除结尾控制符/空白(不影响中间合法空格)
return s.replaceAll("[\\p{Cntrl}]+","").trim();
}
/* -------- 若仍需要旧接口,可保留 (不建议再用于新前端) -------- */
@Deprecated
public static String desEncrypt(String data) throws Exception {
return decryptLegacyNoPadding(data);
}
/* 加密(若前端不再使用,可忽略;保留旧实现避免影响历史) */
@Deprecated
public static String encrypt(String data) throws Exception {
try{
//"算法/模式/补码方式"NoPadding PkcsPadding
Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
int blockSize = cipher.getBlockSize();
byte[] dataBytes = data.getBytes(StandardCharsets.UTF_8);
byte[] dataBytes = data.getBytes();
int plaintextLength = dataBytes.length;
if (plaintextLength % blockSize != 0) {
plaintextLength += (blockSize - (plaintextLength % blockSize));
plaintextLength = plaintextLength + (blockSize - (plaintextLength % blockSize));
}
byte[] plaintext = new byte[plaintextLength];
System.arraycopy(dataBytes, 0, plaintext, 0, dataBytes.length);
SecretKeySpec keyspec = new SecretKeySpec(KEY.getBytes(StandardCharsets.UTF_8), "AES");
IvParameterSpec ivspec = new IvParameterSpec(IV.getBytes(StandardCharsets.UTF_8));
SecretKeySpec keyspec = new SecretKeySpec(key.getBytes(), "AES");
IvParameterSpec ivspec = new IvParameterSpec(iv.getBytes());
cipher.init(Cipher.ENCRYPT_MODE, keyspec, ivspec);
byte[] encrypted = cipher.doFinal(plaintext);
return Base64.encodeToString(encrypted);
}catch(Exception e){
throw new IllegalStateException("legacy encrypt error", e);
return Base64.getEncoder().encodeToString(encrypted);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
// public static void main(String[] args) throws Exception {
// // 前端 CBC/Pkcs7 密文测试
// String frontCipher = encrypt("sa"); // 仅验证管道是否可用(旧方式)
// System.out.println(resolvePassword(frontCipher));
/**
* 解密方法
* @param data 要解密的数据
* @param key 解密key
* @param iv 解密iv
* @return 解密的结果
* @throws Exception
*/
public static String desEncrypt(String data, String key, String iv) throws Exception {
//update-begin-author:taoyan date:2022-5-23 for:VUEN-1084 【vue3】online表单测试发现的新问题 6、解密报错 ---解码失败应该把异常抛出去,在外面处理
byte[] encrypted1 = Base64.getDecoder().decode(data);
Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
SecretKeySpec keyspec = new SecretKeySpec(key.getBytes(), "AES");
IvParameterSpec ivspec = new IvParameterSpec(iv.getBytes());
cipher.init(Cipher.DECRYPT_MODE, keyspec, ivspec);
byte[] original = cipher.doFinal(encrypted1);
String originalString = new String(original);
//加密解码后的字符串会出现\u0000
return originalString.replaceAll("\\u0000", "");
//update-end-author:taoyan date:2022-5-23 for:VUEN-1084 【vue3】online表单测试发现的新问题 6、解密报错 ---解码失败应该把异常抛出去,在外面处理
}
/**
* 使用默认的key和iv加密
* @param data
* @return
* @throws Exception
*/
public static String encrypt(String data) throws Exception {
return encrypt(data, KEY, IV);
}
/**
* 使用默认的key和iv解密
* @param data
* @return
* @throws Exception
*/
public static String desEncrypt(String data) throws Exception {
return desEncrypt(data, KEY, IV);
}
// /**
// * 测试
// */
// public static void main(String args[]) throws Exception {
// String test1 = "sa";
// String test =new String(test1.getBytes(),"UTF-8");
// String data = null;
// String key = KEY;
// String iv = IV;
// // /g2wzfqvMOeazgtsUVbq1kmJawROa6mcRAzwG1/GeJ4=
// data = encrypt(test, key, iv);
// System.out.println("数据:"+test);
// System.out.println("加密:"+data);
// String jiemi =desEncrypt(data, key, iv).trim();
// System.out.println("解密:"+jiemi);
// }
}
}

View File

@ -2,7 +2,6 @@ package org.jeecg.common.util.filter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.jeecg.common.exception.JeecgBootException;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
@ -35,7 +34,6 @@ public class SsrfFileTypeFilter {
FILE_TYPE_WHITE_LIST.add("bmp");
FILE_TYPE_WHITE_LIST.add("svg");
FILE_TYPE_WHITE_LIST.add("ico");
FILE_TYPE_WHITE_LIST.add("heic");
//文本文件
FILE_TYPE_WHITE_LIST.add("txt");
@ -44,7 +42,6 @@ public class SsrfFileTypeFilter {
FILE_TYPE_WHITE_LIST.add("pdf");
FILE_TYPE_WHITE_LIST.add("csv");
// FILE_TYPE_WHITE_LIST.add("xml");
FILE_TYPE_WHITE_LIST.add("md");
//音视频文件
FILE_TYPE_WHITE_LIST.add("mp4");
@ -68,10 +65,6 @@ public class SsrfFileTypeFilter {
FILE_TYPE_WHITE_LIST.add("apk");
FILE_TYPE_WHITE_LIST.add("wgt");
//幻灯片文件后缀
FILE_TYPE_WHITE_LIST.add("ppt");
FILE_TYPE_WHITE_LIST.add("pptx");
//设置禁止文件的头部标记
FILE_TYPE_MAP.put("3c25402070616765206c", "jsp");
FILE_TYPE_MAP.put("3c3f7068700a0a2f2a2a0a202a205048", "php");
@ -150,38 +143,29 @@ public class SsrfFileTypeFilter {
public static void checkDownloadFileType(String filePath) throws IOException {
//文件后缀
String suffix = getFileTypeBySuffix(filePath);
log.debug(" 【文件下载校验】文件后缀 suffix: {}", suffix);
log.info("suffix:{}", suffix);
boolean isAllowExtension = FILE_TYPE_WHITE_LIST.contains(suffix.toLowerCase());
//是否允许下载的文件
if (!isAllowExtension) {
throw new JeecgBootException("下载失败,存在非法文件类型:" + suffix);
throw new IOException("下载失败,存在非法文件类型:" + suffix);
}
}
/**
* 上传文件类型过滤
*
* @param file
*/
public static void checkUploadFileType(MultipartFile file) throws Exception {
checkUploadFileType(file, null);
}
/**
* 上传文件类型过滤
*
* @param file
*/
public static void checkUploadFileType(MultipartFile file, String customPath) throws Exception {
//1. 路径安全校验
validatePathSecurity(customPath);
//2. 校验文件后缀和头
String suffix = getFileType(file, customPath);
log.info("【文件上传校验】文件后缀 suffix: {}customPath{}", suffix, customPath);
//获取文件真是后缀
String suffix = getFileType(file);
log.info("suffix:{}", suffix);
boolean isAllowExtension = FILE_TYPE_WHITE_LIST.contains(suffix.toLowerCase());
//是否允许下载的文件
if (!isAllowExtension) {
throw new JeecgBootException("上传失败,存在非法文件类型:" + suffix);
throw new Exception("上传失败,存在非法文件类型:" + suffix);
}
}
@ -193,8 +177,8 @@ public class SsrfFileTypeFilter {
* @throws Exception
*/
private static String getFileType(MultipartFile file, String customPath) throws Exception {
// 代码逻辑说明: [issue/4672]方法造成的文件被占用注释掉此方法tomcat就能自动清理掉临时文件
private static String getFileType(MultipartFile file) throws Exception {
//update-begin-author:liusq date:20230404 for: [issue/4672]方法造成的文件被占用注释掉此方法tomcat就能自动清理掉临时文件
String fileExtendName = null;
InputStream is = null;
try {
@ -213,7 +197,7 @@ public class SsrfFileTypeFilter {
break;
}
}
log.debug("-----获取到的指定文件类型------"+fileExtendName);
log.info("-----获取到的指定文件类型------"+fileExtendName);
// 如果不是上述类型,则判断扩展名
if (StringUtils.isBlank(fileExtendName)) {
String fileName = file.getOriginalFilename();
@ -224,6 +208,7 @@ public class SsrfFileTypeFilter {
// 如果有扩展名,则返回扩展名
return getFileTypeBySuffix(fileName);
}
log.info("-----最終的文件类型------"+fileExtendName);
is.close();
return fileExtendName;
} catch (Exception e) {
@ -234,6 +219,7 @@ public class SsrfFileTypeFilter {
is.close();
}
}
//update-end-author:liusq date:20230404 for: [issue/4672]方法造成的文件被占用注释掉此方法tomcat就能自动清理掉临时文件
}
/**
@ -257,34 +243,4 @@ public class SsrfFileTypeFilter {
}
return stringBuilder.toString();
}
/**
* 路径安全校验
*/
private static void validatePathSecurity(String customPath) throws JeecgBootException {
if (customPath == null || customPath.trim().isEmpty()) {
return;
}
// 统一分隔符为 /
String normalized = customPath.replace("\\", "/");
// 1. 防止路径遍历攻击
if (normalized.contains("..") || normalized.contains("~")) {
throw new JeecgBootException("上传业务路径包含非法字符!");
}
// 2. 限制路径深度
int depth = normalized.split("/").length;
if (depth > 5) {
throw new JeecgBootException("上传业务路径深度超出限制!");
}
// 3. 限制字符集(只允许字母、数字、下划线、横线、斜杠)
if (!normalized.matches("^[a-zA-Z0-9/_-]+$")) {
throw new JeecgBootException("上传业务路径包含非法字符!");
}
}
}

View File

@ -7,18 +7,12 @@ import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.jeecg.common.constant.CommonConstant;
import org.jeecg.common.constant.SymbolConstant;
import org.jeecg.config.mybatis.MybatisPlusSaasConfig;
import org.springframework.beans.BeanUtils;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.beans.BeanWrapper;
import org.springframework.beans.BeanWrapperImpl;
import java.beans.PropertyDescriptor;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.math.BigDecimal;
import java.math.BigInteger;
@ -27,7 +21,6 @@ import java.sql.Date;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Stream;
/**
*
@ -470,7 +463,7 @@ public class oConvertUtils {
return false;
}
List<String> childs = childArray.toJavaList(String.class);
String[] childs = (String[]) childArray.toArray();
for (String v : childs) {
if (!isIn(v, all)) {
return false;
@ -479,23 +472,6 @@ public class oConvertUtils {
return true;
}
/**
* 判断字符串是否为JSON格式
* @param str
* @return
*/
public static boolean isJson(String str) {
if (str == null || str.trim().isEmpty()) {
return false;
}
try {
com.alibaba.fastjson.JSON.parse(str);
return true;
} catch (Exception e) {
return false;
}
}
/**
* 获取Map对象
*/
@ -568,8 +544,10 @@ public class oConvertUtils {
return "";
} else if (!name.contains(SymbolConstant.UNDERLINE)) {
// 不含下划线,仅将首字母小写
// 代码逻辑说明: TASK #2500 【代码生成器】代码生成器开发一通用模板生成功能
//update-begin--Author:zhoujf Date:20180503 forTASK #2500 【代码生成器】代码生成器开发一通用模板生成功能
//update-begin--Author:zhoujf Date:20180503 forTASK #2500 【代码生成器】代码生成器开发一通用模板生成功能
return name.substring(0, 1).toLowerCase() + name.substring(1).toLowerCase();
//update-end--Author:zhoujf Date:20180503 forTASK #2500 【代码生成器】代码生成器开发一通用模板生成功能
}
// 用下划线将原始字符串分割
String[] camels = name.split("_");
@ -614,6 +592,7 @@ public class oConvertUtils {
return result.substring(0, result.length() - 1);
}
//update-begin--Author:zhoujf Date:20180503 forTASK #2500 【代码生成器】代码生成器开发一通用模板生成功能
/**
* 将下划线大写方式命名的字符串转换为驼峰式。(首字母写)
* 如果转换前的下划线大写方式命名的字符串为空,则返回空字符串。</br>
@ -646,6 +625,7 @@ public class oConvertUtils {
}
return result.toString();
}
//update-end--Author:zhoujf Date:20180503 forTASK #2500 【代码生成器】代码生成器开发一通用模板生成功能
/**
* 将驼峰命名转化成下划线
@ -983,18 +963,17 @@ public class oConvertUtils {
/**
* 判断 sourceList中的元素是否在targetList中出现
*
* 判断 list1中的元素是否在list2中出现
* QQYUN-5326【简流】获取组织人员 单/多 筛选条件 没有部门筛选
* @param sourceList 源列表,要检查的元素列表
* @param targetList 目标列表,用于匹配的列表
* @return 如果sourceList中有任何元素在targetList中存在则返回true否则返回false
* @param list1
* @param list2
* @return
*/
public static boolean isInList(List<String> sourceList, List<String> targetList){
for(String sourceItem: sourceList){
public static boolean isInList(List<String> list1, List<String> list2){
for(String str1: list1){
boolean flag = false;
for(String targetItem: targetList){
if(sourceItem.equals(targetItem)){
for(String str2: list2){
if(str1.equals(str2)){
flag = true;
break;
}
@ -1006,35 +985,6 @@ public class oConvertUtils {
return false;
}
/**
* 判断 sourceList中的所有元素是否都在targetList中存在
* @param sourceList 源列表,要检查的元素列表
* @param targetList 目标列表,用于匹配的列表
* @return 如果sourceList中的所有元素都在targetList中存在则返回true否则返回false
*/
public static boolean isAllInList(List<String> sourceList, List<String> targetList){
if(sourceList == null || sourceList.isEmpty()){
return true; // 空列表视为所有元素都存在
}
if(targetList == null || targetList.isEmpty()){
return false; // 目标列表为空源列表非空时返回false
}
for(String sourceItem: sourceList){
boolean found = false;
for(String targetItem: targetList){
if(sourceItem.equals(targetItem)){
found = true;
break;
}
}
if(!found){
return false; // 有任何一个元素不在目标列表中返回false
}
}
return true; // 所有元素都找到了
}
/**
* 计算文件大小转成MB
* @param uploadCount
@ -1078,158 +1028,5 @@ public class oConvertUtils {
}
return result;
}
/**
* 判断对象是否为空 <br/>
* 支持各种类型的对象
* for for [QQYUN-10990]AIRAG
* @param obj
* @return
* @author chenrui
* @date 2025/2/13 18:34
*/
public static boolean isObjectEmpty(Object obj) {
if (null == obj) {
return true;
}
if (obj instanceof CharSequence) {
return isEmpty(obj);
} else if (obj instanceof Map) {
return ((Map<?, ?>) obj).isEmpty();
} else if (obj instanceof Iterable) {
return isObjectEmpty(((Iterable<?>) obj).iterator());
} else if (obj instanceof Iterator) {
return !((Iterator<?>) obj).hasNext();
} else if (isArray(obj)) {
return 0 == Array.getLength(obj);
}
return false;
}
/**
* iterator 是否为空
* for for [QQYUN-10990]AIRAG
* @param iterator Iterator对象
* @return 是否为空
*/
public static boolean isEmptyIterator(Iterator<?> iterator) {
return null == iterator || false == iterator.hasNext();
}
/**
* 判断对象是否不为空
* for for [QQYUN-10990]AIRAG
* @param object
* @return
* @author chenrui
* @date 2025/2/13 18:35
*/
public static boolean isObjectNotEmpty(Object object) {
return !isObjectEmpty(object);
}
/**
* 如果src大于des返回true
* for [QQYUN-10990]AIRAG
* @param src
* @param des
* @return
* @author: chenrui
* @date: 2018/9/19 15:30
*/
public static boolean isGt(Number src, Number des) {
if (null == src || null == des) {
throw new IllegalArgumentException("参数不能为空");
}
if (src.doubleValue() > des.doubleValue()) {
return true;
}
return false;
}
/**
* 如果src大于等于des返回true
* for [QQYUN-10990]AIRAG
* @param src
* @param des
* @return
* @author: chenrui
* @date: 2018/9/19 15:30
*/
public static boolean isGe(Number src, Number des) {
if (null == src || null == des) {
throw new IllegalArgumentException("参数不能为空");
}
if (src.doubleValue() < des.doubleValue()) {
return false;
}
return true;
}
/**
* 判断是否存在
* for [QQYUN-10990]AIRAG
* @param obj
* @param objs
* @param <T>
* @return
* @author chenrui
* @date 2020/9/12 15:50
*/
public static <T> boolean isIn(T obj, T... objs) {
if (isEmpty(objs)) {
return false;
}
for (T obj1 : objs) {
if (isEqual(obj, obj1)) {
return true;
}
}
return false;
}
/**
* 判断租户ID是否有效
* @param tenantId
* @return
*/
public static boolean isEffectiveTenant(String tenantId) {
return MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL && isNotEmpty(tenantId) && !("0").equals(tenantId);
}
/**
* 复制源对象的非空属性到目标对象(同名属性)
*
* @param source 源对象(页面)
* @param target 目标对象(数据库实体)
*/
public static void copyNonNullFields(Object source, Object target) {
if (source == null || target == null) {
return;
}
// 获取源对象的非空属性名数组
String[] nullPropertyNames = getNullPropertyNames(source);
// 复制:忽略源对象的空属性,仅覆盖目标对象的对应非空属性
BeanUtils.copyProperties(source, target, nullPropertyNames);
}
/**
* 获取源对象中值为 null 的属性名数组
*
* @param source
*/
private static String[] getNullPropertyNames(Object source) {
BeanWrapper beanWrapper = new BeanWrapperImpl(source);
//获取类的属性
PropertyDescriptor[] propertyDescriptors = beanWrapper.getPropertyDescriptors();
// 过滤出值为 null 的属性名
return Stream.of(propertyDescriptors)
.map(PropertyDescriptor::getName)
.filter(name -> beanWrapper.getPropertyValue(name) == null)
.toArray(String[]::new);
}
}

View File

@ -97,8 +97,9 @@ public class OssBootUtil {
* @return oss 中的相对文件路径
*/
public static String upload(MultipartFile file, String fileDir,String customBucket) throws Exception {
// 文件安全校验,防止上传漏洞文件
//update-begin-author:liusq date:20210809 for: 过滤上传文件类型
SsrfFileTypeFilter.checkUploadFileType(file);
//update-end-author:liusq date:20210809 for: 过滤上传文件类型
String filePath = null;
initOss(endPoint, accessKeyId, accessKeySecret);
@ -124,8 +125,9 @@ public class OssBootUtil {
if (!fileDir.endsWith(SymbolConstant.SINGLE_SLASH)) {
fileDir = fileDir.concat(SymbolConstant.SINGLE_SLASH);
}
// 代码逻辑说明: 过滤上传文件夹名特殊字符,防止攻击
//update-begin-author:wangshuai date:20201012 for: 过滤上传文件夹名特殊字符,防止攻击
fileDir=StrAttackFilter.filter(fileDir);
//update-end-author:wangshuai date:20201012 for: 过滤上传文件夹名特殊字符,防止攻击
fileUrl = fileUrl.append(fileDir + fileName);
if (oConvertUtils.isNotEmpty(staticDomain) && staticDomain.toLowerCase().startsWith(CommonConstant.STR_HTTP)) {
@ -262,8 +264,9 @@ public class OssBootUtil {
newBucket = bucket;
}
initOss(endPoint, accessKeyId, accessKeySecret);
// 代码逻辑说明: 替换objectName前缀防止key不一致导致获取不到文件----
//update-begin---author:liusq Date:20220120 for替换objectName前缀防止key不一致导致获取不到文件----
objectName = OssBootUtil.replacePrefix(objectName,bucket);
//update-end---author:liusq Date:20220120 for替换objectName前缀防止key不一致导致获取不到文件----
OSSObject ossObject = ossClient.getObject(newBucket,objectName);
inputStream = new BufferedInputStream(ossObject.getObjectContent());
}catch (Exception e){
@ -291,8 +294,9 @@ public class OssBootUtil {
public static String getObjectUrl(String bucketName, String objectName, Date expires) {
initOss(endPoint, accessKeyId, accessKeySecret);
try{
// 代码逻辑说明: 替换objectName前缀防止key不一致导致获取不到文件----
//update-begin---author:liusq Date:20220120 for替换objectName前缀防止key不一致导致获取不到文件----
objectName = OssBootUtil.replacePrefix(objectName,bucketName);
//update-end---author:liusq Date:20220120 for替换objectName前缀防止key不一致导致获取不到文件----
if(ossClient.doesObjectExist(bucketName,objectName)){
URL url = ossClient.generatePresignedUrl(bucketName,objectName,expires);
//log.info("原始url : {}", url.toString());

View File

@ -3,8 +3,6 @@ package org.jeecg.common.util.security;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.jeecg.common.exception.JeecgSqlInjectionException;
import org.jeecg.common.util.SqlInjectionUtil;
import org.jeecg.common.util.oConvertUtils;
import java.util.*;
import java.util.regex.Matcher;
@ -63,13 +61,11 @@ public abstract class AbstractQueryBlackListHandler {
if(list==null){
return true;
}
log.debug(" 获取sql信息 {} ", list.toString());
log.info(" 获取sql信息 {} ", list.toString());
boolean flag = checkTableAndFieldsName(list);
if(flag == false){
return false;
}
Set<String> xssTableSet = new HashSet<>(Arrays.asList(SqlInjectionUtil.XSS_STR_TABLE.split("\\|")));
for (QueryTable table : list) {
String name = table.getName();
String fieldRule = ruleMap.get(name);
@ -85,16 +81,6 @@ public abstract class AbstractQueryBlackListHandler {
}
}
// 判断是否调用了黑名单数据库
String dbName = table.getDbName();
if (oConvertUtils.isNotEmpty(dbName)) {
dbName = dbName.toLowerCase().trim();
if (xssTableSet.contains(dbName)) {
flag = false;
log.warn("sql黑名单校验数据库【" + dbName + "】禁止查询");
break;
}
}
}
// 返回黑名单校验结果(不合法直接抛出异常)
@ -149,8 +135,6 @@ public abstract class AbstractQueryBlackListHandler {
* 查询的表的信息
*/
protected class QueryTable {
//数据库名
private String dbName;
//表名
private String name;
//表的别名
@ -174,14 +158,6 @@ public abstract class AbstractQueryBlackListHandler {
this.fields.add(field);
}
public String getDbName() {
return dbName;
}
public void setDbName(String dbName) {
this.dbName = dbName;
}
public String getName() {
return name;
}

View File

@ -0,0 +1,33 @@
package org.jeecg.common.util.sqlInjection;
import net.sf.jsqlparser.parser.CCJSqlParserDefaultVisitor;
import net.sf.jsqlparser.parser.SimpleNode;
import net.sf.jsqlparser.statement.select.UnionOp;
import org.jeecg.common.exception.JeecgSqlInjectionException;
/**
* 基于抽象语法树(AST)的注入攻击分析实现
*
* @author guyadong
*/
public class InjectionAstNodeVisitor extends CCJSqlParserDefaultVisitor {
public InjectionAstNodeVisitor() {
}
/**
* 处理禁止联合查询
*
* @param node
* @param data
* @return
*/
@Override
public Object visit(SimpleNode node, Object data) {
Object value = node.jjtGetValue();
if (value instanceof UnionOp) {
throw new JeecgSqlInjectionException("DISABLE UNION");
}
return super.visit(node, data);
}
}

View File

@ -0,0 +1,172 @@
package org.jeecg.common.util.sqlInjection;
import net.sf.jsqlparser.expression.BinaryExpression;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.Function;
import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
import net.sf.jsqlparser.expression.operators.conditional.OrExpression;
import net.sf.jsqlparser.expression.operators.relational.ComparisonOperator;
import net.sf.jsqlparser.schema.Column;
import net.sf.jsqlparser.statement.select.Join;
import net.sf.jsqlparser.statement.select.OrderByElement;
import net.sf.jsqlparser.statement.select.PlainSelect;
import net.sf.jsqlparser.statement.select.SelectItem;
import net.sf.jsqlparser.statement.select.SubSelect;
import net.sf.jsqlparser.statement.select.WithItem;
import net.sf.jsqlparser.util.TablesNamesFinder;
import org.jeecg.common.exception.JeecgSqlInjectionException;
import org.jeecg.common.util.sqlInjection.parse.ConstAnalyzer;
import org.jeecg.common.util.sqlInjection.parse.ParserSupport;
/**
* 基于SQL语法对象的SQL注入攻击分析实现
*
* @author guyadong
*/
public class InjectionSyntaxObjectAnalyzer extends TablesNamesFinder {
/**
* 危险函数名
*/
private static final String DANGROUS_FUNCTIONS = "(sleep|benchmark|extractvalue|updatexml|ST_LatFromGeoHash|ST_LongFromGeoHash|GTID_SUBSET|GTID_SUBTRACT|floor|ST_Pointfromgeohash"
+ "|geometrycollection|multipoint|polygon|multipolygon|linestring|multilinestring)";
private static ThreadLocal<Boolean> disableSubselect = new ThreadLocal<Boolean>() {
@Override
protected Boolean initialValue() {
return true;
}
};
private ConstAnalyzer constAnalyzer = new ConstAnalyzer();
public InjectionSyntaxObjectAnalyzer() {
super();
init(true);
}
@Override
public void visitBinaryExpression(BinaryExpression binaryExpression) {
if (binaryExpression instanceof ComparisonOperator) {
if (isConst(binaryExpression.getLeftExpression()) && isConst(binaryExpression.getRightExpression())) {
/** 禁用恒等式 */
throw new JeecgSqlInjectionException("DISABLE IDENTICAL EQUATION " + binaryExpression);
}
}
super.visitBinaryExpression(binaryExpression);
}
@Override
public void visit(AndExpression andExpression) {
super.visit(andExpression);
checkConstExpress(andExpression.getLeftExpression());
checkConstExpress(andExpression.getRightExpression());
}
@Override
public void visit(OrExpression orExpression) {
super.visit(orExpression);
checkConstExpress(orExpression.getLeftExpression());
checkConstExpress(orExpression.getRightExpression());
}
@Override
public void visit(Function function) {
if (function.getName().matches(DANGROUS_FUNCTIONS)) {
/** 禁用危险函数 */
throw new JeecgSqlInjectionException("DANGROUS FUNCTION: " + function.getName());
}
super.visit(function);
}
@Override
public void visit(WithItem withItem) {
try {
/** 允许 WITH 语句中的子查询 */
disableSubselect.set(false);
super.visit(withItem);
} finally {
disableSubselect.set(true);
}
}
@Override
public void visit(SubSelect subSelect) {
try {
/** 允许语句中的子查询 */
disableSubselect.set(false);
super.visit(subSelect);
} finally {
disableSubselect.set(true);
}
// if (disableSubselect.get()) {
// // 禁用子查询
// throw new JeecgSqlInjectionException("DISABLE subselect " + subSelect);
// }
}
@Override
public void visit(Column tableColumn) {
if (ParserSupport.isBoolean(tableColumn)) {
throw new JeecgSqlInjectionException("DISABLE CONST BOOL " + tableColumn);
}
super.visit(tableColumn);
}
@Override
public void visit(PlainSelect plainSelect) {
if (plainSelect.getSelectItems() != null) {
for (SelectItem item : plainSelect.getSelectItems()) {
item.accept(this);
}
}
if (plainSelect.getFromItem() != null) {
plainSelect.getFromItem().accept(this);
}
if (plainSelect.getJoins() != null) {
for (Join join : plainSelect.getJoins()) {
join.getRightItem().accept(this);
for (Expression e : join.getOnExpressions()) {
e.accept(this);
}
}
}
if (plainSelect.getWhere() != null) {
plainSelect.getWhere().accept(this);
checkConstExpress(plainSelect.getWhere());
}
if (plainSelect.getHaving() != null) {
plainSelect.getHaving().accept(this);
}
if (plainSelect.getOracleHierarchical() != null) {
plainSelect.getOracleHierarchical().accept(this);
}
if (plainSelect.getOrderByElements() != null) {
for (OrderByElement orderByElement : plainSelect.getOrderByElements()) {
orderByElement.getExpression().accept(this);
}
}
if (plainSelect.getGroupBy() != null) {
for (Expression expression : plainSelect.getGroupBy().getGroupByExpressionList().getExpressions()) {
expression.accept(this);
}
}
}
private boolean isConst(Expression expression) {
return constAnalyzer.isConstExpression(expression);
}
private void checkConstExpress(Expression expression) {
if (constAnalyzer.isConstExpression(expression)) {
/** 禁用常量表达式 */
throw new JeecgSqlInjectionException("DISABLE CONST EXPRESSION " + expression);
}
}
}

View File

@ -0,0 +1,65 @@
package org.jeecg.common.util.sqlInjection;
import org.jeecg.common.exception.JeecgSqlInjectionException;
import org.jeecg.common.util.sqlInjection.parse.ParserSupport;
;
/**
* SQL注入攻击分析器
*
* @author guyadong
* 参考:
* https://blog.csdn.net/10km/article/details/127767358
* https://gitee.com/l0km/sql2java/tree/dev/sql2java-manager/src/main/java/gu/sql2java/parser
*/
public class SqlInjectionAnalyzer {
//启用/关闭注入攻击检查
private boolean injectCheckEnable = true;
//防止SQL注入攻击分析实现
private final InjectionSyntaxObjectAnalyzer injectionChecker;
private final InjectionAstNodeVisitor injectionVisitor;
public SqlInjectionAnalyzer() {
this.injectionChecker = new InjectionSyntaxObjectAnalyzer();
this.injectionVisitor = new InjectionAstNodeVisitor();
}
/**
* 启用/关闭注入攻击检查,默认启动
*
* @param enable
* @return
*/
public SqlInjectionAnalyzer injectCheckEnable(boolean enable) {
injectCheckEnable = enable;
return this;
}
/**
* 对解析后的SQL对象执行注入攻击分析有注入攻击的危险则抛出异常{@link JeecgSqlInjectionException}
*
* @param sqlParserInfo
* @throws JeecgSqlInjectionException
*/
public ParserSupport.SqlParserInfo injectAnalyse(ParserSupport.SqlParserInfo sqlParserInfo) throws JeecgSqlInjectionException {
if (null != sqlParserInfo && injectCheckEnable) {
/** SQL注入攻击检查 */
sqlParserInfo.statement.accept(injectionChecker);
sqlParserInfo.simpleNode.jjtAccept(injectionVisitor, null);
}
return sqlParserInfo;
}
/**
* sql校验
*/
public static void checkSql(String sql,boolean check){
SqlInjectionAnalyzer sqlInjectionAnalyzer = new SqlInjectionAnalyzer();
sqlInjectionAnalyzer.injectCheckEnable(check);
ParserSupport.SqlParserInfo sqlParserInfo = ParserSupport.parse0(sql, null,null);
sqlInjectionAnalyzer.injectAnalyse(sqlParserInfo);
}
}

View File

@ -0,0 +1,569 @@
package org.jeecg.common.util.sqlInjection.parse;
import net.sf.jsqlparser.expression.*;
import net.sf.jsqlparser.expression.operators.arithmetic.Addition;
import net.sf.jsqlparser.expression.operators.arithmetic.BitwiseAnd;
import net.sf.jsqlparser.expression.operators.arithmetic.BitwiseLeftShift;
import net.sf.jsqlparser.expression.operators.arithmetic.BitwiseOr;
import net.sf.jsqlparser.expression.operators.arithmetic.BitwiseRightShift;
import net.sf.jsqlparser.expression.operators.arithmetic.BitwiseXor;
import net.sf.jsqlparser.expression.operators.arithmetic.Concat;
import net.sf.jsqlparser.expression.operators.arithmetic.Division;
import net.sf.jsqlparser.expression.operators.arithmetic.IntegerDivision;
import net.sf.jsqlparser.expression.operators.arithmetic.Modulo;
import net.sf.jsqlparser.expression.operators.arithmetic.Multiplication;
import net.sf.jsqlparser.expression.operators.arithmetic.Subtraction;
import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
import net.sf.jsqlparser.expression.operators.conditional.OrExpression;
import net.sf.jsqlparser.expression.operators.conditional.XorExpression;
import net.sf.jsqlparser.expression.operators.relational.Between;
import net.sf.jsqlparser.expression.operators.relational.EqualsTo;
import net.sf.jsqlparser.expression.operators.relational.ExistsExpression;
import net.sf.jsqlparser.expression.operators.relational.ExpressionList;
import net.sf.jsqlparser.expression.operators.relational.FullTextSearch;
import net.sf.jsqlparser.expression.operators.relational.GeometryDistance;
import net.sf.jsqlparser.expression.operators.relational.GreaterThan;
import net.sf.jsqlparser.expression.operators.relational.GreaterThanEquals;
import net.sf.jsqlparser.expression.operators.relational.InExpression;
import net.sf.jsqlparser.expression.operators.relational.IsBooleanExpression;
import net.sf.jsqlparser.expression.operators.relational.IsDistinctExpression;
import net.sf.jsqlparser.expression.operators.relational.IsNullExpression;
import net.sf.jsqlparser.expression.operators.relational.ItemsListVisitor;
import net.sf.jsqlparser.expression.operators.relational.JsonOperator;
import net.sf.jsqlparser.expression.operators.relational.LikeExpression;
import net.sf.jsqlparser.expression.operators.relational.Matches;
import net.sf.jsqlparser.expression.operators.relational.MinorThan;
import net.sf.jsqlparser.expression.operators.relational.MinorThanEquals;
import net.sf.jsqlparser.expression.operators.relational.MultiExpressionList;
import net.sf.jsqlparser.expression.operators.relational.NamedExpressionList;
import net.sf.jsqlparser.expression.operators.relational.NotEqualsTo;
import net.sf.jsqlparser.expression.operators.relational.RegExpMatchOperator;
import net.sf.jsqlparser.expression.operators.relational.RegExpMySQLOperator;
import net.sf.jsqlparser.expression.operators.relational.SimilarToExpression;
import net.sf.jsqlparser.schema.Column;
import net.sf.jsqlparser.statement.select.AllColumns;
import net.sf.jsqlparser.statement.select.AllTableColumns;
import net.sf.jsqlparser.statement.select.OrderByElement;
import net.sf.jsqlparser.statement.select.SubSelect;
/**
* 判断表达是否为常量的分析器
*
* @author guyadong
*/
public class ConstAnalyzer implements ExpressionVisitor, ItemsListVisitor {
private static ThreadLocal<Boolean> constFlag = new ThreadLocal<Boolean>() {
@Override
protected Boolean initialValue() {
return true;
}
};
@Override
public void visit(NullValue value) {
}
@Override
public void visit(Function function) {
constFlag.set(false);
}
@Override
public void visit(SignedExpression expr) {
expr.getExpression().accept(this);
}
@Override
public void visit(JdbcParameter parameter) {
constFlag.set(false);
}
@Override
public void visit(JdbcNamedParameter parameter) {
constFlag.set(false);
}
@Override
public void visit(DoubleValue value) {
}
@Override
public void visit(LongValue value) {
}
@Override
public void visit(DateValue value) {
}
@Override
public void visit(TimeValue value) {
}
@Override
public void visit(TimestampValue value) {
}
@Override
public void visit(Parenthesis parenthesis) {
parenthesis.getExpression().accept(this);
}
@Override
public void visit(StringValue value) {
}
@Override
public void visit(Addition expr) {
visitBinaryExpression(expr);
}
@Override
public void visit(Division expr) {
visitBinaryExpression(expr);
}
@Override
public void visit(IntegerDivision expr) {
visitBinaryExpression(expr);
}
@Override
public void visit(Multiplication expr) {
visitBinaryExpression(expr);
}
@Override
public void visit(Subtraction expr) {
visitBinaryExpression(expr);
}
@Override
public void visit(AndExpression expr) {
visitBinaryExpression(expr);
}
@Override
public void visit(OrExpression expr) {
visitBinaryExpression(expr);
}
@Override
public void visit(XorExpression expr) {
visitBinaryExpression(expr);
}
@Override
public void visit(Between expr) {
expr.getLeftExpression().accept(this);
expr.getBetweenExpressionStart().accept(this);
expr.getBetweenExpressionEnd().accept(this);
}
/**
* 用于处理 OverlapsCondition 类型的表达式
* @param overlapsCondition
*/
@Override
public void visit(OverlapsCondition overlapsCondition) {
constFlag.set(false);
}
/**
* 用于处理 SafeCastExpression 类型的表达式。
* @param safeCastExpression
*/
@Override
public void visit(SafeCastExpression safeCastExpression) {
constFlag.set(false);
}
@Override
public void visit(EqualsTo expr) {
visitBinaryExpression(expr);
}
@Override
public void visit(GreaterThan expr) {
visitBinaryExpression(expr);
}
@Override
public void visit(GreaterThanEquals expr) {
visitBinaryExpression(expr);
}
@Override
public void visit(InExpression expr) {
if (expr.getLeftExpression() != null) {
expr.getLeftExpression().accept(this);
}
}
@Override
public void visit(IsNullExpression expr) {
expr.getLeftExpression().accept(this);
}
@Override
public void visit(FullTextSearch expr) {
constFlag.set(false);
}
@Override
public void visit(IsBooleanExpression expr) {
expr.getLeftExpression().accept(this);
}
@Override
public void visit(LikeExpression expr) {
visitBinaryExpression(expr);
}
@Override
public void visit(MinorThan expr) {
visitBinaryExpression(expr);
}
@Override
public void visit(MinorThanEquals expr) {
visitBinaryExpression(expr);
}
@Override
public void visit(NotEqualsTo expr) {
visitBinaryExpression(expr);
}
@Override
public void visit(Column column) {
if (!ParserSupport.isBoolean(column)) {
constFlag.set(false);
}
}
@Override
public void visit(SubSelect subSelect) {
constFlag.set(false);
}
@Override
public void visit(CaseExpression expr) {
constFlag.set(false);
}
@Override
public void visit(WhenClause expr) {
constFlag.set(false);
}
@Override
public void visit(ExistsExpression expr) {
constFlag.set(false);
}
@Override
public void visit(AnyComparisonExpression expr) {
constFlag.set(false);
}
@Override
public void visit(Concat expr) {
visitBinaryExpression(expr);
}
@Override
public void visit(Matches expr) {
visitBinaryExpression(expr);
}
@Override
public void visit(BitwiseAnd expr) {
visitBinaryExpression(expr);
}
@Override
public void visit(BitwiseOr expr) {
visitBinaryExpression(expr);
}
@Override
public void visit(BitwiseXor expr) {
visitBinaryExpression(expr);
}
@Override
public void visit(CastExpression expr) {
expr.getLeftExpression().accept(this);
}
@Override
public void visit(TryCastExpression expr) {
constFlag.set(false);
}
@Override
public void visit(Modulo expr) {
visitBinaryExpression(expr);
}
@Override
public void visit(AnalyticExpression expr) {
constFlag.set(false);
}
@Override
public void visit(ExtractExpression expr) {
expr.getExpression().accept(this);
}
@Override
public void visit(IntervalExpression expr) {
constFlag.set(false);
}
@Override
public void visit(OracleHierarchicalExpression expr) {
constFlag.set(false);
}
@Override
public void visit(RegExpMatchOperator expr) {
visitBinaryExpression(expr);
}
@Override
public void visit(ExpressionList expressionList) {
for (Expression expr : expressionList.getExpressions()) {
expr.accept(this);
}
}
@Override
public void visit(NamedExpressionList namedExpressionList) {
for (Expression expr : namedExpressionList.getExpressions()) {
expr.accept(this);
}
}
@Override
public void visit(MultiExpressionList multiExprList) {
for (ExpressionList list : multiExprList.getExpressionLists()) {
visit(list);
}
}
@Override
public void visit(NotExpression notExpr) {
notExpr.getExpression().accept(this);
}
@Override
public void visit(BitwiseRightShift expr) {
visitBinaryExpression(expr);
}
@Override
public void visit(BitwiseLeftShift expr) {
visitBinaryExpression(expr);
}
protected void visitBinaryExpression(BinaryExpression expr) {
expr.getLeftExpression().accept(this);
expr.getRightExpression().accept(this);
}
@Override
public void visit(JsonExpression jsonExpr) {
jsonExpr.getExpression().accept(this);
}
@Override
public void visit(JsonOperator expr) {
visitBinaryExpression(expr);
}
@Override
public void visit(RegExpMySQLOperator expr) {
visitBinaryExpression(expr);
}
@Override
public void visit(UserVariable var) {
constFlag.set(false);
}
@Override
public void visit(NumericBind bind) {
constFlag.set(false);
}
@Override
public void visit(KeepExpression expr) {
for (OrderByElement element : expr.getOrderByElements()) {
element.getExpression().accept(this);
}
}
@Override
public void visit(MySQLGroupConcat groupConcat) {
constFlag.set(false);
}
@Override
public void visit(ValueListExpression valueListExpression) {
for (Expression expr : valueListExpression.getExpressionList().getExpressions()) {
expr.accept(this);
}
}
@Override
public void visit(AllColumns allColumns) {
}
@Override
public void visit(AllTableColumns allTableColumns) {
}
@Override
public void visit(AllValue allValue) {
}
@Override
public void visit(IsDistinctExpression isDistinctExpression) {
visitBinaryExpression(isDistinctExpression);
}
@Override
public void visit(RowGetExpression rowGetExpression) {
rowGetExpression.getExpression().accept(this);
}
@Override
public void visit(HexValue hexValue) {
}
@Override
public void visit(OracleHint hint) {
}
@Override
public void visit(TimeKeyExpression timeKeyExpression) {
}
@Override
public void visit(DateTimeLiteralExpression literal) {
}
@Override
public void visit(NextValExpression nextVal) {
constFlag.set(false);
}
@Override
public void visit(CollateExpression col) {
constFlag.set(false);
}
@Override
public void visit(SimilarToExpression expr) {
visitBinaryExpression(expr);
}
@Override
public void visit(ArrayExpression array) {
array.getObjExpression().accept(this);
if (array.getIndexExpression() != null) {
array.getIndexExpression().accept(this);
}
if (array.getStartIndexExpression() != null) {
array.getStartIndexExpression().accept(this);
}
if (array.getStopIndexExpression() != null) {
array.getStopIndexExpression().accept(this);
}
}
@Override
public void visit(ArrayConstructor aThis) {
for (Expression expression : aThis.getExpressions()) {
expression.accept(this);
}
}
@Override
public void visit(VariableAssignment var) {
constFlag.set(false);
}
@Override
public void visit(XMLSerializeExpr expr) {
constFlag.set(false);
}
@Override
public void visit(TimezoneExpression expr) {
expr.getLeftExpression().accept(this);
}
@Override
public void visit(JsonAggregateFunction expression) {
Expression expr = expression.getExpression();
if (expr != null) {
expr.accept(this);
}
expr = expression.getFilterExpression();
if (expr != null) {
expr.accept(this);
}
}
@Override
public void visit(JsonFunction expression) {
for (JsonFunctionExpression expr : expression.getExpressions()) {
expr.getExpression().accept(this);
}
}
@Override
public void visit(ConnectByRootOperator connectByRootOperator) {
constFlag.set(false);
}
@Override
public void visit(OracleNamedFunctionParameter oracleNamedFunctionParameter) {
constFlag.set(false);
}
@Override
public void visit(GeometryDistance geometryDistance) {
visitBinaryExpression(geometryDistance);
}
@Override
public void visit(RowConstructor rowConstructor) {
constFlag.set(false);
}
public boolean isConstExpression(Expression expression) {
if (null != expression) {
constFlag.set(true);
expression.accept(this);
return constFlag.get();
}
return false;
}
}

View File

@ -0,0 +1,177 @@
package org.jeecg.common.util.sqlInjection.parse;
import lombok.extern.slf4j.Slf4j;
import net.sf.jsqlparser.JSQLParserException;
import net.sf.jsqlparser.parser.*;
import net.sf.jsqlparser.schema.Column;
import net.sf.jsqlparser.statement.Statement;
import net.sf.jsqlparser.statement.select.PlainSelect;
import net.sf.jsqlparser.statement.select.Select;
import net.sf.jsqlparser.statement.select.SelectBody;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import java.lang.reflect.InvocationTargetException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import com.google.common.base.Throwables;
import org.jeecg.common.exception.JeecgBootException;
import org.jeecg.common.exception.JeecgSqlInjectionException;
/**
* 解析sql支持
*/
@Slf4j
public class ParserSupport {
/**
* 解析SELECT SQL语句,解析失败或非SELECT语句则抛出异常
*
* @param sql
* @return
*/
public static Select parseSelect(String sql) {
Statement stmt;
try {
stmt = CCJSqlParserUtil.parse(checkNotNull(sql, "sql is null"));
} catch (JSQLParserException e) {
throw new JeecgBootException(e);
}
checkArgument(stmt instanceof Select, "%s is not SELECT statment", sql);
Select select = (Select) stmt;
SelectBody selectBody = select.getSelectBody();
// 暂时只支持简单的SELECT xxxx FROM ....语句不支持复杂语句如WITH
checkArgument(selectBody instanceof PlainSelect, "ONLY SUPPORT plain select statement %s", sql);
return (Select) stmt;
}
/**
* 解析SELECT SQL语句,解析失败或非SELECT语句则
*
* @param sql
* @return
*/
public static Select parseSelectUnchecked(String sql) {
try {
return parseSelect(sql);
} catch (Exception e) {
return null;
}
}
/**
* 实现SQL语句解析,解析成功则返回解析后的{@link Statement}
* 并通过{@code visitor}参数提供基于AST(抽象语法树)的遍历所有节点的能力。
*
* @param sql SQL语句
* @param visitor 遍历所有节点的{@link SimpleNodeVisitor}接口实例,为{@code null}忽略
* @param sqlSyntaxNormalizer SQL语句分析转换器为{@code null}忽略
* @throws JSQLParserException 输入的SQL语句有语法错误
* @see #parse0(String, CCJSqlParserVisitor, SqlSyntaxNormalizer)
*/
public static Statement parse(String sql, CCJSqlParserVisitor visitor, SqlSyntaxNormalizer sqlSyntaxNormalizer) throws JSQLParserException {
return parse0(sql, visitor, sqlSyntaxNormalizer).statement;
}
/**
* 参照{@link CCJSqlParserUtil#parseAST(String)}和{@link CCJSqlParserUtil#parse(String)}实现SQL语句解析,
* 解析成功则返回解析后的{@link SqlParserInfo}对象,
* 并通过{@code visitor}参数提供基于AST(抽象语法树)的遍历所有节点的能力。
*
* @param sql SQL语句
* @param visitor 遍历所有节点的{@link SimpleNodeVisitor}接口实例,为{@code null}忽略
* @param sqlSyntaxAnalyzer SQL语句分析转换器为{@code null}忽略
* @throws JSQLParserException 输入的SQL语句有语法错误
* @see net.sf.jsqlparser.parser.Node#jjtAccept(SimpleNodeVisitor, Object)
*/
public static SqlParserInfo parse0(String sql, CCJSqlParserVisitor visitor, SqlSyntaxNormalizer sqlSyntaxAnalyzer) throws JeecgSqlInjectionException {
//检查是否非select开头暂不支持
if(!sql.toLowerCase().trim().startsWith("select ")) {
log.warn("传入sql 非select开头不支持非select开头的语句解析");
return null;
}
//检查是否存储过程,暂不支持
if(sql.toLowerCase().trim().startsWith("call ")){
log.warn("传入call 开头存储过程,不支持存储过程解析!");
return null;
}
//检查特殊语义的特殊字符,目前检查冒号、$、#三种特殊语义字符
String specialCharacters = "[:$#]";
Pattern pattern = Pattern.compile(specialCharacters);
Matcher matcher = pattern.matcher(sql);
if (matcher.find()) {
sql = sql.replaceAll("[:$#]", "@");
}
checkArgument(null != sql, "sql is null");
boolean allowComplexParsing = CCJSqlParserUtil.getNestingDepth(sql) <= CCJSqlParserUtil.ALLOWED_NESTING_DEPTH;
CCJSqlParser parser = CCJSqlParserUtil.newParser(sql).withAllowComplexParsing(allowComplexParsing);
Statement stmt;
try {
stmt = parser.Statement();
} catch (Exception ex) {
log.error("请注意SQL语法可能存在问题---> {}", ex.getMessage());
throw new JeecgSqlInjectionException("请注意SQL语法可能存在问题:"+sql);
}
if (null != visitor) {
parser.getASTRoot().jjtAccept(visitor, null);
}
if (null != sqlSyntaxAnalyzer) {
stmt.accept(sqlSyntaxAnalyzer.resetChanged());
}
return new SqlParserInfo(stmt.toString(), stmt, (SimpleNode) parser.getASTRoot());
}
/**
* 调用{@link CCJSqlParser}解析SQL语句部件返回解析生成的对象,如{@code 'ORDER BY id DESC'}
*
* @param <T>
* @param input
* @param method 指定调用的{@link CCJSqlParser}解析方法
* @param targetType 返回的解析对象类型
* @return
* @since 3.18.3
*/
public static <T> T parseComponent(String input, String method, Class<T> targetType) {
try {
CCJSqlParser parser = new CCJSqlParser(new StringProvider(input));
try {
return checkNotNull(targetType, "targetType is null").cast(parser.getClass().getMethod(method).invoke(parser));
} catch (InvocationTargetException e) {
Throwables.throwIfUnchecked(e.getTargetException());
throw new RuntimeException(e.getTargetException());
}
} catch (IllegalAccessException | NoSuchMethodException | SecurityException e) {
Throwables.throwIfUnchecked(e);
throw new RuntimeException(e);
}
}
/**
* 如果{@link Column}没有定义table,且字段名为true/false(不区分大小写)则视为布尔常量
*
* @param column
*/
public static boolean isBoolean(Column column) {
return null != column && null == column.getTable() &&
Pattern.compile("(true|false)", Pattern.CASE_INSENSITIVE).matcher(column.getColumnName()).matches();
}
public static class SqlParserInfo {
public String nativeSql;
public Statement statement;
public SimpleNode simpleNode;
SqlParserInfo(String nativeSql, Statement statement, SimpleNode simpleNode) {
this.nativeSql = nativeSql;
this.statement = statement;
this.simpleNode = simpleNode;
}
}
}

View File

@ -0,0 +1,37 @@
package org.jeecg.common.util.sqlInjection.parse;
import net.sf.jsqlparser.util.TablesNamesFinder;
/**
* SQL语句分析转换器基类<br>
* 基于SQL语法对象实现对SQL的修改
* (暂时用不到)
*
* @author guyadong
* @since 3.17.0
*/
public class SqlSyntaxNormalizer extends TablesNamesFinder {
protected static final ThreadLocal<Boolean> changed = new ThreadLocal<>();
public SqlSyntaxNormalizer() {
super();
init(true);
}
/**
* 语句改变返回{@code true},否则返回{@code false}
*/
public boolean changed() {
return Boolean.TRUE.equals(changed.get());
}
/**
* 复位线程局部变量{@link #changed}状态
*/
public SqlSyntaxNormalizer resetChanged() {
changed.remove();
return this;
}
}

View File

@ -1,255 +1,255 @@
//package org.jeecg.common.util.sqlparse;
//
//import lombok.extern.slf4j.Slf4j;
//import net.sf.jsqlparser.JSQLParserException;
//import net.sf.jsqlparser.expression.*;
//import net.sf.jsqlparser.parser.CCJSqlParserManager;
//import net.sf.jsqlparser.schema.Column;
//import net.sf.jsqlparser.schema.Table;
//import net.sf.jsqlparser.statement.Statement;
//import net.sf.jsqlparser.statement.select.*;
//import org.jeecg.common.exception.JeecgBootException;
//import org.jeecg.common.util.sqlparse.vo.SelectSqlInfo;
//
//import java.io.StringReader;
//import java.util.ArrayList;
//import java.util.HashMap;
//import java.util.List;
//import java.util.Map;
//
///**
// * 解析所有表名和字段的类
// */
//@Slf4j
//public class JSqlParserAllTableManager {
//
// private final String sql;
// private final Map<String, SelectSqlInfo> allTableMap = new HashMap<>();
// /**
// * 别名对应实际表名
// */
// private final Map<String, String> tableAliasMap = new HashMap<>();
//
// /**
// * 解析后的sql
// */
// private String parsedSql = null;
//
// JSqlParserAllTableManager(String selectSql) {
// this.sql = selectSql;
// }
//
// /**
// * 开始解析
// *
// * @return
// * @throws JSQLParserException
// */
// public Map<String, SelectSqlInfo> parse() throws JSQLParserException {
// // 1. 创建解析器
// CCJSqlParserManager mgr = new CCJSqlParserManager();
// // 2. 使用解析器解析sql生成具有层次结构的java类
// Statement stmt = mgr.parse(new StringReader(this.sql));
// if (stmt instanceof Select) {
// Select selectStatement = (Select) stmt;
// SelectBody selectBody = selectStatement.getSelectBody();
// this.parsedSql = selectBody.toString();
// // 3. 解析select查询sql的信息
// if (selectBody instanceof PlainSelect) {
// PlainSelect plainSelect = (PlainSelect) selectBody;
// // 4. 合并 fromItems
// List<FromItem> fromItems = new ArrayList<>();
// fromItems.add(plainSelect.getFromItem());
// // 4.1 处理join的表
// List<Join> joins = plainSelect.getJoins();
// if (joins != null) {
// joins.forEach(join -> fromItems.add(join.getRightItem()));
// }
// // 5. 处理 fromItems
// for (FromItem fromItem : fromItems) {
// // 5.1 通过表名的方式from
// if (fromItem instanceof Table) {
// this.addSqlInfoByTable((Table) fromItem);
// }
// // 5.2 通过子查询的方式from
// else if (fromItem instanceof SubSelect) {
// this.handleSubSelect((SubSelect) fromItem);
// }
// }
// // 6. 解析 selectFields
// List<SelectItem> selectItems = plainSelect.getSelectItems();
// for (SelectItem selectItem : selectItems) {
// // 6.1 查询的是全部字段
// if (selectItem instanceof AllColumns) {
// // 当 selectItem 为 AllColumns 时fromItem 必定为 Table
// String tableName = plainSelect.getFromItem(Table.class).getName();
// // 此处必定不为空,因为在解析 fromItem 时,已经将表名添加到 allTableMap 中
// SelectSqlInfo sqlInfo = this.allTableMap.get(tableName);
// assert sqlInfo != null;
// // 设置为查询全部字段
// sqlInfo.setSelectAll(true);
// sqlInfo.setSelectFields(null);
// sqlInfo.setRealSelectFields(null);
// }
// // 6.2 查询的是带表别名( u.* )的全部字段
// else if (selectItem instanceof AllTableColumns) {
// AllTableColumns allTableColumns = (AllTableColumns) selectItem;
// String aliasName = allTableColumns.getTable().getName();
// // 通过别名获取表名
// String tableName = this.tableAliasMap.get(aliasName);
// if (tableName == null) {
// tableName = aliasName;
// }
// SelectSqlInfo sqlInfo = this.allTableMap.get(tableName);
// // 如果此处为空,则说明该字段是通过子查询获取的,所以可以不处理,只有实际表才需要处理
// if (sqlInfo != null) {
// // 设置为查询全部字段
// sqlInfo.setSelectAll(true);
// sqlInfo.setSelectFields(null);
// sqlInfo.setRealSelectFields(null);
// }
// }
// // 6.3 各种字段表达式处理
// else if (selectItem instanceof SelectExpressionItem) {
// SelectExpressionItem selectExpressionItem = (SelectExpressionItem) selectItem;
// Expression expression = selectExpressionItem.getExpression();
// Alias alias = selectExpressionItem.getAlias();
// this.handleExpression(expression, alias, plainSelect.getFromItem());
// }
// }
// } else {
// log.warn("暂时尚未处理该类型的 SelectBody: {}", selectBody.getClass().getName());
// throw new JeecgBootException("暂时尚未处理该类型的 SelectBody");
// }
// } else {
// // 非 select 查询sql不做处理
// throw new JeecgBootException("非 select 查询sql不做处理");
// }
// return this.allTableMap;
// }
//
// /**
// * 处理子查询
// *
// * @param subSelect
// */
// private void handleSubSelect(SubSelect subSelect) {
// try {
// String subSelectSql = subSelect.getSelectBody().toString();
// // 递归调用解析
// Map<String, SelectSqlInfo> map = JSqlParserUtils.parseAllSelectTable(subSelectSql);
// if (map != null) {
// this.assignMap(map);
// }
// } catch (Exception e) {
// log.error("解析子查询出错", e);
// }
// }
//
// /**
// * 处理查询字段表达式
// *
// * @param expression
// */
// private void handleExpression(Expression expression, Alias alias, FromItem fromItem) {
// // 处理函数式字段 CONCAT(name,'(',age,')')
// if (expression instanceof Function) {
// Function functionExp = (Function) expression;
// List<Expression> expressions = functionExp.getParameters().getExpressions();
// for (Expression expItem : expressions) {
// this.handleExpression(expItem, null, fromItem);
// }
// return;
// }
// // 处理字段上的子查询
// if (expression instanceof SubSelect) {
// this.handleSubSelect((SubSelect) expression);
// return;
// }
// // 不处理字面量
// if (expression instanceof StringValue ||
// expression instanceof NullValue ||
// expression instanceof LongValue ||
// expression instanceof DoubleValue ||
// expression instanceof HexValue ||
// expression instanceof DateValue ||
// expression instanceof TimestampValue ||
// expression instanceof TimeValue
// ) {
// return;
// }
//
// // 处理字段
// if (expression instanceof Column) {
// Column column = (Column) expression;
// // 查询字段名
// String fieldName = column.getColumnName();
// String aliasName = fieldName;
// if (alias != null) {
// aliasName = alias.getName();
// }
// String tableName;
// if (column.getTable() != null) {
// // 通过列的表名获取 sqlInfo
// // 例如 user.name这里的 tableName 就是 user
// tableName = column.getTable().getName();
// // 有可能是别名,需要转换为真实表名
// if (this.tableAliasMap.get(tableName) != null) {
// tableName = this.tableAliasMap.get(tableName);
// }
// } else {
// // 当column的table为空时说明是 fromItem 中的字段
// tableName = ((Table) fromItem).getName();
// }
// SelectSqlInfo $sqlInfo = this.allTableMap.get(tableName);
// if ($sqlInfo != null) {
// $sqlInfo.addSelectField(aliasName, fieldName);
// } else {
// log.warn("发生意外情况,未找到表名为 {} 的 SelectSqlInfo", tableName);
// }
// }
// }
//
// /**
// * 根据表名添加sqlInfo
// *
// * @param table
// */
// private void addSqlInfoByTable(Table table) {
// String tableName = table.getName();
// // 解析 aliasName
// if (table.getAlias() != null) {
// this.tableAliasMap.put(table.getAlias().getName(), tableName);
// }
// SelectSqlInfo sqlInfo = new SelectSqlInfo(this.parsedSql);
// sqlInfo.setFromTableName(table.getName());
// this.allTableMap.put(sqlInfo.getFromTableName(), sqlInfo);
// }
//
// /**
// * 合并map
// *
// * @param source
// */
// private void assignMap(Map<String, SelectSqlInfo> source) {
// for (Map.Entry<String, SelectSqlInfo> entry : source.entrySet()) {
// SelectSqlInfo sqlInfo = this.allTableMap.get(entry.getKey());
// if (sqlInfo == null) {
// this.allTableMap.put(entry.getKey(), entry.getValue());
// } else {
// // 合并
// if (sqlInfo.getSelectFields() == null) {
// sqlInfo.setSelectFields(entry.getValue().getSelectFields());
// } else {
// sqlInfo.getSelectFields().addAll(entry.getValue().getSelectFields());
// }
// if (sqlInfo.getRealSelectFields() == null) {
// sqlInfo.setRealSelectFields(entry.getValue().getRealSelectFields());
// } else {
// sqlInfo.getRealSelectFields().addAll(entry.getValue().getRealSelectFields());
// }
// }
// }
// }
//
//}
package org.jeecg.common.util.sqlparse;
import lombok.extern.slf4j.Slf4j;
import net.sf.jsqlparser.JSQLParserException;
import net.sf.jsqlparser.expression.*;
import net.sf.jsqlparser.parser.CCJSqlParserManager;
import net.sf.jsqlparser.schema.Column;
import net.sf.jsqlparser.schema.Table;
import net.sf.jsqlparser.statement.Statement;
import net.sf.jsqlparser.statement.select.*;
import org.jeecg.common.exception.JeecgBootException;
import org.jeecg.common.util.sqlparse.vo.SelectSqlInfo;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 解析所有表名和字段的类
*/
@Slf4j
public class JSqlParserAllTableManager {
private final String sql;
private final Map<String, SelectSqlInfo> allTableMap = new HashMap<>();
/**
* 别名对应实际表名
*/
private final Map<String, String> tableAliasMap = new HashMap<>();
/**
* 解析后的sql
*/
private String parsedSql = null;
JSqlParserAllTableManager(String selectSql) {
this.sql = selectSql;
}
/**
* 开始解析
*
* @return
* @throws JSQLParserException
*/
public Map<String, SelectSqlInfo> parse() throws JSQLParserException {
// 1. 创建解析器
CCJSqlParserManager mgr = new CCJSqlParserManager();
// 2. 使用解析器解析sql生成具有层次结构的java类
Statement stmt = mgr.parse(new StringReader(this.sql));
if (stmt instanceof Select) {
Select selectStatement = (Select) stmt;
SelectBody selectBody = selectStatement.getSelectBody();
this.parsedSql = selectBody.toString();
// 3. 解析select查询sql的信息
if (selectBody instanceof PlainSelect) {
PlainSelect plainSelect = (PlainSelect) selectBody;
// 4. 合并 fromItems
List<FromItem> fromItems = new ArrayList<>();
fromItems.add(plainSelect.getFromItem());
// 4.1 处理join的表
List<Join> joins = plainSelect.getJoins();
if (joins != null) {
joins.forEach(join -> fromItems.add(join.getRightItem()));
}
// 5. 处理 fromItems
for (FromItem fromItem : fromItems) {
// 5.1 通过表名的方式from
if (fromItem instanceof Table) {
this.addSqlInfoByTable((Table) fromItem);
}
// 5.2 通过子查询的方式from
else if (fromItem instanceof SubSelect) {
this.handleSubSelect((SubSelect) fromItem);
}
}
// 6. 解析 selectFields
List<SelectItem> selectItems = plainSelect.getSelectItems();
for (SelectItem selectItem : selectItems) {
// 6.1 查询的是全部字段
if (selectItem instanceof AllColumns) {
// 当 selectItem 为 AllColumns 时fromItem 必定为 Table
String tableName = plainSelect.getFromItem(Table.class).getName();
// 此处必定不为空,因为在解析 fromItem 时,已经将表名添加到 allTableMap 中
SelectSqlInfo sqlInfo = this.allTableMap.get(tableName);
assert sqlInfo != null;
// 设置为查询全部字段
sqlInfo.setSelectAll(true);
sqlInfo.setSelectFields(null);
sqlInfo.setRealSelectFields(null);
}
// 6.2 查询的是带表别名( u.* )的全部字段
else if (selectItem instanceof AllTableColumns) {
AllTableColumns allTableColumns = (AllTableColumns) selectItem;
String aliasName = allTableColumns.getTable().getName();
// 通过别名获取表名
String tableName = this.tableAliasMap.get(aliasName);
if (tableName == null) {
tableName = aliasName;
}
SelectSqlInfo sqlInfo = this.allTableMap.get(tableName);
// 如果此处为空,则说明该字段是通过子查询获取的,所以可以不处理,只有实际表才需要处理
if (sqlInfo != null) {
// 设置为查询全部字段
sqlInfo.setSelectAll(true);
sqlInfo.setSelectFields(null);
sqlInfo.setRealSelectFields(null);
}
}
// 6.3 各种字段表达式处理
else if (selectItem instanceof SelectExpressionItem) {
SelectExpressionItem selectExpressionItem = (SelectExpressionItem) selectItem;
Expression expression = selectExpressionItem.getExpression();
Alias alias = selectExpressionItem.getAlias();
this.handleExpression(expression, alias, plainSelect.getFromItem());
}
}
} else {
log.warn("暂时尚未处理该类型的 SelectBody: {}", selectBody.getClass().getName());
throw new JeecgBootException("暂时尚未处理该类型的 SelectBody");
}
} else {
// 非 select 查询sql不做处理
throw new JeecgBootException("非 select 查询sql不做处理");
}
return this.allTableMap;
}
/**
* 处理子查询
*
* @param subSelect
*/
private void handleSubSelect(SubSelect subSelect) {
try {
String subSelectSql = subSelect.getSelectBody().toString();
// 递归调用解析
Map<String, SelectSqlInfo> map = JSqlParserUtils.parseAllSelectTable(subSelectSql);
if (map != null) {
this.assignMap(map);
}
} catch (Exception e) {
log.error("解析子查询出错", e);
}
}
/**
* 处理查询字段表达式
*
* @param expression
*/
private void handleExpression(Expression expression, Alias alias, FromItem fromItem) {
// 处理函数式字段 CONCAT(name,'(',age,')')
if (expression instanceof Function) {
Function functionExp = (Function) expression;
List<Expression> expressions = functionExp.getParameters().getExpressions();
for (Expression expItem : expressions) {
this.handleExpression(expItem, null, fromItem);
}
return;
}
// 处理字段上的子查询
if (expression instanceof SubSelect) {
this.handleSubSelect((SubSelect) expression);
return;
}
// 不处理字面量
if (expression instanceof StringValue ||
expression instanceof NullValue ||
expression instanceof LongValue ||
expression instanceof DoubleValue ||
expression instanceof HexValue ||
expression instanceof DateValue ||
expression instanceof TimestampValue ||
expression instanceof TimeValue
) {
return;
}
// 处理字段
if (expression instanceof Column) {
Column column = (Column) expression;
// 查询字段名
String fieldName = column.getColumnName();
String aliasName = fieldName;
if (alias != null) {
aliasName = alias.getName();
}
String tableName;
if (column.getTable() != null) {
// 通过列的表名获取 sqlInfo
// 例如 user.name这里的 tableName 就是 user
tableName = column.getTable().getName();
// 有可能是别名,需要转换为真实表名
if (this.tableAliasMap.get(tableName) != null) {
tableName = this.tableAliasMap.get(tableName);
}
} else {
// 当column的table为空时说明是 fromItem 中的字段
tableName = ((Table) fromItem).getName();
}
SelectSqlInfo $sqlInfo = this.allTableMap.get(tableName);
if ($sqlInfo != null) {
$sqlInfo.addSelectField(aliasName, fieldName);
} else {
log.warn("发生意外情况,未找到表名为 {} 的 SelectSqlInfo", tableName);
}
}
}
/**
* 根据表名添加sqlInfo
*
* @param table
*/
private void addSqlInfoByTable(Table table) {
String tableName = table.getName();
// 解析 aliasName
if (table.getAlias() != null) {
this.tableAliasMap.put(table.getAlias().getName(), tableName);
}
SelectSqlInfo sqlInfo = new SelectSqlInfo(this.parsedSql);
sqlInfo.setFromTableName(table.getName());
this.allTableMap.put(sqlInfo.getFromTableName(), sqlInfo);
}
/**
* 合并map
*
* @param source
*/
private void assignMap(Map<String, SelectSqlInfo> source) {
for (Map.Entry<String, SelectSqlInfo> entry : source.entrySet()) {
SelectSqlInfo sqlInfo = this.allTableMap.get(entry.getKey());
if (sqlInfo == null) {
this.allTableMap.put(entry.getKey(), entry.getValue());
} else {
// 合并
if (sqlInfo.getSelectFields() == null) {
sqlInfo.setSelectFields(entry.getValue().getSelectFields());
} else {
sqlInfo.getSelectFields().addAll(entry.getValue().getSelectFields());
}
if (sqlInfo.getRealSelectFields() == null) {
sqlInfo.setRealSelectFields(entry.getValue().getRealSelectFields());
} else {
sqlInfo.getRealSelectFields().addAll(entry.getValue().getRealSelectFields());
}
}
}
}
}

View File

@ -1,190 +1,190 @@
//package org.jeecg.common.util.sqlparse;
//
//import lombok.extern.slf4j.Slf4j;
//import net.sf.jsqlparser.JSQLParserException;
//import net.sf.jsqlparser.expression.*;
//import net.sf.jsqlparser.parser.CCJSqlParserManager;
//import net.sf.jsqlparser.schema.Column;
//import net.sf.jsqlparser.schema.Table;
//import net.sf.jsqlparser.statement.Statement;
//import net.sf.jsqlparser.statement.select.*;
//import org.jeecg.common.exception.JeecgBootException;
//import org.jeecg.common.util.oConvertUtils;
//import org.jeecg.common.util.sqlparse.vo.SelectSqlInfo;
//
//import java.io.StringReader;
//import java.util.List;
//import java.util.Map;
//
//@Slf4j
//public class JSqlParserUtils {
//
// /**
// * 解析 查询selectsql的信息
// * 此方法会展开所有子查询到一个map里
// * key只存真实的表名如果查询的没有真实的表名则会被忽略。
// * value只存真实的字段名如果查询的没有真实的字段名则会被忽略。
// * <p>
// * 例如SELECT a.*,d.age,(SELECT count(1) FROM sys_depart) AS count FROM (SELECT username AS foo, realname FROM sys_user) a, demo d
// * 解析后的结果为:{sys_user=[username, realname], demo=[age], sys_depart=[]}
// *
// * @param selectSql
// * @return
// */
// public static Map<String, SelectSqlInfo> parseAllSelectTable(String selectSql) throws JSQLParserException {
// if (oConvertUtils.isEmpty(selectSql)) {
// return null;
// }
// // log.info("解析查询Sql{}", selectSql);
// JSqlParserAllTableManager allTableManager = new JSqlParserAllTableManager(selectSql);
// return allTableManager.parse();
// }
//
// /**
// * 解析 查询selectsql的信息子查询嵌套
// *
// * @param selectSql
// * @return
// */
// public static SelectSqlInfo parseSelectSqlInfo(String selectSql) throws JSQLParserException {
// if (oConvertUtils.isEmpty(selectSql)) {
// return null;
// }
// // log.info("解析查询Sql{}", selectSql);
// // 使用 JSqlParer 解析sql
// // 1、创建解析器
// CCJSqlParserManager mgr = new CCJSqlParserManager();
// // 2、使用解析器解析sql生成具有层次结构的java类
// Statement stmt = mgr.parse(new StringReader(selectSql));
// if (stmt instanceof Select) {
// Select selectStatement = (Select) stmt;
// // 3、解析select查询sql的信息
// return JSqlParserUtils.parseBySelectBody(selectStatement.getSelectBody());
// } else {
// // 非 select 查询sql不做处理
// throw new JeecgBootException("非 select 查询sql不做处理");
// }
// }
//
// /**
// * 解析 select 查询sql的信息
// *
// * @param selectBody
// * @return
// */
// private static SelectSqlInfo parseBySelectBody(SelectBody selectBody) {
// // 判断是否使用了union等操作
// if (selectBody instanceof SetOperationList) {
// // 如果使用了union等操作则只解析第一个查询
// List<SelectBody> selectBodyList = ((SetOperationList) selectBody).getSelects();
// return JSqlParserUtils.parseBySelectBody(selectBodyList.get(0));
// }
// // 简单的select查询
// if (selectBody instanceof PlainSelect) {
// SelectSqlInfo sqlInfo = new SelectSqlInfo(selectBody);
// PlainSelect plainSelect = (PlainSelect) selectBody;
// FromItem fromItem = plainSelect.getFromItem();
// // 解析 aliasName
// if (fromItem.getAlias() != null) {
// sqlInfo.setFromTableAliasName(fromItem.getAlias().getName());
// }
// // 解析 表名
// if (fromItem instanceof Table) {
// // 通过表名的方式from
// Table fromTable = (Table) fromItem;
// sqlInfo.setFromTableName(fromTable.getName());
// } else if (fromItem instanceof SubSelect) {
// // 通过子查询的方式from
// SubSelect fromSubSelect = (SubSelect) fromItem;
// SelectSqlInfo subSqlInfo = JSqlParserUtils.parseBySelectBody(fromSubSelect.getSelectBody());
// sqlInfo.setFromSubSelect(subSqlInfo);
// }
// // 解析 selectFields
// List<SelectItem> selectItems = plainSelect.getSelectItems();
// for (SelectItem selectItem : selectItems) {
// if (selectItem instanceof AllColumns || selectItem instanceof AllTableColumns) {
// // 全部字段
// sqlInfo.setSelectAll(true);
// sqlInfo.setSelectFields(null);
// sqlInfo.setRealSelectFields(null);
// break;
// } else if (selectItem instanceof SelectExpressionItem) {
// // 获取单个查询字段名
// SelectExpressionItem selectExpressionItem = (SelectExpressionItem) selectItem;
// Expression expression = selectExpressionItem.getExpression();
// Alias alias = selectExpressionItem.getAlias();
// JSqlParserUtils.handleExpression(sqlInfo, expression, alias);
// }
// }
// return sqlInfo;
// } else {
// log.warn("暂时尚未处理该类型的 SelectBody: {}", selectBody.getClass().getName());
// throw new JeecgBootException("暂时尚未处理该类型的 SelectBody");
// }
// }
//
// /**
// * 处理查询字段表达式
// *
// * @param sqlInfo
// * @param expression
// * @param alias 是否有别名无传null
// */
// private static void handleExpression(SelectSqlInfo sqlInfo, Expression expression, Alias alias) {
// // 处理函数式字段 CONCAT(name,'(',age,')')
// if (expression instanceof Function) {
// JSqlParserUtils.handleFunctionExpression((Function) expression, sqlInfo);
// return;
// }
// // 处理字段上的子查询
// if (expression instanceof SubSelect) {
// SubSelect subSelect = (SubSelect) expression;
// SelectSqlInfo subSqlInfo = JSqlParserUtils.parseBySelectBody(subSelect.getSelectBody());
// // 注:字段上的子查询,必须只查询一个字段,否则会报错,所以可以放心合并
// sqlInfo.getSelectFields().addAll(subSqlInfo.getSelectFields());
// sqlInfo.getRealSelectFields().addAll(subSqlInfo.getAllRealSelectFields());
// return;
// }
// // 不处理字面量
// if (expression instanceof StringValue ||
// expression instanceof NullValue ||
// expression instanceof LongValue ||
// expression instanceof DoubleValue ||
// expression instanceof HexValue ||
// expression instanceof DateValue ||
// expression instanceof TimestampValue ||
// expression instanceof TimeValue
// ) {
// return;
// }
//
// // 查询字段名
// String selectField = expression.toString();
// // 实际查询字段名
// String realSelectField = selectField;
// // 判断是否有别名
// if (alias != null) {
// selectField = alias.getName();
// }
// // 获取真实字段名
// if (expression instanceof Column) {
// Column column = (Column) expression;
// realSelectField = column.getColumnName();
// }
// sqlInfo.addSelectField(selectField, realSelectField);
// }
//
// /**
// * 处理函数式字段
// *
// * @param functionExp
// * @param sqlInfo
// */
// private static void handleFunctionExpression(Function functionExp, SelectSqlInfo sqlInfo) {
// List<Expression> expressions = functionExp.getParameters().getExpressions();
// for (Expression expression : expressions) {
// JSqlParserUtils.handleExpression(sqlInfo, expression, null);
// }
// }
//
//}
package org.jeecg.common.util.sqlparse;
import lombok.extern.slf4j.Slf4j;
import net.sf.jsqlparser.JSQLParserException;
import net.sf.jsqlparser.expression.*;
import net.sf.jsqlparser.parser.CCJSqlParserManager;
import net.sf.jsqlparser.schema.Column;
import net.sf.jsqlparser.schema.Table;
import net.sf.jsqlparser.statement.Statement;
import net.sf.jsqlparser.statement.select.*;
import org.jeecg.common.exception.JeecgBootException;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.common.util.sqlparse.vo.SelectSqlInfo;
import java.io.StringReader;
import java.util.List;
import java.util.Map;
@Slf4j
public class JSqlParserUtils {
/**
* 解析 查询selectsql的信息
* 此方法会展开所有子查询到一个map里
* key只存真实的表名如果查询的没有真实的表名则会被忽略。
* value只存真实的字段名如果查询的没有真实的字段名则会被忽略。
* <p>
* 例如SELECT a.*,d.age,(SELECT count(1) FROM sys_depart) AS count FROM (SELECT username AS foo, realname FROM sys_user) a, demo d
* 解析后的结果为:{sys_user=[username, realname], demo=[age], sys_depart=[]}
*
* @param selectSql
* @return
*/
public static Map<String, SelectSqlInfo> parseAllSelectTable(String selectSql) throws JSQLParserException {
if (oConvertUtils.isEmpty(selectSql)) {
return null;
}
// log.info("解析查询Sql{}", selectSql);
JSqlParserAllTableManager allTableManager = new JSqlParserAllTableManager(selectSql);
return allTableManager.parse();
}
/**
* 解析 查询selectsql的信息子查询嵌套
*
* @param selectSql
* @return
*/
public static SelectSqlInfo parseSelectSqlInfo(String selectSql) throws JSQLParserException {
if (oConvertUtils.isEmpty(selectSql)) {
return null;
}
// log.info("解析查询Sql{}", selectSql);
// 使用 JSqlParer 解析sql
// 1、创建解析器
CCJSqlParserManager mgr = new CCJSqlParserManager();
// 2、使用解析器解析sql生成具有层次结构的java类
Statement stmt = mgr.parse(new StringReader(selectSql));
if (stmt instanceof Select) {
Select selectStatement = (Select) stmt;
// 3、解析select查询sql的信息
return JSqlParserUtils.parseBySelectBody(selectStatement.getSelectBody());
} else {
// 非 select 查询sql不做处理
throw new JeecgBootException("非 select 查询sql不做处理");
}
}
/**
* 解析 select 查询sql的信息
*
* @param selectBody
* @return
*/
private static SelectSqlInfo parseBySelectBody(SelectBody selectBody) {
// 判断是否使用了union等操作
if (selectBody instanceof SetOperationList) {
// 如果使用了union等操作则只解析第一个查询
List<SelectBody> selectBodyList = ((SetOperationList) selectBody).getSelects();
return JSqlParserUtils.parseBySelectBody(selectBodyList.get(0));
}
// 简单的select查询
if (selectBody instanceof PlainSelect) {
SelectSqlInfo sqlInfo = new SelectSqlInfo(selectBody);
PlainSelect plainSelect = (PlainSelect) selectBody;
FromItem fromItem = plainSelect.getFromItem();
// 解析 aliasName
if (fromItem.getAlias() != null) {
sqlInfo.setFromTableAliasName(fromItem.getAlias().getName());
}
// 解析 表名
if (fromItem instanceof Table) {
// 通过表名的方式from
Table fromTable = (Table) fromItem;
sqlInfo.setFromTableName(fromTable.getName());
} else if (fromItem instanceof SubSelect) {
// 通过子查询的方式from
SubSelect fromSubSelect = (SubSelect) fromItem;
SelectSqlInfo subSqlInfo = JSqlParserUtils.parseBySelectBody(fromSubSelect.getSelectBody());
sqlInfo.setFromSubSelect(subSqlInfo);
}
// 解析 selectFields
List<SelectItem> selectItems = plainSelect.getSelectItems();
for (SelectItem selectItem : selectItems) {
if (selectItem instanceof AllColumns || selectItem instanceof AllTableColumns) {
// 全部字段
sqlInfo.setSelectAll(true);
sqlInfo.setSelectFields(null);
sqlInfo.setRealSelectFields(null);
break;
} else if (selectItem instanceof SelectExpressionItem) {
// 获取单个查询字段名
SelectExpressionItem selectExpressionItem = (SelectExpressionItem) selectItem;
Expression expression = selectExpressionItem.getExpression();
Alias alias = selectExpressionItem.getAlias();
JSqlParserUtils.handleExpression(sqlInfo, expression, alias);
}
}
return sqlInfo;
} else {
log.warn("暂时尚未处理该类型的 SelectBody: {}", selectBody.getClass().getName());
throw new JeecgBootException("暂时尚未处理该类型的 SelectBody");
}
}
/**
* 处理查询字段表达式
*
* @param sqlInfo
* @param expression
* @param alias 是否有别名无传null
*/
private static void handleExpression(SelectSqlInfo sqlInfo, Expression expression, Alias alias) {
// 处理函数式字段 CONCAT(name,'(',age,')')
if (expression instanceof Function) {
JSqlParserUtils.handleFunctionExpression((Function) expression, sqlInfo);
return;
}
// 处理字段上的子查询
if (expression instanceof SubSelect) {
SubSelect subSelect = (SubSelect) expression;
SelectSqlInfo subSqlInfo = JSqlParserUtils.parseBySelectBody(subSelect.getSelectBody());
// 注:字段上的子查询,必须只查询一个字段,否则会报错,所以可以放心合并
sqlInfo.getSelectFields().addAll(subSqlInfo.getSelectFields());
sqlInfo.getRealSelectFields().addAll(subSqlInfo.getAllRealSelectFields());
return;
}
// 不处理字面量
if (expression instanceof StringValue ||
expression instanceof NullValue ||
expression instanceof LongValue ||
expression instanceof DoubleValue ||
expression instanceof HexValue ||
expression instanceof DateValue ||
expression instanceof TimestampValue ||
expression instanceof TimeValue
) {
return;
}
// 查询字段名
String selectField = expression.toString();
// 实际查询字段名
String realSelectField = selectField;
// 判断是否有别名
if (alias != null) {
selectField = alias.getName();
}
// 获取真实字段名
if (expression instanceof Column) {
Column column = (Column) expression;
realSelectField = column.getColumnName();
}
sqlInfo.addSelectField(selectField, realSelectField);
}
/**
* 处理函数式字段
*
* @param functionExp
* @param sqlInfo
*/
private static void handleFunctionExpression(Function functionExp, SelectSqlInfo sqlInfo) {
List<Expression> expressions = functionExp.getParameters().getExpressions();
for (Expression expression : expressions) {
JSqlParserUtils.handleExpression(sqlInfo, expression, null);
}
}
}

View File

@ -1,101 +1,101 @@
//package org.jeecg.common.util.sqlparse.vo;
//
//import lombok.Data;
//import net.sf.jsqlparser.statement.select.SelectBody;
//
//import java.util.HashSet;
//import java.util.Set;
//
///**
// * select 查询 sql 的信息
// */
//@Data
//public class SelectSqlInfo {
//
// /**
// * 查询的表名如果是子查询则此处为null
// */
// private String fromTableName;
// /**
// * 表别名
// */
// private String fromTableAliasName;
// /**
// * 通过子查询获取的表信息例如select name from (select * from user) u
// * 如果不是子查询则为null
// */
// private SelectSqlInfo fromSubSelect;
// /**
// * 查询的字段集合,如果是 * 则为null如果设了别名则为别名
// */
// private Set<String> selectFields;
// /**
// * 真实的查询字段集合,如果是 * 则为null如果设了别名则为原始字段名
// */
// private Set<String> realSelectFields;
// /**
// * 是否是查询所有字段
// */
// private boolean selectAll;
//
// /**
// * 解析之后的 SQL (关键字都是大写)
// */
// private final String parsedSql;
//
// public SelectSqlInfo(String parsedSql) {
// this.parsedSql = parsedSql;
// }
//
// public SelectSqlInfo(SelectBody selectBody) {
// this.parsedSql = selectBody.toString();
// }
//
// public void addSelectField(String selectField, String realSelectField) {
// if (this.selectFields == null) {
// this.selectFields = new HashSet<>();
// }
// if (this.realSelectFields == null) {
// this.realSelectFields = new HashSet<>();
// }
// this.selectFields.add(selectField);
// this.realSelectFields.add(realSelectField);
// }
//
// /**
// * 获取所有字段,包括子查询里的。
// *
// * @return
// */
// public Set<String> getAllRealSelectFields() {
// Set<String> fields = new HashSet<>();
// // 递归获取所有字段,起个直观的方法名为:
// this.recursiveGetAllFields(this, fields);
// return fields;
// }
//
// /**
// * 递归获取所有字段
// */
// private void recursiveGetAllFields(SelectSqlInfo sqlInfo, Set<String> fields) {
// if (!sqlInfo.isSelectAll() && sqlInfo.getRealSelectFields() != null) {
// fields.addAll(sqlInfo.getRealSelectFields());
// }
// if (sqlInfo.getFromSubSelect() != null) {
// recursiveGetAllFields(sqlInfo.getFromSubSelect(), fields);
// }
// }
//
// @Override
// public String toString() {
// return "SelectSqlInfo{" +
// "fromTableName='" + fromTableName + '\'' +
// ", fromSubSelect=" + fromSubSelect +
// ", aliasName='" + fromTableAliasName + '\'' +
// ", selectFields=" + selectFields +
// ", realSelectFields=" + realSelectFields +
// ", selectAll=" + selectAll +
// "}";
// }
//
//}
package org.jeecg.common.util.sqlparse.vo;
import lombok.Data;
import net.sf.jsqlparser.statement.select.SelectBody;
import java.util.HashSet;
import java.util.Set;
/**
* select 查询 sql 的信息
*/
@Data
public class SelectSqlInfo {
/**
* 查询的表名如果是子查询则此处为null
*/
private String fromTableName;
/**
* 表别名
*/
private String fromTableAliasName;
/**
* 通过子查询获取的表信息例如select name from (select * from user) u
* 如果不是子查询则为null
*/
private SelectSqlInfo fromSubSelect;
/**
* 查询的字段集合,如果是 * 则为null如果设了别名则为别名
*/
private Set<String> selectFields;
/**
* 真实的查询字段集合,如果是 * 则为null如果设了别名则为原始字段名
*/
private Set<String> realSelectFields;
/**
* 是否是查询所有字段
*/
private boolean selectAll;
/**
* 解析之后的 SQL (关键字都是大写)
*/
private final String parsedSql;
public SelectSqlInfo(String parsedSql) {
this.parsedSql = parsedSql;
}
public SelectSqlInfo(SelectBody selectBody) {
this.parsedSql = selectBody.toString();
}
public void addSelectField(String selectField, String realSelectField) {
if (this.selectFields == null) {
this.selectFields = new HashSet<>();
}
if (this.realSelectFields == null) {
this.realSelectFields = new HashSet<>();
}
this.selectFields.add(selectField);
this.realSelectFields.add(realSelectField);
}
/**
* 获取所有字段,包括子查询里的。
*
* @return
*/
public Set<String> getAllRealSelectFields() {
Set<String> fields = new HashSet<>();
// 递归获取所有字段,起个直观的方法名为:
this.recursiveGetAllFields(this, fields);
return fields;
}
/**
* 递归获取所有字段
*/
private void recursiveGetAllFields(SelectSqlInfo sqlInfo, Set<String> fields) {
if (!sqlInfo.isSelectAll() && sqlInfo.getRealSelectFields() != null) {
fields.addAll(sqlInfo.getRealSelectFields());
}
if (sqlInfo.getFromSubSelect() != null) {
recursiveGetAllFields(sqlInfo.getFromSubSelect(), fields);
}
}
@Override
public String toString() {
return "SelectSqlInfo{" +
"fromTableName='" + fromTableName + '\'' +
", fromSubSelect=" + fromSubSelect +
", aliasName='" + fromTableAliasName + '\'' +
", selectFields=" + selectFields +
", realSelectFields=" + realSelectFields +
", selectAll=" + selectAll +
"}";
}
}

View File

@ -3,14 +3,13 @@ package org.jeecg.config;
import org.jeecgframework.core.util.ApplicationContextUtil;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
/**
* @Author: Scott
* @Date: 2018/2/7
* @description: autopoi 配置类
*/
@Lazy(false)
@Configuration
public class AutoPoiConfig {

View File

@ -25,7 +25,6 @@ import lombok.extern.slf4j.Slf4j;
* @Version:1.0
*/
@Slf4j
@Lazy(false)
@Service
public class AutoPoiDictConfig implements AutoPoiDictServiceI {
final static String EXCEL_SPLIT_TAG = "_";
@ -60,15 +59,17 @@ public class AutoPoiDictConfig implements AutoPoiDictServiceI {
for (DictModel t : dictList) {
// 代码逻辑说明: [issues/4917]excel 导出异常---
//update-begin---author:liusq Date:20230517 for[issues/4917]excel 导出异常---
if(t!=null && t.getText()!=null && t.getValue()!=null){
// 代码逻辑说明: [issues/I4MBB3]@Excel dicText字段的值有下划线时导入功能不能正确解析---
//update-end---author:liusq Date:20230517 for[issues/4917]excel 导出异常---
//update-begin---author:scott Date:20211220 for[issues/I4MBB3]@Excel dicText字段的值有下划线时导入功能不能正确解析---
if(t.getValue().contains(EXCEL_SPLIT_TAG)){
String val = t.getValue().replace(EXCEL_SPLIT_TAG,TEMP_EXCEL_SPLIT_TAG);
dictReplaces.add(t.getText() + EXCEL_SPLIT_TAG + val);
}else{
dictReplaces.add(t.getText() + EXCEL_SPLIT_TAG + t.getValue());
}
//update-end---author:20211220 Date:20211220 for[issues/I4MBB3]@Excel dicText字段的值有下划线时导入功能不能正确解析---
}
}
if (dictReplaces != null && dictReplaces.size() != 0) {

View File

@ -1,12 +1,7 @@
package org.jeecg.config;
import lombok.Getter;
import lombok.Setter;
import org.jeecg.config.tencent.JeecgTencent;
import org.jeecg.config.vo.*;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Role;
import org.springframework.stereotype.Component;
@ -16,17 +11,12 @@ import org.springframework.stereotype.Component;
*/
@Component("jeecgBaseConfig")
@ConfigurationProperties(prefix = "jeecg")
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public class JeecgBaseConfig {
/**
* 签名密钥串(字典等敏感接口)
* @TODO 降低使用成本加的默认值,实际以 yml配置 为准
*/
private String signatureSecret = "dd05f1c54d63749eda95f9fa6d49v442a";
/**
* 自定义后台资源前缀解决表单设计器无法通过前端nginx转发访问
*/
private String customResourcePrefixPath;
/**
* 需要加强校验的接口清单
*/
@ -42,10 +32,6 @@ public class JeecgBaseConfig {
*/
private Firewall firewall;
/**
* shiro拦截排除
*/
private Shiro shiro;
/**
* 上传文件配置
*/
@ -78,42 +64,6 @@ public class JeecgBaseConfig {
*/
private BaiduApi baiduApi;
/**
* minio配置
*/
@Getter
@Setter
private JeecgMinio minio;
/**
* oss配置
*/
@Getter
@Setter
private JeecgOSS oss;
/**
* 短信发送方式 aliyun阿里云短信 tencent腾讯云短信
*/
@Getter
@Setter
private String smsSendType = "aliyun";
/**
* 腾讯配置
*/
@Getter
@Setter
private JeecgTencent tencent;
public String getCustomResourcePrefixPath() {
return customResourcePrefixPath;
}
public void setCustomResourcePrefixPath(String customResourcePrefixPath) {
this.customResourcePrefixPath = customResourcePrefixPath;
}
public Elasticsearch getElasticsearch() {
return elasticsearch;
}
@ -138,14 +88,6 @@ public class JeecgBaseConfig {
this.signatureSecret = signatureSecret;
}
public Shiro getShiro() {
return shiro;
}
public void setShiro(Shiro shiro) {
this.shiro = shiro;
}
public Path getPath() {
return path;
}

View File

@ -1,29 +0,0 @@
package org.jeecg.config;
import org.jeecg.config.vo.GaoDeApi;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
/**
* 高德账号配置
*/
@Lazy(false)
@Configuration("jeecgGaodeBaseConfig")
@ConfigurationProperties(prefix = "jeecg.jmreport")
public class JeecgGaodeBaseConfig {
/**
* 高德开放API配置
*/
private GaoDeApi gaoDeApi;
public GaoDeApi getGaoDeApi() {
return gaoDeApi;
}
public void setGaoDeApi(GaoDeApi gaoDeApi) {
this.gaoDeApi = gaoDeApi;
}
}

View File

@ -2,14 +2,12 @@ package org.jeecg.config;
import lombok.Data;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
/**
* 设置静态参数初始化
* @author: jeecg-boot
*/
@Lazy(false)
@Component
@Data
public class StaticConfig {

View File

@ -1,7 +1,7 @@
//package org.jeecg.config;
//
//
//import io.swagger.v3.oas.annotations.Operation;
//import io.swagger.annotations.ApiOperation;
//import org.jeecg.common.constant.CommonConstant;
//import org.jeecg.config.mybatis.MybatisPlusSaasConfig;
//import org.springframework.beans.BeansException;
@ -95,11 +95,13 @@
// List<Parameter> pars = new ArrayList<>();
// tokenPar.name(CommonConstant.X_ACCESS_TOKEN).description("token").modelRef(new ModelRef("string")).parameterType("header").required(false).build();
// pars.add(tokenPar.build());
// //update-begin-author:liusq---date:2024-08-15--for: 开启多租户时全局参数增加租户id
// if(MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL){
// ParameterBuilder tenantPar = new ParameterBuilder();
// tenantPar.name(CommonConstant.TENANT_ID).description("租户ID").modelRef(new ModelRef("string")).parameterType("header").required(false).build();
// pars.add(tenantPar.build());
// }
// //update-end-author:liusq---date:2024-08-15--for: 开启多租户时全局参数增加租户id
//
// return pars;
// }

View File

@ -3,68 +3,22 @@ package org.jeecg.config;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.Paths;
import io.swagger.v3.oas.models.info.Contact;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.info.License;
import io.swagger.v3.oas.models.security.SecurityRequirement;
import io.swagger.v3.oas.models.security.SecurityScheme;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.constant.CommonConstant;
import org.springdoc.core.customizers.OperationCustomizer;
import org.springdoc.core.filters.GlobalOpenApiMethodFilter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springdoc.core.models.GroupedOpenApi;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
/**
* @author eightmonth
*/
@Slf4j
@Configuration
@ConditionalOnProperty(prefix = "knife4j", name = "production", havingValue = "false", matchIfMissing = true)
@PropertySource("classpath:config/default-spring-doc.properties")
public class Swagger3Config implements WebMvcConfigurer {
// 路径匹配结果缓存,避免重复计算
private static final Map<String, Boolean> EXCLUDED_PATHS_CACHE = new ConcurrentHashMap<>();
// 定义不需要注入安全要求的路径集合
private static final Set<String> excludedPaths = new HashSet<>(Arrays.asList(
"/sys/randomImage/**",
"/sys/login",
"/sys/phoneLogin",
"/sys/mLogin",
"/sys/sms",
"/sys/cas/client/validateLogin",
"/test/jeecgDemo/demo3",
"/sys/thirdLogin/**",
"/sys/user/register"
));
// 预处理通配符模式,提高匹配效率
private static final Set<String> wildcardPatterns = new HashSet<>();
private static final Set<String> exactPatterns = new HashSet<>();
static {
// 初始化时分离精确匹配和通配符匹配
for (String pattern : excludedPaths) {
if (pattern.endsWith("/**")) {
wildcardPatterns.add(pattern.substring(0, pattern.length() - 3));
} else {
exactPatterns.add(pattern);
}
}
}
/**
/**
*
* 显示swagger-ui.html文档展示页还必须注入swagger资源
*
@ -78,75 +32,28 @@ public class Swagger3Config implements WebMvcConfigurer {
}
@Bean
public GlobalOpenApiMethodFilter globalOpenApiMethodFilter() {
return method -> method.isAnnotationPresent(Operation.class);
public GroupedOpenApi swaggerOpenApi() {
return GroupedOpenApi.builder()
.group("default")
.packagesToScan("org.jeecg")
// 剔除以下几个包路径的接口生成文档
.packagesToExclude("org.jeecg.modules.drag", "org.jeecg.modules.online", "org.jeecg.modules.jmreport")
// 加了Operation注解的方法才生成接口文档
.addOpenApiMethodFilter(method -> method.isAnnotationPresent(Operation.class))
.build();
}
@Bean
public OperationCustomizer operationCustomizer() {
return (operation, handlerMethod) -> {
String path = getFullPath(handlerMethod);
if (!isExcludedPath(path)) {
operation.addSecurityItem(new SecurityRequirement().addList(CommonConstant.X_ACCESS_TOKEN));
}else{
log.info("忽略加入 X_ACCESS_TOKEN 的 PATH:" + path);
}
return operation;
};
}
private String getFullPath(HandlerMethod handlerMethod) {
StringBuilder fullPath = new StringBuilder();
// 获取类级别的路径
RequestMapping classMapping = handlerMethod.getBeanType().getAnnotation(RequestMapping.class);
if (classMapping != null && classMapping.value().length > 0) {
fullPath.append(classMapping.value()[0]);
}
// 获取方法级别的路径
RequestMapping methodMapping = handlerMethod.getMethodAnnotation(RequestMapping.class);
if (methodMapping != null && methodMapping.value().length > 0) {
String methodPath = methodMapping.value()[0];
// 确保路径正确拼接,处理斜杠
if (!fullPath.toString().endsWith("/") && !methodPath.startsWith("/")) {
fullPath.append("/");
}
fullPath.append(methodPath);
}
return fullPath.toString();
}
private boolean isExcludedPath(String path) {
// 使用缓存避免重复计算
return EXCLUDED_PATHS_CACHE.computeIfAbsent(path, p -> {
// 精确匹配
if (exactPatterns.contains(p)) {
return true;
}
// 通配符匹配
return wildcardPatterns.stream().anyMatch(p::startsWith);
});
}
@Bean
public OpenAPI customOpenAPI() {
return new OpenAPI()
.info(new Info()
.title("JeecgBoot 后台服务API接口文档")
.version("3.9.0")
.version("1.0")
.contact(new Contact().name("北京国炬信息技术有限公司").url("www.jeccg.com").email("jeecgos@163.com"))
.description("后台API接口")
.description( "后台API接口")
.termsOfService("NO terms of service")
.license(new License().name("Apache 2.0").url("http://www.apache.org/licenses/LICENSE-2.0.html")))
.addSecurityItem(new SecurityRequirement().addList(CommonConstant.X_ACCESS_TOKEN))
.components(new Components().addSecuritySchemes(CommonConstant.X_ACCESS_TOKEN,
new SecurityScheme()
.name(CommonConstant.X_ACCESS_TOKEN)
.type(SecurityScheme.Type.APIKEY)
.in(SecurityScheme.In.HEADER) // 关键:指定为 header
));
.license(new License().name("Apache 2.0").url("http://www.apache.org/licenses/LICENSE-2.0.html"))
);
}
}
}

View File

@ -0,0 +1,19 @@
package org.jeecg.config;
import io.undertow.server.DefaultByteBufferPool;
import io.undertow.websockets.jsr.WebSocketDeploymentInfo;
import org.springframework.boot.web.embedded.undertow.UndertowServletWebServerFactory;
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.stereotype.Component;
@Component
public class UndertowCustomizer implements WebServerFactoryCustomizer<UndertowServletWebServerFactory> {
@Override
public void customize(UndertowServletWebServerFactory factory) {
factory.addDeploymentInfoCustomizers(deploymentInfo -> {
WebSocketDeploymentInfo webSocketDeploymentInfo = new WebSocketDeploymentInfo();
webSocketDeploymentInfo.setBuffers(new DefaultByteBufferPool(false, 1024));
deploymentInfo.addServletContextAttribute("io.undertow.websockets.jsr.WebSocketDeploymentInfo", webSocketDeploymentInfo);
});
}
}

View File

@ -10,20 +10,21 @@ import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;
import io.micrometer.prometheusmetrics.PrometheusMeterRegistry;
import jakarta.annotation.PostConstruct;
import io.micrometer.prometheus.PrometheusMeterRegistry;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.boot.actuate.web.exchanges.InMemoryHttpExchangeRepository;
import org.springframework.boot.autoconfigure.jackson.JacksonProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.http.CacheControl;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
@ -39,7 +40,6 @@ import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* Spring Boot 2.0 解决跨域问题
@ -47,7 +47,6 @@ import java.util.concurrent.TimeUnit;
* @Author qinfeng
*
*/
@Slf4j
@Configuration
public class WebMvcConfiguration implements WebMvcConfigurer {
@ -59,14 +58,6 @@ public class WebMvcConfiguration implements WebMvcConfigurer {
@Autowired(required = false)
private PrometheusMeterRegistry prometheusMeterRegistry;
/**
* meterRegistryPostProcessor
* for [QQYUN-12558]【监控】系统监控的头两个tab不好使接口404
*/
@Autowired(required = false)
@Qualifier("meterRegistryPostProcessor")
private BeanPostProcessor meterRegistryPostProcessor;
/**
* 静态资源的配置 - 使得可以从磁盘中读取 Html、图片、视频、音频等
*/
@ -79,8 +70,6 @@ public class WebMvcConfiguration implements WebMvcConfigurer {
.addResourceLocations("file:" + jeecgBaseConfig.getPath().getWebapp() + "//");
}
resourceHandlerRegistration.addResourceLocations(staticLocations.split(","));
// 设置缓存控制标头 Cache-Control有效期为30天
resourceHandlerRegistration.setCacheControl(CacheControl.maxAge(30, TimeUnit.DAYS));
}
/**
@ -89,7 +78,7 @@ public class WebMvcConfiguration implements WebMvcConfigurer {
*/
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/").setViewName("redirect:/doc.html");
registry.addViewController("/").setViewName("doc.html");
}
@Bean
@ -142,6 +131,7 @@ public class WebMvcConfiguration implements WebMvcConfigurer {
return objectMapper;
}
//update-begin---author:chenrui ---date:20240514 for[QQYUN-9247]系统监控功能优化------------
// /**
// * SpringBootAdmin的Httptrace不见了
// * https://blog.csdn.net/u013810234/article/details/110097201
@ -150,21 +140,15 @@ public class WebMvcConfiguration implements WebMvcConfigurer {
// public InMemoryHttpTraceRepository getInMemoryHttpTrace(){
// return new InMemoryHttpTraceRepository();
// }
//update-end---author:chenrui ---date:20240514 for[QQYUN-9247]系统监控功能优化------------
/**
* 在Bean初始化完成后立即配置PrometheusMeterRegistry避免在Meter注册后才配置MeterFilter
* for [QQYUN-12558]【监控】系统监控的头两个tab不好使接口404
* @author chenrui
* @date 2025/5/26 16:46
* 解决metrics端点不显示jvm信息的问题(zyf)
*/
@PostConstruct
public void initPrometheusMeterRegistry() {
// 确保在应用启动早期就配置MeterFilter避免警告
if (null != meterRegistryPostProcessor && null != prometheusMeterRegistry) {
meterRegistryPostProcessor.postProcessAfterInitialization(prometheusMeterRegistry, "prometheusMeterRegistry");
log.info("PrometheusMeterRegistry 配置完成");
}
@Bean
InitializingBean forcePrometheusPostProcessor(BeanPostProcessor meterRegistryPostProcessor) {
return () -> meterRegistryPostProcessor.postProcessAfterInitialization(prometheusMeterRegistry, "");
}
// /**

View File

@ -8,14 +8,12 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class LowCodeModeConfiguration implements WebMvcConfigurer {
private final LowCodeModeInterceptor lowCodeModeInterceptor;
public LowCodeModeConfiguration(LowCodeModeInterceptor lowCodeModeInterceptor) {
this.lowCodeModeInterceptor = lowCodeModeInterceptor;
public LowCodeModeInterceptor payInterceptor() {
return new LowCodeModeInterceptor();
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(lowCodeModeInterceptor).addPathPatterns(LowCodeUrlsEnum.getLowCodeInterceptUrls());
registry.addInterceptor(payInterceptor()).addPathPatterns(LowCodeUrlsEnum.getLowCodeInterceptUrls());
}
}

View File

@ -2,7 +2,6 @@ package org.jeecg.config.firewall.interceptor;
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.SecurityUtils;
import org.jeecg.common.api.CommonAPI;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.constant.CommonConstant;
@ -11,7 +10,8 @@ import org.jeecg.common.system.vo.LoginUser;
import org.jeecg.common.util.CommonUtils;
import org.jeecg.common.util.SpringContextUtils;
import org.jeecg.config.JeecgBaseConfig;
import org.springframework.stereotype.Component;
import org.jeecg.config.security.utils.SecureUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.servlet.HandlerInterceptor;
import jakarta.annotation.Resource;
@ -38,7 +38,6 @@ import java.util.Set;
* @date 20230904
*/
@Slf4j
@Component
public class LowCodeModeInterceptor implements HandlerInterceptor {
/**
* 低代码开发模式
@ -48,33 +47,26 @@ public class LowCodeModeInterceptor implements HandlerInterceptor {
@Resource
private JeecgBaseConfig jeecgBaseConfig;
@Autowired
private CommonAPI commonAPI;
/**
* 在请求处理之前进行调用
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
CommonAPI commonAPI = null;
log.info("低代码模式,拦截请求路径:" + request.getRequestURI());
//1、验证是否开启低代码开发模式控制
if (jeecgBaseConfig == null) {
jeecgBaseConfig = SpringContextUtils.getBean(JeecgBaseConfig.class);
}
if (commonAPI == null) {
commonAPI = SpringContextUtils.getBean(CommonAPI.class);
}
if (jeecgBaseConfig.getFirewall()!=null && LowCodeModeInterceptor.LOW_CODE_MODE_PROD.equals(jeecgBaseConfig.getFirewall().getLowCodeMode())) {
String requestURI = request.getRequestURI().substring(request.getContextPath().length());
log.info("低代码模式,拦截请求路径:" + requestURI);
LoginUser loginUser = (LoginUser) SecurityUtils.getSubject().getPrincipal();
LoginUser loginUser = SecureUtil.currentUser();
Set<String> hasRoles = null;
if (loginUser == null) {
loginUser = commonAPI.getUserByName(JwtUtil.getUserNameByToken(SpringContextUtils.getHttpServletRequest()));
}
if (loginUser != null) {
//当前登录人拥有的角色
hasRoles = commonAPI.queryUserRolesById(loginUser.getId());
}

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