Compare commits

..

1 Commits

Author SHA1 Message Date
83edfa9090 版本发布日期更新 2025-09-24 13:18:16 +08:00
1079 changed files with 176225 additions and 69043 deletions

View File

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

View File

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

2
.gitignore vendored
View File

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

View File

@ -3,6 +3,9 @@ AIGC应用平台介绍
一个全栈式 AI 开发平台,旨在帮助开发者快速构建和部署个性化的 AI 应用。
> JDK说明AI流程编排引擎暂时不支持jdk21所以目前只能使用jdk8或者jdk17启动项目。
JeecgBoot平台的AIGC功能模块是一套类似`Dify``AIGC应用开发平台`+`知识库问答`是一款基于LLM大语言模型AI应用平台和 RAG 的知识库问答系统。
其直观的界面结合了 AI 流程编排、RAG 管道、知识库管理、模型管理、对接向量库、实时运行可观察等让您可以快速从原型到生产拥有AI服务能力。
@ -106,10 +109,6 @@ JeecgBoot平台的AIGC功能模块是一套类似`Dify`的`AIGC应用开发
| ChatGTP | √ |
| Qwq | √ |
| 智库 | √ |
| claude | √ |
| vl模型 | √ |
| 千帆大模型 | √ |
| 通义千问 | √ |
| Ollama本地搭建大模型 | √ |
| 等等。。 | √ |

View File

@ -1,4 +1,4 @@
[中文](./README.md) | English
![JEECG](https://jeecgos.oss-cn-beijing.aliyuncs.com/files/logov3.png "JeecgBoot低代码开发平台")
@ -7,12 +7,12 @@
JEECG BOOT AI Low Code Platform
===============
Current version: 3.9.1 (Release date: 2026-01-28)
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)
[![](https://img.shields.io/badge/Author-guojusoft-orange.svg)](http://www.jeecg.com)
[![](https://img.shields.io/badge/version-3.9.1-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 forks](https://img.shields.io/github/forks/zhangdaiscott/jeecg-boot.svg?style=social&label=Fork)](https://github.com/zhangdaiscott/jeecg-boot)
@ -301,41 +301,6 @@ AI APP: https://help.jeecg.com/aigc
##### PC
##### AI Model and Application Management
![](https://oscimg.oschina.net/oscnet/up-0b1779e923566ccebb2d5a9cc9220c78b4a.png)
![](https://oscimg.oschina.net/oscnet/up-c8956df1d37d66b2d40136afaeca677628b.png)
![](https://oscimg.oschina.net/oscnet/up-8c348eeafd89673ca8cd1a2705014e3ac04.png)
AI Workflow Orchestration
![](https://oscimg.oschina.net/oscnet/up-2343657de2c7ac8010bc471470d084075ae.png)
MCP and Tool Management
![](https://oscimg.oschina.net/oscnet/up-8119d5dbc72e534236a3d042e11534c52ad.png)
AI Knowledge Base (Supports various document formats, with excellent markdown compatibility)
![](https://oscimg.oschina.net/oscnet/up-e2e9c118982ea366ed7f2b9827d4bb46c5d.png)
AI Toolbox
![](https://oscimg.oschina.net/oscnet/up-bf2a808d22a11fd83e577ad74741d97884b.png)
AI Chat Assistant
![](https://oscimg.oschina.net/oscnet/up-2a51accc2ff0b647e0ee058a58d291fe811.png)
![](https://oscimg.oschina.net/oscnet/up-ea1069c2a92a3ab2963d88763016cb037c2.png)
![](https://oscimg.oschina.net/oscnet//65298d5710b4e6039a5f802b5f8505c5.png)
![](https://oscimg.oschina.net/oscnet/up-000530d95df337b43089ac77e562494f454.png)
![输入图片说明](https://static.oschina.net/uploads/img/201904/14155402_AmlV.png "在这里输入图片标题")

124
README-Enterprise.md Normal file
View File

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

197
README.md
View File

@ -1,15 +1,14 @@
中文 | [English](./README.en-US.md)
JeecgBoot AI低代码平台
===============
当前最新版本: 3.9.1发布日期2026-01-28
当前最新版本: 3.8.3发布日期2025-10-09
[![AUR](https://img.shields.io/badge/license-Apache%20License%202.0-blue.svg)](https://github.com/jeecgboot/JeecgBoot/blob/master/LICENSE)
[![](https://img.shields.io/badge/Author-北京国炬软件-orange.svg)](https://jeecg.com)
[![](https://img.shields.io/badge/blog-技术博客-orange.svg)](https://jeecg.blog.csdn.net)
[![](https://img.shields.io/badge/version-3.9.1-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 forks](https://img.shields.io/github/forks/zhangdaiscott/jeecg-boot.svg?style=social&label=Fork)](https://github.com/jeecgboot/JeecgBoot)
@ -20,21 +19,16 @@ JeecgBoot AI低代码平台
<h3 align="center">企业级AI低代码平台</h3>
JeecgBoot 是一款融合代码生成与AI应用的低代码开发平台助力企业快速实现低代码开发和构建AI应用。平台支持MCP和插件扩展提供聊天式业务操作(如“一句话创建用户”),大幅提升开发效率与用户便捷性。
JeecgBoot是一款企业级低代码平台集成了AI应用平台功能旨在帮助开发者快速实现低代码开发和构建、部署个性化的 AI 应用。
前后端分离架构Ant Design4、Vue3SpringBoot2/3SpringCloud AlibabaMybatis-plusShiro/SpringAuthorizationServer强大的代码生成器让前后端代码一键生成无需写任何代码提供强大的报表和大屏工具满足企业级数据产品需求
引领AI低代码开发模式: AI生成->OnlineCoding-> 代码生成-> 手工MERGE 帮助Java项目解决80%的重复工作让开发更多关注业务提高效率、节省成本同时又不失灵活性低代码能力Online表单、表单设计、流程设计、Online报表、大屏/仪表盘设计、报表设计; AI应用平台功能AI知识库问答、AI模型管理、AI流程编排、AI聊天等支持含ChatGPT、DeepSeek、Ollama等多种AI大模型
采用前后端分离架构Ant Design&Vue3SpringBoot3SpringCloud AlibabaMybatis-plus强大代码生成器实现前后端一键生成无需手写代码。
平台引领AI低代码开发模式AI生成→在线编码→代码生成→手工合并解决Java项目80%重复工作,提升效率,节省成本,兼顾灵活性。
具备强大且颗粒化的权限控制支持按钮权限和数据权限设置满足大型业务系统需求。功能涵盖在线表单、表单设计、流程设计、门户设计、报表与大屏设计、OA办公、AI应用、AI知识库、大模型管理、AI流程编排、AI聊天支持ChatGPT、DeepSeek、Ollama等多种AI大模型。
`傻瓜式报表:` JimuReport是一款自主研发的强大开源企业级Web报表工具。它通过零编码的拖拽式操作赋能用户如同搭积木般轻松构建各类复杂报表全面满足企业数据可视化与分析需求助力企业级数据产品的高效打造与应用。
`AI赋能低代码:` 提供一套成熟AI应用平台功能包含AI应用管理、AI模型管理、AI对话助手、AI知识库问答、AI流程编排、AI流程设计器AI建表等功能; 支持各种AI大模型ChatGPT、DeepSeek、Ollama、智普、千问等.
`傻瓜式大屏:` JimuBI一款自主研发的强大的大屏和仪表盘设计工具。专注数字孪生与数据可视化支持交互式大屏、仪表盘、门户和移动端实现“一次开发多端适配”。 大屏设计类Word风格支持多屏切换自由拖拽轻松打造炫酷动态界面。
`JEECG宗旨是:` 简单功能由OnlineCoding零代码搭建做到`零代码开发`复杂功能由代码生成器生成进行手工Merge 实现`低代码开发`,既保证了`智能`又兼顾`灵活`,解决了当前低代码产品普遍不灵活的弊端!
`成熟AI应用功能:` 提供一套完善AI应用平台: 涵盖AI应用管理、AI模型管理、智能对话助手、知识库问答、流程编排与设计器、AI建表、MCP插件配置等功能。平台兼容主流大模型包括ChatGPT、DeepSeek、Ollama、智普、千问等助力企业高效构建智能化应用推动低代码开发与AI深度融合
`JEECG宗旨是:` JEECG旨在通过OnlineCoding平台实现简单功能的零代码快速搭建同时针对复杂功能采用代码生成器生成代码并手工合并打造智能且灵活的低代码开发模式有效解决了当前低代码产品普遍缺乏灵活性的问题提升开发效率的同时兼顾系统的扩展性和定制化能力。
`JEECG业务流程:` JEECG业务流程采用BPM工作流引擎实现业务审批扩展任务接口供开发人员编写业务逻辑表单提供表单设计器、在线配置表单和编码表单等多种解决方案。通过流程与表单的分离设计松耦合及任务节点的灵活配置既保障了企业流程的安全性与保密性又大幅降低了开发人员的工作量。
`JEECG业务流程:` 采用工作流来实现、扩展出任务接口,供开发编写业务逻辑,表单提供多种解决方案: 表单设计器、online配置表单、编码表单。同时实现了流程与表单的分离设计松耦合、并支持任务节点灵活配置既保证了公司流程的保密性又减少了开发人员的工作量
@ -42,8 +36,8 @@ JeecgBoot 是一款融合代码生成与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 内核,因此它们具有良好的兼容性。
@ -54,16 +48,15 @@ JeecgBoot低代码平台兼容所有J2EE项目开发支持信创国产化
版本说明
-----------------------------------
|下载 | SpringBoot3.5 + Shiro |SpringBoot3.5+ SpringAuthorizationServer | SpringBoot3.5 + Sa-Token | SpringBoot2.7(JDK17/JDK8) |
|------|---------------------------------------------------------|----------------------------|-------------------|--------------------------------------------|
| Github | [`main`](https://github.com/jeecgboot/JeecgBoot) | [`springboot3_sas`](https://github.com/jeecgboot/JeecgBoot/tree/springboot3_sas) 分支 | [`springboot3-satoken`](https://github.com/jeecgboot/JeecgBoot/tree/springboot3-satoken) 分支|[`springboot2`](https://github.com/jeecgboot/JeecgBoot/tree/springboot2) 分支|
| Gitee | [`main`](https://github.com/jeecgboot/JeecgBoot) | [`springboot3_sas`](https://gitee.com/jeecg/JeecgBoot/tree/springboot3_sas) 分支| [`springboot3-satoken`](https://gitee.com/jeecg/JeecgBoot/tree/springboot3-satoken) 分支|[`springboot2`](https://github.com/jeecgboot/JeecgBoot/tree/springboot2) 分支 |
|下载 | 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) 分支|
| 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+Shiro+Mybatis+SpringCloudAlibaba(支持单体和微服务切换).
- `jeecg-boot` 是后端JAVA源码项目支持单体和微服务切换.
- `jeecgboot-vue3` 是前端VUE3源码项目vue3+vite6+ts最新技术栈.
- `JeecgUniapp` 是[配套APP框架](https://github.com/jeecgboot/JeecgUniapp) 适配多个终端支持APP、小程序、H5、鸿蒙、鸿蒙Next.
- `jeecg-boot-starter` 是[jeecg-boot对应的底层封装starter](https://github.com/jeecgboot/jeecg-boot-starter) 微服务启动、xxljob、分布式锁starter、rabbitmq、分布式事务、分库分表shardingsphere等.
- 参考 [文档](https://help.jeecg.com/ui/2dev/mini) 可以删除不需要的demo制作一个精简版本
@ -85,8 +78,8 @@ JeecgBoot低代码平台兼容所有J2EE项目开发支持信创国产化
技术文档
-----------------------------------
- 在线演示: [平台演示](https://boot3.jeecg.com) | [APP演示](https://jeecg.com/appIndex)
- 官方网站: [http://www.jeecg.com](http://www.jeecg.com)
- 在线演示: [平台演示](https://boot3.jeecg.com) | [APP演示](https://jeecg.com/appIndex)
- 入门指南: [快速入门](http://www.jeecg.com/doc/quickstart) | [代码生成使用](https://help.jeecg.com/java/codegen/online) | [开发文档](https://help.jeecg.com) | [AI应用手册](https://help.jeecg.com/aigc) | [视频教程](http://jeecg.com/doc/video)
- 技术支持: [反馈问题](https://github.com/jeecgboot/JeecgBoot/issues/new?template=bug_report.md) | [低代码体验一分钟](https://jeecg.blog.csdn.net/article/details/106079007)
- QQ交流群 964611995、⑩716488839(满)、⑨808791225(满)、其他(满)
@ -109,11 +102,8 @@ JeecgBoot平台提供了一套完善的AI应用管理系统模块是一套类
为什么选择JeecgBoot?
-----------------------------------
> 界内首款AI低代码开发平台同时具备AI应用平台和低代码平台通过AI驱动低代码开发
> 开源界"小普元"超越传统商业平台。引领低代码开发模式(OnlineCoding-> 代码生成器 -> 手工MERGE),低代码开发同时又支持灵活编码, 可以帮助解决Java项目70%的重复工作,让开发更多关注业务。既能快速提高开发效率,节省成本,同时又不失灵活性
- 1.提供了一套完善的AI应用管理系统模块是一套类似`Dify``AIGC应用开发平台`+`知识库问答`是一款基于LLM大语言模型AI应用平台和 RAG 的知识库问答系统。 其直观的界面结合了 AI 流程编排、RAG 管道、知识库管理、模型管理、对接向量库、实时运行可观察等让您可以快速从原型到生产拥有AI服务能力
- 2.采用最新主流前后分离框架Spring Boot3 + MyBatisPlus + Vue3.0 + TypeScript + Vite6 + Ant Design Vue4 )等新技术方案。便于学习容易上手,代码生成器依赖性低,灵活的扩展能力,可快速实现二次开发。
- 1.采用最新主流前后分离框架Spring Boot3 + MyBatis + Shiro//SpringAuthorizationServer + Ant Design4 + Vue3容易上手代码生成器依赖性低灵活的扩展能力可快速实现二次开发
- 2.前端大版本换代,最新版采用 Vue3.0 + TypeScript + Vite6 + Ant Design Vue4 等新技术方案
- 3.支持微服务Spring Cloud AlibabaNacos、Gateway、Sentinel、Skywalking提供简易机制支持单体和微服务自由切换这样可以满足各类项目需求
- 4.开发效率高支持在线建表和AI建表提供强大代码生成器单表、树列表、一对多、一对一等数据模型增删改查功能一键生成菜单配置直接使用。
- 5.代码生成器提供强大模板机制,支持自定义模板,目前提供四套风格模板(单表两套、树模型一套、一对多三套)。
@ -159,15 +149,12 @@ JeecgBoot平台提供了一套完善的AI应用管理系统模块是一套类
技术架构
技术架构
-----------------------------------
#### 前端
- 前端环境要求Node.js要求`Node 20+` 版本以上、pnpm 要求`9+` 版本以上
` ( Vite 不再支持已结束生命周期EOL的 Node.js 18。现在需要使用 Node.js 20.19+ 或 22.12+)`
- 依赖管理node、npm、pnpm
- 前端IDE建议IDEA、WebStorm、Vscode
- 采用 Vue3.0+TypeScript+Vite6+Ant-Design-Vue4等新技术方案包括二次封装组件、utils、hooks、动态菜单、权限校验、按钮级别权限控制等功能
@ -177,16 +164,16 @@ JeecgBoot平台提供了一套完善的AI应用管理系统模块是一套类
#### 后端
- IDE建议 IDEA (必须安装lombok插件 )
- 语言Java 默认jdk17(jdk21、jdk24)
- 语言Java 默认jdk17(支持jdk8、jdk21)
- 依赖管理Maven
- 基础框架Spring Boot 3.5.5
- 微服务框架: Spring Cloud Alibaba 2023.0.3.3
- 持久层框架MybatisPlus 3.5.12
- 基础框架Spring Boot 3.5.5/2.7.18
- 微服务框架: Spring Cloud Alibaba 2021.0.6.2
- 持久层框架MybatisPlus 3.5.3.2
- 报表工具: 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
- 数据库连接池阿里巴巴Druid 1.2.24
- AI大模型支持 `ChatGPT` `DeepSeek` `千问`等各种常规模式
- 数据库连接池阿里巴巴Druid 1.1.24
- AI大模型支持 `ChatGPT` `DeepSeek`切换
- 日志打印logback
- 缓存Redis
- 其他autopoi, fastjsonpoiSwagger-uiquartz, lombok简化代码等。
@ -230,31 +217,38 @@ JeecgBoot平台提供了一套完善的AI应用管理系统模块是一套类
- 15、CAS 单点登录 √
- 16、路由限流 √
#### 微服务架构图
![微服务架构图](https://jeecgos.oss-cn-beijing.aliyuncs.com/files/jeecgboot_springcloud2022.png "在这里输入图片标题")
### 功能清单
开源版与企业版区别?
-----------------------------------
- JeecgBoot开源版采用 [Apache-2.0 license](LICENSE) 协议附加补充条款:允许商用使用,不会造成侵权行为,允许基于本平台软件开展业务系统开发(但在任何情况下,您不得使用本软件开发可能被认为与本软件竞争的软件).
- 商业版与开源版主要区别在于商业版提供了技术支持 和 更多的企业级功能(例如Online图表、流程监控、流程设计、流程审批、表单设计器、表单视图、积木报表企业版、OA办公、商业APP、零代码应用、Online模块源码等功能). [更多商业功能介绍,点击查看](README-Enterprise.md)
- JeecgBoot未来发展方向是零代码平台的建设也就是团队的另外一款产品 [敲敲云零代码](https://www.qiaoqiaoyun.com) 无需编码即可通过拖拽快速搭建企业级应用与JeecgBoot低代码平台形成互补满足从简单业务到复杂系统的全场景开发需求目前已经开源[欢迎下载](https://qiaoqiaoyun.com/downloadCode)
### Jeecg Boot 产品功能蓝图
![功能蓝图](https://jeecgos.oss-cn-beijing.aliyuncs.com/upload/test/Jeecg-Boot-lantu202005_1590912449914.jpg "在这里输入图片标题")
#### 系统功能架构图
![](https://oscimg.oschina.net/oscnet/up-1569487b95a07dbc3599fb1349a2e3aaae1.png)
### 开源版功能清单
```
├─AI应用平台
│ ├─AI模型管理
│ ├─AI应用管理
│ ├─AI知识库
│ ├─AI流程编排
│ ├─AI聊天助手(支持图片、文件)
│ ├─AI聊天助手支持嵌入第三方、支持移动端
│ ├─MCP插件管理
│ ├─提示词管理
│ ├─AI应用门户汇总各种AI应用场景
│ ├─支持各种常见模型ChatGPT和DeepSeek、ollama等
├─工具箱
│ ├─OCR识别
│ ├─AI 海报
│ ├─AI 写作
│ ├─AI 简历
├─AI辅助功能
│ ├─AI建表Online表单
│ ├─AI生成报表Online报表
│ ├─AI生成大屏
├─系统管理
│ ├─用户管理
│ ├─角色管理
@ -281,6 +275,18 @@ JeecgBoot平台提供了一套完善的AI应用管理系统模块是一套类
│ ├─系统编码规则
│ ├─系统校验规则
│ ├─APP版本管理
├─AI应用平台
│ ├─AI知识库问答系统
│ ├─AI大模型管理
│ ├─AI流程编排
│ ├─AI流程设计器
│ ├─AI对话支持图片
│ ├─AI对话助手(智能问答)
│ ├─AI建表Online表单
│ ├─AI聊天窗口支持嵌入第三方
│ ├─AI聊天窗口支持移动端
│ ├─支持常见大模型ChatGPT和DeepSeek、ollama等等
│ ├─AI OCR示例
├─数据可视化
│ ├─报表设计器(支持打印设计)
│ ├─大屏设和仪表盘设计
@ -391,47 +397,6 @@ JeecgBoot平台提供了一套完善的AI应用管理系统模块是一套类
### 系统效果
##### AI模型与应用管理
![](https://oscimg.oschina.net/oscnet/up-0b1779e923566ccebb2d5a9cc9220c78b4a.png)
![](https://oscimg.oschina.net/oscnet/up-c8956df1d37d66b2d40136afaeca677628b.png)
![](https://oscimg.oschina.net/oscnet/up-8c348eeafd89673ca8cd1a2705014e3ac04.png)
AI流程编排
![](https://oscimg.oschina.net/oscnet/up-2343657de2c7ac8010bc471470d084075ae.png)
MCP和工具管理
![](https://oscimg.oschina.net/oscnet/up-8119d5dbc72e534236a3d042e11534c52ad.png)
AI知识库支持各种文档格式尤其markdown适配很好
![](https://oscimg.oschina.net/oscnet/up-e2e9c118982ea366ed7f2b9827d4bb46c5d.png)
AI工具箱
![](https://oscimg.oschina.net/oscnet/up-bf2a808d22a11fd83e577ad74741d97884b.png)
AI聊天助手
![](https://oscimg.oschina.net/oscnet/up-2a51accc2ff0b647e0ee058a58d291fe811.png)
![](https://oscimg.oschina.net/oscnet/up-ea1069c2a92a3ab2963d88763016cb037c2.png)
![](https://oscimg.oschina.net/oscnet//65298d5710b4e6039a5f802b5f8505c5.png)
AI写文章
![](https://oscimg.oschina.net/oscnet/up-e3ee5b1fe497308805aa5e324b72994af79.png)
##### PC端
![](https://oscimg.oschina.net/oscnet/up-000530d95df337b43089ac77e562494f454.png)
@ -451,6 +416,21 @@ AI写文章
![](https://oscimg.oschina.net/oscnet/up-16c07e000278329b69b228ae3189814b8e9.png)
##### AI功能
AI聊天助手
![](https://oscimg.oschina.net/oscnet//65298d5710b4e6039a5f802b5f8505c5.png)
AI建表
![](https://oscimg.oschina.net/oscnet/up-381423599f219a67def45dfd9a99df8ef3f.png)
![](https://oscimg.oschina.net/oscnet/up-1508c2b0708c365605f68893044ee11f20d.png)
AI写文章
![](https://oscimg.oschina.net/oscnet/up-e3ee5b1fe497308805aa5e324b72994af79.png)
##### 仪表盘设计器
@ -523,21 +503,6 @@ AI写文章
#### 微服务架构图
![微服务架构图](https://jeecgos.oss-cn-beijing.aliyuncs.com/files/jeecgboot_springcloud2022.png "在这里输入图片标题")
### Jeecg Boot 产品功能蓝图
![功能蓝图](https://jeecgos.oss-cn-beijing.aliyuncs.com/upload/test/Jeecg-Boot-lantu202005_1590912449914.jpg "在这里输入图片标题")
#### 系统功能架构图
![](https://oscimg.oschina.net/oscnet/up-1569487b95a07dbc3599fb1349a2e3aaae1.png)
## 捐赠

View File

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

View File

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

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属于平台级产品每次升级改动较大目前做不到平滑升级。
### 增量升级方案
#### 1.代码合并
本地通过svn或git做好主干在分支上做业务开发jeecg每次版本发布可以手工覆盖主干的代码对比合并代码
@ -12,12 +11,5 @@
- 其他库请手工执行SQL, 目录: `jeecg-module-system\jeecg-system-start\src\main\resources\flyway\sql\mysql`
> 注意: 升级sql只提供mysql版本如果有权限升级, 还需要手工角色授权,退出重新登录才好使。
#### 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.兼容问题
#### 3.兼容问题
每次发版,会针对不兼容地方重点说明。

View File

@ -63,7 +63,6 @@ services:
build:
context: ./jeecg-module-system/jeecg-system-start
restart: on-failure
mac_address: 02:42:ac:11:00:02
depends_on:
- jeecg-boot-mysql
- jeecg-boot-redis

View File

@ -2,9 +2,9 @@
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>org.jeecgframework.boot3</groupId>
<groupId>org.jeecgframework.boot</groupId>
<artifactId>jeecg-boot-parent</artifactId>
<version>3.9.1</version>
<version>3.8.3</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>jeecg-boot-base-core</artifactId>
@ -42,14 +42,8 @@
<dependencies>
<!--jeecg-tools-->
<dependency>
<groupId>org.jeecgframework.boot3</groupId>
<groupId>org.jeecgframework.boot</groupId>
<artifactId>jeecg-boot-common</artifactId>
<exclusions>
<exclusion>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<!--集成springmvc框架并实现自动配置 -->
<dependency>
@ -104,7 +98,7 @@
<!-- mybatis-plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<dependency>
@ -113,22 +107,22 @@
<version>${mybatis-plus.version}</version>
</dependency>
<!-- minidao -->
<dependency>
<groupId>org.jeecgframework.boot3</groupId>
<artifactId>minidao-spring-boot-starter-jsqlparser-4.9</artifactId>
</dependency>
<dependency>
<groupId>org.jeecgframework</groupId>
<artifactId>minidao-spring-boot-starter-jsqlparser-4.9</artifactId>
</dependency>
<!-- druid -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-3-starter</artifactId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>${druid.version}</version>
</dependency>
<!-- 动态数据源 -->
<dependency>
<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>
</dependency>
@ -179,6 +173,7 @@
<artifactId>DmDialect-for-hibernate5.0</artifactId>
<version>${dm8.version}</version>
</dependency>
<!-- Quartz定时任务 -->
<dependency>
<groupId>org.springframework.boot</groupId>
@ -196,56 +191,7 @@
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-starter</artifactId>
<classifier>jakarta</classifier>
<version>${shiro.version}</version>
<exclusions>
<exclusion>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<classifier>jakarta</classifier>
<version>${shiro.version}</version>
<!-- 排除仍使用了javax.servlet的依赖 -->
<exclusions>
<exclusion>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
</exclusion>
<exclusion>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-web</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- 引入适配jakarta的依赖包 -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<classifier>jakarta</classifier>
<version>${shiro.version}</version>
<exclusions>
<exclusion>
<groupId>commons-beanutils</groupId>
<artifactId>commons-beanutils</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-web</artifactId>
<classifier>jakarta</classifier>
<version>${shiro.version}</version>
<exclusions>
<exclusion>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- shiro-redis -->
<dependency>
@ -264,16 +210,12 @@
</exclusions>
</dependency>
<!-- knife4j -->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-openapi3-ui</artifactId>
<artifactId>knife4j-openapi3-spring-boot-starter</artifactId>
<version>${knife4j-spring-boot-starter.version}</version>
</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 -->
@ -296,7 +238,7 @@
<!-- AutoPoi Excel工具类-->
<dependency>
<groupId>org.jeecgframework</groupId>
<artifactId>autopoi-spring-boot-3-starter</artifactId>
<artifactId>autopoi-web</artifactId>
</dependency>
<dependency>
<groupId>xerces</groupId>
@ -305,7 +247,7 @@
<optional>true</optional>
</dependency>
<!-- minio文件存储服务 -->
<!-- mini文件存储服务 -->
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
@ -314,14 +256,6 @@
<artifactId>checker-qual</artifactId>
<groupId>org.checkerframework</groupId>
</exclusion>
<exclusion>
<groupId>com.google.errorprone</groupId>
<artifactId>error_prone_annotations</artifactId>
</exclusion>
<exclusion>
<groupId>org.apache.commons</groupId>
<artifactId>commons-compress</artifactId>
</exclusion>
</exclusions>
</dependency>
@ -341,16 +275,6 @@
<dependency>
<groupId>com.xkcoding.justauth</groupId>
<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>
<groupId>com.squareup.okhttp3</groupId>
@ -376,24 +300,8 @@
</dependency>
<!-- chatgpt -->
<dependency>
<groupId>org.jeecgframework.boot3</groupId>
<groupId>org.jeecgframework.boot</groupId>
<artifactId>jeecg-boot-starter-chatgpt</artifactId>
</dependency>
<!-- 腾讯云 -->
<dependency>
<groupId>com.tencentcloudapi</groupId>
<artifactId>tencentcloud-sdk-java-sms</artifactId>
<version>${tencentcloud-sdk-java-sms.version}</version>
<exclusions>
<exclusion>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
</exclusion>
<exclusion>
<groupId>com.squareup.okio</groupId>
<artifactId>okio</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
</project>
</project>

View File

@ -132,6 +132,7 @@ public interface CommonAPI {
*/
Map<String, List<DictModel>> translateManyDict(String dictCodes, String keys);
//update-begin---author:chenrui ---date:20231221 for[issues/#5643]解决分布式下表字典跨库无法查询问题------------
/**
* 15 字典表的 翻译,可批量
* @param table
@ -142,6 +143,7 @@ public interface CommonAPI {
* @return
*/
List<DictModel> translateDictFromTableByKeys(String table, String text, String code, String keys, String dataSource);
//update-end---author:chenrui ---date:20231221 for[issues/#5643]解决分布式下表字典跨库无法查询问题------------
/**
* 16 运行AIRag流程

View File

@ -33,10 +33,4 @@ public class AiragFlowDTO implements Serializable {
* 输入参数
*/
private Map<String, Object> inputParams;
/**
* 是否流式返回
*/
private boolean isStream;
}

View File

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

View File

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

View File

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

View File

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

View File

@ -20,14 +20,14 @@ import org.jeecg.common.system.vo.LoginUser;
import org.jeecg.common.util.IpUtils;
import org.jeecg.common.util.SpringContextUtils;
import org.jeecg.common.util.oConvertUtils;
import org.springframework.core.StandardReflectionParameterNameDiscoverer;
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
import org.springframework.stereotype.Component;
import org.springframework.validation.BindingResult;
import org.springframework.web.multipart.MultipartFile;
import jakarta.annotation.Resource;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import javax.annotation.Resource;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.Date;
@ -121,8 +121,9 @@ public class AutoLogAspect {
if (operateType > 0) {
return operateType;
}
// 代码逻辑说明: 阿里云代码扫描规范(不允许任何魔法值出现在代码中)------------
//update-begin---author:wangshuai ---date:20220331 for阿里云代码扫描规范(不允许任何魔法值出现在代码中)------------
return OperateTypeEnum.getTypeByMethodName(methodName);
//update-end---author:wangshuai ---date:20220331 for阿里云代码扫描规范(不允许任何魔法值出现在代码中)------------
}
/**
@ -142,15 +143,14 @@ public class AutoLogAspect {
// https://my.oschina.net/mengzhang6/blog/2395893
Object[] arguments = new Object[paramsArray.length];
for (int i = 0; i < paramsArray.length; i++) {
if (paramsArray[i] instanceof BindingResult || paramsArray[i] instanceof ServletRequest || paramsArray[i] instanceof ServletResponse || paramsArray[i] instanceof MultipartFile || paramsArray[i] instanceof MultipartFile[]) {
if (paramsArray[i] instanceof BindingResult || paramsArray[i] instanceof ServletRequest || paramsArray[i] instanceof ServletResponse || paramsArray[i] instanceof MultipartFile) {
//ServletRequest不能序列化从入参里排除否则报异常java.lang.IllegalStateException: It is illegal to call this method if the current request is not in asynchronous mode (i.e. isAsyncStarted() returns false)
//ServletResponse不能序列化 从入参里排除否则报异常java.lang.IllegalStateException: getOutputStream() has already been called for this response
//MultipartFile和MultipartFile[]不能序列化,从入参里排除
continue;
}
arguments[i] = paramsArray[i];
}
// 代码逻辑说明: 日志数据太长的直接过滤掉
//update-begin-author:taoyan date:20200724 for:日志数据太长的直接过滤掉
PropertyFilter profilter = new PropertyFilter() {
@Override
public boolean apply(Object o, String name, Object value) {
@ -165,13 +165,14 @@ public class AutoLogAspect {
}
};
params = JSONObject.toJSONString(arguments, profilter);
//update-end-author:taoyan date:20200724 for:日志数据太长的直接过滤掉
} else {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
// 请求的方法参数值
Object[] args = joinPoint.getArgs();
// 请求的方法参数名称
StandardReflectionParameterNameDiscoverer u= new StandardReflectionParameterNameDiscoverer();
LocalVariableTableParameterNameDiscoverer u = new LocalVariableTableParameterNameDiscoverer();
String[] paramNames = u.getParameterNames(method);
if (args != null && paramNames != null) {
for (int i = 0; i < args.length; i++) {

View File

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

View File

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

View File

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

View File

@ -91,23 +91,6 @@ public interface CommonConstant {
public static String PREFIX_USER_SHIRO_CACHE = "shiro:cache:org.jeecg.config.shiro.ShiroRealm.authorizationCache:";
/** 登录用户Token令牌缓存KEY前缀 */
String PREFIX_USER_TOKEN = "prefix_user_token:";
/** 登录用户Token令牌作废提示信息比如 “不允许同一账号多地同时登录,会往这个变量存提示信息” */
String PREFIX_USER_TOKEN_ERROR_MSG = "prefix_user_token:error:msg_";
/**============================== 【是否允许同一账号多地同时登录】登录客户端类型常量 ==============================*/
/** 客户端类型PC端 */
String CLIENT_TYPE_PC = "PC";
/** 客户端类型APP端 */
String CLIENT_TYPE_APP = "APP";
/** 客户端类型:手机号登录 */
String CLIENT_TYPE_PHONE = "PHONE";
String PREFIX_USER_TOKEN_PC = "prefix_user_token:single_login:pc:";
/** 单点登录用户在APP端的Token缓存KEY前缀 (username -> token) */
String PREFIX_USER_TOKEN_APP = "prefix_user_token:single_login:app:";
/** 单点登录用户在手机号登录的Token缓存KEY前缀 (username -> token) */
String PREFIX_USER_TOKEN_PHONE = "prefix_user_token:single_login:phone:";
/**============================== 【是否允许同一账号多地同时登录】登录客户端类型常量 ==============================*/
// /** Token缓存时间3600秒即一小时 */
// int TOKEN_EXPIRE_TIME = 3600;
@ -325,10 +308,6 @@ public interface CommonConstant {
*/
String SYS_ROLE_ADMIN = "admin";
/**
* 考勤补卡业务状态 0处理中
*/
String SIGN_PATCH_BIZ_STATUS_0 = "0";
/**
* 考勤补卡业务状态 1同意 2不同意
*/
@ -612,6 +591,7 @@ public interface CommonConstant {
String ORDER_TYPE_DESC = "DESC";
//update-begin---author:scott ---date:2023-09-10 for积木报表常量----
/**
* 报表允许设计开发的角色
*/
@ -626,7 +606,9 @@ public interface CommonConstant {
* 数据隔离模式: 按照租户隔离
*/
public static final String SAAS_MODE_TENANT = "tenant";
//update-end---author:scott ---date::2023-09-10 for积木报表常量----
//update-begin---author:wangshuai---date:2024-04-07---for:修改手机号常量---
/**
* 修改手机号短信验证码redis-key的前缀
*/
@ -651,6 +633,7 @@ public interface CommonConstant {
* 修改手机号
*/
String UPDATE_PHONE = "updatePhone";
//update-end---author:wangshuai---date:2024-04-07---for:修改手机号常量---
/**
* 修改手机号验证码请求次数超出
@ -726,19 +709,4 @@ public interface CommonConstant {
* 部门名称redisKey(全路径)
*/
String DEPART_NAME_REDIS_KEY_PRE = "sys:cache:departPathName:";
/**
* 默认用户排序值
*/
Integer DEFAULT_USER_SORT = 1000;
/**
* 发送短信方式:腾讯
*/
String SMS_SEND_TYPE_TENCENT = "tencent";
/**
* 发送短信方式:阿里云
*/
String SMS_SEND_TYPE_ALI_YUN = "aliyun";
}

View File

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

View File

@ -47,8 +47,4 @@ public interface TenantConstant {
*/
String APP_ADMIN = "appAdmin";
/**
* 增加SignatureCheck注解POST请求的URL
*/
String[] SIGNATURE_CHECK_POST_URL = { "/sys/tenant/joinTenantByHouseNumber", "/sys/tenant/invitationUser" };
}

View File

@ -23,11 +23,7 @@ public enum NoticeTypeEnum {
/**
* 督办
*/
NOTICE_TYPE_SUPERVISE("督办管理", "supe"),
/**
* 考勤
*/
NOTICE_TYPE_ATTENDANCE("考勤消息", "attendance");
NOTICE_TYPE_SUPERVISE("督办管理", "supe");
/**
* 文件类型名称

View File

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

View File

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

View File

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

View File

@ -1,8 +1,6 @@
package org.jeecg.common.exception;
import cn.hutool.core.util.ObjectUtil;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.shiro.SecurityUtils;
@ -35,6 +33,8 @@ import org.springframework.web.multipart.MaxUploadSizeExceededException;
import org.springframework.web.multipart.MultipartException;
import org.springframework.web.servlet.NoHandlerFoundException;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.util.Map;
import java.util.stream.Collectors;
@ -121,12 +121,13 @@ public class JeecgBootExceptionHandler {
@ExceptionHandler(Exception.class)
public Result<?> handleException(Exception e){
log.error(e.getMessage(), e);
// 代码逻辑说明: 处理Sentinel限流自定义异常
//update-begin---author:zyf ---date:20220411 for处理Sentinel限流自定义异常
Throwable throwable = e.getCause();
SentinelErrorInfoEnum errorInfoEnum = SentinelErrorInfoEnum.getErrorByException(throwable);
if (ObjectUtil.isNotEmpty(errorInfoEnum)) {
return Result.error(errorInfoEnum.getError());
}
//update-end---author:zyf ---date:20220411 for处理Sentinel限流自定义异常
addSysLog(e);
return Result.error("操作失败,"+e.getMessage());
}
@ -223,6 +224,7 @@ public class JeecgBootExceptionHandler {
return Result.error("校验失败存在SQL注入风险" + msg);
}
//update-begin---author:chenrui ---date:20240423 for[QQYUN-8732]把错误的日志都抓取了 方便后续处理,单独弄个日志类型------------
/**
* 添加异常新系统日志
* @param e 异常
@ -241,6 +243,7 @@ public class JeecgBootExceptionHandler {
} catch (NullPointerException | BeansException ignored) {
}
if (null != request) {
//update-begin---author:chenrui ---date:20250408 for[QQYUN-11716]上传大图片失败没有精确提示------------
//请求的参数
if (!isTooBigException(e)) {
// 文件上传过大异常时不能获取参数,否则会报错
@ -249,6 +252,7 @@ public class JeecgBootExceptionHandler {
log.setMethod(oConvertUtils.mapToString(request.getParameterMap()));
}
}
//update-end---author:chenrui ---date:20250408 for[QQYUN-11716]上传大图片失败没有精确提示------------
// 请求地址
log.setRequestUrl(request.getRequestURI());
//设置IP地址
@ -272,6 +276,7 @@ public class JeecgBootExceptionHandler {
baseCommonService.addLog(log);
}
//update-end---author:chenrui ---date:20240423 for[QQYUN-8732]把错误的日志都抓取了 方便后续处理,单独弄个日志类型------------
/**
* 是否文件过大异常

View File

@ -24,9 +24,9 @@ import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.MultipartHttpServletRequest;
import org.springframework.web.servlet.ModelAndView;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.*;
@ -68,16 +68,12 @@ public class JeecgController<T, S extends IService<T>> {
//此处设置的filename无效 ,前端会重更新设置一下
mv.addObject(NormalExcelConstants.FILE_NAME, title);
mv.addObject(NormalExcelConstants.CLASS, clazz);
// 代码逻辑说明: 【QQYUN-13930】统一改成导出xlsx格式---
ExportParams exportParams=new ExportParams(title + "报表", "导出人:" + sysUser.getRealname(), title, ExcelType.XSSF);
//update-begin--Author:liusq Date:20210126 for图片导出报错ImageBasePath未设置--------------------
ExportParams exportParams=new ExportParams(title + "报表", "导出人:" + sysUser.getRealname(), title);
exportParams.setImageBasePath(jeecgBaseConfig.getPath().getUpload());
//update-end--Author:liusq Date:20210126 for图片导出报错ImageBasePath未设置----------------------
mv.addObject(NormalExcelConstants.PARAMS,exportParams);
mv.addObject(NormalExcelConstants.DATA_LIST, exportList);
// 代码逻辑说明: 【issues/9052】BasicTable列表页导出excel可以指定列---
String exportFields = request.getParameter(NormalExcelConstants.EXPORT_FIELDS);
if(oConvertUtils.isNotEmpty(exportFields)){
mv.addObject(NormalExcelConstants.EXPORT_FIELDS, exportFields);
}
return mv;
}
/**
@ -98,12 +94,14 @@ public class JeecgController<T, S extends IService<T>> {
// Step.2 计算分页sheet数据
double total = service.count();
int count = (int)Math.ceil(total/pageNum);
//update-begin-author:liusq---date:20220629--for: 多sheet导出根据选择导出写法调整 ---
// Step.3 过滤选中数据
String selections = request.getParameter("selections");
if (oConvertUtils.isNotEmpty(selections)) {
List<String> selectionList = Arrays.asList(selections.split(","));
queryWrapper.in("id",selectionList);
}
//update-end-author:liusq---date:20220629--for: 多sheet导出根据选择导出写法调整 ---
// Step.4 多sheet处理
List<Map<String, Object>> listMap = new ArrayList<Map<String, Object>>();
for (int i = 1; i <=count ; i++) {
@ -222,15 +220,16 @@ public class JeecgController<T, S extends IService<T>> {
params.setNeedSave(true);
try {
List<T> list = ExcelImportUtil.importExcel(file.getInputStream(), clazz, params);
// 代码逻辑说明: 批量插入数据
//update-begin-author:taoyan date:20190528 for:批量插入数据
long start = System.currentTimeMillis();
service.saveBatch(list);
//400条 saveBatch消耗时间1592毫秒 循环插入消耗时间1947毫秒
//1200条 saveBatch消耗时间3687毫秒 循环插入消耗时间5212毫秒
log.info("消耗时间" + (System.currentTimeMillis() - start) + "毫秒");
//update-end-author:taoyan date:20190528 for:批量插入数据
return Result.ok("文件导入成功!数据行数:" + list.size());
} catch (Exception e) {
// 代码逻辑说明: 导入数据重复增加提示
//update-begin-author:taoyan date:20211124 for: 导入数据重复增加提示
String msg = e.getMessage();
log.error(msg, e);
if(msg!=null && msg.indexOf("Duplicate entry")>=0){
@ -238,6 +237,7 @@ public class JeecgController<T, S extends IService<T>> {
}else{
return Result.error("文件导入失败:" + e.getMessage());
}
//update-end-author:taoyan date:20211124 for: 导入数据重复增加提示
} finally {
try {
file.getInputStream().close();

View File

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

View File

@ -5,7 +5,7 @@ import org.jeecg.common.system.vo.SysUserCacheInfo;
import org.jeecg.common.util.SpringContextUtils;
import org.springframework.util.StringUtils;
import jakarta.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequest;
import java.util.ArrayList;
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.interfaces.DecodedJWT;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.base.Joiner;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Date;
import java.util.Objects;
import java.util.stream.Collectors;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.SecurityUtils;
@ -41,10 +39,8 @@ import org.jeecg.common.util.oConvertUtils;
@Slf4j
public class JwtUtil {
/**PC端Token有效期为7天Token在reids中缓存时间为两倍*/
public static final long EXPIRE_TIME = (7 * 12) * 60 * 60 * 1000L;
/**APP端Token有效期为30天Token在reids中缓存时间为两倍*/
public static final long APP_EXPIRE_TIME = (30 * 12) * 60 * 60 * 1000L;
/**Token有效期为7天Token在reids中缓存时间为两倍*/
public static final long EXPIRE_TIME = (7 * 12) * 60 * 60 * 1000;
static final String WELL_NUMBER = SymbolConstant.WELL_NUMBER + SymbolConstant.LEFT_CURLY_BRACKET;
/**
@ -88,7 +84,7 @@ public class JwtUtil {
DecodedJWT jwt = verifier.verify(token);
return true;
} catch (Exception e) {
log.warn("Token验证失败" + e.getMessage(),e);
log.error(e.getMessage(), e);
return false;
}
}
@ -114,9 +110,7 @@ public class JwtUtil {
* @param username 用户名
* @param secret 用户的密码
* @return 加密的token
* @deprecated 请使用sign(String username, String secret, String clientType)方法代替
*/
@Deprecated
public static String sign(String username, String secret) {
Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
Algorithm algorithm = Algorithm.HMAC256(secret);
@ -125,68 +119,6 @@ public class JwtUtil {
}
/**
* 生成签名,5min后过期
*
* @param username 用户名
* @param secret 用户的密码
* @param expireTime 过期时间
* @return 加密的token
* @deprecated 请使用sign(String username, String secret, String clientType)方法代替
*/
@Deprecated
public static String sign(String username, String secret, Long expireTime) {
Date date = new Date(System.currentTimeMillis() + expireTime);
Algorithm algorithm = Algorithm.HMAC256(secret);
// 附带username信息
return JWT.create().withClaim("username", username).withExpiresAt(date).sign(algorithm);
}
/**
* 生成签名,根据客户端类型自动选择过期时间
* for [JHHB-1030]【鉴权】移动端用户token到期后续期时间变成pc端时长
*
* @param username 用户名
* @param secret 用户的密码
* @param clientType 客户端类型PC或APP
* @return 加密的token
*/
public static String sign(String username, String secret, String clientType) {
// 根据客户端类型选择对应的过期时间
long expireTime = CommonConstant.CLIENT_TYPE_APP.equalsIgnoreCase(clientType)
? APP_EXPIRE_TIME
: EXPIRE_TIME;
Date date = new Date(System.currentTimeMillis() + expireTime);
Algorithm algorithm = Algorithm.HMAC256(secret);
// 附带username和clientType信息
return JWT.create()
.withClaim("username", username)
.withClaim("clientType", clientType)
.withExpiresAt(date)
.sign(algorithm);
}
/**
* 从token中获取客户端类型
* for [JHHB-1030]【鉴权】移动端用户token到期后续期时间变成pc端时长
*
* @param token JWT token
* @return 客户端类型如果不存在则返回PC兼容旧token
*/
public static String getClientType(String token) {
try {
DecodedJWT jwt = JWT.decode(token);
String clientType = jwt.getClaim("clientType").asString();
// 如果clientType为空返回默认值PC兼容旧token
return oConvertUtils.isNotEmpty(clientType) ? clientType : CommonConstant.CLIENT_TYPE_PC;
} catch (JWTDecodeException e) {
log.warn("解析token中的clientType失败使用默认值PC" + e.getMessage());
return CommonConstant.CLIENT_TYPE_PC;
}
}
/**
* 根据request中的token获取用户账号
*
@ -266,6 +198,7 @@ public class JwtUtil {
} else {
key = key;
}
//update-begin---author:chenrui ---date:20250107 for[QQYUN-10785]数据权限,查看自己拥有部门的权限中存在问题 #7288------------
// 是否存在字符串标志
boolean multiStr;
if(oConvertUtils.isNotEmpty(key) && key.trim().matches("^\\[\\w+]$")){
@ -274,6 +207,7 @@ public class JwtUtil {
} else {
multiStr = false;
}
//update-end---author:chenrui ---date:20250107 for[QQYUN-10785]数据权限,查看自己拥有部门的权限中存在问题 #7288------------
//替换为当前系统时间(年月日)
if (key.equals(DataBaseConstant.SYS_DATE)|| key.toLowerCase().equals(DataBaseConstant.SYS_DATE_TABLE)) {
returnValue = DateUtils.formatDate();
@ -342,17 +276,20 @@ public class JwtUtil {
if(user==null){
//TODO 暂时使用用户登录部门,存在逻辑缺陷,不是用户所拥有的部门
returnValue = sysUser.getOrgCode();
// 代码逻辑说明: [QQYUN-10785]数据权限,查看自己拥有部门的权限中存在问题 #7288------------
//update-begin---author:chenrui ---date:20250107 for[QQYUN-10785]数据权限,查看自己拥有部门的权限中存在问题 #7288------------
returnValue = multiStr ? "'" + returnValue + "'" : returnValue;
//update-end---author:chenrui ---date:20250107 for[QQYUN-10785]数据权限,查看自己拥有部门的权限中存在问题 #7288------------
}else{
if(user.isOneDepart()) {
returnValue = user.getSysMultiOrgCode().get(0);
// 代码逻辑说明: [QQYUN-10785]数据权限,查看自己拥有部门的权限中存在问题 #7288------------
//update-begin---author:chenrui ---date:20250107 for[QQYUN-10785]数据权限,查看自己拥有部门的权限中存在问题 #7288------------
returnValue = multiStr ? "'" + returnValue + "'" : returnValue;
//update-end---author:chenrui ---date:20250107 for[QQYUN-10785]数据权限,查看自己拥有部门的权限中存在问题 #7288------------
}else {
// 代码逻辑说明: [QQYUN-10785]数据权限,查看自己拥有部门的权限中存在问题 #7288------------
//update-begin---author:chenrui ---date:20250107 for[QQYUN-10785]数据权限,查看自己拥有部门的权限中存在问题 #7288------------
returnValue = user.getSysMultiOrgCode().stream()
.filter(Objects::nonNull)
//update-begin---author:chenrui ---date:20250224 for[issues/7288]数据权限,查看自己拥有部门的权限中存在问题 #7288------------
.map(orgCode -> {
if (multiStr) {
return "'" + orgCode + "'";
@ -360,7 +297,9 @@ public class JwtUtil {
return orgCode;
}
})
//update-end---author:chenrui ---date:20250224 for[issues/7288]数据权限,查看自己拥有部门的权限中存在问题 #7288------------
.collect(Collectors.joining(", "));
//update-end---author:chenrui ---date:20250107 for[QQYUN-10785]数据权限,查看自己拥有部门的权限中存在问题 #7288------------
}
}
}
@ -374,7 +313,7 @@ public class JwtUtil {
}
}
// 代码逻辑说明: 多租户ID作为系统变量
//update-begin-author:taoyan date:20210330 for:多租户ID作为系统变量
else if (key.equals(TenantConstant.TENANT_ID) || key.toLowerCase().equals(TenantConstant.TENANT_ID_TABLE)){
try {
returnValue = SpringContextUtils.getHttpServletRequest().getHeader(CommonConstant.TENANT_ID);
@ -382,6 +321,7 @@ public class JwtUtil {
log.warn("获取系统租户异常:" + e.getMessage());
}
}
//update-end-author:taoyan date:20210330 for:多租户ID作为系统变量
if(returnValue!=null){returnValue = returnValue + moshi;}
return returnValue;
}

View File

@ -3,7 +3,9 @@ package org.jeecg.common.system.util;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.system.annotation.EnumDict;
import org.jeecg.common.system.vo.DictModel;
import org.jeecg.common.util.SpringContextUtils;
import org.jeecg.common.util.oConvertUtils;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
@ -11,7 +13,6 @@ import org.springframework.core.type.classreading.CachingMetadataReaderFactory;
import org.springframework.core.type.classreading.MetadataReader;
import org.springframework.core.type.classreading.MetadataReaderFactory;
import org.springframework.util.ClassUtils;
import java.lang.reflect.Method;
import java.util.*;
@ -66,13 +67,13 @@ public class ResourceUtil {
synchronized (ResourceUtil.class) {
if (!initialized) {
long startTime = System.currentTimeMillis();
log.debug("【枚举字典加载】开始初始化枚举字典数据...");
log.info("【枚举字典加载】开始初始化枚举字典数据...");
initEnumDictData();
initialized = true;
long endTime = System.currentTimeMillis();
log.debug("【枚举字典加载】枚举字典数据初始化完成,共加载 {} 个字典,总耗时: {}ms", enumDictData.size(), endTime - startTime);
log.info("【枚举字典加载】枚举字典数据初始化完成,共加载 {} 个字典,总耗时: {}ms", enumDictData.size(), endTime - startTime);
}
}
}
@ -102,7 +103,7 @@ public class ResourceUtil {
}
long scanEndTime = System.currentTimeMillis();
log.debug("【枚举字典加载】文件扫描完成,总共找到 {} 个枚举类文件,扫描耗时: {}ms", allResources.size(), scanEndTime - scanStartTime);
log.info("【枚举字典加载】文件扫描完成,总共找到 {} 个枚举类文件,扫描耗时: {}ms", allResources.size(), scanEndTime - scanStartTime);
MetadataReaderFactory readerFactory = new CachingMetadataReaderFactory(resourcePatternResolver);
@ -125,7 +126,7 @@ public class ResourceUtil {
}
long processEndTime = System.currentTimeMillis();
log.debug("【枚举字典加载】处理完成,实际处理 {} 个带注解的枚举类,处理耗时: {}ms", processedCount, processEndTime - processStartTime);
log.info("【枚举字典加载】处理完成,实际处理 {} 个带注解的枚举类,处理耗时: {}ms", processedCount, processEndTime - processStartTime);
}
/**
@ -182,10 +183,10 @@ public class ResourceUtil {
for (DictModel dm : dictItemList) {
String value = dm.getValue();
if (keySet.contains(value)) {
// 修复bug获取或创建该dictCode对应的list而不是每次都创建新的list
List<DictModel> list = map.computeIfAbsent(code, k -> new ArrayList<>());
List<DictModel> list = new ArrayList<>();
list.add(new DictModel(value, dm.getText()));
//break;
map.put(code, list);
break;
}
}
}

View File

@ -150,7 +150,7 @@ public class SqlConcatUtil {
}
private static String getInConditionValue(Object value,boolean isString) {
// 代码逻辑说明: 查询条件如果输入,导致sql报错
//update-begin-author:taoyan date:20210628 for: 查询条件如果输入,导致sql报错
String[] temp = value.toString().split(",");
if(temp.length==0){
return "('')";
@ -168,6 +168,7 @@ public class SqlConcatUtil {
}else {
return "("+value.toString()+")";
}
//update-end-author:taoyan date:20210628 for: 查询条件如果输入,导致sql报错
}
/**
@ -214,6 +215,7 @@ public class SqlConcatUtil {
}
}else {
//update-begin-author:taoyan date:2022-6-30 for: issues/3810 数据权限规则问题
// 走到这里说明 value不带有任何模糊查询的标识(*或者%)
if (ruleEnum == QueryRuleEnum.LEFT_LIKE) {
if (DataBaseConstant.DB_TYPE_SQLSERVER.equals(getDbType())) {
@ -234,6 +236,7 @@ public class SqlConcatUtil {
return "'%" + str + "%'";
}
}
//update-end-author:taoyan date:2022-6-30 for: issues/3810 数据权限规则问题
}
}

View File

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

View File

@ -19,7 +19,7 @@ import org.springframework.jdbc.datasource.DriverManagerDataSource;
import org.springframework.util.FileCopyUtils;
import org.springframework.web.multipart.MultipartFile;
import jakarta.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequest;
import javax.sql.DataSource;
import java.io.ByteArrayInputStream;
import java.io.File;
@ -56,9 +56,7 @@ public class CommonUtils {
public static String uploadOnlineImage(byte[] data,String basePath,String bizPath,String uploadType){
String dbPath = null;
String fileName = "image" + Math.round(Math.random() * 100000000000L);
//update-begin---author:wangshuai---date:2026-01-08---for:【QQYUN-14535】ai生成图片的后缀不一致的导致不展示---
fileName += "." + PoiPublicUtil.getFileExtendName(data).toLowerCase();
//update-end---author:wangshuai---date:2026-01-08---for:【QQYUN-14535】ai生成图片的后缀不一致的导致不展示---
fileName += "." + PoiPublicUtil.getFileExtendName(data);
try {
if(CommonConstant.UPLOAD_TYPE_LOCAL.equals(uploadType)){
File file = new File(basePath + File.separator + bizPath + File.separator );
@ -155,9 +153,9 @@ public class CommonUtils {
*/
public static String uploadLocal(MultipartFile mf,String bizPath,String uploadpath){
try {
// 文件安全校验,防止上传漏洞文件
SsrfFileTypeFilter.checkUploadFileType(mf, bizPath);
//update-begin-author:liusq date:20210809 for: 过滤上传文件类型
SsrfFileTypeFilter.checkUploadFileType(mf);
//update-end-author:liusq date:20210809 for: 过滤上传文件类型
String fileName = null;
File file = new File(uploadpath + File.separator + bizPath + File.separator );
if (!file.exists()) {
@ -368,7 +366,7 @@ public class CommonUtils {
}else{
baseDomainPath = scheme + "://" + serverName + ":" + serverPort + contextPath ;
}
log.info("-----Common getBaseUrl----- : " + baseDomainPath);
log.debug("-----Common getBaseUrl----- : " + baseDomainPath);
return baseDomainPath;
}

View File

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

View File

@ -1,7 +1,6 @@
package org.jeecg.common.util;
import cn.hutool.core.io.IoUtil;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;
@ -11,6 +10,7 @@ import org.jeecg.common.constant.CommonConstant;
import org.jeecg.common.exception.JeecgBootException;
import org.jeecg.common.util.filter.SsrfFileTypeFilter;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -335,12 +335,13 @@ public class SqlInjectionUtil {
return table;
}
// 代码逻辑说明: 表单设计器列表翻译存在表名带条件,导致翻译出问题----
//update-begin---author:scott ---date:2024-05-28 for表单设计器列表翻译存在表名带条件,导致翻译出问题----
int index = table.toLowerCase().indexOf(" where ");
if (index != -1) {
table = table.substring(0, index);
log.info("截掉where之后的新表名" + table);
}
//update-end---author:scott ---date::2024-05-28 for表单设计器列表翻译存在表名带条件导致翻译出问题----
table = table.trim();
/**

View File

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

View File

@ -11,7 +11,7 @@ import org.jeecg.common.exception.JeecgBoot401Exception;
import org.jeecg.common.system.util.JwtUtil;
import org.jeecg.common.system.vo.LoginUser;
import jakarta.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequest;
/**
* @Author scott
@ -121,9 +121,7 @@ public class TokenUtils {
}
// 校验token是否超时失效 & 或者账号密码是否错误
if (!jwtTokenRefresh(token, username, user.getPassword(), redisUtil)) {
// 用户登录Token过期提示信息
String userLoginTokenErrorMsg = oConvertUtils.getString(redisUtil.get(CommonConstant.PREFIX_USER_TOKEN_ERROR_MSG + token));
throw new JeecgBoot401Exception(oConvertUtils.isEmpty(userLoginTokenErrorMsg)? CommonConstant.TOKEN_IS_INVALID_MSG: userLoginTokenErrorMsg);
throw new JeecgBoot401Exception(CommonConstant.TOKEN_IS_INVALID_MSG);
}
return true;
}
@ -141,15 +139,10 @@ public class TokenUtils {
if (oConvertUtils.isNotEmpty(cacheToken)) {
// 校验token有效性
if (!JwtUtil.verify(cacheToken, userName, passWord)) {
// 从token中解析客户端类型保持续期时使用相同的客户端类型
String clientType = JwtUtil.getClientType(token);
String newAuthorization = JwtUtil.sign(userName, passWord, clientType);
// 根据客户端类型设置对应的缓存有效时间
long expireTime = CommonConstant.CLIENT_TYPE_APP.equalsIgnoreCase(clientType)
? JwtUtil.APP_EXPIRE_TIME * 2 / 1000
: JwtUtil.EXPIRE_TIME * 2 / 1000;
String newAuthorization = JwtUtil.sign(userName, passWord);
// 设置Toekn缓存有效时间
redisUtil.set(CommonConstant.PREFIX_USER_TOKEN + token, newAuthorization);
redisUtil.expire(CommonConstant.PREFIX_USER_TOKEN + token, expireTime);
redisUtil.expire(CommonConstant.PREFIX_USER_TOKEN + token, JwtUtil.EXPIRE_TIME * 2 / 1000);
}
return true;
}

View File

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

View File

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

View File

@ -2,7 +2,6 @@ package org.jeecg.common.util.filter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.jeecg.common.exception.JeecgBootException;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
@ -150,38 +149,29 @@ public class SsrfFileTypeFilter {
public static void checkDownloadFileType(String filePath) throws IOException {
//文件后缀
String suffix = getFileTypeBySuffix(filePath);
log.debug(" 【文件下载校验】文件后缀 suffix: {}", suffix);
log.info("suffix:{}", suffix);
boolean isAllowExtension = FILE_TYPE_WHITE_LIST.contains(suffix.toLowerCase());
//是否允许下载的文件
if (!isAllowExtension) {
throw new JeecgBootException("下载失败,存在非法文件类型:" + suffix);
throw new IOException("下载失败,存在非法文件类型:" + suffix);
}
}
/**
* 上传文件类型过滤
*
* @param file
*/
public static void checkUploadFileType(MultipartFile file) throws Exception {
checkUploadFileType(file, null);
}
/**
* 上传文件类型过滤
*
* @param file
*/
public static void checkUploadFileType(MultipartFile file, String customPath) throws Exception {
//1. 路径安全校验
validatePathSecurity(customPath);
//2. 校验文件后缀和头
String suffix = getFileType(file, customPath);
log.info("【文件上传校验】文件后缀 suffix: {}customPath{}", suffix, customPath);
//获取文件真是后缀
String suffix = getFileType(file);
log.info("suffix:{}", suffix);
boolean isAllowExtension = FILE_TYPE_WHITE_LIST.contains(suffix.toLowerCase());
//是否允许下载的文件
if (!isAllowExtension) {
throw new JeecgBootException("上传失败,存在非法文件类型:" + suffix);
throw new Exception("上传失败,存在非法文件类型:" + suffix);
}
}
@ -193,8 +183,8 @@ public class SsrfFileTypeFilter {
* @throws Exception
*/
private static String getFileType(MultipartFile file, String customPath) throws Exception {
// 代码逻辑说明: [issue/4672]方法造成的文件被占用注释掉此方法tomcat就能自动清理掉临时文件
private static String getFileType(MultipartFile file) throws Exception {
//update-begin-author:liusq date:20230404 for: [issue/4672]方法造成的文件被占用注释掉此方法tomcat就能自动清理掉临时文件
String fileExtendName = null;
InputStream is = null;
try {
@ -213,7 +203,7 @@ public class SsrfFileTypeFilter {
break;
}
}
log.debug("-----获取到的指定文件类型------"+fileExtendName);
log.info("-----获取到的指定文件类型------"+fileExtendName);
// 如果不是上述类型,则判断扩展名
if (StringUtils.isBlank(fileExtendName)) {
String fileName = file.getOriginalFilename();
@ -224,6 +214,7 @@ public class SsrfFileTypeFilter {
// 如果有扩展名,则返回扩展名
return getFileTypeBySuffix(fileName);
}
log.info("-----最終的文件类型------"+fileExtendName);
is.close();
return fileExtendName;
} catch (Exception e) {
@ -234,6 +225,7 @@ public class SsrfFileTypeFilter {
is.close();
}
}
//update-end-author:liusq date:20230404 for: [issue/4672]方法造成的文件被占用注释掉此方法tomcat就能自动清理掉临时文件
}
/**
@ -257,67 +249,4 @@ public class SsrfFileTypeFilter {
}
return stringBuilder.toString();
}
/**
* 路径安全校验
*/
private static void validatePathSecurity(String customPath) throws JeecgBootException {
if (customPath == null || customPath.trim().isEmpty()) {
return;
}
// 统一分隔符为 /
String normalized = customPath.replace("\\", "/");
// 1. 防止路径遍历攻击
if (normalized.contains("..") || normalized.contains("~")) {
throw new JeecgBootException("上传业务路径包含非法字符!");
}
// 2. 限制路径深度
int depth = normalized.split("/").length;
if (depth > 5) {
throw new JeecgBootException("上传业务路径深度超出限制!");
}
// 3. 限制字符集(只允许字母、数字、下划线、横线、斜杠)
if (!normalized.matches("^[a-zA-Z0-9/_-]+$")) {
throw new JeecgBootException("上传业务路径包含非法字符!");
}
}
/**
* 校验文件路径安全性,防止路径遍历攻击
* @param filePath 文件路径
*/
public static void checkPathTraversal(String filePath) {
if (StringUtils.isBlank(filePath)) {
return;
}
// 1. 防止路径遍历:不允许 ..
if (filePath.contains("..")) {
throw new JeecgBootException("文件路径包含非法字符");
}
// 2. 防止URL编码绕过%2e = .
String fileLower = filePath.toLowerCase();
if (fileLower.contains("%2e")) {
throw new JeecgBootException("文件路径包含非法字符");
}
}
/**
* 批量校验文件路径安全性(逗号分隔的多个文件路径)
* @param files 逗号分隔的文件路径
*/
public static void checkPathTraversalBatch(String files) {
if (StringUtils.isBlank(files)) {
return;
}
for (String file : files.split(",")) {
if (StringUtils.isNotBlank(file)) {
checkPathTraversal(file.trim());
}
}
}
}

View File

@ -10,11 +10,7 @@ import org.jeecg.common.constant.SymbolConstant;
import org.jeecg.config.mybatis.MybatisPlusSaasConfig;
import org.springframework.beans.BeanUtils;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.beans.BeanWrapper;
import org.springframework.beans.BeanWrapperImpl;
import java.beans.PropertyDescriptor;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
@ -27,7 +23,6 @@ import java.sql.Date;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Stream;
/**
*
@ -568,8 +563,10 @@ public class oConvertUtils {
return "";
} else if (!name.contains(SymbolConstant.UNDERLINE)) {
// 不含下划线,仅将首字母小写
// 代码逻辑说明: TASK #2500 【代码生成器】代码生成器开发一通用模板生成功能
//update-begin--Author:zhoujf Date:20180503 forTASK #2500 【代码生成器】代码生成器开发一通用模板生成功能
//update-begin--Author:zhoujf Date:20180503 forTASK #2500 【代码生成器】代码生成器开发一通用模板生成功能
return name.substring(0, 1).toLowerCase() + name.substring(1).toLowerCase();
//update-end--Author:zhoujf Date:20180503 forTASK #2500 【代码生成器】代码生成器开发一通用模板生成功能
}
// 用下划线将原始字符串分割
String[] camels = name.split("_");
@ -614,6 +611,7 @@ public class oConvertUtils {
return result.substring(0, result.length() - 1);
}
//update-begin--Author:zhoujf Date:20180503 forTASK #2500 【代码生成器】代码生成器开发一通用模板生成功能
/**
* 将下划线大写方式命名的字符串转换为驼峰式。(首字母写)
* 如果转换前的下划线大写方式命名的字符串为空,则返回空字符串。</br>
@ -646,6 +644,7 @@ public class oConvertUtils {
}
return result.toString();
}
//update-end--Author:zhoujf Date:20180503 forTASK #2500 【代码生成器】代码生成器开发一通用模板生成功能
/**
* 将驼峰命名转化成下划线
@ -983,18 +982,17 @@ public class oConvertUtils {
/**
* 判断 sourceList中的元素是否在targetList中出现
*
* 判断 list1中的元素是否在list2中出现
* QQYUN-5326【简流】获取组织人员 单/多 筛选条件 没有部门筛选
* @param sourceList 源列表,要检查的元素列表
* @param targetList 目标列表,用于匹配的列表
* @return 如果sourceList中有任何元素在targetList中存在则返回true否则返回false
* @param list1
* @param list2
* @return
*/
public static boolean isInList(List<String> sourceList, List<String> targetList){
for(String sourceItem: sourceList){
public static boolean isInList(List<String> list1, List<String> list2){
for(String str1: list1){
boolean flag = false;
for(String targetItem: targetList){
if(sourceItem.equals(targetItem)){
for(String str2: list2){
if(str1.equals(str2)){
flag = true;
break;
}
@ -1006,35 +1004,6 @@ public class oConvertUtils {
return false;
}
/**
* 判断 sourceList中的所有元素是否都在targetList中存在
* @param sourceList 源列表,要检查的元素列表
* @param targetList 目标列表,用于匹配的列表
* @return 如果sourceList中的所有元素都在targetList中存在则返回true否则返回false
*/
public static boolean isAllInList(List<String> sourceList, List<String> targetList){
if(sourceList == null || sourceList.isEmpty()){
return true; // 空列表视为所有元素都存在
}
if(targetList == null || targetList.isEmpty()){
return false; // 目标列表为空源列表非空时返回false
}
for(String sourceItem: sourceList){
boolean found = false;
for(String targetItem: targetList){
if(sourceItem.equals(targetItem)){
found = true;
break;
}
}
if(!found){
return false; // 有任何一个元素不在目标列表中返回false
}
}
return true; // 所有元素都找到了
}
/**
* 计算文件大小转成MB
* @param uploadCount
@ -1199,58 +1168,5 @@ public class oConvertUtils {
public static boolean isEffectiveTenant(String tenantId) {
return MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL && isNotEmpty(tenantId) && !("0").equals(tenantId);
}
/**
* 复制源对象的非空属性到目标对象(同名属性)
*
* @param source 源对象(页面)
* @param target 目标对象(数据库实体)
*/
public static void copyNonNullFields(Object source, Object target) {
if (source == null || target == null) {
return;
}
// 获取源对象的非空属性名数组
String[] nullPropertyNames = getNullPropertyNames(source);
// 复制:忽略源对象的空属性,仅覆盖目标对象的对应非空属性
BeanUtils.copyProperties(source, target, nullPropertyNames);
}
/**
* 获取源对象中值为 null 的属性名数组
*
* @param source
*/
private static String[] getNullPropertyNames(Object source) {
BeanWrapper beanWrapper = new BeanWrapperImpl(source);
//获取类的属性
PropertyDescriptor[] propertyDescriptors = beanWrapper.getPropertyDescriptors();
// 过滤出值为 null 的属性名
return Stream.of(propertyDescriptors)
.map(PropertyDescriptor::getName)
.filter(name -> beanWrapper.getPropertyValue(name) == null)
.toArray(String[]::new);
}
/**
* String转换long类型
*
* @param v
* @param def
* @return
*/
public static long getLong(Object v, long def) {
if (v == null) {
return def;
};
if (v instanceof Number) {
return ((Number) v).longValue();
}
try {
return Long.parseLong(v.toString());
} catch (Exception e) {
return def;
}
}
}

View File

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

View File

@ -63,7 +63,7 @@ public abstract class AbstractQueryBlackListHandler {
if(list==null){
return true;
}
log.debug(" 获取sql信息 {} ", list.toString());
log.info(" 获取sql信息 {} ", list.toString());
boolean flag = checkTableAndFieldsName(list);
if(flag == false){
return false;

View File

@ -1,24 +0,0 @@
package org.jeecg.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* ai配置类通用的配置可以放到这里面
*
* @Author: wangshuai
* @Date: 2025/12/17 14:00
*/
@Data
@Component
@ConfigurationProperties(prefix = AiRagConfigBean.PREFIX)
public class AiRagConfigBean {
public static final String PREFIX = "jeecg.airag";
/**
* 敏感节点
* stdio mpc命令行功能开启sqlAI流程SQL节点开启
*/
private String allowSensitiveNodes = "";
}

View File

@ -3,7 +3,7 @@ package org.jeecg.config;
import java.util.ArrayList;
import java.util.List;
import jakarta.annotation.Resource;
import javax.annotation.Resource;
import org.jeecg.common.api.CommonAPI;
import org.jeecg.common.system.vo.DictModel;
@ -60,15 +60,17 @@ public class AutoPoiDictConfig implements AutoPoiDictServiceI {
for (DictModel t : dictList) {
// 代码逻辑说明: [issues/4917]excel 导出异常---
//update-begin---author:liusq Date:20230517 for[issues/4917]excel 导出异常---
if(t!=null && t.getText()!=null && t.getValue()!=null){
// 代码逻辑说明: [issues/I4MBB3]@Excel dicText字段的值有下划线时导入功能不能正确解析---
//update-end---author:liusq Date:20230517 for[issues/4917]excel 导出异常---
//update-begin---author:scott Date:20211220 for[issues/I4MBB3]@Excel dicText字段的值有下划线时导入功能不能正确解析---
if(t.getValue().contains(EXCEL_SPLIT_TAG)){
String val = t.getValue().replace(EXCEL_SPLIT_TAG,TEMP_EXCEL_SPLIT_TAG);
dictReplaces.add(t.getText() + EXCEL_SPLIT_TAG + val);
}else{
dictReplaces.add(t.getText() + EXCEL_SPLIT_TAG + t.getValue());
}
//update-end---author:20211220 Date:20211220 for[issues/I4MBB3]@Excel dicText字段的值有下划线时导入功能不能正确解析---
}
}
if (dictReplaces != null && dictReplaces.size() != 0) {

View File

@ -2,9 +2,7 @@ package org.jeecg.config;
import java.io.IOException;
import com.alibaba.druid.spring.boot3.autoconfigure.DruidDataSourceAutoConfigure;
import com.alibaba.druid.spring.boot3.autoconfigure.properties.DruidStatProperties;
import jakarta.servlet.*;
import javax.servlet.*;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
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.Configuration;
import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure;
import com.alibaba.druid.spring.boot.autoconfigure.properties.DruidStatProperties;
import com.alibaba.druid.util.Utils;
/**

View File

@ -1,12 +1,7 @@
package org.jeecg.config;
import lombok.Getter;
import lombok.Setter;
import org.jeecg.config.tencent.JeecgTencent;
import org.jeecg.config.vo.*;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Role;
import org.springframework.stereotype.Component;
@ -16,7 +11,6 @@ import org.springframework.stereotype.Component;
*/
@Component("jeecgBaseConfig")
@ConfigurationProperties(prefix = "jeecg")
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public class JeecgBaseConfig {
/**
* 签名密钥串(字典等敏感接口)
@ -76,35 +70,7 @@ public class JeecgBaseConfig {
/**
* 百度开放API配置
*/
private BaiduApi baiduApi;
/**
* minio配置
*/
@Getter
@Setter
private JeecgMinio minio;
/**
* oss配置
*/
@Getter
@Setter
private JeecgOSS oss;
/**
* 短信发送方式 aliyun阿里云短信 tencent腾讯云短信
*/
@Getter
@Setter
private String smsSendType = "aliyun";
/**
* 腾讯配置
*/
@Getter
@Setter
private JeecgTencent tencent;
private BaiduApi baiduApi;
public String getCustomResourcePrefixPath() {
return customResourcePrefixPath;

View File

@ -95,11 +95,13 @@
// List<Parameter> pars = new ArrayList<>();
// tokenPar.name(CommonConstant.X_ACCESS_TOKEN).description("token").modelRef(new ModelRef("string")).parameterType("header").required(false).build();
// pars.add(tokenPar.build());
// //update-begin-author:liusq---date:2024-08-15--for: 开启多租户时全局参数增加租户id
// if(MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL){
// ParameterBuilder tenantPar = new ParameterBuilder();
// tenantPar.name(CommonConstant.TENANT_ID).description("租户ID").modelRef(new ModelRef("string")).parameterType("header").required(false).build();
// pars.add(tenantPar.build());
// }
// //update-end-author:liusq---date:2024-08-15--for: 开启多租户时全局参数增加租户id
//
// return pars;
// }

View File

@ -10,37 +10,28 @@ import io.swagger.v3.oas.models.security.SecurityRequirement;
import io.swagger.v3.oas.models.security.SecurityScheme;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.constant.CommonConstant;
import org.springdoc.core.customizers.OperationCustomizer;
import org.springdoc.core.customizers.GlobalOpenApiCustomizer;
import org.springdoc.core.filters.GlobalOpenApiMethodFilter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
/**
* @author eightmonth
*/
@Slf4j
@Configuration
@ConditionalOnProperty(prefix = "knife4j", name = "production", havingValue = "false", matchIfMissing = true)
@PropertySource("classpath:config/default-spring-doc.properties")
public class Swagger3Config implements WebMvcConfigurer {
// 路径匹配结果缓存,避免重复计算
private static final Map<String, Boolean> EXCLUDED_PATHS_CACHE = new ConcurrentHashMap<>();
// 定义不需要注入安全要求的路径集合
private static final Set<String> excludedPaths = new HashSet<>(Arrays.asList(
"/sys/randomImage/**",
Set<String> excludedPaths = new HashSet<>(Arrays.asList(
"/sys/randomImage/{key}",
"/sys/login",
"/sys/phoneLogin",
"/sys/mLogin",
@ -50,20 +41,7 @@ public class Swagger3Config implements WebMvcConfigurer {
"/sys/thirdLogin/**",
"/sys/user/register"
));
// 预处理通配符模式,提高匹配效率
private static final Set<String> wildcardPatterns = new HashSet<>();
private static final Set<String> exactPatterns = new HashSet<>();
static {
// 初始化时分离精确匹配和通配符匹配
for (String pattern : excludedPaths) {
if (pattern.endsWith("/**")) {
wildcardPatterns.add(pattern.substring(0, pattern.length() - 3));
} else {
exactPatterns.add(pattern);
}
}
}
/**
*
* 显示swagger-ui.html文档展示页还必须注入swagger资源
@ -83,70 +61,40 @@ public class Swagger3Config implements WebMvcConfigurer {
}
@Bean
public OperationCustomizer operationCustomizer() {
return (operation, handlerMethod) -> {
String path = getFullPath(handlerMethod);
if (!isExcludedPath(path)) {
operation.addSecurityItem(new SecurityRequirement().addList(CommonConstant.X_ACCESS_TOKEN));
}else{
log.info("忽略加入 X_ACCESS_TOKEN 的 PATH:" + path);
public GlobalOpenApiCustomizer globalOpenApiCustomizer() {
return openApi -> {
// 全局添加鉴权参数
if (openApi.getPaths() != null) {
openApi.getPaths().forEach((path, pathItem) -> {
//log.debug("path: {}", path);
// 检查当前路径是否在排除列表中
boolean isExcluded = excludedPaths.stream().anyMatch(
excludedPath -> excludedPath.equals(path) || (excludedPath.endsWith("**") && path.startsWith(excludedPath.substring(0, excludedPath.length() - 2)))
);
if (!isExcluded) {
// 接口添加鉴权参数
pathItem.readOperations().forEach(operation ->
operation.addSecurityItem(new SecurityRequirement().addList(CommonConstant.X_ACCESS_TOKEN))
);
}
});
}
return operation;
};
}
private String getFullPath(HandlerMethod handlerMethod) {
StringBuilder fullPath = new StringBuilder();
// 获取类级别的路径
RequestMapping classMapping = handlerMethod.getBeanType().getAnnotation(RequestMapping.class);
if (classMapping != null && classMapping.value().length > 0) {
fullPath.append(classMapping.value()[0]);
}
// 获取方法级别的路径
RequestMapping methodMapping = handlerMethod.getMethodAnnotation(RequestMapping.class);
if (methodMapping != null && methodMapping.value().length > 0) {
String methodPath = methodMapping.value()[0];
// 确保路径正确拼接,处理斜杠
if (!fullPath.toString().endsWith("/") && !methodPath.startsWith("/")) {
fullPath.append("/");
}
fullPath.append(methodPath);
}
return fullPath.toString();
}
private boolean isExcludedPath(String path) {
// 使用缓存避免重复计算
return EXCLUDED_PATHS_CACHE.computeIfAbsent(path, p -> {
// 精确匹配
if (exactPatterns.contains(p)) {
return true;
}
// 通配符匹配
return wildcardPatterns.stream().anyMatch(p::startsWith);
});
}
@Bean
public OpenAPI customOpenAPI() {
return new OpenAPI()
.info(new Info()
.title("JeecgBoot 后台服务API接口文档")
.version("3.9.1")
.version("3.8.3")
.contact(new Contact().name("北京国炬信息技术有限公司").url("www.jeccg.com").email("jeecgos@163.com"))
.description("后台API接口")
.description( "后台API接口")
.termsOfService("NO terms of service")
.license(new License().name("Apache 2.0").url("http://www.apache.org/licenses/LICENSE-2.0.html")))
.addSecurityItem(new SecurityRequirement().addList(CommonConstant.X_ACCESS_TOKEN))
.components(new Components().addSecuritySchemes(CommonConstant.X_ACCESS_TOKEN,
new SecurityScheme()
.name(CommonConstant.X_ACCESS_TOKEN)
.type(SecurityScheme.Type.APIKEY)
.in(SecurityScheme.In.HEADER) // 关键:指定为 header
));
new SecurityScheme().name(CommonConstant.X_ACCESS_TOKEN).type(SecurityScheme.Type.HTTP)));
}
}

View File

@ -1,32 +0,0 @@
package org.jeecg.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
/**
* 任务调度器配置
* 提供 ThreadPoolTaskScheduler Bean 用于 AI RAG 流程调度等功能
* 仅当容器中不存在 ThreadPoolTaskScheduler 时才创建
*
* @author jeecg
*/
@Slf4j
@Configuration
public class TaskSchedulerConfig {
@Bean
@ConditionalOnMissingBean(ThreadPoolTaskScheduler.class)
public ThreadPoolTaskScheduler taskScheduler() {
log.info("初始化定时任务调度器 ThreadPoolTaskScheduler");
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
scheduler.setPoolSize(10);
scheduler.setThreadNamePrefix("airag-scheduler-");
scheduler.setWaitForTasksToCompleteOnShutdown(true);
scheduler.setAwaitTerminationSeconds(60);
return scheduler;
}
}

View File

@ -10,18 +10,17 @@ import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;
import io.micrometer.prometheusmetrics.PrometheusMeterRegistry;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import io.micrometer.prometheus.PrometheusMeterRegistry;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.context.event.EventListener;
import org.springframework.http.CacheControl;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
@ -33,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.WebMvcConfigurer;
import javax.annotation.Resource;
import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.time.LocalDateTime;
@ -47,7 +47,6 @@ import java.util.concurrent.TimeUnit;
* @Author qinfeng
*
*/
@Slf4j
@Configuration
public class WebMvcConfiguration implements WebMvcConfigurer {
@ -142,6 +141,7 @@ public class WebMvcConfiguration implements WebMvcConfigurer {
return objectMapper;
}
//update-begin---author:chenrui ---date:20240514 for[QQYUN-9247]系统监控功能优化------------
// /**
// * SpringBootAdmin的Httptrace不见了
// * https://blog.csdn.net/u013810234/article/details/110097201
@ -150,20 +150,20 @@ public class WebMvcConfiguration implements WebMvcConfigurer {
// public InMemoryHttpTraceRepository getInMemoryHttpTrace(){
// return new InMemoryHttpTraceRepository();
// }
//update-end---author:chenrui ---date:20240514 for[QQYUN-9247]系统监控功能优化------------
/**
* 在Bean初始化完成后立即配置PrometheusMeterRegistry避免在Meter注册后才配置MeterFilter
* 监听应用启动完成事件,确保 PrometheusMeterRegistry 已经初始化
* for [QQYUN-12558]【监控】系统监控的头两个tab不好使接口404
* @param event
* @author chenrui
* @date 2025/5/26 16:46
*/
@PostConstruct
public void initPrometheusMeterRegistry() {
// 确保在应用启动早期就配置MeterFilter避免警告
if (null != meterRegistryPostProcessor && null != prometheusMeterRegistry) {
meterRegistryPostProcessor.postProcessAfterInitialization(prometheusMeterRegistry, "prometheusMeterRegistry");
log.info("PrometheusMeterRegistry 配置完成");
@EventListener
public void onApplicationReady(ApplicationReadyEvent event) {
if(null != meterRegistryPostProcessor){
meterRegistryPostProcessor.postProcessAfterInitialization(prometheusMeterRegistry, "");
}
}

View File

@ -3,8 +3,8 @@ package org.jeecg.config.filter;
import org.jeecg.common.constant.CommonConstant;
import org.jeecg.config.sign.util.BodyReaderHttpServletRequestWrapper;
import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
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.oConvertUtils;
import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**

View File

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

View File

@ -129,18 +129,20 @@ public class MybatisInterceptor implements Interceptor {
Field[] fields = null;
if (parameter instanceof ParamMap) {
ParamMap<?> p = (ParamMap<?>) parameter;
// 代码逻辑说明: 批量更新报错issues/IZA3Q--
//update-begin-author:scott date:20190729 for:批量更新报错issues/IZA3Q--
String et = "et";
if (p.containsKey(et)) {
parameter = p.get(et);
} else {
parameter = p.get("param1");
}
//update-end-author:scott date:20190729 for:批量更新报错issues/IZA3Q-
// 代码逻辑说明: 更新指定字段时报错 issues/#516-
//update-begin-author:scott date:20190729 for:更新指定字段时报错 issues/#516-
if (parameter == null) {
return invocation.proceed();
}
//update-end-author:scott date:20190729 for:更新指定字段时报错 issues/#516-
fields = oConvertUtils.getAllFields(parameter);
} else {
@ -182,6 +184,7 @@ public class MybatisInterceptor implements Interceptor {
// TODO Auto-generated method stub
}
//update-begin--Author:scott Date:20191213 for关于使用Quzrtz 开启线程任务, #465
/**
* 获取登录用户
* @return
@ -196,5 +199,6 @@ public class MybatisInterceptor implements Interceptor {
}
return sysUser;
}
//update-end--Author:scott Date:20191213 for关于使用Quzrtz 开启线程任务, #465
}

View File

@ -11,7 +11,7 @@ import org.jeecg.common.util.SpringContextUtils;
import org.jeecg.config.mybatis.ThreadLocalDataHelper;
import org.springframework.stereotype.Component;
import jakarta.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequest;
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.ModelAndView;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* 动态数据源切换拦截器

View File

@ -1,6 +1,5 @@
package org.jeecg.config.oss;
import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.constant.CommonConstant;
import org.jeecg.common.constant.SymbolConstant;
@ -29,7 +28,7 @@ public class MinioConfig {
@Value(value = "${jeecg.minio.bucketName}")
private String bucketName;
@PostConstruct
@Bean
public void initMinio(){
if(!minioUrl.startsWith(CommonConstant.STR_HTTP)){
minioUrl = "http://" + minioUrl;

View File

@ -1,6 +1,5 @@
package org.jeecg.config.oss;
import jakarta.annotation.PostConstruct;
import org.jeecg.common.util.oss.OssBootUtil;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
@ -29,7 +28,7 @@ public class OssConfiguration {
private String staticDomain;
@PostConstruct
@Bean
public void initOssBootConfiguration() {
OssBootUtil.setEndPoint(endpoint);
OssBootUtil.setAccessKeyId(accessKeyId);

View File

@ -1,8 +1,5 @@
package org.jeecg.config.shiro;
import jakarta.annotation.Resource;
import jakarta.servlet.DispatcherType;
import jakarta.servlet.Filter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
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.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.spring.web.ShiroUrlPathHelper;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.crazycake.shiro.*;
import org.jeecg.common.constant.CommonConstant;
@ -33,10 +29,12 @@ import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.filter.DelegatingFilterProxy;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import redis.clients.jedis.HostAndPort;
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.*;
@ -113,7 +111,7 @@ public class ShiroConfig {
filterChainDefinitionMap.put("/sys/checkAuth", "anon"); //授权接口排除
filterChainDefinitionMap.put("/openapi/call/**", "anon"); // 开放平台接口排除
// 代码逻辑说明: 排除静态资源后缀
//update-begin--Author:scott Date:20221116 for排除静态资源后缀
filterChainDefinitionMap.put("/", "anon");
filterChainDefinitionMap.put("/doc.html", "anon");
filterChainDefinitionMap.put("/**/*.js", "anon");
@ -131,6 +129,7 @@ public class ShiroConfig {
filterChainDefinitionMap.put("/**/*.glb", "anon");
filterChainDefinitionMap.put("/**/*.wasm", "anon");
//update-end--Author:scott Date:20221116 for排除静态资源后缀
filterChainDefinitionMap.put("/druid/**", "anon");
filterChainDefinitionMap.put("/swagger-ui.html", "anon");
@ -138,7 +137,9 @@ public class ShiroConfig {
filterChainDefinitionMap.put("/webjars/**", "anon");
filterChainDefinitionMap.put("/v3/**", "anon");
// update-begin--Author:sunjianlei Date:20210510 for排除消息通告查看详情页面用于第三方APP
filterChainDefinitionMap.put("/sys/annountCement/show/**", "anon");
// update-end--Author:sunjianlei Date:20210510 for排除消息通告查看详情页面用于第三方APP
//积木报表排除
filterChainDefinitionMap.put("/jmreport/**", "anon");
@ -157,7 +158,7 @@ public class ShiroConfig {
filterChainDefinitionMap.put("/drag/onlDragDatasetHead/getTotalDataByCompId", "anon");
filterChainDefinitionMap.put("/drag/mock/json/**", "anon");
filterChainDefinitionMap.put("/drag/onlDragDatasetHead/getDictByCodes", "anon");
filterChainDefinitionMap.put("/drag/onlDragDatasetHead/queryAllById", "anon");
filterChainDefinitionMap.put("/jimubi/view", "anon");
filterChainDefinitionMap.put("/jimubi/share/view/**", "anon");
@ -190,8 +191,6 @@ public class ShiroConfig {
// 企业微信证书排除
filterChainDefinitionMap.put("/WW_verify*", "anon");
filterChainDefinitionMap.put("/openapi/call/**", "anon");
// 添加自己的过滤器并且取名为jwt
Map<String, Filter> filterMap = new HashMap<String, Filter>(1);
//如果cloudServer为空 则说明是单体 需要加载跨域配置【微服务跨域切换】
@ -208,6 +207,7 @@ public class ShiroConfig {
return shiroFilterFactoryBean;
}
//update-begin---author:chenrui ---date:20240126 for【QQYUN-7932】AI助手------------
/**
* spring过滤装饰器 <br/>
@ -223,24 +223,21 @@ public class ShiroConfig {
FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setFilter(new DelegatingFilterProxy("shiroFilterFactoryBean"));
registration.setEnabled(true);
// 代码逻辑说明: [issues/7491]运行时长,效率慢
//update-begin---author:chenrui ---date:20241202 for[issues/7491]运行时间好长,效率慢 ------------
registration.addUrlPatterns("/test/ai/chat/send");
//update-end---author:chenrui ---date:20241202 for[issues/7491]运行时间好长,效率慢 ------------
registration.addUrlPatterns("/airag/flow/run");
registration.addUrlPatterns("/airag/flow/debug");
registration.addUrlPatterns("/airag/chat/send");
registration.addUrlPatterns("/airag/app/debug");
registration.addUrlPatterns("/airag/app/prompt/generate");
registration.addUrlPatterns("/airag/chat/receive/**");
// 添加SSE接口的异步支持
registration.addUrlPatterns("/airag/extData/evaluator/debug");
registration.addUrlPatterns("/drag/onlDragDatasetHead/generateChartSse");
registration.addUrlPatterns("/drag/onlDragDatasetHead/updateChartOptSse");
registration.addUrlPatterns("/drag/onlDragDatasetHead/generateSqlSse");
//支持异步
registration.setAsyncSupported(true);
registration.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ASYNC);
return registration;
}
//update-end---author:chenrui ---date:20240126 for【QQYUN-7932】AI助手------------
@Bean("securityManager")
public DefaultWebSecurityManager securityManager(ShiroRealm myRealm) {
@ -361,23 +358,12 @@ public class ShiroConfig {
JedisCluster jedisCluster = new JedisCluster(portSet);
redisManager.setJedisCluster(jedisCluster);
}
//update-end--Author:scott Date:20210531 for修改集群模式下未设置redis密码的bug issues/I3QNIC
manager = redisManager;
}
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) {
List<String> urls = new ArrayList<>();
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.oConvertUtils;
import org.jeecg.config.mybatis.MybatisPlusSaasConfig;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.annotation.Lazy;
import org.springframework.context.annotation.Role;
import org.springframework.stereotype.Component;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.util.Set;
/**
@ -37,7 +35,6 @@ import java.util.Set;
*/
@Component
@Slf4j
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public class ShiroRealm extends AuthorizingRealm {
@Lazy
@Resource
@ -83,7 +80,7 @@ public class ShiroRealm extends AuthorizingRealm {
Set<String> permissionSet = commonApi.queryUserAuths(userId);
info.addStringPermissions(permissionSet);
//System.out.println(permissionSet);
log.debug("===============Shiro权限认证成功==============");
log.info("===============Shiro权限认证成功==============");
return info;
}
@ -110,8 +107,8 @@ public class ShiroRealm extends AuthorizingRealm {
loginUser = this.checkUserTokenIsEffect(token);
} catch (AuthenticationException e) {
log.error("—————校验 check token 失败——————————"+ e.getMessage(), e);
// 重新抛出异常让JwtFilter统一处理避免返回两次错误响应
throw e;
JwtUtil.responseError(SpringContextUtils.getHttpServletResponse(),401,e.getMessage());
return null;
}
return new SimpleAuthenticationInfo(loginUser, token, getName());
}
@ -141,11 +138,9 @@ public class ShiroRealm extends AuthorizingRealm {
}
// 校验token是否超时失效 & 或者账号密码是否错误
if (!jwtTokenRefresh(token, username, loginUser.getPassword())) {
// 用户登录Token过期提示信息
String userLoginTokenErrorMsg = oConvertUtils.getString(redisUtil.get(CommonConstant.PREFIX_USER_TOKEN_ERROR_MSG + token));
throw new AuthenticationException(oConvertUtils.isEmpty(userLoginTokenErrorMsg)? CommonConstant.TOKEN_IS_INVALID_MSG: userLoginTokenErrorMsg);
throw new AuthenticationException(CommonConstant.TOKEN_IS_INVALID_MSG);
}
// 代码逻辑说明: 校验用户的tenant_id和前端传过来的是否一致
//update-begin-author:taoyan date:20210609 for:校验用户的tenant_id和前端传过来的是否一致
String userTenantIds = loginUser.getRelTenantIds();
if(MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL && oConvertUtils.isNotEmpty(userTenantIds)){
String contextTenantId = TenantContext.getTenant();
@ -154,7 +149,7 @@ public class ShiroRealm extends AuthorizingRealm {
//登录用户无租户前端header中租户ID值为 0
String str ="0";
if(oConvertUtils.isNotEmpty(contextTenantId) && !str.equals(contextTenantId)){
// 代码逻辑说明: /issues/I4O14W 用户租户信息变更判断漏洞
//update-begin-author:taoyan date:20211227 for: /issues/I4O14W 用户租户信息变更判断漏洞
String[] arr = userTenantIds.split(",");
if(!oConvertUtils.isIn(contextTenantId, arr)){
boolean isAuthorization = false;
@ -179,8 +174,10 @@ public class ShiroRealm extends AuthorizingRealm {
}
//*********************************************
}
//update-end-author:taoyan date:20211227 for: /issues/I4O14W 用户租户信息变更判断漏洞
}
}
//update-end-author:taoyan date:20210609 for:校验用户的tenant_id和前端传过来的是否一致
return loginUser;
}
@ -202,22 +199,19 @@ public class ShiroRealm extends AuthorizingRealm {
if (oConvertUtils.isNotEmpty(cacheToken)) {
// 校验token有效性
if (!JwtUtil.verify(cacheToken, userName, passWord)) {
// 从token中解析客户端类型保持续期时使用相同的客户端类型
String clientType = JwtUtil.getClientType(token);
String newAuthorization = JwtUtil.sign(userName, passWord, clientType);
// 根据客户端类型设置对应的缓存有效时间
long expireTime = CommonConstant.CLIENT_TYPE_APP.equalsIgnoreCase(clientType)
? JwtUtil.APP_EXPIRE_TIME * 2 / 1000
: JwtUtil.EXPIRE_TIME * 2 / 1000;
String newAuthorization = JwtUtil.sign(userName, passWord);
// 设置超时时间
redisUtil.set(CommonConstant.PREFIX_USER_TOKEN + token, newAuthorization);
redisUtil.expire(CommonConstant.PREFIX_USER_TOKEN + token, expireTime);
redisUtil.expire(CommonConstant.PREFIX_USER_TOKEN + token, JwtUtil.EXPIRE_TIME *2 / 1000);
log.debug("——————————用户在线操作更新token保证不掉线—————————jwtTokenRefresh——————— "+ token);
}
//update-begin--Author:scott Date:20191005 for解决每次请求都重写redis中 token缓存问题
// else {
// // 设置超时时间
// redisUtil.set(CommonConstant.PREFIX_USER_TOKEN + token, cacheToken);
// redisUtil.expire(CommonConstant.PREFIX_USER_TOKEN + token, JwtUtil.EXPIRE_TIME / 1000);
// }
//update-end--Author:scott Date:20191005 for解决每次请求都重写redis中 token缓存问题
return true;
}
@ -233,7 +227,8 @@ public class ShiroRealm extends AuthorizingRealm {
@Override
public void clearCache(PrincipalCollection principals) {
super.clearCache(principals);
// 代码逻辑说明: 【TV360X-1320】分配权限必须退出重新登录才生效造成很多用户困扰---
//update-begin---author:scott ---date::2024-06-18 for【TV360X-1320】分配权限必须退出重新登录才生效造成很多用户困扰---
super.clearCachedAuthorizationInfo(principals);
//update-end---author:scott ---date::2024-06-18 for【TV360X-1320】分配权限必须退出重新登录才生效造成很多用户困扰---
}
}

View File

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

View File

@ -13,10 +13,10 @@ import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.RequestMethod;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* @Description: 鉴权登录拦截器
@ -56,13 +56,9 @@ public class JwtFilter extends BasicHttpAuthenticationFilter {
executeLogin(request, response);
return true;
} catch (Exception e) {
// 使用异常中的具体错误信息,保留"不允许同一账号多地同时登录"等具体提示
String errorMsg = e.getMessage();
if (oConvertUtils.isEmpty(errorMsg)) {
errorMsg = CommonConstant.TOKEN_IS_INVALID_MSG;
}
JwtUtil.responseError((HttpServletResponse)response, 401, errorMsg);
JwtUtil.responseError((HttpServletResponse)response,401,CommonConstant.TOKEN_IS_INVALID_MSG);
return false;
//throw new AuthenticationException("Token失效请重新登录", e);
}
}
@ -73,10 +69,11 @@ public class JwtFilter extends BasicHttpAuthenticationFilter {
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
String token = httpServletRequest.getHeader(CommonConstant.X_ACCESS_TOKEN);
// 代码逻辑说明: JT-355 OA聊天添加token验证获取token参数
// update-begin--Author:lvdandan Date:20210105 forJT-355 OA聊天添加token验证获取token参数
if (oConvertUtils.isEmpty(token)) {
token = httpServletRequest.getParameter("token");
}
// update-end--Author:lvdandan Date:20210105 forJT-355 OA聊天添加token验证获取token参数
JwtToken jwtToken = new JwtToken(token);
// 提交给realm进行登入如果错误他会抛出异常并被捕获
@ -109,9 +106,10 @@ public class JwtFilter extends BasicHttpAuthenticationFilter {
httpServletResponse.setStatus(HttpStatus.OK.value());
return false;
}
// 代码逻辑说明: 多租户用到
//update-begin-author:taoyan date:20200708 for:多租户用到
String tenantId = httpServletRequest.getHeader(CommonConstant.TENANT_ID);
TenantContext.setTenant(tenantId);
//update-end-author:taoyan date:20200708 for:多租户用到
return super.preHandle(request, response);
}

View File

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

View File

@ -4,7 +4,6 @@ import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.config.shiro.IgnoreAuth;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.*;
@ -21,7 +20,6 @@ import java.util.stream.Collectors;
* @date 2024/4/18 11:35
*/
@Slf4j
@Lazy(false)
@Component
@AllArgsConstructor
public class IgnoreAuthPostProcessor implements InitializingBean {
@ -35,15 +33,10 @@ public class IgnoreAuthPostProcessor implements InitializingBean {
long startTime = System.currentTimeMillis();
List<String> ignoreAuthUrls = new ArrayList<>();
// 优化直接从HandlerMethod过滤避免重复扫描
requestMappingHandlerMapping.getHandlerMethods().values().stream()
.filter(handlerMethod -> handlerMethod.getMethod().isAnnotationPresent(IgnoreAuth.class))
.forEach(handlerMethod -> {
Class<?> clazz = handlerMethod.getBeanType();
Method method = handlerMethod.getMethod();
ignoreAuthUrls.addAll(processIgnoreAuthMethod(clazz, method));
});
Set<Class<?>> restControllers = requestMappingHandlerMapping.getHandlerMethods().values().stream().map(HandlerMethod::getBeanType).collect(Collectors.toSet());
for (Class<?> restController : restControllers) {
ignoreAuthUrls.addAll(postProcessRestController(restController));
}
log.info("Init Token ignoreAuthUrls Config [ 集合 ] {}", ignoreAuthUrls);
if (!CollectionUtils.isEmpty(ignoreAuthUrls)) {
@ -53,30 +46,44 @@ public class IgnoreAuthPostProcessor implements InitializingBean {
// 计算方法的耗时
long endTime = System.currentTimeMillis();
long elapsedTime = endTime - startTime;
log.info("Init Token ignoreAuthUrls Config [ 耗时 ] " + elapsedTime + "ms");
log.info("Init Token ignoreAuthUrls Config [ 耗时 ] " + elapsedTime + "毫秒");
}
// 优化:新方法处理单个@IgnoreAuth方法减少重复注解检查
private List<String> processIgnoreAuthMethod(Class<?> clazz, Method method) {
private List<String> postProcessRestController(Class<?> clazz) {
List<String> ignoreAuthUrls = new ArrayList<>();
RequestMapping base = clazz.getAnnotation(RequestMapping.class);
String[] baseUrl = Objects.nonNull(base) ? base.value() : new String[]{};
String[] uri = null;
if (method.isAnnotationPresent(RequestMapping.class)) {
uri = method.getAnnotation(RequestMapping.class).value();
} else if (method.isAnnotationPresent(GetMapping.class)) {
uri = method.getAnnotation(GetMapping.class).value();
} else if (method.isAnnotationPresent(PostMapping.class)) {
uri = method.getAnnotation(PostMapping.class).value();
} else if (method.isAnnotationPresent(PutMapping.class)) {
uri = method.getAnnotation(PutMapping.class).value();
} else if (method.isAnnotationPresent(DeleteMapping.class)) {
uri = method.getAnnotation(DeleteMapping.class).value();
} else if (method.isAnnotationPresent(PatchMapping.class)) {
uri = method.getAnnotation(PatchMapping.class).value();
Method[] methods = clazz.getDeclaredMethods();
for (Method method : methods) {
if (method.isAnnotationPresent(IgnoreAuth.class) && method.isAnnotationPresent(RequestMapping.class)) {
RequestMapping requestMapping = method.getAnnotation(RequestMapping.class);
String[] uri = requestMapping.value();
ignoreAuthUrls.addAll(rebuildUrl(baseUrl, uri));
} else if (method.isAnnotationPresent(IgnoreAuth.class) && method.isAnnotationPresent(GetMapping.class)) {
GetMapping requestMapping = method.getAnnotation(GetMapping.class);
String[] uri = requestMapping.value();
ignoreAuthUrls.addAll(rebuildUrl(baseUrl, uri));
} else if (method.isAnnotationPresent(IgnoreAuth.class) && method.isAnnotationPresent(PostMapping.class)) {
PostMapping requestMapping = method.getAnnotation(PostMapping.class);
String[] uri = requestMapping.value();
ignoreAuthUrls.addAll(rebuildUrl(baseUrl, uri));
} else if (method.isAnnotationPresent(IgnoreAuth.class) && method.isAnnotationPresent(PutMapping.class)) {
PutMapping requestMapping = method.getAnnotation(PutMapping.class);
String[] uri = requestMapping.value();
ignoreAuthUrls.addAll(rebuildUrl(baseUrl, uri));
} else if (method.isAnnotationPresent(IgnoreAuth.class) && method.isAnnotationPresent(DeleteMapping.class)) {
DeleteMapping requestMapping = method.getAnnotation(DeleteMapping.class);
String[] uri = requestMapping.value();
ignoreAuthUrls.addAll(rebuildUrl(baseUrl, uri));
} else if (method.isAnnotationPresent(IgnoreAuth.class) && method.isAnnotationPresent(PatchMapping.class)) {
PatchMapping requestMapping = method.getAnnotation(PatchMapping.class);
String[] uri = requestMapping.value();
ignoreAuthUrls.addAll(rebuildUrl(baseUrl, uri));
}
}
return uri != null ? rebuildUrl(baseUrl, uri) : Collections.emptyList();
return ignoreAuthUrls;
}
private List<String> rebuildUrl(String[] bases, String[] uris) {

View File

@ -1,32 +0,0 @@
package org.jeecg.config.sign.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 签名校验注解
* 用于方法级别的签名验证功能等同于yml中的jeecg.signUrls配置
* 参考DragSignatureAspect的设计思路使用AOP切面实现
*
* @author GitHub Copilot
* @since 2025-12-15
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SignatureCheck {
/**
* 是否启用签名校验
* @return true-启用(默认), false-禁用
*/
boolean enabled() default true;
/**
* 签名校验失败时的错误消息
* @return 错误消息
*/
String errorMessage() default "Sign签名校验失败";
}

View File

@ -1,112 +0,0 @@
package org.jeecg.config.sign.aspect;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.jeecg.config.sign.annotation.SignatureCheck;
import org.jeecg.config.sign.interceptor.SignAuthInterceptor;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.web.bind.annotation.RequestBody;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.Arrays;
/**
* 基于AOP的签名验证切面
* 复用SignAuthInterceptor的成熟签名验证逻辑
*
* @author GitHub Copilot
* @since 2025-12-15
*/
@Aspect
@Slf4j
@Component("signatureCheckAspect")
public class SignatureCheckAspect {
/**
* 复用SignAuthInterceptor的签名验证逻辑
*/
private final SignAuthInterceptor signAuthInterceptor = new SignAuthInterceptor();
/**
* 验签切点:拦截所有标记了@SignatureCheck注解的方法
*/
@Pointcut("@annotation(org.jeecg.config.sign.annotation.SignatureCheck)")
private void signatureCheckPointCut() {
}
/**
* 开始验签
*/
@Before("signatureCheckPointCut()")
public void doSignatureValidation(JoinPoint point) throws Exception {
// 获取方法上的注解
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
SignatureCheck signatureCheck = method.getAnnotation(SignatureCheck.class);
log.info("AOP签名验证: {}.{}", method.getDeclaringClass().getSimpleName(), method.getName());
// 如果注解被禁用,直接返回
if (!signatureCheck.enabled()) {
log.info("签名验证已禁用,跳过");
return;
}
// update-begin---author:sjlei---date:20260115 for: 查找带有@RequestBody注解的参数解决签名校验时读取请求体为空的问题
Object bodyParam = null;
Object[] args = point.getArgs();
Annotation[][] parameterAnnotations = method.getParameterAnnotations();
for (int i = 0; i < args.length; i++) {
Object arg = args[i];
Annotation[] annotations = parameterAnnotations[i];
boolean hasRequestBodyAnnotation = Arrays.stream(annotations).anyMatch(annotation -> annotation.annotationType().equals(RequestBody.class));
if (hasRequestBodyAnnotation) {
// 捕获携带@RequestBody注解的参数供签名校验使用
bodyParam = arg;
}
}
// update-end-----author:sjlei---date:20260115 for: 查找带有@RequestBody注解的参数解决签名校验时读取请求体为空的问题
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes == null) {
log.error("无法获取请求上下文");
throw new IllegalArgumentException("无法获取请求上下文");
}
HttpServletRequest request = attributes.getRequest();
log.info("X-SIGN: {}, X-TIMESTAMP: {}", request.getHeader("X-SIGN"), request.getHeader("X-TIMESTAMP"));
try {
// 直接调用SignAuthInterceptor的验证逻辑
signAuthInterceptor.validateSignature(request, bodyParam);
log.info("AOP签名验证通过");
} catch (IllegalArgumentException e) {
// 使用注解中配置的错误消息,或者保留原始错误消息
String errorMessage = signatureCheck.errorMessage();
log.error("AOP签名验证失败: {}", e.getMessage());
if ("Sign签名校验失败".equals(errorMessage)) {
// 如果是默认错误消息,使用原始的详细错误信息
throw e;
} else {
// 如果是自定义错误消息,使用自定义消息
throw new IllegalArgumentException(errorMessage, e);
}
} catch (Exception e) {
// 包装其他异常
String errorMessage = signatureCheck.errorMessage();
log.error("AOP签名验证异常: {}", e.getMessage());
throw new IllegalArgumentException(errorMessage, e);
}
}
}

View File

@ -1,7 +1,6 @@
package org.jeecg.config.sign.interceptor;
import org.apache.commons.lang3.StringUtils;
import org.jeecg.common.constant.TenantConstant;
import org.jeecg.common.util.PathMatcherUtil;
import org.jeecg.config.JeecgBaseConfig;
import org.jeecg.config.filter.RequestBodyReserveFilter;
@ -11,7 +10,7 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import jakarta.annotation.Resource;
import javax.annotation.Resource;
/**
* 签名 拦截器配置
@ -42,7 +41,7 @@ public class SignAuthConfiguration implements WebMvcConfigurer {
registry.addInterceptor(signAuthInterceptor()).addPathPatterns(signUrlsArray);
}
// 代码逻辑说明: issues/I53J5E post请求X_SIGN签名拦截校验后报错, request body 为空
//update-begin-author:taoyan date:20220427 for: issues/I53J5E post请求X_SIGN签名拦截校验后报错, request body 为空
@Bean
public RequestBodyReserveFilter requestBodyReserveFilter(){
return new RequestBodyReserveFilter();
@ -65,9 +64,8 @@ public class SignAuthConfiguration implements WebMvcConfigurer {
//------------------------------------------------------------
// 建议此处只添加post请求地址而不是所有的都需要走过滤器
registration.addUrlPatterns(signUrlsArray);
// 增加注解签名请求
registration.addUrlPatterns(TenantConstant.SIGNATURE_CHECK_POST_URL);
return registration;
}
//update-end-author:taoyan date:20220427 for: issues/I53J5E post请求X_SIGN签名拦截校验后报错, request body 为空
}

View File

@ -4,8 +4,8 @@ package org.jeecg.config.sign.interceptor;
import java.io.PrintWriter;
import java.util.SortedMap;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.constant.CommonConstant;
@ -33,104 +33,63 @@ public class SignAuthInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
log.info("签名拦截器 Interceptor request URI = " + request.getRequestURI());
log.info("Sign Interceptor request URI = " + request.getRequestURI());
HttpServletRequest requestWrapper = new BodyReaderHttpServletRequestWrapper(request);
//获取全部参数(包括URL和body上的)
SortedMap<String, String> allParams = HttpUtils.getAllParams(requestWrapper);
//对参数进行签名验证
String headerSign = request.getHeader(CommonConstant.X_SIGN);
String xTimestamp = request.getHeader(CommonConstant.X_TIMESTAMP);
try {
// 调用验证逻辑
validateSignature(request);
return true;
} catch (IllegalArgumentException e) {
// 验证失败,返回错误响应
log.error("Sign 签名校验失败!{}", e.getMessage());
if(oConvertUtils.isEmpty(xTimestamp)){
Result<?> result = Result.error("Sign签名校验失败时间戳为空");
log.error("Sign 签名校验失败Header xTimestamp 为空");
//校验失败返回前端
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
PrintWriter out = response.getWriter();
Result<?> result = Result.error(e.getMessage());
out.print(JSON.toJSON(result));
return false;
}
//客户端时间
Long clientTimestamp = Long.parseLong(xTimestamp);
int length = 14;
int length1000 = 1000;
//1.校验签名时间兼容X_TIMESTAMP的新老格式
if (xTimestamp.length() == length) {
//a. X_TIMESTAMP格式是 yyyyMMddHHmmss (例子20220308152143)
if ((DateUtils.getCurrentTimestamp() - clientTimestamp) > MAX_EXPIRE) {
log.error("签名验证失败:X-TIMESTAMP已过期注意系统时间和服务器时间是否有误差");
throw new IllegalArgumentException("签名验证失败:X-TIMESTAMP已过期");
}
} else {
//b. X_TIMESTAMP格式是 时间戳 (例子1646552406000)
if ((System.currentTimeMillis() - clientTimestamp) > (MAX_EXPIRE * length1000)) {
log.error("签名验证失败:X-TIMESTAMP已过期注意系统时间和服务器时间是否有误差");
throw new IllegalArgumentException("签名验证失败:X-TIMESTAMP已过期");
}
}
//2.校验签名
boolean isSigned = SignUtil.verifySign(allParams,headerSign);
if (isSigned) {
log.debug("Sign 签名通过Header Sign : {}",headerSign);
return true;
} else {
log.info("sign allParams: {}", allParams);
log.error("request URI = " + request.getRequestURI());
log.error("Sign 签名校验失败Header Sign : {}",headerSign);
//校验失败返回前端
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
PrintWriter out = response.getWriter();
Result<?> result = Result.error("Sign签名校验失败");
out.print(JSON.toJSON(result));
return false;
}
}
/**
* 签名验证核心逻辑
* 提取出来供AOP切面复用
* @param request HTTP请求
* @throws IllegalArgumentException 验证失败时抛出异常
*/
public void validateSignature(HttpServletRequest request) throws IllegalArgumentException {
validateSignature(request, null);
}
/**
* 签名验证核心逻辑
* 提取出来供AOP切面复用
* @param request HTTP请求
* @throws IllegalArgumentException 验证失败时抛出异常
*/
public void validateSignature(HttpServletRequest request, Object bodyParam) throws IllegalArgumentException {
try {
log.debug("开始签名验证: {} {}", request.getMethod(), request.getRequestURI());
HttpServletRequest requestWrapper = new BodyReaderHttpServletRequestWrapper(request);
//获取全部参数(包括URL和body上的)
SortedMap<String, String> allParams = HttpUtils.getAllParams(requestWrapper, bodyParam);
log.debug("提取参数: {}", allParams);
//对参数进行签名验证
String headerSign = request.getHeader(CommonConstant.X_SIGN);
String xTimestamp = request.getHeader(CommonConstant.X_TIMESTAMP);
if(oConvertUtils.isEmpty(xTimestamp)){
log.error("Sign签名校验失败时间戳为空");
throw new IllegalArgumentException("Sign签名校验失败请求参数不完整");
}
//客户端时间
Long clientTimestamp = Long.parseLong(xTimestamp);
int length = 14;
int length1000 = 1000;
//1.校验签名时间兼容X_TIMESTAMP的新老格式
if (xTimestamp.length() == length) {
//a. X_TIMESTAMP格式是 yyyyMMddHHmmss (例子20220308152143)
long currentTimestamp = DateUtils.getCurrentTimestamp();
long timeDiff = currentTimestamp - clientTimestamp;
log.debug("时间戳验证(yyyyMMddHHmmss): 时间差{}秒", timeDiff);
if (timeDiff > MAX_EXPIRE) {
log.error("时间戳已过期: {}秒 > {}秒", timeDiff, MAX_EXPIRE);
throw new IllegalArgumentException("签名验证失败,请求时效性验证失败!");
}
} else {
//b. X_TIMESTAMP格式是 时间戳 (例子1646552406000)
long currentTime = System.currentTimeMillis();
long timeDiff = currentTime - clientTimestamp;
long maxExpireMs = MAX_EXPIRE * length1000;
log.debug("时间戳验证(Unix): 时间差{}ms", timeDiff);
if (timeDiff > maxExpireMs) {
log.error("时间戳已过期: {}ms > {}ms", timeDiff, maxExpireMs);
throw new IllegalArgumentException("签名验证失败,请求时效性验证失败!");
}
}
//2.校验签名
boolean isSigned = SignUtil.verifySign(allParams,headerSign);
if (isSigned) {
log.debug("签名验证通过");
} else {
log.error("签名验证失败, 参数: {}", allParams);
throw new IllegalArgumentException("Sign签名校验失败");
}
} catch (IllegalArgumentException e) {
// 重新抛出签名验证异常
throw e;
} catch (Exception e) {
// 包装其他异常如IOException
log.error("签名验证异常: {}", e.getMessage());
throw new IllegalArgumentException("Sign签名校验失败" + e.getMessage());
}
}
}

View File

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

View File

@ -10,7 +10,7 @@ import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;
import jakarta.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.constant.SymbolConstant;
@ -35,25 +35,25 @@ public class HttpUtils {
* @date 20210621
* @param request
*/
public static SortedMap<String, String> getAllParams(HttpServletRequest request, Object bodyParam) throws IOException {
public static SortedMap<String, String> getAllParams(HttpServletRequest request) throws IOException {
SortedMap<String, String> result = new TreeMap<>();
// 获取URL上最后带逗号的参数变量 sys/dict/getDictItems/sys_user,realname,username
String pathVariable = request.getRequestURI().substring(request.getRequestURI().lastIndexOf("/") + 1);
if (pathVariable.contains(SymbolConstant.COMMA)) {
log.debug(" pathVariable: {}",pathVariable);
log.info(" pathVariable: {}",pathVariable);
String deString = URLDecoder.decode(pathVariable, "UTF-8");
//https://www.52dianzi.com/category/article/37/565371.html
if(deString.contains("%")){
try {
deString = URLDecoder.decode(deString, "UTF-8");
log.debug("存在%情况下,执行两次解码 — pathVariable decode: {}",deString);
log.info("存在%情况下,执行两次解码 — pathVariable decode: {}",deString);
} catch (Exception e) {
//e.printStackTrace();
}
}
log.debug(" pathVariable decode: {}",deString);
log.info(" pathVariable decode: {}",deString);
result.put(SignUtil.X_PATH_VARIABLE, deString);
}
// 获取URL上的参数
@ -65,13 +65,7 @@ public class HttpUtils {
Map<String, String> allRequestParam = new HashMap<>(16);
// get请求不需要拿body参数
if (!HttpMethod.GET.name().equals(request.getMethod())) {
if (bodyParam != null) {
// update-begin---author:sjlei---date:20260115 for: 解决签名校验时读取请求体为空的问题
allRequestParam = JSONObject.parseObject(JSONObject.toJSONString(bodyParam), Map.class);
// update-end-----author:sjlei---date:20260115 for: 解决签名校验时读取请求体为空的问题
} else {
allRequestParam = getAllRequestParam(request);
}
allRequestParam = getAllRequestParam(request);
}
// 将URL的参数和body参数进行合并
if (allRequestParam != null) {
@ -97,15 +91,15 @@ public class HttpUtils {
// 获取URL上最后带逗号的参数变量 sys/dict/getDictItems/sys_user,realname,username
String pathVariable = url.substring(url.lastIndexOf("/") + 1);
if (pathVariable.contains(SymbolConstant.COMMA)) {
log.debug(" pathVariable: {}",pathVariable);
log.info(" pathVariable: {}",pathVariable);
String deString = URLDecoder.decode(pathVariable, "UTF-8");
//https://www.52dianzi.com/category/article/37/565371.html
if(deString.contains("%")){
deString = URLDecoder.decode(deString, "UTF-8");
log.debug("存在%情况下,执行两次解码 — pathVariable decode: {}",deString);
log.info("存在%情况下,执行两次解码 — pathVariable decode: {}",deString);
}
log.debug(" pathVariable decode: {}",deString);
log.info(" pathVariable decode: {}",deString);
result.put(SignUtil.X_PATH_VARIABLE, deString);
}
// 获取URL上的参数
@ -180,10 +174,11 @@ public class HttpUtils {
String[] params = param.split("&");
for (String s : params) {
int index = s.indexOf("=");
// 代码逻辑说明: [issues/5879]数据查询传ds=“”造成的异常------------
//update-begin---author:chenrui ---date:20240222 for[issues/5879]数据查询传ds=“”造成的异常------------
if (index != -1) {
result.put(s.substring(0, index), s.substring(index + 1));
}
//update-end---author:chenrui ---date:20240222 for[issues/5879]数据查询传ds=“”造成的异常------------
}
return result;
}
@ -207,10 +202,11 @@ public class HttpUtils {
String[] params = param.split("&");
for (String s : params) {
int index = s.indexOf("=");
// 代码逻辑说明: [issues/5879]数据查询传ds=“”造成的异常------------
//update-begin---author:chenrui ---date:20240222 for[issues/5879]数据查询传ds=“”造成的异常------------
if (index != -1) {
result.put(s.substring(0, index), s.substring(index + 1));
}
//update-end---author:chenrui ---date:20240222 for[issues/5879]数据查询传ds=“”造成的异常------------
}
return result;
}

View File

@ -11,12 +11,7 @@ import org.springframework.util.DigestUtils;
import org.springframework.util.StringUtils;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;
/**
* 签名工具类
@ -39,7 +34,7 @@ public class SignUtil {
}
// 把参数加密
String paramsSign = getParamsSign(params);
log.debug("Param Sign : {}", paramsSign);
log.info("Param Sign : {}", paramsSign);
return !StringUtils.isEmpty(paramsSign) && headerSign.equals(paramsSign);
}
@ -52,9 +47,14 @@ public class SignUtil {
//去掉 Url 里的时间戳
params.remove("_t");
String paramsJsonStr = JSONObject.toJSONString(params);
log.debug("Param paramsJsonStr : {}", paramsJsonStr);
log.info("Param paramsJsonStr : {}", paramsJsonStr);
//设置签名秘钥
String signatureSecret = SignUtil.getSignatureSecret();
JeecgBaseConfig jeecgBaseConfig = SpringContextUtils.getBean(JeecgBaseConfig.class);
String signatureSecret = jeecgBaseConfig.getSignatureSecret();
String curlyBracket = SymbolConstant.DOLLAR + SymbolConstant.LEFT_CURLY_BRACKET;
if(oConvertUtils.isEmpty(signatureSecret) || signatureSecret.contains(curlyBracket)){
throw new JeecgBootException("签名密钥 ${jeecg.signatureSecret} 缺少配置 ");
}
try {
//【issues/I484RW】2.4.6部署后下拉搜索框提示“sign签名检验失败”
return DigestUtils.md5DigestAsHex((paramsJsonStr + signatureSecret).getBytes("UTF-8")).toUpperCase();
@ -63,129 +63,4 @@ public class SignUtil {
return null;
}
}
/**
* 通过前端签名算法生成签名
*
* @param url 请求的完整URL包含查询参数
* @param requestParams 使用 @RequestParam 获取的参数集合
* @param requestBodyParams 使用 @RequestBody 获取的参数集合
* @return 计算得到的签名大写MD5若参数不足返回 null
*/
public static String generateRequestSign(String url, Map<String, Object> requestParams, Map<String, Object> requestBodyParams) {
if (oConvertUtils.isEmpty(url)) {
return null;
}
// 解析URL上的查询参数与路径变量
Map<String, String> urlParams = parseQueryString(url);
// 合并URL参数与@RequestParam参数确保数值和布尔类型转换为字符串
Map<String, String> mergedParams = mergeObject(urlParams, requestParams);
// 按需合并@RequestBody参数
if (requestBodyParams != null && !requestBodyParams.isEmpty()) {
mergedParams = mergeObject(mergedParams, requestBodyParams);
}
// 按键名升序排序,保持与前端一致的签名顺序
SortedMap<String, String> sortedParams = new TreeMap<>(mergedParams);
// 去除时间戳字段,避免参与签名
sortedParams.remove("_t");
// 序列化为JSON字符串
String paramsJsonStr = JSONObject.toJSONString(sortedParams);
// 读取签名秘钥
String signatureSecret = getSignatureSecret();
// 计算MD5摘要并转大写
return DigestUtils.md5DigestAsHex((paramsJsonStr + signatureSecret).getBytes(StandardCharsets.UTF_8)).toUpperCase();
}
/**
* 解析URL中的查询参数并处理末尾逗号分隔的路径变量片段。
*
* @param url 请求的完整URL
* @return 解析后的参数映射,数值与布尔类型均转换为字符串
*/
private static Map<String, String> parseQueryString(String url) {
Map<String, String> result = new HashMap<>(16);
int fragmentIndex = url.indexOf('#');
if (fragmentIndex >= 0) {
url = url.substring(0, fragmentIndex);
}
int questionIndex = url.indexOf('?');
String paramString = null;
if (questionIndex >= 0 && questionIndex < url.length() - 1) {
paramString = url.substring(questionIndex + 1);
}
// 处理路径变量末尾以逗号分隔的段,例如 /sys/dict/getDictItems/sys_user,realname,username
int lastSlashIndex = url.lastIndexOf(SymbolConstant.SINGLE_SLASH);
if (lastSlashIndex >= 0 && lastSlashIndex < url.length() - 1) {
String lastPathVariable = url.substring(lastSlashIndex + 1);
int qIndexInPath = lastPathVariable.indexOf('?');
if (qIndexInPath >= 0) {
lastPathVariable = lastPathVariable.substring(0, qIndexInPath);
}
if (lastPathVariable.contains(SymbolConstant.COMMA)) {
String decodedPathVariable = URLDecoder.decode(lastPathVariable, StandardCharsets.UTF_8);
result.put(X_PATH_VARIABLE, decodedPathVariable);
}
}
if (oConvertUtils.isNotEmpty(paramString)) {
String[] pairs = paramString.split(SymbolConstant.AND);
for (String pair : pairs) {
int equalIndex = pair.indexOf('=');
if (equalIndex > 0 && equalIndex < pair.length() - 1) {
String key = pair.substring(0, equalIndex);
String value = pair.substring(equalIndex + 1);
// 解码并统一类型为字符串
String decodedKey = URLDecoder.decode(key, StandardCharsets.UTF_8);
String decodedValue = URLDecoder.decode(value, StandardCharsets.UTF_8);
result.put(decodedKey, decodedValue);
}
}
}
return result;
}
/**
* 合并两个参数映射,并保证数值与布尔类型统一转为字符串。
*
* @param target 初始参数映射
* @param source 待合并的参数映射
* @return 合并后的新映射
*/
private static Map<String, String> mergeObject(Map<String, String> target, Map<String, Object> source) {
Map<String, String> merged = new HashMap<>(16);
if (target != null && !target.isEmpty()) {
merged.putAll(target);
}
if (source != null && !source.isEmpty()) {
for (Map.Entry<String, Object> entry : source.entrySet()) {
String key = entry.getKey();
Object value = entry.getValue();
if (value instanceof Number) {
// 数值类型转字符串,保持前后端一致
merged.put(key, String.valueOf(value));
} else if (value instanceof Boolean) {
// 布尔类型转字符串,保持前后端一致
merged.put(key, String.valueOf(value));
} else if (value != null) {
merged.put(key, String.valueOf(value));
}
}
}
return merged;
}
/**
* 读取并校验签名秘钥配置。
*
* @return 有效的签名秘钥
*/
private static String getSignatureSecret() {
JeecgBaseConfig jeecgBaseConfig = SpringContextUtils.getBean(JeecgBaseConfig.class);
String signatureSecret = jeecgBaseConfig.getSignatureSecret();
String curlyBracket = SymbolConstant.DOLLAR + SymbolConstant.LEFT_CURLY_BRACKET;
if (oConvertUtils.isEmpty(signatureSecret) || signatureSecret.contains(curlyBracket)) {
throw new JeecgBootException("签名密钥 ${jeecg.signatureSecret} 缺少配置 ");
}
return signatureSecret;
}
}
}

View File

@ -1,38 +0,0 @@
package org.jeecg.config.tencent;
import lombok.Data;
/**
* @Description: 腾讯短信配置
*
* @author: wangshuai
* @date: 2025/10/30 18:22
*/
@Data
public class JeecgTencent {
/**
* 接入域名
*/
private String endpoint;
/**
* api秘钥id
*/
private String secretId;
/**
* api秘钥key
*/
private String secretKey;
/**
* 应用id
*/
private String sdkAppId;
/**
* 地域信息
*/
private String region;
}

View File

@ -19,42 +19,11 @@ public class Firewall {
* 低代码模式dev:开发模式prod:发布模式——关闭所有在线开发配置能力)
*/
private String lowCodeMode;
/**
* 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录)
*/
private Boolean isConcurrent = true;
/**
* 是否开启默认密码登录提醒true 登录后提示必须修改默认密码)
*/
private Boolean enableDefaultPwdCheck = false;
/**
* 是否开启登录验证码校验true 开启false 关闭并跳过验证码逻辑)
*/
private Boolean enableLoginCaptcha = true;
// /**
// * 表字典安全模式white:白名单——配置了白名单的表才能通过表字典方式访问black:黑名单——配置了黑名单的表不允许表字典方式访问)
// */
// private String tableDictMode;
public Boolean getEnableLoginCaptcha() {
return enableLoginCaptcha;
}
public void setEnableLoginCaptcha(Boolean enableLoginCaptcha) {
this.enableLoginCaptcha = enableLoginCaptcha;
}
public Boolean getEnableDefaultPwdCheck() {
return enableDefaultPwdCheck;
}
public void setEnableDefaultPwdCheck(Boolean enableDefaultPwdCheck) {
this.enableDefaultPwdCheck = enableDefaultPwdCheck;
}
public Boolean getDataSourceSafe() {
return dataSourceSafe;
}
@ -78,12 +47,4 @@ public class Firewall {
public void setDisableSelectAll(Boolean disableSelectAll) {
this.disableSelectAll = disableSelectAll;
}
public Boolean getIsConcurrent() {
return isConcurrent;
}
public void setIsConcurrent(Boolean isConcurrent) {
this.isConcurrent = isConcurrent;
}
}

View File

@ -1,11 +0,0 @@
package org.jeecg.config.vo;
import lombok.Data;
@Data
public class JeecgMinio {
private String minio_url;
private String bucketName;
}

View File

@ -1,11 +0,0 @@
package org.jeecg.config.vo;
import lombok.Data;
@Data
public class JeecgOSS {
private String endpoint;
private String bucketName;
}

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 487 B

View File

@ -1,429 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
MCP Stdio 工具 - 修复编码问题
确保所有输出都使用UTF-8编码
"""
import json
import sys
import os
from typing import Dict, Any
import logging
# 强制使用UTF-8编码
if sys.platform == "win32":
# Windows需要特殊处理
import io
sys.stdin = io.TextIOWrapper(sys.stdin.buffer, encoding='utf-8')
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8')
else:
# Unix-like系统
sys.stdin.reconfigure(encoding='utf-8')
sys.stdout.reconfigure(encoding='utf-8')
sys.stderr.reconfigure(encoding='utf-8')
# 设置环境变量
os.environ['PYTHONIOENCODING'] = 'utf-8'
os.environ['PYTHONUTF8'] = '1'
# 配置日志
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
encoding='utf-8'
)
logger = logging.getLogger("mcp-tool")
class FixedMCPServer:
"""修复编码问题的MCP服务器"""
def __init__(self):
self.tools = {}
self.initialize_tools()
def initialize_tools(self):
"""初始化工具集"""
# 获取时间
self.tools["get_time"] = {
"name": "get_time",
"description": "获取当前时间",
"inputSchema": {
"type": "object",
"properties": {
"format": {
"type": "string",
"description": "时间格式",
"enum": ["iso", "timestamp", "human", "chinese"],
"default": "iso"
}
}
}
}
# 文本处理工具
self.tools["text_process"] = {
"name": "text_process",
"description": "文本处理工具",
"inputSchema": {
"type": "object",
"properties": {
"text": {
"type": "string",
"description": "输入文本"
},
"operation": {
"type": "string",
"description": "操作类型",
"enum": ["length", "upper", "lower", "reverse", "count_words"],
"default": "length"
}
},
"required": ["text"]
}
}
# 数据格式工具
self.tools["format_data"] = {
"name": "format_data",
"description": "格式化数据",
"inputSchema": {
"type": "object",
"properties": {
"data": {
"type": "string",
"description": "原始数据"
},
"format": {
"type": "string",
"description": "格式类型",
"enum": ["json", "yaml", "xml"],
"default": "json"
}
},
"required": ["data"]
}
}
def handle_request(self, request: Dict[str, Any]) -> Dict[str, Any]:
"""处理请求"""
try:
method = request.get("method")
params = request.get("params", {})
if method == "tools/list":
return self.handle_tools_list()
elif method == "tools/call":
return self.handle_tool_call(params)
elif method == "ping":
return {"result": "pong"}
else:
return self.create_error_response(
code=-32601,
message="Method not found"
)
except Exception as e:
logger.error(f"Error handling request: {e}")
return self.create_error_response(
code=-32603,
message=f"Internal error: {str(e)}"
)
def handle_tools_list(self) -> Dict[str, Any]:
"""列出所有工具 - 确保返回标准JSON"""
return {
"result": {
"tools": list(self.tools.values())
}
}
def handle_tool_call(self, params: Dict[str, Any]) -> Dict[str, Any]:
"""调用工具 - 修复响应格式"""
name = params.get("name")
arguments = params.get("arguments", {})
if name not in self.tools:
return self.create_error_response(
code=-32602,
message=f"Tool '{name}' not found"
)
try:
if name == "get_time":
result = self.execute_get_time(arguments)
elif name == "text_process":
result = self.execute_text_process(arguments)
elif name == "format_data":
result = self.execute_format_data(arguments)
else:
return self.create_error_response(
code=-32602,
message="Tool not implemented"
)
# 确保返回正确的MCP响应格式
return self.create_success_response(result)
except Exception as e:
logger.error(f"Tool execution error: {e}")
return self.create_error_response(
code=-32603,
message=f"Tool execution failed: {str(e)}"
)
def execute_get_time(self, args: Dict[str, Any]) -> Dict[str, Any]:
"""获取时间 - 支持中文"""
from datetime import datetime
try:
format_type = args.get("format", "iso")
now = datetime.now()
if format_type == "iso":
result = now.isoformat()
elif format_type == "timestamp":
result = now.timestamp()
elif format_type == "human":
result = now.strftime("%Y-%m-%d %H:%M:%S")
elif format_type == "chinese":
result = now.strftime("%Y年%m月%d%H时%M分%S秒")
else:
result = now.isoformat()
logger.info(f"当前系统时间:{result}")
return {
"status": "success",
"format": format_type,
"time": result,
"timestamp": now.timestamp(),
"date": now.strftime("%Y-%m-%d"),
"time_12h": now.strftime("%I:%M:%S %p")
}
except Exception as e:
return {
"status": "error",
"error": str(e)
}
def execute_text_process(self, args: Dict[str, Any]) -> Dict[str, Any]:
"""文本处理"""
try:
text = args.get("text", "")
operation = args.get("operation", "length")
if operation == "length":
result = len(text)
result_str = f"文本长度: {result} 个字符"
elif operation == "upper":
result = text.upper()
result_str = f"大写: {result}"
elif operation == "lower":
result = text.lower()
result_str = f"小写: {result}"
elif operation == "reverse":
result = text[::-1]
result_str = f"反转: {result}"
elif operation == "count_words":
words = len(text.split())
result = words
result_str = f"单词数: {words}"
else:
raise ValueError(f"未知操作: {operation}")
return {
"status": "success",
"operation": operation,
"original_text": text,
"result": result,
"result_str": result_str,
"text_length": len(text)
}
except Exception as e:
return {
"status": "error",
"error": str(e),
"operation": args.get("operation", "")
}
def execute_format_data(self, args: Dict[str, Any]) -> Dict[str, Any]:
"""格式化数据"""
try:
data_str = args.get("data", "")
format_type = args.get("format", "json")
# 尝试解析为JSON
try:
data = json.loads(data_str)
is_json = True
except:
data = data_str
is_json = False
if format_type == "json":
if is_json:
result = json.dumps(data, ensure_ascii=False, indent=2)
else:
# 如果不是JSON包装成JSON
result = json.dumps({"text": data}, ensure_ascii=False, indent=2)
elif format_type == "yaml":
import yaml
result = yaml.dump(data, allow_unicode=True, default_flow_style=False)
elif format_type == "xml":
# 简单的XML格式化
if isinstance(data, dict):
result = "<data>"
for k, v in data.items():
result += f"\n <{k}>{v}</{k}>"
result += "\n</data>"
else:
result = f"<text>{data}</text>"
else:
result = str(data)
return {
"status": "success",
"format": format_type,
"original": data_str,
"formatted": result,
"length": len(result)
}
except Exception as e:
return {
"status": "error",
"error": str(e),
"format": args.get("format", "")
}
def create_success_response(self, data: Dict[str, Any]) -> Dict[str, Any]:
"""创建成功响应 - 确保符合MCP规范"""
# 将数据转换为JSON字符串作为文本内容
content_text = json.dumps(data, ensure_ascii=False, indent=2)
return {
"result": {
"content": [
{
"type": "text",
"text": content_text
}
],
"isError": False
}
}
def create_error_response(self, code: int, message: str) -> Dict[str, Any]:
"""创建错误响应"""
return {
"error": {
"code": code,
"message": message
}
}
def safe_json_dump(data: Dict[str, Any]) -> str:
"""安全的JSON序列化确保UTF-8编码"""
try:
return json.dumps(data, ensure_ascii=False, separators=(',', ':'))
except:
# 如果失败使用ASCII转义
return json.dumps(data, ensure_ascii=True, separators=(',', ':'))
def main():
"""主函数 - 修复Stdio通信"""
logger.info("启动MCP Stdio服务器 (修复编码版)...")
server = FixedMCPServer()
# 初始握手消息
init_message = {
"jsonrpc": "2.0",
"id": 1,
"result": {
"protocolVersion": "2024-11-05",
"capabilities": {
"tools": {}
},
"serverInfo": {
"name": "fixed-mcp-server",
"version": "1.0.0"
}
}
}
# 发送初始化响应
try:
sys.stdout.write(safe_json_dump(init_message) + "\n")
sys.stdout.flush()
except Exception as e:
logger.error(f"发送初始化消息失败: {e}")
return
logger.info("MCP服务器已初始化")
# 主循环
line_num = 0
while True:
try:
line = sys.stdin.readline()
if not line:
logger.info("输入流结束")
break
line = line.strip()
line_num += 1
if not line:
continue
logger.info(f"收到第 {line_num} 行: {line[:100]}...")
try:
request = json.loads(line)
logger.info(f"解析请求: {request.get('method', 'unknown')}")
# 处理请求
response = server.handle_request(request)
response["jsonrpc"] = "2.0"
response["id"] = request.get("id")
# 发送响应
response_json = safe_json_dump(response)
sys.stdout.write(response_json + "\n")
sys.stdout.flush()
logger.info(f"发送响应: {response.get('result', response.get('error', {}))}")
except json.JSONDecodeError as e:
logger.error(f"JSON解析错误: {e}")
error_response = {
"jsonrpc": "2.0",
"error": {
"code": -32700,
"message": f"Parse error at line {line_num}"
},
"id": None
}
sys.stdout.write(safe_json_dump(error_response) + "\n")
sys.stdout.flush()
except KeyboardInterrupt:
logger.info("接收到中断信号")
break
except Exception as e:
logger.error(f"未处理的错误: {e}")
break
logger.info("MCP服务器已停止")
if __name__ == "__main__":
main()

View File

@ -4,9 +4,9 @@
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.jeecgframework.boot3</groupId>
<groupId>org.jeecgframework.boot</groupId>
<artifactId>jeecg-boot-module</artifactId>
<version>3.9.1</version>
<version>3.8.3</version>
</parent>
<artifactId>jeecg-boot-module-airag</artifactId>
@ -31,68 +31,34 @@
</repositories>
<properties>
<kotlin.version>2.2.0</kotlin.version>
<liteflow.version>2.15.0</liteflow.version>
<apache-tika.version>3.2.3</apache-tika.version>
<langchain4j.version>0.35.0</langchain4j.version>
<apache-tika.version>2.9.1</apache-tika.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-bom</artifactId>
<version>1.9.1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-community-bom</artifactId>
<version>1.9.1-beta17</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- system单体 api-->
<dependency>
<groupId>org.jeecgframework.boot3</groupId>
<groupId>org.jeecgframework.boot</groupId>
<artifactId>jeecg-system-local-api</artifactId>
</dependency>
<!-- 微服务starter和system微服务 api
<dependency>
<groupId>org.jeecgframework.boot3</groupId>
<groupId>org.jeecgframework.boot</groupId>
<artifactId>jeecg-boot-starter-cloud</artifactId>
</dependency>
<dependency>
<groupId>org.jeecgframework.boot3</groupId>
<groupId>org.jeecgframework.boot</groupId>
<artifactId>jeecg-system-cloud-api</artifactId>
</dependency>-->
<!-- aiflow依赖 -->
<dependency>
<groupId>org.jeecgframework.boot3</groupId>
<groupId>org.jeecgframework.boot</groupId>
<artifactId>jeecg-aiflow</artifactId>
<version>3.9.1-beta1</version>
<exclusions>
<exclusion>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
</exclusion>
<exclusion>
<groupId>commons-beanutils</groupId>
<artifactId>commons-beanutils</artifactId>
</exclusion>
<exclusion>
<groupId>com.yomahub</groupId>
<artifactId>liteflow-script-python</artifactId>
</exclusion>
</exclusions>
<version>1.2.0</version>
</dependency>
<!-- begin 注意:这几个依赖体积较大,每个50MB。若发布需要使用,请<scope>provided</scope> 删除 -->
<!-- beigin 这两个依赖太多每个50M左右,如果你发布需要使用,请<scope>provided</scope>删掉 -->
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-scripting-jsr223</artifactId>
@ -105,18 +71,12 @@
<version>${liteflow.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.yomahub</groupId>
<artifactId>liteflow-script-groovy</artifactId>
<version>${liteflow.version}</version>
<scope>runtime</scope>
</dependency>
<!-- end 注意这几个依赖体积较大每个约50MB。若发布时需要使用请将 <scope>provided</scope> 删除 -->
<!-- end 这两个依赖太多每个包50M左右如果你发布需要使用请把<scope>provided</scope>删掉 -->
<!-- aiflow 脚本依赖 -->
<dependency>
<groupId>com.yomahub</groupId>
<artifactId>liteflow-script-python</artifactId>
<artifactId>liteflow-script-groovy</artifactId>
<version>${liteflow.version}</version>
<scope>runtime</scope>
</dependency>
@ -147,22 +107,15 @@
<!-- aiflow 脚本依赖 -->
<!-- langChain4j model support -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-open-ai</artifactId>
</dependency>
<!-- langChain4j mcp support -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-mcp</artifactId>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-ollama</artifactId>
<version>${langchain4j.version}</version>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-community-zhipu-ai</artifactId>
<artifactId>langchain4j-zhipu-ai</artifactId>
<version>${langchain4j.version}</version>
<exclusions>
<exclusion>
<artifactId>checker-qual</artifactId>
@ -176,11 +129,13 @@
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-community-qianfan</artifactId>
<artifactId>langchain4j-qianfan</artifactId>
<version>${langchain4j.version}</version>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-community-dashscope</artifactId>
<artifactId>langchain4j-dashscope</artifactId>
<version>${langchain4j.version}</version>
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
@ -192,21 +147,13 @@
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-anthropic</artifactId>
</dependency>
<!-- langChain4j vextor support -->
<dependency>
<groupId>org.jeecgframework</groupId>
<artifactId>langchain4j-pgvector</artifactId>
<version>1.3.0-beta9</version>
</dependency>
<!-- langChain4j Document Parser 适用于excel、ppt、word -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-document-parser-apache-poi</artifactId>
<version>${langchain4j.version}</version>
</dependency>
<!-- langChain4j Document Parser -->
<dependency>
<groupId>org.apache.tika</groupId>
<artifactId>tika-core</artifactId>
@ -233,12 +180,7 @@
<artifactId>tika-parser-text-module</artifactId>
<version>${apache-tika.version}</version>
</dependency>
<!-- word模版引擎 -->
<dependency>
<groupId>com.deepoove</groupId>
<artifactId>poi-tl</artifactId>
<version>1.12.2</version>
</dependency>
</dependencies>
</project>

View File

@ -38,25 +38,4 @@ public class AiAppConsts {
*/
public static final String APP_TYPE_CHAT_FLOW = "chatFLow";
/**
* 应用元数据:流程输入参数
* for [issues/8545]新建AI应用的时候只能选择没有自定义参数的AI流程
*/
public static final String APP_METADATA_FLOW_INPUTS = "flowInputs";
/**
* 是否开启记忆
*/
public static final Integer IZ_OPEN_MEMORY = 1;
/**
* 会话标题最大长度
*/
public static final int CONVERSATION_MAX_TITLE_LENGTH = 10;
/**
* AI写作的应用id
*/
public static final String WRITER_APP_ID = "2010634128233779202";
}

View File

@ -104,68 +104,4 @@ public class Prompts {
" - 反幻觉校验:\"所有数据需标注来源,不确定信息用[需核实]标记\"\n" +
" - 风格校准器:\"对比[目标风格]与生成内容的余弦相似度低于0.7时启动重写\"\n" +
" - 伦理审查模块:\"自动过滤涉及隐私/偏见/违法内容,替换为[合规表达]\"";
/**
* 提示词生成角色及通用要求
*/
public static final String GENERATE_GUIDE_HEADER = "# 角色\n" +
"你是一位AI提示词专家请根据提供的配置信息生成针对AI智能体的“使用指南”提示词。\n" +
"\n" +
"## 通用要求\n" +
"1. 生成的内容将作为系统提示词的一部分。\n" +
"2. **严禁**包含任何角色设定开场白(如“你是一个...AI助手”、“在对话过程中...”等)。\n" +
"3. **只输出提示词内容**不要包含任何解释、寒暄或Markdown代码块标记。\n" +
"4. 语气专业、清晰、指令性强。\n" +
"5. 说明内容请使用中文。\n\n";
/**
* 变量生成提示词
*/
public static final String GENERATE_VAR_PART = "## 任务:生成变量使用指南\n" +
"### 输入信息\n" +
"**变量列表**\n" +
"%s\n" +
"### 要求\n" +
"1. 请生成一段**变量使用指南**。\n" +
"2. **遍历生成**:请遍历【输入信息】中的所有变量,为**每一个**变量生成一条具体的使用指南。\n" +
"3. **格式要求**:请仿照以下句式,根据变量的实际含义生成(确保包含{{变量名}}\n" +
" 例如针对name变量 -> “回复问题时,请称呼你的用户为{{name}}。”\n" +
" 例如针对age变量 -> “用户的年龄是{{age}},请在对话中适时使用。”\n" +
" 例如:针对其他变量 -> “用户的[变量描述]是{{[变量名]}},请在对话中适时使用。”\n" +
"4. **通用更新指令**请在变量指南的最后单独生成一条指令明确指示AI“当从用户对话中获取到上述变量<列出所有变量名,用顿号分隔>)的**新信息**时,**必须立即调用** `update_variable` 工具进行存储。**注意**:调用前请检查上下文,如果已调用过该工具或变量值未改变,**严禁**重复调用。”\n" +
"5. **保留原文**:如果输入信息中包含具体的行为指令(如“回复问题时,请称呼你的用户为{{name}}”),请在生成的指南中**直接引用原文**,不要进行改写或格式化,以免改变用户的原意。\n\n";
/**
* 记忆库生成提示词
*/
public static final String GENERATE_MEMORY_PART = "## 任务:生成记忆库使用指南\n" +
"### 输入信息\n" +
"**记忆库描述**\n" +
"%s\n" +
"### 要求\n" +
"1. 请生成一段**记忆库使用指南**,加入【工具使用强制协议】:\n" +
" - **全自动存储(无需用户指令)**:你必须时刻像一个观察者一样分析对话。一旦检测到符合记忆库描述的信息(尤其是:**姓名、职业、年龄**、联系方式、偏好、经历等),**立即**调用 `add_memory` 工具存储。**绝对不要**询问用户是否需要存储,也不要等待用户明确指令。这是你的后台职责。\n" +
" - **全自动检索(强制优先)**\n" +
" * **禁止直接反问**:当用户提出依赖个人信息的问题(如“推荐适合我的...”或“我之前说过...”)时,**绝对禁止**直接反问用户“你的爱好是什么?”。\n" +
" * **必须先查后答**:你必须**先假设**记忆库中已经有了答案,并**立即调用** `query_memory` 进行验证。只有当工具返回“未找到相关信息”后,你才有资格询问用户。\n" +
" * **宁可查空,不可不查**:即使你觉得可能没有记录,也必须先走一遍查询流程。\n" +
" - **动态调整**:请根据【输入信息】中提供的**记忆库状态描述**,明确界定哪些信息属于“自动捕获”的范围。\n" +
" - **行为准则**\n" +
" * 你的记忆动作应该是**主动且无感**的。用户只负责聊天,你负责记住一切重要细节。\n" +
" * **禁止口头空谈**:严禁只回复“我知道了”、“已记住”而实际不调用工具。这是严重错误。\n" +
" - **示例演示**\n" +
" * 自动存储(职业):用户说“我是网络工程师” -> (捕捉到职业信息) -> **立即自动调用** `add_memory(content='用户职业是网络工程师')` -> (存储成功) -> 回复“原来是同行,网络工程很有趣...”。\n" +
" * 自动查询(场景):用户说“根据我的爱好推荐旅游地点” -> **严禁**直接问“你有什么爱好?” -> **必须立即调用** `query_memory(queryText='用户爱好')` -> (若查到:爬山) -> 回复“既然你喜欢爬山,推荐去黄山...”。\n" +
" * 自动查询(常规):用户问“今天吃什么好?” -> (需要了解口味) -> **立即自动调用** `query_memory(queryText='用户饮食偏好')` -> (获取到不吃香菜) -> 回复“推荐一家不放香菜的...”。\n\n";
/**
* ai写作提示词
*/
public static final String AI_WRITER_PROMPT ="请撰写一篇关于 [{}] 的文章。文章的内容格式:{},语气:{},语言:{},长度:{}。";
/**
* ai写作回复提示词
*/
public static final String AI_REPLY_PROMPT = "请针对如下内容:[{}] 做个回复。回复内容参考:[{}], 回复格式:{},语气:{},语言:{},长度:{}。";
}

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