mirror of
https://github.com/jeecgboot/JeecgBoot.git
synced 2026-01-24 03:56:53 +08:00
Compare commits
19 Commits
main
...
springboot
| Author | SHA1 | Date | |
|---|---|---|---|
| 74d80459e1 | |||
| bdc1e7988b | |||
| 5730ed2dd2 | |||
| 5743d71f3c | |||
| db16cb5fda | |||
| 07c012adc7 | |||
| 8edb547113 | |||
| da9e8570cd | |||
| 5e37b4de8f | |||
| cab42b819c | |||
| d3b8948f40 | |||
| 766ec0df52 | |||
| b72c343090 | |||
| cdd7c5c5a1 | |||
| 120cf85eb4 | |||
| a3969a5c63 | |||
| 1222c76ff8 | |||
| 6048866313 | |||
| 83edfa9090 |
1
.github/ISSUE_TEMPLATE/bug_report.md
vendored
1
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -9,7 +9,6 @@ assignees: getActivity
|
|||||||
|
|
||||||
##### 版本号:
|
##### 版本号:
|
||||||
|
|
||||||
|
|
||||||
##### 分支:
|
##### 分支:
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
1
.github/ISSUE_TEMPLATE/feature_request.md
vendored
1
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@ -8,7 +8,6 @@ assignees: getActivity
|
|||||||
|
|
||||||
##### 版本号:
|
##### 版本号:
|
||||||
|
|
||||||
|
|
||||||
##### 分支:
|
##### 分支:
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
2
.gitignore
vendored
2
.gitignore
vendored
@ -13,5 +13,3 @@ os_del.cmd
|
|||||||
os_del_doc.cmd
|
os_del_doc.cmd
|
||||||
.svn
|
.svn
|
||||||
derby.log
|
derby.log
|
||||||
.cursor
|
|
||||||
.history
|
|
||||||
@ -3,6 +3,9 @@ AIGC应用平台介绍
|
|||||||
|
|
||||||
一个全栈式 AI 开发平台,旨在帮助开发者快速构建和部署个性化的 AI 应用。
|
一个全栈式 AI 开发平台,旨在帮助开发者快速构建和部署个性化的 AI 应用。
|
||||||
|
|
||||||
|
> JDK说明:AI流程编排引擎暂时不支持jdk21,所以目前只能使用jdk8或者jdk17启动项目。
|
||||||
|
|
||||||
|
|
||||||
JeecgBoot平台的AIGC功能模块,是一套类似`Dify`的`AIGC应用开发平台`+`知识库问答`,是一款基于LLM大语言模型AI应用平台和 RAG 的知识库问答系统。
|
JeecgBoot平台的AIGC功能模块,是一套类似`Dify`的`AIGC应用开发平台`+`知识库问答`,是一款基于LLM大语言模型AI应用平台和 RAG 的知识库问答系统。
|
||||||
其直观的界面结合了 AI 流程编排、RAG 管道、知识库管理、模型管理、对接向量库、实时运行可观察等,让您可以快速从原型到生产,拥有AI服务能力。
|
其直观的界面结合了 AI 流程编排、RAG 管道、知识库管理、模型管理、对接向量库、实时运行可观察等,让您可以快速从原型到生产,拥有AI服务能力。
|
||||||
|
|
||||||
@ -106,10 +109,6 @@ JeecgBoot平台的AIGC功能模块,是一套类似`Dify`的`AIGC应用开发
|
|||||||
| ChatGTP | √ |
|
| ChatGTP | √ |
|
||||||
| Qwq | √ |
|
| Qwq | √ |
|
||||||
| 智库 | √ |
|
| 智库 | √ |
|
||||||
| claude | √ |
|
|
||||||
| vl模型 | √ |
|
|
||||||
| 千帆大模型 | √ |
|
|
||||||
| 通义千问 | √ |
|
|
||||||
| Ollama本地搭建大模型 | √ |
|
| Ollama本地搭建大模型 | √ |
|
||||||
| 等等。。 | √ |
|
| 等等。。 | √ |
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
[中文](./README.md) | English
|
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@ -7,12 +7,12 @@
|
|||||||
JEECG BOOT AI Low Code Platform
|
JEECG BOOT AI Low Code Platform
|
||||||
===============
|
===============
|
||||||
|
|
||||||
Current version: 3.9.1 (Release date: 2026-01-22)
|
Current version: 3.8.3 (Release date: 2025-10-09)
|
||||||
|
|
||||||
|
|
||||||
[](https://github.com/zhangdaiscott/jeecg-boot/blob/master/LICENSE)
|
[](https://github.com/zhangdaiscott/jeecg-boot/blob/master/LICENSE)
|
||||||
[](http://www.jeecg.com)
|
[](http://www.jeecg.com)
|
||||||
[](https://github.com/zhangdaiscott/jeecg-boot)
|
[](https://github.com/zhangdaiscott/jeecg-boot)
|
||||||
[](https://github.com/zhangdaiscott/jeecg-boot)
|
[](https://github.com/zhangdaiscott/jeecg-boot)
|
||||||
[](https://github.com/zhangdaiscott/jeecg-boot)
|
[](https://github.com/zhangdaiscott/jeecg-boot)
|
||||||
|
|
||||||
124
README-Enterprise.md
Normal file
124
README-Enterprise.md
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
|
||||||
|
JeecgBoot低代码平台(商业版介绍)
|
||||||
|
===============
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
项目介绍
|
||||||
|
-----------------------------------
|
||||||
|
|
||||||
|
<h3 align="center">企业级AI低代码平台</h3>
|
||||||
|
|
||||||
|
|
||||||
|
JeecgBoot是一款集成AI应用的,基于BPM流程的低代码平台,旨在帮助企业快速实现低代码开发和构建个性化AI应用!前后端分离架构Ant Design&Vue3,SpringBoot,SpringCloud Alibaba,Mybatis-plus,Shiro。强大的代码生成器让前后端代码一键生成,无需写任何代码! 引领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://www.bilibili.com/video/BV1Nk4y1o7Qc)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### 商业版功能简述
|
||||||
|
|
||||||
|
> 详细的功能介绍,[请联系官方](https://jeecg.com/vip)
|
||||||
|
|
||||||
|
```
|
||||||
|
│─更多商业功能
|
||||||
|
│ ├─流程设计器
|
||||||
|
│ ├─简流设计器(类钉钉版)
|
||||||
|
│ ├─门户设计(NEW)
|
||||||
|
│ ├─表单设计器
|
||||||
|
│ ├─大屏设计器
|
||||||
|
│ └─我的任务
|
||||||
|
│ └─历史流程
|
||||||
|
│ └─历史流程
|
||||||
|
│ └─流程实例管理
|
||||||
|
│ └─流程监听管理
|
||||||
|
│ └─流程表达式
|
||||||
|
│ └─我发起的流程
|
||||||
|
│ └─我的抄送
|
||||||
|
│ └─流程委派、抄送、跳转
|
||||||
|
│ └─OA办公组件
|
||||||
|
│ └─零代码应用管理(无需编码,在线搭建应用系统)
|
||||||
|
│ ├─积木报表企业版(含jimureport、jimubi)
|
||||||
|
│ ├─AI流程设计器源码
|
||||||
|
│ ├─Online全模块功能和源码
|
||||||
|
│ ├─AI写文章(CMS)
|
||||||
|
│ ├─AI表单字段建议(表单设计器)
|
||||||
|
│ ├─OA办公协同组件
|
||||||
|
│ ├─在线聊天功能
|
||||||
|
│ ├─设计表单移动适配
|
||||||
|
│ ├─设计表单支持外部填报
|
||||||
|
│ ├─设计表单AI字段建议
|
||||||
|
│ ├─设计表单视图功能(支持多种类型含日历、表格、看板、甘特图)
|
||||||
|
│ └─。。。
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##### 流程设计
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##### 表单设计器
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
74
README.md
74
README.md
@ -1,15 +1,15 @@
|
|||||||
中文 | [English](./README.en-US.md)
|
|
||||||
|
|
||||||
JeecgBoot AI低代码平台
|
JeecgBoot AI低代码平台
|
||||||
===============
|
===============
|
||||||
|
|
||||||
当前最新版本: 3.9.1(发布日期:2026-01-22)
|
当前最新版本: 3.8.3(发布日期:2025-10-09)
|
||||||
|
|
||||||
|
> 重要提醒: v3.8.3 是 SpringBoot2 的最终版本,后续将不再维护。建议大家尽快升级至基于 SpringBoot3版本,以获得更好的性能和支持。
|
||||||
|
|
||||||
[](https://github.com/jeecgboot/JeecgBoot/blob/master/LICENSE)
|
[](https://github.com/jeecgboot/JeecgBoot/blob/master/LICENSE)
|
||||||
[](https://jeecg.com)
|
[](https://jeecg.com)
|
||||||
[](https://jeecg.blog.csdn.net)
|
[](https://jeecg.blog.csdn.net)
|
||||||
[](https://github.com/jeecgboot/JeecgBoot)
|
[](https://github.com/jeecgboot/JeecgBoot)
|
||||||
[](https://github.com/jeecgboot/JeecgBoot)
|
[](https://github.com/jeecgboot/JeecgBoot)
|
||||||
[](https://github.com/jeecgboot/JeecgBoot)
|
[](https://github.com/jeecgboot/JeecgBoot)
|
||||||
|
|
||||||
@ -20,21 +20,16 @@ JeecgBoot AI低代码平台
|
|||||||
|
|
||||||
<h3 align="center">企业级AI低代码平台</h3>
|
<h3 align="center">企业级AI低代码平台</h3>
|
||||||
|
|
||||||
JeecgBoot 是一款融合代码生成与AI应用的低代码开发平台,助力企业快速实现低代码开发和构建AI应用。平台支持MCP和插件扩展,提供聊天式业务操作(如“一句话创建用户”),大幅提升开发效率与用户便捷性。
|
JeecgBoot是一款企业级低代码平台集成了AI应用平台功能,旨在帮助开发者快速实现低代码开发和构建、部署个性化的 AI 应用。
|
||||||
|
前后端分离架构Ant Design4、Vue3,SpringBoot2/3,SpringCloud Alibaba,Mybatis-plus,Shiro/SpringAuthorizationServer,强大的代码生成器让前后端代码一键生成,无需写任何代码;提供强大的报表和大屏工具,满足企业级数据产品需求!
|
||||||
|
引领AI低代码开发模式: AI生成->OnlineCoding-> 代码生成-> 手工MERGE, 帮助Java项目解决80%的重复工作,让开发更多关注业务,提高效率、节省成本,同时又不失灵活性!低代码能力:Online表单、表单设计、流程设计、Online报表、大屏/仪表盘设计、报表设计; AI应用平台功能:AI知识库问答、AI模型管理、AI流程编排、AI聊天等,支持含ChatGPT、DeepSeek、Ollama等多种AI大模型
|
||||||
|
|
||||||
采用前后端分离架构(Ant Design&Vue3,SpringBoot3,SpringCloud Alibaba,Mybatis-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业务流程:` 采用工作流来实现、扩展出任务接口,供开发编写业务逻辑,表单提供多种解决方案: 表单设计器、online配置表单、编码表单。同时实现了流程与表单的分离设计(松耦合)、并支持任务节点灵活配置,既保证了公司流程的保密性,又减少了开发人员的工作量。
|
||||||
|
|
||||||
`JEECG宗旨是:` JEECG旨在通过OnlineCoding平台实现简单功能的零代码快速搭建,同时针对复杂功能采用代码生成器生成代码并手工合并,打造智能且灵活的低代码开发模式,有效解决了当前低代码产品普遍缺乏灵活性的问题,提升开发效率的同时兼顾系统的扩展性和定制化能力。
|
|
||||||
|
|
||||||
`JEECG业务流程:` JEECG业务流程采用BPM工作流引擎实现业务审批,扩展任务接口供开发人员编写业务逻辑,表单提供表单设计器、在线配置表单和编码表单等多种解决方案。通过流程与表单的分离设计(松耦合)及任务节点的灵活配置,既保障了企业流程的安全性与保密性,又大幅降低了开发人员的工作量。
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -42,8 +37,8 @@ JeecgBoot 是一款融合代码生成与AI应用的低代码开发平台,助
|
|||||||
|
|
||||||
适用项目
|
适用项目
|
||||||
-----------------------------------
|
-----------------------------------
|
||||||
JeecgBoot低代码平台兼容所有J2EE项目开发,支持信创国产化,特别适用于SAAS、企业信息管理系统(MIS)、内部办公系统(OA)、企业资源计划系统(ERP)、客户关系管理系统(CRM)及AI知识库等场景。其半智能手工Merge开发模式,可显著提升70%以上的开发效率,极大降低开发成本。同时,JeecgBoot还是一款全栈式AI开发平台,助力企业快速构建和部署个性化AI应用。。
|
JeecgBoot低代码平台,可以应用在任何J2EE项目的开发中,支持信创国产化。尤其适合SAAS项目、企业信息管理系统(MIS)、内部办公系统(OA)、企业资源计划系统(ERP)、客户关系管理系统(CRM)、AI知识库等,其半智能手工Merge的开发方式,可以显著提高开发效率70%以上,极大降低开发成本。
|
||||||
|
又是一个全栈式 AI 开发平台,快速帮助企业构建和部署个性化的 AI 应用。
|
||||||
|
|
||||||
**信创兼容说明**
|
**信创兼容说明**
|
||||||
- 操作系统:国产麒麟、银河麒麟等国产系统几乎都是基于 Linux 内核,因此它们具有良好的兼容性。
|
- 操作系统:国产麒麟、银河麒麟等国产系统几乎都是基于 Linux 内核,因此它们具有良好的兼容性。
|
||||||
@ -54,16 +49,15 @@ JeecgBoot低代码平台兼容所有J2EE项目开发,支持信创国产化,
|
|||||||
版本说明
|
版本说明
|
||||||
-----------------------------------
|
-----------------------------------
|
||||||
|
|
||||||
|下载 | SpringBoot3.5 + Shiro |SpringBoot3.5+ SpringAuthorizationServer | SpringBoot3.5 + Sa-Token | SpringBoot2.7(JDK17/JDK8) |
|
|下载 | JDK17 + SpringBoot3.3 + Shiro |JDK17 + SpringBoot3.3+ SpringAuthorizationServer | JDK17/JDK8 + SpringBoot2.7 |
|
||||||
|------|---------------------------------------------------------|----------------------------|-------------------|--------------------------------------------|
|
|------|----------------------------------------------------|-----------------------------------------------------------------------------|--------------------------------------------|
|
||||||
| 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) 分支|
|
| 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 | [`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) 分支 |
|
| 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最新技术栈).
|
- `jeecgboot-vue3` 是前端VUE3源码项目(vue3+vite6+ts最新技术栈).
|
||||||
- `JeecgUniapp` 是[配套APP框架](https://github.com/jeecgboot/JeecgUniapp) 适配多个终端,支持APP、小程序、H5、鸿蒙、鸿蒙Next.
|
- `JeecgUniapp` 是[配套APP框架](https://github.com/jeecgboot/JeecgUniapp) 适配多个终端,支持APP、小程序、H5、鸿蒙、鸿蒙Next.
|
||||||
- `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,制作一个精简版本
|
- 参考 [文档](https://help.jeecg.com/ui/2dev/mini) 可以删除不需要的demo,制作一个精简版本
|
||||||
|
|
||||||
|
|
||||||
@ -88,7 +82,7 @@ JeecgBoot低代码平台兼容所有J2EE项目开发,支持信创国产化,
|
|||||||
- 官方网站: [http://www.jeecg.com](http://www.jeecg.com)
|
- 官方网站: [http://www.jeecg.com](http://www.jeecg.com)
|
||||||
- 在线演示: [平台演示](https://boot3.jeecg.com) | [APP演示](https://jeecg.com/appIndex)
|
- 在线演示: [平台演示](https://boot3.jeecg.com) | [APP演示](https://jeecg.com/appIndex)
|
||||||
- 入门指南: [快速入门](http://www.jeecg.com/doc/quickstart) | [代码生成使用](https://help.jeecg.com/java/codegen/online) | [开发文档](https://help.jeecg.com) | [AI应用手册](https://help.jeecg.com/aigc) | [视频教程](http://jeecg.com/doc/video)
|
- 入门指南: [快速入门](http://www.jeecg.com/doc/quickstart) | [代码生成使用](https://help.jeecg.com/java/codegen/online) | [开发文档](https://help.jeecg.com) | [AI应用手册](https://help.jeecg.com/aigc) | [视频教程](http://jeecg.com/doc/video)
|
||||||
- AI编程实战视频: [JEECG低代码与Cursor+GitHub Copilot实现AI高效编程实战](https://www.bilibili.com/video/BV11XyaBVEoH)
|
- AI编程: [JEECG低代码+AI编程Cursor+GitHub Copilot实现高效编程](https://www.bilibili.com/video/BV11XyaBVEoH)
|
||||||
- 技术支持: [反馈问题](https://github.com/jeecgboot/JeecgBoot/issues/new?template=bug_report.md) | [低代码体验一分钟](https://jeecg.blog.csdn.net/article/details/106079007)
|
- 技术支持: [反馈问题](https://github.com/jeecgboot/JeecgBoot/issues/new?template=bug_report.md) | [低代码体验一分钟](https://jeecg.blog.csdn.net/article/details/106079007)
|
||||||
- QQ交流群 : 964611995、⑩716488839(满)、⑨808791225(满)、其他(满)
|
- QQ交流群 : 964611995、⑩716488839(满)、⑨808791225(满)、其他(满)
|
||||||
|
|
||||||
@ -110,7 +104,7 @@ JeecgBoot平台提供了一套完善的AI应用管理系统模块,是一套类
|
|||||||
|
|
||||||
为什么选择JeecgBoot?
|
为什么选择JeecgBoot?
|
||||||
-----------------------------------
|
-----------------------------------
|
||||||
- 1.采用最新主流前后分离框架(Spring Boot3 + MyBatis + Shiro/SpringAuthorizationServer + Ant Design4 + Vue3),容易上手;代码生成器依赖性低,灵活的扩展能力,可快速实现二次开发。
|
- 1.采用最新主流前后分离框架(Spring Boot3 + MyBatis + Shiro//SpringAuthorizationServer + Ant Design4 + Vue3),容易上手;代码生成器依赖性低,灵活的扩展能力,可快速实现二次开发。
|
||||||
- 2.前端大版本换代,最新版采用 Vue3.0 + TypeScript + Vite6 + Ant Design Vue4 等新技术方案。
|
- 2.前端大版本换代,最新版采用 Vue3.0 + TypeScript + Vite6 + Ant Design Vue4 等新技术方案。
|
||||||
- 3.支持微服务Spring Cloud Alibaba(Nacos、Gateway、Sentinel、Skywalking),提供简易机制,支持单体和微服务自由切换(这样可以满足各类项目需求)。
|
- 3.支持微服务Spring Cloud Alibaba(Nacos、Gateway、Sentinel、Skywalking),提供简易机制,支持单体和微服务自由切换(这样可以满足各类项目需求)。
|
||||||
- 4.开发效率高,支持在线建表和AI建表,提供强大代码生成器,单表、树列表、一对多、一对一等数据模型,增删改查功能一键生成,菜单配置直接使用。
|
- 4.开发效率高,支持在线建表和AI建表,提供强大代码生成器,单表、树列表、一对多、一对一等数据模型,增删改查功能一键生成,菜单配置直接使用。
|
||||||
@ -164,7 +158,7 @@ JeecgBoot平台提供了一套完善的AI应用管理系统模块,是一套类
|
|||||||
|
|
||||||
- 前端环境要求:Node.js要求`Node 20+` 版本以上、pnpm 要求`9+` 版本以上
|
- 前端环境要求:Node.js要求`Node 20+` 版本以上、pnpm 要求`9+` 版本以上
|
||||||
|
|
||||||
` ( Vite 不再支持已结束生命周期(EOL)的 Node.js 18。现在需要使用 Node.js 20.19+ 或 22.12+)`
|
> Vite 不再支持已结束生命周期(EOL)的 Node.js 18。现在需要使用 Node.js 20.19+ 或 22.12+。
|
||||||
|
|
||||||
- 依赖管理:node、npm、pnpm
|
- 依赖管理:node、npm、pnpm
|
||||||
- 前端IDE建议:IDEA、WebStorm、Vscode
|
- 前端IDE建议:IDEA、WebStorm、Vscode
|
||||||
@ -175,16 +169,16 @@ JeecgBoot平台提供了一套完善的AI应用管理系统模块,是一套类
|
|||||||
#### 后端
|
#### 后端
|
||||||
|
|
||||||
- IDE建议: IDEA (必须安装lombok插件 )
|
- IDE建议: IDEA (必须安装lombok插件 )
|
||||||
- 语言:Java 默认jdk17(jdk21、jdk24)
|
- 语言:Java 默认jdk17(支持jdk8、jdk21)
|
||||||
- 依赖管理:Maven
|
- 依赖管理:Maven
|
||||||
- 基础框架:Spring Boot 3.5.5
|
- 基础框架:Spring Boot 3.5.5/2.7.18
|
||||||
- 微服务框架: Spring Cloud Alibaba 2023.0.3.3
|
- 微服务框架: Spring Cloud Alibaba 2021.0.6.2
|
||||||
- 持久层框架:MybatisPlus 3.5.12
|
- 持久层框架:MybatisPlus 3.5.3.2
|
||||||
- 报表工具: JimuReport 2.1.3
|
- 报表工具: JimuReport 2.1.3
|
||||||
- 安全框架:Apache Shiro 2.0.4,Jwt 4.5.0
|
- 安全框架:Apache Shiro/SpringAuthorizationServer,Jwt 4.5.0
|
||||||
- 微服务技术栈:Spring Cloud Alibaba、Nacos、Gateway、Sentinel、Skywalking
|
- 微服务技术栈:Spring Cloud Alibaba、Nacos、Gateway、Sentinel、Skywalking
|
||||||
- 数据库连接池:阿里巴巴Druid 1.2.24
|
- 数据库连接池:阿里巴巴Druid 1.1.24
|
||||||
- AI大模型:支持 `ChatGPT` `DeepSeek` `千问`等各种常规模式
|
- AI大模型:支持 `ChatGPT` `DeepSeek`切换
|
||||||
- 日志打印:logback
|
- 日志打印:logback
|
||||||
- 缓存:Redis
|
- 缓存:Redis
|
||||||
- 其他:autopoi, fastjson,poi,Swagger-ui,quartz, lombok(简化代码)等。
|
- 其他:autopoi, fastjson,poi,Swagger-ui,quartz, lombok(简化代码)等。
|
||||||
@ -233,6 +227,20 @@ JeecgBoot平台提供了一套完善的AI应用管理系统模块,是一套类
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
开源版与企业版区别?
|
||||||
|
-----------------------------------
|
||||||
|
|
||||||
|
- 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 产品功能蓝图
|
### Jeecg Boot 产品功能蓝图
|
||||||

|

|
||||||
|
|
||||||
@ -505,4 +513,4 @@ AI写文章
|
|||||||
|
|
||||||
如果觉得还不错,请作者喝杯咖啡吧 ☺
|
如果觉得还不错,请作者喝杯咖啡吧 ☺
|
||||||
|
|
||||||

|

|
||||||
|
|||||||
@ -125,7 +125,7 @@ services:
|
|||||||
hostname: jeecg-boot-sentinel
|
hostname: jeecg-boot-sentinel
|
||||||
networks:
|
networks:
|
||||||
- jeecg-boot
|
- jeecg-boot
|
||||||
|
|
||||||
jeecg-boot-xxljob:
|
jeecg-boot-xxljob:
|
||||||
build:
|
build:
|
||||||
context: ./jeecg-boot/jeecg-server-cloud/jeecg-visual/jeecg-cloud-xxljob
|
context: ./jeecg-boot/jeecg-server-cloud/jeecg-visual/jeecg-cloud-xxljob
|
||||||
@ -135,7 +135,7 @@ services:
|
|||||||
hostname: jeecg-boot-xxljob
|
hostname: jeecg-boot-xxljob
|
||||||
networks:
|
networks:
|
||||||
- jeecg-boot
|
- jeecg-boot
|
||||||
|
|
||||||
jeecg-vue:
|
jeecg-vue:
|
||||||
build:
|
build:
|
||||||
context: ./jeecgboot-vue3
|
context: ./jeecgboot-vue3
|
||||||
|
|||||||
@ -1,12 +1,13 @@
|
|||||||
|
|
||||||
JeecgBoot 低代码开发平台
|
JeecgBoot 低代码开发平台
|
||||||
===============
|
===============
|
||||||
|
|
||||||
当前最新版本: 3.9.1(发布日期: 2026-01-22)
|
当前最新版本: 3.8.3(发布日期:2025-09-22)
|
||||||
|
|
||||||
|
|
||||||
[](https://github.com/zhangdaiscott/jeecg-boot/blob/master/LICENSE)
|
[](https://github.com/zhangdaiscott/jeecg-boot/blob/master/LICENSE)
|
||||||
[](http://jeecg.com/aboutusIndex)
|
[](http://jeecg.com/aboutusIndex)
|
||||||
[](https://github.com/zhangdaiscott/jeecg-boot)
|
[](https://github.com/zhangdaiscott/jeecg-boot)
|
||||||
[](https://github.com/zhangdaiscott/jeecg-boot)
|
[](https://github.com/zhangdaiscott/jeecg-boot)
|
||||||
[](https://github.com/zhangdaiscott/jeecg-boot)
|
[](https://github.com/zhangdaiscott/jeecg-boot)
|
||||||
|
|
||||||
@ -15,127 +16,43 @@ JeecgBoot 低代码开发平台
|
|||||||
项目介绍
|
项目介绍
|
||||||
-----------------------------------
|
-----------------------------------
|
||||||
|
|
||||||
<h3 align="center">企业级AI低代码平台</h3>
|
<h3 align="center">Java Low Code Platform for Enterprise web applications</h3>
|
||||||
|
|
||||||
JeecgBoot 是一款基于BPM流程和代码生成的AI低代码平台,助力企业快速实现低代码开发和构建AI应用。
|
|
||||||
采用前后端分离架构(Ant Design&Vue3,SpringBoot3,SpringCloud Alibaba,Mybatis-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/)
|
|
||||||
|
|
||||||
|
|
||||||
|
JeecgBoot 是一款基于代码生成器的`低代码开发平台`!前后端分离架构 SpringBoot2.x和3.x,SpringCloud,Ant Design Vue3,Mybatis-plus,Shiro,JWT,支持微服务。强大的代码生成器让前后端代码一键生成,实现低代码开发! JeecgBoot 引领新的低代码开发模式(OnlineCoding-> 代码生成器-> 手工MERGE), 帮助解决Java项目70%的重复工作,让开发更多关注业务。既能快速提高效率,节省研发成本,同时又不失灵活性!
|
||||||
|
|
||||||
|
|
||||||
#### 项目说明
|
#### 项目说明
|
||||||
|
|
||||||
| 项目名 | 说明 |
|
| 项目名 | 说明 |
|
||||||
|--------------------|------------------------------------|
|
|--------------------|------------------------|
|
||||||
| `jeecg-boot` | 后端源码JAVA(SpringBoot3微服务架构) |
|
| `jeecg-boot` | 后端源码JAVA(SpringBoot微服务架构) |
|
||||||
| `jeecgboot-vue3` | 前端源码VUE3(vue3+vite6+antd4+ts最新技术栈) |
|
| `jeecgboot-vue3` | 前端源码VUE3(vue3+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)
|
- 官方网站: [http://www.jeecg.com](http://www.jeecg.com)
|
||||||
- 在线演示: [平台演示](https://boot3.jeecg.com) | [APP演示](https://jeecg.com/appIndex)
|
- 新手指南: [快速入门](http://www.jeecg.com/doc/quickstart)
|
||||||
- 入门指南: [快速入门](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(满)、其他(满)
|
- QQ交流群 : 964611995、⑩716488839(满)、⑨808791225(满)、其他(满)
|
||||||
|
- 在线演示 : [在线演示](http://boot3.jeecg.com) | [APP演示](http://jeecg.com/appIndex)
|
||||||
|
> 演示系统的登录账号密码,请点击 [获取账号密码](http://jeecg.com/doc/demo) 获取
|
||||||
|
|
||||||
|
|
||||||
AI 应用平台介绍
|
|
||||||
|
启动项目
|
||||||
-----------------------------------
|
-----------------------------------
|
||||||
|
|
||||||
一个全栈式 AI 开发平台,旨在帮助开发者快速构建和部署个性化的 AI 应用。
|
- [IDEA启动前后端项目](https://help.jeecg.com/java/setup/idea/startup)
|
||||||
|
- [Docker一键启动前后端](https://help.jeecg.com/java/docker/quick)
|
||||||
JeecgBoot平台提供了一套完善的AI应用管理系统模块,是一套类似`Dify`的`AIGC应用开发平台`+`知识库问答`,是一款基于LLM大语言模型AI应用平台和 RAG 的知识库问答系统。
|
|
||||||
其直观的界面结合了 AI 流程编排、RAG 管道、知识库管理、模型管理、对接向量库、实时运行可观察等,让您可以快速从原型到生产,拥有AI服务能力。
|
|
||||||
|
|
||||||
- [详细专题介绍,请点击查看](README-AI.md)
|
|
||||||
|
|
||||||
- AI视频介绍
|
|
||||||
|
|
||||||
[](https://www.bilibili.com/video/BV1zmd7YFE4w)
|
|
||||||
|
|
||||||
|
|
||||||
为什么选择JeecgBoot?
|
微服务启动
|
||||||
-----------------------------------
|
-----------------------------------
|
||||||
- 1.采用最新主流前后分离框架(Spring Boot3 + MyBatis + Shiro/SpringAuthorizationServer + Ant Design4 + Vue3),容易上手;代码生成器依赖性低,灵活的扩展能力,可快速实现二次开发。
|
- [单体快速切换微服务](https://help.jeecg.com/java/springcloud/switchcloud/monomer)
|
||||||
- 2.前端大版本换代,最新版采用 Vue3.0 + TypeScript + Vite6 + Ant Design Vue4 等新技术方案。
|
- [Docker启动微服务后台](https://help.jeecg.com/java/docker/springcloud)
|
||||||
- 3.支持微服务Spring Cloud Alibaba(Nacos、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.权限控制采用RBAC(Role-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.提供丰富的示例代码,涵盖了常用的业务场景,便于学习和参考。
|
|
||||||
|
|
||||||
|
|
||||||
技术架构:
|
技术架构:
|
||||||
@ -144,33 +61,28 @@ JeecgBoot平台提供了一套完善的AI应用管理系统模块,是一套类
|
|||||||
#### 后端
|
#### 后端
|
||||||
|
|
||||||
- IDE建议: IDEA (必须安装lombok插件 )
|
- IDE建议: IDEA (必须安装lombok插件 )
|
||||||
- 语言:Java 默认jdk17(jdk21、jdk24)
|
- 语言:Java 8+ (支持17)
|
||||||
- 依赖管理:Maven
|
- 依赖管理:Maven
|
||||||
- 基础框架:Spring Boot 3.5.5
|
- 基础框架:Spring Boot 2.7.18
|
||||||
- 微服务框架: Spring Cloud Alibaba 2023.0.3.3
|
- 微服务框架: Spring Cloud Alibaba 2021.0.1.0
|
||||||
- 持久层框架:MybatisPlus 3.5.12
|
- 持久层框架:MybatisPlus 3.5.3.2
|
||||||
- 报表工具: JimuReport 2.1.3
|
- 报表工具: JimuReport 1.9.4
|
||||||
- 安全框架:Apache Shiro 2.0.4,Jwt 4.5.0
|
- 安全框架:Apache Shiro 1.12.0,Jwt 3.11.0
|
||||||
- 微服务技术栈:Spring Cloud Alibaba、Nacos、Gateway、Sentinel、Skywalking
|
- 微服务技术栈:Spring Cloud Alibaba、Nacos、Gateway、Sentinel、Skywalking
|
||||||
- 数据库连接池:阿里巴巴Druid 1.2.24
|
- 数据库连接池:阿里巴巴Druid 1.1.24
|
||||||
- AI大模型:支持 `ChatGPT` `DeepSeek` `千问`等各种常规模式
|
|
||||||
- 日志打印:logback
|
- 日志打印:logback
|
||||||
- 缓存:Redis
|
- 缓存:Redis
|
||||||
- 其他:autopoi, fastjson,poi,Swagger-ui,quartz, lombok(简化代码)等。
|
- 其他:autopoi, fastjson,poi,Swagger-ui,quartz, lombok(简化代码)等。
|
||||||
- 默认提供MySQL5.7+数据库脚本
|
- 默认数据库脚本:MySQL5.7+
|
||||||
- [其他数据库,需要自己转](https://my.oschina.net/jeecg/blog/4905722)
|
- [其他数据库,需要自己转](https://my.oschina.net/jeecg/blog/4905722)
|
||||||
|
|
||||||
|
|
||||||
#### 前端
|
#### 前端
|
||||||
|
|
||||||
- 前端环境要求:Node.js要求`Node 20+` 版本以上、pnpm 要求`9+` 版本以上
|
- 前端IDE建议:WebStorm、Vscode
|
||||||
` ( Vite 不再支持已结束生命周期(EOL)的 Node.js 18。现在需要使用 Node.js 20.19+ 或 22.12+)`
|
- 采用 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
|
- 依赖管理: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
BIN
jeecg-boot/db/其他数据库脚本/jeecgboot-oracle11g.dmp
Normal file
BIN
jeecg-boot/db/其他数据库脚本/jeecgboot-oracle11g.dmp
Normal file
Binary file not shown.
29702
jeecg-boot/db/其他数据库脚本/jeecgboot-oracle11g.sql
Normal file
29702
jeecg-boot/db/其他数据库脚本/jeecgboot-oracle11g.sql
Normal file
File diff suppressed because one or more lines are too long
25749
jeecg-boot/db/其他数据库脚本/jeecgboot-postgresql17.sql
Normal file
25749
jeecg-boot/db/其他数据库脚本/jeecgboot-postgresql17.sql
Normal file
File diff suppressed because one or more lines are too long
48117
jeecg-boot/db/其他数据库脚本/jeecgboot-sqlserver2017.sql
Normal file
48117
jeecg-boot/db/其他数据库脚本/jeecgboot-sqlserver2017.sql
Normal file
File diff suppressed because one or more lines are too long
@ -3,7 +3,6 @@
|
|||||||
> JeecgBoot属于平台级产品,每次升级改动较大,目前做不到平滑升级。
|
> JeecgBoot属于平台级产品,每次升级改动较大,目前做不到平滑升级。
|
||||||
|
|
||||||
### 增量升级方案
|
### 增量升级方案
|
||||||
|
|
||||||
#### 1.代码合并
|
#### 1.代码合并
|
||||||
本地通过svn或git做好主干,在分支上做业务开发,jeecg每次版本发布,可以手工覆盖主干的代码,对比合并代码;
|
本地通过svn或git做好主干,在分支上做业务开发,jeecg每次版本发布,可以手工覆盖主干的代码,对比合并代码;
|
||||||
|
|
||||||
@ -12,12 +11,5 @@
|
|||||||
- 其他库请手工执行SQL, 目录: `jeecg-module-system\jeecg-system-start\src\main\resources\flyway\sql\mysql`
|
- 其他库请手工执行SQL, 目录: `jeecg-module-system\jeecg-system-start\src\main\resources\flyway\sql\mysql`
|
||||||
> 注意: 升级sql只提供mysql版本;如果有权限升级, 还需要手工角色授权,退出重新登录才好使。
|
> 注意: 升级sql只提供mysql版本;如果有权限升级, 还需要手工角色授权,退出重新登录才好使。
|
||||||
|
|
||||||
#### 3.其他数据库脚本说明
|
#### 3.兼容问题
|
||||||
原先官方默认提供oracle和SqlServer的脚本,但是维护成本太高,未提供脚本的数据库,可以参考下面的文档自己转
|
|
||||||
https://my.oschina.net/jeecg/blog/4905722
|
|
||||||
(注意:定时任务的表qrtz_*,需要删掉用原始的脚本重新执行一下)
|
|
||||||
quartz-2.2.3-distribution.tar.gz放到百度网盘中,大家自己下载,执行所需数据库脚本
|
|
||||||
https://pan.baidu.com/s/1WrmZdUuAPg3iBwJ-LoHWyg?pwd=8mdz
|
|
||||||
|
|
||||||
#### 4.兼容问题
|
|
||||||
每次发版,会针对不兼容地方重点说明。
|
每次发版,会针对不兼容地方重点说明。
|
||||||
@ -63,7 +63,6 @@ services:
|
|||||||
build:
|
build:
|
||||||
context: ./jeecg-module-system/jeecg-system-start
|
context: ./jeecg-module-system/jeecg-system-start
|
||||||
restart: on-failure
|
restart: on-failure
|
||||||
mac_address: 02:42:ac:11:00:02
|
|
||||||
depends_on:
|
depends_on:
|
||||||
- jeecg-boot-mysql
|
- jeecg-boot-mysql
|
||||||
- jeecg-boot-redis
|
- jeecg-boot-redis
|
||||||
|
|||||||
@ -2,9 +2,9 @@
|
|||||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
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">
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
<parent>
|
<parent>
|
||||||
<groupId>org.jeecgframework.boot3</groupId>
|
<groupId>org.jeecgframework.boot</groupId>
|
||||||
<artifactId>jeecg-boot-parent</artifactId>
|
<artifactId>jeecg-boot-parent</artifactId>
|
||||||
<version>3.9.1</version>
|
<version>3.8.3</version>
|
||||||
</parent>
|
</parent>
|
||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
<artifactId>jeecg-boot-base-core</artifactId>
|
<artifactId>jeecg-boot-base-core</artifactId>
|
||||||
@ -42,14 +42,8 @@
|
|||||||
<dependencies>
|
<dependencies>
|
||||||
<!--jeecg-tools-->
|
<!--jeecg-tools-->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.jeecgframework.boot3</groupId>
|
<groupId>org.jeecgframework.boot</groupId>
|
||||||
<artifactId>jeecg-boot-common</artifactId>
|
<artifactId>jeecg-boot-common</artifactId>
|
||||||
<exclusions>
|
|
||||||
<exclusion>
|
|
||||||
<groupId>commons-logging</groupId>
|
|
||||||
<artifactId>commons-logging</artifactId>
|
|
||||||
</exclusion>
|
|
||||||
</exclusions>
|
|
||||||
</dependency>
|
</dependency>
|
||||||
<!--集成springmvc框架并实现自动配置 -->
|
<!--集成springmvc框架并实现自动配置 -->
|
||||||
<dependency>
|
<dependency>
|
||||||
@ -104,7 +98,7 @@
|
|||||||
<!-- mybatis-plus -->
|
<!-- mybatis-plus -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.baomidou</groupId>
|
<groupId>com.baomidou</groupId>
|
||||||
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
|
<artifactId>mybatis-plus-boot-starter</artifactId>
|
||||||
<version>${mybatis-plus.version}</version>
|
<version>${mybatis-plus.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
@ -113,22 +107,22 @@
|
|||||||
<version>${mybatis-plus.version}</version>
|
<version>${mybatis-plus.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<!-- minidao -->
|
<!-- minidao -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.jeecgframework.boot3</groupId>
|
<groupId>org.jeecgframework</groupId>
|
||||||
<artifactId>minidao-spring-boot-starter-jsqlparser-4.9</artifactId>
|
<artifactId>minidao-spring-boot-starter-jsqlparser-4.9</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- druid -->
|
<!-- druid -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.alibaba</groupId>
|
<groupId>com.alibaba</groupId>
|
||||||
<artifactId>druid-spring-boot-3-starter</artifactId>
|
<artifactId>druid-spring-boot-starter</artifactId>
|
||||||
<version>${druid.version}</version>
|
<version>${druid.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- 动态数据源 -->
|
<!-- 动态数据源 -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.baomidou</groupId>
|
<groupId>com.baomidou</groupId>
|
||||||
<artifactId>dynamic-datasource-spring-boot3-starter</artifactId>
|
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
|
||||||
<version>${dynamic-datasource-spring-boot-starter.version}</version>
|
<version>${dynamic-datasource-spring-boot-starter.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
@ -179,6 +173,7 @@
|
|||||||
<artifactId>DmDialect-for-hibernate5.0</artifactId>
|
<artifactId>DmDialect-for-hibernate5.0</artifactId>
|
||||||
<version>${dm8.version}</version>
|
<version>${dm8.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- Quartz定时任务 -->
|
<!-- Quartz定时任务 -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
@ -196,56 +191,7 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.apache.shiro</groupId>
|
<groupId>org.apache.shiro</groupId>
|
||||||
<artifactId>shiro-spring-boot-starter</artifactId>
|
<artifactId>shiro-spring-boot-starter</artifactId>
|
||||||
<classifier>jakarta</classifier>
|
|
||||||
<version>${shiro.version}</version>
|
<version>${shiro.version}</version>
|
||||||
<exclusions>
|
|
||||||
<exclusion>
|
|
||||||
<groupId>org.apache.shiro</groupId>
|
|
||||||
<artifactId>shiro-spring</artifactId>
|
|
||||||
</exclusion>
|
|
||||||
</exclusions>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.apache.shiro</groupId>
|
|
||||||
<artifactId>shiro-spring</artifactId>
|
|
||||||
<classifier>jakarta</classifier>
|
|
||||||
<version>${shiro.version}</version>
|
|
||||||
<!-- 排除仍使用了javax.servlet的依赖 -->
|
|
||||||
<exclusions>
|
|
||||||
<exclusion>
|
|
||||||
<groupId>org.apache.shiro</groupId>
|
|
||||||
<artifactId>shiro-core</artifactId>
|
|
||||||
</exclusion>
|
|
||||||
<exclusion>
|
|
||||||
<groupId>org.apache.shiro</groupId>
|
|
||||||
<artifactId>shiro-web</artifactId>
|
|
||||||
</exclusion>
|
|
||||||
</exclusions>
|
|
||||||
</dependency>
|
|
||||||
<!-- 引入适配jakarta的依赖包 -->
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.apache.shiro</groupId>
|
|
||||||
<artifactId>shiro-core</artifactId>
|
|
||||||
<classifier>jakarta</classifier>
|
|
||||||
<version>${shiro.version}</version>
|
|
||||||
<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>
|
</dependency>
|
||||||
<!-- shiro-redis -->
|
<!-- shiro-redis -->
|
||||||
<dependency>
|
<dependency>
|
||||||
@ -264,16 +210,12 @@
|
|||||||
</exclusions>
|
</exclusions>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!-- knife4j -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.github.xiaoymin</groupId>
|
<groupId>com.github.xiaoymin</groupId>
|
||||||
<artifactId>knife4j-openapi3-ui</artifactId>
|
<artifactId>knife4j-openapi3-spring-boot-starter</artifactId>
|
||||||
<version>${knife4j-spring-boot-starter.version}</version>
|
<version>${knife4j-spring-boot-starter.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
|
||||||
<groupId>org.springdoc</groupId>
|
|
||||||
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
|
|
||||||
<version>2.7.0</version>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<!-- 代码生成器 -->
|
<!-- 代码生成器 -->
|
||||||
<!-- 如下载失败,请参考此文档 https://help.jeecg.com/java/setup/maven.html -->
|
<!-- 如下载失败,请参考此文档 https://help.jeecg.com/java/setup/maven.html -->
|
||||||
@ -296,7 +238,7 @@
|
|||||||
<!-- AutoPoi Excel工具类-->
|
<!-- AutoPoi Excel工具类-->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.jeecgframework</groupId>
|
<groupId>org.jeecgframework</groupId>
|
||||||
<artifactId>autopoi-spring-boot-3-starter</artifactId>
|
<artifactId>autopoi-web</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>xerces</groupId>
|
<groupId>xerces</groupId>
|
||||||
@ -305,7 +247,7 @@
|
|||||||
<optional>true</optional>
|
<optional>true</optional>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- minio文件存储服务 -->
|
<!-- mini文件存储服务 -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.minio</groupId>
|
<groupId>io.minio</groupId>
|
||||||
<artifactId>minio</artifactId>
|
<artifactId>minio</artifactId>
|
||||||
@ -314,14 +256,6 @@
|
|||||||
<artifactId>checker-qual</artifactId>
|
<artifactId>checker-qual</artifactId>
|
||||||
<groupId>org.checkerframework</groupId>
|
<groupId>org.checkerframework</groupId>
|
||||||
</exclusion>
|
</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>
|
</exclusions>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
@ -341,16 +275,6 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.xkcoding.justauth</groupId>
|
<groupId>com.xkcoding.justauth</groupId>
|
||||||
<artifactId>justauth-spring-boot-starter</artifactId>
|
<artifactId>justauth-spring-boot-starter</artifactId>
|
||||||
<exclusions>
|
|
||||||
<exclusion>
|
|
||||||
<groupId>org.springframework.boot</groupId>
|
|
||||||
<artifactId>spring-boot-autoconfigure</artifactId>
|
|
||||||
</exclusion>
|
|
||||||
<exclusion>
|
|
||||||
<groupId>org.springframework.boot</groupId>
|
|
||||||
<artifactId>spring-boot-configuration-processor</artifactId>
|
|
||||||
</exclusion>
|
|
||||||
</exclusions>
|
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.squareup.okhttp3</groupId>
|
<groupId>com.squareup.okhttp3</groupId>
|
||||||
@ -376,24 +300,8 @@
|
|||||||
</dependency>
|
</dependency>
|
||||||
<!-- chatgpt -->
|
<!-- chatgpt -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.jeecgframework.boot3</groupId>
|
<groupId>org.jeecgframework.boot</groupId>
|
||||||
<artifactId>jeecg-boot-starter-chatgpt</artifactId>
|
<artifactId>jeecg-boot-starter-chatgpt</artifactId>
|
||||||
</dependency>
|
</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>
|
</dependencies>
|
||||||
</project>
|
</project>
|
||||||
|
|||||||
@ -132,6 +132,7 @@ public interface CommonAPI {
|
|||||||
*/
|
*/
|
||||||
Map<String, List<DictModel>> translateManyDict(String dictCodes, String keys);
|
Map<String, List<DictModel>> translateManyDict(String dictCodes, String keys);
|
||||||
|
|
||||||
|
//update-begin---author:chenrui ---date:20231221 for:[issues/#5643]解决分布式下表字典跨库无法查询问题------------
|
||||||
/**
|
/**
|
||||||
* 15 字典表的 翻译,可批量
|
* 15 字典表的 翻译,可批量
|
||||||
* @param table
|
* @param table
|
||||||
@ -142,6 +143,7 @@ public interface CommonAPI {
|
|||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
List<DictModel> translateDictFromTableByKeys(String table, String text, String code, String keys, String dataSource);
|
List<DictModel> translateDictFromTableByKeys(String table, String text, String code, String keys, String dataSource);
|
||||||
|
//update-end---author:chenrui ---date:20231221 for:[issues/#5643]解决分布式下表字典跨库无法查询问题------------
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 16 运行AIRag流程
|
* 16 运行AIRag流程
|
||||||
|
|||||||
@ -33,10 +33,4 @@ public class AiragFlowDTO implements Serializable {
|
|||||||
* 输入参数
|
* 输入参数
|
||||||
*/
|
*/
|
||||||
private Map<String, Object> inputParams;
|
private Map<String, Object> inputParams;
|
||||||
|
|
||||||
/**
|
|
||||||
* 是否流式返回
|
|
||||||
*/
|
|
||||||
private boolean isStream;
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
|
||||||
}
|
|
||||||
|
|
||||||
@ -2,7 +2,7 @@ package org.jeecg.common.api.dto;
|
|||||||
|
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import javax.servlet.http.HttpServletResponse;
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -30,10 +30,12 @@ public class OnlineAuthDTO implements Serializable {
|
|||||||
*/
|
*/
|
||||||
private String onlineFormUrl;
|
private String onlineFormUrl;
|
||||||
|
|
||||||
|
//update-begin---author:chenrui ---date:20240123 for:[QQYUN-7992]【online】工单申请下的online表单,未配置online表单开发菜单,操作报错无权限------------
|
||||||
/**
|
/**
|
||||||
* online工单的地址
|
* online工单的地址
|
||||||
*/
|
*/
|
||||||
private String onlineWorkOrderUrl;
|
private String onlineWorkOrderUrl;
|
||||||
|
//update-end---author:chenrui ---date:20240123 for:[QQYUN-7992]【online】工单申请下的online表单,未配置online表单开发菜单,操作报错无权限------------
|
||||||
|
|
||||||
public OnlineAuthDTO(){
|
public OnlineAuthDTO(){
|
||||||
|
|
||||||
|
|||||||
@ -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;
|
|
||||||
}
|
|
||||||
@ -20,14 +20,14 @@ import org.jeecg.common.system.vo.LoginUser;
|
|||||||
import org.jeecg.common.util.IpUtils;
|
import org.jeecg.common.util.IpUtils;
|
||||||
import org.jeecg.common.util.SpringContextUtils;
|
import org.jeecg.common.util.SpringContextUtils;
|
||||||
import org.jeecg.common.util.oConvertUtils;
|
import org.jeecg.common.util.oConvertUtils;
|
||||||
import org.springframework.core.StandardReflectionParameterNameDiscoverer;
|
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.validation.BindingResult;
|
import org.springframework.validation.BindingResult;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
import jakarta.annotation.Resource;
|
import javax.annotation.Resource;
|
||||||
import jakarta.servlet.ServletRequest;
|
import javax.servlet.ServletRequest;
|
||||||
import jakarta.servlet.ServletResponse;
|
import javax.servlet.ServletResponse;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
|
||||||
@ -121,8 +121,9 @@ public class AutoLogAspect {
|
|||||||
if (operateType > 0) {
|
if (operateType > 0) {
|
||||||
return operateType;
|
return operateType;
|
||||||
}
|
}
|
||||||
// 代码逻辑说明: 阿里云代码扫描规范(不允许任何魔法值出现在代码中)------------
|
//update-begin---author:wangshuai ---date:20220331 for:阿里云代码扫描规范(不允许任何魔法值出现在代码中)------------
|
||||||
return OperateTypeEnum.getTypeByMethodName(methodName);
|
return OperateTypeEnum.getTypeByMethodName(methodName);
|
||||||
|
//update-end---author:wangshuai ---date:20220331 for:阿里云代码扫描规范(不允许任何魔法值出现在代码中)------------
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -145,12 +146,11 @@ public class AutoLogAspect {
|
|||||||
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 || paramsArray[i] instanceof MultipartFile[]) {
|
||||||
//ServletRequest不能序列化,从入参里排除,否则报异常:java.lang.IllegalStateException: It is illegal to call this method if the current request is not in asynchronous mode (i.e. isAsyncStarted() returns false)
|
//ServletRequest不能序列化,从入参里排除,否则报异常:java.lang.IllegalStateException: It is illegal to call this method if the current request is not in asynchronous mode (i.e. isAsyncStarted() returns false)
|
||||||
//ServletResponse不能序列化 从入参里排除,否则报异常:java.lang.IllegalStateException: getOutputStream() has already been called for this response
|
//ServletResponse不能序列化 从入参里排除,否则报异常:java.lang.IllegalStateException: getOutputStream() has already been called for this response
|
||||||
//MultipartFile和MultipartFile[]不能序列化,从入参里排除
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
arguments[i] = paramsArray[i];
|
arguments[i] = paramsArray[i];
|
||||||
}
|
}
|
||||||
// 代码逻辑说明: 日志数据太长的直接过滤掉
|
//update-begin-author:taoyan date:20200724 for:日志数据太长的直接过滤掉
|
||||||
PropertyFilter profilter = new PropertyFilter() {
|
PropertyFilter profilter = new PropertyFilter() {
|
||||||
@Override
|
@Override
|
||||||
public boolean apply(Object o, String name, Object value) {
|
public boolean apply(Object o, String name, Object value) {
|
||||||
@ -165,13 +165,14 @@ public class AutoLogAspect {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
params = JSONObject.toJSONString(arguments, profilter);
|
params = JSONObject.toJSONString(arguments, profilter);
|
||||||
|
//update-end-author:taoyan date:20200724 for:日志数据太长的直接过滤掉
|
||||||
} else {
|
} else {
|
||||||
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
|
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
|
||||||
Method method = signature.getMethod();
|
Method method = signature.getMethod();
|
||||||
// 请求的方法参数值
|
// 请求的方法参数值
|
||||||
Object[] args = joinPoint.getArgs();
|
Object[] args = joinPoint.getArgs();
|
||||||
// 请求的方法参数名称
|
// 请求的方法参数名称
|
||||||
StandardReflectionParameterNameDiscoverer u= new StandardReflectionParameterNameDiscoverer();
|
LocalVariableTableParameterNameDiscoverer u = new LocalVariableTableParameterNameDiscoverer();
|
||||||
String[] paramNames = u.getParameterNames(method);
|
String[] paramNames = u.getParameterNames(method);
|
||||||
if (args != null && paramNames != null) {
|
if (args != null && paramNames != null) {
|
||||||
for (int i = 0; i < args.length; i++) {
|
for (int i = 0; i < args.length; i++) {
|
||||||
|
|||||||
@ -105,24 +105,29 @@ public class DictAspect {
|
|||||||
Map<String, List<String>> dataListMap = new HashMap<>(5);
|
Map<String, List<String>> dataListMap = new HashMap<>(5);
|
||||||
//取出结果集
|
//取出结果集
|
||||||
List<Object> records=((IPage) ((Result) result).getResult()).getRecords();
|
List<Object> records=((IPage) ((Result) result).getResult()).getRecords();
|
||||||
// 代码逻辑说明: 【VUEN-1230】 判断是否含有字典注解,没有注解返回-----
|
//update-begin--Author:zyf -- Date:20220606 ----for:【VUEN-1230】 判断是否含有字典注解,没有注解返回-----
|
||||||
Boolean hasDict= checkHasDict(records);
|
Boolean hasDict= checkHasDict(records);
|
||||||
if(!hasDict){
|
if(!hasDict){
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
log.debug(" __ 进入字典翻译切面 DictAspect —— " );
|
log.debug(" __ 进入字典翻译切面 DictAspect —— " );
|
||||||
|
//update-end--Author:zyf -- Date:20220606 ----for:【VUEN-1230】 判断是否含有字典注解,没有注解返回-----
|
||||||
for (Object record : records) {
|
for (Object record : records) {
|
||||||
String json="{}";
|
String json="{}";
|
||||||
try {
|
try {
|
||||||
|
//update-begin--Author:zyf -- Date:20220531 ----for:【issues/#3629】 DictAspect Jackson序列化报错-----
|
||||||
//解决@JsonFormat注解解析不了的问题详见SysAnnouncement类的@JsonFormat
|
//解决@JsonFormat注解解析不了的问题详见SysAnnouncement类的@JsonFormat
|
||||||
json = objectMapper.writeValueAsString(record);
|
json = objectMapper.writeValueAsString(record);
|
||||||
|
//update-end--Author:zyf -- Date:20220531 ----for:【issues/#3629】 DictAspect Jackson序列化报错-----
|
||||||
} catch (JsonProcessingException e) {
|
} catch (JsonProcessingException e) {
|
||||||
log.error("json解析失败"+e.getMessage(),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);
|
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()) {
|
//for (Field field : record.getClass().getDeclaredFields()) {
|
||||||
// 遍历所有字段,把字典Code取出来,放到 map 里
|
// 遍历所有字段,把字典Code取出来,放到 map 里
|
||||||
for (Field field : oConvertUtils.getAllFields(record)) {
|
for (Field field : oConvertUtils.getAllFields(record)) {
|
||||||
@ -130,6 +135,7 @@ public class DictAspect {
|
|||||||
if (oConvertUtils.isEmpty(value)) {
|
if (oConvertUtils.isEmpty(value)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
//update-end--Author:scott -- Date:20190603 ----for:解决继承实体字段无法翻译问题------
|
||||||
if (field.getAnnotation(Dict.class) != null) {
|
if (field.getAnnotation(Dict.class) != null) {
|
||||||
if (!dictFieldList.contains(field)) {
|
if (!dictFieldList.contains(field)) {
|
||||||
dictFieldList.add(field);
|
dictFieldList.add(field);
|
||||||
@ -137,22 +143,26 @@ public class DictAspect {
|
|||||||
String code = field.getAnnotation(Dict.class).dicCode();
|
String code = field.getAnnotation(Dict.class).dicCode();
|
||||||
String text = field.getAnnotation(Dict.class).dicText();
|
String text = field.getAnnotation(Dict.class).dicText();
|
||||||
String table = field.getAnnotation(Dict.class).dictTable();
|
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();
|
String dataSource = field.getAnnotation(Dict.class).ds();
|
||||||
|
//update-end---author:chenrui ---date:20231221 for:[issues/#5643]解决分布式下表字典跨库无法查询问题------------
|
||||||
List<String> dataList;
|
List<String> dataList;
|
||||||
String dictCode = code;
|
String dictCode = code;
|
||||||
if (!StringUtils.isEmpty(table)) {
|
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);
|
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<>());
|
dataList = dataListMap.computeIfAbsent(dictCode, k -> new ArrayList<>());
|
||||||
this.listAddAllDeduplicate(dataList, Arrays.asList(value.split(",")));
|
this.listAddAllDeduplicate(dataList, Arrays.asList(value.split(",")));
|
||||||
}
|
}
|
||||||
//date类型默认转换string格式化日期
|
//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){
|
//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");
|
//SimpleDateFormat aDate=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
|
||||||
// item.put(field.getName(), aDate.format(new Date((Long) item.get(field.getName()))));
|
// 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);
|
items.add(item);
|
||||||
}
|
}
|
||||||
@ -166,12 +176,15 @@ public class DictAspect {
|
|||||||
String code = field.getAnnotation(Dict.class).dicCode();
|
String code = field.getAnnotation(Dict.class).dicCode();
|
||||||
String text = field.getAnnotation(Dict.class).dicText();
|
String text = field.getAnnotation(Dict.class).dicText();
|
||||||
String table = field.getAnnotation(Dict.class).dictTable();
|
String table = field.getAnnotation(Dict.class).dictTable();
|
||||||
|
//update-begin---author:chenrui ---date:20231221 for:[issues/#5643]解决分布式下表字典跨库无法查询问题------------
|
||||||
// 自定义的字典表数据源
|
// 自定义的字典表数据源
|
||||||
String dataSource = field.getAnnotation(Dict.class).ds();
|
String dataSource = field.getAnnotation(Dict.class).ds();
|
||||||
|
//update-end---author:chenrui ---date:20231221 for:[issues/#5643]解决分布式下表字典跨库无法查询问题------------
|
||||||
String fieldDictCode = code;
|
String fieldDictCode = code;
|
||||||
if (!StringUtils.isEmpty(table)) {
|
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);
|
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());
|
String value = record.getString(field.getName());
|
||||||
@ -273,20 +286,25 @@ public class DictAspect {
|
|||||||
String[] arr = dictCode.split(",");
|
String[] arr = dictCode.split(",");
|
||||||
String table = arr[0], text = arr[1], code = arr[2];
|
String table = arr[0], text = arr[1], code = arr[2];
|
||||||
String values = String.join(",", needTranslDataTable);
|
String values = String.join(",", needTranslDataTable);
|
||||||
|
//update-begin---author:chenrui ---date:20231221 for:[issues/#5643]解决分布式下表字典跨库无法查询问题------------
|
||||||
// 自定义的数据源
|
// 自定义的数据源
|
||||||
String dataSource = null;
|
String dataSource = null;
|
||||||
if (arr.length > 3) {
|
if (arr.length > 3) {
|
||||||
dataSource = arr[3];
|
dataSource = arr[3];
|
||||||
}
|
}
|
||||||
|
//update-end---author:chenrui ---date:20231221 for:[issues/#5643]解决分布式下表字典跨库无法查询问题------------
|
||||||
log.debug("translateDictFromTableByKeys.dictCode:" + dictCode);
|
log.debug("translateDictFromTableByKeys.dictCode:" + dictCode);
|
||||||
log.debug("translateDictFromTableByKeys.values:" + values);
|
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){
|
if(null == dataSource){
|
||||||
dataSource = "";
|
dataSource = "";
|
||||||
}
|
}
|
||||||
|
//update-end---author:wangshuai---date:2024-01-09---for:微服务下为空报错没有参数需要传递空字符串---
|
||||||
|
|
||||||
List<DictModel> texts = commonApi.translateDictFromTableByKeys(table, text, code, values, dataSource);
|
List<DictModel> texts = commonApi.translateDictFromTableByKeys(table, text, code, values, dataSource);
|
||||||
|
//update-end---author:chenrui ---date:20231221 for:[issues/#5643]解决分布式下表字典跨库无法查询问题------------
|
||||||
log.debug("translateDictFromTableByKeys.result:" + texts);
|
log.debug("translateDictFromTableByKeys.result:" + texts);
|
||||||
List<DictModel> list = translText.computeIfAbsent(dictCode, k -> new ArrayList<>());
|
List<DictModel> list = translText.computeIfAbsent(dictCode, k -> new ArrayList<>());
|
||||||
list.addAll(texts);
|
list.addAll(texts);
|
||||||
@ -295,8 +313,10 @@ public class DictAspect {
|
|||||||
for (DictModel dict : texts) {
|
for (DictModel dict : texts) {
|
||||||
String redisKey = String.format("sys:cache:dictTable::SimpleKey [%s,%s]", dictCode, dict.getValue());
|
String redisKey = String.format("sys:cache:dictTable::SimpleKey [%s,%s]", dictCode, dict.getValue());
|
||||||
try {
|
try {
|
||||||
|
// update-begin-author:taoyan date:20211012 for: 字典表翻译注解缓存未更新 issues/3061
|
||||||
// 保留5分钟
|
// 保留5分钟
|
||||||
redisTemplate.opsForValue().set(redisKey, dict.getText(), 300, TimeUnit.SECONDS);
|
redisTemplate.opsForValue().set(redisKey, dict.getText(), 300, TimeUnit.SECONDS);
|
||||||
|
// update-end-author:taoyan date:20211012 for: 字典表翻译注解缓存未更新 issues/3061
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.warn(e.getMessage(), e);
|
log.warn(e.getMessage(), e);
|
||||||
}
|
}
|
||||||
@ -380,7 +400,7 @@ public class DictAspect {
|
|||||||
if (k.trim().length() == 0) {
|
if (k.trim().length() == 0) {
|
||||||
continue; //跳过循环
|
continue; //跳过循环
|
||||||
}
|
}
|
||||||
// 代码逻辑说明: !56 优化微服务应用下存在表字段需要字典翻译时加载缓慢问题-----
|
//update-begin--Author:scott -- Date:20210531 ----for: !56 优化微服务应用下存在表字段需要字典翻译时加载缓慢问题-----
|
||||||
if (!StringUtils.isEmpty(table)){
|
if (!StringUtils.isEmpty(table)){
|
||||||
log.debug("--DictAspect------dicTable="+ table+" ,dicText= "+text+" ,dicCode="+code);
|
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());
|
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());
|
tmpValue = commonApi.translateDict(code, k.trim());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
//update-end--Author:scott -- Date:20210531 ----for: !56 优化微服务应用下存在表字段需要字典翻译时加载缓慢问题-----
|
||||||
|
|
||||||
if (tmpValue != null) {
|
if (tmpValue != null) {
|
||||||
if (!"".equals(textValue.toString())) {
|
if (!"".equals(textValue.toString())) {
|
||||||
|
|||||||
@ -21,7 +21,7 @@ import org.springframework.beans.factory.annotation.Autowired;
|
|||||||
import org.springframework.context.annotation.Lazy;
|
import org.springframework.context.annotation.Lazy;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@ -57,6 +57,7 @@ public class PermissionDataAspect {
|
|||||||
String requestMethod = request.getMethod();
|
String requestMethod = request.getMethod();
|
||||||
String requestPath = request.getRequestURI().substring(request.getContextPath().length());
|
String requestPath = request.getRequestURI().substring(request.getContextPath().length());
|
||||||
requestPath = filterUrl(requestPath);
|
requestPath = filterUrl(requestPath);
|
||||||
|
//update-begin-author:taoyan date:20211027 for:JTC-132【online报表权限】online报表带参数的菜单配置数据权限无效
|
||||||
//先判断是否online报表请求
|
//先判断是否online报表请求
|
||||||
if(requestPath.indexOf(UrlMatchEnum.CGREPORT_DATA.getMatchUrl())>=0 || requestPath.indexOf(UrlMatchEnum.CGREPORT_ONLY_DATA.getMatchUrl())>=0){
|
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;
|
requestPath+="?"+urlParamString;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
//update-end-author:taoyan date:20211027 for:JTC-132【online报表权限】online报表带参数的菜单配置数据权限无效
|
||||||
log.debug("拦截请求 >> {} ; 请求类型 >> {} . ", requestPath, requestMethod);
|
log.debug("拦截请求 >> {} ; 请求类型 >> {} . ", requestPath, requestMethod);
|
||||||
String username = JwtUtil.getUserNameByToken(request);
|
String username = JwtUtil.getUserNameByToken(request);
|
||||||
//查询数据权限信息
|
//查询数据权限信息
|
||||||
|
|||||||
@ -41,6 +41,7 @@ public @interface Dict {
|
|||||||
String dictTable() default "";
|
String dictTable() default "";
|
||||||
|
|
||||||
|
|
||||||
|
//update-begin---author:chenrui ---date:20231221 for:[issues/#5643]解决分布式下表字典跨库无法查询问题------------
|
||||||
/**
|
/**
|
||||||
* 方法描述: 数据字典表所在数据源名称
|
* 方法描述: 数据字典表所在数据源名称
|
||||||
* 作 者: chenrui
|
* 作 者: chenrui
|
||||||
@ -49,4 +50,5 @@ public @interface Dict {
|
|||||||
* @return 返回类型: String
|
* @return 返回类型: String
|
||||||
*/
|
*/
|
||||||
String ds() default "";
|
String ds() default "";
|
||||||
|
//update-end---author:chenrui ---date:20231221 for:[issues/#5643]解决分布式下表字典跨库无法查询问题------------
|
||||||
}
|
}
|
||||||
|
|||||||
@ -91,23 +91,6 @@ public interface CommonConstant {
|
|||||||
public static String PREFIX_USER_SHIRO_CACHE = "shiro:cache:org.jeecg.config.shiro.ShiroRealm.authorizationCache:";
|
public static String PREFIX_USER_SHIRO_CACHE = "shiro:cache:org.jeecg.config.shiro.ShiroRealm.authorizationCache:";
|
||||||
/** 登录用户Token令牌缓存KEY前缀 */
|
/** 登录用户Token令牌缓存KEY前缀 */
|
||||||
String PREFIX_USER_TOKEN = "prefix_user_token:";
|
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秒即一小时 */
|
// /** Token缓存时间:3600秒即一小时 */
|
||||||
// int TOKEN_EXPIRE_TIME = 3600;
|
// int TOKEN_EXPIRE_TIME = 3600;
|
||||||
|
|
||||||
@ -325,10 +308,6 @@ public interface CommonConstant {
|
|||||||
*/
|
*/
|
||||||
String SYS_ROLE_ADMIN = "admin";
|
String SYS_ROLE_ADMIN = "admin";
|
||||||
|
|
||||||
/**
|
|
||||||
* 考勤补卡业务状态 (0:处理中)
|
|
||||||
*/
|
|
||||||
String SIGN_PATCH_BIZ_STATUS_0 = "0";
|
|
||||||
/**
|
/**
|
||||||
* 考勤补卡业务状态 (1:同意 2:不同意)
|
* 考勤补卡业务状态 (1:同意 2:不同意)
|
||||||
*/
|
*/
|
||||||
@ -612,6 +591,7 @@ public interface CommonConstant {
|
|||||||
String ORDER_TYPE_DESC = "DESC";
|
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";
|
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的前缀
|
* 修改手机号短信验证码redis-key的前缀
|
||||||
*/
|
*/
|
||||||
@ -651,6 +633,7 @@ public interface CommonConstant {
|
|||||||
* 修改手机号
|
* 修改手机号
|
||||||
*/
|
*/
|
||||||
String UPDATE_PHONE = "updatePhone";
|
String UPDATE_PHONE = "updatePhone";
|
||||||
|
//update-end---author:wangshuai---date:2024-04-07---for:修改手机号常量---
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 修改手机号验证码请求次数超出
|
* 修改手机号验证码请求次数超出
|
||||||
@ -726,19 +709,4 @@ public interface CommonConstant {
|
|||||||
* 部门名称redisKey(全路径)
|
* 部门名称redisKey(全路径)
|
||||||
*/
|
*/
|
||||||
String DEPART_NAME_REDIS_KEY_PRE = "sys:cache:departPathName:";
|
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";
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -39,18 +39,20 @@ public class ProvinceCityArea {
|
|||||||
this.initAreaList();
|
this.initAreaList();
|
||||||
if(areaList!=null && areaList.size()>0){
|
if(areaList!=null && areaList.size()>0){
|
||||||
for(int i=areaList.size()-1;i>=0;i--){
|
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 areaText = areaList.get(i).getText();
|
||||||
String cityText = areaList.get(i).getAheadText();
|
String cityText = areaList.get(i).getAheadText();
|
||||||
if(text.indexOf(areaText)>=0 && (cityText!=null && text.indexOf(cityText)>=0)){
|
if(text.indexOf(areaText)>=0 && (cityText!=null && text.indexOf(cityText)>=0)){
|
||||||
return areaList.get(i).getId();
|
return areaList.get(i).getId();
|
||||||
}
|
}
|
||||||
|
//update-end-author:taoyan date:2022-5-24 for:VUEN-1088 online 导入 省市区导入后 导入数据错乱 北京市/市辖区/西城区-->山西省/晋城市/城区
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// update-begin-author:sunjianlei date:20220121 for:【JTC-704】数据导入错误 省市区组件,文件中为北京市,导入后,导为了山西省
|
||||||
/**
|
/**
|
||||||
* 获取省市区code,精准匹配
|
* 获取省市区code,精准匹配
|
||||||
* @param texts 文本数组,省,市,区
|
* @param texts 文本数组,省,市,区
|
||||||
@ -115,6 +117,7 @@ public class ProvinceCityArea {
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
// update-end-author:sunjianlei date:20220121 for:【JTC-704】数据导入错误 省市区组件,文件中为北京市,导入后,导为了山西省
|
||||||
|
|
||||||
public void getAreaByCode(String code,List<String> ls){
|
public void getAreaByCode(String code,List<String> ls){
|
||||||
for(Area area: areaList){
|
for(Area area: areaList){
|
||||||
@ -151,8 +154,9 @@ public class ProvinceCityArea {
|
|||||||
for(String areaKey:areaJson.keySet()){
|
for(String areaKey:areaJson.keySet()){
|
||||||
//System.out.println("········"+areaKey);
|
//System.out.println("········"+areaKey);
|
||||||
Area area = new Area(areaKey,areaJson.getString(areaKey),cityKey);
|
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));
|
area.setAheadText(cityJson.getString(cityKey));
|
||||||
|
//update-end-author:taoyan date:2022-5-24 for:VUEN-1088 online 导入 省市区导入后 导入数据错乱 北京市/市辖区/西城区-->山西省/晋城市/城区
|
||||||
this.areaList.add(area);
|
this.areaList.add(area);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -47,8 +47,4 @@ public interface TenantConstant {
|
|||||||
*/
|
*/
|
||||||
String APP_ADMIN = "appAdmin";
|
String APP_ADMIN = "appAdmin";
|
||||||
|
|
||||||
/**
|
|
||||||
* 增加SignatureCheck注解POST请求的URL
|
|
||||||
*/
|
|
||||||
String[] SIGNATURE_CHECK_POST_URL = { "/sys/tenant/joinTenantByHouseNumber", "/sys/tenant/invitationUser" };
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -23,11 +23,7 @@ public enum NoticeTypeEnum {
|
|||||||
/**
|
/**
|
||||||
* 督办
|
* 督办
|
||||||
*/
|
*/
|
||||||
NOTICE_TYPE_SUPERVISE("督办管理", "supe"),
|
NOTICE_TYPE_SUPERVISE("督办管理", "supe");
|
||||||
/**
|
|
||||||
* 考勤
|
|
||||||
*/
|
|
||||||
NOTICE_TYPE_ATTENDANCE("考勤消息", "attendance");
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 文件类型名称
|
* 文件类型名称
|
||||||
|
|||||||
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -73,7 +73,7 @@ public class SensitiveDataAspect {
|
|||||||
SensitiveInfoUtil.handleNestedObject(result, entity, isEncode);
|
SensitiveInfoUtil.handleNestedObject(result, entity, isEncode);
|
||||||
}
|
}
|
||||||
long endTime=System.currentTimeMillis();
|
long endTime=System.currentTimeMillis();
|
||||||
log.debug((isEncode ? "加密操作," : "解密操作,") + "Aspect程序耗时:" + (endTime - startTime) + "ms");
|
log.info((isEncode ? "加密操作," : "解密操作,") + "Aspect程序耗时:" + (endTime - startTime) + "ms");
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -387,7 +387,7 @@ public class JeecgElasticsearchTemplate {
|
|||||||
data.remove("id");
|
data.remove("id");
|
||||||
bodySb.append(data.toJSONString()).append("\n");
|
bodySb.append(data.toJSONString()).append("\n");
|
||||||
}
|
}
|
||||||
//System.out.println("+-+-+-: bodySb.toString(): " + bodySb.toString());
|
System.out.println("+-+-+-: bodySb.toString(): " + bodySb.toString());
|
||||||
HttpHeaders headers = RestUtil.getHeaderApplicationJson();
|
HttpHeaders headers = RestUtil.getHeaderApplicationJson();
|
||||||
RestUtil.request(url, HttpMethod.PUT, headers, null, bodySb, JSONObject.class);
|
RestUtil.request(url, HttpMethod.PUT, headers, null, bodySb, JSONObject.class);
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@ -1,8 +1,6 @@
|
|||||||
package org.jeecg.common.exception;
|
package org.jeecg.common.exception;
|
||||||
|
|
||||||
import cn.hutool.core.util.ObjectUtil;
|
import cn.hutool.core.util.ObjectUtil;
|
||||||
import jakarta.annotation.Resource;
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.commons.lang3.exception.ExceptionUtils;
|
import org.apache.commons.lang3.exception.ExceptionUtils;
|
||||||
import org.apache.shiro.SecurityUtils;
|
import org.apache.shiro.SecurityUtils;
|
||||||
@ -35,6 +33,8 @@ import org.springframework.web.multipart.MaxUploadSizeExceededException;
|
|||||||
import org.springframework.web.multipart.MultipartException;
|
import org.springframework.web.multipart.MultipartException;
|
||||||
import org.springframework.web.servlet.NoHandlerFoundException;
|
import org.springframework.web.servlet.NoHandlerFoundException;
|
||||||
|
|
||||||
|
import javax.annotation.Resource;
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@ -121,12 +121,13 @@ public class JeecgBootExceptionHandler {
|
|||||||
@ExceptionHandler(Exception.class)
|
@ExceptionHandler(Exception.class)
|
||||||
public Result<?> handleException(Exception e){
|
public Result<?> handleException(Exception e){
|
||||||
log.error(e.getMessage(), e);
|
log.error(e.getMessage(), e);
|
||||||
// 代码逻辑说明: 处理Sentinel限流自定义异常
|
//update-begin---author:zyf ---date:20220411 for:处理Sentinel限流自定义异常
|
||||||
Throwable throwable = e.getCause();
|
Throwable throwable = e.getCause();
|
||||||
SentinelErrorInfoEnum errorInfoEnum = SentinelErrorInfoEnum.getErrorByException(throwable);
|
SentinelErrorInfoEnum errorInfoEnum = SentinelErrorInfoEnum.getErrorByException(throwable);
|
||||||
if (ObjectUtil.isNotEmpty(errorInfoEnum)) {
|
if (ObjectUtil.isNotEmpty(errorInfoEnum)) {
|
||||||
return Result.error(errorInfoEnum.getError());
|
return Result.error(errorInfoEnum.getError());
|
||||||
}
|
}
|
||||||
|
//update-end---author:zyf ---date:20220411 for:处理Sentinel限流自定义异常
|
||||||
addSysLog(e);
|
addSysLog(e);
|
||||||
return Result.error("操作失败,"+e.getMessage());
|
return Result.error("操作失败,"+e.getMessage());
|
||||||
}
|
}
|
||||||
@ -223,6 +224,7 @@ public class JeecgBootExceptionHandler {
|
|||||||
return Result.error("校验失败,存在SQL注入风险!" + msg);
|
return Result.error("校验失败,存在SQL注入风险!" + msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//update-begin---author:chenrui ---date:20240423 for:[QQYUN-8732]把错误的日志都抓取了 方便后续处理,单独弄个日志类型------------
|
||||||
/**
|
/**
|
||||||
* 添加异常新系统日志
|
* 添加异常新系统日志
|
||||||
* @param e 异常
|
* @param e 异常
|
||||||
@ -241,6 +243,7 @@ public class JeecgBootExceptionHandler {
|
|||||||
} catch (NullPointerException | BeansException ignored) {
|
} catch (NullPointerException | BeansException ignored) {
|
||||||
}
|
}
|
||||||
if (null != request) {
|
if (null != request) {
|
||||||
|
//update-begin---author:chenrui ---date:20250408 for:[QQYUN-11716]上传大图片失败没有精确提示------------
|
||||||
//请求的参数
|
//请求的参数
|
||||||
if (!isTooBigException(e)) {
|
if (!isTooBigException(e)) {
|
||||||
// 文件上传过大异常时不能获取参数,否则会报错
|
// 文件上传过大异常时不能获取参数,否则会报错
|
||||||
@ -249,6 +252,7 @@ public class JeecgBootExceptionHandler {
|
|||||||
log.setMethod(oConvertUtils.mapToString(request.getParameterMap()));
|
log.setMethod(oConvertUtils.mapToString(request.getParameterMap()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
//update-end---author:chenrui ---date:20250408 for:[QQYUN-11716]上传大图片失败没有精确提示------------
|
||||||
// 请求地址
|
// 请求地址
|
||||||
log.setRequestUrl(request.getRequestURI());
|
log.setRequestUrl(request.getRequestURI());
|
||||||
//设置IP地址
|
//设置IP地址
|
||||||
@ -272,6 +276,7 @@ public class JeecgBootExceptionHandler {
|
|||||||
|
|
||||||
baseCommonService.addLog(log);
|
baseCommonService.addLog(log);
|
||||||
}
|
}
|
||||||
|
//update-end---author:chenrui ---date:20240423 for:[QQYUN-8732]把错误的日志都抓取了 方便后续处理,单独弄个日志类型------------
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 是否文件过大异常
|
* 是否文件过大异常
|
||||||
|
|||||||
@ -24,9 +24,9 @@ import org.springframework.web.multipart.MultipartFile;
|
|||||||
import org.springframework.web.multipart.MultipartHttpServletRequest;
|
import org.springframework.web.multipart.MultipartHttpServletRequest;
|
||||||
import org.springframework.web.servlet.ModelAndView;
|
import org.springframework.web.servlet.ModelAndView;
|
||||||
|
|
||||||
import jakarta.annotation.Resource;
|
import javax.annotation.Resource;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import javax.servlet.http.HttpServletResponse;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|
||||||
@ -68,16 +68,12 @@ public class JeecgController<T, S extends IService<T>> {
|
|||||||
//此处设置的filename无效 ,前端会重更新设置一下
|
//此处设置的filename无效 ,前端会重更新设置一下
|
||||||
mv.addObject(NormalExcelConstants.FILE_NAME, title);
|
mv.addObject(NormalExcelConstants.FILE_NAME, title);
|
||||||
mv.addObject(NormalExcelConstants.CLASS, clazz);
|
mv.addObject(NormalExcelConstants.CLASS, clazz);
|
||||||
// 代码逻辑说明: 【QQYUN-13930】统一改成导出xlsx格式---
|
//update-begin--Author:liusq Date:20210126 for:图片导出报错,ImageBasePath未设置--------------------
|
||||||
ExportParams exportParams=new ExportParams(title + "报表", "导出人:" + sysUser.getRealname(), title, ExcelType.XSSF);
|
ExportParams exportParams=new ExportParams(title + "报表", "导出人:" + sysUser.getRealname(), title);
|
||||||
exportParams.setImageBasePath(jeecgBaseConfig.getPath().getUpload());
|
exportParams.setImageBasePath(jeecgBaseConfig.getPath().getUpload());
|
||||||
|
//update-end--Author:liusq Date:20210126 for:图片导出报错,ImageBasePath未设置----------------------
|
||||||
mv.addObject(NormalExcelConstants.PARAMS,exportParams);
|
mv.addObject(NormalExcelConstants.PARAMS,exportParams);
|
||||||
mv.addObject(NormalExcelConstants.DATA_LIST, exportList);
|
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;
|
return mv;
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
@ -98,12 +94,14 @@ public class JeecgController<T, S extends IService<T>> {
|
|||||||
// Step.2 计算分页sheet数据
|
// Step.2 计算分页sheet数据
|
||||||
double total = service.count();
|
double total = service.count();
|
||||||
int count = (int)Math.ceil(total/pageNum);
|
int count = (int)Math.ceil(total/pageNum);
|
||||||
|
//update-begin-author:liusq---date:20220629--for: 多sheet导出根据选择导出写法调整 ---
|
||||||
// Step.3 过滤选中数据
|
// Step.3 过滤选中数据
|
||||||
String selections = request.getParameter("selections");
|
String selections = request.getParameter("selections");
|
||||||
if (oConvertUtils.isNotEmpty(selections)) {
|
if (oConvertUtils.isNotEmpty(selections)) {
|
||||||
List<String> selectionList = Arrays.asList(selections.split(","));
|
List<String> selectionList = Arrays.asList(selections.split(","));
|
||||||
queryWrapper.in("id",selectionList);
|
queryWrapper.in("id",selectionList);
|
||||||
}
|
}
|
||||||
|
//update-end-author:liusq---date:20220629--for: 多sheet导出根据选择导出写法调整 ---
|
||||||
// Step.4 多sheet处理
|
// Step.4 多sheet处理
|
||||||
List<Map<String, Object>> listMap = new ArrayList<Map<String, Object>>();
|
List<Map<String, Object>> listMap = new ArrayList<Map<String, Object>>();
|
||||||
for (int i = 1; i <=count ; i++) {
|
for (int i = 1; i <=count ; i++) {
|
||||||
@ -222,15 +220,16 @@ public class JeecgController<T, S extends IService<T>> {
|
|||||||
params.setNeedSave(true);
|
params.setNeedSave(true);
|
||||||
try {
|
try {
|
||||||
List<T> list = ExcelImportUtil.importExcel(file.getInputStream(), clazz, params);
|
List<T> list = ExcelImportUtil.importExcel(file.getInputStream(), clazz, params);
|
||||||
// 代码逻辑说明: 批量插入数据
|
//update-begin-author:taoyan date:20190528 for:批量插入数据
|
||||||
long start = System.currentTimeMillis();
|
long start = System.currentTimeMillis();
|
||||||
service.saveBatch(list);
|
service.saveBatch(list);
|
||||||
//400条 saveBatch消耗时间1592毫秒 循环插入消耗时间1947毫秒
|
//400条 saveBatch消耗时间1592毫秒 循环插入消耗时间1947毫秒
|
||||||
//1200条 saveBatch消耗时间3687毫秒 循环插入消耗时间5212毫秒
|
//1200条 saveBatch消耗时间3687毫秒 循环插入消耗时间5212毫秒
|
||||||
log.info("消耗时间" + (System.currentTimeMillis() - start) + "毫秒");
|
log.info("消耗时间" + (System.currentTimeMillis() - start) + "毫秒");
|
||||||
|
//update-end-author:taoyan date:20190528 for:批量插入数据
|
||||||
return Result.ok("文件导入成功!数据行数:" + list.size());
|
return Result.ok("文件导入成功!数据行数:" + list.size());
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
// 代码逻辑说明: 导入数据重复增加提示
|
//update-begin-author:taoyan date:20211124 for: 导入数据重复增加提示
|
||||||
String msg = e.getMessage();
|
String msg = e.getMessage();
|
||||||
log.error(msg, e);
|
log.error(msg, e);
|
||||||
if(msg!=null && msg.indexOf("Duplicate entry")>=0){
|
if(msg!=null && msg.indexOf("Duplicate entry")>=0){
|
||||||
@ -238,6 +237,7 @@ public class JeecgController<T, S extends IService<T>> {
|
|||||||
}else{
|
}else{
|
||||||
return Result.error("文件导入失败:" + e.getMessage());
|
return Result.error("文件导入失败:" + e.getMessage());
|
||||||
}
|
}
|
||||||
|
//update-end-author:taoyan date:20211124 for: 导入数据重复增加提示
|
||||||
} finally {
|
} finally {
|
||||||
try {
|
try {
|
||||||
file.getInputStream().close();
|
file.getInputStream().close();
|
||||||
|
|||||||
@ -97,6 +97,7 @@ public class QueryGenerator {
|
|||||||
return queryWrapper;
|
return queryWrapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//update-begin---author:chenrui ---date:20240527 for:[TV360X-378]增加自定义字段查询规则功能------------
|
||||||
/**
|
/**
|
||||||
* 获取查询条件构造器QueryWrapper实例 通用查询条件已被封装完成
|
* 获取查询条件构造器QueryWrapper实例 通用查询条件已被封装完成
|
||||||
* @param searchObj 查询实体
|
* @param searchObj 查询实体
|
||||||
@ -111,6 +112,7 @@ public class QueryGenerator {
|
|||||||
log.debug("---查询条件构造器初始化完成,耗时:"+(System.currentTimeMillis()-start)+"毫秒----");
|
log.debug("---查询条件构造器初始化完成,耗时:"+(System.currentTimeMillis()-start)+"毫秒----");
|
||||||
return queryWrapper;
|
return queryWrapper;
|
||||||
}
|
}
|
||||||
|
//update-end---author:chenrui ---date:20240527 for:[TV360X-378]增加自定义字段查询规则功能------------
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 组装Mybatis Plus 查询条件
|
* 组装Mybatis Plus 查询条件
|
||||||
@ -140,6 +142,7 @@ public class QueryGenerator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
String name, type, column;
|
String name, type, column;
|
||||||
|
// update-begin--Author:taoyan Date:20200923 for:issues/1671 如果字段加注解了@TableField(exist = false),不走DB查询-------
|
||||||
//定义实体字段和数据库字段名称的映射 高级查询中 只能获取实体字段 如果设置TableField注解 那么查询条件会出问题
|
//定义实体字段和数据库字段名称的映射 高级查询中 只能获取实体字段 如果设置TableField注解 那么查询条件会出问题
|
||||||
Map<String,String> fieldColumnMap = new HashMap<>(5);
|
Map<String,String> fieldColumnMap = new HashMap<>(5);
|
||||||
for (int i = 0; i < origDescriptors.length; i++) {
|
for (int i = 0; i < origDescriptors.length; i++) {
|
||||||
@ -185,7 +188,7 @@ public class QueryGenerator {
|
|||||||
queryWrapper.and(j -> j.like(field,vals[0]));
|
queryWrapper.and(j -> j.like(field,vals[0]));
|
||||||
}
|
}
|
||||||
}else {
|
}else {
|
||||||
// 代码逻辑说明: [TV360X-378]增加自定义字段查询规则功能------------
|
//update-begin---author:chenrui ---date:20240527 for:[TV360X-378]增加自定义字段查询规则功能------------
|
||||||
QueryRuleEnum rule;
|
QueryRuleEnum rule;
|
||||||
if(null != customRuleMap && customRuleMap.containsKey(name)) {
|
if(null != customRuleMap && customRuleMap.containsKey(name)) {
|
||||||
// 有自定义规则,使用自定义规则.
|
// 有自定义规则,使用自定义规则.
|
||||||
@ -194,6 +197,7 @@ public class QueryGenerator {
|
|||||||
//根据参数值带什么关键字符串判断走什么类型的查询
|
//根据参数值带什么关键字符串判断走什么类型的查询
|
||||||
rule = convert2Rule(value);
|
rule = convert2Rule(value);
|
||||||
}
|
}
|
||||||
|
//update-end---author:chenrui ---date:20240527 for:[TV360X-378]增加自定义字段查询规则功能------------
|
||||||
value = replaceValue(rule,value);
|
value = replaceValue(rule,value);
|
||||||
// add -begin 添加判断为字符串时设为全模糊查询
|
// add -begin 添加判断为字符串时设为全模糊查询
|
||||||
//if( (rule==null || QueryRuleEnum.EQ.equals(rule)) && "class java.lang.String".equals(type)) {
|
//if( (rule==null || QueryRuleEnum.EQ.equals(rule)) && "class java.lang.String".equals(type)) {
|
||||||
@ -213,6 +217,7 @@ public class QueryGenerator {
|
|||||||
|
|
||||||
//高级查询
|
//高级查询
|
||||||
doSuperQuery(queryWrapper, parameterMap, fieldColumnMap);
|
doSuperQuery(queryWrapper, parameterMap, fieldColumnMap);
|
||||||
|
// update-end--Author:taoyan Date:20200923 for:issues/1671 如果字段加注解了@TableField(exist = false),不走DB查询-------
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -255,16 +260,16 @@ public class QueryGenerator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if(oConvertUtils.isNotEmpty(column)){
|
if(oConvertUtils.isNotEmpty(column)){
|
||||||
log.debug("单字段排序规则>> column:" + column + ",排序方式:" + order);
|
log.info("单字段排序规则>> column:" + column + ",排序方式:" + order);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1. 列表多字段排序优先
|
// 1. 列表多字段排序优先
|
||||||
if(parameterMap!=null&& parameterMap.containsKey("sortInfoString")) {
|
if(parameterMap!=null&& parameterMap.containsKey("sortInfoString")) {
|
||||||
// 多字段排序
|
// 多字段排序
|
||||||
String sortInfoString = parameterMap.get("sortInfoString")[0];
|
String sortInfoString = parameterMap.get("sortInfoString")[0];
|
||||||
log.debug("多字段排序规则>> sortInfoString:" + sortInfoString);
|
log.info("多字段排序规则>> sortInfoString:" + sortInfoString);
|
||||||
List<OrderItem> orderItemList = SqlConcatUtil.getQueryConditionOrders(column, order, sortInfoString);
|
List<OrderItem> orderItemList = SqlConcatUtil.getQueryConditionOrders(column, order, sortInfoString);
|
||||||
log.debug(orderItemList.toString());
|
log.info(orderItemList.toString());
|
||||||
if (orderItemList != null && !orderItemList.isEmpty()) {
|
if (orderItemList != null && !orderItemList.isEmpty()) {
|
||||||
for (OrderItem item : orderItemList) {
|
for (OrderItem item : orderItemList) {
|
||||||
// 一、获取排序数据库字段
|
// 一、获取排序数据库字段
|
||||||
@ -316,11 +321,13 @@ public class QueryGenerator {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//update-begin-author:scott date:2022-11-07 for:避免用户自定义表无默认字段{创建时间},导致排序报错
|
||||||
//TODO 避免用户自定义表无默认字段创建时间,导致排序报错
|
//TODO 避免用户自定义表无默认字段创建时间,导致排序报错
|
||||||
if(DataBaseConstant.CREATE_TIME.equals(column) && !fieldColumnMap.containsKey(DataBaseConstant.CREATE_TIME)){
|
if(DataBaseConstant.CREATE_TIME.equals(column) && !fieldColumnMap.containsKey(DataBaseConstant.CREATE_TIME)){
|
||||||
column = "id";
|
column = "id";
|
||||||
log.warn("检测到实体里没有字段createTime,改成采用ID排序!");
|
log.warn("检测到实体里没有字段createTime,改成采用ID排序!");
|
||||||
}
|
}
|
||||||
|
//update-end-author:scott date:2022-11-07 for:避免用户自定义表无默认字段{创建时间},导致排序报错
|
||||||
|
|
||||||
if (oConvertUtils.isNotEmpty(column) && oConvertUtils.isNotEmpty(order)) {
|
if (oConvertUtils.isNotEmpty(column) && oConvertUtils.isNotEmpty(order)) {
|
||||||
//字典字段,去掉字典翻译文本后缀
|
//字典字段,去掉字典翻译文本后缀
|
||||||
@ -328,12 +335,15 @@ public class QueryGenerator {
|
|||||||
column = column.substring(0, column.lastIndexOf(CommonConstant.DICT_TEXT_SUFFIX));
|
column = column.substring(0, column.lastIndexOf(CommonConstant.DICT_TEXT_SUFFIX));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//update-begin-author:taoyan date:2022-5-16 for: issues/3676 获取系统用户列表时,使用SQL注入生效
|
||||||
//判断column是不是当前实体的
|
//判断column是不是当前实体的
|
||||||
log.debug("当前字段有:"+ allFields);
|
log.debug("当前字段有:"+ allFields);
|
||||||
if (!allColumnExist(column, allFields)) {
|
if (!allColumnExist(column, allFields)) {
|
||||||
throw new JeecgBootException("请注意,将要排序的列字段不存在:" + column);
|
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 的值
|
//多字段排序方法没有读取 MybatisPlus 注解 @TableField 里 value 的值
|
||||||
if (column.contains(",")) {
|
if (column.contains(",")) {
|
||||||
List<String> columnList = Arrays.asList(column.split(","));
|
List<String> columnList = Arrays.asList(column.split(","));
|
||||||
@ -344,10 +354,12 @@ public class QueryGenerator {
|
|||||||
}else{
|
}else{
|
||||||
column = fieldColumnMap.get(column);
|
column = fieldColumnMap.get(column);
|
||||||
}
|
}
|
||||||
|
//update-end-author:scott date:2022-10-10 for:【jeecg-boot/issues/I5FJU6】doMultiFieldsOrder() 多字段排序方法存在问题
|
||||||
|
|
||||||
//SQL注入check
|
//SQL注入check
|
||||||
SqlInjectionUtil.filterContentMulti(column);
|
SqlInjectionUtil.filterContentMulti(column);
|
||||||
|
|
||||||
|
//update-begin--Author:scott Date:20210531 for:36 多条件排序无效问题修正-------
|
||||||
// 排序规则修改
|
// 排序规则修改
|
||||||
// 将现有排序 _ 前端传递排序条件{....,column: 'column1,column2',order: 'desc'} 翻译成sql "column1,column2 desc"
|
// 将现有排序 _ 前端传递排序条件{....,column: 'column1,column2',order: 'desc'} 翻译成sql "column1,column2 desc"
|
||||||
// 修改为 _ 前端传递排序条件{....,column: 'column1,column2',order: 'desc'} 翻译成sql "column1 desc,column2 desc"
|
// 修改为 _ 前端传递排序条件{....,column: 'column1,column2',order: 'desc'} 翻译成sql "column1 desc,column2 desc"
|
||||||
@ -356,9 +368,11 @@ public class QueryGenerator {
|
|||||||
} else {
|
} else {
|
||||||
queryWrapper.orderByDesc(SqlInjectionUtil.getSqlInjectSortFields(column.split(",")));
|
queryWrapper.orderByDesc(SqlInjectionUtil.getSqlInjectSortFields(column.split(",")));
|
||||||
}
|
}
|
||||||
|
//update-end--Author:scott Date:20210531 for:36 多条件排序无效问题修正-------
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//update-begin-author:taoyan date:2022-5-23 for: issues/3676 获取系统用户列表时,使用SQL注入生效
|
||||||
/**
|
/**
|
||||||
* 多字段排序 判断所传字段是否存在
|
* 多字段排序 判断所传字段是否存在
|
||||||
* @return
|
* @return
|
||||||
@ -378,6 +392,7 @@ public class QueryGenerator {
|
|||||||
}
|
}
|
||||||
return exist;
|
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 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();
|
String superQueryMatchType = parameterMap.get(SUPER_QUERY_MATCH_TYPE) != null ? parameterMap.get(SUPER_QUERY_MATCH_TYPE)[0] : MatchTypeEnum.AND.getValue();
|
||||||
MatchTypeEnum matchType = MatchTypeEnum.getByValue(superQueryMatchType);
|
MatchTypeEnum matchType = MatchTypeEnum.getByValue(superQueryMatchType);
|
||||||
// 代码逻辑说明: 高级查询的条件要用括号括起来,防止和用户的其他条件冲突 -------
|
// update-begin--Author:sunjianlei Date:20200325 for:高级查询的条件要用括号括起来,防止和用户的其他条件冲突 -------
|
||||||
try {
|
try {
|
||||||
superQueryParams = URLDecoder.decode(superQueryParams, "UTF-8");
|
superQueryParams = URLDecoder.decode(superQueryParams, "UTF-8");
|
||||||
List<QueryCondition> conditions = JSON.parseArray(superQueryParams, QueryCondition.class);
|
List<QueryCondition> conditions = JSON.parseArray(superQueryParams, QueryCondition.class);
|
||||||
if (conditions == null || conditions.size() == 0) {
|
if (conditions == null || conditions.size() == 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// 代码逻辑说明: 【JTC-573】 过滤空条件查询,防止 sql 拼接多余的 and
|
// update-begin-author:sunjianlei date:20220119 for: 【JTC-573】 过滤空条件查询,防止 sql 拼接多余的 and
|
||||||
List<QueryCondition> filterConditions = conditions.stream().filter(
|
List<QueryCondition> filterConditions = conditions.stream().filter(
|
||||||
rule -> (oConvertUtils.isNotEmpty(rule.getField())
|
rule -> (oConvertUtils.isNotEmpty(rule.getField())
|
||||||
&& oConvertUtils.isNotEmpty(rule.getRule())
|
&& oConvertUtils.isNotEmpty(rule.getRule())
|
||||||
@ -408,6 +423,7 @@ public class QueryGenerator {
|
|||||||
if (filterConditions.size() == 0) {
|
if (filterConditions.size() == 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// update-end-author:sunjianlei date:20220119 for: 【JTC-573】 过滤空条件查询,防止 sql 拼接多余的 and
|
||||||
log.debug("---高级查询参数-->" + filterConditions);
|
log.debug("---高级查询参数-->" + filterConditions);
|
||||||
|
|
||||||
queryWrapper.and(andWrapper -> {
|
queryWrapper.and(andWrapper -> {
|
||||||
@ -422,14 +438,14 @@ public class QueryGenerator {
|
|||||||
|
|
||||||
log.debug("SuperQuery ==> " + rule.toString());
|
log.debug("SuperQuery ==> " + rule.toString());
|
||||||
|
|
||||||
// 代码逻辑说明: 【高级查询】 oracle 日期等于查询报错
|
//update-begin-author:taoyan date:20201228 for: 【高级查询】 oracle 日期等于查询报错
|
||||||
Object queryValue = rule.getVal();
|
Object queryValue = rule.getVal();
|
||||||
if("date".equals(rule.getType())){
|
if("date".equals(rule.getType())){
|
||||||
queryValue = DateUtils.str2Date(rule.getVal(),DateUtils.date_sdf.get());
|
queryValue = DateUtils.str2Date(rule.getVal(),DateUtils.date_sdf.get());
|
||||||
}else if("datetime".equals(rule.getType())){
|
}else if("datetime".equals(rule.getType())){
|
||||||
queryValue = DateUtils.str2Date(rule.getVal(), DateUtils.datetimeFormat.get());
|
queryValue = DateUtils.str2Date(rule.getVal(), DateUtils.datetimeFormat.get());
|
||||||
}
|
}
|
||||||
// 代码逻辑说明: 【/issues/I3VR8E】高级查询没有类型转换,查询参数都是字符串类型 ----
|
// update-begin--author:sunjianlei date:20210702 for:【/issues/I3VR8E】高级查询没有类型转换,查询参数都是字符串类型 ----
|
||||||
String dbType = rule.getDbType();
|
String dbType = rule.getDbType();
|
||||||
if (oConvertUtils.isNotEmpty(dbType)) {
|
if (oConvertUtils.isNotEmpty(dbType)) {
|
||||||
try {
|
try {
|
||||||
@ -462,8 +478,9 @@ public class QueryGenerator {
|
|||||||
log.error("高级查询值转换失败:", e);
|
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);
|
addEasyQuery(andWrapper, fieldColumnMap.get(rule.getField()), QueryRuleEnum.getByValue(rule.getRule()), queryValue);
|
||||||
|
//update-end-author:taoyan date:20201228 for: 【高级查询】 oracle 日期等于查询报错
|
||||||
|
|
||||||
// 如果拼接方式是OR,就拼接OR
|
// 如果拼接方式是OR,就拼接OR
|
||||||
if (MatchTypeEnum.OR == matchType && i < (filterConditions.size() - 1)) {
|
if (MatchTypeEnum.OR == matchType && i < (filterConditions.size() - 1)) {
|
||||||
@ -479,6 +496,7 @@ public class QueryGenerator {
|
|||||||
log.error("--高级查询拼接失败:" + e.getMessage());
|
log.error("--高级查询拼接失败:" + e.getMessage());
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
|
// update-end--Author:sunjianlei Date:20200325 for:高级查询的条件要用括号括起来,防止和用户的其他条件冲突 -------
|
||||||
}
|
}
|
||||||
//log.info(" superQuery getCustomSqlSegment: "+ queryWrapper.getCustomSqlSegment());
|
//log.info(" superQuery getCustomSqlSegment: "+ queryWrapper.getCustomSqlSegment());
|
||||||
}
|
}
|
||||||
@ -490,7 +508,7 @@ public class QueryGenerator {
|
|||||||
*/
|
*/
|
||||||
public static QueryRuleEnum convert2Rule(Object value) {
|
public static QueryRuleEnum convert2Rule(Object value) {
|
||||||
// 避免空数据
|
// 避免空数据
|
||||||
// 代码逻辑说明: 查询条件输入空格导致return null后续判断导致抛出null异常
|
// update-begin-author:taoyan date:20210629 for: 查询条件输入空格导致return null后续判断导致抛出null异常
|
||||||
if (value == null) {
|
if (value == null) {
|
||||||
return QueryRuleEnum.EQ;
|
return QueryRuleEnum.EQ;
|
||||||
}
|
}
|
||||||
@ -498,8 +516,10 @@ public class QueryGenerator {
|
|||||||
if (val.length() == 0) {
|
if (val.length() == 0) {
|
||||||
return QueryRuleEnum.EQ;
|
return QueryRuleEnum.EQ;
|
||||||
}
|
}
|
||||||
|
// update-end-author:taoyan date:20210629 for: 查询条件输入空格导致return null后续判断导致抛出null异常
|
||||||
QueryRuleEnum rule =null;
|
QueryRuleEnum rule =null;
|
||||||
|
|
||||||
|
//update-begin--Author:scott Date:20190724 for:initQueryWrapper组装sql查询条件错误 #284-------------------
|
||||||
//TODO 此处规则,只适用于 le lt ge gt
|
//TODO 此处规则,只适用于 le lt ge gt
|
||||||
// step 2 .>= =<
|
// step 2 .>= =<
|
||||||
int length2 = 2;
|
int length2 = 2;
|
||||||
@ -515,12 +535,14 @@ public class QueryGenerator {
|
|||||||
rule = QueryRuleEnum.getByValue(val.substring(0, 1));
|
rule = QueryRuleEnum.getByValue(val.substring(0, 1));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
//update-end--Author:scott Date:20190724 for:initQueryWrapper组装sql查询条件错误 #284---------------------
|
||||||
|
|
||||||
// step 3 like
|
// step 3 like
|
||||||
// 代码逻辑说明: /issues/3382 默认带*就走模糊,但是如果只有一个*,那么走等于查询
|
//update-begin-author:taoyan for: /issues/3382 默认带*就走模糊,但是如果只有一个*,那么走等于查询
|
||||||
if(rule == null && val.equals(STAR)){
|
if(rule == null && val.equals(STAR)){
|
||||||
rule = QueryRuleEnum.EQ;
|
rule = QueryRuleEnum.EQ;
|
||||||
}
|
}
|
||||||
|
//update-end-author:taoyan for: /issues/3382 默认带*就走模糊,但是如果只有一个*,那么走等于查询
|
||||||
if (rule == null && val.contains(STAR)) {
|
if (rule == null && val.contains(STAR)) {
|
||||||
if (val.startsWith(STAR) && val.endsWith(STAR)) {
|
if (val.startsWith(STAR) && val.endsWith(STAR)) {
|
||||||
rule = QueryRuleEnum.LIKE;
|
rule = QueryRuleEnum.LIKE;
|
||||||
@ -545,10 +567,12 @@ public class QueryGenerator {
|
|||||||
rule = QueryRuleEnum.EQ_WITH_ADD;
|
rule = QueryRuleEnum.EQ_WITH_ADD;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//update-begin--Author:taoyan Date:20201229 for:initQueryWrapper组装sql查询条件错误 #284---------------------
|
||||||
//特殊处理:Oracle的表达式to_date('xxx','yyyy-MM-dd')含有逗号,会被识别为in查询,转为等于查询
|
//特殊处理:Oracle的表达式to_date('xxx','yyyy-MM-dd')含有逗号,会被识别为in查询,转为等于查询
|
||||||
if(rule == QueryRuleEnum.IN && val.indexOf(YYYY_MM_DD)>=0 && val.indexOf(TO_DATE)>=0){
|
if(rule == QueryRuleEnum.IN && val.indexOf(YYYY_MM_DD)>=0 && val.indexOf(TO_DATE)>=0){
|
||||||
rule = QueryRuleEnum.EQ;
|
rule = QueryRuleEnum.EQ;
|
||||||
}
|
}
|
||||||
|
//update-end--Author:taoyan Date:20201229 for:initQueryWrapper组装sql查询条件错误 #284---------------------
|
||||||
|
|
||||||
return rule != null ? rule : QueryRuleEnum.EQ;
|
return rule != null ? rule : QueryRuleEnum.EQ;
|
||||||
}
|
}
|
||||||
@ -568,10 +592,11 @@ public class QueryGenerator {
|
|||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
String val = (value + "").toString().trim();
|
String val = (value + "").toString().trim();
|
||||||
// 代码逻辑说明: 查询条件的值为等号(=)bug #3443
|
//update-begin-author:taoyan date:20220302 for: 查询条件的值为等号(=)bug #3443
|
||||||
if(QueryRuleEnum.EQ.getValue().equals(val)){
|
if(QueryRuleEnum.EQ.getValue().equals(val)){
|
||||||
return val;
|
return val;
|
||||||
}
|
}
|
||||||
|
//update-end-author:taoyan date:20220302 for: 查询条件的值为等号(=)bug #3443
|
||||||
if (rule == QueryRuleEnum.LIKE) {
|
if (rule == QueryRuleEnum.LIKE) {
|
||||||
value = val.substring(1, val.length() - 1);
|
value = val.substring(1, val.length() - 1);
|
||||||
//mysql 模糊查询之特殊字符下划线 (_、\)
|
//mysql 模糊查询之特殊字符下划线 (_、\)
|
||||||
@ -589,19 +614,21 @@ public class QueryGenerator {
|
|||||||
} else if (rule == QueryRuleEnum.EQ_WITH_ADD) {
|
} else if (rule == QueryRuleEnum.EQ_WITH_ADD) {
|
||||||
value = val.replaceAll("\\+\\+", COMMA);
|
value = val.replaceAll("\\+\\+", COMMA);
|
||||||
}else {
|
}else {
|
||||||
// 代码逻辑说明: initQueryWrapper组装sql查询条件错误 #284-------------------
|
//update-begin--Author:scott Date:20190724 for:initQueryWrapper组装sql查询条件错误 #284-------------------
|
||||||
if(val.startsWith(rule.getValue())){
|
if(val.startsWith(rule.getValue())){
|
||||||
//TODO 此处逻辑应该注释掉-> 如果查询内容中带有查询匹配规则符号,就会被截取的(比如:>=您好)
|
//TODO 此处逻辑应该注释掉-> 如果查询内容中带有查询匹配规则符号,就会被截取的(比如:>=您好)
|
||||||
value = val.replaceFirst(rule.getValue(),"");
|
value = val.replaceFirst(rule.getValue(),"");
|
||||||
}else if(val.startsWith(rule.getCondition()+QUERY_SEPARATE_KEYWORD)){
|
}else if(val.startsWith(rule.getCondition()+QUERY_SEPARATE_KEYWORD)){
|
||||||
value = val.replaceFirst(rule.getCondition()+QUERY_SEPARATE_KEYWORD,"").trim();
|
value = val.replaceFirst(rule.getCondition()+QUERY_SEPARATE_KEYWORD,"").trim();
|
||||||
}
|
}
|
||||||
|
//update-end--Author:scott Date:20190724 for:initQueryWrapper组装sql查询条件错误 #284-------------------
|
||||||
}
|
}
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void addQueryByRule(QueryWrapper<?> queryWrapper,String name,String type,String value,QueryRuleEnum rule) throws ParseException {
|
private static void addQueryByRule(QueryWrapper<?> queryWrapper,String name,String type,String value,QueryRuleEnum rule) throws ParseException {
|
||||||
if(oConvertUtils.isNotEmpty(value)) {
|
if(oConvertUtils.isNotEmpty(value)) {
|
||||||
|
//update-begin--Author:sunjianlei Date:20220104 for:【JTC-409】修复逗号分割情况下没有转换类型,导致类型严格的数据库查询报错 -------------------
|
||||||
// 针对数字类型字段,多值查询
|
// 针对数字类型字段,多值查询
|
||||||
if(value.contains(COMMA)){
|
if(value.contains(COMMA)){
|
||||||
Object[] temp = Arrays.stream(value.split(COMMA)).map(v -> {
|
Object[] temp = Arrays.stream(value.split(COMMA)).map(v -> {
|
||||||
@ -617,6 +644,7 @@ public class QueryGenerator {
|
|||||||
}
|
}
|
||||||
Object temp = QueryGenerator.parseByType(value, type, rule);
|
Object temp = QueryGenerator.parseByType(value, type, rule);
|
||||||
addEasyQuery(queryWrapper, name, rule, temp);
|
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[]) {
|
}else if(value instanceof String[]) {
|
||||||
queryWrapper.in(name, (Object[]) value);
|
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()) {
|
else if(value.getClass().isArray()) {
|
||||||
queryWrapper.in(name, (Object[])value);
|
queryWrapper.in(name, (Object[])value);
|
||||||
}else {
|
}else {
|
||||||
queryWrapper.in(name, value);
|
queryWrapper.in(name, value);
|
||||||
}
|
}
|
||||||
|
//update-end-author:taoyan date:20200909 for:【bug】in 类型多值查询 不适配postgresql #1671
|
||||||
break;
|
break;
|
||||||
case LIKE:
|
case LIKE:
|
||||||
queryWrapper.like(name, value);
|
queryWrapper.like(name, value);
|
||||||
@ -753,7 +782,7 @@ public class QueryGenerator {
|
|||||||
case NOT_RIGHT_LIKE:
|
case NOT_RIGHT_LIKE:
|
||||||
queryWrapper.notLikeRight(name, value);
|
queryWrapper.notLikeRight(name, value);
|
||||||
break;
|
break;
|
||||||
// 代码逻辑说明: [TV360X-378]下拉多框根据条件查询不出来:增加自定义字段查询规则功能------------
|
//update-begin---author:chenrui ---date:20240527 for:[TV360X-378]下拉多框根据条件查询不出来:增加自定义字段查询规则功能------------
|
||||||
case LIKE_WITH_OR:
|
case LIKE_WITH_OR:
|
||||||
final String nameFinal = name;
|
final String nameFinal = name;
|
||||||
Object[] vals;
|
Object[] vals;
|
||||||
@ -762,7 +791,7 @@ public class QueryGenerator {
|
|||||||
} else if (value instanceof String[]) {
|
} else if (value instanceof String[]) {
|
||||||
vals = (Object[]) value;
|
vals = (Object[]) value;
|
||||||
}
|
}
|
||||||
// 代码逻辑说明: 【bug】in 类型多值查询 不适配postgresql #1671
|
//update-begin-author:taoyan date:20200909 for:【bug】in 类型多值查询 不适配postgresql #1671
|
||||||
else if (value.getClass().isArray()) {
|
else if (value.getClass().isArray()) {
|
||||||
vals = (Object[]) value;
|
vals = (Object[]) value;
|
||||||
} else {
|
} else {
|
||||||
@ -777,6 +806,7 @@ public class QueryGenerator {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
//update-end---author:chenrui ---date:20240527 for:[TV360X-378]下拉多框根据条件查询不出来:增加自定义字段查询规则功能------------
|
||||||
default:
|
default:
|
||||||
log.info("--查询规则未匹配到---");
|
log.info("--查询规则未匹配到---");
|
||||||
break;
|
break;
|
||||||
@ -790,8 +820,10 @@ public class QueryGenerator {
|
|||||||
private static boolean judgedIsUselessField(String name) {
|
private static boolean judgedIsUselessField(String name) {
|
||||||
return "class".equals(name) || "ids".equals(name)
|
return "class".equals(name) || "ids".equals(name)
|
||||||
|| "page".equals(name) || "rows".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
|
//// https://github.com/jeecgboot/JeecgBoot/issues/6937
|
||||||
// || "sort".equals(name) || "order".equals(name)
|
// || "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() {
|
public static Map<String, SysPermissionDataRuleModel> getRuleMap() {
|
||||||
Map<String, SysPermissionDataRuleModel> ruleMap = new HashMap<>(5);
|
Map<String, SysPermissionDataRuleModel> ruleMap = new HashMap<>(5);
|
||||||
List<SysPermissionDataRuleModel> list = null;
|
List<SysPermissionDataRuleModel> list = null;
|
||||||
// 代码逻辑说明: QQYUN-5441 【简流】获取多个用户/部门/角色 设置部门查询 报错
|
//update-begin-author:taoyan date:2023-6-1 for:QQYUN-5441 【简流】获取多个用户/部门/角色 设置部门查询 报错
|
||||||
try {
|
try {
|
||||||
list = JeecgDataAutorUtils.loadDataSearchConditon();
|
list = JeecgDataAutorUtils.loadDataSearchConditon();
|
||||||
}catch (Exception e){
|
}catch (Exception e){
|
||||||
log.error("根据request对象获取权限数据失败,可能是定时任务中执行的。", e);
|
log.error("根据request对象获取权限数据失败,可能是定时任务中执行的。", e);
|
||||||
}
|
}
|
||||||
|
//update-end-author:taoyan date:2023-6-1 for:QQYUN-5441 【简流】获取多个用户/部门/角色 设置部门查询 报错
|
||||||
if(list != null&&list.size()>0){
|
if(list != null&&list.size()>0){
|
||||||
if(list.get(0)==null){
|
if(list.get(0)==null){
|
||||||
return ruleMap;
|
return ruleMap;
|
||||||
@ -846,8 +879,9 @@ public class QueryGenerator {
|
|||||||
addEasyQuery(queryWrapper, name, rule, DateUtils.str2Date(dateStr,DateUtils.datetimeFormat.get()));
|
addEasyQuery(queryWrapper, name, rule, DateUtils.str2Date(dateStr,DateUtils.datetimeFormat.get()));
|
||||||
}
|
}
|
||||||
}else {
|
}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));
|
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;
|
return null;
|
||||||
}
|
}
|
||||||
Set<String> varParams = new HashSet<String>();
|
Set<String> varParams = new HashSet<String>();
|
||||||
// 代码逻辑说明: [QQYUN-10785]数据权限,查看自己拥有部门的权限中存在问题 #7288------------
|
//update-begin---author:chenrui ---date:20250108 for:[QQYUN-10785]数据权限,查看自己拥有部门的权限中存在问题 #7288------------
|
||||||
String regex = "#\\{\\[*\\w+]*}";
|
String regex = "#\\{\\[*\\w+]*}";
|
||||||
|
//update-end---author:chenrui ---date:20250108 for:[QQYUN-10785]数据权限,查看自己拥有部门的权限中存在问题 #7288------------
|
||||||
|
|
||||||
Pattern p = Pattern.compile(regex);
|
Pattern p = Pattern.compile(regex);
|
||||||
Matcher m = p.matcher(sql);
|
Matcher m = p.matcher(sql);
|
||||||
@ -958,8 +993,9 @@ public class QueryGenerator {
|
|||||||
Class propType = origDescriptors[i].getPropertyType();
|
Class propType = origDescriptors[i].getPropertyType();
|
||||||
boolean isString = propType.equals(String.class);
|
boolean isString = propType.equals(String.class);
|
||||||
Object value;
|
Object value;
|
||||||
// 代码逻辑说明: [TV360X-539]数据权限,配置日期等于条件时后端报转换错误------------
|
//update-begin---author:chenrui ---date:20240527 for:[TV360X-539]数据权限,配置日期等于条件时后端报转换错误------------
|
||||||
if(isString || Date.class.equals(propType)) {
|
if(isString || Date.class.equals(propType)) {
|
||||||
|
//update-end---author:chenrui ---date:20240527 for:[TV360X-539]数据权限,配置日期等于条件时后端报转换错误------------
|
||||||
value = converRuleValue(dataRule.getRuleValue());
|
value = converRuleValue(dataRule.getRuleValue());
|
||||||
}else {
|
}else {
|
||||||
value = NumberUtils.parseNumber(dataRule.getRuleValue(),propType);
|
value = NumberUtils.parseNumber(dataRule.getRuleValue(),propType);
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import org.jeecg.common.system.vo.SysUserCacheInfo;
|
|||||||
import org.jeecg.common.util.SpringContextUtils;
|
import org.jeecg.common.util.SpringContextUtils;
|
||||||
import org.springframework.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
|||||||
@ -6,18 +6,16 @@ import com.auth0.jwt.algorithms.Algorithm;
|
|||||||
import com.auth0.jwt.exceptions.JWTDecodeException;
|
import com.auth0.jwt.exceptions.JWTDecodeException;
|
||||||
import com.auth0.jwt.interfaces.DecodedJWT;
|
import com.auth0.jwt.interfaces.DecodedJWT;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.google.common.base.Joiner;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
import javax.servlet.ServletResponse;
|
||||||
import jakarta.servlet.ServletResponse;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletResponse;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import javax.servlet.http.HttpSession;
|
||||||
import jakarta.servlet.http.HttpSession;
|
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.shiro.SecurityUtils;
|
import org.apache.shiro.SecurityUtils;
|
||||||
@ -41,10 +39,8 @@ import org.jeecg.common.util.oConvertUtils;
|
|||||||
@Slf4j
|
@Slf4j
|
||||||
public class JwtUtil {
|
public class JwtUtil {
|
||||||
|
|
||||||
/**PC端,Token有效期为7天(Token在reids中缓存时间为两倍)*/
|
/**Token有效期为7天(Token在reids中缓存时间为两倍)*/
|
||||||
public static final long EXPIRE_TIME = (7 * 12) * 60 * 60 * 1000L;
|
public static final long EXPIRE_TIME = (7 * 12) * 60 * 60 * 1000;
|
||||||
/**APP端,Token有效期为30天(Token在reids中缓存时间为两倍)*/
|
|
||||||
public static final long APP_EXPIRE_TIME = (30 * 12) * 60 * 60 * 1000L;
|
|
||||||
static final String WELL_NUMBER = SymbolConstant.WELL_NUMBER + SymbolConstant.LEFT_CURLY_BRACKET;
|
static final String WELL_NUMBER = SymbolConstant.WELL_NUMBER + SymbolConstant.LEFT_CURLY_BRACKET;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -88,7 +84,7 @@ public class JwtUtil {
|
|||||||
DecodedJWT jwt = verifier.verify(token);
|
DecodedJWT jwt = verifier.verify(token);
|
||||||
return true;
|
return true;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.warn("Token验证失败:" + e.getMessage(),e);
|
log.error(e.getMessage(), e);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -114,9 +110,7 @@ public class JwtUtil {
|
|||||||
* @param username 用户名
|
* @param username 用户名
|
||||||
* @param secret 用户的密码
|
* @param secret 用户的密码
|
||||||
* @return 加密的token
|
* @return 加密的token
|
||||||
* @deprecated 请使用sign(String username, String secret, String clientType)方法代替
|
|
||||||
*/
|
*/
|
||||||
@Deprecated
|
|
||||||
public static String sign(String username, String secret) {
|
public static String sign(String username, String secret) {
|
||||||
Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
|
Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
|
||||||
Algorithm algorithm = Algorithm.HMAC256(secret);
|
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获取用户账号
|
* 根据request中的token获取用户账号
|
||||||
*
|
*
|
||||||
@ -266,6 +198,7 @@ public class JwtUtil {
|
|||||||
} else {
|
} else {
|
||||||
key = key;
|
key = key;
|
||||||
}
|
}
|
||||||
|
//update-begin---author:chenrui ---date:20250107 for:[QQYUN-10785]数据权限,查看自己拥有部门的权限中存在问题 #7288------------
|
||||||
// 是否存在字符串标志
|
// 是否存在字符串标志
|
||||||
boolean multiStr;
|
boolean multiStr;
|
||||||
if(oConvertUtils.isNotEmpty(key) && key.trim().matches("^\\[\\w+]$")){
|
if(oConvertUtils.isNotEmpty(key) && key.trim().matches("^\\[\\w+]$")){
|
||||||
@ -274,6 +207,7 @@ public class JwtUtil {
|
|||||||
} else {
|
} else {
|
||||||
multiStr = false;
|
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)) {
|
if (key.equals(DataBaseConstant.SYS_DATE)|| key.toLowerCase().equals(DataBaseConstant.SYS_DATE_TABLE)) {
|
||||||
returnValue = DateUtils.formatDate();
|
returnValue = DateUtils.formatDate();
|
||||||
@ -342,17 +276,20 @@ public class JwtUtil {
|
|||||||
if(user==null){
|
if(user==null){
|
||||||
//TODO 暂时使用用户登录部门,存在逻辑缺陷,不是用户所拥有的部门
|
//TODO 暂时使用用户登录部门,存在逻辑缺陷,不是用户所拥有的部门
|
||||||
returnValue = sysUser.getOrgCode();
|
returnValue = sysUser.getOrgCode();
|
||||||
// 代码逻辑说明: [QQYUN-10785]数据权限,查看自己拥有部门的权限中存在问题 #7288------------
|
//update-begin---author:chenrui ---date:20250107 for:[QQYUN-10785]数据权限,查看自己拥有部门的权限中存在问题 #7288------------
|
||||||
returnValue = multiStr ? "'" + returnValue + "'" : returnValue;
|
returnValue = multiStr ? "'" + returnValue + "'" : returnValue;
|
||||||
|
//update-end---author:chenrui ---date:20250107 for:[QQYUN-10785]数据权限,查看自己拥有部门的权限中存在问题 #7288------------
|
||||||
}else{
|
}else{
|
||||||
if(user.isOneDepart()) {
|
if(user.isOneDepart()) {
|
||||||
returnValue = user.getSysMultiOrgCode().get(0);
|
returnValue = user.getSysMultiOrgCode().get(0);
|
||||||
// 代码逻辑说明: [QQYUN-10785]数据权限,查看自己拥有部门的权限中存在问题 #7288------------
|
//update-begin---author:chenrui ---date:20250107 for:[QQYUN-10785]数据权限,查看自己拥有部门的权限中存在问题 #7288------------
|
||||||
returnValue = multiStr ? "'" + returnValue + "'" : returnValue;
|
returnValue = multiStr ? "'" + returnValue + "'" : returnValue;
|
||||||
|
//update-end---author:chenrui ---date:20250107 for:[QQYUN-10785]数据权限,查看自己拥有部门的权限中存在问题 #7288------------
|
||||||
}else {
|
}else {
|
||||||
// 代码逻辑说明: [QQYUN-10785]数据权限,查看自己拥有部门的权限中存在问题 #7288------------
|
//update-begin---author:chenrui ---date:20250107 for:[QQYUN-10785]数据权限,查看自己拥有部门的权限中存在问题 #7288------------
|
||||||
returnValue = user.getSysMultiOrgCode().stream()
|
returnValue = user.getSysMultiOrgCode().stream()
|
||||||
.filter(Objects::nonNull)
|
.filter(Objects::nonNull)
|
||||||
|
//update-begin---author:chenrui ---date:20250224 for:[issues/7288]数据权限,查看自己拥有部门的权限中存在问题 #7288------------
|
||||||
.map(orgCode -> {
|
.map(orgCode -> {
|
||||||
if (multiStr) {
|
if (multiStr) {
|
||||||
return "'" + orgCode + "'";
|
return "'" + orgCode + "'";
|
||||||
@ -360,7 +297,9 @@ public class JwtUtil {
|
|||||||
return orgCode;
|
return orgCode;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
//update-end---author:chenrui ---date:20250224 for:[issues/7288]数据权限,查看自己拥有部门的权限中存在问题 #7288------------
|
||||||
.collect(Collectors.joining(", "));
|
.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)){
|
else if (key.equals(TenantConstant.TENANT_ID) || key.toLowerCase().equals(TenantConstant.TENANT_ID_TABLE)){
|
||||||
try {
|
try {
|
||||||
returnValue = SpringContextUtils.getHttpServletRequest().getHeader(CommonConstant.TENANT_ID);
|
returnValue = SpringContextUtils.getHttpServletRequest().getHeader(CommonConstant.TENANT_ID);
|
||||||
@ -382,6 +321,7 @@ public class JwtUtil {
|
|||||||
log.warn("获取系统租户异常:" + e.getMessage());
|
log.warn("获取系统租户异常:" + e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
//update-end-author:taoyan date:20210330 for:多租户ID作为系统变量
|
||||||
if(returnValue!=null){returnValue = returnValue + moshi;}
|
if(returnValue!=null){returnValue = returnValue + moshi;}
|
||||||
return returnValue;
|
return returnValue;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,7 +3,9 @@ package org.jeecg.common.system.util;
|
|||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.jeecg.common.system.annotation.EnumDict;
|
import org.jeecg.common.system.annotation.EnumDict;
|
||||||
import org.jeecg.common.system.vo.DictModel;
|
import org.jeecg.common.system.vo.DictModel;
|
||||||
|
import org.jeecg.common.util.SpringContextUtils;
|
||||||
import org.jeecg.common.util.oConvertUtils;
|
import org.jeecg.common.util.oConvertUtils;
|
||||||
|
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
|
||||||
import org.springframework.core.io.Resource;
|
import org.springframework.core.io.Resource;
|
||||||
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
|
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
|
||||||
import org.springframework.core.io.support.ResourcePatternResolver;
|
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.MetadataReader;
|
||||||
import org.springframework.core.type.classreading.MetadataReaderFactory;
|
import org.springframework.core.type.classreading.MetadataReaderFactory;
|
||||||
import org.springframework.util.ClassUtils;
|
import org.springframework.util.ClassUtils;
|
||||||
|
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|
||||||
@ -66,13 +67,13 @@ public class ResourceUtil {
|
|||||||
synchronized (ResourceUtil.class) {
|
synchronized (ResourceUtil.class) {
|
||||||
if (!initialized) {
|
if (!initialized) {
|
||||||
long startTime = System.currentTimeMillis();
|
long startTime = System.currentTimeMillis();
|
||||||
log.debug("【枚举字典加载】开始初始化枚举字典数据...");
|
log.info("【枚举字典加载】开始初始化枚举字典数据...");
|
||||||
|
|
||||||
initEnumDictData();
|
initEnumDictData();
|
||||||
initialized = true;
|
initialized = true;
|
||||||
|
|
||||||
long endTime = System.currentTimeMillis();
|
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();
|
long scanEndTime = System.currentTimeMillis();
|
||||||
log.debug("【枚举字典加载】文件扫描完成,总共找到 {} 个枚举类文件,扫描耗时: {}ms", allResources.size(), scanEndTime - scanStartTime);
|
log.info("【枚举字典加载】文件扫描完成,总共找到 {} 个枚举类文件,扫描耗时: {}ms", allResources.size(), scanEndTime - scanStartTime);
|
||||||
|
|
||||||
MetadataReaderFactory readerFactory = new CachingMetadataReaderFactory(resourcePatternResolver);
|
MetadataReaderFactory readerFactory = new CachingMetadataReaderFactory(resourcePatternResolver);
|
||||||
|
|
||||||
@ -125,7 +126,7 @@ public class ResourceUtil {
|
|||||||
}
|
}
|
||||||
|
|
||||||
long processEndTime = System.currentTimeMillis();
|
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) {
|
for (DictModel dm : dictItemList) {
|
||||||
String value = dm.getValue();
|
String value = dm.getValue();
|
||||||
if (keySet.contains(value)) {
|
if (keySet.contains(value)) {
|
||||||
// 修复bug:获取或创建该dictCode对应的list,而不是每次都创建新的list
|
List<DictModel> list = new ArrayList<>();
|
||||||
List<DictModel> list = map.computeIfAbsent(code, k -> new ArrayList<>());
|
|
||||||
list.add(new DictModel(value, dm.getText()));
|
list.add(new DictModel(value, dm.getText()));
|
||||||
//break;
|
map.put(code, list);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -150,7 +150,7 @@ public class SqlConcatUtil {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static String getInConditionValue(Object value,boolean isString) {
|
private static String getInConditionValue(Object value,boolean isString) {
|
||||||
// 代码逻辑说明: 查询条件如果输入,导致sql报错
|
//update-begin-author:taoyan date:20210628 for: 查询条件如果输入,导致sql报错
|
||||||
String[] temp = value.toString().split(",");
|
String[] temp = value.toString().split(",");
|
||||||
if(temp.length==0){
|
if(temp.length==0){
|
||||||
return "('')";
|
return "('')";
|
||||||
@ -168,6 +168,7 @@ public class SqlConcatUtil {
|
|||||||
}else {
|
}else {
|
||||||
return "("+value.toString()+")";
|
return "("+value.toString()+")";
|
||||||
}
|
}
|
||||||
|
//update-end-author:taoyan date:20210628 for: 查询条件如果输入,导致sql报错
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -214,6 +215,7 @@ public class SqlConcatUtil {
|
|||||||
}
|
}
|
||||||
}else {
|
}else {
|
||||||
|
|
||||||
|
//update-begin-author:taoyan date:2022-6-30 for: issues/3810 数据权限规则问题
|
||||||
// 走到这里说明 value不带有任何模糊查询的标识(*或者%)
|
// 走到这里说明 value不带有任何模糊查询的标识(*或者%)
|
||||||
if (ruleEnum == QueryRuleEnum.LEFT_LIKE) {
|
if (ruleEnum == QueryRuleEnum.LEFT_LIKE) {
|
||||||
if (DataBaseConstant.DB_TYPE_SQLSERVER.equals(getDbType())) {
|
if (DataBaseConstant.DB_TYPE_SQLSERVER.equals(getDbType())) {
|
||||||
@ -234,6 +236,7 @@ public class SqlConcatUtil {
|
|||||||
return "'%" + str + "%'";
|
return "'%" + str + "%'";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
//update-end-author:taoyan date:2022-6-30 for: issues/3810 数据权限规则问题
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import java.util.Map;
|
|||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
|
|||||||
@ -19,7 +19,7 @@ import org.springframework.jdbc.datasource.DriverManagerDataSource;
|
|||||||
import org.springframework.util.FileCopyUtils;
|
import org.springframework.util.FileCopyUtils;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
import javax.sql.DataSource;
|
import javax.sql.DataSource;
|
||||||
import java.io.ByteArrayInputStream;
|
import java.io.ByteArrayInputStream;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
@ -56,9 +56,7 @@ public class CommonUtils {
|
|||||||
public static String uploadOnlineImage(byte[] data,String basePath,String bizPath,String uploadType){
|
public static String uploadOnlineImage(byte[] data,String basePath,String bizPath,String uploadType){
|
||||||
String dbPath = null;
|
String dbPath = null;
|
||||||
String fileName = "image" + Math.round(Math.random() * 100000000000L);
|
String fileName = "image" + Math.round(Math.random() * 100000000000L);
|
||||||
//update-begin---author:wangshuai---date:2026-01-08---for:【QQYUN-14535】ai生成图片的后缀不一致的,导致不展示---
|
fileName += "." + PoiPublicUtil.getFileExtendName(data);
|
||||||
fileName += "." + PoiPublicUtil.getFileExtendName(data).toLowerCase();
|
|
||||||
//update-end---author:wangshuai---date:2026-01-08---for:【QQYUN-14535】ai生成图片的后缀不一致的,导致不展示---
|
|
||||||
try {
|
try {
|
||||||
if(CommonConstant.UPLOAD_TYPE_LOCAL.equals(uploadType)){
|
if(CommonConstant.UPLOAD_TYPE_LOCAL.equals(uploadType)){
|
||||||
File file = new File(basePath + File.separator + bizPath + File.separator );
|
File file = new File(basePath + File.separator + bizPath + File.separator );
|
||||||
|
|||||||
@ -10,9 +10,7 @@ import com.aliyuncs.exceptions.ClientException;
|
|||||||
import com.aliyuncs.profile.DefaultProfile;
|
import com.aliyuncs.profile.DefaultProfile;
|
||||||
import com.aliyuncs.profile.IClientProfile;
|
import com.aliyuncs.profile.IClientProfile;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.jeecg.common.constant.CommonConstant;
|
|
||||||
import org.jeecg.common.constant.enums.DySmsEnum;
|
import org.jeecg.common.constant.enums.DySmsEnum;
|
||||||
import org.jeecg.config.JeecgBaseConfig;
|
|
||||||
import org.jeecg.config.JeecgSmsTemplateConfig;
|
import org.jeecg.config.JeecgSmsTemplateConfig;
|
||||||
import org.jeecg.config.StaticConfig;
|
import org.jeecg.config.StaticConfig;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
@ -63,21 +61,17 @@ public class DySmsHelper {
|
|||||||
|
|
||||||
|
|
||||||
public static boolean sendSms(String phone, JSONObject templateParamJson, DySmsEnum dySmsEnum) throws ClientException {
|
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.defaultConnectTimeout", "10000");
|
||||||
System.setProperty("sun.net.client.defaultReadTimeout", "10000");
|
System.setProperty("sun.net.client.defaultReadTimeout", "10000");
|
||||||
|
|
||||||
// 代码逻辑说明: 配置类数据获取
|
//update-begin-author:taoyan date:20200811 for:配置类数据获取
|
||||||
StaticConfig staticConfig = SpringContextUtils.getBean(StaticConfig.class);
|
StaticConfig staticConfig = SpringContextUtils.getBean(StaticConfig.class);
|
||||||
//logger.info("阿里大鱼短信秘钥 accessKeyId:" + staticConfig.getAccessKeyId());
|
//logger.info("阿里大鱼短信秘钥 accessKeyId:" + staticConfig.getAccessKeyId());
|
||||||
//logger.info("阿里大鱼短信秘钥 accessKeySecret:"+ staticConfig.getAccessKeySecret());
|
//logger.info("阿里大鱼短信秘钥 accessKeySecret:"+ staticConfig.getAccessKeySecret());
|
||||||
setAccessKeyId(staticConfig.getAccessKeyId());
|
setAccessKeyId(staticConfig.getAccessKeyId());
|
||||||
setAccessKeySecret(staticConfig.getAccessKeySecret());
|
setAccessKeySecret(staticConfig.getAccessKeySecret());
|
||||||
|
//update-end-author:taoyan date:20200811 for:配置类数据获取
|
||||||
|
|
||||||
//初始化acsClient,暂不支持region化
|
//初始化acsClient,暂不支持region化
|
||||||
IClientProfile profile = DefaultProfile.getProfile("cn-hangzhou", accessKeyId, accessKeySecret);
|
IClientProfile profile = DefaultProfile.getProfile("cn-hangzhou", accessKeyId, accessKeySecret);
|
||||||
@ -87,7 +81,7 @@ public class DySmsHelper {
|
|||||||
//验证json参数
|
//验证json参数
|
||||||
validateParam(templateParamJson,dySmsEnum);
|
validateParam(templateParamJson,dySmsEnum);
|
||||||
|
|
||||||
// 代码逻辑说明: 【QQYUN-9422】短信模板管理,阿里云---
|
//update-begin---author:wangshuai---date:2024-11-05---for:【QQYUN-9422】短信模板管理,阿里云---
|
||||||
String templateCode = dySmsEnum.getTemplateCode();
|
String templateCode = dySmsEnum.getTemplateCode();
|
||||||
JeecgSmsTemplateConfig baseConfig = SpringContextUtils.getBean(JeecgSmsTemplateConfig.class);
|
JeecgSmsTemplateConfig baseConfig = SpringContextUtils.getBean(JeecgSmsTemplateConfig.class);
|
||||||
if(baseConfig != null && CollectionUtil.isNotEmpty(baseConfig.getTemplateCode())){
|
if(baseConfig != null && CollectionUtil.isNotEmpty(baseConfig.getTemplateCode())){
|
||||||
@ -103,6 +97,7 @@ public class DySmsHelper {
|
|||||||
logger.info("yml中读取签名名称{}",baseConfig.getSignature());
|
logger.info("yml中读取签名名称{}",baseConfig.getSignature());
|
||||||
signName = baseConfig.getSignature();
|
signName = baseConfig.getSignature();
|
||||||
}
|
}
|
||||||
|
//update-end---author:wangshuai---date:2024-11-05---for:【QQYUN-9422】短信模板管理,阿里云---
|
||||||
|
|
||||||
//组装请求对象-具体描述见控制台-文档部分内容
|
//组装请求对象-具体描述见控制台-文档部分内容
|
||||||
SendSmsRequest request = new SendSmsRequest();
|
SendSmsRequest request = new SendSmsRequest();
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
package org.jeecg.common.util;
|
package org.jeecg.common.util;
|
||||||
|
|
||||||
import cn.hutool.core.io.IoUtil;
|
import cn.hutool.core.io.IoUtil;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
|
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
|
||||||
import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;
|
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.exception.JeecgBootException;
|
||||||
import org.jeecg.common.util.filter.SsrfFileTypeFilter;
|
import org.jeecg.common.util.filter.SsrfFileTypeFilter;
|
||||||
|
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
import java.io.*;
|
import java.io.*;
|
||||||
import java.net.HttpURLConnection;
|
import java.net.HttpURLConnection;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
|
|||||||
@ -4,13 +4,13 @@ import com.alibaba.fastjson.JSON;
|
|||||||
import com.alibaba.fastjson.JSONObject;
|
import com.alibaba.fastjson.JSONObject;
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.jeecg.common.constant.SymbolConstant;
|
import org.jeecg.common.constant.SymbolConstant;
|
||||||
import org.jeecg.common.handler.IFillRuleHandler;
|
import org.jeecg.common.handler.IFillRuleHandler;
|
||||||
import org.jeecg.common.system.query.QueryGenerator;
|
import org.jeecg.common.system.query.QueryGenerator;
|
||||||
|
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -38,12 +38,14 @@ public class HTMLUtils {
|
|||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
public static String parseMarkdown(String markdownContent) {
|
public static String parseMarkdown(String markdownContent) {
|
||||||
|
//update-begin---author:wangshuai---date:2024-06-26---for:【TV360X-1344】JDK17 邮箱发送失败,需要换写法---
|
||||||
/*PegDownProcessor pdp = new PegDownProcessor();
|
/*PegDownProcessor pdp = new PegDownProcessor();
|
||||||
return pdp.markdownToHtml(markdownContent);*/
|
return pdp.markdownToHtml(markdownContent);*/
|
||||||
Parser parser = Parser.builder().build();
|
Parser parser = Parser.builder().build();
|
||||||
Node document = parser.parse(markdownContent);
|
Node document = parser.parse(markdownContent);
|
||||||
HtmlRenderer renderer = HtmlRenderer.builder().build();
|
HtmlRenderer renderer = HtmlRenderer.builder().build();
|
||||||
return renderer.render(document);
|
return renderer.render(document);
|
||||||
|
//update-end---author:wangshuai---date:2024-06-26---for:【TV360X-1344】JDK17 邮箱发送失败,需要换写法---
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
package org.jeecg.common.util;
|
package org.jeecg.common.util;
|
||||||
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.jeecg.common.constant.CommonConstant;
|
import org.jeecg.common.constant.CommonConstant;
|
||||||
|
|||||||
@ -161,10 +161,11 @@ public class MinioUtil {
|
|||||||
public static String getObjectUrl(String bucketName, String objectName, Integer expires) {
|
public static String getObjectUrl(String bucketName, String objectName, Integer expires) {
|
||||||
initMinio(minioUrl, minioName,minioPass);
|
initMinio(minioUrl, minioName,minioPass);
|
||||||
try{
|
try{
|
||||||
// 代码逻辑说明: 获取文件外链报错提示method不能为空,导致文件下载和预览失败----
|
//update-begin---author:liusq Date:20220121 for:获取文件外链报错提示method不能为空,导致文件下载和预览失败----
|
||||||
GetPresignedObjectUrlArgs objectArgs = GetPresignedObjectUrlArgs.builder().object(objectName)
|
GetPresignedObjectUrlArgs objectArgs = GetPresignedObjectUrlArgs.builder().object(objectName)
|
||||||
.bucket(bucketName)
|
.bucket(bucketName)
|
||||||
.expiry(expires).method(Method.GET).build();
|
.expiry(expires).method(Method.GET).build();
|
||||||
|
//update-begin---author:liusq Date:20220121 for:获取文件外链报错提示method不能为空,导致文件下载和预览失败----
|
||||||
String url = minioClient.getPresignedObjectUrl(objectArgs);
|
String url = minioClient.getPresignedObjectUrl(objectArgs);
|
||||||
return URLDecoder.decode(url,"UTF-8");
|
return URLDecoder.decode(url,"UTF-8");
|
||||||
}catch (Exception e){
|
}catch (Exception e){
|
||||||
|
|||||||
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -99,8 +99,9 @@ public class PasswordUtil {
|
|||||||
Cipher cipher = Cipher.getInstance(ALGORITHM);
|
Cipher cipher = Cipher.getInstance(ALGORITHM);
|
||||||
|
|
||||||
cipher.init(Cipher.ENCRYPT_MODE, key, parameterSpec);
|
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"));
|
encipheredData = cipher.doFinal(plaintext.getBytes("utf-8"));
|
||||||
|
//update-end-author:sccott date:20180815 for:中文作为用户名时,加密的密码windows和linux会得到不同的结果 gitee/issues/IZUD7
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
}
|
}
|
||||||
return bytesToHexString(encipheredData);
|
return bytesToHexString(encipheredData);
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import com.alibaba.fastjson.JSONObject;
|
|||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.springframework.http.*;
|
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.http.converter.StringHttpMessageConverter;
|
||||||
import org.springframework.web.client.RestTemplate;
|
import org.springframework.web.client.RestTemplate;
|
||||||
|
|
||||||
@ -46,7 +46,7 @@ public class RestUtil {
|
|||||||
|
|
||||||
public static String getBaseUrl() {
|
public static String getBaseUrl() {
|
||||||
String basepath = getDomain() + getPath();
|
String basepath = getDomain() + getPath();
|
||||||
log.debug(" RestUtil.getBaseUrl: " + basepath);
|
log.info(" RestUtil.getBaseUrl: " + basepath);
|
||||||
return basepath;
|
return basepath;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -56,19 +56,12 @@ public class RestUtil {
|
|||||||
private final static RestTemplate RT;
|
private final static RestTemplate RT;
|
||||||
|
|
||||||
static {
|
static {
|
||||||
// 解决[issues/8859]online表单java增强失效------------
|
SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
|
||||||
// 使用 Apache HttpClient 避免 JDK HttpURLConnection 的 too many bytes written 问题
|
|
||||||
HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory();
|
|
||||||
requestFactory.setConnectTimeout(30000);
|
requestFactory.setConnectTimeout(30000);
|
||||||
requestFactory.setReadTimeout(30000);
|
requestFactory.setReadTimeout(30000);
|
||||||
RT = new RestTemplate(requestFactory);
|
RT = new RestTemplate(requestFactory);
|
||||||
// 解决乱码问题(替换 StringHttpMessageConverter 为 UTF-8)
|
// 解决乱码问题
|
||||||
for (int i = 0; i < RT.getMessageConverters().size(); i++) {
|
RT.getMessageConverters().set(1, new StringHttpMessageConverter(StandardCharsets.UTF_8));
|
||||||
if (RT.getMessageConverters().get(i) instanceof StringHttpMessageConverter) {
|
|
||||||
RT.getMessageConverters().set(i, new StringHttpMessageConverter(StandardCharsets.UTF_8));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static RestTemplate getRestTemplate() {
|
public static RestTemplate getRestTemplate() {
|
||||||
@ -199,7 +192,7 @@ public class RestUtil {
|
|||||||
* @return ResponseEntity<responseType>
|
* @return ResponseEntity<responseType>
|
||||||
*/
|
*/
|
||||||
public static <T> ResponseEntity<T> request(String url, HttpMethod method, HttpHeaders headers, JSONObject variables, Object params, Class<T> 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)) {
|
if (StringUtils.isEmpty(url)) {
|
||||||
throw new RuntimeException("url 不能为空");
|
throw new RuntimeException("url 不能为空");
|
||||||
}
|
}
|
||||||
@ -223,16 +216,6 @@ public class RestUtil {
|
|||||||
if (variables != null && !variables.isEmpty()) {
|
if (variables != null && !variables.isEmpty()) {
|
||||||
url += ("?" + asUrlVariables(variables));
|
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);
|
HttpEntity<String> request = new HttpEntity<>(body, headers);
|
||||||
return RT.exchange(url, method, request, responseType);
|
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,
|
public static <T> ResponseEntity<T> request(String url, HttpMethod method, HttpHeaders headers,
|
||||||
JSONObject variables, Object params, Class<T> responseType, int timeout) {
|
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)) {
|
if (StringUtils.isEmpty(url)) {
|
||||||
throw new RuntimeException("url 不能为空");
|
throw new RuntimeException("url 不能为空");
|
||||||
@ -267,18 +250,12 @@ public class RestUtil {
|
|||||||
// 创建自定义RestTemplate(如果需要设置超时)
|
// 创建自定义RestTemplate(如果需要设置超时)
|
||||||
RestTemplate restTemplate = RT;
|
RestTemplate restTemplate = RT;
|
||||||
if (timeout > 0) {
|
if (timeout > 0) {
|
||||||
// 代码逻辑说明: [issues/8859]online表单java增强失效------------
|
SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
|
||||||
HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory();
|
|
||||||
requestFactory.setConnectTimeout(timeout);
|
requestFactory.setConnectTimeout(timeout);
|
||||||
requestFactory.setReadTimeout(timeout);
|
requestFactory.setReadTimeout(timeout);
|
||||||
restTemplate = new RestTemplate(requestFactory);
|
restTemplate = new RestTemplate(requestFactory);
|
||||||
// 解决乱码问题(替换 StringHttpMessageConverter 为 UTF-8)
|
// 解决乱码问题
|
||||||
for (int i = 0; i < restTemplate.getMessageConverters().size(); i++) {
|
restTemplate.getMessageConverters().set(1, new StringHttpMessageConverter(StandardCharsets.UTF_8));
|
||||||
if (restTemplate.getMessageConverters().get(i) instanceof StringHttpMessageConverter) {
|
|
||||||
restTemplate.getMessageConverters().set(i, new StringHttpMessageConverter(StandardCharsets.UTF_8));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 请求体
|
// 请求体
|
||||||
@ -296,21 +273,11 @@ public class RestUtil {
|
|||||||
url += ("?" + asUrlVariables(variables));
|
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);
|
HttpEntity<String> request = new HttpEntity<>(body, headers);
|
||||||
return restTemplate.exchange(url, method, request, responseType);
|
return restTemplate.exchange(url, method, request, responseType);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取JSON请求头
|
* 获取JSON请求头
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
package org.jeecg.common.util;
|
package org.jeecg.common.util;
|
||||||
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
import org.jeecg.common.constant.CommonConstant;
|
import org.jeecg.common.constant.CommonConstant;
|
||||||
import org.jeecg.common.constant.ServiceNameConstants;
|
import org.jeecg.common.constant.ServiceNameConstants;
|
||||||
|
|||||||
@ -335,12 +335,13 @@ public class SqlInjectionUtil {
|
|||||||
return table;
|
return table;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 代码逻辑说明: 表单设计器列表翻译存在表名带条件,导致翻译出问题----
|
//update-begin---author:scott ---date:2024-05-28 for:表单设计器列表翻译存在表名带条件,导致翻译出问题----
|
||||||
int index = table.toLowerCase().indexOf(" where ");
|
int index = table.toLowerCase().indexOf(" where ");
|
||||||
if (index != -1) {
|
if (index != -1) {
|
||||||
table = table.substring(0, index);
|
table = table.substring(0, index);
|
||||||
log.info("截掉where之后的新表名:" + table);
|
log.info("截掉where之后的新表名:" + table);
|
||||||
}
|
}
|
||||||
|
//update-end---author:scott ---date::2024-05-28 for:表单设计器列表翻译存在表名带条件,导致翻译出问题----
|
||||||
|
|
||||||
table = table.trim();
|
table = table.trim();
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -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]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -11,7 +11,7 @@ import org.jeecg.common.exception.JeecgBoot401Exception;
|
|||||||
import org.jeecg.common.system.util.JwtUtil;
|
import org.jeecg.common.system.util.JwtUtil;
|
||||||
import org.jeecg.common.system.vo.LoginUser;
|
import org.jeecg.common.system.vo.LoginUser;
|
||||||
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @Author scott
|
* @Author scott
|
||||||
@ -121,9 +121,7 @@ public class TokenUtils {
|
|||||||
}
|
}
|
||||||
// 校验token是否超时失效 & 或者账号密码是否错误
|
// 校验token是否超时失效 & 或者账号密码是否错误
|
||||||
if (!jwtTokenRefresh(token, username, user.getPassword(), redisUtil)) {
|
if (!jwtTokenRefresh(token, username, user.getPassword(), redisUtil)) {
|
||||||
// 用户登录Token过期提示信息
|
throw new JeecgBoot401Exception(CommonConstant.TOKEN_IS_INVALID_MSG);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -141,15 +139,10 @@ public class TokenUtils {
|
|||||||
if (oConvertUtils.isNotEmpty(cacheToken)) {
|
if (oConvertUtils.isNotEmpty(cacheToken)) {
|
||||||
// 校验token有效性
|
// 校验token有效性
|
||||||
if (!JwtUtil.verify(cacheToken, userName, passWord)) {
|
if (!JwtUtil.verify(cacheToken, userName, passWord)) {
|
||||||
// 从token中解析客户端类型,保持续期时使用相同的客户端类型
|
String newAuthorization = JwtUtil.sign(userName, passWord);
|
||||||
String clientType = JwtUtil.getClientType(token);
|
// 设置Toekn缓存有效时间
|
||||||
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;
|
|
||||||
redisUtil.set(CommonConstant.PREFIX_USER_TOKEN + token, newAuthorization);
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -54,10 +54,11 @@ public class FreemarkerParseFactory {
|
|||||||
//classic_compatible设置,解决报空指针错误
|
//classic_compatible设置,解决报空指针错误
|
||||||
SQL_CONFIG.setClassicCompatible(true);
|
SQL_CONFIG.setClassicCompatible(true);
|
||||||
|
|
||||||
// 解决freemarker模板注入问题 禁止解析ObjectConstructor,Execute和freemarker.template.utility.JythonRuntime。
|
//update-begin-author:taoyan date:2022-8-10 for: freemarker模板注入问题 禁止解析ObjectConstructor,Execute和freemarker.template.utility.JythonRuntime。
|
||||||
//https://ackcent.com/in-depth-freemarker-template-injection/
|
//https://ackcent.com/in-depth-freemarker-template-injection/
|
||||||
TPL_CONFIG.setNewBuiltinClassResolver(TemplateClassResolver.SAFER_RESOLVER);
|
TPL_CONFIG.setNewBuiltinClassResolver(TemplateClassResolver.SAFER_RESOLVER);
|
||||||
SQL_CONFIG.setNewBuiltinClassResolver(TemplateClassResolver.SAFER_RESOLVER);
|
SQL_CONFIG.setNewBuiltinClassResolver(TemplateClassResolver.SAFER_RESOLVER);
|
||||||
|
//update-end-author:taoyan date:2022-8-10 for: freemarker模板注入问题 禁止解析ObjectConstructor,Execute和freemarker.template.utility.JythonRuntime。
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -72,12 +73,13 @@ public class FreemarkerParseFactory {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
// 代码逻辑说明: 解决问题 - 错误提示sql文件不存在,实际问题是sql freemarker用法错误-----
|
//update-begin--Author:scott Date:20180320 for:解决问题 - 错误提示sql文件不存在,实际问题是sql freemarker用法错误-----
|
||||||
if (e instanceof ParseException) {
|
if (e instanceof ParseException) {
|
||||||
log.error(e.getMessage(), e.fillInStackTrace());
|
log.error(e.getMessage(), e.fillInStackTrace());
|
||||||
throw new Exception(e);
|
throw new Exception(e);
|
||||||
}
|
}
|
||||||
log.debug("----isExistTemplate----" + e.toString());
|
log.debug("----isExistTemplate----" + e.toString());
|
||||||
|
//update-end--Author:scott Date:20180320 for:解决问题 - 错误提示sql文件不存在,实际问题是sql freemarker用法错误------
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@ -1,107 +1,124 @@
|
|||||||
package org.jeecg.common.util.encryption;
|
package org.jeecg.common.util.encryption;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import org.apache.shiro.codec.Base64;
|
||||||
import org.apache.shiro.lang.codec.Base64;
|
|
||||||
import org.jeecg.common.util.oConvertUtils;
|
|
||||||
import javax.crypto.Cipher;
|
import javax.crypto.Cipher;
|
||||||
import javax.crypto.spec.IvParameterSpec;
|
import javax.crypto.spec.IvParameterSpec;
|
||||||
import javax.crypto.spec.SecretKeySpec;
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* AES 工具 (兼容历史 NoPadding + 新 PKCS5Padding)
|
* @Description: AES 加密
|
||||||
|
* @author: jeecg-boot
|
||||||
|
* @date: 2022/3/30 11:48
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
|
||||||
public class AesEncryptUtil {
|
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");
|
* @param data 要加密的数据
|
||||||
SecretKeySpec ks = new SecretKeySpec(KEY.getBytes(StandardCharsets.UTF_8), "AES");
|
* @param key 加密key
|
||||||
IvParameterSpec ivSpec = new IvParameterSpec(IV.getBytes(StandardCharsets.UTF_8));
|
* @param iv 加密iv
|
||||||
cipher.init(Cipher.DECRYPT_MODE, ks, ivSpec);
|
* @return 加密的结果
|
||||||
byte[] plain = cipher.doFinal(Base64.decode(cipherBase64));
|
* @throws Exception
|
||||||
return new String(plain, StandardCharsets.UTF_8);
|
*/
|
||||||
}
|
public static String encrypt(String data, String key, String iv) throws Exception {
|
||||||
|
try {
|
||||||
|
|
||||||
/* -------- 旧版:CBC + NoPadding (手工补 0) -------- */
|
//"算法/模式/补码方式"NoPadding PkcsPadding
|
||||||
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{
|
|
||||||
Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
|
Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
|
||||||
int blockSize = cipher.getBlockSize();
|
int blockSize = cipher.getBlockSize();
|
||||||
byte[] dataBytes = data.getBytes(StandardCharsets.UTF_8);
|
|
||||||
|
byte[] dataBytes = data.getBytes();
|
||||||
int plaintextLength = dataBytes.length;
|
int plaintextLength = dataBytes.length;
|
||||||
if (plaintextLength % blockSize != 0) {
|
if (plaintextLength % blockSize != 0) {
|
||||||
plaintextLength += (blockSize - (plaintextLength % blockSize));
|
plaintextLength = plaintextLength + (blockSize - (plaintextLength % blockSize));
|
||||||
}
|
}
|
||||||
|
|
||||||
byte[] plaintext = new byte[plaintextLength];
|
byte[] plaintext = new byte[plaintextLength];
|
||||||
System.arraycopy(dataBytes, 0, plaintext, 0, dataBytes.length);
|
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);
|
cipher.init(Cipher.ENCRYPT_MODE, keyspec, ivspec);
|
||||||
byte[] encrypted = cipher.doFinal(plaintext);
|
byte[] encrypted = cipher.doFinal(plaintext);
|
||||||
|
|
||||||
return Base64.encodeToString(encrypted);
|
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"); // 仅验证管道是否可用(旧方式)
|
* @param data 要解密的数据
|
||||||
// System.out.println(resolvePassword(frontCipher));
|
* @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);
|
||||||
// }
|
// }
|
||||||
}
|
|
||||||
|
}
|
||||||
|
|||||||
@ -194,7 +194,7 @@ public class SsrfFileTypeFilter {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
private static String getFileType(MultipartFile file, String customPath) throws Exception {
|
private static String getFileType(MultipartFile file, String customPath) throws Exception {
|
||||||
// 代码逻辑说明: [issue/4672]方法造成的文件被占用,注释掉此方法tomcat就能自动清理掉临时文件
|
//update-begin-author:liusq date:20230404 for: [issue/4672]方法造成的文件被占用,注释掉此方法tomcat就能自动清理掉临时文件
|
||||||
String fileExtendName = null;
|
String fileExtendName = null;
|
||||||
InputStream is = null;
|
InputStream is = null;
|
||||||
try {
|
try {
|
||||||
@ -234,6 +234,7 @@ public class SsrfFileTypeFilter {
|
|||||||
is.close();
|
is.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
//update-end-author:liusq date:20230404 for: [issue/4672]方法造成的文件被占用,注释掉此方法tomcat就能自动清理掉临时文件
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -10,11 +10,7 @@ import org.jeecg.common.constant.SymbolConstant;
|
|||||||
import org.jeecg.config.mybatis.MybatisPlusSaasConfig;
|
import org.jeecg.config.mybatis.MybatisPlusSaasConfig;
|
||||||
import org.springframework.beans.BeanUtils;
|
import org.springframework.beans.BeanUtils;
|
||||||
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
import org.springframework.beans.BeanWrapper;
|
|
||||||
import org.springframework.beans.BeanWrapperImpl;
|
|
||||||
|
|
||||||
import java.beans.PropertyDescriptor;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.UnsupportedEncodingException;
|
import java.io.UnsupportedEncodingException;
|
||||||
@ -27,7 +23,6 @@ import java.sql.Date;
|
|||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
import java.util.stream.Stream;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@ -568,8 +563,10 @@ public class oConvertUtils {
|
|||||||
return "";
|
return "";
|
||||||
} else if (!name.contains(SymbolConstant.UNDERLINE)) {
|
} else if (!name.contains(SymbolConstant.UNDERLINE)) {
|
||||||
// 不含下划线,仅将首字母小写
|
// 不含下划线,仅将首字母小写
|
||||||
// 代码逻辑说明: TASK #2500 【代码生成器】代码生成器开发一通用模板生成功能
|
//update-begin--Author:zhoujf Date:20180503 for:TASK #2500 【代码生成器】代码生成器开发一通用模板生成功能
|
||||||
|
//update-begin--Author:zhoujf Date:20180503 for:TASK #2500 【代码生成器】代码生成器开发一通用模板生成功能
|
||||||
return name.substring(0, 1).toLowerCase() + name.substring(1).toLowerCase();
|
return name.substring(0, 1).toLowerCase() + name.substring(1).toLowerCase();
|
||||||
|
//update-end--Author:zhoujf Date:20180503 for:TASK #2500 【代码生成器】代码生成器开发一通用模板生成功能
|
||||||
}
|
}
|
||||||
// 用下划线将原始字符串分割
|
// 用下划线将原始字符串分割
|
||||||
String[] camels = name.split("_");
|
String[] camels = name.split("_");
|
||||||
@ -614,6 +611,7 @@ public class oConvertUtils {
|
|||||||
return result.substring(0, result.length() - 1);
|
return result.substring(0, result.length() - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//update-begin--Author:zhoujf Date:20180503 for:TASK #2500 【代码生成器】代码生成器开发一通用模板生成功能
|
||||||
/**
|
/**
|
||||||
* 将下划线大写方式命名的字符串转换为驼峰式。(首字母写)
|
* 将下划线大写方式命名的字符串转换为驼峰式。(首字母写)
|
||||||
* 如果转换前的下划线大写方式命名的字符串为空,则返回空字符串。</br>
|
* 如果转换前的下划线大写方式命名的字符串为空,则返回空字符串。</br>
|
||||||
@ -646,6 +644,7 @@ public class oConvertUtils {
|
|||||||
}
|
}
|
||||||
return result.toString();
|
return result.toString();
|
||||||
}
|
}
|
||||||
|
//update-end--Author:zhoujf Date:20180503 for:TASK #2500 【代码生成器】代码生成器开发一通用模板生成功能
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 将驼峰命名转化成下划线
|
* 将驼峰命名转化成下划线
|
||||||
@ -983,18 +982,17 @@ public class oConvertUtils {
|
|||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 判断 sourceList中的元素是否在targetList中出现
|
* 判断 list1中的元素是否在list2中出现
|
||||||
*
|
|
||||||
* QQYUN-5326【简流】获取组织人员 单/多 筛选条件 没有部门筛选
|
* QQYUN-5326【简流】获取组织人员 单/多 筛选条件 没有部门筛选
|
||||||
* @param sourceList 源列表,要检查的元素列表
|
* @param list1
|
||||||
* @param targetList 目标列表,用于匹配的列表
|
* @param list2
|
||||||
* @return 如果sourceList中有任何元素在targetList中存在则返回true,否则返回false
|
* @return
|
||||||
*/
|
*/
|
||||||
public static boolean isInList(List<String> sourceList, List<String> targetList){
|
public static boolean isInList(List<String> list1, List<String> list2){
|
||||||
for(String sourceItem: sourceList){
|
for(String str1: list1){
|
||||||
boolean flag = false;
|
boolean flag = false;
|
||||||
for(String targetItem: targetList){
|
for(String str2: list2){
|
||||||
if(sourceItem.equals(targetItem)){
|
if(str1.equals(str2)){
|
||||||
flag = true;
|
flag = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -1006,35 +1004,6 @@ public class oConvertUtils {
|
|||||||
return false;
|
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
|
* 计算文件大小转成MB
|
||||||
* @param uploadCount
|
* @param uploadCount
|
||||||
@ -1199,58 +1168,5 @@ public class oConvertUtils {
|
|||||||
public static boolean isEffectiveTenant(String tenantId) {
|
public static boolean isEffectiveTenant(String tenantId) {
|
||||||
return MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL && isNotEmpty(tenantId) && !("0").equals(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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -124,8 +124,9 @@ public class OssBootUtil {
|
|||||||
if (!fileDir.endsWith(SymbolConstant.SINGLE_SLASH)) {
|
if (!fileDir.endsWith(SymbolConstant.SINGLE_SLASH)) {
|
||||||
fileDir = fileDir.concat(SymbolConstant.SINGLE_SLASH);
|
fileDir = fileDir.concat(SymbolConstant.SINGLE_SLASH);
|
||||||
}
|
}
|
||||||
// 代码逻辑说明: 过滤上传文件夹名特殊字符,防止攻击
|
//update-begin-author:wangshuai date:20201012 for: 过滤上传文件夹名特殊字符,防止攻击
|
||||||
fileDir=StrAttackFilter.filter(fileDir);
|
fileDir=StrAttackFilter.filter(fileDir);
|
||||||
|
//update-end-author:wangshuai date:20201012 for: 过滤上传文件夹名特殊字符,防止攻击
|
||||||
fileUrl = fileUrl.append(fileDir + fileName);
|
fileUrl = fileUrl.append(fileDir + fileName);
|
||||||
|
|
||||||
if (oConvertUtils.isNotEmpty(staticDomain) && staticDomain.toLowerCase().startsWith(CommonConstant.STR_HTTP)) {
|
if (oConvertUtils.isNotEmpty(staticDomain) && staticDomain.toLowerCase().startsWith(CommonConstant.STR_HTTP)) {
|
||||||
@ -262,8 +263,9 @@ public class OssBootUtil {
|
|||||||
newBucket = bucket;
|
newBucket = bucket;
|
||||||
}
|
}
|
||||||
initOss(endPoint, accessKeyId, accessKeySecret);
|
initOss(endPoint, accessKeyId, accessKeySecret);
|
||||||
// 代码逻辑说明: 替换objectName前缀,防止key不一致导致获取不到文件----
|
//update-begin---author:liusq Date:20220120 for:替换objectName前缀,防止key不一致导致获取不到文件----
|
||||||
objectName = OssBootUtil.replacePrefix(objectName,bucket);
|
objectName = OssBootUtil.replacePrefix(objectName,bucket);
|
||||||
|
//update-end---author:liusq Date:20220120 for:替换objectName前缀,防止key不一致导致获取不到文件----
|
||||||
OSSObject ossObject = ossClient.getObject(newBucket,objectName);
|
OSSObject ossObject = ossClient.getObject(newBucket,objectName);
|
||||||
inputStream = new BufferedInputStream(ossObject.getObjectContent());
|
inputStream = new BufferedInputStream(ossObject.getObjectContent());
|
||||||
}catch (Exception e){
|
}catch (Exception e){
|
||||||
@ -291,8 +293,9 @@ public class OssBootUtil {
|
|||||||
public static String getObjectUrl(String bucketName, String objectName, Date expires) {
|
public static String getObjectUrl(String bucketName, String objectName, Date expires) {
|
||||||
initOss(endPoint, accessKeyId, accessKeySecret);
|
initOss(endPoint, accessKeyId, accessKeySecret);
|
||||||
try{
|
try{
|
||||||
// 代码逻辑说明: 替换objectName前缀,防止key不一致导致获取不到文件----
|
//update-begin---author:liusq Date:20220120 for:替换objectName前缀,防止key不一致导致获取不到文件----
|
||||||
objectName = OssBootUtil.replacePrefix(objectName,bucketName);
|
objectName = OssBootUtil.replacePrefix(objectName,bucketName);
|
||||||
|
//update-end---author:liusq Date:20220120 for:替换objectName前缀,防止key不一致导致获取不到文件----
|
||||||
if(ossClient.doesObjectExist(bucketName,objectName)){
|
if(ossClient.doesObjectExist(bucketName,objectName)){
|
||||||
URL url = ossClient.generatePresignedUrl(bucketName,objectName,expires);
|
URL url = ossClient.generatePresignedUrl(bucketName,objectName,expires);
|
||||||
//log.info("原始url : {}", url.toString());
|
//log.info("原始url : {}", url.toString());
|
||||||
|
|||||||
@ -63,7 +63,7 @@ public abstract class AbstractQueryBlackListHandler {
|
|||||||
if(list==null){
|
if(list==null){
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
log.debug(" 获取sql信息 :{} ", list.toString());
|
log.info(" 获取sql信息 :{} ", list.toString());
|
||||||
boolean flag = checkTableAndFieldsName(list);
|
boolean flag = checkTableAndFieldsName(list);
|
||||||
if(flag == false){
|
if(flag == false){
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@ -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命令行功能开启,sql:AI流程SQL节点开启
|
|
||||||
*/
|
|
||||||
private String allowSensitiveNodes = "";
|
|
||||||
}
|
|
||||||
@ -3,7 +3,7 @@ package org.jeecg.config;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import jakarta.annotation.Resource;
|
import javax.annotation.Resource;
|
||||||
|
|
||||||
import org.jeecg.common.api.CommonAPI;
|
import org.jeecg.common.api.CommonAPI;
|
||||||
import org.jeecg.common.system.vo.DictModel;
|
import org.jeecg.common.system.vo.DictModel;
|
||||||
@ -60,15 +60,17 @@ public class AutoPoiDictConfig implements AutoPoiDictServiceI {
|
|||||||
|
|
||||||
|
|
||||||
for (DictModel t : dictList) {
|
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){
|
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)){
|
if(t.getValue().contains(EXCEL_SPLIT_TAG)){
|
||||||
String val = t.getValue().replace(EXCEL_SPLIT_TAG,TEMP_EXCEL_SPLIT_TAG);
|
String val = t.getValue().replace(EXCEL_SPLIT_TAG,TEMP_EXCEL_SPLIT_TAG);
|
||||||
dictReplaces.add(t.getText() + EXCEL_SPLIT_TAG + val);
|
dictReplaces.add(t.getText() + EXCEL_SPLIT_TAG + val);
|
||||||
}else{
|
}else{
|
||||||
dictReplaces.add(t.getText() + EXCEL_SPLIT_TAG + t.getValue());
|
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) {
|
if (dictReplaces != null && dictReplaces.size() != 0) {
|
||||||
|
|||||||
@ -2,9 +2,7 @@ package org.jeecg.config;
|
|||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
import com.alibaba.druid.spring.boot3.autoconfigure.DruidDataSourceAutoConfigure;
|
import javax.servlet.*;
|
||||||
import com.alibaba.druid.spring.boot3.autoconfigure.properties.DruidStatProperties;
|
|
||||||
import jakarta.servlet.*;
|
|
||||||
|
|
||||||
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
|
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
@ -13,6 +11,8 @@ import org.springframework.boot.web.servlet.FilterRegistrationBean;
|
|||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure;
|
||||||
|
import com.alibaba.druid.spring.boot.autoconfigure.properties.DruidStatProperties;
|
||||||
import com.alibaba.druid.util.Utils;
|
import com.alibaba.druid.util.Utils;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -1,12 +1,7 @@
|
|||||||
package org.jeecg.config;
|
package org.jeecg.config;
|
||||||
|
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.Setter;
|
|
||||||
import org.jeecg.config.tencent.JeecgTencent;
|
|
||||||
import org.jeecg.config.vo.*;
|
import org.jeecg.config.vo.*;
|
||||||
import org.springframework.beans.factory.config.BeanDefinition;
|
|
||||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
import org.springframework.context.annotation.Role;
|
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
|
||||||
@ -16,7 +11,6 @@ import org.springframework.stereotype.Component;
|
|||||||
*/
|
*/
|
||||||
@Component("jeecgBaseConfig")
|
@Component("jeecgBaseConfig")
|
||||||
@ConfigurationProperties(prefix = "jeecg")
|
@ConfigurationProperties(prefix = "jeecg")
|
||||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
|
||||||
public class JeecgBaseConfig {
|
public class JeecgBaseConfig {
|
||||||
/**
|
/**
|
||||||
* 签名密钥串(字典等敏感接口)
|
* 签名密钥串(字典等敏感接口)
|
||||||
@ -76,35 +70,7 @@ public class JeecgBaseConfig {
|
|||||||
/**
|
/**
|
||||||
* 百度开放API配置
|
* 百度开放API配置
|
||||||
*/
|
*/
|
||||||
private BaiduApi baiduApi;
|
private BaiduApi baiduApi;
|
||||||
|
|
||||||
/**
|
|
||||||
* minio配置
|
|
||||||
*/
|
|
||||||
@Getter
|
|
||||||
@Setter
|
|
||||||
private JeecgMinio minio;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* oss配置
|
|
||||||
*/
|
|
||||||
@Getter
|
|
||||||
@Setter
|
|
||||||
private JeecgOSS oss;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 短信发送方式 aliyun阿里云短信 tencent腾讯云短信
|
|
||||||
*/
|
|
||||||
@Getter
|
|
||||||
@Setter
|
|
||||||
private String smsSendType = "aliyun";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 腾讯配置
|
|
||||||
*/
|
|
||||||
@Getter
|
|
||||||
@Setter
|
|
||||||
private JeecgTencent tencent;
|
|
||||||
|
|
||||||
public String getCustomResourcePrefixPath() {
|
public String getCustomResourcePrefixPath() {
|
||||||
return customResourcePrefixPath;
|
return customResourcePrefixPath;
|
||||||
|
|||||||
@ -95,11 +95,13 @@
|
|||||||
// List<Parameter> pars = new ArrayList<>();
|
// List<Parameter> pars = new ArrayList<>();
|
||||||
// tokenPar.name(CommonConstant.X_ACCESS_TOKEN).description("token").modelRef(new ModelRef("string")).parameterType("header").required(false).build();
|
// tokenPar.name(CommonConstant.X_ACCESS_TOKEN).description("token").modelRef(new ModelRef("string")).parameterType("header").required(false).build();
|
||||||
// pars.add(tokenPar.build());
|
// pars.add(tokenPar.build());
|
||||||
|
// //update-begin-author:liusq---date:2024-08-15--for: 开启多租户时,全局参数增加租户id
|
||||||
// if(MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL){
|
// if(MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL){
|
||||||
// ParameterBuilder tenantPar = new ParameterBuilder();
|
// ParameterBuilder tenantPar = new ParameterBuilder();
|
||||||
// tenantPar.name(CommonConstant.TENANT_ID).description("租户ID").modelRef(new ModelRef("string")).parameterType("header").required(false).build();
|
// tenantPar.name(CommonConstant.TENANT_ID).description("租户ID").modelRef(new ModelRef("string")).parameterType("header").required(false).build();
|
||||||
// pars.add(tenantPar.build());
|
// pars.add(tenantPar.build());
|
||||||
// }
|
// }
|
||||||
|
// //update-end-author:liusq---date:2024-08-15--for: 开启多租户时,全局参数增加租户id
|
||||||
//
|
//
|
||||||
// return pars;
|
// return pars;
|
||||||
// }
|
// }
|
||||||
|
|||||||
@ -10,37 +10,28 @@ import io.swagger.v3.oas.models.security.SecurityRequirement;
|
|||||||
import io.swagger.v3.oas.models.security.SecurityScheme;
|
import io.swagger.v3.oas.models.security.SecurityScheme;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.jeecg.common.constant.CommonConstant;
|
import org.jeecg.common.constant.CommonConstant;
|
||||||
import org.springdoc.core.customizers.OperationCustomizer;
|
import org.springdoc.core.customizers.GlobalOpenApiCustomizer;
|
||||||
import org.springdoc.core.filters.GlobalOpenApiMethodFilter;
|
import org.springdoc.core.filters.GlobalOpenApiMethodFilter;
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.context.annotation.PropertySource;
|
import org.springframework.context.annotation.PropertySource;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
|
||||||
import org.springframework.web.method.HandlerMethod;
|
|
||||||
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
|
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
|
||||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author eightmonth
|
* @author eightmonth
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Configuration
|
@Configuration
|
||||||
@ConditionalOnProperty(prefix = "knife4j", name = "production", havingValue = "false", matchIfMissing = true)
|
|
||||||
@PropertySource("classpath:config/default-spring-doc.properties")
|
@PropertySource("classpath:config/default-spring-doc.properties")
|
||||||
public class Swagger3Config implements WebMvcConfigurer {
|
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(
|
Set<String> excludedPaths = new HashSet<>(Arrays.asList(
|
||||||
"/sys/randomImage/**",
|
"/sys/randomImage/{key}",
|
||||||
"/sys/login",
|
"/sys/login",
|
||||||
"/sys/phoneLogin",
|
"/sys/phoneLogin",
|
||||||
"/sys/mLogin",
|
"/sys/mLogin",
|
||||||
@ -50,20 +41,7 @@ public class Swagger3Config implements WebMvcConfigurer {
|
|||||||
"/sys/thirdLogin/**",
|
"/sys/thirdLogin/**",
|
||||||
"/sys/user/register"
|
"/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资源:
|
* 显示swagger-ui.html文档展示页,还必须注入swagger资源:
|
||||||
@ -83,70 +61,40 @@ public class Swagger3Config implements WebMvcConfigurer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public OperationCustomizer operationCustomizer() {
|
public GlobalOpenApiCustomizer globalOpenApiCustomizer() {
|
||||||
return (operation, handlerMethod) -> {
|
return openApi -> {
|
||||||
String path = getFullPath(handlerMethod);
|
// 全局添加鉴权参数
|
||||||
if (!isExcludedPath(path)) {
|
if (openApi.getPaths() != null) {
|
||||||
operation.addSecurityItem(new SecurityRequirement().addList(CommonConstant.X_ACCESS_TOKEN));
|
openApi.getPaths().forEach((path, pathItem) -> {
|
||||||
}else{
|
//log.debug("path: {}", path);
|
||||||
log.info("忽略加入 X_ACCESS_TOKEN 的 PATH:" + path);
|
// 检查当前路径是否在排除列表中
|
||||||
|
boolean isExcluded = excludedPaths.stream().anyMatch(
|
||||||
|
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
|
@Bean
|
||||||
public OpenAPI customOpenAPI() {
|
public OpenAPI customOpenAPI() {
|
||||||
return new OpenAPI()
|
return new OpenAPI()
|
||||||
.info(new Info()
|
.info(new Info()
|
||||||
.title("JeecgBoot 后台服务API接口文档")
|
.title("JeecgBoot 后台服务API接口文档")
|
||||||
.version("3.9.1")
|
.version("3.8.3")
|
||||||
.contact(new Contact().name("北京国炬信息技术有限公司").url("www.jeccg.com").email("jeecgos@163.com"))
|
.contact(new Contact().name("北京国炬信息技术有限公司").url("www.jeccg.com").email("jeecgos@163.com"))
|
||||||
.description("后台API接口")
|
.description( "后台API接口")
|
||||||
.termsOfService("NO terms of service")
|
.termsOfService("NO terms of service")
|
||||||
.license(new License().name("Apache 2.0").url("http://www.apache.org/licenses/LICENSE-2.0.html")))
|
.license(new License().name("Apache 2.0").url("http://www.apache.org/licenses/LICENSE-2.0.html")))
|
||||||
.addSecurityItem(new SecurityRequirement().addList(CommonConstant.X_ACCESS_TOKEN))
|
.addSecurityItem(new SecurityRequirement().addList(CommonConstant.X_ACCESS_TOKEN))
|
||||||
.components(new Components().addSecuritySchemes(CommonConstant.X_ACCESS_TOKEN,
|
.components(new Components().addSecuritySchemes(CommonConstant.X_ACCESS_TOKEN,
|
||||||
new SecurityScheme()
|
new SecurityScheme().name(CommonConstant.X_ACCESS_TOKEN).type(SecurityScheme.Type.HTTP)));
|
||||||
.name(CommonConstant.X_ACCESS_TOKEN)
|
|
||||||
.type(SecurityScheme.Type.APIKEY)
|
|
||||||
.in(SecurityScheme.In.HEADER) // 关键:指定为 header
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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.LocalDateSerializer;
|
||||||
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
|
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
|
||||||
import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;
|
import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;
|
||||||
import io.micrometer.prometheusmetrics.PrometheusMeterRegistry;
|
import io.micrometer.prometheus.PrometheusMeterRegistry;
|
||||||
import jakarta.annotation.PostConstruct;
|
|
||||||
import jakarta.annotation.Resource;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.beans.factory.annotation.Qualifier;
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.beans.factory.config.BeanPostProcessor;
|
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.Bean;
|
||||||
import org.springframework.context.annotation.Conditional;
|
import org.springframework.context.annotation.Conditional;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.context.annotation.Primary;
|
import org.springframework.context.annotation.Primary;
|
||||||
|
import org.springframework.context.event.EventListener;
|
||||||
import org.springframework.http.CacheControl;
|
import org.springframework.http.CacheControl;
|
||||||
import org.springframework.http.converter.HttpMessageConverter;
|
import org.springframework.http.converter.HttpMessageConverter;
|
||||||
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
|
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.ViewControllerRegistry;
|
||||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||||
|
|
||||||
|
import javax.annotation.Resource;
|
||||||
import java.text.SimpleDateFormat;
|
import java.text.SimpleDateFormat;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
@ -47,7 +47,6 @@ import java.util.concurrent.TimeUnit;
|
|||||||
* @Author qinfeng
|
* @Author qinfeng
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
|
||||||
@Configuration
|
@Configuration
|
||||||
public class WebMvcConfiguration implements WebMvcConfigurer {
|
public class WebMvcConfiguration implements WebMvcConfigurer {
|
||||||
|
|
||||||
@ -142,6 +141,7 @@ public class WebMvcConfiguration implements WebMvcConfigurer {
|
|||||||
return objectMapper;
|
return objectMapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//update-begin---author:chenrui ---date:20240514 for:[QQYUN-9247]系统监控功能优化------------
|
||||||
// /**
|
// /**
|
||||||
// * SpringBootAdmin的Httptrace不见了
|
// * SpringBootAdmin的Httptrace不见了
|
||||||
// * https://blog.csdn.net/u013810234/article/details/110097201
|
// * https://blog.csdn.net/u013810234/article/details/110097201
|
||||||
@ -150,20 +150,20 @@ public class WebMvcConfiguration implements WebMvcConfigurer {
|
|||||||
// public InMemoryHttpTraceRepository getInMemoryHttpTrace(){
|
// public InMemoryHttpTraceRepository getInMemoryHttpTrace(){
|
||||||
// return new InMemoryHttpTraceRepository();
|
// return new InMemoryHttpTraceRepository();
|
||||||
// }
|
// }
|
||||||
|
//update-end---author:chenrui ---date:20240514 for:[QQYUN-9247]系统监控功能优化------------
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 在Bean初始化完成后立即配置PrometheusMeterRegistry,避免在Meter注册后才配置MeterFilter
|
* 监听应用启动完成事件,确保 PrometheusMeterRegistry 已经初始化
|
||||||
* for [QQYUN-12558]【监控】系统监控的头两个tab不好使,接口404
|
* for [QQYUN-12558]【监控】系统监控的头两个tab不好使,接口404
|
||||||
|
* @param event
|
||||||
* @author chenrui
|
* @author chenrui
|
||||||
* @date 2025/5/26 16:46
|
* @date 2025/5/26 16:46
|
||||||
*/
|
*/
|
||||||
@PostConstruct
|
@EventListener
|
||||||
public void initPrometheusMeterRegistry() {
|
public void onApplicationReady(ApplicationReadyEvent event) {
|
||||||
// 确保在应用启动早期就配置MeterFilter,避免警告
|
if(null != meterRegistryPostProcessor){
|
||||||
if (null != meterRegistryPostProcessor && null != prometheusMeterRegistry) {
|
meterRegistryPostProcessor.postProcessAfterInitialization(prometheusMeterRegistry, "");
|
||||||
meterRegistryPostProcessor.postProcessAfterInitialization(prometheusMeterRegistry, "prometheusMeterRegistry");
|
|
||||||
log.info("PrometheusMeterRegistry 配置完成");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -3,8 +3,8 @@ package org.jeecg.config.filter;
|
|||||||
import org.jeecg.common.constant.CommonConstant;
|
import org.jeecg.common.constant.CommonConstant;
|
||||||
import org.jeecg.config.sign.util.BodyReaderHttpServletRequestWrapper;
|
import org.jeecg.config.sign.util.BodyReaderHttpServletRequestWrapper;
|
||||||
|
|
||||||
import jakarta.servlet.*;
|
import javax.servlet.*;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -7,9 +7,9 @@ import org.jeecg.common.util.SpringContextUtils;
|
|||||||
import org.jeecg.common.util.TokenUtils;
|
import org.jeecg.common.util.TokenUtils;
|
||||||
import org.jeecg.common.util.oConvertUtils;
|
import org.jeecg.common.util.oConvertUtils;
|
||||||
|
|
||||||
import jakarta.servlet.*;
|
import javax.servlet.*;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import javax.servlet.http.HttpServletResponse;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -14,9 +14,9 @@ import org.jeecg.config.JeecgBaseConfig;
|
|||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.web.servlet.HandlerInterceptor;
|
import org.springframework.web.servlet.HandlerInterceptor;
|
||||||
|
|
||||||
import jakarta.annotation.Resource;
|
import javax.annotation.Resource;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import javax.servlet.http.HttpServletResponse;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.PrintWriter;
|
import java.io.PrintWriter;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|||||||
@ -129,18 +129,20 @@ public class MybatisInterceptor implements Interceptor {
|
|||||||
Field[] fields = null;
|
Field[] fields = null;
|
||||||
if (parameter instanceof ParamMap) {
|
if (parameter instanceof ParamMap) {
|
||||||
ParamMap<?> p = (ParamMap<?>) parameter;
|
ParamMap<?> p = (ParamMap<?>) parameter;
|
||||||
// 代码逻辑说明: 批量更新报错issues/IZA3Q--
|
//update-begin-author:scott date:20190729 for:批量更新报错issues/IZA3Q--
|
||||||
String et = "et";
|
String et = "et";
|
||||||
if (p.containsKey(et)) {
|
if (p.containsKey(et)) {
|
||||||
parameter = p.get(et);
|
parameter = p.get(et);
|
||||||
} else {
|
} else {
|
||||||
parameter = p.get("param1");
|
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) {
|
if (parameter == null) {
|
||||||
return invocation.proceed();
|
return invocation.proceed();
|
||||||
}
|
}
|
||||||
|
//update-end-author:scott date:20190729 for:更新指定字段时报错 issues/#516-
|
||||||
|
|
||||||
fields = oConvertUtils.getAllFields(parameter);
|
fields = oConvertUtils.getAllFields(parameter);
|
||||||
} else {
|
} else {
|
||||||
@ -182,6 +184,7 @@ public class MybatisInterceptor implements Interceptor {
|
|||||||
// TODO Auto-generated method stub
|
// TODO Auto-generated method stub
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//update-begin--Author:scott Date:20191213 for:关于使用Quzrtz 开启线程任务, #465
|
||||||
/**
|
/**
|
||||||
* 获取登录用户
|
* 获取登录用户
|
||||||
* @return
|
* @return
|
||||||
@ -196,5 +199,6 @@ public class MybatisInterceptor implements Interceptor {
|
|||||||
}
|
}
|
||||||
return sysUser;
|
return sysUser;
|
||||||
}
|
}
|
||||||
|
//update-end--Author:scott Date:20191213 for:关于使用Quzrtz 开启线程任务, #465
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,7 +11,7 @@ import org.jeecg.common.util.SpringContextUtils;
|
|||||||
import org.jeecg.config.mybatis.ThreadLocalDataHelper;
|
import org.jeecg.config.mybatis.ThreadLocalDataHelper;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -6,8 +6,8 @@ import org.apache.commons.lang3.StringUtils;
|
|||||||
import org.springframework.web.servlet.HandlerInterceptor;
|
import org.springframework.web.servlet.HandlerInterceptor;
|
||||||
import org.springframework.web.servlet.ModelAndView;
|
import org.springframework.web.servlet.ModelAndView;
|
||||||
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 动态数据源切换拦截器
|
* 动态数据源切换拦截器
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
package org.jeecg.config.oss;
|
package org.jeecg.config.oss;
|
||||||
|
|
||||||
import jakarta.annotation.PostConstruct;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.jeecg.common.constant.CommonConstant;
|
import org.jeecg.common.constant.CommonConstant;
|
||||||
import org.jeecg.common.constant.SymbolConstant;
|
import org.jeecg.common.constant.SymbolConstant;
|
||||||
@ -29,7 +28,7 @@ public class MinioConfig {
|
|||||||
@Value(value = "${jeecg.minio.bucketName}")
|
@Value(value = "${jeecg.minio.bucketName}")
|
||||||
private String bucketName;
|
private String bucketName;
|
||||||
|
|
||||||
@PostConstruct
|
@Bean
|
||||||
public void initMinio(){
|
public void initMinio(){
|
||||||
if(!minioUrl.startsWith(CommonConstant.STR_HTTP)){
|
if(!minioUrl.startsWith(CommonConstant.STR_HTTP)){
|
||||||
minioUrl = "http://" + minioUrl;
|
minioUrl = "http://" + minioUrl;
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
package org.jeecg.config.oss;
|
package org.jeecg.config.oss;
|
||||||
|
|
||||||
import jakarta.annotation.PostConstruct;
|
|
||||||
import org.jeecg.common.util.oss.OssBootUtil;
|
import org.jeecg.common.util.oss.OssBootUtil;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
@ -29,7 +28,7 @@ public class OssConfiguration {
|
|||||||
private String staticDomain;
|
private String staticDomain;
|
||||||
|
|
||||||
|
|
||||||
@PostConstruct
|
@Bean
|
||||||
public void initOssBootConfiguration() {
|
public void initOssBootConfiguration() {
|
||||||
OssBootUtil.setEndPoint(endpoint);
|
OssBootUtil.setEndPoint(endpoint);
|
||||||
OssBootUtil.setAccessKeyId(accessKeyId);
|
OssBootUtil.setAccessKeyId(accessKeyId);
|
||||||
|
|||||||
@ -1,8 +1,5 @@
|
|||||||
package org.jeecg.config.shiro;
|
package org.jeecg.config.shiro;
|
||||||
|
|
||||||
import jakarta.annotation.Resource;
|
|
||||||
import jakarta.servlet.DispatcherType;
|
|
||||||
import jakarta.servlet.Filter;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
|
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
|
||||||
import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;
|
import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;
|
||||||
@ -11,7 +8,6 @@ import org.apache.shiro.mgt.SecurityManager;
|
|||||||
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
|
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
|
||||||
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
|
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
|
||||||
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
|
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
|
||||||
import org.apache.shiro.spring.web.ShiroUrlPathHelper;
|
|
||||||
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
|
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
|
||||||
import org.crazycake.shiro.*;
|
import org.crazycake.shiro.*;
|
||||||
import org.jeecg.common.constant.CommonConstant;
|
import org.jeecg.common.constant.CommonConstant;
|
||||||
@ -33,10 +29,12 @@ import org.springframework.util.CollectionUtils;
|
|||||||
import org.springframework.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
import org.springframework.web.filter.DelegatingFilterProxy;
|
import org.springframework.web.filter.DelegatingFilterProxy;
|
||||||
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
|
|
||||||
import redis.clients.jedis.HostAndPort;
|
import redis.clients.jedis.HostAndPort;
|
||||||
import redis.clients.jedis.JedisCluster;
|
import redis.clients.jedis.JedisCluster;
|
||||||
|
|
||||||
|
import javax.annotation.Resource;
|
||||||
|
import javax.servlet.DispatcherType;
|
||||||
|
import javax.servlet.Filter;
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|
||||||
@ -113,7 +111,7 @@ public class ShiroConfig {
|
|||||||
filterChainDefinitionMap.put("/sys/checkAuth", "anon"); //授权接口排除
|
filterChainDefinitionMap.put("/sys/checkAuth", "anon"); //授权接口排除
|
||||||
filterChainDefinitionMap.put("/openapi/call/**", "anon"); // 开放平台接口排除
|
filterChainDefinitionMap.put("/openapi/call/**", "anon"); // 开放平台接口排除
|
||||||
|
|
||||||
// 代码逻辑说明: 排除静态资源后缀
|
//update-begin--Author:scott Date:20221116 for:排除静态资源后缀
|
||||||
filterChainDefinitionMap.put("/", "anon");
|
filterChainDefinitionMap.put("/", "anon");
|
||||||
filterChainDefinitionMap.put("/doc.html", "anon");
|
filterChainDefinitionMap.put("/doc.html", "anon");
|
||||||
filterChainDefinitionMap.put("/**/*.js", "anon");
|
filterChainDefinitionMap.put("/**/*.js", "anon");
|
||||||
@ -131,6 +129,7 @@ public class ShiroConfig {
|
|||||||
|
|
||||||
filterChainDefinitionMap.put("/**/*.glb", "anon");
|
filterChainDefinitionMap.put("/**/*.glb", "anon");
|
||||||
filterChainDefinitionMap.put("/**/*.wasm", "anon");
|
filterChainDefinitionMap.put("/**/*.wasm", "anon");
|
||||||
|
//update-end--Author:scott Date:20221116 for:排除静态资源后缀
|
||||||
|
|
||||||
filterChainDefinitionMap.put("/druid/**", "anon");
|
filterChainDefinitionMap.put("/druid/**", "anon");
|
||||||
filterChainDefinitionMap.put("/swagger-ui.html", "anon");
|
filterChainDefinitionMap.put("/swagger-ui.html", "anon");
|
||||||
@ -138,7 +137,9 @@ public class ShiroConfig {
|
|||||||
filterChainDefinitionMap.put("/webjars/**", "anon");
|
filterChainDefinitionMap.put("/webjars/**", "anon");
|
||||||
filterChainDefinitionMap.put("/v3/**", "anon");
|
filterChainDefinitionMap.put("/v3/**", "anon");
|
||||||
|
|
||||||
|
// update-begin--Author:sunjianlei Date:20210510 for:排除消息通告查看详情页面(用于第三方APP)
|
||||||
filterChainDefinitionMap.put("/sys/annountCement/show/**", "anon");
|
filterChainDefinitionMap.put("/sys/annountCement/show/**", "anon");
|
||||||
|
// update-end--Author:sunjianlei Date:20210510 for:排除消息通告查看详情页面(用于第三方APP)
|
||||||
|
|
||||||
//积木报表排除
|
//积木报表排除
|
||||||
filterChainDefinitionMap.put("/jmreport/**", "anon");
|
filterChainDefinitionMap.put("/jmreport/**", "anon");
|
||||||
@ -190,8 +191,6 @@ public class ShiroConfig {
|
|||||||
// 企业微信证书排除
|
// 企业微信证书排除
|
||||||
filterChainDefinitionMap.put("/WW_verify*", "anon");
|
filterChainDefinitionMap.put("/WW_verify*", "anon");
|
||||||
|
|
||||||
filterChainDefinitionMap.put("/openapi/call/**", "anon");
|
|
||||||
|
|
||||||
// 添加自己的过滤器并且取名为jwt
|
// 添加自己的过滤器并且取名为jwt
|
||||||
Map<String, Filter> filterMap = new HashMap<String, Filter>(1);
|
Map<String, Filter> filterMap = new HashMap<String, Filter>(1);
|
||||||
//如果cloudServer为空 则说明是单体 需要加载跨域配置【微服务跨域切换】
|
//如果cloudServer为空 则说明是单体 需要加载跨域配置【微服务跨域切换】
|
||||||
@ -208,6 +207,7 @@ public class ShiroConfig {
|
|||||||
return shiroFilterFactoryBean;
|
return shiroFilterFactoryBean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//update-begin---author:chenrui ---date:20240126 for:【QQYUN-7932】AI助手------------
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* spring过滤装饰器 <br/>
|
* spring过滤装饰器 <br/>
|
||||||
@ -223,24 +223,21 @@ public class ShiroConfig {
|
|||||||
FilterRegistrationBean registration = new FilterRegistrationBean();
|
FilterRegistrationBean registration = new FilterRegistrationBean();
|
||||||
registration.setFilter(new DelegatingFilterProxy("shiroFilterFactoryBean"));
|
registration.setFilter(new DelegatingFilterProxy("shiroFilterFactoryBean"));
|
||||||
registration.setEnabled(true);
|
registration.setEnabled(true);
|
||||||
// 代码逻辑说明: [issues/7491]运行耗时长,效率慢
|
//update-begin---author:chenrui ---date:20241202 for:[issues/7491]运行时间好长,效率慢 ------------
|
||||||
registration.addUrlPatterns("/test/ai/chat/send");
|
registration.addUrlPatterns("/test/ai/chat/send");
|
||||||
|
//update-end---author:chenrui ---date:20241202 for:[issues/7491]运行时间好长,效率慢 ------------
|
||||||
registration.addUrlPatterns("/airag/flow/run");
|
registration.addUrlPatterns("/airag/flow/run");
|
||||||
registration.addUrlPatterns("/airag/flow/debug");
|
registration.addUrlPatterns("/airag/flow/debug");
|
||||||
registration.addUrlPatterns("/airag/chat/send");
|
registration.addUrlPatterns("/airag/chat/send");
|
||||||
registration.addUrlPatterns("/airag/app/debug");
|
registration.addUrlPatterns("/airag/app/debug");
|
||||||
registration.addUrlPatterns("/airag/app/prompt/generate");
|
registration.addUrlPatterns("/airag/app/prompt/generate");
|
||||||
registration.addUrlPatterns("/airag/chat/receive/**");
|
registration.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.setAsyncSupported(true);
|
||||||
registration.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ASYNC);
|
registration.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ASYNC);
|
||||||
return registration;
|
return registration;
|
||||||
}
|
}
|
||||||
|
//update-end---author:chenrui ---date:20240126 for:【QQYUN-7932】AI助手------------
|
||||||
|
|
||||||
@Bean("securityManager")
|
@Bean("securityManager")
|
||||||
public DefaultWebSecurityManager securityManager(ShiroRealm myRealm) {
|
public DefaultWebSecurityManager securityManager(ShiroRealm myRealm) {
|
||||||
@ -361,23 +358,12 @@ public class ShiroConfig {
|
|||||||
JedisCluster jedisCluster = new JedisCluster(portSet);
|
JedisCluster jedisCluster = new JedisCluster(portSet);
|
||||||
redisManager.setJedisCluster(jedisCluster);
|
redisManager.setJedisCluster(jedisCluster);
|
||||||
}
|
}
|
||||||
|
//update-end--Author:scott Date:20210531 for:修改集群模式下未设置redis密码的bug issues/I3QNIC
|
||||||
manager = redisManager;
|
manager = redisManager;
|
||||||
}
|
}
|
||||||
return manager;
|
return manager;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 解决 ShiroRequestMappingConfig 获取 requestMappingHandlerMapping Bean 冲突
|
|
||||||
* spring-boot-autoconfigure:3.4.5 和 spring-boot-actuator-autoconfigure:3.4.5
|
|
||||||
*/
|
|
||||||
@Primary
|
|
||||||
@Bean
|
|
||||||
public RequestMappingHandlerMapping overridedRequestMappingHandlerMapping() {
|
|
||||||
RequestMappingHandlerMapping mapping = new RequestMappingHandlerMapping();
|
|
||||||
mapping.setUrlPathHelper(new ShiroUrlPathHelper());
|
|
||||||
return mapping;
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<String> rebuildUrl(String[] bases, String[] uris) {
|
private List<String> rebuildUrl(String[] bases, String[] uris) {
|
||||||
List<String> urls = new ArrayList<>();
|
List<String> urls = new ArrayList<>();
|
||||||
for (String base : bases) {
|
for (String base : bases) {
|
||||||
|
|||||||
@ -20,13 +20,11 @@ import org.jeecg.common.util.SpringContextUtils;
|
|||||||
import org.jeecg.common.util.TokenUtils;
|
import org.jeecg.common.util.TokenUtils;
|
||||||
import org.jeecg.common.util.oConvertUtils;
|
import org.jeecg.common.util.oConvertUtils;
|
||||||
import org.jeecg.config.mybatis.MybatisPlusSaasConfig;
|
import org.jeecg.config.mybatis.MybatisPlusSaasConfig;
|
||||||
import org.springframework.beans.factory.config.BeanDefinition;
|
|
||||||
import org.springframework.context.annotation.Lazy;
|
import org.springframework.context.annotation.Lazy;
|
||||||
import org.springframework.context.annotation.Role;
|
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
import jakarta.annotation.Resource;
|
import javax.annotation.Resource;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -37,7 +35,6 @@ import java.util.Set;
|
|||||||
*/
|
*/
|
||||||
@Component
|
@Component
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
|
||||||
public class ShiroRealm extends AuthorizingRealm {
|
public class ShiroRealm extends AuthorizingRealm {
|
||||||
@Lazy
|
@Lazy
|
||||||
@Resource
|
@Resource
|
||||||
@ -83,7 +80,7 @@ public class ShiroRealm extends AuthorizingRealm {
|
|||||||
Set<String> permissionSet = commonApi.queryUserAuths(userId);
|
Set<String> permissionSet = commonApi.queryUserAuths(userId);
|
||||||
info.addStringPermissions(permissionSet);
|
info.addStringPermissions(permissionSet);
|
||||||
//System.out.println(permissionSet);
|
//System.out.println(permissionSet);
|
||||||
log.debug("===============Shiro权限认证成功==============");
|
log.info("===============Shiro权限认证成功==============");
|
||||||
return info;
|
return info;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -110,8 +107,8 @@ public class ShiroRealm extends AuthorizingRealm {
|
|||||||
loginUser = this.checkUserTokenIsEffect(token);
|
loginUser = this.checkUserTokenIsEffect(token);
|
||||||
} catch (AuthenticationException e) {
|
} catch (AuthenticationException e) {
|
||||||
log.error("—————校验 check token 失败——————————"+ e.getMessage(), e);
|
log.error("—————校验 check token 失败——————————"+ e.getMessage(), e);
|
||||||
// 重新抛出异常,让JwtFilter统一处理,避免返回两次错误响应
|
JwtUtil.responseError(SpringContextUtils.getHttpServletResponse(),401,e.getMessage());
|
||||||
throw e;
|
return null;
|
||||||
}
|
}
|
||||||
return new SimpleAuthenticationInfo(loginUser, token, getName());
|
return new SimpleAuthenticationInfo(loginUser, token, getName());
|
||||||
}
|
}
|
||||||
@ -141,11 +138,9 @@ public class ShiroRealm extends AuthorizingRealm {
|
|||||||
}
|
}
|
||||||
// 校验token是否超时失效 & 或者账号密码是否错误
|
// 校验token是否超时失效 & 或者账号密码是否错误
|
||||||
if (!jwtTokenRefresh(token, username, loginUser.getPassword())) {
|
if (!jwtTokenRefresh(token, username, loginUser.getPassword())) {
|
||||||
// 用户登录Token过期提示信息
|
throw new AuthenticationException(CommonConstant.TOKEN_IS_INVALID_MSG);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
// 代码逻辑说明: 校验用户的tenant_id和前端传过来的是否一致
|
//update-begin-author:taoyan date:20210609 for:校验用户的tenant_id和前端传过来的是否一致
|
||||||
String userTenantIds = loginUser.getRelTenantIds();
|
String userTenantIds = loginUser.getRelTenantIds();
|
||||||
if(MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL && oConvertUtils.isNotEmpty(userTenantIds)){
|
if(MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL && oConvertUtils.isNotEmpty(userTenantIds)){
|
||||||
String contextTenantId = TenantContext.getTenant();
|
String contextTenantId = TenantContext.getTenant();
|
||||||
@ -154,7 +149,7 @@ public class ShiroRealm extends AuthorizingRealm {
|
|||||||
//登录用户无租户,前端header中租户ID值为 0
|
//登录用户无租户,前端header中租户ID值为 0
|
||||||
String str ="0";
|
String str ="0";
|
||||||
if(oConvertUtils.isNotEmpty(contextTenantId) && !str.equals(contextTenantId)){
|
if(oConvertUtils.isNotEmpty(contextTenantId) && !str.equals(contextTenantId)){
|
||||||
// 代码逻辑说明: /issues/I4O14W 用户租户信息变更判断漏洞
|
//update-begin-author:taoyan date:20211227 for: /issues/I4O14W 用户租户信息变更判断漏洞
|
||||||
String[] arr = userTenantIds.split(",");
|
String[] arr = userTenantIds.split(",");
|
||||||
if(!oConvertUtils.isIn(contextTenantId, arr)){
|
if(!oConvertUtils.isIn(contextTenantId, arr)){
|
||||||
boolean isAuthorization = false;
|
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;
|
return loginUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -202,22 +199,19 @@ public class ShiroRealm extends AuthorizingRealm {
|
|||||||
if (oConvertUtils.isNotEmpty(cacheToken)) {
|
if (oConvertUtils.isNotEmpty(cacheToken)) {
|
||||||
// 校验token有效性
|
// 校验token有效性
|
||||||
if (!JwtUtil.verify(cacheToken, userName, passWord)) {
|
if (!JwtUtil.verify(cacheToken, userName, passWord)) {
|
||||||
// 从token中解析客户端类型,保持续期时使用相同的客户端类型
|
String newAuthorization = JwtUtil.sign(userName, passWord);
|
||||||
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;
|
|
||||||
redisUtil.set(CommonConstant.PREFIX_USER_TOKEN + token, newAuthorization);
|
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);
|
log.debug("——————————用户在线操作,更新token保证不掉线—————————jwtTokenRefresh——————— "+ token);
|
||||||
}
|
}
|
||||||
|
//update-begin--Author:scott Date:20191005 for:解决每次请求,都重写redis中 token缓存问题
|
||||||
// else {
|
// else {
|
||||||
// // 设置超时时间
|
// // 设置超时时间
|
||||||
// redisUtil.set(CommonConstant.PREFIX_USER_TOKEN + token, cacheToken);
|
// redisUtil.set(CommonConstant.PREFIX_USER_TOKEN + token, cacheToken);
|
||||||
// redisUtil.expire(CommonConstant.PREFIX_USER_TOKEN + token, JwtUtil.EXPIRE_TIME / 1000);
|
// redisUtil.expire(CommonConstant.PREFIX_USER_TOKEN + token, JwtUtil.EXPIRE_TIME / 1000);
|
||||||
// }
|
// }
|
||||||
|
//update-end--Author:scott Date:20191005 for:解决每次请求,都重写redis中 token缓存问题
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -233,7 +227,8 @@ public class ShiroRealm extends AuthorizingRealm {
|
|||||||
@Override
|
@Override
|
||||||
public void clearCache(PrincipalCollection principals) {
|
public void clearCache(PrincipalCollection principals) {
|
||||||
super.clearCache(principals);
|
super.clearCache(principals);
|
||||||
// 代码逻辑说明: 【TV360X-1320】分配权限必须退出重新登录才生效,造成很多用户困扰---
|
//update-begin---author:scott ---date::2024-06-18 for:【TV360X-1320】分配权限必须退出重新登录才生效,造成很多用户困扰---
|
||||||
super.clearCachedAuthorizationInfo(principals);
|
super.clearCachedAuthorizationInfo(principals);
|
||||||
|
//update-end---author:scott ---date::2024-06-18 for:【TV360X-1320】分配权限必须退出重新登录才生效,造成很多用户困扰---
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,7 +12,7 @@ import org.apache.shiro.web.servlet.AbstractShiroFilter;
|
|||||||
import org.apache.shiro.mgt.SecurityManager;
|
import org.apache.shiro.mgt.SecurityManager;
|
||||||
import org.springframework.beans.factory.BeanInitializationException;
|
import org.springframework.beans.factory.BeanInitializationException;
|
||||||
|
|
||||||
import jakarta.servlet.Filter;
|
import javax.servlet.Filter;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -13,10 +13,10 @@ import org.springframework.http.HttpHeaders;
|
|||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.web.bind.annotation.RequestMethod;
|
import org.springframework.web.bind.annotation.RequestMethod;
|
||||||
|
|
||||||
import jakarta.servlet.ServletRequest;
|
import javax.servlet.ServletRequest;
|
||||||
import jakarta.servlet.ServletResponse;
|
import javax.servlet.ServletResponse;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @Description: 鉴权登录拦截器
|
* @Description: 鉴权登录拦截器
|
||||||
@ -56,13 +56,9 @@ public class JwtFilter extends BasicHttpAuthenticationFilter {
|
|||||||
executeLogin(request, response);
|
executeLogin(request, response);
|
||||||
return true;
|
return true;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
// 使用异常中的具体错误信息,保留"不允许同一账号多地同时登录"等具体提示
|
JwtUtil.responseError((HttpServletResponse)response,401,CommonConstant.TOKEN_IS_INVALID_MSG);
|
||||||
String errorMsg = e.getMessage();
|
|
||||||
if (oConvertUtils.isEmpty(errorMsg)) {
|
|
||||||
errorMsg = CommonConstant.TOKEN_IS_INVALID_MSG;
|
|
||||||
}
|
|
||||||
JwtUtil.responseError((HttpServletResponse)response, 401, errorMsg);
|
|
||||||
return false;
|
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 {
|
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
|
||||||
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
|
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
|
||||||
String token = httpServletRequest.getHeader(CommonConstant.X_ACCESS_TOKEN);
|
String token = httpServletRequest.getHeader(CommonConstant.X_ACCESS_TOKEN);
|
||||||
// 代码逻辑说明: JT-355 OA聊天添加token验证,获取token参数
|
// update-begin--Author:lvdandan Date:20210105 for:JT-355 OA聊天添加token验证,获取token参数
|
||||||
if (oConvertUtils.isEmpty(token)) {
|
if (oConvertUtils.isEmpty(token)) {
|
||||||
token = httpServletRequest.getParameter("token");
|
token = httpServletRequest.getParameter("token");
|
||||||
}
|
}
|
||||||
|
// update-end--Author:lvdandan Date:20210105 for:JT-355 OA聊天添加token验证,获取token参数
|
||||||
|
|
||||||
JwtToken jwtToken = new JwtToken(token);
|
JwtToken jwtToken = new JwtToken(token);
|
||||||
// 提交给realm进行登入,如果错误他会抛出异常并被捕获
|
// 提交给realm进行登入,如果错误他会抛出异常并被捕获
|
||||||
@ -109,9 +106,10 @@ public class JwtFilter extends BasicHttpAuthenticationFilter {
|
|||||||
httpServletResponse.setStatus(HttpStatus.OK.value());
|
httpServletResponse.setStatus(HttpStatus.OK.value());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// 代码逻辑说明: 多租户用到
|
//update-begin-author:taoyan date:20200708 for:多租户用到
|
||||||
String tenantId = httpServletRequest.getHeader(CommonConstant.TENANT_ID);
|
String tenantId = httpServletRequest.getHeader(CommonConstant.TENANT_ID);
|
||||||
TenantContext.setTenant(tenantId);
|
TenantContext.setTenant(tenantId);
|
||||||
|
//update-end-author:taoyan date:20200708 for:多租户用到
|
||||||
|
|
||||||
return super.preHandle(request, response);
|
return super.preHandle(request, response);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
package org.jeecg.config.shiro.filters;
|
package org.jeecg.config.shiro.filters;
|
||||||
|
|
||||||
import jakarta.servlet.ServletRequest;
|
import javax.servlet.ServletRequest;
|
||||||
import jakarta.servlet.ServletResponse;
|
import javax.servlet.ServletResponse;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import javax.servlet.http.HttpServletResponse;
|
||||||
import org.apache.shiro.subject.Subject;
|
import org.apache.shiro.subject.Subject;
|
||||||
import org.apache.shiro.web.filter.AccessControlFilter;
|
import org.apache.shiro.web.filter.AccessControlFilter;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|||||||
@ -4,7 +4,6 @@ import lombok.AllArgsConstructor;
|
|||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.jeecg.config.shiro.IgnoreAuth;
|
import org.jeecg.config.shiro.IgnoreAuth;
|
||||||
import org.springframework.beans.factory.InitializingBean;
|
import org.springframework.beans.factory.InitializingBean;
|
||||||
import org.springframework.context.annotation.Lazy;
|
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.util.CollectionUtils;
|
import org.springframework.util.CollectionUtils;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
@ -21,7 +20,6 @@ import java.util.stream.Collectors;
|
|||||||
* @date 2024/4/18 11:35
|
* @date 2024/4/18 11:35
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Lazy(false)
|
|
||||||
@Component
|
@Component
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
public class IgnoreAuthPostProcessor implements InitializingBean {
|
public class IgnoreAuthPostProcessor implements InitializingBean {
|
||||||
@ -35,15 +33,10 @@ public class IgnoreAuthPostProcessor implements InitializingBean {
|
|||||||
long startTime = System.currentTimeMillis();
|
long startTime = System.currentTimeMillis();
|
||||||
|
|
||||||
List<String> ignoreAuthUrls = new ArrayList<>();
|
List<String> ignoreAuthUrls = new ArrayList<>();
|
||||||
|
Set<Class<?>> restControllers = requestMappingHandlerMapping.getHandlerMethods().values().stream().map(HandlerMethod::getBeanType).collect(Collectors.toSet());
|
||||||
// 优化:直接从HandlerMethod过滤,避免重复扫描
|
for (Class<?> restController : restControllers) {
|
||||||
requestMappingHandlerMapping.getHandlerMethods().values().stream()
|
ignoreAuthUrls.addAll(postProcessRestController(restController));
|
||||||
.filter(handlerMethod -> handlerMethod.getMethod().isAnnotationPresent(IgnoreAuth.class))
|
}
|
||||||
.forEach(handlerMethod -> {
|
|
||||||
Class<?> clazz = handlerMethod.getBeanType();
|
|
||||||
Method method = handlerMethod.getMethod();
|
|
||||||
ignoreAuthUrls.addAll(processIgnoreAuthMethod(clazz, method));
|
|
||||||
});
|
|
||||||
|
|
||||||
log.info("Init Token ignoreAuthUrls Config [ 集合 ] :{}", ignoreAuthUrls);
|
log.info("Init Token ignoreAuthUrls Config [ 集合 ] :{}", ignoreAuthUrls);
|
||||||
if (!CollectionUtils.isEmpty(ignoreAuthUrls)) {
|
if (!CollectionUtils.isEmpty(ignoreAuthUrls)) {
|
||||||
@ -53,30 +46,44 @@ public class IgnoreAuthPostProcessor implements InitializingBean {
|
|||||||
// 计算方法的耗时
|
// 计算方法的耗时
|
||||||
long endTime = System.currentTimeMillis();
|
long endTime = System.currentTimeMillis();
|
||||||
long elapsedTime = endTime - startTime;
|
long elapsedTime = endTime - startTime;
|
||||||
log.info("Init Token ignoreAuthUrls Config [ 耗时 ] :" + elapsedTime + "ms");
|
log.info("Init Token ignoreAuthUrls Config [ 耗时 ] :" + elapsedTime + "毫秒");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 优化:新方法处理单个@IgnoreAuth方法,减少重复注解检查
|
private List<String> postProcessRestController(Class<?> clazz) {
|
||||||
private List<String> processIgnoreAuthMethod(Class<?> clazz, Method method) {
|
List<String> ignoreAuthUrls = new ArrayList<>();
|
||||||
RequestMapping base = clazz.getAnnotation(RequestMapping.class);
|
RequestMapping base = clazz.getAnnotation(RequestMapping.class);
|
||||||
String[] baseUrl = Objects.nonNull(base) ? base.value() : new String[]{};
|
String[] baseUrl = Objects.nonNull(base) ? base.value() : new String[]{};
|
||||||
|
Method[] methods = clazz.getDeclaredMethods();
|
||||||
String[] uri = null;
|
|
||||||
if (method.isAnnotationPresent(RequestMapping.class)) {
|
for (Method method : methods) {
|
||||||
uri = method.getAnnotation(RequestMapping.class).value();
|
if (method.isAnnotationPresent(IgnoreAuth.class) && method.isAnnotationPresent(RequestMapping.class)) {
|
||||||
} else if (method.isAnnotationPresent(GetMapping.class)) {
|
RequestMapping requestMapping = method.getAnnotation(RequestMapping.class);
|
||||||
uri = method.getAnnotation(GetMapping.class).value();
|
String[] uri = requestMapping.value();
|
||||||
} else if (method.isAnnotationPresent(PostMapping.class)) {
|
ignoreAuthUrls.addAll(rebuildUrl(baseUrl, uri));
|
||||||
uri = method.getAnnotation(PostMapping.class).value();
|
} else if (method.isAnnotationPresent(IgnoreAuth.class) && method.isAnnotationPresent(GetMapping.class)) {
|
||||||
} else if (method.isAnnotationPresent(PutMapping.class)) {
|
GetMapping requestMapping = method.getAnnotation(GetMapping.class);
|
||||||
uri = method.getAnnotation(PutMapping.class).value();
|
String[] uri = requestMapping.value();
|
||||||
} else if (method.isAnnotationPresent(DeleteMapping.class)) {
|
ignoreAuthUrls.addAll(rebuildUrl(baseUrl, uri));
|
||||||
uri = method.getAnnotation(DeleteMapping.class).value();
|
} else if (method.isAnnotationPresent(IgnoreAuth.class) && method.isAnnotationPresent(PostMapping.class)) {
|
||||||
} else if (method.isAnnotationPresent(PatchMapping.class)) {
|
PostMapping requestMapping = method.getAnnotation(PostMapping.class);
|
||||||
uri = method.getAnnotation(PatchMapping.class).value();
|
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) {
|
private List<String> rebuildUrl(String[] bases, String[] uris) {
|
||||||
|
|||||||
@ -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签名校验失败!";
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,7 +1,6 @@
|
|||||||
package org.jeecg.config.sign.interceptor;
|
package org.jeecg.config.sign.interceptor;
|
||||||
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.jeecg.common.constant.TenantConstant;
|
|
||||||
import org.jeecg.common.util.PathMatcherUtil;
|
import org.jeecg.common.util.PathMatcherUtil;
|
||||||
import org.jeecg.config.JeecgBaseConfig;
|
import org.jeecg.config.JeecgBaseConfig;
|
||||||
import org.jeecg.config.filter.RequestBodyReserveFilter;
|
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.InterceptorRegistry;
|
||||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||||
|
|
||||||
import jakarta.annotation.Resource;
|
import javax.annotation.Resource;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 签名 拦截器配置
|
* 签名 拦截器配置
|
||||||
@ -42,7 +41,7 @@ public class SignAuthConfiguration implements WebMvcConfigurer {
|
|||||||
registry.addInterceptor(signAuthInterceptor()).addPathPatterns(signUrlsArray);
|
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
|
@Bean
|
||||||
public RequestBodyReserveFilter requestBodyReserveFilter(){
|
public RequestBodyReserveFilter requestBodyReserveFilter(){
|
||||||
return new RequestBodyReserveFilter();
|
return new RequestBodyReserveFilter();
|
||||||
@ -65,9 +64,8 @@ public class SignAuthConfiguration implements WebMvcConfigurer {
|
|||||||
//------------------------------------------------------------
|
//------------------------------------------------------------
|
||||||
// 建议此处只添加post请求地址而不是所有的都需要走过滤器
|
// 建议此处只添加post请求地址而不是所有的都需要走过滤器
|
||||||
registration.addUrlPatterns(signUrlsArray);
|
registration.addUrlPatterns(signUrlsArray);
|
||||||
// 增加注解签名请求
|
|
||||||
registration.addUrlPatterns(TenantConstant.SIGNATURE_CHECK_POST_URL);
|
|
||||||
return registration;
|
return registration;
|
||||||
}
|
}
|
||||||
|
//update-end-author:taoyan date:20220427 for: issues/I53J5E post请求X_SIGN签名拦截校验后报错, request body 为空
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,8 +4,8 @@ package org.jeecg.config.sign.interceptor;
|
|||||||
import java.io.PrintWriter;
|
import java.io.PrintWriter;
|
||||||
import java.util.SortedMap;
|
import java.util.SortedMap;
|
||||||
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
import org.jeecg.common.api.vo.Result;
|
import org.jeecg.common.api.vo.Result;
|
||||||
import org.jeecg.common.constant.CommonConstant;
|
import org.jeecg.common.constant.CommonConstant;
|
||||||
@ -33,104 +33,63 @@ public class SignAuthInterceptor implements HandlerInterceptor {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
|
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 {
|
if(oConvertUtils.isEmpty(xTimestamp)){
|
||||||
// 调用验证逻辑
|
Result<?> result = Result.error("Sign签名校验失败,时间戳为空!");
|
||||||
validateSignature(request);
|
log.error("Sign 签名校验失败!Header xTimestamp 为空");
|
||||||
return true;
|
//校验失败返回前端
|
||||||
} catch (IllegalArgumentException e) {
|
|
||||||
// 验证失败,返回错误响应
|
|
||||||
log.error("Sign 签名校验失败!{}", e.getMessage());
|
|
||||||
response.setCharacterEncoding("UTF-8");
|
response.setCharacterEncoding("UTF-8");
|
||||||
response.setContentType("application/json; charset=utf-8");
|
response.setContentType("application/json; charset=utf-8");
|
||||||
PrintWriter out = response.getWriter();
|
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));
|
out.print(JSON.toJSON(result));
|
||||||
return false;
|
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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
package org.jeecg.config.sign.util;
|
package org.jeecg.config.sign.util;
|
||||||
|
|
||||||
import jakarta.servlet.ReadListener;
|
import javax.servlet.ReadListener;
|
||||||
import jakarta.servlet.ServletInputStream;
|
import javax.servlet.ServletInputStream;
|
||||||
import jakarta.servlet.ServletRequest;
|
import javax.servlet.ServletRequest;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
import jakarta.servlet.http.HttpServletRequestWrapper;
|
import javax.servlet.http.HttpServletRequestWrapper;
|
||||||
import java.io.*;
|
import java.io.*;
|
||||||
import java.nio.charset.Charset;
|
import java.nio.charset.Charset;
|
||||||
|
|
||||||
|
|||||||
@ -10,7 +10,7 @@ import java.util.Map;
|
|||||||
import java.util.SortedMap;
|
import java.util.SortedMap;
|
||||||
import java.util.TreeMap;
|
import java.util.TreeMap;
|
||||||
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.jeecg.common.constant.SymbolConstant;
|
import org.jeecg.common.constant.SymbolConstant;
|
||||||
@ -35,25 +35,25 @@ public class HttpUtils {
|
|||||||
* @date 20210621
|
* @date 20210621
|
||||||
* @param request
|
* @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<>();
|
SortedMap<String, String> result = new TreeMap<>();
|
||||||
// 获取URL上最后带逗号的参数变量 sys/dict/getDictItems/sys_user,realname,username
|
// 获取URL上最后带逗号的参数变量 sys/dict/getDictItems/sys_user,realname,username
|
||||||
String pathVariable = request.getRequestURI().substring(request.getRequestURI().lastIndexOf("/") + 1);
|
String pathVariable = request.getRequestURI().substring(request.getRequestURI().lastIndexOf("/") + 1);
|
||||||
if (pathVariable.contains(SymbolConstant.COMMA)) {
|
if (pathVariable.contains(SymbolConstant.COMMA)) {
|
||||||
log.debug(" pathVariable: {}",pathVariable);
|
log.info(" pathVariable: {}",pathVariable);
|
||||||
String deString = URLDecoder.decode(pathVariable, "UTF-8");
|
String deString = URLDecoder.decode(pathVariable, "UTF-8");
|
||||||
|
|
||||||
//https://www.52dianzi.com/category/article/37/565371.html
|
//https://www.52dianzi.com/category/article/37/565371.html
|
||||||
if(deString.contains("%")){
|
if(deString.contains("%")){
|
||||||
try {
|
try {
|
||||||
deString = URLDecoder.decode(deString, "UTF-8");
|
deString = URLDecoder.decode(deString, "UTF-8");
|
||||||
log.debug("存在%情况下,执行两次解码 — pathVariable decode: {}",deString);
|
log.info("存在%情况下,执行两次解码 — pathVariable decode: {}",deString);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
//e.printStackTrace();
|
//e.printStackTrace();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
log.debug(" pathVariable decode: {}",deString);
|
log.info(" pathVariable decode: {}",deString);
|
||||||
result.put(SignUtil.X_PATH_VARIABLE, deString);
|
result.put(SignUtil.X_PATH_VARIABLE, deString);
|
||||||
}
|
}
|
||||||
// 获取URL上的参数
|
// 获取URL上的参数
|
||||||
@ -65,13 +65,7 @@ public class HttpUtils {
|
|||||||
Map<String, String> allRequestParam = new HashMap<>(16);
|
Map<String, String> allRequestParam = new HashMap<>(16);
|
||||||
// get请求不需要拿body参数
|
// get请求不需要拿body参数
|
||||||
if (!HttpMethod.GET.name().equals(request.getMethod())) {
|
if (!HttpMethod.GET.name().equals(request.getMethod())) {
|
||||||
if (bodyParam != null) {
|
allRequestParam = getAllRequestParam(request);
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// 将URL的参数和body参数进行合并
|
// 将URL的参数和body参数进行合并
|
||||||
if (allRequestParam != null) {
|
if (allRequestParam != null) {
|
||||||
@ -97,15 +91,15 @@ public class HttpUtils {
|
|||||||
// 获取URL上最后带逗号的参数变量 sys/dict/getDictItems/sys_user,realname,username
|
// 获取URL上最后带逗号的参数变量 sys/dict/getDictItems/sys_user,realname,username
|
||||||
String pathVariable = url.substring(url.lastIndexOf("/") + 1);
|
String pathVariable = url.substring(url.lastIndexOf("/") + 1);
|
||||||
if (pathVariable.contains(SymbolConstant.COMMA)) {
|
if (pathVariable.contains(SymbolConstant.COMMA)) {
|
||||||
log.debug(" pathVariable: {}",pathVariable);
|
log.info(" pathVariable: {}",pathVariable);
|
||||||
String deString = URLDecoder.decode(pathVariable, "UTF-8");
|
String deString = URLDecoder.decode(pathVariable, "UTF-8");
|
||||||
|
|
||||||
//https://www.52dianzi.com/category/article/37/565371.html
|
//https://www.52dianzi.com/category/article/37/565371.html
|
||||||
if(deString.contains("%")){
|
if(deString.contains("%")){
|
||||||
deString = URLDecoder.decode(deString, "UTF-8");
|
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);
|
result.put(SignUtil.X_PATH_VARIABLE, deString);
|
||||||
}
|
}
|
||||||
// 获取URL上的参数
|
// 获取URL上的参数
|
||||||
@ -180,10 +174,11 @@ public class HttpUtils {
|
|||||||
String[] params = param.split("&");
|
String[] params = param.split("&");
|
||||||
for (String s : params) {
|
for (String s : params) {
|
||||||
int index = s.indexOf("=");
|
int index = s.indexOf("=");
|
||||||
// 代码逻辑说明: [issues/5879]数据查询传ds=“”造成的异常------------
|
//update-begin---author:chenrui ---date:20240222 for:[issues/5879]数据查询传ds=“”造成的异常------------
|
||||||
if (index != -1) {
|
if (index != -1) {
|
||||||
result.put(s.substring(0, index), s.substring(index + 1));
|
result.put(s.substring(0, index), s.substring(index + 1));
|
||||||
}
|
}
|
||||||
|
//update-end---author:chenrui ---date:20240222 for:[issues/5879]数据查询传ds=“”造成的异常------------
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@ -207,10 +202,11 @@ public class HttpUtils {
|
|||||||
String[] params = param.split("&");
|
String[] params = param.split("&");
|
||||||
for (String s : params) {
|
for (String s : params) {
|
||||||
int index = s.indexOf("=");
|
int index = s.indexOf("=");
|
||||||
// 代码逻辑说明: [issues/5879]数据查询传ds=“”造成的异常------------
|
//update-begin---author:chenrui ---date:20240222 for:[issues/5879]数据查询传ds=“”造成的异常------------
|
||||||
if (index != -1) {
|
if (index != -1) {
|
||||||
result.put(s.substring(0, index), s.substring(index + 1));
|
result.put(s.substring(0, index), s.substring(index + 1));
|
||||||
}
|
}
|
||||||
|
//update-end---author:chenrui ---date:20240222 for:[issues/5879]数据查询传ds=“”造成的异常------------
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,12 +11,7 @@ import org.springframework.util.DigestUtils;
|
|||||||
import org.springframework.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
import java.io.UnsupportedEncodingException;
|
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.SortedMap;
|
||||||
import java.util.TreeMap;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 签名工具类
|
* 签名工具类
|
||||||
@ -39,7 +34,7 @@ public class SignUtil {
|
|||||||
}
|
}
|
||||||
// 把参数加密
|
// 把参数加密
|
||||||
String paramsSign = getParamsSign(params);
|
String paramsSign = getParamsSign(params);
|
||||||
log.debug("Param Sign : {}", paramsSign);
|
log.info("Param Sign : {}", paramsSign);
|
||||||
return !StringUtils.isEmpty(paramsSign) && headerSign.equals(paramsSign);
|
return !StringUtils.isEmpty(paramsSign) && headerSign.equals(paramsSign);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -52,9 +47,14 @@ public class SignUtil {
|
|||||||
//去掉 Url 里的时间戳
|
//去掉 Url 里的时间戳
|
||||||
params.remove("_t");
|
params.remove("_t");
|
||||||
String paramsJsonStr = JSONObject.toJSONString(params);
|
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 {
|
try {
|
||||||
//【issues/I484RW】2.4.6部署后,下拉搜索框提示“sign签名检验失败”
|
//【issues/I484RW】2.4.6部署后,下拉搜索框提示“sign签名检验失败”
|
||||||
return DigestUtils.md5DigestAsHex((paramsJsonStr + signatureSecret).getBytes("UTF-8")).toUpperCase();
|
return DigestUtils.md5DigestAsHex((paramsJsonStr + signatureSecret).getBytes("UTF-8")).toUpperCase();
|
||||||
@ -63,129 +63,4 @@ public class SignUtil {
|
|||||||
return null;
|
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -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;
|
|
||||||
}
|
|
||||||
@ -19,42 +19,11 @@ public class Firewall {
|
|||||||
* 低代码模式(dev:开发模式,prod:发布模式——关闭所有在线开发配置能力)
|
* 低代码模式(dev:开发模式,prod:发布模式——关闭所有在线开发配置能力)
|
||||||
*/
|
*/
|
||||||
private String lowCodeMode;
|
private String lowCodeMode;
|
||||||
/**
|
|
||||||
* 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录)
|
|
||||||
*/
|
|
||||||
private Boolean isConcurrent = true;
|
|
||||||
/**
|
|
||||||
* 是否开启默认密码登录提醒(true 登录后提示必须修改默认密码)
|
|
||||||
*/
|
|
||||||
private Boolean enableDefaultPwdCheck = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 是否开启登录验证码校验(true 开启;false 关闭并跳过验证码逻辑)
|
|
||||||
*/
|
|
||||||
private Boolean enableLoginCaptcha = true;
|
|
||||||
|
|
||||||
// /**
|
// /**
|
||||||
// * 表字典安全模式(white:白名单——配置了白名单的表才能通过表字典方式访问,black:黑名单——配置了黑名单的表不允许表字典方式访问)
|
// * 表字典安全模式(white:白名单——配置了白名单的表才能通过表字典方式访问,black:黑名单——配置了黑名单的表不允许表字典方式访问)
|
||||||
// */
|
// */
|
||||||
// private String tableDictMode;
|
// 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() {
|
public Boolean getDataSourceSafe() {
|
||||||
return dataSourceSafe;
|
return dataSourceSafe;
|
||||||
}
|
}
|
||||||
@ -78,12 +47,4 @@ public class Firewall {
|
|||||||
public void setDisableSelectAll(Boolean disableSelectAll) {
|
public void setDisableSelectAll(Boolean disableSelectAll) {
|
||||||
this.disableSelectAll = disableSelectAll;
|
this.disableSelectAll = disableSelectAll;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Boolean getIsConcurrent() {
|
|
||||||
return isConcurrent;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setIsConcurrent(Boolean isConcurrent) {
|
|
||||||
this.isConcurrent = isConcurrent;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,11 +0,0 @@
|
|||||||
package org.jeecg.config.vo;
|
|
||||||
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
public class JeecgMinio {
|
|
||||||
|
|
||||||
private String minio_url;
|
|
||||||
private String bucketName;
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,11 +0,0 @@
|
|||||||
package org.jeecg.config.vo;
|
|
||||||
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
public class JeecgOSS {
|
|
||||||
|
|
||||||
private String endpoint;
|
|
||||||
private String bucketName;
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -14,8 +14,8 @@ import org.jeecg.common.util.SpringContextUtils;
|
|||||||
import org.jeecg.common.util.oConvertUtils;
|
import org.jeecg.common.util.oConvertUtils;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import jakarta.annotation.Resource;
|
import javax.annotation.Resource;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -1 +0,0 @@
|
|||||||
org.jeecg.config.DruidWallConfigRegister
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 487 B |
@ -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()
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user