Compare commits

..

59 Commits

Author SHA1 Message Date
74d80459e1 Revise README for version 3.8.3 and upgrade notice
Updated version information and maintenance notice.
2025-11-28 11:16:50 +08:00
bdc1e7988b Online报表(带参数)预览后台报错 #9000 2025-11-11 15:09:26 +08:00
5730ed2dd2 固定@vitejs/plugin-vue、rollup、rollup-plugin-visualizer版本号,导致打包报错 2025-11-10 16:00:41 +08:00
5743d71f3c JEECG低代码+AI编程Cursor+GitHub Copilot实现高效编程 2025-11-04 11:58:17 +08:00
db16cb5fda 3.8.3-master分支:租户用户 菜单下 新增用户报错 #9039 2025-10-28 13:40:02 +08:00
07c012adc7 [issues/8906] 0.35中智谱等模型不支持工具调用 #54 2025-10-14 22:46:28 +08:00
8edb547113 issue格式 2025-10-14 18:01:35 +08:00
da9e8570cd ,调整Node.js版本要求说明 2025-10-10 18:08:56 +08:00
5e37b4de8f 解决jdk8兼容问题 2025-10-10 18:05:14 +08:00
cab42b819c 日志注解@AutoLog 多文件上传时报错 #8945 2025-10-10 17:06:32 +08:00
d3b8948f40 Path Traversal Vulnerability /sys/comment/addFile /sys/upload/uploadMinio endpoint (notice the uploadlocal function is different from the /sys/common/upload ) #8827 2025-09-29 18:29:11 +08:00
766ec0df52 BI大屏分享地址,提示需要token错误处理 2025-09-28 18:17:13 +08:00
b72c343090 提供其他数据库脚本 2025-09-28 14:30:34 +08:00
cdd7c5c5a1 添加jeecg-boot网络到sentinel和xxljob服务 2025-09-25 13:48:00 +08:00
120cf85eb4 微服务docker启动增加sentinel和xxljob 2025-09-25 12:55:25 +08:00
a3969a5c63 Oracle11g数据库 多租户管理>>添加租户 报错 #8897 2025-09-25 12:54:54 +08:00
1222c76ff8 更新xxl-job配置,修改adminAddresses和地址为jeecg-boot-xxljob 2025-09-25 11:19:31 +08:00
6048866313 升级nacos配置 2025-09-25 10:26:03 +08:00
83edfa9090 版本发布日期更新 2025-09-24 13:18:16 +08:00
5d5d9fc53d Merge branch 'master' of https://github.com/zhangdaiscott/jeecg-boot 2025-09-24 11:16:50 +08:00
002bfe25f8 【v3.8.3】升级数据库,暂时先删除 2025-09-24 11:06:54 +08:00
7cb2dc4fde Update @vitejs/plugin-vue-jsx version
Updated version of @vitejs/plugin-vue-jsx to 4.1.1.
自动下载的依赖 @vitejs/plugin-vue-jsx@4.2.0 是 CJS 格式 , 1.0.0-beta.38 版本的 @rolldown/pluginutils 是 ESM 格式模块不兼容
2025-09-18 17:32:11 +08:00
ab151879b3 XXL-JOB版本号错误,修改至v2.4.0 2025-09-14 16:03:45 +08:00
000ae1db30 升级前端pnpm lock 2025-09-14 15:52:27 +08:00
63e066180d 【v3.8.3】更新pom.xml,升级Kotlin和Liteflow版本 2025-09-14 12:57:48 +08:00
44c1079f87 【v3.8.3】优化顶部导航风格菜单的样式,支持外部链接打开及菜单重定向 2025-09-14 11:59:09 +08:00
e825e0f912 【v3.8.3】升级数据库 2025-09-14 11:58:05 +08:00
132e89b0e1 【v3.8.3】底层core的一些功能修改 2025-09-14 11:57:47 +08:00
881a637285 【v3.8.3】AI助手调用系统业务扩展接口,支持添加用户、查询用户、查询角色及授予角色功能 2025-09-14 11:57:37 +08:00
02e9f8984f 【v3.8.3】更新EnumDict.java,添加使用说明和配置要求 2025-09-14 11:55:45 +08:00
a4343fc2cb 【v3.8.3】底层core的一些功能修改 2025-09-14 11:55:31 +08:00
152e8c7aaa 【v3.8.3】优化枚举字典数据加载,支持多包路径扫描,提升初始化性能 2025-09-14 11:54:01 +08:00
d7dc81455d 【v3.8.3】用户组织机构大改造(新增主岗位、主岗位、用户签名) 2025-09-14 11:53:36 +08:00
aefdcd6315 【v3.8.3】大数据导出共通类 2025-09-14 11:50:56 +08:00
1cf4054e76 【v3.8.3】租户大改造 2025-09-14 11:50:37 +08:00
7829cf18d7 解决升级mybatisPlus后SqlServer分页使用OFFSET 2025-09-14 11:49:55 +08:00
69c3a9da9a 【v3.8.3】升级版本号至3.8.3,更新依赖项和排除项 2025-09-14 11:49:42 +08:00
4d34150479 升级kingbase8驱动和切换回tomcat 2025-09-14 11:48:00 +08:00
29687c8908 【v3.8.3】升级版本号3.8.3和docker配置 2025-09-14 10:48:13 +08:00
2e93a92dde 【v3.8.3】修改配置文件,删除undertow 2025-09-14 10:45:32 +08:00
d383f7458d 【v3.8.3】企业微信通知采用卡片 2025-09-14 10:44:08 +08:00
700318e1c1 【v3.8.3】首页配置功能改造 2025-09-14 10:43:21 +08:00
d728d6b090 【v3.8.3】升级代码生成器模板 2025-09-14 10:43:00 +08:00
7abc2e4c9c 【v3.8.3】大数据导出示例 2025-09-14 10:42:45 +08:00
da2b0cc354 【v3.8.3】升级aiflow 2025-09-14 10:41:53 +08:00
a6751c22be 【v3.8.3】功能性能优化 2025-09-14 10:41:25 +08:00
f087525a75 【v3.8.3】undertow不稳定切换回tomcat 2025-09-14 10:40:21 +08:00
4f46213df6 【v3.8.3】前端小改动汇总集合 2025-09-14 10:39:48 +08:00
d76842ae07 【v3.8.3】airag优化体验升级 2025-09-14 10:38:41 +08:00
8c64db46e5 【v3.8.3】默认首页改造 2025-09-14 10:37:29 +08:00
81fb2ac3b2 【v3.8.3】切换回Tomcat信息 2025-09-14 10:37:14 +08:00
fa98817aeb 【v3.8.3】部门大改造关联修改 2025-09-14 10:37:05 +08:00
1158977826 更新版本号至3.8.3,修改README.md以反映最新框架和工具版本 2025-09-14 10:27:07 +08:00
8b6def0ee3 导出excel总提示格式不匹配和日志大数据导出示例 2025-09-14 10:26:52 +08:00
39c0d5b3f5 【v3.8.3】组织机构部门大改造(支持子公司、岗位与不能功能划分更清晰,岗位可以设置上下级,岗位可以设置职级,支持回报关系) 2025-09-14 10:26:24 +08:00
adeebee840 【v3.8.3】用户大改造,取消原职位换成职位字典、原职位改成岗位职级、新增主岗位和兼职岗位 2025-09-14 10:24:02 +08:00
862aaa8632 【v3.8.3】我的租户大改造 2025-09-14 10:22:26 +08:00
6a11ff8a64 【v3.8.3】升级版本号 2025-09-14 10:16:56 +08:00
73059b8a53 优化AIRG 2025-09-13 16:15:23 +08:00
469 changed files with 47085 additions and 16173 deletions

View File

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

View File

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

View File

@ -7,12 +7,12 @@
JEECG BOOT AI Low Code Platform JEECG BOOT AI Low Code Platform
=============== ===============
Current version: 3.8.2 (Release date: 2025-08-04) Current version: 3.8.3 (Release date: 2025-10-09)
[![AUR](https://img.shields.io/badge/license-Apache%20License%202.0-blue.svg)](https://github.com/zhangdaiscott/jeecg-boot/blob/master/LICENSE) [![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/Author-guojusoft-orange.svg)](http://www.jeecg.com)
[![](https://img.shields.io/badge/version-3.8.2-brightgreen.svg)](https://github.com/zhangdaiscott/jeecg-boot) [![](https://img.shields.io/badge/version-3.8.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 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) [![GitHub forks](https://img.shields.io/github/forks/zhangdaiscott/jeecg-boot.svg?style=social&label=Fork)](https://github.com/zhangdaiscott/jeecg-boot)

View File

@ -2,13 +2,14 @@
JeecgBoot AI低代码平台 JeecgBoot AI低代码平台
=============== ===============
当前最新版本: 3.8.2发布日期2025-08-04 当前最新版本: 3.8.3发布日期2025-10-09
> 重要提醒: v3.8.3 是 SpringBoot2 的最终版本,后续将不再维护。建议大家尽快升级至基于 SpringBoot3版本以获得更好的性能和支持。
[![AUR](https://img.shields.io/badge/license-Apache%20License%202.0-blue.svg)](https://github.com/jeecgboot/JeecgBoot/blob/master/LICENSE) [![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/Author-北京国炬软件-orange.svg)](https://jeecg.com)
[![](https://img.shields.io/badge/blog-技术博客-orange.svg)](https://jeecg.blog.csdn.net) [![](https://img.shields.io/badge/blog-技术博客-orange.svg)](https://jeecg.blog.csdn.net)
[![](https://img.shields.io/badge/version-3.8.2-brightgreen.svg)](https://github.com/jeecgboot/JeecgBoot) [![](https://img.shields.io/badge/version-3.8.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 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) [![GitHub forks](https://img.shields.io/github/forks/zhangdaiscott/jeecg-boot.svg?style=social&label=Fork)](https://github.com/jeecgboot/JeecgBoot)
@ -19,18 +20,16 @@ JeecgBoot AI低代码平台
<h3 align="center">企业级AI低代码平台</h3> <h3 align="center">企业级AI低代码平台</h3>
JeecgBoot 是一款基于BPM流程和代码生成的AI低代码平台助力企业快速实现低代码开发和构建AI应用。 JeecgBoot是一款企业级低代码平台集成了AI应用平台功能旨在帮助开发者快速实现低代码开发和构建、部署个性化的 AI 应用。
采用前后端分离架构Ant Design&Vue3SpringBoot3SpringCloud AlibabaMybatis-plus,强大代码生成器实现前后端一键生成,无需手写代码。 前后端分离架构Ant Design4、Vue3SpringBoot2/3SpringCloud AlibabaMybatis-plusShiro/SpringAuthorizationServer,强大代码生成器前后端代码一键生成,无需写任何代码;提供强大的报表和大屏工具,满足企业级数据产品需求!
平台引领AI低代码开发模式AI生成→在线编码→代码生成→手工合并,解决Java项目80%重复工作,提升效率节省成本,兼顾灵活性。 引领AI低代码开发模式: AI生成->OnlineCoding-> 代码生成-> 手工MERGE 帮助Java项目解决80%重复工作,让开发更多关注业务,提高效率节省成本,同时又不失灵活性低代码能力Online表单、表单设计、流程设计、Online报表、大屏/仪表盘设计、报表设计; AI应用平台功能AI知识库问答、AI模型管理、AI流程编排、AI聊天等支持含ChatGPT、DeepSeek、Ollama等多种AI大模型
具备强大且颗粒化的权限控制支持按钮权限和数据权限设置满足大型业务系统需求。功能涵盖在线表单、表单设计、流程设计、门户设计、报表与大屏设计、OA办公、AI应用、AI知识库、大模型管理、AI流程编排、AI聊天支持ChatGPT、DeepSeek、Ollama等多种AI大模型。
`AI赋能报表:` 积木报表是一款自主研发的强大开源企业级Web报表与大屏工具。它通过零编码的拖拽式操作赋能用户如同搭积木般轻松构建各类复杂报表和数据大屏全面满足企业数据可视化与分析需求助力企业级数据产品的高效打造与应用。
`AI赋能低代码:` 提供完善成熟AI应用平台,涵盖AI应用管理、AI模型管理、智能对话助手、知识库问答、流程编排设计器AI建表等多项功能。平台兼容多种主流大模型,包括ChatGPT、DeepSeek、Ollama、智普、千问等助力企业高效构建智能化应用推动低代码开发与AI深度融合。 `AI赋能低代码:` 提供一套成熟AI应用平台功能:包含AI应用管理、AI模型管理、AI对话助手、AI知识库问答、AI流程编排、AI流程设计器AI建表等功能; 支持各种AI大模型ChatGPT、DeepSeek、Ollama、智普、千问等.
`JEECG宗旨是:` JEECG旨在通过OnlineCoding平台实现简单功能的零代码快速搭建,同时针对复杂功能采用代码生成器生成代码并手工合并,打造智能且灵活的低代码开发模式,有效解决了当前低代码产品普遍缺乏灵活性的问题,提升开发效率的同时兼顾系统的扩展性和定制化能力。 `JEECG宗旨是:` 简单功能由OnlineCoding零代码搭建做到`零代码开发`复杂功能代码生成器生成进行手工Merge 实现`低代码开发`,既保证了`智能`又兼顾`灵活`,解决了当前低代码产品普遍不灵活的弊端!
`JEECG业务流程:` JEECG业务流程采用BPM工作流引擎实现业务审批扩展任务接口供开发人员编写业务逻辑,表单提供表单设计器、在线配置表单编码表单等多种解决方案。通过流程与表单的分离设计(松耦合)任务节点灵活配置,既保障了企业流程的安全性与保密性,又大幅降低了开发人员的工作量。 `JEECG业务流程:` 采用工作流来实现、扩展任务接口供开发编写业务逻辑,表单提供多种解决方案: 表单设计器、online配置表单编码表单。同时实现了流程与表单的分离设计(松耦合)、并支持任务节点灵活配置,既保证了公司流程的保密性,又减少了开发人员的工作量。
@ -38,8 +37,8 @@ JeecgBoot 是一款基于BPM流程和代码生成的AI低代码平台助力
适用项目 适用项目
----------------------------------- -----------------------------------
JeecgBoot低代码平台兼容所有J2EE项目开发支持信创国产化,特别适用于SAAS、企业信息管理系统MIS、内部办公系统OA、企业资源计划系统ERP、客户关系管理系统CRMAI知识库等场景。其半智能手工Merge开发式,可显著提升70%以上的开发效率极大降低开发成本。同时JeecgBoot还是一款全栈式AI开发平台助力企业快速构建和部署个性化AI应用。 JeecgBoot低代码平台,可以应用在任何J2EE项目开发,支持信创国产化。尤其适合SAAS项目、企业信息管理系统MIS、内部办公系统OA、企业资源计划系统ERP、客户关系管理系统CRMAI知识库等其半智能手工Merge开发式,可显著提高开发效率70%以上,极大降低开发成本
又是一个全栈式 AI 开发平台,快速帮助企业构建和部署个性化的 AI 应用。
**信创兼容说明** **信创兼容说明**
- 操作系统:国产麒麟、银河麒麟等国产系统几乎都是基于 Linux 内核,因此它们具有良好的兼容性。 - 操作系统:国产麒麟、银河麒麟等国产系统几乎都是基于 Linux 内核,因此它们具有良好的兼容性。
@ -50,13 +49,13 @@ JeecgBoot低代码平台兼容所有J2EE项目开发支持信创国产化
版本说明 版本说明
----------------------------------- -----------------------------------
|下载 | JDK17 + SpringBoot3.3 + Shiro |JDK17 + SpringBoot3.3+ SpringAuthorizationServer | JDK17/JDK8 + SpringBoot2.7 | |下载 | JDK17 + SpringBoot3.3 + Shiro |JDK17 + SpringBoot3.3+ SpringAuthorizationServer | JDK17/JDK8 + SpringBoot2.7 |
|------|----------------------------------------------------|--------------------------------------------|--------------------------------------------| |------|----------------------------------------------------|-----------------------------------------------------------------------------|--------------------------------------------|
| Github | [`springboot3`](https://github.com/jeecgboot/JeecgBoot/tree/springboot3) | [`springboot3_sas`](https://github.com/jeecgboot/JeecgBoot/tree/springboot3_sas) 分支 |[`master`](https://github.com/jeecgboot/JeecgBoot) 分支| | Github | [`springboot3`](https://github.com/jeecgboot/JeecgBoot/tree/springboot3) | [`springboot3_sas`](https://github.com/jeecgboot/JeecgBoot/tree/springboot3_sas) 分支 |[`master`](https://github.com/jeecgboot/JeecgBoot) 分支|
| Gitee | [`springboot3`](https://gitee.com/jeecg/JeecgBoot/tree/springboot3/) | [`springboot3_sas`](https://gitee.com/jeecg/JeecgBoot/tree/springboot3_sas) 分支 |[`master`](https://gitee.com/jeecg/JeecgBoot) 分支 | | Gitee | [`springboot3`](https://gitee.com/jeecg/JeecgBoot/tree/springboot3/) | [`springboot3_sas`](https://gitee.com/jeecg/JeecgBoot/tree/springboot3_sas) 分支 |[`master`](https://gitee.com/jeecg/JeecgBoot) 分支 |
- `jeecg-boot` 是后端JAVA源码项目Springboot3+SpringCloudAlibaba(支持单体和微服务切换). - `jeecg-boot` 是后端JAVA源码项目支持单体和微服务切换.
- `jeecgboot-vue3` 是前端VUE3源码项目vue3+vite6+ts最新技术栈. - `jeecgboot-vue3` 是前端VUE3源码项目vue3+vite6+ts最新技术栈.
- `JeecgUniapp` 是[配套APP框架](https://github.com/jeecgboot/JeecgUniapp) 适配多个终端支持APP、小程序、H5、鸿蒙、鸿蒙Next. - `JeecgUniapp` 是[配套APP框架](https://github.com/jeecgboot/JeecgUniapp) 适配多个终端支持APP、小程序、H5、鸿蒙、鸿蒙Next.
- 参考 [文档](https://help.jeecg.com/ui/2dev/mini) 可以删除不需要的demo制作一个精简版本 - 参考 [文档](https://help.jeecg.com/ui/2dev/mini) 可以删除不需要的demo制作一个精简版本
@ -83,6 +82,7 @@ JeecgBoot低代码平台兼容所有J2EE项目开发支持信创国产化
- 官方网站: [http://www.jeecg.com](http://www.jeecg.com) - 官方网站: [http://www.jeecg.com](http://www.jeecg.com)
- 在线演示: [平台演示](https://boot3.jeecg.com) | [APP演示](https://jeecg.com/appIndex) - 在线演示: [平台演示](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) - 入门指南: [快速入门](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低代码+AI编程Cursor+GitHub Copilot实现高效编程](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) - 技术支持: [反馈问题](https://github.com/jeecgboot/JeecgBoot/issues/new?template=bug_report.md) | [低代码体验一分钟](https://jeecg.blog.csdn.net/article/details/106079007)
- QQ交流群 964611995、⑩716488839(满)、⑨808791225(满)、其他(满) - QQ交流群 964611995、⑩716488839(满)、⑨808791225(满)、其他(满)
@ -104,7 +104,7 @@ JeecgBoot平台提供了一套完善的AI应用管理系统模块是一套类
为什么选择JeecgBoot? 为什么选择JeecgBoot?
----------------------------------- -----------------------------------
- 1.采用最新主流前后分离框架Spring Boot3 + MyBatis + Ant Design4 + Vue3容易上手代码生成器依赖性低灵活的扩展能力可快速实现二次开发。 - 1.采用最新主流前后分离框架Spring Boot3 + MyBatis + Shiro//SpringAuthorizationServer + Ant Design4 + Vue3容易上手代码生成器依赖性低灵活的扩展能力可快速实现二次开发。
- 2.前端大版本换代,最新版采用 Vue3.0 + TypeScript + Vite6 + Ant Design Vue4 等新技术方案。 - 2.前端大版本换代,最新版采用 Vue3.0 + TypeScript + Vite6 + Ant Design Vue4 等新技术方案。
- 3.支持微服务Spring Cloud AlibabaNacos、Gateway、Sentinel、Skywalking提供简易机制支持单体和微服务自由切换这样可以满足各类项目需求 - 3.支持微服务Spring Cloud AlibabaNacos、Gateway、Sentinel、Skywalking提供简易机制支持单体和微服务自由切换这样可以满足各类项目需求
- 4.开发效率高支持在线建表和AI建表提供强大代码生成器单表、树列表、一对多、一对一等数据模型增删改查功能一键生成菜单配置直接使用。 - 4.开发效率高支持在线建表和AI建表提供强大代码生成器单表、树列表、一对多、一对一等数据模型增删改查功能一键生成菜单配置直接使用。
@ -157,6 +157,9 @@ JeecgBoot平台提供了一套完善的AI应用管理系统模块是一套类
#### 前端 #### 前端
- 前端环境要求Node.js要求`Node 20+` 版本以上、pnpm 要求`9+` 版本以上 - 前端环境要求Node.js要求`Node 20+` 版本以上、pnpm 要求`9+` 版本以上
> Vite 不再支持已结束生命周期EOL的 Node.js 18。现在需要使用 Node.js 20.19+ 或 22.12+。
- 依赖管理node、npm、pnpm - 依赖管理node、npm、pnpm
- 前端IDE建议IDEA、WebStorm、Vscode - 前端IDE建议IDEA、WebStorm、Vscode
- 采用 Vue3.0+TypeScript+Vite6+Ant-Design-Vue4等新技术方案包括二次封装组件、utils、hooks、动态菜单、权限校验、按钮级别权限控制等功能 - 采用 Vue3.0+TypeScript+Vite6+Ant-Design-Vue4等新技术方案包括二次封装组件、utils、hooks、动态菜单、权限校验、按钮级别权限控制等功能
@ -166,16 +169,16 @@ JeecgBoot平台提供了一套完善的AI应用管理系统模块是一套类
#### 后端 #### 后端
- IDE建议 IDEA (必须安装lombok插件 ) - IDE建议 IDEA (必须安装lombok插件 )
- 语言Java 默认jdk17(jdk21) - 语言Java 默认jdk17(支持jdk8、jdk21)
- 依赖管理Maven - 依赖管理Maven
- 基础框架Spring Boot 3.5.5 - 基础框架Spring Boot 3.5.5/2.7.18
- 微服务框架: Spring Cloud Alibaba 2023.0.3.3 - 微服务框架: Spring Cloud Alibaba 2021.0.6.2
- 持久层框架MybatisPlus 3.5.12 - 持久层框架MybatisPlus 3.5.3.2
- 报表工具: JimuReport 2.1.3 - 报表工具: JimuReport 2.1.3
- 安全框架Apache Shiro 2.0.4Jwt 4.5.0 - 安全框架Apache Shiro/SpringAuthorizationServerJwt 4.5.0
- 微服务技术栈Spring Cloud Alibaba、Nacos、Gateway、Sentinel、Skywalking - 微服务技术栈Spring Cloud Alibaba、Nacos、Gateway、Sentinel、Skywalking
- 数据库连接池阿里巴巴Druid 1.2.24 - 数据库连接池阿里巴巴Druid 1.1.24
- AI大模型支持 `ChatGPT` `DeepSeek` `千问`等各种常规模式 - AI大模型支持 `ChatGPT` `DeepSeek`切换
- 日志打印logback - 日志打印logback
- 缓存Redis - 缓存Redis
- 其他autopoi, fastjsonpoiSwagger-uiquartz, lombok简化代码等。 - 其他autopoi, fastjsonpoiSwagger-uiquartz, lombok简化代码等。

View File

@ -109,27 +109,32 @@ services:
# environment: # environment:
# RABBITMQ_DEFAULT_USER: guest # RABBITMQ_DEFAULT_USER: guest
# RABBITMQ_DEFAULT_PASS: guest # RABBITMQ_DEFAULT_PASS: guest
# jeecg-boot-sentinel:
# restart: on-failure jeecg-boot-sentinel:
# build: restart: on-failure
# context: ./jeecg-visual/jeecg-cloud-sentinel build:
# ports: context: ./jeecg-boot/jeecg-server-cloud/jeecg-visual/jeecg-cloud-sentinel
# - 9000:9000 ports:
# depends_on: - 9000:9000
# - jeecg-boot-nacos depends_on:
# - jeecg-boot-demo - jeecg-boot-nacos
# - jeecg-boot-system - jeecg-boot-demo
# - jeecg-boot-gateway - jeecg-boot-system
# container_name: jeecg-boot-sentinel - jeecg-boot-gateway
# hostname: jeecg-boot-sentinel container_name: jeecg-boot-sentinel
# hostname: jeecg-boot-sentinel
# jeecg-boot-xxljob: networks:
# build: - jeecg-boot
# context: ./jeecg-visual/jeecg-cloud-xxljob
# ports: jeecg-boot-xxljob:
# - 9080:9080 build:
# container_name: jeecg-boot-xxljob context: ./jeecg-boot/jeecg-server-cloud/jeecg-visual/jeecg-cloud-xxljob
# hostname: jeecg-boot-xxljob ports:
- 9080:9080
container_name: jeecg-boot-xxljob
hostname: jeecg-boot-xxljob
networks:
- jeecg-boot
jeecg-vue: jeecg-vue:
build: build:

View File

@ -2,12 +2,12 @@
JeecgBoot 低代码开发平台 JeecgBoot 低代码开发平台
=============== ===============
当前最新版本: 3.8.2发布日期2025-08-04 当前最新版本: 3.8.3发布日期2025-09-22
[![AUR](https://img.shields.io/badge/license-Apache%20License%202.0-blue.svg)](https://github.com/zhangdaiscott/jeecg-boot/blob/master/LICENSE) [![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/Author-北京国炬软件-orange.svg)](http://jeecg.com/aboutusIndex)
[![](https://img.shields.io/badge/version-3.8.2-brightgreen.svg)](https://github.com/zhangdaiscott/jeecg-boot) [![](https://img.shields.io/badge/version-3.8.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 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) [![GitHub forks](https://img.shields.io/github/forks/zhangdaiscott/jeecg-boot.svg?style=social&label=Fork)](https://github.com/zhangdaiscott/jeecg-boot)

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.2.0 # XXL-JOB v2.4.0
# Copyright (c) 2015-present, xuxueli. # Copyright (c) 2015-present, xuxueli.
CREATE database if NOT EXISTS `xxl_job` default character set utf8mb4 collate utf8mb4_general_ci; CREATE database if NOT EXISTS `xxl_job` default character set utf8mb4 collate utf8mb4_general_ci;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -3,7 +3,6 @@
> JeecgBoot属于平台级产品每次升级改动较大目前做不到平滑升级。 > JeecgBoot属于平台级产品每次升级改动较大目前做不到平滑升级。
### 增量升级方案 ### 增量升级方案
#### 1.代码合并 #### 1.代码合并
本地通过svn或git做好主干在分支上做业务开发jeecg每次版本发布可以手工覆盖主干的代码对比合并代码 本地通过svn或git做好主干在分支上做业务开发jeecg每次版本发布可以手工覆盖主干的代码对比合并代码
@ -12,12 +11,5 @@
- 其他库请手工执行SQL, 目录: `jeecg-module-system\jeecg-system-start\src\main\resources\flyway\sql\mysql` - 其他库请手工执行SQL, 目录: `jeecg-module-system\jeecg-system-start\src\main\resources\flyway\sql\mysql`
> 注意: 升级sql只提供mysql版本如果有权限升级, 还需要手工角色授权,退出重新登录才好使。 > 注意: 升级sql只提供mysql版本如果有权限升级, 还需要手工角色授权,退出重新登录才好使。
#### 3.其他数据库脚本说明 #### 3.兼容问题
原先官方默认提供oracle和SqlServer的脚本但是维护成本太高未提供脚本的数据库可以参考下面的文档自己转
https://my.oschina.net/jeecg/blog/4905722
注意定时任务的表qrtz_*,需要删掉用原始的脚本重新执行一下)
quartz-2.2.3-distribution.tar.gz放到百度网盘中大家自己下载执行所需数据库脚本
https://pan.baidu.com/s/1WrmZdUuAPg3iBwJ-LoHWyg?pwd=8mdz
#### 4.兼容问题
每次发版,会针对不兼容地方重点说明。 每次发版,会针对不兼容地方重点说明。

View File

@ -24,8 +24,8 @@ services:
jeecg-boot-redis: jeecg-boot-redis:
image: registry.cn-hangzhou.aliyuncs.com/jeecgdocker/redis:5.0 image: registry.cn-hangzhou.aliyuncs.com/jeecgdocker/redis:5.0
ports: # ports:
- 6379:6379 # - 6379:6379
restart: always restart: always
hostname: jeecg-boot-redis hostname: jeecg-boot-redis
container_name: jeecg-boot-redis container_name: jeecg-boot-redis
@ -39,12 +39,26 @@ services:
POSTGRES_USER: postgres POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres POSTGRES_PASSWORD: postgres
POSTGRES_DB: vector_db POSTGRES_DB: vector_db
ports: # ports:
- 5432:5432 # - 5432:5432
restart: always restart: always
networks: networks:
- jeecg-boot - 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: jeecg-boot-system:
build: build:
context: ./jeecg-module-system/jeecg-system-start context: ./jeecg-module-system/jeecg-system-start
@ -59,6 +73,8 @@ services:
- 8080:8080 - 8080:8080
networks: networks:
- jeecg-boot - jeecg-boot
volumes:
- ./config:/jeecg-boot/config
networks: networks:
jeecg-boot: jeecg-boot:

View File

@ -4,7 +4,7 @@
<parent> <parent>
<groupId>org.jeecgframework.boot</groupId> <groupId>org.jeecgframework.boot</groupId>
<artifactId>jeecg-boot-parent</artifactId> <artifactId>jeecg-boot-parent</artifactId>
<version>3.8.2</version> <version>3.8.3</version>
</parent> </parent>
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<artifactId>jeecg-boot-base-core</artifactId> <artifactId>jeecg-boot-base-core</artifactId>
@ -43,22 +43,12 @@
<!--jeecg-tools--> <!--jeecg-tools-->
<dependency> <dependency>
<groupId>org.jeecgframework.boot</groupId> <groupId>org.jeecgframework.boot</groupId>
<artifactId>jeecg-boot-common3</artifactId> <artifactId>jeecg-boot-common</artifactId>
</dependency> </dependency>
<!--集成springmvc框架并实现自动配置 --> <!--集成springmvc框架并实现自动配置 -->
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId> <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> </dependency>
<!-- websocket --> <!-- websocket -->
<dependency> <dependency>
@ -108,7 +98,7 @@
<!-- mybatis-plus --> <!-- mybatis-plus -->
<dependency> <dependency>
<groupId>com.baomidou</groupId> <groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId> <artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version> <version>${mybatis-plus.version}</version>
</dependency> </dependency>
<dependency> <dependency>
@ -117,22 +107,22 @@
<version>${mybatis-plus.version}</version> <version>${mybatis-plus.version}</version>
</dependency> </dependency>
<!-- minidao --> <!-- minidao -->
<dependency> <dependency>
<groupId>org.jeecgframework.boot3</groupId> <groupId>org.jeecgframework</groupId>
<artifactId>minidao-spring-boot-starter-jsqlparser-4.9</artifactId> <artifactId>minidao-spring-boot-starter-jsqlparser-4.9</artifactId>
</dependency> </dependency>
<!-- druid --> <!-- druid -->
<dependency> <dependency>
<groupId>com.alibaba</groupId> <groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-3-starter</artifactId> <artifactId>druid-spring-boot-starter</artifactId>
<version>${druid.version}</version> <version>${druid.version}</version>
</dependency> </dependency>
<!-- 动态数据源 --> <!-- 动态数据源 -->
<dependency> <dependency>
<groupId>com.baomidou</groupId> <groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot3-starter</artifactId> <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
<version>${dynamic-datasource-spring-boot-starter.version}</version> <version>${dynamic-datasource-spring-boot-starter.version}</version>
</dependency> </dependency>
@ -147,7 +137,7 @@
<!-- sqlserver--> <!-- sqlserver-->
<dependency> <dependency>
<groupId>com.microsoft.sqlserver</groupId> <groupId>com.microsoft.sqlserver</groupId>
<artifactId>sqljdbc4</artifactId> <artifactId>mssql-jdbc</artifactId>
<version>${sqljdbc4.version}</version> <version>${sqljdbc4.version}</version>
<scope>runtime</scope> <scope>runtime</scope>
</dependency> </dependency>
@ -169,13 +159,13 @@
<dependency> <dependency>
<groupId>org.jeecgframework</groupId> <groupId>org.jeecgframework</groupId>
<artifactId>kingbase8</artifactId> <artifactId>kingbase8</artifactId>
<version>9.0.0</version> <version>${kingbase8.version}</version>
<scope>runtime</scope> <scope>runtime</scope>
</dependency> </dependency>
<!--达梦数据库驱动 版本号1-3-26-2023.07.26-197096-20046-ENT --> <!--达梦数据库驱动 版本号1-3-26-2023.07.26-197096-20046-ENT -->
<dependency> <dependency>
<groupId>com.dameng</groupId> <groupId>com.dameng</groupId>
<artifactId>Dm8JdbcDriver18</artifactId> <artifactId>DmJdbcDriver18</artifactId>
<version>${dm8.version}</version> <version>${dm8.version}</version>
</dependency> </dependency>
<dependency> <dependency>
@ -201,50 +191,7 @@
<dependency> <dependency>
<groupId>org.apache.shiro</groupId> <groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-starter</artifactId> <artifactId>shiro-spring-boot-starter</artifactId>
<classifier>jakarta</classifier>
<version>${shiro.version}</version> <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>
</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> </dependency>
<!-- shiro-redis --> <!-- shiro-redis -->
<dependency> <dependency>
@ -263,23 +210,12 @@
</exclusions> </exclusions>
</dependency> </dependency>
<!-- knife4j -->
<!-- <dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
<version>${knife4j-spring-boot-starter.version}</version>
</dependency>-->
<!-- knife4j 升级springboot3.4.5报错 -->
<dependency> <dependency>
<groupId>com.github.xiaoymin</groupId> <groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-openapi3-ui</artifactId> <artifactId>knife4j-openapi3-spring-boot-starter</artifactId>
<version>${knife4j-spring-boot-starter.version}</version> <version>${knife4j-spring-boot-starter.version}</version>
</dependency> </dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.7.0</version>
</dependency>
<!-- 代码生成器 --> <!-- 代码生成器 -->
<!-- 如下载失败,请参考此文档 https://help.jeecg.com/java/setup/maven.html --> <!-- 如下载失败,请参考此文档 https://help.jeecg.com/java/setup/maven.html -->
@ -301,19 +237,8 @@
<!-- AutoPoi Excel工具类--> <!-- AutoPoi Excel工具类-->
<dependency> <dependency>
<groupId>org.jeecgframework.boot3</groupId> <groupId>org.jeecgframework</groupId>
<artifactId>autopoi-web</artifactId> <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>
<dependency> <dependency>
<groupId>xerces</groupId> <groupId>xerces</groupId>
@ -350,16 +275,6 @@
<dependency> <dependency>
<groupId>com.xkcoding.justauth</groupId> <groupId>com.xkcoding.justauth</groupId>
<artifactId>justauth-spring-boot-starter</artifactId> <artifactId>justauth-spring-boot-starter</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</exclusion>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
</exclusion>
</exclusions>
</dependency> </dependency>
<dependency> <dependency>
<groupId>com.squareup.okhttp3</groupId> <groupId>com.squareup.okhttp3</groupId>
@ -386,7 +301,7 @@
<!-- chatgpt --> <!-- chatgpt -->
<dependency> <dependency>
<groupId>org.jeecgframework.boot</groupId> <groupId>org.jeecgframework.boot</groupId>
<artifactId>jeecg-boot-starter3-chatgpt</artifactId> <artifactId>jeecg-boot-starter-chatgpt</artifactId>
</dependency> </dependency>
</dependencies> </dependencies>
</project> </project>

View File

@ -1,5 +1,6 @@
package org.jeecg.common.api; package org.jeecg.common.api;
import org.jeecg.common.api.dto.AiragFlowDTO;
import org.jeecg.common.system.vo.*; import org.jeecg.common.system.vo.*;
import java.util.List; import java.util.List;
@ -144,4 +145,15 @@ public interface CommonAPI {
List<DictModel> translateDictFromTableByKeys(String table, String text, String code, String keys, String dataSource); List<DictModel> translateDictFromTableByKeys(String table, String text, String code, String keys, String dataSource);
//update-end---author:chenrui ---date:20231221 for[issues/#5643]解决分布式下表字典跨库无法查询问题------------ //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
*/
Object runAiragFlow(AiragFlowDTO airagFlowDTO);
} }

View File

@ -0,0 +1,36 @@
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

@ -2,7 +2,7 @@ package org.jeecg.common.api.dto;
import lombok.Data; import lombok.Data;
import jakarta.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import java.io.Serializable; import java.io.Serializable;
/** /**

View File

@ -20,14 +20,14 @@ import org.jeecg.common.system.vo.LoginUser;
import org.jeecg.common.util.IpUtils; import org.jeecg.common.util.IpUtils;
import org.jeecg.common.util.SpringContextUtils; import org.jeecg.common.util.SpringContextUtils;
import org.jeecg.common.util.oConvertUtils; import org.jeecg.common.util.oConvertUtils;
import org.springframework.core.StandardReflectionParameterNameDiscoverer; import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.validation.BindingResult; import org.springframework.validation.BindingResult;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import jakarta.annotation.Resource; import javax.annotation.Resource;
import jakarta.servlet.ServletRequest; import javax.servlet.ServletRequest;
import jakarta.servlet.ServletResponse; import javax.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.util.Date; import java.util.Date;
@ -143,7 +143,7 @@ public class AutoLogAspect {
// https://my.oschina.net/mengzhang6/blog/2395893 // https://my.oschina.net/mengzhang6/blog/2395893
Object[] arguments = new Object[paramsArray.length]; Object[] arguments = new Object[paramsArray.length];
for (int i = 0; i < paramsArray.length; i++) { 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) { if (paramsArray[i] instanceof BindingResult || paramsArray[i] instanceof ServletRequest || paramsArray[i] instanceof ServletResponse || paramsArray[i] instanceof MultipartFile || 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) //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 //ServletResponse不能序列化 从入参里排除否则报异常java.lang.IllegalStateException: getOutputStream() has already been called for this response
continue; continue;
@ -172,7 +172,7 @@ public class AutoLogAspect {
// 请求的方法参数值 // 请求的方法参数值
Object[] args = joinPoint.getArgs(); Object[] args = joinPoint.getArgs();
// 请求的方法参数名称 // 请求的方法参数名称
StandardReflectionParameterNameDiscoverer u=new StandardReflectionParameterNameDiscoverer(); LocalVariableTableParameterNameDiscoverer u = new LocalVariableTableParameterNameDiscoverer();
String[] paramNames = u.getParameterNames(method); String[] paramNames = u.getParameterNames(method);
if (args != null && paramNames != null) { if (args != null && paramNames != null) {
for (int i = 0; i < args.length; i++) { for (int i = 0; i < args.length; i++) {

View File

@ -21,7 +21,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import jakarta.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.util.List; import java.util.List;

View File

@ -642,11 +642,12 @@ public interface CommonConstant {
/** /**
* 自定义首页关联关系(ROLE:表示角色 USER:表示用户) * 自定义首页关联关系(ROLE:表示角色 USER:表示用户 DEFAULT:默认首页)
* *
*/ */
String HOME_RELATION_ROLE = "ROLE"; String HOME_RELATION_ROLE = "ROLE";
String HOME_RELATION_USER = "USER"; String HOME_RELATION_USER = "USER";
String HOME_RELATION_DEFAULT = "DEFAULT";
/** /**
* 是否置顶(0否 1是) * 是否置顶(0否 1是)
@ -659,4 +660,53 @@ public interface CommonConstant {
String FLOW_FOCUS_NOTICE_PREFIX = "flow:runtimeData:focus:notice:"; String FLOW_FOCUS_NOTICE_PREFIX = "flow:runtimeData:focus:notice:";
//任务缓办时间缓存前缀 //任务缓办时间缓存前缀
String FLOW_TASK_DELAY_PREFIX = "flow:runtimeData:task:delay:"; 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:";
} }

View File

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

View File

@ -121,7 +121,7 @@ public class ProvinceCityArea {
public void getAreaByCode(String code,List<String> ls){ public void getAreaByCode(String code,List<String> ls){
for(Area area: areaList){ for(Area area: areaList){
if(area.getId().equals(code)){ if(null != area && area.getId().equals(code)){
String pid = area.getPid(); String pid = area.getPid();
ls.add(0,area.getText()); ls.add(0,area.getText());
getAreaByCode(pid,ls); getAreaByCode(pid,ls);

View File

@ -0,0 +1,97 @@
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

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

View File

@ -14,7 +14,16 @@ public enum NoticeTypeEnum {
NOTICE_TYPE_PLAN("日程消息","plan"), NOTICE_TYPE_PLAN("日程消息","plan"),
//暂时没用到 //暂时没用到
NOTICE_TYPE_MEETING("会议消息","meeting"), NOTICE_TYPE_MEETING("会议消息","meeting"),
NOTICE_TYPE_SYSTEM("系统消息","system"); NOTICE_TYPE_SYSTEM("系统消息","system"),
/**
* 协同工作
* for [JHHB-136]【vue3】协同工作系统消息需要添加一个类型
*/
NOTICE_TYPE_COLLABORATION("协同工作", "collab"),
/**
* 督办
*/
NOTICE_TYPE_SUPERVISE("督办管理", "supe");
/** /**
* 文件类型名称 * 文件类型名称

View File

@ -0,0 +1,180 @@
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,7 +23,25 @@ public enum SysAnnmentTypeEnum {
/** /**
* 邀请用户跳转到个人设置 * 邀请用户跳转到个人设置
*/ */
TENANT_INVITE("tenant_invite", "url", "/system/usersetting"); 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");
/** /**
* 业务类型(email:邮件 bpm:流程) * 业务类型(email:邮件 bpm:流程)

View File

@ -1,9 +1,6 @@
package org.jeecg.common.exception; package org.jeecg.common.exception;
import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.ObjectUtil;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import io.undertow.server.RequestTooBigException;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.exception.ExceptionUtils; import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.shiro.SecurityUtils; import org.apache.shiro.SecurityUtils;
@ -36,6 +33,8 @@ import org.springframework.web.multipart.MaxUploadSizeExceededException;
import org.springframework.web.multipart.MultipartException; import org.springframework.web.multipart.MultipartException;
import org.springframework.web.servlet.NoHandlerFoundException; import org.springframework.web.servlet.NoHandlerFoundException;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.util.Map; import java.util.Map;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -180,7 +179,7 @@ public class JeecgBootExceptionHandler {
@ExceptionHandler(MultipartException.class) @ExceptionHandler(MultipartException.class)
public Result<?> handleMaxUploadSizeExceededException(MultipartException e) { public Result<?> handleMaxUploadSizeExceededException(MultipartException e) {
Throwable cause = e.getCause(); Throwable cause = e.getCause();
if (cause instanceof IllegalStateException && cause.getCause() instanceof RequestTooBigException) { if (cause instanceof IllegalStateException) {
log.error("文件大小超出限制: {}", cause.getMessage(), e); log.error("文件大小超出限制: {}", cause.getMessage(), e);
addSysLog(e); addSysLog(e);
return Result.error("文件大小超出限制, 请压缩或降低文件质量!"); return Result.error("文件大小超出限制, 请压缩或降低文件质量!");
@ -291,7 +290,7 @@ public class JeecgBootExceptionHandler {
boolean isTooBigException = false; boolean isTooBigException = false;
if(e instanceof MultipartException){ if(e instanceof MultipartException){
Throwable cause = e.getCause(); Throwable cause = e.getCause();
if (cause instanceof IllegalStateException && cause.getCause() instanceof RequestTooBigException){ if (cause instanceof IllegalStateException){
isTooBigException = true; isTooBigException = true;
} }
} }

View File

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

View File

@ -18,14 +18,15 @@ import org.jeecgframework.poi.excel.entity.ExportParams;
import org.jeecgframework.poi.excel.entity.ImportParams; import org.jeecgframework.poi.excel.entity.ImportParams;
import org.jeecgframework.poi.excel.entity.enmus.ExcelType; import org.jeecgframework.poi.excel.entity.enmus.ExcelType;
import org.jeecgframework.poi.excel.view.JeecgEntityExcelView; import org.jeecgframework.poi.excel.view.JeecgEntityExcelView;
import org.jeecgframework.poi.handler.inter.IExcelExportServer;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.MultipartHttpServletRequest; import org.springframework.web.multipart.MultipartHttpServletRequest;
import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.ModelAndView;
import jakarta.annotation.Resource; import javax.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import java.io.IOException; import java.io.IOException;
import java.util.*; import java.util.*;
@ -127,6 +128,53 @@ public class JeecgController<T, S extends IService<T>> {
return mv; 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传入导出字段参数 * 根据权限导出excel传入导出字段参数

View File

@ -13,7 +13,6 @@ import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors; import lombok.experimental.Accessors;
import io.swagger.v3.oas.annotations.media.Schema;
/** /**
* @Description: Entity基类 * @Description: Entity基类

View File

@ -414,9 +414,11 @@ public class QueryGenerator {
} }
// update-begin-author:sunjianlei date:20220119 for: 【JTC-573】 过滤空条件查询,防止 sql 拼接多余的 and // update-begin-author:sunjianlei date:20220119 for: 【JTC-573】 过滤空条件查询,防止 sql 拼接多余的 and
List<QueryCondition> filterConditions = conditions.stream().filter( List<QueryCondition> filterConditions = conditions.stream().filter(
rule -> oConvertUtils.isNotEmpty(rule.getField()) rule -> (oConvertUtils.isNotEmpty(rule.getField())
&& oConvertUtils.isNotEmpty(rule.getRule()) && oConvertUtils.isNotEmpty(rule.getRule())
&& oConvertUtils.isNotEmpty(rule.getVal()) && oConvertUtils.isNotEmpty(rule.getVal())
)
|| "empty".equals(rule.getRule())
).collect(Collectors.toList()); ).collect(Collectors.toList());
if (filterConditions.size() == 0) { if (filterConditions.size() == 0) {
return; return;
@ -427,9 +429,12 @@ public class QueryGenerator {
queryWrapper.and(andWrapper -> { queryWrapper.and(andWrapper -> {
for (int i = 0; i < filterConditions.size(); i++) { for (int i = 0; i < filterConditions.size(); i++) {
QueryCondition rule = filterConditions.get(i); QueryCondition rule = filterConditions.get(i);
if (oConvertUtils.isNotEmpty(rule.getField()) if (
&& oConvertUtils.isNotEmpty(rule.getRule()) (
&& oConvertUtils.isNotEmpty(rule.getVal())) { oConvertUtils.isNotEmpty(rule.getField()) && oConvertUtils.isNotEmpty(rule.getRule()) && oConvertUtils.isNotEmpty(rule.getVal())
)
|| "empty".equals(rule.getRule())
) {
log.debug("SuperQuery ==> " + rule.toString()); log.debug("SuperQuery ==> " + rule.toString());
@ -716,7 +721,11 @@ public class QueryGenerator {
* @param value 查询条件值 * @param value 查询条件值
*/ */
public static void addEasyQuery(QueryWrapper<?> queryWrapper, String name, QueryRuleEnum rule, Object value) { public static void addEasyQuery(QueryWrapper<?> queryWrapper, String name, QueryRuleEnum rule, Object value) {
if (name==null || value == null || rule == null || oConvertUtils.isEmpty(value)) { if (
(
name==null || value == null || rule == null || oConvertUtils.isEmpty(value)
)
&& !QueryRuleEnum.EMPTY.equals(rule)) {
return; return;
} }
name = oConvertUtils.camelToUnderline(name); name = oConvertUtils.camelToUnderline(name);
@ -728,6 +737,9 @@ public class QueryGenerator {
case GE: case GE:
queryWrapper.ge(name, value); queryWrapper.ge(name, value);
break; break;
case EMPTY:
queryWrapper.isNull(name);
break;
case LT: case LT:
queryWrapper.lt(name, value); queryWrapper.lt(name, value);
break; break;

View File

@ -5,7 +5,7 @@ import org.jeecg.common.system.vo.SysUserCacheInfo;
import org.jeecg.common.util.SpringContextUtils; import org.jeecg.common.util.SpringContextUtils;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
import jakarta.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;

View File

@ -6,18 +6,16 @@ import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTDecodeException; import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.interfaces.DecodedJWT; import com.auth0.jwt.interfaces.DecodedJWT;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.base.Joiner;
import java.io.IOException; import java.io.IOException;
import java.io.OutputStream; import java.io.OutputStream;
import java.util.Date; import java.util.Date;
import java.util.Objects; import java.util.Objects;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import javax.servlet.ServletResponse;
import jakarta.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession;
import jakarta.servlet.http.HttpSession;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.SecurityUtils; import org.apache.shiro.SecurityUtils;
@ -51,24 +49,24 @@ public class JwtUtil {
* @param code * @param code
* @param errorMsg * @param errorMsg
*/ */
public static void responseError(ServletResponse response, Integer code, String errorMsg) { public static void responseError(HttpServletResponse response, Integer code, String errorMsg) {
HttpServletResponse httpServletResponse = (HttpServletResponse) response; try {
// issues/I4YH95浏览器显示乱码问题 Result jsonResult = new Result(code, errorMsg);
httpServletResponse.setHeader("Content-type", "text/html;charset=UTF-8"); jsonResult.setSuccess(false);
Result jsonResult = new Result(code, errorMsg);
jsonResult.setSuccess(false); // 设置响应头和内容类型
OutputStream os = null; response.setStatus(code);
try { response.setHeader("Content-type", "text/html;charset=UTF-8");
os = httpServletResponse.getOutputStream(); response.setContentType("application/json;charset=UTF-8");
httpServletResponse.setCharacterEncoding("UTF-8"); // 使用 ObjectMapper 序列化为 JSON 字符串
httpServletResponse.setStatus(code); ObjectMapper objectMapper = new ObjectMapper();
os.write(new ObjectMapper().writeValueAsString(jsonResult).getBytes("UTF-8")); String json = objectMapper.writeValueAsString(jsonResult);
os.flush(); response.getWriter().write(json);
os.close(); response.getWriter().flush();
} catch (IOException e) { } catch (IOException e) {
log.error(e.getMessage(), e); log.error(e.getMessage(), e);
} }
} }
/** /**
* 校验token是否正确 * 校验token是否正确
@ -101,7 +99,7 @@ public class JwtUtil {
DecodedJWT jwt = JWT.decode(token); DecodedJWT jwt = JWT.decode(token);
return jwt.getClaim("username").asString(); return jwt.getClaim("username").asString();
} catch (JWTDecodeException e) { } catch (JWTDecodeException e) {
log.warn(e.getMessage(), e); log.error(e.getMessage(), e);
return null; return null;
} }
} }

View File

@ -13,31 +13,33 @@ import org.springframework.core.type.classreading.CachingMetadataReaderFactory;
import org.springframework.core.type.classreading.MetadataReader; import org.springframework.core.type.classreading.MetadataReader;
import org.springframework.core.type.classreading.MetadataReaderFactory; import org.springframework.core.type.classreading.MetadataReaderFactory;
import org.springframework.util.ClassUtils; import org.springframework.util.ClassUtils;
import java.lang.reflect.Method;
import java.util.ArrayList; import java.util.*;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/** /**
* 资源加载工具类 * 枚举字典数据 资源加载工具类
*
* @Author taoYan * @Author taoYan
* @Date 2022/7/8 10:40 * @Date 2022/7/8 10:40
**/ **/
@Slf4j @Slf4j
public class ResourceUtil { 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); private final static Map<String, List<DictModel>> enumDictData = new HashMap<>(5);
/**
* 所有java类
*/
private final static String CLASS_PATTERN="/**/*.class";
/** /**
* 所有枚举java类 * 所有枚举java类
*/ */
@ -45,9 +47,9 @@ public class ResourceUtil {
private final static String CLASS_ENUM_PATTERN="/**/*Enum.class"; private final static String CLASS_ENUM_PATTERN="/**/*Enum.class";
/** /**
* 包路径 org.jeecg * 初始化状态标识
*/ */
private final static String BASE_PACKAGE = "org.jeecg"; private static volatile boolean initialized = false;
/** /**
* 枚举类中获取字典数据的方法名 * 枚举类中获取字典数据的方法名
@ -55,59 +57,135 @@ public class ResourceUtil {
private final static String METHOD_NAME = "getDictList"; private final static String METHOD_NAME = "getDictList";
/** /**
* 获取枚举字典数据
* 获取枚举类对应的字典数据 SysDictServiceImpl#queryAllDictItems() * 获取枚举类对应的字典数据 SysDictServiceImpl#queryAllDictItems()
* @return *
* @return 枚举字典数据
*/ */
public static Map<String, List<DictModel>> getEnumDictData(){ public static Map<String, List<DictModel>> getEnumDictData() {
if(enumDictData.keySet().size()>0){ if (!initialized) {
return enumDictData; synchronized (ResourceUtil.class) {
} if (!initialized) {
ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver(); long startTime = System.currentTimeMillis();
String pattern = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX + ClassUtils.convertClassNameToResourcePath(BASE_PACKAGE) + CLASS_ENUM_PATTERN; log.info("【枚举字典加载】开始初始化枚举字典数据...");
try {
Resource[] resources = resourcePatternResolver.getResources(pattern); initEnumDictData();
MetadataReaderFactory readerFactory = new CachingMetadataReaderFactory(resourcePatternResolver); initialized = true;
for (Resource resource : resources) {
MetadataReader reader = readerFactory.getMetadataReader(resource); long endTime = System.currentTimeMillis();
String classname = reader.getClassMetadata().getClassName(); log.info("【枚举字典加载】枚举字典数据初始化完成,共加载 {} 个字典,总耗时: {}ms", enumDictData.size(), endTime - startTime);
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; return enumDictData;
} }
/** /**
* 用于后端字典翻译 SysDictServiceImpl#queryManyDictByKeys(java.util.List, java.util.List) * 使用多包路径扫描方式初始化枚举字典数据
* @param dictCodeList
* @param keys
* @return
*/ */
public static Map<String, List<DictModel>> queryManyDictByKeys(List<String> dictCodeList, List<String> keys){ private static void initEnumDictData() {
if(enumDictData.keySet().size()==0){ ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();
getEnumDictData();
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.info("【枚举字典加载】文件扫描完成,总共找到 {} 个枚举类文件,扫描耗时: {}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.info("【枚举字典加载】处理完成,实际处理 {} 个带注解的枚举类,处理耗时: {}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 字典数据映射
*/
public static Map<String, List<DictModel>> queryManyDictByKeys(List<String> dictCodeList, List<String> keys) {
Map<String, List<DictModel>> enumDict = getEnumDictData();
Map<String, List<DictModel>> map = new HashMap<>(); Map<String, List<DictModel>> map = new HashMap<>();
for (String code : enumDictData.keySet()) {
if(dictCodeList.indexOf(code)>=0){ // 使用更高效的查找方式
List<DictModel> dictItemList = enumDictData.get(code); Set<String> dictCodeSet = new HashSet<>(dictCodeList);
for(DictModel dm: dictItemList){ 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) {
String value = dm.getValue(); String value = dm.getValue();
if(keys.indexOf(value)>=0){ if (keySet.contains(value)) {
List<DictModel> list = new ArrayList<>(); List<DictModel> list = new ArrayList<>();
list.add(new DictModel(value, dm.getText())); list.add(new DictModel(value, dm.getText()));
map.put(code,list); map.put(code, list);
break; break;
} }
} }
@ -116,21 +194,4 @@ public class ResourceUtil {
return map; 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

@ -144,4 +144,8 @@ public class LoginUser {
/**设备id uniapp推送用*/ /**设备id uniapp推送用*/
private String clientId; private String clientId;
/**
* 主岗位
*/
private String mainDepPostId;
} }

View File

@ -5,7 +5,7 @@ import java.util.Map;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import jakarta.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
/** /**
* *

View File

@ -19,12 +19,13 @@ import org.springframework.jdbc.datasource.DriverManagerDataSource;
import org.springframework.util.FileCopyUtils; import org.springframework.util.FileCopyUtils;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import jakarta.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.sql.DataSource; import javax.sql.DataSource;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.sql.Connection; import java.sql.Connection;
import java.sql.DatabaseMetaData; import java.sql.DatabaseMetaData;
import java.sql.SQLException; import java.sql.SQLException;
@ -152,9 +153,9 @@ public class CommonUtils {
*/ */
public static String uploadLocal(MultipartFile mf,String bizPath,String uploadpath){ public static String uploadLocal(MultipartFile mf,String bizPath,String uploadpath){
try { try {
//update-begin-author:liusq date:20210809 for: 过滤上传文件类型 // 文件安全校验,防止上传漏洞文件
SsrfFileTypeFilter.checkUploadFileType(mf); SsrfFileTypeFilter.checkUploadFileType(mf, bizPath);
//update-end-author:liusq date:20210809 for: 过滤上传文件类型
String fileName = null; String fileName = null;
File file = new File(uploadpath + File.separator + bizPath + File.separator ); File file = new File(uploadpath + File.separator + bizPath + File.separator );
if (!file.exists()) { if (!file.exists()) {
@ -163,6 +164,10 @@ public class CommonUtils {
} }
// 获取文件名 // 获取文件名
String orgName = mf.getOriginalFilename(); 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); orgName = CommonUtils.getFileName(orgName);
if(orgName.indexOf(SymbolConstant.SPOT)!=-1){ if(orgName.indexOf(SymbolConstant.SPOT)!=-1){
fileName = orgName.substring(0, orgName.lastIndexOf(".")) + "_" + System.currentTimeMillis() + orgName.substring(orgName.lastIndexOf(".")); fileName = orgName.substring(0, orgName.lastIndexOf(".")) + "_" + System.currentTimeMillis() + orgName.substring(orgName.lastIndexOf("."));
@ -242,6 +247,10 @@ public class CommonUtils {
try { try {
DataSource dataSource = SpringContextUtils.getApplicationContext().getBean(DataSource.class); DataSource dataSource = SpringContextUtils.getApplicationContext().getBean(DataSource.class);
dbTypeEnum = JdbcUtils.getDbType(dataSource.getConnection().getMetaData().getURL()); 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; return dbTypeEnum;
} catch (SQLException e) { } catch (SQLException e) {
log.warn(e.getMessage(), e); log.warn(e.getMessage(), e);

View File

@ -13,6 +13,8 @@ import java.time.LocalDate;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.ZoneId; import java.time.ZoneId;
import java.time.temporal.ChronoUnit; import java.time.temporal.ChronoUnit;
import java.util.List;
import java.util.ArrayList;
import java.util.Calendar; import java.util.Calendar;
import java.util.Date; import java.util.Date;
import java.util.GregorianCalendar; import java.util.GregorianCalendar;
@ -814,4 +816,44 @@ public class DateUtils extends PropertyEditorSupport {
return calendar1.get(Calendar.YEAR) == calendar2.get(Calendar.YEAR); 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

@ -1,14 +1,22 @@
package org.jeecg.common.util; package org.jeecg.common.util;
import jakarta.servlet.http.HttpServletResponse; import cn.hutool.core.io.IoUtil;
import lombok.extern.slf4j.Slf4j; 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.FilenameUtils;
import org.apache.commons.io.IOUtils;
import org.jeecg.common.constant.CommonConstant;
import org.jeecg.common.exception.JeecgBootException; import org.jeecg.common.exception.JeecgBootException;
import org.jeecg.common.util.filter.SsrfFileTypeFilter;
import javax.servlet.http.HttpServletResponse;
import java.io.*; import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL; import java.net.URL;
import java.net.URLConnection; import java.net.URLConnection;
import java.net.URLEncoder; import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files; import java.nio.file.Files;
import java.util.List; import java.util.List;
import java.util.zip.ZipEntry; import java.util.zip.ZipEntry;
@ -203,4 +211,150 @@ public class FileDownloadUtils {
dir.mkdirs(); 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

@ -4,13 +4,13 @@ import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject; import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.jeecg.common.constant.SymbolConstant; import org.jeecg.common.constant.SymbolConstant;
import org.jeecg.common.handler.IFillRuleHandler; import org.jeecg.common.handler.IFillRuleHandler;
import org.jeecg.common.system.query.QueryGenerator; import org.jeecg.common.system.query.QueryGenerator;
import javax.servlet.http.HttpServletRequest;
/** /**

View File

@ -1,6 +1,6 @@
package org.jeecg.common.util; package org.jeecg.common.util;
import jakarta.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.jeecg.common.constant.CommonConstant; import org.jeecg.common.constant.CommonConstant;

View File

@ -55,13 +55,11 @@ public class MinioUtil {
*/ */
public static String upload(MultipartFile file, String bizPath, String customBucket) throws Exception { public static String upload(MultipartFile file, String bizPath, String customBucket) throws Exception {
String fileUrl = ""; String fileUrl = "";
//update-begin-author:wangshuai date:20201012 for: 过滤上传文件夹名特殊字符,防止攻击 // 业务路径过滤,防止攻击
bizPath = StrAttackFilter.filter(bizPath); bizPath = StrAttackFilter.filter(bizPath);
//update-end-author:wangshuai date:20201012 for: 过滤上传文件夹名特殊字符,防止攻击
//update-begin-author:liusq date:20210809 for: 过滤上传文件类型 // 文件安全校验,防止上传漏洞文件
SsrfFileTypeFilter.checkUploadFileType(file); SsrfFileTypeFilter.checkUploadFileType(file, bizPath);
//update-end-author:liusq date:20210809 for: 过滤上传文件类型
String newBucket = bucketName; String newBucket = bucketName;
if(oConvertUtils.isNotEmpty(customBucket)){ if(oConvertUtils.isNotEmpty(customBucket)){

View File

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

View File

@ -221,6 +221,63 @@ public class RestUtil {
return RT.exchange(url, method, request, responseType); 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) {
SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
requestFactory.setConnectTimeout(timeout);
requestFactory.setReadTimeout(timeout);
restTemplate = new RestTemplate(requestFactory);
// 解决乱码问题
restTemplate.getMessageConverters().set(1, new StringHttpMessageConverter(StandardCharsets.UTF_8));
}
// 请求体
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));
}
// 发送请求
HttpEntity<String> request = new HttpEntity<>(body, headers);
return restTemplate.exchange(url, method, request, responseType);
}
/** /**
* 获取JSON请求头 * 获取JSON请求头
*/ */

View File

@ -0,0 +1,37 @@
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

@ -1,7 +1,7 @@
package org.jeecg.common.util; package org.jeecg.common.util;
import jakarta.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import org.jeecg.common.constant.CommonConstant; import org.jeecg.common.constant.CommonConstant;
import org.jeecg.common.constant.ServiceNameConstants; import org.jeecg.common.constant.ServiceNameConstants;

View File

@ -11,7 +11,7 @@ import org.jeecg.common.exception.JeecgBoot401Exception;
import org.jeecg.common.system.util.JwtUtil; import org.jeecg.common.system.util.JwtUtil;
import org.jeecg.common.system.vo.LoginUser; import org.jeecg.common.system.vo.LoginUser;
import jakarta.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
/** /**
* @Author scott * @Author scott
@ -65,6 +65,10 @@ public class TokenUtils {
if (tenantId == null) { if (tenantId == null) {
tenantId = oConvertUtils.getString(request.getHeader(CommonConstant.TENANT_ID)); tenantId = oConvertUtils.getString(request.getHeader(CommonConstant.TENANT_ID));
} }
if (oConvertUtils.isNotEmpty(tenantId) && "undefined".equals(tenantId)) {
return null;
}
return tenantId; return tenantId;
} }

View File

@ -1,6 +1,7 @@
package org.jeecg.common.util.encryption; package org.jeecg.common.util.encryption;
import org.apache.shiro.lang.codec.Base64; import org.apache.shiro.codec.Base64;
import javax.crypto.Cipher; import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec; import javax.crypto.spec.SecretKeySpec;

View File

@ -2,6 +2,7 @@ package org.jeecg.common.util.filter;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.jeecg.common.exception.JeecgBootException;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import java.io.IOException; import java.io.IOException;
@ -149,29 +150,38 @@ public class SsrfFileTypeFilter {
public static void checkDownloadFileType(String filePath) throws IOException { public static void checkDownloadFileType(String filePath) throws IOException {
//文件后缀 //文件后缀
String suffix = getFileTypeBySuffix(filePath); String suffix = getFileTypeBySuffix(filePath);
log.info("suffix:{}", suffix); log.debug(" 【文件下载校验】文件后缀 suffix: {}", suffix);
boolean isAllowExtension = FILE_TYPE_WHITE_LIST.contains(suffix.toLowerCase()); boolean isAllowExtension = FILE_TYPE_WHITE_LIST.contains(suffix.toLowerCase());
//是否允许下载的文件 //是否允许下载的文件
if (!isAllowExtension) { if (!isAllowExtension) {
throw new IOException("下载失败,存在非法文件类型:" + suffix); throw new JeecgBootException("下载失败,存在非法文件类型:" + suffix);
} }
} }
/** /**
* 上传文件类型过滤 * 上传文件类型过滤
* *
* @param file * @param file
*/ */
public static void checkUploadFileType(MultipartFile file) throws Exception { public static void checkUploadFileType(MultipartFile file) throws Exception {
//获取文件真是后缀 checkUploadFileType(file, null);
String suffix = getFileType(file); }
log.info("suffix:{}", suffix); /**
* 上传文件类型过滤
*
* @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);
boolean isAllowExtension = FILE_TYPE_WHITE_LIST.contains(suffix.toLowerCase()); boolean isAllowExtension = FILE_TYPE_WHITE_LIST.contains(suffix.toLowerCase());
//是否允许下载的文件 //是否允许下载的文件
if (!isAllowExtension) { if (!isAllowExtension) {
throw new Exception("上传失败,存在非法文件类型:" + suffix); throw new JeecgBootException("上传失败,存在非法文件类型:" + suffix);
} }
} }
@ -183,7 +193,7 @@ public class SsrfFileTypeFilter {
* @throws Exception * @throws Exception
*/ */
private static String getFileType(MultipartFile file) throws Exception { private static String getFileType(MultipartFile file, String customPath) throws Exception {
//update-begin-author:liusq date:20230404 for: [issue/4672]方法造成的文件被占用注释掉此方法tomcat就能自动清理掉临时文件 //update-begin-author:liusq date:20230404 for: [issue/4672]方法造成的文件被占用注释掉此方法tomcat就能自动清理掉临时文件
String fileExtendName = null; String fileExtendName = null;
InputStream is = null; InputStream is = null;
@ -203,7 +213,7 @@ public class SsrfFileTypeFilter {
break; break;
} }
} }
log.info("-----获取到的指定文件类型------"+fileExtendName); log.debug("-----获取到的指定文件类型------"+fileExtendName);
// 如果不是上述类型,则判断扩展名 // 如果不是上述类型,则判断扩展名
if (StringUtils.isBlank(fileExtendName)) { if (StringUtils.isBlank(fileExtendName)) {
String fileName = file.getOriginalFilename(); String fileName = file.getOriginalFilename();
@ -214,7 +224,6 @@ public class SsrfFileTypeFilter {
// 如果有扩展名,则返回扩展名 // 如果有扩展名,则返回扩展名
return getFileTypeBySuffix(fileName); return getFileTypeBySuffix(fileName);
} }
log.info("-----最終的文件类型------"+fileExtendName);
is.close(); is.close();
return fileExtendName; return fileExtendName;
} catch (Exception e) { } catch (Exception e) {
@ -249,4 +258,34 @@ public class SsrfFileTypeFilter {
} }
return stringBuilder.toString(); 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

@ -10,7 +10,7 @@ import org.jeecg.common.constant.SymbolConstant;
import org.jeecg.config.mybatis.MybatisPlusSaasConfig; import org.jeecg.config.mybatis.MybatisPlusSaasConfig;
import org.springframework.beans.BeanUtils; import org.springframework.beans.BeanUtils;
import jakarta.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.UnsupportedEncodingException; import java.io.UnsupportedEncodingException;
@ -474,6 +474,23 @@ public class oConvertUtils {
return true; 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对象 * 获取Map对象
*/ */
@ -1132,7 +1149,15 @@ public class oConvertUtils {
* @date 2020/9/12 15:50 * @date 2020/9/12 15:50
*/ */
public static <T> boolean isIn(T obj, T... objs) { public static <T> boolean isIn(T obj, T... objs) {
return isIn(obj, objs); if (isEmpty(objs)) {
return false;
}
for (T obj1 : objs) {
if (isEqual(obj, obj1)) {
return true;
}
}
return false;
} }
/** /**

View File

@ -97,9 +97,8 @@ public class OssBootUtil {
* @return oss 中的相对文件路径 * @return oss 中的相对文件路径
*/ */
public static String upload(MultipartFile file, String fileDir,String customBucket) throws Exception { public static String upload(MultipartFile file, String fileDir,String customBucket) throws Exception {
//update-begin-author:liusq date:20210809 for: 过滤上传文件类型 // 文件安全校验,防止上传漏洞文件
SsrfFileTypeFilter.checkUploadFileType(file); SsrfFileTypeFilter.checkUploadFileType(file);
//update-end-author:liusq date:20210809 for: 过滤上传文件类型
String filePath = null; String filePath = null;
initOss(endPoint, accessKeyId, accessKeySecret); initOss(endPoint, accessKeyId, accessKeySecret);

View File

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

View File

@ -3,7 +3,7 @@ package org.jeecg.config;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import jakarta.annotation.Resource; import javax.annotation.Resource;
import org.jeecg.common.api.CommonAPI; import org.jeecg.common.api.CommonAPI;
import org.jeecg.common.system.vo.DictModel; import org.jeecg.common.system.vo.DictModel;
@ -25,6 +25,7 @@ import lombok.extern.slf4j.Slf4j;
* @Version:1.0 * @Version:1.0
*/ */
@Slf4j @Slf4j
@Lazy(false)
@Service @Service
public class AutoPoiDictConfig implements AutoPoiDictServiceI { public class AutoPoiDictConfig implements AutoPoiDictServiceI {
final static String EXCEL_SPLIT_TAG = "_"; final static String EXCEL_SPLIT_TAG = "_";

View File

@ -2,9 +2,7 @@ package org.jeecg.config;
import java.io.IOException; import java.io.IOException;
import com.alibaba.druid.spring.boot3.autoconfigure.DruidDataSourceAutoConfigure; import javax.servlet.*;
import com.alibaba.druid.spring.boot3.autoconfigure.properties.DruidStatProperties;
import jakarta.servlet.*;
import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
@ -13,6 +11,8 @@ import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure;
import com.alibaba.druid.spring.boot.autoconfigure.properties.DruidStatProperties;
import com.alibaba.druid.util.Utils; import com.alibaba.druid.util.Utils;
/** /**

View File

@ -12,7 +12,6 @@ import java.util.HashMap;
import java.util.Map; import java.util.Map;
/** /**
* @author eightmonth@qq.com
* 启动程序修改DruidWallConfig配置 * 启动程序修改DruidWallConfig配置
* 允许SELECT语句的WHERE子句是一个永真条件 * 允许SELECT语句的WHERE子句是一个永真条件
* @author eightmonth * @author eightmonth

View File

@ -1,10 +1,7 @@
package org.jeecg.config; package org.jeecg.config;
import org.jeecg.config.vo.*; import org.jeecg.config.vo.*;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Role;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@ -14,7 +11,6 @@ import org.springframework.stereotype.Component;
*/ */
@Component("jeecgBaseConfig") @Component("jeecgBaseConfig")
@ConfigurationProperties(prefix = "jeecg") @ConfigurationProperties(prefix = "jeecg")
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public class JeecgBaseConfig { public class JeecgBaseConfig {
/** /**
* 签名密钥串(字典等敏感接口) * 签名密钥串(字典等敏感接口)
@ -76,11 +72,6 @@ public class JeecgBaseConfig {
*/ */
private BaiduApi baiduApi; private BaiduApi baiduApi;
/**
* 高德开放API配置
*/
private GaoDeApi gaoDeApi;
public String getCustomResourcePrefixPath() { public String getCustomResourcePrefixPath() {
return customResourcePrefixPath; return customResourcePrefixPath;
} }
@ -177,11 +168,4 @@ public class JeecgBaseConfig {
this.baiduApi = baiduApi; this.baiduApi = baiduApi;
} }
public GaoDeApi getGaoDeApi() {
return gaoDeApi;
}
public void setGaoDeApi(GaoDeApi gaoDeApi) {
this.gaoDeApi = gaoDeApi;
}
} }

View File

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

View File

@ -10,13 +10,11 @@ import io.swagger.v3.oas.models.security.SecurityRequirement;
import io.swagger.v3.oas.models.security.SecurityScheme; import io.swagger.v3.oas.models.security.SecurityScheme;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.constant.CommonConstant; import org.jeecg.common.constant.CommonConstant;
import org.springdoc.core.customizers.OperationCustomizer; import org.springdoc.core.customizers.GlobalOpenApiCustomizer;
import org.springdoc.core.filters.GlobalOpenApiMethodFilter; import org.springdoc.core.filters.GlobalOpenApiMethodFilter;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource; 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.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@ -63,53 +61,26 @@ public class Swagger3Config implements WebMvcConfigurer {
} }
@Bean @Bean
public OperationCustomizer operationCustomizer() { public GlobalOpenApiCustomizer globalOpenApiCustomizer() {
return (operation, handlerMethod) -> { return openApi -> {
String path = getFullPath(handlerMethod); // 全局添加鉴权参数
if (!isExcludedPath(path)) { if (openApi.getPaths() != null) {
operation.addSecurityItem(new SecurityRequirement().addList(CommonConstant.X_ACCESS_TOKEN)); openApi.getPaths().forEach((path, pathItem) -> {
}else{ //log.debug("path: {}", path);
log.info("忽略加入 X_ACCESS_TOKEN 的 PATH:" + path); // 检查当前路径是否在排除列表中
} boolean isExcluded = excludedPaths.stream().anyMatch(
return operation; excludedPath -> excludedPath.equals(path) || (excludedPath.endsWith("**") && path.startsWith(excludedPath.substring(0, excludedPath.length() - 2)))
}; );
}
private String getFullPath(HandlerMethod handlerMethod) { if (!isExcluded) {
StringBuilder fullPath = new StringBuilder(); // 接口添加鉴权参数
pathItem.readOperations().forEach(operation ->
// 获取类级别的路径 operation.addSecurityItem(new SecurityRequirement().addList(CommonConstant.X_ACCESS_TOKEN))
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 excludedPaths.stream()
.anyMatch(pattern -> {
if (pattern.endsWith("/**")) {
// 处理通配符匹配
String basePath = pattern.substring(0, pattern.length() - 3);
return path.startsWith(basePath);
} }
// 精确匹配
return pattern.equals(path);
}); });
}
};
} }
@Bean @Bean
@ -117,17 +88,13 @@ public class Swagger3Config implements WebMvcConfigurer {
return new OpenAPI() return new OpenAPI()
.info(new Info() .info(new Info()
.title("JeecgBoot 后台服务API接口文档") .title("JeecgBoot 后台服务API接口文档")
.version("3.8.2") .version("3.8.3")
.contact(new Contact().name("北京国炬信息技术有限公司").url("www.jeccg.com").email("jeecgos@163.com")) .contact(new Contact().name("北京国炬信息技术有限公司").url("www.jeccg.com").email("jeecgos@163.com"))
.description("后台API接口") .description( "后台API接口")
.termsOfService("NO terms of service") .termsOfService("NO terms of service")
.license(new License().name("Apache 2.0").url("http://www.apache.org/licenses/LICENSE-2.0.html"))) .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)) .addSecurityItem(new SecurityRequirement().addList(CommonConstant.X_ACCESS_TOKEN))
.components(new Components().addSecuritySchemes(CommonConstant.X_ACCESS_TOKEN, .components(new Components().addSecuritySchemes(CommonConstant.X_ACCESS_TOKEN,
new SecurityScheme() new SecurityScheme().name(CommonConstant.X_ACCESS_TOKEN).type(SecurityScheme.Type.HTTP)));
.name(CommonConstant.X_ACCESS_TOKEN)
.type(SecurityScheme.Type.APIKEY)
.in(SecurityScheme.In.HEADER) // 关键:指定为 header
));
} }
} }

View File

@ -1,19 +0,0 @@
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,10 +10,7 @@ import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer; import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer; import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;
import io.micrometer.prometheusmetrics.PrometheusMeterRegistry; import io.micrometer.prometheus.PrometheusMeterRegistry;
import jakarta.annotation.Resource;
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.Autowired;
import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
@ -26,7 +23,6 @@ import org.springframework.context.annotation.Primary;
import org.springframework.context.event.EventListener; import org.springframework.context.event.EventListener;
import org.springframework.http.CacheControl; import org.springframework.http.CacheControl;
import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
@ -36,6 +32,7 @@ import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry; import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import javax.annotation.Resource;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.LocalDateTime; import java.time.LocalDateTime;

View File

@ -3,8 +3,8 @@ package org.jeecg.config.filter;
import org.jeecg.common.constant.CommonConstant; import org.jeecg.common.constant.CommonConstant;
import org.jeecg.config.sign.util.BodyReaderHttpServletRequestWrapper; import org.jeecg.config.sign.util.BodyReaderHttpServletRequestWrapper;
import jakarta.servlet.*; import javax.servlet.*;
import jakarta.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import java.io.IOException; import java.io.IOException;
/** /**

View File

@ -7,9 +7,9 @@ import org.jeecg.common.util.SpringContextUtils;
import org.jeecg.common.util.TokenUtils; import org.jeecg.common.util.TokenUtils;
import org.jeecg.common.util.oConvertUtils; import org.jeecg.common.util.oConvertUtils;
import jakarta.servlet.*; import javax.servlet.*;
import jakarta.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import java.io.IOException; import java.io.IOException;
/** /**

View File

@ -14,9 +14,9 @@ import org.jeecg.config.JeecgBaseConfig;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.HandlerInterceptor;
import jakarta.annotation.Resource; import javax.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import java.io.IOException; import java.io.IOException;
import java.io.PrintWriter; import java.io.PrintWriter;
import java.util.Set; import java.util.Set;

View File

@ -11,7 +11,7 @@ import org.jeecg.common.util.SpringContextUtils;
import org.jeecg.config.mybatis.ThreadLocalDataHelper; import org.jeecg.config.mybatis.ThreadLocalDataHelper;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import jakarta.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method; import java.lang.reflect.Method;
/** /**

View File

@ -6,8 +6,8 @@ import org.apache.commons.lang3.StringUtils;
import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.ModelAndView;
import jakarta.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
/** /**
* 动态数据源切换拦截器 * 动态数据源切换拦截器

View File

@ -1,6 +1,5 @@
package org.jeecg.config.oss; package org.jeecg.config.oss;
import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.constant.CommonConstant; import org.jeecg.common.constant.CommonConstant;
import org.jeecg.common.constant.SymbolConstant; import org.jeecg.common.constant.SymbolConstant;
@ -9,11 +8,13 @@ import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
/** /**
* Minio文件上传配置文件 * Minio文件上传配置文件
* @author: jeecg-boot * @author: jeecg-boot
*/ */
@Lazy(false)
@Slf4j @Slf4j
@Configuration @Configuration
@ConditionalOnProperty(prefix = "jeecg.minio", name = "minio_url") @ConditionalOnProperty(prefix = "jeecg.minio", name = "minio_url")
@ -27,7 +28,7 @@ public class MinioConfig {
@Value(value = "${jeecg.minio.bucketName}") @Value(value = "${jeecg.minio.bucketName}")
private String bucketName; private String bucketName;
@PostConstruct @Bean
public void initMinio(){ public void initMinio(){
if(!minioUrl.startsWith(CommonConstant.STR_HTTP)){ if(!minioUrl.startsWith(CommonConstant.STR_HTTP)){
minioUrl = "http://" + minioUrl; minioUrl = "http://" + minioUrl;

View File

@ -1,16 +1,17 @@
package org.jeecg.config.oss; package org.jeecg.config.oss;
import jakarta.annotation.PostConstruct;
import org.jeecg.common.util.oss.OssBootUtil; import org.jeecg.common.util.oss.OssBootUtil;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
/** /**
* 云存储 配置 * 云存储 配置
* @author: jeecg-boot * @author: jeecg-boot
*/ */
@Lazy(false)
@Configuration @Configuration
@ConditionalOnProperty(prefix = "jeecg.oss", name = "endpoint") @ConditionalOnProperty(prefix = "jeecg.oss", name = "endpoint")
public class OssConfiguration { public class OssConfiguration {
@ -27,7 +28,7 @@ public class OssConfiguration {
private String staticDomain; private String staticDomain;
@PostConstruct @Bean
public void initOssBootConfiguration() { public void initOssBootConfiguration() {
OssBootUtil.setEndPoint(endpoint); OssBootUtil.setEndPoint(endpoint);
OssBootUtil.setAccessKeyId(accessKeyId); OssBootUtil.setAccessKeyId(accessKeyId);

View File

@ -1,8 +1,5 @@
package org.jeecg.config.shiro; package org.jeecg.config.shiro;
import jakarta.annotation.Resource;
import jakarta.servlet.DispatcherType;
import jakarta.servlet.Filter;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig; import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import org.apache.shiro.mgt.DefaultSessionStorageEvaluator; import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;
@ -11,7 +8,6 @@ import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.LifecycleBeanPostProcessor; import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor; import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean; import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.spring.web.ShiroUrlPathHelper;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager; import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.crazycake.shiro.*; import org.crazycake.shiro.*;
import org.jeecg.common.constant.CommonConstant; import org.jeecg.common.constant.CommonConstant;
@ -21,20 +17,25 @@ import org.jeecg.config.shiro.filters.CustomShiroFilterFactoryBean;
import org.jeecg.config.shiro.filters.JwtFilter; import org.jeecg.config.shiro.filters.JwtFilter;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator; import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.*; import org.springframework.context.annotation.*;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.env.Environment; import org.springframework.core.env.Environment;
import org.springframework.core.type.filter.AnnotationTypeFilter;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
import org.springframework.util.CollectionUtils; import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.filter.DelegatingFilterProxy; import org.springframework.web.filter.DelegatingFilterProxy;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import redis.clients.jedis.HostAndPort; import redis.clients.jedis.HostAndPort;
import redis.clients.jedis.JedisCluster; import redis.clients.jedis.JedisCluster;
import javax.annotation.Resource;
import javax.servlet.DispatcherType;
import javax.servlet.Filter;
import java.lang.reflect.Method;
import java.util.*; import java.util.*;
/** /**
@ -45,7 +46,6 @@ import java.util.*;
@Slf4j @Slf4j
@Configuration @Configuration
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public class ShiroConfig { public class ShiroConfig {
@Resource @Resource
@ -126,6 +126,7 @@ public class ShiroConfig {
filterChainDefinitionMap.put("/**/*.ttf", "anon"); filterChainDefinitionMap.put("/**/*.ttf", "anon");
filterChainDefinitionMap.put("/**/*.woff", "anon"); filterChainDefinitionMap.put("/**/*.woff", "anon");
filterChainDefinitionMap.put("/**/*.woff2", "anon"); filterChainDefinitionMap.put("/**/*.woff2", "anon");
filterChainDefinitionMap.put("/**/*.glb", "anon"); filterChainDefinitionMap.put("/**/*.glb", "anon");
filterChainDefinitionMap.put("/**/*.wasm", "anon"); filterChainDefinitionMap.put("/**/*.wasm", "anon");
//update-end--Author:scott Date:20221116 for排除静态资源后缀 //update-end--Author:scott Date:20221116 for排除静态资源后缀
@ -157,7 +158,7 @@ public class ShiroConfig {
filterChainDefinitionMap.put("/drag/onlDragDatasetHead/getTotalDataByCompId", "anon"); filterChainDefinitionMap.put("/drag/onlDragDatasetHead/getTotalDataByCompId", "anon");
filterChainDefinitionMap.put("/drag/mock/json/**", "anon"); filterChainDefinitionMap.put("/drag/mock/json/**", "anon");
filterChainDefinitionMap.put("/drag/onlDragDatasetHead/getDictByCodes", "anon"); filterChainDefinitionMap.put("/drag/onlDragDatasetHead/getDictByCodes", "anon");
filterChainDefinitionMap.put("/drag/onlDragDatasetHead/queryAllById", "anon");
filterChainDefinitionMap.put("/jimubi/view", "anon"); filterChainDefinitionMap.put("/jimubi/view", "anon");
filterChainDefinitionMap.put("/jimubi/share/view/**", "anon"); filterChainDefinitionMap.put("/jimubi/share/view/**", "anon");
@ -177,6 +178,8 @@ public class ShiroConfig {
filterChainDefinitionMap.put("/sys/version/app3version", "anon"); filterChainDefinitionMap.put("/sys/version/app3version", "anon");
//仪表盘(按钮通信) //仪表盘(按钮通信)
filterChainDefinitionMap.put("/dragChannelSocket/**","anon"); filterChainDefinitionMap.put("/dragChannelSocket/**","anon");
//App vue3版本查询版本接口
filterChainDefinitionMap.put("/sys/version/app3version", "anon");
//性能监控——安全隐患泄露TOEKNdurid连接池也有 //性能监控——安全隐患泄露TOEKNdurid连接池也有
//filterChainDefinitionMap.put("/actuator/**", "anon"); //filterChainDefinitionMap.put("/actuator/**", "anon");
@ -228,6 +231,7 @@ public class ShiroConfig {
registration.addUrlPatterns("/airag/chat/send"); registration.addUrlPatterns("/airag/chat/send");
registration.addUrlPatterns("/airag/app/debug"); registration.addUrlPatterns("/airag/app/debug");
registration.addUrlPatterns("/airag/app/prompt/generate"); registration.addUrlPatterns("/airag/app/prompt/generate");
registration.addUrlPatterns("/airag/chat/receive/**");
//支持异步 //支持异步
registration.setAsyncSupported(true); registration.setAsyncSupported(true);
registration.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ASYNC); registration.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ASYNC);
@ -360,18 +364,6 @@ public class ShiroConfig {
return manager; return manager;
} }
/**
* 解决 ShiroRequestMappingConfig 获取 requestMappingHandlerMapping Bean 冲突
* spring-boot-autoconfigure:3.4.5 和 spring-boot-actuator-autoconfigure:3.4.5
*/
@Primary
@Bean
public RequestMappingHandlerMapping overridedRequestMappingHandlerMapping() {
RequestMappingHandlerMapping mapping = new RequestMappingHandlerMapping();
mapping.setUrlPathHelper(new ShiroUrlPathHelper());
return mapping;
}
private List<String> rebuildUrl(String[] bases, String[] uris) { private List<String> rebuildUrl(String[] bases, String[] uris) {
List<String> urls = new ArrayList<>(); List<String> urls = new ArrayList<>();
for (String base : bases) { for (String base : bases) {

View File

@ -20,13 +20,11 @@ import org.jeecg.common.util.SpringContextUtils;
import org.jeecg.common.util.TokenUtils; import org.jeecg.common.util.TokenUtils;
import org.jeecg.common.util.oConvertUtils; import org.jeecg.common.util.oConvertUtils;
import org.jeecg.config.mybatis.MybatisPlusSaasConfig; import org.jeecg.config.mybatis.MybatisPlusSaasConfig;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.springframework.context.annotation.Role;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import jakarta.annotation.Resource; import javax.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import java.util.Set; import java.util.Set;
/** /**
@ -37,7 +35,6 @@ import java.util.Set;
*/ */
@Component @Component
@Slf4j @Slf4j
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public class ShiroRealm extends AuthorizingRealm { public class ShiroRealm extends AuthorizingRealm {
@Lazy @Lazy
@Resource @Resource
@ -109,8 +106,8 @@ public class ShiroRealm extends AuthorizingRealm {
try { try {
loginUser = this.checkUserTokenIsEffect(token); loginUser = this.checkUserTokenIsEffect(token);
} catch (AuthenticationException e) { } catch (AuthenticationException e) {
log.error("—————校验 check token 失败——————————"+ e.getMessage(), e);
JwtUtil.responseError(SpringContextUtils.getHttpServletResponse(),401,e.getMessage()); JwtUtil.responseError(SpringContextUtils.getHttpServletResponse(),401,e.getMessage());
e.printStackTrace();
return null; return null;
} }
return new SimpleAuthenticationInfo(loginUser, token, getName()); return new SimpleAuthenticationInfo(loginUser, token, getName());
@ -125,7 +122,7 @@ public class ShiroRealm extends AuthorizingRealm {
// 解密获得username用于和数据库进行对比 // 解密获得username用于和数据库进行对比
String username = JwtUtil.getUsername(token); String username = JwtUtil.getUsername(token);
if (username == null) { if (username == null) {
throw new AuthenticationException("token非法无效!"); throw new AuthenticationException("Token非法无效!");
} }
// 查询用户信息 // 查询用户信息

View File

@ -12,7 +12,7 @@ import org.apache.shiro.web.servlet.AbstractShiroFilter;
import org.apache.shiro.mgt.SecurityManager; import org.apache.shiro.mgt.SecurityManager;
import org.springframework.beans.factory.BeanInitializationException; import org.springframework.beans.factory.BeanInitializationException;
import jakarta.servlet.Filter; import javax.servlet.Filter;
import java.util.Map; import java.util.Map;
/** /**

View File

@ -13,10 +13,10 @@ import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestMethod;
import jakarta.servlet.ServletRequest; import javax.servlet.ServletRequest;
import jakarta.servlet.ServletResponse; import javax.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
/** /**
* @Description: 鉴权登录拦截器 * @Description: 鉴权登录拦截器
@ -56,7 +56,7 @@ public class JwtFilter extends BasicHttpAuthenticationFilter {
executeLogin(request, response); executeLogin(request, response);
return true; return true;
} catch (Exception e) { } catch (Exception e) {
JwtUtil.responseError(response,401,CommonConstant.TOKEN_IS_INVALID_MSG); JwtUtil.responseError((HttpServletResponse)response,401,CommonConstant.TOKEN_IS_INVALID_MSG);
return false; return false;
//throw new AuthenticationException("Token失效请重新登录", e); //throw new AuthenticationException("Token失效请重新登录", e);
} }

View File

@ -1,9 +1,9 @@
package org.jeecg.config.shiro.filters; package org.jeecg.config.shiro.filters;
import jakarta.servlet.ServletRequest; import javax.servlet.ServletRequest;
import jakarta.servlet.ServletResponse; import javax.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import org.apache.shiro.subject.Subject; import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.AccessControlFilter; import org.apache.shiro.web.filter.AccessControlFilter;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;

View File

@ -10,7 +10,7 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import jakarta.annotation.Resource; import javax.annotation.Resource;
/** /**
* 签名 拦截器配置 * 签名 拦截器配置

View File

@ -4,8 +4,8 @@ package org.jeecg.config.sign.interceptor;
import java.io.PrintWriter; import java.io.PrintWriter;
import java.util.SortedMap; import java.util.SortedMap;
import jakarta.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import org.jeecg.common.api.vo.Result; import org.jeecg.common.api.vo.Result;
import org.jeecg.common.constant.CommonConstant; import org.jeecg.common.constant.CommonConstant;

View File

@ -1,10 +1,10 @@
package org.jeecg.config.sign.util; package org.jeecg.config.sign.util;
import jakarta.servlet.ReadListener; import javax.servlet.ReadListener;
import jakarta.servlet.ServletInputStream; import javax.servlet.ServletInputStream;
import jakarta.servlet.ServletRequest; import javax.servlet.ServletRequest;
import jakarta.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletRequestWrapper; import javax.servlet.http.HttpServletRequestWrapper;
import java.io.*; import java.io.*;
import java.nio.charset.Charset; import java.nio.charset.Charset;

View File

@ -10,7 +10,7 @@ import java.util.Map;
import java.util.SortedMap; import java.util.SortedMap;
import java.util.TreeMap; import java.util.TreeMap;
import jakarta.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.constant.SymbolConstant; import org.jeecg.common.constant.SymbolConstant;

View File

@ -14,8 +14,8 @@ import org.jeecg.common.util.SpringContextUtils;
import org.jeecg.common.util.oConvertUtils; import org.jeecg.common.util.oConvertUtils;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import jakarta.annotation.Resource; import javax.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import java.util.*; import java.util.*;
/** /**

View File

@ -6,7 +6,7 @@
<parent> <parent>
<groupId>org.jeecgframework.boot</groupId> <groupId>org.jeecgframework.boot</groupId>
<artifactId>jeecg-boot-module</artifactId> <artifactId>jeecg-boot-module</artifactId>
<version>3.8.2</version> <version>3.8.3</version>
</parent> </parent>
<artifactId>jeecg-boot-module-airag</artifactId> <artifactId>jeecg-boot-module-airag</artifactId>
@ -31,8 +31,6 @@
</repositories> </repositories>
<properties> <properties>
<kotlin.version>1.6.21</kotlin.version>
<liteflow.version>2.12.4.1</liteflow.version>
<langchain4j.version>0.35.0</langchain4j.version> <langchain4j.version>0.35.0</langchain4j.version>
<apache-tika.version>2.9.1</apache-tika.version> <apache-tika.version>2.9.1</apache-tika.version>
</properties> </properties>
@ -55,9 +53,9 @@
<!-- aiflow依赖 --> <!-- aiflow依赖 -->
<dependency> <dependency>
<groupId>org.jeecgframework.boot3</groupId> <groupId>org.jeecgframework.boot</groupId>
<artifactId>jeecg-aiflow</artifactId> <artifactId>jeecg-aiflow</artifactId>
<version>1.1.1</version> <version>1.2.0</version>
</dependency> </dependency>
<!-- beigin 这两个依赖太多每个包50M左右如果你发布需要使用请把<scope>provided</scope>删掉 --> <!-- beigin 这两个依赖太多每个包50M左右如果你发布需要使用请把<scope>provided</scope>删掉 -->
@ -153,7 +151,7 @@
<dependency> <dependency>
<groupId>org.jeecgframework</groupId> <groupId>org.jeecgframework</groupId>
<artifactId>langchain4j-pgvector</artifactId> <artifactId>langchain4j-pgvector</artifactId>
<version>0.35.0</version> <version>${langchain4j.version}</version>
</dependency> </dependency>
<!-- langChain4j Document Parser --> <!-- langChain4j Document Parser -->
<dependency> <dependency>

View File

@ -21,8 +21,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import jakarta.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import java.util.Arrays;
/** /**
* @Description: AI应用 * @Description: AI应用

View File

@ -1,7 +1,5 @@
package org.jeecg.modules.airag.app.controller; package org.jeecg.modules.airag.app.controller;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.api.vo.Result; import org.jeecg.common.api.vo.Result;
import org.jeecg.common.constant.CommonConstant; import org.jeecg.common.constant.CommonConstant;
@ -17,6 +15,8 @@ import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.MultipartHttpServletRequest; import org.springframework.web.multipart.MultipartHttpServletRequest;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/** /**
@ -156,6 +156,20 @@ public class AiragChatController {
return chatService.clearMessage(conversationId); return chatService.clearMessage(conversationId);
} }
/**
* 继续接收消息
*
* @param requestId
* @return
* @author chenrui
* @date 2025/8/11 17:49
*/
@IgnoreAuth
@GetMapping(value = "/receive/{requestId}")
public SseEmitter receiveByRequestId(@PathVariable(name = "requestId", required = true) String requestId) {
return chatService.receiveByRequestId(requestId);
}
/** /**
* 根据请求ID停止某个请求的处理 * 根据请求ID停止某个请求的处理

View File

@ -102,4 +102,13 @@ public interface IAiragChatService {
* @date 2025/4/21 14:17 * @date 2025/4/21 14:17
*/ */
Result<?> initChat(String appId); Result<?> initChat(String appId);
/**
* 继续接收消息
* @param requestId
* @return
* @author chenrui
* @date 2025/8/11 17:39
*/
SseEmitter receiveByRequestId(String requestId);
} }

View File

@ -8,13 +8,13 @@ import dev.langchain4j.service.TokenStream;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.api.vo.Result; import org.jeecg.common.api.vo.Result;
import org.jeecg.common.exception.JeecgBootBizTipException; import org.jeecg.common.exception.JeecgBootBizTipException;
import org.jeecg.common.exception.JeecgBootException;
import org.jeecg.common.system.api.ISysBaseAPI; import org.jeecg.common.system.api.ISysBaseAPI;
import org.jeecg.common.system.util.JwtUtil; import org.jeecg.common.system.util.JwtUtil;
import org.jeecg.common.util.*; import org.jeecg.common.util.*;
import org.jeecg.modules.airag.app.consts.AiAppConsts; import org.jeecg.modules.airag.app.consts.AiAppConsts;
import org.jeecg.modules.airag.app.entity.AiragApp; import org.jeecg.modules.airag.app.entity.AiragApp;
import org.jeecg.modules.airag.app.mapper.AiragAppMapper; import org.jeecg.modules.airag.app.mapper.AiragAppMapper;
import org.jeecg.modules.airag.app.service.IAiragAppService;
import org.jeecg.modules.airag.app.service.IAiragChatService; import org.jeecg.modules.airag.app.service.IAiragChatService;
import org.jeecg.modules.airag.app.vo.AppDebugParams; import org.jeecg.modules.airag.app.vo.AppDebugParams;
import org.jeecg.modules.airag.app.vo.ChatConversation; import org.jeecg.modules.airag.app.vo.ChatConversation;
@ -31,6 +31,8 @@ import org.jeecg.modules.airag.flow.consts.FlowConsts;
import org.jeecg.modules.airag.flow.service.IAiragFlowService; import org.jeecg.modules.airag.flow.service.IAiragFlowService;
import org.jeecg.modules.airag.flow.vo.api.FlowRunParams; import org.jeecg.modules.airag.flow.vo.api.FlowRunParams;
import org.jeecg.modules.airag.llm.entity.AiragModel; import org.jeecg.modules.airag.llm.entity.AiragModel;
import org.jeecg.modules.airag.llm.handler.AIChatHandler;
import org.jeecg.modules.airag.llm.handler.JeecgToolsProvider;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.BoundValueOperations; import org.springframework.data.redis.core.BoundValueOperations;
@ -38,11 +40,10 @@ import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import jakarta.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import java.io.IOException; import java.io.IOException;
import java.util.*; import java.util.*;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.*;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -74,6 +75,14 @@ public class AiragChatServiceImpl implements IAiragChatService {
@Autowired @Autowired
private RedisUtil redisUtil; private RedisUtil redisUtil;
@Autowired
JeecgToolsProvider jeecgToolsProvider;
/**
* 重新接收消息
*/
private static final ExecutorService SSE_THREAD_POOL = Executors.newFixedThreadPool(10); // 最大10个线程
@Override @Override
public SseEmitter send(ChatSendParams chatSendParams) { public SseEmitter send(ChatSendParams chatSendParams) {
AssertUtils.assertNotEmpty("参数异常", chatSendParams); AssertUtils.assertNotEmpty("参数异常", chatSendParams);
@ -148,7 +157,9 @@ public class AiragChatServiceImpl implements IAiragChatService {
// 发送完成事件 // 发送完成事件
emitter.send(SseEmitter.event().data(eventData)); emitter.send(SseEmitter.event().data(eventData));
} catch (Exception e) { } catch (Exception e) {
log.error("终止会话时发生错误", e); if(!e.getMessage().contains("ResponseBodyEmitter has already completed")){
log.error("终止会话时发生错误", e);
}
try { try {
// 防止异常冒泡 // 防止异常冒泡
emitter.completeWithError(e); emitter.completeWithError(e);
@ -250,6 +261,96 @@ public class AiragChatServiceImpl implements IAiragChatService {
return Result.ok(app); return Result.ok(app);
} }
@Override
public SseEmitter receiveByRequestId(String requestId) {
AssertUtils.assertNotEmpty("请选择会话",requestId);
if(AiragLocalCache.get(AiragConsts.CACHE_TYPE_SSE, requestId) == null){
return null;
}
List<EventData> datas = AiragLocalCache.get(AiragConsts.CACHE_TYPE_SSE_HISTORY_MSG, requestId);
if(null == datas){
return null;
}
SseEmitter emitter = createSSE(requestId);
// 120秒
final long timeoutMillis = 120_000L;
// 使用线程池提交任务
SSE_THREAD_POOL.submit(() -> {
int lastIndex = 0;
long lastActiveTime = System.currentTimeMillis();
try {
while (true) {
if(lastIndex < datas.size()) {
try {
EventData eventData = datas.get(lastIndex++);
String eventStr = JSONObject.toJSONString(eventData);
log.debug("[AI应用]继续接收-接收LLM返回消息:{}", eventStr);
emitter.send(SseEmitter.event().data(eventStr));
// 有新消息,重置计时
lastActiveTime = System.currentTimeMillis();
} catch (IOException e) {
log.error("[AI应用]继续接收-发送消息失败");
}
} else {
// 没有新消息了
if (AiragLocalCache.get(AiragConsts.CACHE_TYPE_SSE, requestId) == null) {
// 主线程sse已经被移除,退出线程.
log.info("[AI应用]继续接收-SSE消息推送完成: {}", requestId);
break;
} else if (System.currentTimeMillis() - lastActiveTime > timeoutMillis) {
// 主线程未结束,等待超时,
log.warn("[AI应用]继续接收-等待消息更新超时,释放线程: {}", requestId);
break;
} else {
// 主线程未结束, 未超时, 休眠一会再查
log.warn("[AI应用]继续接收-等待消息更新: {}", requestId);
Thread.sleep(500);
}
}
}
} catch (Exception e) {
log.error("SSE消息推送异常", e);
} finally {
try {
// 发送完成事件
emitter.send(SseEmitter.event().data(new EventData(requestId, null, EventData.EVENT_MESSAGE_END)));
} catch (Exception e) {
log.error("终止会话时发生错误", e);
try {
// 防止异常冒泡
emitter.completeWithError(e);
} catch (Exception ignore) {}
} finally {
// 关闭emitter
try {
emitter.complete();
} catch (Exception ignore) {}
}
}
});
return emitter;
}
/**
* 创建SSE
* @param requestId
* @return
* @author chenrui
* @date 2025/8/12 15:30
*/
private static SseEmitter createSSE(String requestId) {
SseEmitter emitter = new SseEmitter(-0L);
emitter.onError(throwable -> {
log.warn("SEE向客户端发送消息失败: {}", throwable.getMessage());
AiragLocalCache.remove(AiragConsts.CACHE_TYPE_SSE, requestId);
AiragLocalCache.remove(AiragConsts.CACHE_TYPE_SSE_SEND_TIME, requestId);
try {
emitter.complete();
} catch (Exception ignore) {}
});
return emitter;
}
@Override @Override
public Result<?> deleteConversation(String conversationId) { public Result<?> deleteConversation(String conversationId) {
AssertUtils.assertNotEmpty("请选择要删除的会话", conversationId); AssertUtils.assertNotEmpty("请选择要删除的会话", conversationId);
@ -522,22 +623,14 @@ public class AiragChatServiceImpl implements IAiragChatService {
AiragApp aiApp = chatConversation.getApp(); AiragApp aiApp = chatConversation.getApp();
// 每次会话都生成一个新的,用来缓存emitter // 每次会话都生成一个新的,用来缓存emitter
String requestId = UUIDGenerator.generate(); String requestId = UUIDGenerator.generate();
SseEmitter emitter = new SseEmitter(-0L); SseEmitter emitter = createSSE(requestId);
emitter.onError(throwable -> {
log.warn("SEE向客户端发送消息失败: {}", throwable.getMessage());
AiragLocalCache.remove(AiragConsts.CACHE_TYPE_SSE, requestId);
try {
emitter.complete();
} catch (Exception ignore) {}
});
EventData eventRequestId = new EventData(requestId, null, EventData.EVENT_INIT_REQUEST_ID, chatConversation.getId(), topicId);
eventRequestId.setData(EventMessageData.builder().message("").build());
sendMessage2Client(emitter, eventRequestId);
// 缓存emitter // 缓存emitter
AiragLocalCache.put(AiragConsts.CACHE_TYPE_SSE, requestId, emitter); AiragLocalCache.put(AiragConsts.CACHE_TYPE_SSE, requestId, emitter);
// 缓存开始发送时间 // 缓存开始发送时间
log.info("[AI-CHAT]开始发送消息,requestId:{}", requestId); log.info("[AI-CHAT]开始发送消息,requestId:{}", requestId);
AiragLocalCache.put(AiragConsts.CACHE_TYPE_SSE_SEND_TIME, requestId, System.currentTimeMillis()); AiragLocalCache.put(AiragConsts.CACHE_TYPE_SSE_SEND_TIME, requestId, System.currentTimeMillis());
// 初始化历史消息缓存
AiragLocalCache.put(AiragConsts.CACHE_TYPE_SSE_HISTORY_MSG, requestId, new CopyOnWriteArrayList<>());
try { try {
// 组装用户消息 // 组装用户消息
UserMessage userMessage = aiChatHandler.buildUserMessage(sendParams.getContent(), sendParams.getImages()); UserMessage userMessage = aiChatHandler.buildUserMessage(sendParams.getContent(), sendParams.getImages());
@ -561,6 +654,10 @@ public class AiragChatServiceImpl implements IAiragChatService {
// 发消息 // 发消息
sendWithDefault(requestId, chatConversation, topicId, null, messages, null); sendWithDefault(requestId, chatConversation, topicId, null, messages, null);
} }
// 发送就绪消息
EventData eventRequestId = new EventData(requestId, null, EventData.EVENT_INIT_REQUEST_ID, chatConversation.getId(), topicId);
eventRequestId.setData(EventMessageData.builder().message("").build());
sendMessage2Client(emitter, eventRequestId);
} catch (Throwable e) { } catch (Throwable e) {
log.error(e.getMessage(), e); log.error(e.getMessage(), e);
EventData eventData = new EventData(requestId, null, EventData.EVENT_FLOW_ERROR, chatConversation.getId(), topicId); EventData eventData = new EventData(requestId, null, EventData.EVENT_FLOW_ERROR, chatConversation.getId(), topicId);
@ -725,6 +822,10 @@ public class AiragChatServiceImpl implements IAiragChatService {
if (null == aiChatParams) { if (null == aiChatParams) {
aiChatParams = new AIChatParams(); aiChatParams = new AIChatParams();
} }
// 如果是默认app,加载系统默认工具
if(chatConversation.getApp().getId().equals(AiAppConsts.DEFAULT_APP_ID)){
aiChatParams.setTools(jeecgToolsProvider.getDefaultTools());
}
aiChatParams.setKnowIds(chatConversation.getApp().getKnowIds()); aiChatParams.setKnowIds(chatConversation.getApp().getKnowIds());
aiChatParams.setMaxMsgNumber(oConvertUtils.getInt(chatConversation.getApp().getMsgNum(), 5)); aiChatParams.setMaxMsgNumber(oConvertUtils.getInt(chatConversation.getApp().getMsgNum(), 5));
HttpServletRequest httpRequest = SpringContextUtils.getHttpServletRequest(); HttpServletRequest httpRequest = SpringContextUtils.getHttpServletRequest();
@ -739,6 +840,19 @@ public class AiragChatServiceImpl implements IAiragChatService {
} }
} catch (Exception e) { } catch (Exception e) {
log.error(e.getMessage(), e); log.error(e.getMessage(), e);
// sse
SseEmitter emitter = AiragLocalCache.get(AiragConsts.CACHE_TYPE_SSE, requestId);
if (null == emitter) {
log.warn("[AI应用]接收LLM返回会话已关闭{}", requestId);
return;
}
String errMsg = "调用大模型接口失败,详情请查看后台日志。";
if(e instanceof JeecgBootException){
errMsg = e.getMessage();
}
EventData eventData = new EventData(requestId, null, EventData.EVENT_FLOW_ERROR, chatConversation.getId(), topicId);
eventData.setData(EventFlowData.builder().success(false).message(errMsg).build());
closeSSE(emitter, eventData);
throw new JeecgBootBizTipException("调用大模型接口失败:" + e.getMessage()); throw new JeecgBootBizTipException("调用大模型接口失败:" + e.getMessage());
} }
/** /**
@ -808,7 +922,7 @@ public class AiragChatServiceImpl implements IAiragChatService {
// 异常结束 // 异常结束
log.error("调用模型异常:" + respText); log.error("调用模型异常:" + respText);
if (respText.contains("insufficient Balance")) { if (respText.contains("insufficient Balance")) {
respText = "言模型账号余额不足!"; respText = "言模型账号余额不足!";
} }
EventData eventData = new EventData(requestId, null, EventData.EVENT_FLOW_ERROR, chatConversation.getId(), topicId); EventData eventData = new EventData(requestId, null, EventData.EVENT_FLOW_ERROR, chatConversation.getId(), topicId);
eventData.setData(EventFlowData.builder().success(false).message(respText).build()); eventData.setData(EventFlowData.builder().success(false).message(respText).build());
@ -837,6 +951,14 @@ public class AiragChatServiceImpl implements IAiragChatService {
//update-end---author:chenrui ---date:20250425 for[QQYUN-12203]AI 聊天,超时或者服务器报错,给个友好提示------------ //update-end---author:chenrui ---date:20250425 for[QQYUN-12203]AI 聊天,超时或者服务器报错,给个友好提示------------
} else { } else {
errMsg = "调用大模型接口失败,详情请查看后台日志。"; errMsg = "调用大模型接口失败,详情请查看后台日志。";
// 根据常见异常关键字做细致翻译
for (Map.Entry<String, String> entry : AIChatHandler.MODEL_ERROR_MAP.entrySet()) {
String key = entry.getKey();
String value = entry.getValue();
if (error.getMessage().contains(key)) {
errMsg = value;
}
}
EventData eventData = new EventData(requestId, null, EventData.EVENT_FLOW_ERROR, chatConversation.getId(), topicId); EventData eventData = new EventData(requestId, null, EventData.EVENT_FLOW_ERROR, chatConversation.getId(), topicId);
eventData.setData(EventFlowData.builder().success(false).message(errMsg).build()); eventData.setData(EventFlowData.builder().success(false).message(errMsg).build());
closeSSE(emitter, eventData); closeSSE(emitter, eventData);
@ -858,6 +980,12 @@ public class AiragChatServiceImpl implements IAiragChatService {
String eventStr = JSONObject.toJSONString(eventData); String eventStr = JSONObject.toJSONString(eventData);
log.debug("[AI应用]接收LLM返回消息:{}", eventStr); log.debug("[AI应用]接收LLM返回消息:{}", eventStr);
emitter.send(SseEmitter.event().data(eventStr)); emitter.send(SseEmitter.event().data(eventStr));
List<EventData> historyMsg = AiragLocalCache.get(AiragConsts.CACHE_TYPE_SSE_HISTORY_MSG, eventData.getRequestId());
if (null == historyMsg) {
historyMsg = new CopyOnWriteArrayList<>();
AiragLocalCache.put(AiragConsts.CACHE_TYPE_SSE_HISTORY_MSG, eventData.getRequestId(), historyMsg);
}
historyMsg.add(eventData);
} catch (IOException e) { } catch (IOException e) {
log.error("发送消息失败", e); log.error("发送消息失败", e);
} }

View File

@ -22,7 +22,7 @@ import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import jakarta.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;

View File

@ -15,6 +15,7 @@ import org.jeecg.common.system.base.controller.JeecgController;
import org.jeecg.common.system.query.QueryGenerator; import org.jeecg.common.system.query.QueryGenerator;
import org.jeecg.common.util.AssertUtils; import org.jeecg.common.util.AssertUtils;
import org.jeecg.common.util.TokenUtils; import org.jeecg.common.util.TokenUtils;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.config.mybatis.MybatisPlusSaasConfig; import org.jeecg.config.mybatis.MybatisPlusSaasConfig;
import org.jeecg.modules.airag.llm.consts.LLMConsts; import org.jeecg.modules.airag.llm.consts.LLMConsts;
import org.jeecg.modules.airag.llm.entity.AiragModel; import org.jeecg.modules.airag.llm.entity.AiragModel;
@ -25,8 +26,8 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.ModelAndView;
import jakarta.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
@ -77,6 +78,11 @@ public class AiragModelController extends JeecgController<AiragModel, IAiragMode
AssertUtils.assertNotEmpty("模型名称不能为空", airagModel.getName()); AssertUtils.assertNotEmpty("模型名称不能为空", airagModel.getName());
AssertUtils.assertNotEmpty("模型类型不能为空", airagModel.getModelType()); AssertUtils.assertNotEmpty("模型类型不能为空", airagModel.getModelType());
AssertUtils.assertNotEmpty("基础模型不能为空", airagModel.getModelName()); AssertUtils.assertNotEmpty("基础模型不能为空", airagModel.getModelName());
// 默认未激活
if(oConvertUtils.isObjectEmpty(airagModel.getActivateFlag())){
airagModel.setActivateFlag(0);
}
airagModel.setActivateFlag(0);
airagModelService.save(airagModel); airagModelService.save(airagModel);
return Result.OK("添加成功!"); return Result.OK("添加成功!");
} }
@ -164,7 +170,7 @@ public class AiragModelController extends JeecgController<AiragModel, IAiragMode
AssertUtils.assertNotEmpty("基础模型不能为空", airagModel.getModelName()); AssertUtils.assertNotEmpty("基础模型不能为空", airagModel.getModelName());
try { try {
if(LLMConsts.MODEL_TYPE_LLM.equals(airagModel.getModelType())){ if(LLMConsts.MODEL_TYPE_LLM.equals(airagModel.getModelType())){
aiChatHandler.completions(airagModel, Collections.singletonList(UserMessage.from("test connection")), null); aiChatHandler.completions(airagModel, Collections.singletonList(UserMessage.from("To test whether it can be successfully called, simply return success")), null);
}else{ }else{
AiModelOptions aiModelOptions = EmbeddingHandler.buildModelOptions(airagModel); AiModelOptions aiModelOptions = EmbeddingHandler.buildModelOptions(airagModel);
EmbeddingModel embeddingModel = AiModelFactory.createEmbeddingModel(aiModelOptions); EmbeddingModel embeddingModel = AiModelFactory.createEmbeddingModel(aiModelOptions);
@ -172,9 +178,12 @@ public class AiragModelController extends JeecgController<AiragModel, IAiragMode
} }
}catch (Exception e){ }catch (Exception e){
log.error("测试模型连接失败", e); log.error("测试模型连接失败", e);
return Result.error("测试模型连接失败" + e.getMessage()); return Result.error("测试模型连接失败" + e.getMessage());
} }
return Result.OK("测试模型连接成功"); // 测试成功激活数据
airagModel.setActivateFlag(1);
airagModelService.updateById(airagModel);
return Result.OK("");
} }
} }

View File

@ -121,4 +121,11 @@ public class AiragModel implements Serializable {
@Excel(name = "模型参数", width = 15) @Excel(name = "模型参数", width = 15)
@Schema(description = "模型参数") @Schema(description = "模型参数")
private String modelParams; private String modelParams;
/**
* 是否激活(0=未激活,1=已激活)
*/
@Excel(name = "是否激活", width = 15)
@Schema(description = "是否激活")
private Integer activateFlag;
} }

View File

@ -6,6 +6,7 @@ import dev.langchain4j.rag.query.router.QueryRouter;
import dev.langchain4j.service.TokenStream; import dev.langchain4j.service.TokenStream;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.jeecg.ai.handler.LLMHandler; import org.jeecg.ai.handler.LLMHandler;
import org.jeecg.common.exception.JeecgBootException;
import org.jeecg.common.util.AssertUtils; import org.jeecg.common.util.AssertUtils;
import org.jeecg.common.util.oConvertUtils; import org.jeecg.common.util.oConvertUtils;
import org.jeecg.modules.airag.common.handler.AIChatParams; import org.jeecg.modules.airag.common.handler.AIChatParams;
@ -22,9 +23,7 @@ import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.util.ArrayList; import java.util.*;
import java.util.Base64;
import java.util.List;
import java.util.regex.Matcher; import java.util.regex.Matcher;
/** /**
@ -83,6 +82,7 @@ public class AIChatHandler implements IAIChatHandler {
AssertUtils.assertNotEmpty("请选择模型", modelId); AssertUtils.assertNotEmpty("请选择模型", modelId);
AiragModel airagModel = airagModelMapper.getByIdIgnoreTenant(modelId); AiragModel airagModel = airagModelMapper.getByIdIgnoreTenant(modelId);
AssertUtils.assertSame("模型未激活,请先在[AI模型配置]中[测试激活]模型", airagModel.getActivateFlag(), 1);
return completions(airagModel, messages, params); return completions(airagModel, messages, params);
} }
@ -98,7 +98,25 @@ public class AIChatHandler implements IAIChatHandler {
*/ */
public String completions(AiragModel airagModel, List<ChatMessage> messages, AIChatParams params) { public String completions(AiragModel airagModel, List<ChatMessage> messages, AIChatParams params) {
params = mergeParams(airagModel, params); params = mergeParams(airagModel, params);
String resp = llmHandler.completions(messages, params); String resp;
try {
resp = llmHandler.completions(messages, params);
} catch (Exception e) {
// langchain4j 异常友好提示
String errMsg = "调用大模型接口失败,详情请查看后台日志。";
if (oConvertUtils.isNotEmpty(e.getMessage())) {
// // 根据常见异常关键字做细致翻译
// for (Map.Entry<String, String> entry : MODEL_ERROR_MAP.entrySet()) {
// String key = entry.getKey();
// String value = entry.getValue();
// if (errMsg.contains(key)) {
// errMsg = value;
// }
// }
}
log.error("AI模型调用异常: {}", errMsg, e);
throw new JeecgBootException(errMsg);
}
if (resp.contains("</think>") if (resp.contains("</think>")
&& (null == params.getNoThinking() || params.getNoThinking())) { && (null == params.getNoThinking() || params.getNoThinking())) {
String[] thinkSplit = resp.split("</think>"); String[] thinkSplit = resp.split("</think>");
@ -151,6 +169,7 @@ public class AIChatHandler implements IAIChatHandler {
AssertUtils.assertNotEmpty("请选择模型", modelId); AssertUtils.assertNotEmpty("请选择模型", modelId);
AiragModel airagModel = airagModelMapper.getByIdIgnoreTenant(modelId); AiragModel airagModel = airagModelMapper.getByIdIgnoreTenant(modelId);
AssertUtils.assertSame("模型未激活,请先在[AI模型配置]中[测试激活]模型", airagModel.getActivateFlag(), 1);
return chat(airagModel, messages, params); return chat(airagModel, messages, params);
} }

View File

@ -0,0 +1,42 @@
package org.jeecg.modules.airag.llm.handler;
import dev.langchain4j.agent.tool.ToolSpecification;
import dev.langchain4j.service.tool.ToolExecutor;
import lombok.Getter;
import java.util.Map;
/**
* for [QQYUN-13565]【AI助手】新增创建用户和查询用户的工具扩展
* @Description: jeecg llm工具提供者
* @Author: chenrui
* @Date: 2025/8/26 18:06
*/
public interface JeecgToolsProvider {
/**
* 获取默认的工具列表
* @return
* @author chenrui
* @date 2025/8/27 09:49
*/
public Map<ToolSpecification, ToolExecutor> getDefaultTools();
/**
* jeecgLlm工具类
* @author chenrui
* @date 2025/8/27 09:49
*/
@Getter
class JeecgLlmTools{
ToolSpecification toolSpecification;
ToolExecutor toolExecutor;
public JeecgLlmTools(ToolSpecification toolSpecification, ToolExecutor toolExecutor) {
this.toolSpecification = toolSpecification;
this.toolExecutor = toolExecutor;
}
}
}

View File

@ -27,7 +27,7 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import jakarta.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;

View File

@ -5,7 +5,7 @@
<parent> <parent>
<artifactId>jeecg-boot-module</artifactId> <artifactId>jeecg-boot-module</artifactId>
<groupId>org.jeecgframework.boot</groupId> <groupId>org.jeecgframework.boot</groupId>
<version>3.8.2</version> <version>3.8.3</version>
</parent> </parent>
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>

View File

@ -8,7 +8,7 @@ import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import jakarta.annotation.Resource; import javax.annotation.Resource;
/** /**
* 服务端提供方——feign接口 * 服务端提供方——feign接口

View File

@ -6,8 +6,8 @@ import org.apache.commons.io.IOUtils;
import org.jeecg.common.api.vo.Result; import org.jeecg.common.api.vo.Result;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import jakarta.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import javax.swing.filechooser.FileSystemView; import javax.swing.filechooser.FileSystemView;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;

View File

@ -18,7 +18,7 @@ import org.jeecg.modules.demo.mock.vxe.entity.MockEntity;
import org.jeecg.modules.demo.mock.vxe.websocket.VxeSocket; import org.jeecg.modules.demo.mock.vxe.websocket.VxeSocket;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import jakarta.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.net.URLDecoder; import java.net.URLDecoder;

View File

@ -6,12 +6,12 @@ import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.constant.VxeSocketConst; import org.jeecg.common.constant.VxeSocketConst;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import jakarta.websocket.OnClose; import javax.websocket.OnClose;
import jakarta.websocket.OnMessage; import javax.websocket.OnMessage;
import jakarta.websocket.OnOpen; import javax.websocket.OnOpen;
import jakarta.websocket.Session; import javax.websocket.Session;
import jakarta.websocket.server.PathParam; import javax.websocket.server.PathParam;
import jakarta.websocket.server.ServerEndpoint; import javax.websocket.server.ServerEndpoint;
import java.util.Collection; import java.util.Collection;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;

View File

@ -29,8 +29,8 @@ import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.ModelAndView;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import jakarta.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Date; import java.util.Date;
@ -64,7 +64,7 @@ public class JeecgDemoController extends JeecgController<JeecgDemo, IJeecgDemoSe
*/ */
@Operation(summary = "获取Demo数据列表") @Operation(summary = "获取Demo数据列表")
@GetMapping(value = "/list") @GetMapping(value = "/list")
@PermissionData(pageComponent = "jeecg/JeecgDemoList") @PermissionData(pageComponent = "system/examples/demo/index")
public Result<?> list(JeecgDemo jeecgDemo, @RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo, @RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize, public Result<?> list(JeecgDemo jeecgDemo, @RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo, @RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize,
HttpServletRequest req) { HttpServletRequest req) {
QueryWrapper<JeecgDemo> queryWrapper = QueryGenerator.initQueryWrapper(jeecgDemo, req.getParameterMap()); QueryWrapper<JeecgDemo> queryWrapper = QueryGenerator.initQueryWrapper(jeecgDemo, req.getParameterMap());

View File

@ -10,10 +10,7 @@ import org.jeecg.modules.demo.test.entity.JeecgDemo;
import org.jeecg.modules.demo.test.service.IJeecgDemoService; import org.jeecg.modules.demo.test.service.IJeecgDemoService;
import org.jeecg.modules.demo.test.service.IJeecgDynamicDataService; import org.jeecg.modules.demo.test.service.IJeecgDynamicDataService;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.*;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List; import java.util.List;

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