mirror of
https://github.com/jeecgboot/JeecgBoot.git
synced 2026-01-04 04:45:28 +08:00
Compare commits
109 Commits
aa55c3a86c
...
springboot
| Author | SHA1 | Date | |
|---|---|---|---|
| 3e107d766e | |||
| cac4121209 | |||
| 26087172df | |||
| 281c3ff3c8 | |||
| 38d44c2487 | |||
| 8c88f8adf5 | |||
| 526734c5a5 | |||
| 44b48ad916 | |||
| 1a3ae4f61c | |||
| 859c509f08 | |||
| 0704f187af | |||
| 199d2b439e | |||
| 5f898ed034 | |||
| 5a9cb05c86 | |||
| 98936680d5 | |||
| fc043fd5f3 | |||
| 54531002a7 | |||
| 728a95c00d | |||
| 13c9951c1f | |||
| 8dfaa3c3e1 | |||
| 050c478dce | |||
| 2688e8b6e2 | |||
| a9f30f0ca5 | |||
| 1bf4a0595a | |||
| 74cd57fd99 | |||
| c9ac4c9945 | |||
| 3b3371ee1a | |||
| 1d3bde9fe7 | |||
| 90b50a51a7 | |||
| 668ac59a5c | |||
| 5f01bdd29b | |||
| 82bfcc7b14 | |||
| ef210a2242 | |||
| 435445cb4e | |||
| fdf7cd1f6b | |||
| 92a38f41b0 | |||
| 7ed3bcc912 | |||
| 2706c0a519 | |||
| f4b80365a9 | |||
| 1108aa5288 | |||
| 9919ae2bc5 | |||
| 1f73837b7d | |||
| 9571e0b169 | |||
| 1a923596db | |||
| 62549e0a1c | |||
| 2740a2f419 | |||
| 899264250c | |||
| 0be7d00eb2 | |||
| 3599120c94 | |||
| 7152ae9e49 | |||
| 58b41db786 | |||
| cf9b407a18 | |||
| d715c7a0ac | |||
| aca407e1ce | |||
| cfea79a187 | |||
| a194d4e9b2 | |||
| 7848d1fb33 | |||
| 91fa645878 | |||
| 21585e4d25 | |||
| c9fc948658 | |||
| b97d041e7f | |||
| 0489d30296 | |||
| ed87ac3bff | |||
| 761dbf0343 | |||
| 23c628057b | |||
| 2ac14709ba | |||
| f9cff08716 | |||
| 6492f2c99a | |||
| bf32385a06 | |||
| a6feb2fd9d | |||
| 6ef637c46f | |||
| b84eb25d41 | |||
| 4326cecad4 | |||
| ec5810176b | |||
| aff307c3ff | |||
| acfd3bb3e4 | |||
| 52082fb256 | |||
| 736515f63a | |||
| a250163198 | |||
| 1ed1f315a4 | |||
| f7670dca3a | |||
| b24ac544c8 | |||
| c7c31e0945 | |||
| 468af57489 | |||
| c85bb1f62d | |||
| b4fa11a605 | |||
| b2240848e0 | |||
| 4a888a4e19 | |||
| bc6f336745 | |||
| 0d86df8e9e | |||
| 3db673b67d | |||
| 3ba5395d33 | |||
| e7eed37470 | |||
| 30ac3f7c72 | |||
| 03e6c97d80 | |||
| b9f6f6dc53 | |||
| 107e13c8af | |||
| 0512b41b2b | |||
| d6d880f887 | |||
| b0e974a418 | |||
| 388fa9b8c2 | |||
| bc04bd1433 | |||
| 35aba0784d | |||
| c3822ab702 | |||
| d4487356f0 | |||
| ae4363dc72 | |||
| 3e6c7651ee | |||
| c0ffd14b7a | |||
| 914875d6a1 |
3
.github/ISSUE_TEMPLATE/bug_report.md
vendored
3
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -10,6 +10,9 @@ assignees: getActivity
|
|||||||
##### 版本号:
|
##### 版本号:
|
||||||
|
|
||||||
|
|
||||||
|
##### 分支:
|
||||||
|
|
||||||
|
|
||||||
##### 问题描述:
|
##### 问题描述:
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
4
.github/ISSUE_TEMPLATE/feature_request.md
vendored
4
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@ -6,10 +6,12 @@ assignees: getActivity
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
||||||
##### 版本号:
|
##### 版本号:
|
||||||
|
|
||||||
|
|
||||||
|
##### 分支:
|
||||||
|
|
||||||
|
|
||||||
##### 问题描述:
|
##### 问题描述:
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
2
.gitignore
vendored
2
.gitignore
vendored
@ -13,3 +13,5 @@ os_del.cmd
|
|||||||
os_del_doc.cmd
|
os_del_doc.cmd
|
||||||
.svn
|
.svn
|
||||||
derby.log
|
derby.log
|
||||||
|
.cursor
|
||||||
|
.history
|
||||||
@ -3,9 +3,6 @@ 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服务能力。
|
||||||
|
|
||||||
|
|||||||
@ -7,12 +7,12 @@
|
|||||||
JEECG BOOT AI Low Code Platform
|
JEECG BOOT AI Low Code Platform
|
||||||
===============
|
===============
|
||||||
|
|
||||||
Current version: 3.8.3 (Release date: 2025-10-09)
|
Current version: 3.9.0 (Release date: 2025-12-01)
|
||||||
|
|
||||||
|
|
||||||
[](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)
|
||||||
|
|
||||||
|
|||||||
@ -1,124 +0,0 @@
|
|||||||
|
|
||||||
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字段建议
|
|
||||||
│ ├─设计表单视图功能(支持多种类型含日历、表格、看板、甘特图)
|
|
||||||
│ └─。。。
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
##### 流程设计
|
|
||||||

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

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

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

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

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

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

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

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

|
|
||||||
42
README.md
42
README.md
@ -2,13 +2,13 @@
|
|||||||
JeecgBoot AI低代码平台
|
JeecgBoot AI低代码平台
|
||||||
===============
|
===============
|
||||||
|
|
||||||
当前最新版本: 3.8.3(发布日期:2025-10-09)
|
当前最新版本: 3.9.0(发布日期:2025-12-01)
|
||||||
|
|
||||||
|
|
||||||
[](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)
|
||||||
|
|
||||||
@ -19,14 +19,17 @@ JeecgBoot AI低代码平台
|
|||||||
|
|
||||||
<h3 align="center">企业级AI低代码平台</h3>
|
<h3 align="center">企业级AI低代码平台</h3>
|
||||||
|
|
||||||
JeecgBoot 是一款基于BPM流程和代码生成的AI低代码平台,助力企业快速实现低代码开发和构建AI应用。
|
JeecgBoot 是一款融合代码生成与AI应用的低代码开发平台,助力企业快速实现低代码开发和构建AI应用。平台支持MCP和插件扩展,提供聊天式业务操作(如“一句话创建用户”),大幅提升开发效率与用户便捷性。
|
||||||
|
|
||||||
采用前后端分离架构(Ant Design&Vue3,SpringBoot3,SpringCloud Alibaba,Mybatis-plus),强大代码生成器实现前后端一键生成,无需手写代码。
|
采用前后端分离架构(Ant Design&Vue3,SpringBoot3,SpringCloud Alibaba,Mybatis-plus),强大代码生成器实现前后端一键生成,无需手写代码。
|
||||||
平台引领AI低代码开发模式:AI生成→在线编码→代码生成→手工合并,解决Java项目80%重复工作,提升效率,节省成本,兼顾灵活性。
|
平台引领AI低代码开发模式:AI生成→在线编码→代码生成→手工合并,解决Java项目80%重复工作,提升效率,节省成本,兼顾灵活性。
|
||||||
具备强大且颗粒化的权限控制,支持按钮权限和数据权限设置,满足大型业务系统需求。功能涵盖在线表单、表单设计、流程设计、门户设计、报表与大屏设计、OA办公、AI应用、AI知识库、大模型管理、AI流程编排、AI聊天,支持ChatGPT、DeepSeek、Ollama等多种AI大模型。
|
具备强大且颗粒化的权限控制,支持按钮权限和数据权限设置,满足大型业务系统需求。功能涵盖在线表单、表单设计、流程设计、门户设计、报表与大屏设计、OA办公、AI应用、AI知识库、大模型管理、AI流程编排、AI聊天,支持ChatGPT、DeepSeek、Ollama等多种AI大模型。
|
||||||
|
|
||||||
`AI赋能报表:` 积木报表是一款自主研发的强大开源企业级Web报表与大屏工具。它通过零编码的拖拽式操作,赋能用户如同搭积木般轻松构建各类复杂报表和数据大屏,全面满足企业数据可视化与分析需求,助力企业级数据产品的高效打造与应用。
|
`傻瓜式报表:` JimuReport是一款自主研发的强大开源企业级Web报表工具。它通过零编码的拖拽式操作,赋能用户如同搭积木般轻松构建各类复杂报表,全面满足企业数据可视化与分析需求,助力企业级数据产品的高效打造与应用。
|
||||||
|
|
||||||
`AI赋能低代码:` 提供完善成熟的AI应用平台,涵盖AI应用管理、AI模型管理、智能对话助手、知识库问答、流程编排与设计器、AI建表等多项功能。平台兼容多种主流大模型,包括ChatGPT、DeepSeek、Ollama、智普、千问等,助力企业高效构建智能化应用,推动低代码开发与AI深度融合。
|
`傻瓜式大屏:` JimuBI一款自主研发的强大的大屏和仪表盘设计工具。专注数字孪生与数据可视化,支持交互式大屏、仪表盘、门户和移动端,实现“一次开发,多端适配”。 大屏设计类Word风格,支持多屏切换,自由拖拽,轻松打造炫酷动态界面。
|
||||||
|
|
||||||
|
`成熟AI应用功能:` 提供一套完善AI应用平台: 涵盖AI应用管理、AI模型管理、智能对话助手、知识库问答、流程编排与设计器、AI建表、MCP插件配置等功能。平台兼容主流大模型,包括ChatGPT、DeepSeek、Ollama、智普、千问等,助力企业高效构建智能化应用,推动低代码开发与AI深度融合。
|
||||||
|
|
||||||
`JEECG宗旨是:` JEECG旨在通过OnlineCoding平台实现简单功能的零代码快速搭建,同时针对复杂功能采用代码生成器生成代码并手工合并,打造智能且灵活的低代码开发模式,有效解决了当前低代码产品普遍缺乏灵活性的问题,提升开发效率的同时兼顾系统的扩展性和定制化能力。
|
`JEECG宗旨是:` JEECG旨在通过OnlineCoding平台实现简单功能的零代码快速搭建,同时针对复杂功能采用代码生成器生成代码并手工合并,打造智能且灵活的低代码开发模式,有效解决了当前低代码产品普遍缺乏灵活性的问题,提升开发效率的同时兼顾系统的扩展性和定制化能力。
|
||||||
|
|
||||||
@ -50,15 +53,16 @@ JeecgBoot低代码平台兼容所有J2EE项目开发,支持信创国产化,
|
|||||||
版本说明
|
版本说明
|
||||||
-----------------------------------
|
-----------------------------------
|
||||||
|
|
||||||
|下载 | JDK17 + SpringBoot3.3 + Shiro |JDK17 + SpringBoot3.3+ SpringAuthorizationServer | JDK17/JDK8 + SpringBoot2.7 |
|
|下载 | SpringBoot3.5 + Shiro |SpringBoot3.5+ SpringAuthorizationServer | SpringBoot3.5 + Sa-Token | SpringBoot2.7(JDK17/JDK8) |
|
||||||
|------|----------------------------------------------------|--------------------------------------------|--------------------------------------------|
|
|------|---------------------------------------------------------|----------------------------|-------------------|--------------------------------------------|
|
||||||
| Github | [`springboot3`](https://github.com/jeecgboot/JeecgBoot/tree/springboot3) | [`springboot3_sas`](https://github.com/jeecgboot/JeecgBoot/tree/springboot3_sas) 分支 |[`master`](https://github.com/jeecgboot/JeecgBoot) 分支|
|
| Github | [`main`](https://github.com/jeecgboot/JeecgBoot) | [`springboot3_sas`](https://github.com/jeecgboot/JeecgBoot/tree/springboot3_sas) 分支 | [`springboot3-satoken`](https://github.com/jeecgboot/JeecgBoot/tree/springboot3-satoken) 分支|[`springboot2`](https://github.com/jeecgboot/JeecgBoot/tree/springboot2) 分支|
|
||||||
| Gitee | [`springboot3`](https://gitee.com/jeecg/JeecgBoot/tree/springboot3/) | [`springboot3_sas`](https://gitee.com/jeecg/JeecgBoot/tree/springboot3_sas) 分支 |[`master`](https://gitee.com/jeecg/JeecgBoot) 分支 |
|
| Gitee | [`main`](https://github.com/jeecgboot/JeecgBoot) | [`springboot3_sas`](https://gitee.com/jeecg/JeecgBoot/tree/springboot3_sas) 分支| [`springboot3-satoken`](https://gitee.com/jeecg/JeecgBoot/tree/springboot3-satoken) 分支|[`springboot2`](https://github.com/jeecgboot/JeecgBoot/tree/springboot2) 分支 |
|
||||||
|
|
||||||
|
|
||||||
- `jeecg-boot` 是后端JAVA源码项目Springboot3+SpringCloudAlibaba(支持单体和微服务切换).
|
- `jeecg-boot` 是后端JAVA源码项目Springboot3+Shiro+Mybatis+SpringCloudAlibaba(支持单体和微服务切换).
|
||||||
- `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,制作一个精简版本
|
||||||
|
|
||||||
|
|
||||||
@ -83,6 +87,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)
|
||||||
- 技术支持: [反馈问题](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(满)、其他(满)
|
||||||
|
|
||||||
@ -157,6 +162,9 @@ JeecgBoot平台提供了一套完善的AI应用管理系统模块,是一套类
|
|||||||
#### 前端
|
#### 前端
|
||||||
|
|
||||||
- 前端环境要求:Node.js要求`Node 20+` 版本以上、pnpm 要求`9+` 版本以上
|
- 前端环境要求:Node.js要求`Node 20+` 版本以上、pnpm 要求`9+` 版本以上
|
||||||
|
|
||||||
|
` ( Vite 不再支持已结束生命周期(EOL)的 Node.js 18。现在需要使用 Node.js 20.19+ 或 22.12+)`
|
||||||
|
|
||||||
- 依赖管理:node、npm、pnpm
|
- 依赖管理:node、npm、pnpm
|
||||||
- 前端IDE建议:IDEA、WebStorm、Vscode
|
- 前端IDE建议:IDEA、WebStorm、Vscode
|
||||||
- 采用 Vue3.0+TypeScript+Vite6+Ant-Design-Vue4等新技术方案,包括二次封装组件、utils、hooks、动态菜单、权限校验、按钮级别权限控制等功能
|
- 采用 Vue3.0+TypeScript+Vite6+Ant-Design-Vue4等新技术方案,包括二次封装组件、utils、hooks、动态菜单、权限校验、按钮级别权限控制等功能
|
||||||
@ -224,20 +232,6 @@ 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 产品功能蓝图
|
||||||

|

|
||||||
|
|
||||||
|
|||||||
@ -2,12 +2,12 @@
|
|||||||
JeecgBoot 低代码开发平台
|
JeecgBoot 低代码开发平台
|
||||||
===============
|
===============
|
||||||
|
|
||||||
当前最新版本: 3.8.3(发布日期:2025-09-22)
|
当前最新版本: 3.9.0(发布日期:2025-12-01)
|
||||||
|
|
||||||
|
|
||||||
[](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)
|
||||||
|
|
||||||
@ -16,43 +16,127 @@ JeecgBoot 低代码开发平台
|
|||||||
项目介绍
|
项目介绍
|
||||||
-----------------------------------
|
-----------------------------------
|
||||||
|
|
||||||
<h3 align="center">Java Low Code Platform for Enterprise web applications</h3>
|
<h3 align="center">企业级AI低代码平台</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(SpringBoot微服务架构) |
|
| `jeecg-boot` | 后端源码JAVA(SpringBoot3微服务架构) |
|
||||||
| `jeecgboot-vue3` | 前端源码VUE3(vue3+vite5+ts最新技术栈) |
|
| `jeecgboot-vue3` | 前端源码VUE3(vue3+vite6+antd4+ts最新技术栈) |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
技术文档
|
|
||||||
-----------------------------------
|
|
||||||
|
|
||||||
- 官方网站: [http://www.jeecg.com](http://www.jeecg.com)
|
|
||||||
- 新手指南: [快速入门](http://www.jeecg.com/doc/quickstart)
|
|
||||||
- QQ交流群 : 964611995、⑩716488839(满)、⑨808791225(满)、其他(满)
|
|
||||||
- 在线演示 : [在线演示](http://boot3.jeecg.com) | [APP演示](http://jeecg.com/appIndex)
|
|
||||||
> 演示系统的登录账号密码,请点击 [获取账号密码](http://jeecg.com/doc/demo) 获取
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
启动项目
|
启动项目
|
||||||
-----------------------------------
|
-----------------------------------
|
||||||
|
|
||||||
- [IDEA启动前后端项目](https://help.jeecg.com/java/setup/idea/startup)
|
> 默认账号密码: admin/123456
|
||||||
- [Docker一键启动前后端](https://help.jeecg.com/java/docker/quick)
|
|
||||||
|
- [开发环境搭建](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)
|
||||||
|
|
||||||
|
|
||||||
微服务启动
|
技术文档
|
||||||
-----------------------------------
|
-----------------------------------
|
||||||
- [单体快速切换微服务](https://help.jeecg.com/java/springcloud/switchcloud/monomer)
|
|
||||||
- [Docker启动微服务后台](https://help.jeecg.com/java/docker/springcloud)
|
|
||||||
|
|
||||||
|
- 官方网站: [http://www.jeecg.com](http://www.jeecg.com)
|
||||||
|
- 在线演示: [平台演示](https://boot3.jeecg.com) | [APP演示](https://jeecg.com/appIndex)
|
||||||
|
- 入门指南: [快速入门](http://www.jeecg.com/doc/quickstart) | [代码生成使用](https://help.jeecg.com/java/codegen/online) | [开发文档](https://help.jeecg.com) | [AI应用手册](https://help.jeecg.com/aigc) | [视频教程](http://jeecg.com/doc/video)
|
||||||
|
- 技术支持: [反馈问题](https://github.com/jeecgboot/JeecgBoot/issues/new?template=bug_report.md) | [低代码体验一分钟](https://jeecg.blog.csdn.net/article/details/106079007)
|
||||||
|
- QQ交流群 : 964611995、⑩716488839(满)、⑨808791225(满)、其他(满)
|
||||||
|
|
||||||
|
|
||||||
|
AI 应用平台介绍
|
||||||
|
-----------------------------------
|
||||||
|
|
||||||
|
一个全栈式 AI 开发平台,旨在帮助开发者快速构建和部署个性化的 AI 应用。
|
||||||
|
|
||||||
|
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),容易上手;代码生成器依赖性低,灵活的扩展能力,可快速实现二次开发。
|
||||||
|
- 2.前端大版本换代,最新版采用 Vue3.0 + TypeScript + Vite6 + Ant Design Vue4 等新技术方案。
|
||||||
|
- 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.提供丰富的示例代码,涵盖了常用的业务场景,便于学习和参考。
|
||||||
|
|
||||||
|
|
||||||
技术架构:
|
技术架构:
|
||||||
@ -61,28 +145,33 @@ JeecgBoot 是一款基于代码生成器的`低代码开发平台`!前后端
|
|||||||
#### 后端
|
#### 后端
|
||||||
|
|
||||||
- IDE建议: IDEA (必须安装lombok插件 )
|
- IDE建议: IDEA (必须安装lombok插件 )
|
||||||
- 语言:Java 8+ (支持17)
|
- 语言:Java 默认jdk17(jdk21、jdk24)
|
||||||
- 依赖管理:Maven
|
- 依赖管理:Maven
|
||||||
- 基础框架:Spring Boot 2.7.18
|
- 基础框架:Spring Boot 3.5.5
|
||||||
- 微服务框架: Spring Cloud Alibaba 2021.0.1.0
|
- 微服务框架: Spring Cloud Alibaba 2023.0.3.3
|
||||||
- 持久层框架:MybatisPlus 3.5.3.2
|
- 持久层框架:MybatisPlus 3.5.12
|
||||||
- 报表工具: JimuReport 1.9.4
|
- 报表工具: JimuReport 2.1.3
|
||||||
- 安全框架:Apache Shiro 1.12.0,Jwt 3.11.0
|
- 安全框架:Apache Shiro 2.0.4,Jwt 4.5.0
|
||||||
- 微服务技术栈:Spring Cloud Alibaba、Nacos、Gateway、Sentinel、Skywalking
|
- 微服务技术栈:Spring Cloud Alibaba、Nacos、Gateway、Sentinel、Skywalking
|
||||||
- 数据库连接池:阿里巴巴Druid 1.1.24
|
- 数据库连接池:阿里巴巴Druid 1.2.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)
|
||||||
|
|
||||||
|
|
||||||
#### 前端
|
#### 前端
|
||||||
|
|
||||||
- 前端IDE建议:WebStorm、Vscode
|
- 前端环境要求:Node.js要求`Node 20+` 版本以上、pnpm 要求`9+` 版本以上
|
||||||
- 采用 Vue3.0+TypeScript+Vite+Ant-Design-Vue等新技术方案,包括二次封装组件、utils、hooks、动态菜单、权限校验、按钮级别权限控制等功能
|
` ( Vite 不再支持已结束生命周期(EOL)的 Node.js 18。现在需要使用 Node.js 20.19+ 或 22.12+)`
|
||||||
- 最新技术栈: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
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
368
jeecg-boot/Shiro到Sa-Token迁移指南.md
Normal file
368
jeecg-boot/Shiro到Sa-Token迁移指南.md
Normal file
@ -0,0 +1,368 @@
|
|||||||
|
# `Shiro 到 Sa-Token 迁移指南`
|
||||||
|
|
||||||
|
本项目已从 **Apache Shiro 2.0.4** 迁移到 **Sa-Token 1.44.0**,采用 JWT-Simple 模式,完全兼容原 JWT token 格式。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📦 1. 依赖配置
|
||||||
|
|
||||||
|
### 1.1 Maven 依赖
|
||||||
|
|
||||||
|
移除 Shiro 相关依赖,新增:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<dependency>
|
||||||
|
<groupId>cn.dev33</groupId>
|
||||||
|
<artifactId>sa-token-spring-boot3-starter</artifactId>
|
||||||
|
<version>1.44.0</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>cn.dev33</groupId>
|
||||||
|
<artifactId>sa-token-redis-jackson</artifactId>
|
||||||
|
<version>1.44.0</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>cn.dev33</groupId>
|
||||||
|
<artifactId>sa-token-jwt</artifactId>
|
||||||
|
<version>1.44.0</version>
|
||||||
|
</dependency>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.2 配置文件(application.yml)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
sa-token:
|
||||||
|
token-name: X-Access-Token
|
||||||
|
timeout: 2592000 # token有效期30天
|
||||||
|
is-concurrent: true # 允许同账号并发登录
|
||||||
|
token-style: jwt-simple # JWT模式(兼容原格式)
|
||||||
|
jwt-secret-key: "your-secret-key-here"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💡 2. 核心代码实现
|
||||||
|
|
||||||
|
### 2.1 登录逻辑(⚠️ 使用 username 作为 loginId)
|
||||||
|
|
||||||
|
```java
|
||||||
|
// 从数据库查询用户信息
|
||||||
|
SysUser sysUser = userService.getUserByUsername(username);
|
||||||
|
|
||||||
|
// 执行登录(自动完成:Sa-Token登录 + 存储Session + 返回token)
|
||||||
|
String token = LoginUserUtils.doLogin(sysUser);
|
||||||
|
|
||||||
|
// 返回token给前端
|
||||||
|
return Result.ok(token);
|
||||||
|
```
|
||||||
|
|
||||||
|
**💡 设计说明:**
|
||||||
|
- `doLogin()` 方法自动完成:
|
||||||
|
1. 调用 `StpUtil.login(username)` (使用 username 而非 userId)
|
||||||
|
2. 调用 `setSessionUser()` 存储用户信息(自动清除 password 等15个字段)
|
||||||
|
3. 返回生成的 token
|
||||||
|
- 减少 Redis 存储约 50%,密码不再存储到 Session
|
||||||
|
|
||||||
|
### 2.2 权限认证接口(⚠️ 必须手动实现缓存)
|
||||||
|
|
||||||
|
```java
|
||||||
|
@Component
|
||||||
|
public class StpInterfaceImpl implements StpInterface {
|
||||||
|
|
||||||
|
@Lazy @Resource
|
||||||
|
private CommonAPI commonApi;
|
||||||
|
|
||||||
|
private static final long CACHE_TIMEOUT = 60 * 60 * 24 * 30; // 30天
|
||||||
|
private static final String PERMISSION_CACHE_PREFIX = "satoken:user-permission:";
|
||||||
|
private static final String ROLE_CACHE_PREFIX = "satoken:user-role:";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public List<String> getPermissionList(Object loginId, String loginType) {
|
||||||
|
String username = loginId.toString();
|
||||||
|
String cacheKey = PERMISSION_CACHE_PREFIX + username;
|
||||||
|
SaTokenDao dao = SaManager.getSaTokenDao();
|
||||||
|
|
||||||
|
// 1. 先从缓存获取
|
||||||
|
List<String> permissionList = (List<String>) dao.getObject(cacheKey);
|
||||||
|
|
||||||
|
if (permissionList == null) {
|
||||||
|
// 2. 缓存未命中,查询数据库
|
||||||
|
log.warn("权限缓存未命中,查询数据库 [ username={} ]", username);
|
||||||
|
|
||||||
|
String userId = commonApi.getUserIdByName(username);
|
||||||
|
Set<String> permissionSet = commonApi.queryUserAuths(userId);
|
||||||
|
permissionList = new ArrayList<>(permissionSet);
|
||||||
|
|
||||||
|
// 3. 将结果缓存起来
|
||||||
|
dao.setObject(cacheKey, permissionList, CACHE_TIMEOUT);
|
||||||
|
}
|
||||||
|
|
||||||
|
return permissionList;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<String> getRoleList(Object loginId, String loginType) {
|
||||||
|
// 实现类似 getPermissionList(),使用 ROLE_CACHE_PREFIX
|
||||||
|
// 详见:StpInterfaceImpl.java
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清除缓存的静态方法
|
||||||
|
public static void clearUserCache(List<String> usernameList) {
|
||||||
|
SaTokenDao dao = SaManager.getSaTokenDao();
|
||||||
|
for (String username : usernameList) {
|
||||||
|
dao.deleteObject(PERMISSION_CACHE_PREFIX + username);
|
||||||
|
dao.deleteObject(ROLE_CACHE_PREFIX + username);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**⚠️ 关键:** Sa-Token 的 `StpInterface` **不提供自动缓存**,必须手动实现,否则每次请求都会查询数据库!
|
||||||
|
|
||||||
|
### 2.3 Filter 配置(支持 URL 参数传递 token)
|
||||||
|
|
||||||
|
```java
|
||||||
|
@Bean
|
||||||
|
@Primary
|
||||||
|
public StpLogic getStpLogicJwt() {
|
||||||
|
return new StpLogicJwtForSimple() {
|
||||||
|
@Override
|
||||||
|
public String getTokenValue() {
|
||||||
|
SaRequest request = SaHolder.getRequest();
|
||||||
|
|
||||||
|
// 优先级:Header > URL参数"token" > URL参数"X-Access-Token"
|
||||||
|
String tokenValue = request.getHeader(getConfigOrGlobal().getTokenName());
|
||||||
|
if (isEmpty(tokenValue)) {
|
||||||
|
tokenValue = request.getParam("token"); // 兼容 WebSocket、积木报表
|
||||||
|
}
|
||||||
|
if (isEmpty(tokenValue)) {
|
||||||
|
tokenValue = request.getParam(getConfigOrGlobal().getTokenName());
|
||||||
|
}
|
||||||
|
|
||||||
|
return isEmpty(tokenValue) ? super.getTokenValue() : tokenValue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public SaServletFilter getSaServletFilter() {
|
||||||
|
return new SaServletFilter()
|
||||||
|
.addInclude("/**")
|
||||||
|
.setExcludeList(getExcludeUrls()) // 排除登录、静态资源等
|
||||||
|
.setAuth(obj -> {
|
||||||
|
// 检查是否是免认证路径
|
||||||
|
String servletPath = SaHolder.getRequest().getRequestPath();
|
||||||
|
if (InMemoryIgnoreAuth.contains(servletPath)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ⚠️ 关键:如果请求带 token,先切换到对应的登录会话
|
||||||
|
try {
|
||||||
|
String token = StpUtil.getTokenValue();
|
||||||
|
if (isNotEmpty(token)) {
|
||||||
|
Object loginId = StpUtil.getLoginIdByToken(token);
|
||||||
|
if (loginId != null) {
|
||||||
|
StpUtil.switchTo(loginId); // 切换登录会话
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.debug("切换登录会话失败: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 最终校验登录状态
|
||||||
|
StpUtil.checkLogin();
|
||||||
|
})
|
||||||
|
.setError(e -> {
|
||||||
|
// 返回401 JSON响应
|
||||||
|
SaHolder.getResponse()
|
||||||
|
.setStatus(401)
|
||||||
|
.setHeader("Content-Type", "application/json;charset=UTF-8");
|
||||||
|
return JwtUtil.responseErrorJson(401, "Token失效,请重新登录!");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.4 全局异常处理
|
||||||
|
|
||||||
|
```java
|
||||||
|
@ExceptionHandler(NotLoginException.class)
|
||||||
|
public Result<?> handleNotLoginException(NotLoginException e) {
|
||||||
|
log.warn("用户未登录或Token失效: {}", e.getMessage());
|
||||||
|
return Result.error(401, "Token失效,请重新登录!");
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler(NotPermissionException.class)
|
||||||
|
public Result<?> handleNotPermissionException(NotPermissionException e) {
|
||||||
|
log.warn("权限不足: {}", e.getMessage());
|
||||||
|
return Result.error(403, "用户权限不足,无法访问!");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 3. API 迁移对照表
|
||||||
|
|
||||||
|
### 3.1 注解替换
|
||||||
|
|
||||||
|
| Shiro | Sa-Token | 说明 |
|
||||||
|
|-------|----------|------|
|
||||||
|
| `@RequiresPermissions("user:add")` | `@SaCheckPermission("user:add")` | 权限校验 |
|
||||||
|
| `@RequiresRoles("admin")` | `@SaCheckRole("admin")` | 角色校验 |
|
||||||
|
|
||||||
|
### 3.2 API 替换
|
||||||
|
|
||||||
|
| Shiro | Sa-Token | 说明 |
|
||||||
|
|-------|----------|------|
|
||||||
|
| `SecurityUtils.getSubject().getPrincipal()` | `LoginUserUtils.getSessionUser()` | 获取登录用户 |
|
||||||
|
| `Subject.login(token)` | `LoginUserUtils.doLogin(sysUser)` | 登录(推荐) |
|
||||||
|
| `Subject.login(token)` | `StpUtil.login(username)` | 登录(底层API) |
|
||||||
|
| `Subject.logout()` | `StpUtil.logout()` | 退出登录 |
|
||||||
|
| `Subject.isAuthenticated()` | `StpUtil.isLogin()` | 判断是否登录 |
|
||||||
|
| `Subject.hasRole("admin")` | `StpUtil.hasRole("admin")` | 判断角色 |
|
||||||
|
| `Subject.isPermitted("user:add")` | `StpUtil.hasPermission("user:add")` | 判断权限 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚠️ 4. 重要特性说明
|
||||||
|
|
||||||
|
### 4.1 JWT-Simple 模式特性
|
||||||
|
|
||||||
|
- ✅ **生成标准 JWT token**:与原 Shiro JWT 格式完全兼容
|
||||||
|
- ✅ **仍然检查 Redis Session**:支持强制退出(与纯 JWT 无状态模式不同)
|
||||||
|
- ✅ **支持 URL 参数传递**:兼容 WebSocket、积木报表等场景
|
||||||
|
- ⚠️ **非完全无状态**:依赖 Redis 存储会话和权限缓存
|
||||||
|
|
||||||
|
### 4.2 Session 数据优化
|
||||||
|
|
||||||
|
`LoginUserUtils.setSessionUser()` 会自动清除以下字段:
|
||||||
|
|
||||||
|
```
|
||||||
|
password, workNo, birthday, sex, email, phone, status,
|
||||||
|
delFlag, activitiSync, createTime, userIdentity, post,
|
||||||
|
telephone, clientId, mainDepPostId
|
||||||
|
```
|
||||||
|
|
||||||
|
**优势:**
|
||||||
|
- 减少 Redis 存储约 **50%**
|
||||||
|
- 密码不再存储在 Session 中,**安全性提升**
|
||||||
|
|
||||||
|
### 4.3 权限缓存动态更新
|
||||||
|
|
||||||
|
修改角色权限后,系统会自动清除受影响用户的权限缓存:
|
||||||
|
|
||||||
|
```java
|
||||||
|
// SysPermissionController.saveRolePermission() 中
|
||||||
|
@RequestMapping(value = "/saveRolePermission", method = RequestMethod.POST)
|
||||||
|
public Result<String> saveRolePermission(@RequestBody JSONObject json) {
|
||||||
|
String roleId = json.getString("roleId");
|
||||||
|
String permissionIds = json.getString("permissionIds");
|
||||||
|
String lastPermissionIds = json.getString("lastpermissionIds");
|
||||||
|
|
||||||
|
// 保存角色权限关系
|
||||||
|
sysRolePermissionService.saveRolePermission(roleId, permissionIds, lastPermissionIds);
|
||||||
|
|
||||||
|
// ⚠️ 关键:清除拥有该角色的所有用户的权限缓存
|
||||||
|
clearRolePermissionCache(roleId);
|
||||||
|
|
||||||
|
return Result.ok("保存成功!");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 实现:查询该角色下的所有用户,批量清除缓存
|
||||||
|
private void clearRolePermissionCache(String roleId) {
|
||||||
|
List<String> usernameList = new ArrayList<>();
|
||||||
|
|
||||||
|
// 分页查询拥有该角色的用户
|
||||||
|
int pageNo = 1, pageSize = 100;
|
||||||
|
while (true) {
|
||||||
|
Page<SysUser> page = new Page<>(pageNo, pageSize);
|
||||||
|
IPage<SysUser> userPage = sysUserService.getUserByRoleId(page, roleId, null, null);
|
||||||
|
|
||||||
|
if (userPage.getRecords().isEmpty()) break;
|
||||||
|
|
||||||
|
for (SysUser user : userPage.getRecords()) {
|
||||||
|
usernameList.add(user.getUsername());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pageNo >= userPage.getPages()) break;
|
||||||
|
pageNo++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量清除用户权限和角色缓存
|
||||||
|
if (!usernameList.isEmpty()) {
|
||||||
|
StpInterfaceImpl.clearUserCache(usernameList);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**结果:** 权限变更立即生效,用户无需重新登录。
|
||||||
|
|
||||||
|
|
||||||
|
## ✅ 6. 测试清单
|
||||||
|
|
||||||
|
### 6.1 登录功能测试
|
||||||
|
|
||||||
|
| 测试项 | 测试状态 | 说明 |
|
||||||
|
|--------|---------|------|
|
||||||
|
| 账号密码登录 | ✅ 通过 | 验证 `/sys/login` 接口 |
|
||||||
|
| 手机号登录 | ✅ 通过 | 验证 `/sys/phoneLogin` 接口 |
|
||||||
|
| APP 登录 | ✅ 通过 | 验证 APP 端登录流程 |
|
||||||
|
| 扫码登录 | ✅ 通过 | 验证二维码扫码登录 |
|
||||||
|
| 第三方登录 | ⏳ 待测试 | 微信、QQ 等第三方登录 |
|
||||||
|
| 钉钉 OAuth2.0 登录 | ⏳ 待测试 | 钉钉授权登录流程 |
|
||||||
|
| 企业微信 OAuth2.0 登录 | ⏳ 待测试 | 企业微信授权登录流程 |
|
||||||
|
| CAS 单点登录 | ⏳ 待测试 | CAS 单点登录集成 |
|
||||||
|
|
||||||
|
### 6.2 核心功能测试
|
||||||
|
|
||||||
|
| 测试项 | 测试状态 | 说明 |
|
||||||
|
|--------|---------|------|
|
||||||
|
| Token 权限拦截 | ✅ 通过 | 无 token 或失效 token 返回 401 |
|
||||||
|
| 权限注解 `@SaCheckPermission` | ✅ 通过 | 无权限返回 403 |
|
||||||
|
| 角色注解 `@SaCheckRole` | ✅ 通过 | 无角色返回 403 |
|
||||||
|
| `@IgnoreAuth` 免认证 | ✅ 通过 | 无 token 也能正常访问 |
|
||||||
|
| 自动续期(操作不掉线) | ✅ 通过 | 活跃用户 token 自动续期 |
|
||||||
|
| 用户权限变更即刻生效 | ✅ 通过 | 修改角色权限后无需重新登录 |
|
||||||
|
| 积木报表 token 参数模式 | ✅ 通过 | `/jmreport/**?token=xxx` 正常访问 |
|
||||||
|
|
||||||
|
### 6.3 异步和网关测试
|
||||||
|
|
||||||
|
| 测试项 | 测试状态 | 说明 |
|
||||||
|
|--------|---------|------|
|
||||||
|
| 异步接口(`@Async`) | ❌ 有问题 | **需排查:异步线程中获取登录用户失败** |
|
||||||
|
| Gateway 模式权限验证 | ⏳ 待测试 | 网关模式下的权限拦截 |
|
||||||
|
|
||||||
|
### 6.4 多租户测试
|
||||||
|
|
||||||
|
| 测试项 | 测试状态 | 说明 |
|
||||||
|
|--------|---------|------|
|
||||||
|
| 租户 ID 校验 | ⚠️ 缺失 | **需补充:校验用户 tenant_id 和前端传参一致性** |
|
||||||
|
|
||||||
|
### 6.5 测试说明
|
||||||
|
|
||||||
|
**✅ 通过** - 功能正常,符合预期
|
||||||
|
**❌ 有问题** - 功能异常,需要修复
|
||||||
|
**⏳ 待测试** - 尚未测试
|
||||||
|
**⚠️ 缺失** - 功能缺失,需要补充
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 7. 迁移总结
|
||||||
|
|
||||||
|
| 优化项 | 说明 | 收益 |
|
||||||
|
|--------|------|------|
|
||||||
|
| **loginId 设计** | 使用 `username` 而非 `userId` | 语义清晰,与业务逻辑一致 |
|
||||||
|
| **Session 优化** | 清除 15 个不必要字段 | Redis 存储减少 50%,安全性提升 |
|
||||||
|
| **权限缓存** | 手动实现 30 天缓存 | 性能提升 99%,降低 DB 压力 |
|
||||||
|
| **权限实时更新** | 角色权限修改后自动清除缓存 | 无需重新登录即生效 |
|
||||||
|
| **URL Token 支持** | Filter 中实现 `switchTo` | 兼容 WebSocket、积木报表等场景 |
|
||||||
|
| **JWT 兼容** | JWT-Simple 模式 | 完全兼容原 JWT token 格式 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📚 参考资料
|
||||||
|
|
||||||
|
- [Sa-Token 官方文档](https://sa-token.cc/)
|
||||||
|
- [Sa-Token JWT-Simple 模式](https://sa-token.cc/doc.html#/plugin/jwt-extend)
|
||||||
|
- [Sa-Token 权限缓存最佳实践](https://sa-token.cc/doc.html#/fun/jur-cache)
|
||||||
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.
31499
jeecg-boot/db/其他数据库脚本/jeecgboot-oracle11g.sql
Normal file
31499
jeecg-boot/db/其他数据库脚本/jeecgboot-oracle11g.sql
Normal file
File diff suppressed because one or more lines are too long
27491
jeecg-boot/db/其他数据库脚本/jeecgboot-postgresql17.sql
Normal file
27491
jeecg-boot/db/其他数据库脚本/jeecgboot-postgresql17.sql
Normal file
File diff suppressed because one or more lines are too long
50451
jeecg-boot/db/其他数据库脚本/jeecgboot-sqlserver2017.sql
Normal file
50451
jeecg-boot/db/其他数据库脚本/jeecgboot-sqlserver2017.sql
Normal file
File diff suppressed because one or more lines are too long
@ -63,6 +63,7 @@ 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
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>org.jeecgframework.boot3</groupId>
|
<groupId>org.jeecgframework.boot3</groupId>
|
||||||
<artifactId>jeecg-boot-parent</artifactId>
|
<artifactId>jeecg-boot-parent</artifactId>
|
||||||
<version>3.8.3</version>
|
<version>3.9.0</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>
|
||||||
@ -44,6 +44,12 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.jeecgframework.boot3</groupId>
|
<groupId>org.jeecgframework.boot3</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>
|
||||||
@ -173,93 +179,31 @@
|
|||||||
<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>
|
||||||
<artifactId>spring-boot-starter-quartz</artifactId>
|
<artifactId>spring-boot-starter-quartz</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!--JWT-->
|
<!-- Sa-Token 权限认证,在线文档:https://sa-token.cc -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.auth0</groupId>
|
<groupId>cn.dev33</groupId>
|
||||||
<artifactId>java-jwt</artifactId>
|
<artifactId>sa-token-spring-boot3-starter</artifactId>
|
||||||
<version>${java-jwt.version}</version>
|
<version>${sa-token.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<!-- Sa-Token 整合 Redis (使用 jackson 序列化方式) -->
|
||||||
<!--shiro-->
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.apache.shiro</groupId>
|
<groupId>cn.dev33</groupId>
|
||||||
<artifactId>shiro-spring-boot-starter</artifactId>
|
<artifactId>sa-token-redis-jackson</artifactId>
|
||||||
<classifier>jakarta</classifier>
|
<version>${sa-token.version}</version>
|
||||||
<version>${shiro.version}</version>
|
|
||||||
<exclusions>
|
|
||||||
<exclusion>
|
|
||||||
<groupId>org.apache.shiro</groupId>
|
|
||||||
<artifactId>shiro-spring</artifactId>
|
|
||||||
</exclusion>
|
|
||||||
</exclusions>
|
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<!-- Sa-Token 整合 jwt (Simple模式),保持与原JWT token格式兼容 -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.apache.shiro</groupId>
|
<groupId>cn.dev33</groupId>
|
||||||
<artifactId>shiro-spring</artifactId>
|
<artifactId>sa-token-jwt</artifactId>
|
||||||
<classifier>jakarta</classifier>
|
<version>${sa-token.version}</version>
|
||||||
<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>
|
</dependency>
|
||||||
<!-- 引入适配jakarta的依赖包 -->
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.apache.shiro</groupId>
|
|
||||||
<artifactId>shiro-core</artifactId>
|
|
||||||
<classifier>jakarta</classifier>
|
|
||||||
<version>${shiro.version}</version>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.apache.shiro</groupId>
|
|
||||||
<artifactId>shiro-web</artifactId>
|
|
||||||
<classifier>jakarta</classifier>
|
|
||||||
<version>${shiro.version}</version>
|
|
||||||
<exclusions>
|
|
||||||
<exclusion>
|
|
||||||
<groupId>org.apache.shiro</groupId>
|
|
||||||
<artifactId>shiro-core</artifactId>
|
|
||||||
</exclusion>
|
|
||||||
</exclusions>
|
|
||||||
</dependency>
|
|
||||||
<!-- shiro-redis -->
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.crazycake</groupId>
|
|
||||||
<artifactId>shiro-redis</artifactId>
|
|
||||||
<version>${shiro-redis.version}</version>
|
|
||||||
<exclusions>
|
|
||||||
<exclusion>
|
|
||||||
<groupId>org.apache.shiro</groupId>
|
|
||||||
<artifactId>shiro-core</artifactId>
|
|
||||||
</exclusion>
|
|
||||||
<exclusion>
|
|
||||||
<artifactId>checkstyle</artifactId>
|
|
||||||
<groupId>com.puppycrawl.tools</groupId>
|
|
||||||
</exclusion>
|
|
||||||
</exclusions>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
|
|
||||||
<!-- <dependency>
|
|
||||||
<groupId>com.github.xiaoymin</groupId>
|
|
||||||
<artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
|
|
||||||
<version>${knife4j-spring-boot-starter.version}</version>
|
|
||||||
</dependency>-->
|
|
||||||
<!-- knife4j 升级springboot3.4.5报错 -->
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.github.xiaoymin</groupId>
|
<groupId>com.github.xiaoymin</groupId>
|
||||||
<artifactId>knife4j-openapi3-ui</artifactId>
|
<artifactId>knife4j-openapi3-ui</artifactId>
|
||||||
@ -291,8 +235,8 @@
|
|||||||
|
|
||||||
<!-- AutoPoi Excel工具类-->
|
<!-- AutoPoi Excel工具类-->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.jeecgframework.boot3</groupId>
|
<groupId>org.jeecgframework</groupId>
|
||||||
<artifactId>autopoi-web</artifactId>
|
<artifactId>autopoi-spring-boot-3-starter</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>xerces</groupId>
|
<groupId>xerces</groupId>
|
||||||
@ -310,6 +254,14 @@
|
|||||||
<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>
|
||||||
|
|
||||||
@ -367,5 +319,21 @@
|
|||||||
<groupId>org.jeecgframework.boot3</groupId>
|
<groupId>org.jeecgframework.boot3</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>
|
||||||
@ -0,0 +1,21 @@
|
|||||||
|
package org.apache.shiro;
|
||||||
|
|
||||||
|
import org.apache.shiro.subject.Subject;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 兼容处理Online功能使用处理,请勿修改
|
||||||
|
* @author eightmonth@qq.com
|
||||||
|
* @date 2024/4/29 14:05
|
||||||
|
*/
|
||||||
|
public class SecurityUtils {
|
||||||
|
|
||||||
|
|
||||||
|
public static Subject getSubject() {
|
||||||
|
return new Subject() {
|
||||||
|
@Override
|
||||||
|
public Object getPrincipal() {
|
||||||
|
return Subject.super.getPrincipal();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,15 @@
|
|||||||
|
package org.apache.shiro.subject;
|
||||||
|
|
||||||
|
|
||||||
|
import org.jeecg.common.util.LoginUserUtils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 兼容处理Online功能使用处理,请勿修改
|
||||||
|
* @author eightmonth@qq.com
|
||||||
|
* @date 2024/4/29 14:18
|
||||||
|
*/
|
||||||
|
public interface Subject {
|
||||||
|
default Object getPrincipal() {
|
||||||
|
return LoginUserUtils.getSessionUser();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -132,7 +132,6 @@ 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
|
||||||
@ -143,7 +142,6 @@ 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流程
|
||||||
|
|||||||
@ -0,0 +1,48 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
@ -30,12 +30,10 @@ 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(){
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,55 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
@ -2,7 +2,7 @@ package org.jeecg.common.aspect;
|
|||||||
|
|
||||||
import com.alibaba.fastjson.JSONObject;
|
import com.alibaba.fastjson.JSONObject;
|
||||||
import com.alibaba.fastjson.serializer.PropertyFilter;
|
import com.alibaba.fastjson.serializer.PropertyFilter;
|
||||||
import org.apache.shiro.SecurityUtils;
|
import org.jeecg.common.util.LoginUserUtils;
|
||||||
import org.aspectj.lang.JoinPoint;
|
import org.aspectj.lang.JoinPoint;
|
||||||
import org.aspectj.lang.ProceedingJoinPoint;
|
import org.aspectj.lang.ProceedingJoinPoint;
|
||||||
import org.aspectj.lang.annotation.Around;
|
import org.aspectj.lang.annotation.Around;
|
||||||
@ -100,7 +100,7 @@ public class AutoLogAspect {
|
|||||||
//设置IP地址
|
//设置IP地址
|
||||||
dto.setIp(IpUtils.getIpAddr(request));
|
dto.setIp(IpUtils.getIpAddr(request));
|
||||||
//获取登录用户信息
|
//获取登录用户信息
|
||||||
LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal();
|
LoginUser sysUser = LoginUserUtils.getSessionUser();
|
||||||
if(sysUser!=null){
|
if(sysUser!=null){
|
||||||
dto.setUserid(sysUser.getUsername());
|
dto.setUserid(sysUser.getUsername());
|
||||||
dto.setUsername(sysUser.getRealname());
|
dto.setUsername(sysUser.getRealname());
|
||||||
@ -121,9 +121,8 @@ 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:阿里云代码扫描规范(不允许任何魔法值出现在代码中)------------
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -143,14 +142,15 @@ public class AutoLogAspect {
|
|||||||
// https://my.oschina.net/mengzhang6/blog/2395893
|
// https://my.oschina.net/mengzhang6/blog/2395893
|
||||||
Object[] arguments = new Object[paramsArray.length];
|
Object[] arguments = new Object[paramsArray.length];
|
||||||
for (int i = 0; i < paramsArray.length; i++) {
|
for (int i = 0; i < paramsArray.length; i++) {
|
||||||
if (paramsArray[i] instanceof BindingResult || paramsArray[i] instanceof ServletRequest || paramsArray[i] instanceof ServletResponse || paramsArray[i] instanceof MultipartFile) {
|
if (paramsArray[i] instanceof BindingResult || paramsArray[i] instanceof ServletRequest || paramsArray[i] instanceof ServletResponse || paramsArray[i] instanceof MultipartFile || paramsArray[i] instanceof MultipartFile[]) {
|
||||||
//ServletRequest不能序列化,从入参里排除,否则报异常:java.lang.IllegalStateException: It is illegal to call this method if the current request is not in asynchronous mode (i.e. isAsyncStarted() returns false)
|
//ServletRequest不能序列化,从入参里排除,否则报异常:java.lang.IllegalStateException: It is illegal to call this method if the current request is not in asynchronous mode (i.e. isAsyncStarted() returns false)
|
||||||
//ServletResponse不能序列化 从入参里排除,否则报异常:java.lang.IllegalStateException: getOutputStream() has already been called for this response
|
//ServletResponse不能序列化 从入参里排除,否则报异常:java.lang.IllegalStateException: getOutputStream() has already been called for this response
|
||||||
|
//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,14 +165,13 @@ 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();
|
StandardReflectionParameterNameDiscoverer u= new StandardReflectionParameterNameDiscoverer();
|
||||||
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++) {
|
||||||
@ -244,7 +243,7 @@ public class AutoLogAspect {
|
|||||||
sysLog.setIp(IPUtils.getIpAddr(request));
|
sysLog.setIp(IPUtils.getIpAddr(request));
|
||||||
|
|
||||||
//获取登录用户信息
|
//获取登录用户信息
|
||||||
LoginUser sysUser = (LoginUser)SecurityUtils.getSubject().getPrincipal();
|
LoginUser sysUser = LoginUserUtils.getLoginUser();
|
||||||
if(sysUser!=null){
|
if(sysUser!=null){
|
||||||
sysLog.setUserid(sysUser.getUsername());
|
sysLog.setUserid(sysUser.getUsername());
|
||||||
sysLog.setUsername(sysUser.getRealname());
|
sysLog.setUsername(sysUser.getRealname());
|
||||||
|
|||||||
@ -105,29 +105,24 @@ 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();
|
||||||
//update-begin--Author:zyf -- Date:20220606 ----for:【VUEN-1230】 判断是否含有字典注解,没有注解返回-----
|
// 代码逻辑说明: 【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);
|
||||||
}
|
}
|
||||||
//update-begin--Author:scott -- Date:20211223 ----for:【issues/3303】restcontroller返回json数据后key顺序错乱 -----
|
// 代码逻辑说明: 【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)) {
|
||||||
@ -135,7 +130,6 @@ 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);
|
||||||
@ -143,26 +137,22 @@ 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]解决分布式下表字典跨库无法查询问题------------
|
// 代码逻辑说明: [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)) {
|
||||||
//update-begin---author:chenrui ---date:20231221 for:[issues/#5643]解决分布式下表字典跨库无法查询问题------------
|
// 代码逻辑说明: [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);
|
||||||
}
|
}
|
||||||
@ -176,15 +166,12 @@ 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)) {
|
||||||
//update-begin---author:chenrui ---date:20231221 for:[issues/#5643]解决分布式下表字典跨库无法查询问题------------
|
// 代码逻辑说明: [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());
|
||||||
@ -286,25 +273,20 @@ 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);
|
||||||
@ -313,10 +295,8 @@ 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);
|
||||||
}
|
}
|
||||||
@ -400,7 +380,7 @@ public class DictAspect {
|
|||||||
if (k.trim().length() == 0) {
|
if (k.trim().length() == 0) {
|
||||||
continue; //跳过循环
|
continue; //跳过循环
|
||||||
}
|
}
|
||||||
//update-begin--Author:scott -- Date:20210531 ----for: !56 优化微服务应用下存在表字段需要字典翻译时加载缓慢问题-----
|
// 代码逻辑说明: !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());
|
||||||
@ -425,7 +405,6 @@ 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())) {
|
||||||
|
|||||||
@ -57,7 +57,6 @@ 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){
|
||||||
// 获取地址栏参数
|
// 获取地址栏参数
|
||||||
@ -66,7 +65,6 @@ 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,7 +41,6 @@ public @interface Dict {
|
|||||||
String dictTable() default "";
|
String dictTable() default "";
|
||||||
|
|
||||||
|
|
||||||
//update-begin---author:chenrui ---date:20231221 for:[issues/#5643]解决分布式下表字典跨库无法查询问题------------
|
|
||||||
/**
|
/**
|
||||||
* 方法描述: 数据字典表所在数据源名称
|
* 方法描述: 数据字典表所在数据源名称
|
||||||
* 作 者: chenrui
|
* 作 者: chenrui
|
||||||
@ -50,5 +49,4 @@ public @interface Dict {
|
|||||||
* @return 返回类型: String
|
* @return 返回类型: String
|
||||||
*/
|
*/
|
||||||
String ds() default "";
|
String ds() default "";
|
||||||
//update-end---author:chenrui ---date:20231221 for:[issues/#5643]解决分布式下表字典跨库无法查询问题------------
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -87,13 +87,6 @@ public interface CommonConstant {
|
|||||||
/**访问权限认证未通过 510*/
|
/**访问权限认证未通过 510*/
|
||||||
Integer SC_JEECG_NO_AUTHZ=510;
|
Integer SC_JEECG_NO_AUTHZ=510;
|
||||||
|
|
||||||
/** 登录用户Shiro权限缓存KEY前缀 */
|
|
||||||
public static String PREFIX_USER_SHIRO_CACHE = "shiro:cache:org.jeecg.config.shiro.ShiroRealm.authorizationCache:";
|
|
||||||
/** 登录用户Token令牌缓存KEY前缀 */
|
|
||||||
String PREFIX_USER_TOKEN = "prefix_user_token:";
|
|
||||||
// /** Token缓存时间:3600秒即一小时 */
|
|
||||||
// int TOKEN_EXPIRE_TIME = 3600;
|
|
||||||
|
|
||||||
/** 登录二维码 */
|
/** 登录二维码 */
|
||||||
String LOGIN_QRCODE_PRE = "QRCODELOGIN:";
|
String LOGIN_QRCODE_PRE = "QRCODELOGIN:";
|
||||||
String LOGIN_QRCODE = "LQ:";
|
String LOGIN_QRCODE = "LQ:";
|
||||||
@ -308,6 +301,10 @@ 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:不同意)
|
||||||
*/
|
*/
|
||||||
@ -591,7 +588,6 @@ public interface CommonConstant {
|
|||||||
String ORDER_TYPE_DESC = "DESC";
|
String ORDER_TYPE_DESC = "DESC";
|
||||||
|
|
||||||
|
|
||||||
//update-begin---author:scott ---date:2023-09-10 for:积木报表常量----
|
|
||||||
/**
|
/**
|
||||||
* 报表允许设计开发的角色
|
* 报表允许设计开发的角色
|
||||||
*/
|
*/
|
||||||
@ -606,9 +602,7 @@ 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的前缀
|
||||||
*/
|
*/
|
||||||
@ -633,7 +627,6 @@ public interface CommonConstant {
|
|||||||
* 修改手机号
|
* 修改手机号
|
||||||
*/
|
*/
|
||||||
String UPDATE_PHONE = "updatePhone";
|
String UPDATE_PHONE = "updatePhone";
|
||||||
//update-end---author:wangshuai---date:2024-04-07---for:修改手机号常量---
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 修改手机号验证码请求次数超出
|
* 修改手机号验证码请求次数超出
|
||||||
@ -709,4 +702,28 @@ 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";
|
||||||
|
|
||||||
|
/**============================== 【是否允许同一账号多地同时登录】登录客户端类型常量 ==============================*/
|
||||||
|
/** 客户端类型:PC端 */
|
||||||
|
String CLIENT_TYPE_PC = "PC";
|
||||||
|
/** 客户端类型:APP端 */
|
||||||
|
String CLIENT_TYPE_APP = "APP";
|
||||||
|
/** 客户端类型:手机号登录 */
|
||||||
|
String CLIENT_TYPE_PHONE = "PHONE";
|
||||||
|
/**============================== 【是否允许同一账号多地同时登录】登录客户端类型常量 ==============================*/
|
||||||
}
|
}
|
||||||
|
|||||||
@ -39,20 +39,18 @@ 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--){
|
||||||
//update-begin-author:taoyan date:2022-5-24 for:VUEN-1088 online 导入 省市区导入后 导入数据错乱 北京市/市辖区/西城区-->山西省/晋城市/城区
|
// 代码逻辑说明: 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 文本数组,省,市,区
|
||||||
@ -117,7 +115,6 @@ 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){
|
||||||
@ -154,9 +151,8 @@ 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);
|
||||||
//update-begin-author:taoyan date:2022-5-24 for:VUEN-1088 online 导入 省市区导入后 导入数据错乱 北京市/市辖区/西城区-->山西省/晋城市/城区
|
// 代码逻辑说明: 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -23,7 +23,11 @@ public enum NoticeTypeEnum {
|
|||||||
/**
|
/**
|
||||||
* 督办
|
* 督办
|
||||||
*/
|
*/
|
||||||
NOTICE_TYPE_SUPERVISE("督办管理", "supe");
|
NOTICE_TYPE_SUPERVISE("督办管理", "supe"),
|
||||||
|
/**
|
||||||
|
* 考勤
|
||||||
|
*/
|
||||||
|
NOTICE_TYPE_ATTENDANCE("考勤消息", "attendance");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 文件类型名称
|
* 文件类型名称
|
||||||
|
|||||||
@ -0,0 +1,82 @@
|
|||||||
|
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.info((isEncode ? "加密操作," : "解密操作,") + "Aspect程序耗时:" + (endTime - startTime) + "ms");
|
log.debug((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;
|
||||||
|
|||||||
@ -5,9 +5,10 @@ import jakarta.annotation.Resource;
|
|||||||
import jakarta.servlet.http.HttpServletRequest;
|
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.jeecg.common.util.LoginUserUtils;
|
||||||
import org.apache.shiro.authz.AuthorizationException;
|
import cn.dev33.satoken.exception.NotLoginException;
|
||||||
import org.apache.shiro.authz.UnauthorizedException;
|
import cn.dev33.satoken.exception.NotPermissionException;
|
||||||
|
import cn.dev33.satoken.exception.NotRoleException;
|
||||||
import org.jeecg.common.api.dto.LogDTO;
|
import org.jeecg.common.api.dto.LogDTO;
|
||||||
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;
|
||||||
@ -112,22 +113,43 @@ public class JeecgBootExceptionHandler {
|
|||||||
return Result.error("数据库中已存在该记录");
|
return Result.error("数据库中已存在该记录");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ExceptionHandler({UnauthorizedException.class, AuthorizationException.class})
|
/**
|
||||||
public Result<?> handleAuthorizationException(AuthorizationException e){
|
* 处理Sa-Token未登录异常
|
||||||
|
*/
|
||||||
|
@ExceptionHandler(NotLoginException.class)
|
||||||
|
@ResponseStatus(HttpStatus.UNAUTHORIZED)
|
||||||
|
public Result<?> handleNotLoginException(NotLoginException e){
|
||||||
|
log.error("Sa-Token未登录异常: {}", e.getMessage());
|
||||||
|
return new Result(401, CommonConstant.TOKEN_IS_INVALID_MSG);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理Sa-Token无权限异常
|
||||||
|
*/
|
||||||
|
@ExceptionHandler(NotPermissionException.class)
|
||||||
|
public Result<?> handleNotPermissionException(NotPermissionException e){
|
||||||
log.error(e.getMessage(), e);
|
log.error(e.getMessage(), e);
|
||||||
return Result.noauth("没有权限,请联系管理员分配权限!");
|
return Result.noauth("没有权限,请联系管理员分配权限!");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理Sa-Token无角色异常
|
||||||
|
*/
|
||||||
|
@ExceptionHandler(NotRoleException.class)
|
||||||
|
public Result<?> handleNotRoleException(NotRoleException e){
|
||||||
|
log.error(e.getMessage(), e);
|
||||||
|
return Result.noauth("没有角色权限,请联系管理员分配角色!");
|
||||||
|
}
|
||||||
|
|
||||||
@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);
|
||||||
//update-begin---author:zyf ---date:20220411 for:处理Sentinel限流自定义异常
|
// 代码逻辑说明: 处理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());
|
||||||
}
|
}
|
||||||
@ -224,7 +246,6 @@ 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 异常
|
||||||
@ -243,7 +264,6 @@ 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)) {
|
||||||
// 文件上传过大异常时不能获取参数,否则会报错
|
// 文件上传过大异常时不能获取参数,否则会报错
|
||||||
@ -252,7 +272,6 @@ 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地址
|
||||||
@ -267,7 +286,7 @@ public class JeecgBootExceptionHandler {
|
|||||||
|
|
||||||
|
|
||||||
//获取登录用户信息
|
//获取登录用户信息
|
||||||
LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal();
|
LoginUser sysUser = LoginUserUtils.getSessionUser();
|
||||||
if(sysUser!=null){
|
if(sysUser!=null){
|
||||||
log.setUserid(sysUser.getUsername());
|
log.setUserid(sysUser.getUsername());
|
||||||
log.setUsername(sysUser.getRealname());
|
log.setUsername(sysUser.getRealname());
|
||||||
@ -276,7 +295,6 @@ public class JeecgBootExceptionHandler {
|
|||||||
|
|
||||||
baseCommonService.addLog(log);
|
baseCommonService.addLog(log);
|
||||||
}
|
}
|
||||||
//update-end---author:chenrui ---date:20240423 for:[QQYUN-8732]把错误的日志都抓取了 方便后续处理,单独弄个日志类型------------
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 是否文件过大异常
|
* 是否文件过大异常
|
||||||
|
|||||||
@ -6,10 +6,10 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
|||||||
import com.baomidou.mybatisplus.extension.service.IService;
|
import com.baomidou.mybatisplus.extension.service.IService;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.commons.beanutils.PropertyUtils;
|
import org.apache.commons.beanutils.PropertyUtils;
|
||||||
import org.apache.shiro.SecurityUtils;
|
|
||||||
import org.jeecg.common.api.vo.Result;
|
import org.jeecg.common.api.vo.Result;
|
||||||
import org.jeecg.common.system.query.QueryGenerator;
|
import org.jeecg.common.system.query.QueryGenerator;
|
||||||
import org.jeecg.common.system.vo.LoginUser;
|
import org.jeecg.common.system.vo.LoginUser;
|
||||||
|
import org.jeecg.common.util.LoginUserUtils;
|
||||||
import org.jeecg.common.util.oConvertUtils;
|
import org.jeecg.common.util.oConvertUtils;
|
||||||
import org.jeecg.config.JeecgBaseConfig;
|
import org.jeecg.config.JeecgBaseConfig;
|
||||||
import org.jeecgframework.poi.excel.ExcelImportUtil;
|
import org.jeecgframework.poi.excel.ExcelImportUtil;
|
||||||
@ -52,7 +52,7 @@ public class JeecgController<T, S extends IService<T>> {
|
|||||||
protected ModelAndView exportXls(HttpServletRequest request, T object, Class<T> clazz, String title) {
|
protected ModelAndView exportXls(HttpServletRequest request, T object, Class<T> clazz, String title) {
|
||||||
// Step.1 组装查询条件
|
// Step.1 组装查询条件
|
||||||
QueryWrapper<T> queryWrapper = QueryGenerator.initQueryWrapper(object, request.getParameterMap());
|
QueryWrapper<T> queryWrapper = QueryGenerator.initQueryWrapper(object, request.getParameterMap());
|
||||||
LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal();
|
LoginUser sysUser = LoginUserUtils.getSessionUser();
|
||||||
|
|
||||||
// 过滤选中数据
|
// 过滤选中数据
|
||||||
String selections = request.getParameter("selections");
|
String selections = request.getParameter("selections");
|
||||||
@ -68,12 +68,16 @@ 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);
|
||||||
//update-begin--Author:liusq Date:20210126 for:图片导出报错,ImageBasePath未设置--------------------
|
// 代码逻辑说明: 【QQYUN-13930】统一改成导出xlsx格式---
|
||||||
ExportParams exportParams=new ExportParams(title + "报表", "导出人:" + sysUser.getRealname(), title);
|
ExportParams exportParams=new ExportParams(title + "报表", "导出人:" + sysUser.getRealname(), title, ExcelType.XSSF);
|
||||||
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;
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
@ -90,18 +94,16 @@ public class JeecgController<T, S extends IService<T>> {
|
|||||||
protected ModelAndView exportXlsSheet(HttpServletRequest request, T object, Class<T> clazz, String title,String exportFields,Integer pageNum) {
|
protected ModelAndView exportXlsSheet(HttpServletRequest request, T object, Class<T> clazz, String title,String exportFields,Integer pageNum) {
|
||||||
// Step.1 组装查询条件
|
// Step.1 组装查询条件
|
||||||
QueryWrapper<T> queryWrapper = QueryGenerator.initQueryWrapper(object, request.getParameterMap());
|
QueryWrapper<T> queryWrapper = QueryGenerator.initQueryWrapper(object, request.getParameterMap());
|
||||||
LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal();
|
LoginUser sysUser = LoginUserUtils.getSessionUser();
|
||||||
// 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++) {
|
||||||
@ -142,7 +144,7 @@ public class JeecgController<T, S extends IService<T>> {
|
|||||||
protected ModelAndView exportXlsForBigData(HttpServletRequest request, T object, Class<T> clazz, String title,Integer pageSize) {
|
protected ModelAndView exportXlsForBigData(HttpServletRequest request, T object, Class<T> clazz, String title,Integer pageSize) {
|
||||||
// 组装查询条件
|
// 组装查询条件
|
||||||
QueryWrapper<T> queryWrapper = QueryGenerator.initQueryWrapper(object, request.getParameterMap());
|
QueryWrapper<T> queryWrapper = QueryGenerator.initQueryWrapper(object, request.getParameterMap());
|
||||||
LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal();
|
LoginUser sysUser = LoginUserUtils.getSessionUser();
|
||||||
// 计算分页数
|
// 计算分页数
|
||||||
double total = service.count();
|
double total = service.count();
|
||||||
int count = (int) Math.ceil(total / pageSize);
|
int count = (int) Math.ceil(total / pageSize);
|
||||||
@ -220,16 +222,15 @@ 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){
|
||||||
@ -237,7 +238,6 @@ 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();
|
||||||
|
|||||||
@ -13,7 +13,6 @@ import io.swagger.v3.oas.annotations.media.Schema;
|
|||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
import lombok.experimental.Accessors;
|
import lombok.experimental.Accessors;
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @Description: Entity基类
|
* @Description: Entity基类
|
||||||
|
|||||||
@ -97,7 +97,6 @@ public class QueryGenerator {
|
|||||||
return queryWrapper;
|
return queryWrapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
//update-begin---author:chenrui ---date:20240527 for:[TV360X-378]增加自定义字段查询规则功能------------
|
|
||||||
/**
|
/**
|
||||||
* 获取查询条件构造器QueryWrapper实例 通用查询条件已被封装完成
|
* 获取查询条件构造器QueryWrapper实例 通用查询条件已被封装完成
|
||||||
* @param searchObj 查询实体
|
* @param searchObj 查询实体
|
||||||
@ -112,7 +111,6 @@ 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 查询条件
|
||||||
@ -142,7 +140,6 @@ 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++) {
|
||||||
@ -188,7 +185,7 @@ public class QueryGenerator {
|
|||||||
queryWrapper.and(j -> j.like(field,vals[0]));
|
queryWrapper.and(j -> j.like(field,vals[0]));
|
||||||
}
|
}
|
||||||
}else {
|
}else {
|
||||||
//update-begin---author:chenrui ---date:20240527 for:[TV360X-378]增加自定义字段查询规则功能------------
|
// 代码逻辑说明: [TV360X-378]增加自定义字段查询规则功能------------
|
||||||
QueryRuleEnum rule;
|
QueryRuleEnum rule;
|
||||||
if(null != customRuleMap && customRuleMap.containsKey(name)) {
|
if(null != customRuleMap && customRuleMap.containsKey(name)) {
|
||||||
// 有自定义规则,使用自定义规则.
|
// 有自定义规则,使用自定义规则.
|
||||||
@ -197,7 +194,6 @@ 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)) {
|
||||||
@ -217,7 +213,6 @@ public class QueryGenerator {
|
|||||||
|
|
||||||
//高级查询
|
//高级查询
|
||||||
doSuperQuery(queryWrapper, parameterMap, fieldColumnMap);
|
doSuperQuery(queryWrapper, parameterMap, fieldColumnMap);
|
||||||
// update-end--Author:taoyan Date:20200923 for:issues/1671 如果字段加注解了@TableField(exist = false),不走DB查询-------
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -260,16 +255,16 @@ public class QueryGenerator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if(oConvertUtils.isNotEmpty(column)){
|
if(oConvertUtils.isNotEmpty(column)){
|
||||||
log.info("单字段排序规则>> column:" + column + ",排序方式:" + order);
|
log.debug("单字段排序规则>> 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.info("多字段排序规则>> sortInfoString:" + sortInfoString);
|
log.debug("多字段排序规则>> sortInfoString:" + sortInfoString);
|
||||||
List<OrderItem> orderItemList = SqlConcatUtil.getQueryConditionOrders(column, order, sortInfoString);
|
List<OrderItem> orderItemList = SqlConcatUtil.getQueryConditionOrders(column, order, sortInfoString);
|
||||||
log.info(orderItemList.toString());
|
log.debug(orderItemList.toString());
|
||||||
if (orderItemList != null && !orderItemList.isEmpty()) {
|
if (orderItemList != null && !orderItemList.isEmpty()) {
|
||||||
for (OrderItem item : orderItemList) {
|
for (OrderItem item : orderItemList) {
|
||||||
// 一、获取排序数据库字段
|
// 一、获取排序数据库字段
|
||||||
@ -321,13 +316,11 @@ 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)) {
|
||||||
//字典字段,去掉字典翻译文本后缀
|
//字典字段,去掉字典翻译文本后缀
|
||||||
@ -335,15 +328,12 @@ 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(","));
|
||||||
@ -354,12 +344,10 @@ 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"
|
||||||
@ -368,11 +356,9 @@ 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
|
||||||
@ -392,7 +378,6 @@ public class QueryGenerator {
|
|||||||
}
|
}
|
||||||
return exist;
|
return exist;
|
||||||
}
|
}
|
||||||
//update-end-author:taoyan date:2022-5-23 for: issues/3676 获取系统用户列表时,使用SQL注入生效
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 高级查询
|
* 高级查询
|
||||||
@ -405,14 +390,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;
|
||||||
}
|
}
|
||||||
// update-begin-author:sunjianlei date:20220119 for: 【JTC-573】 过滤空条件查询,防止 sql 拼接多余的 and
|
// 代码逻辑说明: 【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())
|
||||||
@ -423,7 +408,6 @@ 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 -> {
|
||||||
@ -438,14 +422,14 @@ public class QueryGenerator {
|
|||||||
|
|
||||||
log.debug("SuperQuery ==> " + rule.toString());
|
log.debug("SuperQuery ==> " + rule.toString());
|
||||||
|
|
||||||
//update-begin-author:taoyan date:20201228 for: 【高级查询】 oracle 日期等于查询报错
|
// 代码逻辑说明: 【高级查询】 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());
|
||||||
}
|
}
|
||||||
// update-begin--author:sunjianlei date:20210702 for:【/issues/I3VR8E】高级查询没有类型转换,查询参数都是字符串类型 ----
|
// 代码逻辑说明: 【/issues/I3VR8E】高级查询没有类型转换,查询参数都是字符串类型 ----
|
||||||
String dbType = rule.getDbType();
|
String dbType = rule.getDbType();
|
||||||
if (oConvertUtils.isNotEmpty(dbType)) {
|
if (oConvertUtils.isNotEmpty(dbType)) {
|
||||||
try {
|
try {
|
||||||
@ -478,9 +462,8 @@ public class QueryGenerator {
|
|||||||
log.error("高级查询值转换失败:", e);
|
log.error("高级查询值转换失败:", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// update-begin--author:sunjianlei date:20210702 for:【/issues/I3VR8E】高级查询没有类型转换,查询参数都是字符串类型 ----
|
// 代码逻辑说明: 【/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)) {
|
||||||
@ -496,7 +479,6 @@ 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());
|
||||||
}
|
}
|
||||||
@ -508,7 +490,7 @@ public class QueryGenerator {
|
|||||||
*/
|
*/
|
||||||
public static QueryRuleEnum convert2Rule(Object value) {
|
public static QueryRuleEnum convert2Rule(Object value) {
|
||||||
// 避免空数据
|
// 避免空数据
|
||||||
// update-begin-author:taoyan date:20210629 for: 查询条件输入空格导致return null后续判断导致抛出null异常
|
// 代码逻辑说明: 查询条件输入空格导致return null后续判断导致抛出null异常
|
||||||
if (value == null) {
|
if (value == null) {
|
||||||
return QueryRuleEnum.EQ;
|
return QueryRuleEnum.EQ;
|
||||||
}
|
}
|
||||||
@ -516,10 +498,8 @@ 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;
|
||||||
@ -535,14 +515,12 @@ 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
|
||||||
//update-begin-author:taoyan for: /issues/3382 默认带*就走模糊,但是如果只有一个*,那么走等于查询
|
// 代码逻辑说明: /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;
|
||||||
@ -567,12 +545,10 @@ 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;
|
||||||
}
|
}
|
||||||
@ -592,11 +568,10 @@ public class QueryGenerator {
|
|||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
String val = (value + "").toString().trim();
|
String val = (value + "").toString().trim();
|
||||||
//update-begin-author:taoyan date:20220302 for: 查询条件的值为等号(=)bug #3443
|
// 代码逻辑说明: 查询条件的值为等号(=)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 模糊查询之特殊字符下划线 (_、\)
|
||||||
@ -614,21 +589,19 @@ 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 {
|
||||||
//update-begin--Author:scott Date:20190724 for:initQueryWrapper组装sql查询条件错误 #284-------------------
|
// 代码逻辑说明: 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 -> {
|
||||||
@ -644,7 +617,6 @@ 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】修复逗号分割情况下没有转换类型,导致类型严格的数据库查询报错 -------------------
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -759,13 +731,12 @@ public class QueryGenerator {
|
|||||||
}else if(value instanceof String[]) {
|
}else if(value instanceof String[]) {
|
||||||
queryWrapper.in(name, (Object[]) value);
|
queryWrapper.in(name, (Object[]) value);
|
||||||
}
|
}
|
||||||
//update-begin-author:taoyan date:20200909 for:【bug】in 类型多值查询 不适配postgresql #1671
|
// 代码逻辑说明: 【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);
|
||||||
@ -782,7 +753,7 @@ public class QueryGenerator {
|
|||||||
case NOT_RIGHT_LIKE:
|
case NOT_RIGHT_LIKE:
|
||||||
queryWrapper.notLikeRight(name, value);
|
queryWrapper.notLikeRight(name, value);
|
||||||
break;
|
break;
|
||||||
//update-begin---author:chenrui ---date:20240527 for:[TV360X-378]下拉多框根据条件查询不出来:增加自定义字段查询规则功能------------
|
// 代码逻辑说明: [TV360X-378]下拉多框根据条件查询不出来:增加自定义字段查询规则功能------------
|
||||||
case LIKE_WITH_OR:
|
case LIKE_WITH_OR:
|
||||||
final String nameFinal = name;
|
final String nameFinal = name;
|
||||||
Object[] vals;
|
Object[] vals;
|
||||||
@ -791,7 +762,7 @@ public class QueryGenerator {
|
|||||||
} else if (value instanceof String[]) {
|
} else if (value instanceof String[]) {
|
||||||
vals = (Object[]) value;
|
vals = (Object[]) value;
|
||||||
}
|
}
|
||||||
//update-begin-author:taoyan date:20200909 for:【bug】in 类型多值查询 不适配postgresql #1671
|
// 代码逻辑说明: 【bug】in 类型多值查询 不适配postgresql #1671
|
||||||
else if (value.getClass().isArray()) {
|
else if (value.getClass().isArray()) {
|
||||||
vals = (Object[]) value;
|
vals = (Object[]) value;
|
||||||
} else {
|
} else {
|
||||||
@ -806,7 +777,6 @@ public class QueryGenerator {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
//update-end---author:chenrui ---date:20240527 for:[TV360X-378]下拉多框根据条件查询不出来:增加自定义字段查询规则功能------------
|
|
||||||
default:
|
default:
|
||||||
log.info("--查询规则未匹配到---");
|
log.info("--查询规则未匹配到---");
|
||||||
break;
|
break;
|
||||||
@ -820,10 +790,8 @@ 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 字段,防止前端排序报错 ------
|
|
||||||
;
|
;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -836,13 +804,12 @@ 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;
|
||||||
//update-begin-author:taoyan date:2023-6-1 for:QQYUN-5441 【简流】获取多个用户/部门/角色 设置部门查询 报错
|
// 代码逻辑说明: 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;
|
||||||
@ -879,9 +846,8 @@ 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 {
|
||||||
//update-begin---author:chenrui ---date:20241125 for:[issues/7481]多租户模式下 数据权限使用变量:#{tenant_id} 报错------------
|
// 代码逻辑说明: [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} 报错------------
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -935,9 +901,8 @@ public class QueryGenerator {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
Set<String> varParams = new HashSet<String>();
|
Set<String> varParams = new HashSet<String>();
|
||||||
//update-begin---author:chenrui ---date:20250108 for:[QQYUN-10785]数据权限,查看自己拥有部门的权限中存在问题 #7288------------
|
// 代码逻辑说明: [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);
|
||||||
@ -993,9 +958,8 @@ 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;
|
||||||
//update-begin---author:chenrui ---date:20240527 for:[TV360X-539]数据权限,配置日期等于条件时后端报转换错误------------
|
// 代码逻辑说明: [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);
|
||||||
|
|||||||
@ -1,31 +1,23 @@
|
|||||||
package org.jeecg.common.system.util;
|
package org.jeecg.common.system.util;
|
||||||
|
|
||||||
import com.auth0.jwt.JWT;
|
import cn.dev33.satoken.stp.StpUtil;
|
||||||
import com.auth0.jwt.JWTVerifier;
|
|
||||||
import com.auth0.jwt.algorithms.Algorithm;
|
|
||||||
import com.auth0.jwt.exceptions.JWTDecodeException;
|
|
||||||
import com.auth0.jwt.interfaces.DecodedJWT;
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.google.common.base.Joiner;
|
import com.google.common.base.Joiner;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.util.Date;
|
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import jakarta.servlet.ServletResponse;
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
|
||||||
import jakarta.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.jeecg.common.api.vo.Result;
|
|
||||||
import org.jeecg.common.constant.CommonConstant;
|
import org.jeecg.common.constant.CommonConstant;
|
||||||
import org.jeecg.common.constant.DataBaseConstant;
|
|
||||||
import org.jeecg.common.constant.SymbolConstant;
|
import org.jeecg.common.constant.SymbolConstant;
|
||||||
import org.jeecg.common.constant.TenantConstant;
|
import org.jeecg.common.constant.TenantConstant;
|
||||||
|
import org.jeecg.common.util.LoginUserUtils;
|
||||||
|
import org.jeecg.common.api.vo.Result;
|
||||||
|
import org.jeecg.common.constant.DataBaseConstant;
|
||||||
import org.jeecg.common.exception.JeecgBootException;
|
import org.jeecg.common.exception.JeecgBootException;
|
||||||
import org.jeecg.common.system.vo.LoginUser;
|
import org.jeecg.common.system.vo.LoginUser;
|
||||||
import org.jeecg.common.system.vo.SysUserCacheInfo;
|
import org.jeecg.common.system.vo.SysUserCacheInfo;
|
||||||
@ -36,93 +28,74 @@ import org.jeecg.common.util.oConvertUtils;
|
|||||||
/**
|
/**
|
||||||
* @Author Scott
|
* @Author Scott
|
||||||
* @Date 2018-07-12 14:23
|
* @Date 2018-07-12 14:23
|
||||||
* @Desc JWT工具类
|
* @Desc JWT工具类 - 已迁移到Sa-Token,此类作为兼容层保留
|
||||||
**/
|
**/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class JwtUtil {
|
public class JwtUtil {
|
||||||
|
|
||||||
/**Token有效期为7天(Token在reids中缓存时间为两倍)*/
|
|
||||||
public static final long EXPIRE_TIME = (7 * 12) * 60 * 60 * 1000;
|
|
||||||
static final String WELL_NUMBER = SymbolConstant.WELL_NUMBER + SymbolConstant.LEFT_CURLY_BRACKET;
|
static final String WELL_NUMBER = SymbolConstant.WELL_NUMBER + SymbolConstant.LEFT_CURLY_BRACKET;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
* 返回错误 JSON 字符串(用于 Sa-Token Filter)
|
||||||
* @param response
|
* @param code 错误码
|
||||||
* @param code
|
* @param errorMsg 错误信息
|
||||||
* @param errorMsg
|
* @return JSON 字符串
|
||||||
*/
|
*/
|
||||||
public static void responseError(HttpServletResponse response, Integer code, String errorMsg) {
|
public static String responseErrorJson(Integer code, String errorMsg) {
|
||||||
try {
|
try {
|
||||||
Result jsonResult = new Result(code, errorMsg);
|
Result jsonResult = new Result(code, errorMsg);
|
||||||
jsonResult.setSuccess(false);
|
jsonResult.setSuccess(false);
|
||||||
|
|
||||||
// 设置响应头和内容类型
|
|
||||||
response.setStatus(code);
|
|
||||||
response.setHeader("Content-type", "text/html;charset=UTF-8");
|
|
||||||
response.setContentType("application/json;charset=UTF-8");
|
|
||||||
// 使用 ObjectMapper 序列化为 JSON 字符串
|
|
||||||
ObjectMapper objectMapper = new ObjectMapper();
|
ObjectMapper objectMapper = new ObjectMapper();
|
||||||
String json = objectMapper.writeValueAsString(jsonResult);
|
return objectMapper.writeValueAsString(jsonResult);
|
||||||
response.getWriter().write(json);
|
|
||||||
response.getWriter().flush();
|
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
log.error(e.getMessage(), e);
|
log.error("生成错误 JSON 失败: {}", e.getMessage());
|
||||||
|
// 返回备用的硬编码 JSON
|
||||||
|
return "{\"success\":false,\"message\":\"" + errorMsg + "\",\"code\":" + code + ",\"result\":null,\"timestamp\":" + System.currentTimeMillis() + "}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 校验token是否正确
|
* 校验token是否正确
|
||||||
*
|
* 注意:此方法已废弃,使用Sa-Token自动校验
|
||||||
* @param token 密钥
|
*
|
||||||
* @param secret 用户的密码
|
* @param token
|
||||||
* @return 是否正确
|
* @return
|
||||||
*/
|
*/
|
||||||
public static boolean verify(String token, String username, String secret) {
|
@Deprecated
|
||||||
|
public static boolean verify(String token){
|
||||||
try {
|
try {
|
||||||
// 根据密码生成JWT效验器
|
// 使用Sa-Token验证
|
||||||
Algorithm algorithm = Algorithm.HMAC256(secret);
|
return StpUtil.getLoginIdByToken(token) != null;
|
||||||
JWTVerifier verifier = JWT.require(algorithm).withClaim("username", username).build();
|
|
||||||
// 效验TOKEN
|
|
||||||
DecodedJWT jwt = verifier.verify(token);
|
|
||||||
return true;
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error(e.getMessage(), e);
|
log.warn(e.getMessage(), e);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获得token中的信息无需secret解密也能获得
|
* 获得Token中的用户名(不校验token是否有效)
|
||||||
*
|
* <p>注意:现在 loginId 就是 username,直接返回
|
||||||
* @return token中包含的用户名
|
*
|
||||||
|
* @param token JWT token
|
||||||
|
* @return 用户名(username),如果 token 无效则返回 null
|
||||||
*/
|
*/
|
||||||
public static String getUsername(String token) {
|
public static String getUsername(String token){
|
||||||
try {
|
try {
|
||||||
DecodedJWT jwt = JWT.decode(token);
|
if(oConvertUtils.isEmpty(token)) {
|
||||||
return jwt.getClaim("username").asString();
|
return null;
|
||||||
} catch (JWTDecodeException e) {
|
}
|
||||||
log.error(e.getMessage(), e);
|
// Sa-Token 的 loginId 现在就是 username,直接返回
|
||||||
|
Object loginId = StpUtil.getLoginIdByToken(token);
|
||||||
|
return loginId != null ? loginId.toString() : null;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("获取用户名失败: {}", e.getMessage());
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 生成签名,5min后过期
|
|
||||||
*
|
|
||||||
* @param username 用户名
|
|
||||||
* @param secret 用户的密码
|
|
||||||
* @return 加密的token
|
|
||||||
*/
|
|
||||||
public static String sign(String username, String secret) {
|
|
||||||
Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
|
|
||||||
Algorithm algorithm = Algorithm.HMAC256(secret);
|
|
||||||
// 附带username信息
|
|
||||||
return JWT.create().withClaim("username", username).withExpiresAt(date).sign(algorithm);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据request中的token获取用户账号
|
* 根据request中的token获取用户账号
|
||||||
|
* 注意:此方法已适配Sa-Token
|
||||||
*
|
*
|
||||||
* @param request
|
* @param request
|
||||||
* @return
|
* @return
|
||||||
@ -136,9 +109,9 @@ public class JwtUtil {
|
|||||||
}
|
}
|
||||||
return username;
|
return username;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 从session中获取变量
|
* 从session中获取变量
|
||||||
* @param key
|
* @param key
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
@ -149,7 +122,7 @@ public class JwtUtil {
|
|||||||
String wellNumber = WELL_NUMBER;
|
String wellNumber = WELL_NUMBER;
|
||||||
|
|
||||||
if(key.indexOf(SymbolConstant.RIGHT_CURLY_BRACKET)!=-1){
|
if(key.indexOf(SymbolConstant.RIGHT_CURLY_BRACKET)!=-1){
|
||||||
moshi = key.substring(key.indexOf("}")+1);
|
moshi = key.substring(key.indexOf("}")+1);
|
||||||
}
|
}
|
||||||
String returnValue = null;
|
String returnValue = null;
|
||||||
if (key.contains(wellNumber)) {
|
if (key.contains(wellNumber)) {
|
||||||
@ -163,16 +136,16 @@ public class JwtUtil {
|
|||||||
if(returnValue!=null){returnValue = returnValue + moshi;}
|
if(returnValue!=null){returnValue = returnValue + moshi;}
|
||||||
return returnValue;
|
return returnValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 从当前用户中获取变量
|
* 从当前用户中获取变量
|
||||||
* @param key
|
* @param key
|
||||||
* @param user
|
* @param user
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
public static String getUserSystemData(String key, SysUserCacheInfo user) {
|
public static String getUserSystemData(String key, SysUserCacheInfo user) {
|
||||||
//1.优先获取 SysUserCacheInfo
|
//1.优先获取 SysUserCacheInfo
|
||||||
if(user==null) {
|
if (user == null) {
|
||||||
try {
|
try {
|
||||||
user = JeecgDataAutorUtils.loadUserInfo();
|
user = JeecgDataAutorUtils.loadUserInfo();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
@ -182,84 +155,82 @@ public class JwtUtil {
|
|||||||
//2.通过shiro获取登录用户信息
|
//2.通过shiro获取登录用户信息
|
||||||
LoginUser sysUser = null;
|
LoginUser sysUser = null;
|
||||||
try {
|
try {
|
||||||
sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal();
|
sysUser = (LoginUser) LoginUserUtils.getSessionUser();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.warn("SecurityUtils.getSubject() 获取用户信息异常:" + e.getMessage());
|
log.warn("SecurityUtils.getSubject() 获取用户信息异常:" + e.getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
//#{sys_user_code}%
|
//#{sys_user_code}%
|
||||||
String moshi = "";
|
String moshi = "";
|
||||||
String wellNumber = WELL_NUMBER;
|
String wellNumber = WELL_NUMBER;
|
||||||
if(key.indexOf(SymbolConstant.RIGHT_CURLY_BRACKET)!=-1){
|
if (key.indexOf(SymbolConstant.RIGHT_CURLY_BRACKET) != -1) {
|
||||||
moshi = key.substring(key.indexOf("}")+1);
|
moshi = key.substring(key.indexOf("}") + 1);
|
||||||
}
|
}
|
||||||
String returnValue = null;
|
String returnValue = null;
|
||||||
//针对特殊标示处理#{sysOrgCode},判断替换
|
//针对特殊标示处理#{sysOrgCode},判断替换
|
||||||
if (key.contains(wellNumber)) {
|
if (key.contains(wellNumber)) {
|
||||||
key = key.substring(2,key.indexOf("}"));
|
key = key.substring(2, key.indexOf("}"));
|
||||||
} 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+]$")) {
|
||||||
key = key.substring(1,key.length()-1);
|
key = key.substring(1, key.length() - 1);
|
||||||
multiStr = true;
|
multiStr = true;
|
||||||
} 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();
|
||||||
}
|
}
|
||||||
//替换为当前系统时间(年月日时分秒)
|
//替换为当前系统时间(年月日时分秒)
|
||||||
else if (key.equals(DataBaseConstant.SYS_TIME)|| key.toLowerCase().equals(DataBaseConstant.SYS_TIME_TABLE)) {
|
else if (key.equals(DataBaseConstant.SYS_TIME) || key.toLowerCase().equals(DataBaseConstant.SYS_TIME_TABLE)) {
|
||||||
returnValue = DateUtils.now();
|
returnValue = DateUtils.now();
|
||||||
}
|
}
|
||||||
//流程状态默认值(默认未发起)
|
//流程状态默认值(默认未发起)
|
||||||
else if (key.equals(DataBaseConstant.BPM_STATUS)|| key.toLowerCase().equals(DataBaseConstant.BPM_STATUS_TABLE)) {
|
else if (key.equals(DataBaseConstant.BPM_STATUS) || key.toLowerCase().equals(DataBaseConstant.BPM_STATUS_TABLE)) {
|
||||||
returnValue = "1";
|
returnValue = "1";
|
||||||
}
|
}
|
||||||
|
|
||||||
//后台任务获取用户信息异常,导致程序中断
|
//后台任务获取用户信息异常,导致程序中断
|
||||||
if(sysUser==null && user==null){
|
if (sysUser == null && user == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
//替换为系统登录用户帐号
|
//替换为系统登录用户帐号
|
||||||
if (key.equals(DataBaseConstant.SYS_USER_CODE)|| key.toLowerCase().equals(DataBaseConstant.SYS_USER_CODE_TABLE)) {
|
if (key.equals(DataBaseConstant.SYS_USER_CODE) || key.toLowerCase().equals(DataBaseConstant.SYS_USER_CODE_TABLE)) {
|
||||||
if(user==null) {
|
if (user == null) {
|
||||||
returnValue = sysUser.getUsername();
|
returnValue = sysUser.getUsername();
|
||||||
}else {
|
} else {
|
||||||
returnValue = user.getSysUserCode();
|
returnValue = user.getSysUserCode();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 替换为系统登录用户ID
|
// 替换为系统登录用户ID
|
||||||
else if (key.equals(DataBaseConstant.SYS_USER_ID) || key.equalsIgnoreCase(DataBaseConstant.SYS_USER_ID_TABLE)) {
|
else if (key.equals(DataBaseConstant.SYS_USER_ID) || key.equalsIgnoreCase(DataBaseConstant.SYS_USER_ID_TABLE)) {
|
||||||
if(user==null) {
|
if (user == null) {
|
||||||
returnValue = sysUser.getId();
|
returnValue = sysUser.getId();
|
||||||
}else {
|
} else {
|
||||||
returnValue = user.getSysUserId();
|
returnValue = user.getSysUserId();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//替换为系统登录用户真实名字
|
//替换为系统登录用户真实名字
|
||||||
else if (key.equals(DataBaseConstant.SYS_USER_NAME)|| key.toLowerCase().equals(DataBaseConstant.SYS_USER_NAME_TABLE)) {
|
else if (key.equals(DataBaseConstant.SYS_USER_NAME) || key.toLowerCase().equals(DataBaseConstant.SYS_USER_NAME_TABLE)) {
|
||||||
if(user==null) {
|
if (user == null) {
|
||||||
returnValue = sysUser.getRealname();
|
returnValue = sysUser.getRealname();
|
||||||
}else {
|
} else {
|
||||||
returnValue = user.getSysUserName();
|
returnValue = user.getSysUserName();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//替换为系统用户登录所使用的机构编码
|
//替换为系统用户登录所使用的机构编码
|
||||||
else if (key.equals(DataBaseConstant.SYS_ORG_CODE)|| key.toLowerCase().equals(DataBaseConstant.SYS_ORG_CODE_TABLE)) {
|
else if (key.equals(DataBaseConstant.SYS_ORG_CODE) || key.toLowerCase().equals(DataBaseConstant.SYS_ORG_CODE_TABLE)) {
|
||||||
if(user==null) {
|
if (user == null) {
|
||||||
returnValue = sysUser.getOrgCode();
|
returnValue = sysUser.getOrgCode();
|
||||||
}else {
|
} else {
|
||||||
returnValue = user.getSysOrgCode();
|
returnValue = user.getSysOrgCode();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -274,24 +245,17 @@ public class JwtUtil {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//替换为系统用户所拥有的所有机构编码
|
//替换为系统用户所拥有的所有机构编码
|
||||||
else if (key.equals(DataBaseConstant.SYS_MULTI_ORG_CODE)|| key.toLowerCase().equals(DataBaseConstant.SYS_MULTI_ORG_CODE_TABLE)) {
|
else if (key.equals(DataBaseConstant.SYS_MULTI_ORG_CODE) || key.toLowerCase().equals(DataBaseConstant.SYS_MULTI_ORG_CODE_TABLE)) {
|
||||||
if(user==null){
|
if (user == null) {
|
||||||
//TODO 暂时使用用户登录部门,存在逻辑缺陷,不是用户所拥有的部门
|
|
||||||
returnValue = sysUser.getOrgCode();
|
returnValue = sysUser.getOrgCode();
|
||||||
//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);
|
||||||
//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 {
|
|
||||||
//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 + "'";
|
||||||
@ -299,9 +263,7 @@ 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------------
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -315,21 +277,17 @@ public class JwtUtil {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//update-begin-author:taoyan date:20210330 for:多租户ID作为系统变量
|
// 多租户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);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.warn("获取系统租户异常:" + e.getMessage());
|
log.warn("获取系统租户异常:" + e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//update-end-author:taoyan date:20210330 for:多租户ID作为系统变量
|
if (returnValue != null) {
|
||||||
if(returnValue!=null){returnValue = returnValue + moshi;}
|
returnValue = returnValue + moshi;
|
||||||
|
}
|
||||||
return returnValue;
|
return returnValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// public static void main(String[] args) {
|
|
||||||
// String token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE1NjUzMzY1MTMsInVzZXJuYW1lIjoiYWRtaW4ifQ.xjhud_tWCNYBOg_aRlMgOdlZoWFFKB_givNElHNw3X0";
|
|
||||||
// System.out.println(JwtUtil.getUsername(token));
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -67,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.info("【枚举字典加载】开始初始化枚举字典数据...");
|
log.debug("【枚举字典加载】开始初始化枚举字典数据...");
|
||||||
|
|
||||||
initEnumDictData();
|
initEnumDictData();
|
||||||
initialized = true;
|
initialized = true;
|
||||||
|
|
||||||
long endTime = System.currentTimeMillis();
|
long endTime = System.currentTimeMillis();
|
||||||
log.info("【枚举字典加载】枚举字典数据初始化完成,共加载 {} 个字典,总耗时: {}ms", enumDictData.size(), endTime - startTime);
|
log.debug("【枚举字典加载】枚举字典数据初始化完成,共加载 {} 个字典,总耗时: {}ms", enumDictData.size(), endTime - startTime);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -103,7 +103,7 @@ public class ResourceUtil {
|
|||||||
}
|
}
|
||||||
|
|
||||||
long scanEndTime = System.currentTimeMillis();
|
long scanEndTime = System.currentTimeMillis();
|
||||||
log.info("【枚举字典加载】文件扫描完成,总共找到 {} 个枚举类文件,扫描耗时: {}ms", allResources.size(), scanEndTime - scanStartTime);
|
log.debug("【枚举字典加载】文件扫描完成,总共找到 {} 个枚举类文件,扫描耗时: {}ms", allResources.size(), scanEndTime - scanStartTime);
|
||||||
|
|
||||||
MetadataReaderFactory readerFactory = new CachingMetadataReaderFactory(resourcePatternResolver);
|
MetadataReaderFactory readerFactory = new CachingMetadataReaderFactory(resourcePatternResolver);
|
||||||
|
|
||||||
@ -126,7 +126,7 @@ public class ResourceUtil {
|
|||||||
}
|
}
|
||||||
|
|
||||||
long processEndTime = System.currentTimeMillis();
|
long processEndTime = System.currentTimeMillis();
|
||||||
log.info("【枚举字典加载】处理完成,实际处理 {} 个带注解的枚举类,处理耗时: {}ms", processedCount, processEndTime - processStartTime);
|
log.debug("【枚举字典加载】处理完成,实际处理 {} 个带注解的枚举类,处理耗时: {}ms", processedCount, processEndTime - processStartTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -150,7 +150,7 @@ public class SqlConcatUtil {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static String getInConditionValue(Object value,boolean isString) {
|
private static String getInConditionValue(Object value,boolean isString) {
|
||||||
//update-begin-author:taoyan date:20210628 for: 查询条件如果输入,导致sql报错
|
// 代码逻辑说明: 查询条件如果输入,导致sql报错
|
||||||
String[] temp = value.toString().split(",");
|
String[] temp = value.toString().split(",");
|
||||||
if(temp.length==0){
|
if(temp.length==0){
|
||||||
return "('')";
|
return "('')";
|
||||||
@ -168,7 +168,6 @@ public class SqlConcatUtil {
|
|||||||
}else {
|
}else {
|
||||||
return "("+value.toString()+")";
|
return "("+value.toString()+")";
|
||||||
}
|
}
|
||||||
//update-end-author:taoyan date:20210628 for: 查询条件如果输入,导致sql报错
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -215,7 +214,6 @@ 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())) {
|
||||||
@ -236,7 +234,6 @@ public class SqlConcatUtil {
|
|||||||
return "'%" + str + "%'";
|
return "'%" + str + "%'";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//update-end-author:taoyan date:2022-6-30 for: issues/3810 数据权限规则问题
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -153,9 +153,9 @@ public class CommonUtils {
|
|||||||
*/
|
*/
|
||||||
public static String uploadLocal(MultipartFile mf,String bizPath,String uploadpath){
|
public static String uploadLocal(MultipartFile mf,String bizPath,String uploadpath){
|
||||||
try {
|
try {
|
||||||
//update-begin-author:liusq date:20210809 for: 过滤上传文件类型
|
// 文件安全校验,防止上传漏洞文件
|
||||||
SsrfFileTypeFilter.checkUploadFileType(mf);
|
SsrfFileTypeFilter.checkUploadFileType(mf, bizPath);
|
||||||
//update-end-author:liusq date:20210809 for: 过滤上传文件类型
|
|
||||||
String fileName = null;
|
String fileName = null;
|
||||||
File file = new File(uploadpath + File.separator + bizPath + File.separator );
|
File file = new File(uploadpath + File.separator + bizPath + File.separator );
|
||||||
if (!file.exists()) {
|
if (!file.exists()) {
|
||||||
|
|||||||
@ -10,7 +10,9 @@ 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;
|
||||||
@ -61,17 +63,21 @@ 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);
|
||||||
@ -81,7 +87,7 @@ public class DySmsHelper {
|
|||||||
//验证json参数
|
//验证json参数
|
||||||
validateParam(templateParamJson,dySmsEnum);
|
validateParam(templateParamJson,dySmsEnum);
|
||||||
|
|
||||||
//update-begin---author:wangshuai---date:2024-11-05---for:【QQYUN-9422】短信模板管理,阿里云---
|
// 代码逻辑说明: 【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())){
|
||||||
@ -97,7 +103,6 @@ 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,7 @@
|
|||||||
package org.jeecg.common.util;
|
package org.jeecg.common.util;
|
||||||
|
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
|
||||||
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;
|
||||||
|
|||||||
@ -38,14 +38,12 @@ 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 邮箱发送失败,需要换写法---
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,175 @@
|
|||||||
|
package org.jeecg.common.util;
|
||||||
|
|
||||||
|
import cn.dev33.satoken.stp.StpUtil;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.jeecg.common.system.vo.LoginUser;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 登录用户工具类
|
||||||
|
* 替代原有的Shiro SecurityUtils工具类
|
||||||
|
* @author jeecg-boot
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
public class LoginUserUtils {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Session中存储登录用户信息的key
|
||||||
|
*/
|
||||||
|
private static final String SESSION_KEY_LOGIN_USER = "loginUser";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行登录并设置用户信息到Session(推荐)
|
||||||
|
*
|
||||||
|
* <p>此方法会:
|
||||||
|
* <ul>
|
||||||
|
* <li>1. 调用 StpUtil.login(username) 生成token和session</li>
|
||||||
|
* <li>2. 将 LoginUser 存入 Session 缓存(清除不必要的字段(密码等15个字段)</li>
|
||||||
|
* <li>3. 返回生成的 token</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* @param sysUser 完整的用户对象(从数据库查询得到)
|
||||||
|
* @return 生成的 token
|
||||||
|
*/
|
||||||
|
public static String doLogin(LoginUser sysUser) {
|
||||||
|
if (sysUser == null) {
|
||||||
|
throw new IllegalArgumentException("用户对象不能为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. 获取 username
|
||||||
|
String username = sysUser.getUsername();
|
||||||
|
|
||||||
|
if (username == null || username.trim().isEmpty()) {
|
||||||
|
throw new IllegalArgumentException("用户名不能为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Sa-Token 登录(使用 username 作为 loginId)
|
||||||
|
StpUtil.login(username);
|
||||||
|
|
||||||
|
// 3. 用户信息到 LoginUser 并存入 Session
|
||||||
|
setSessionUser(sysUser);
|
||||||
|
|
||||||
|
// 4. 返回生成的 token
|
||||||
|
return StpUtil.getTokenValue();
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException("登录失败: " + e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前登录用户信息
|
||||||
|
*
|
||||||
|
* <p>说明:
|
||||||
|
* <ul>
|
||||||
|
* <li>对于需要认证的接口:Sa-Token Filter 已经校验过登录状态,此方法必然能获取到用户</li>
|
||||||
|
* <li>对于已排除拦截的接口:如果未登录或获取失败则返回 null,由业务代码自行判断处理</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* @return 登录用户对象,如果未登录或session中没有则返回null
|
||||||
|
*/
|
||||||
|
public static LoginUser getSessionUser() {
|
||||||
|
// 尝试从Sa-Token的Session中获取用户信息
|
||||||
|
Object loginUser = StpUtil.getSession().get(SESSION_KEY_LOGIN_USER);
|
||||||
|
if (loginUser instanceof LoginUser) {
|
||||||
|
return (LoginUser) loginUser;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据指定的 token 获取登录用户信息
|
||||||
|
*
|
||||||
|
* <p>适用场景:已排除拦截的接口(如 WebSocket),需要显式传入 token 来获取用户信息
|
||||||
|
*
|
||||||
|
* <p>实现方式:临时切换到该 token 对应的会话,然后获取用户信息
|
||||||
|
*
|
||||||
|
* @param token JWT token
|
||||||
|
* @return 登录用户对象,如果 token 无效或session中没有则返回null
|
||||||
|
*/
|
||||||
|
public static LoginUser getSessionUser(String token) {
|
||||||
|
try {
|
||||||
|
// 根据 token 获取登录ID
|
||||||
|
Object loginId = StpUtil.getLoginIdByToken(token);
|
||||||
|
if (loginId == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 临时切换到该 token 对应的登录会话
|
||||||
|
StpUtil.switchTo(loginId);
|
||||||
|
|
||||||
|
// 直接调用无参方法获取用户信息
|
||||||
|
return getSessionUser();
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.debug("根据token获取用户信息失败: {}", e.getMessage());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置当前登录用户信息到Session
|
||||||
|
*
|
||||||
|
* <p>为减少 Redis 存储和保障安全,只保留必要的核心字段:
|
||||||
|
* <ul>
|
||||||
|
* <li>id, username, realname - 基础用户信息</li>
|
||||||
|
* <li>orgCode, orgId, departIds - 部门和数据权限</li>
|
||||||
|
* <li>roleCode - 角色权限</li>
|
||||||
|
* <li>loginTenantId, relTenantIds - 多租户</li>
|
||||||
|
* <li>avatar - 用户头像</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p>⚠️ 注意:调用此方法前需要先调用 StpUtil.login()
|
||||||
|
*
|
||||||
|
* @param loginUser 登录用户对象
|
||||||
|
*/
|
||||||
|
public static void setSessionUser(LoginUser loginUser) {
|
||||||
|
if (loginUser == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ⚠️ 安全与性能:清除不必要的字段,减少 Redis 存储
|
||||||
|
loginUser.setPassword(null); // 密码(安全)
|
||||||
|
loginUser.setWorkNo(null); // 工号
|
||||||
|
loginUser.setBirthday(null); // 生日
|
||||||
|
loginUser.setSex(null); // 性别
|
||||||
|
loginUser.setEmail(null); // 邮箱
|
||||||
|
loginUser.setPhone(null); // 手机号
|
||||||
|
loginUser.setStatus(null); // 状态
|
||||||
|
loginUser.setDelFlag(null); // 删除标志
|
||||||
|
loginUser.setActivitiSync(null); // 工作流同步
|
||||||
|
loginUser.setCreateTime(null); // 创建时间
|
||||||
|
loginUser.setUserIdentity(null); // 用户身份
|
||||||
|
loginUser.setPost(null); // 职务
|
||||||
|
loginUser.setTelephone(null); // 座机
|
||||||
|
loginUser.setRelTenantIds(null); // 关联租户
|
||||||
|
loginUser.setMainDepPostId(null); // 主岗位
|
||||||
|
|
||||||
|
StpUtil.getSession().set(SESSION_KEY_LOGIN_USER, loginUser);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前登录用户名(推荐使用此方法,语义更清晰)
|
||||||
|
* @return 用户名(username)
|
||||||
|
*/
|
||||||
|
public static String getUsername() {
|
||||||
|
return StpUtil.getLoginIdAsString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查是否已登录
|
||||||
|
* @return true-已登录,false-未登录
|
||||||
|
*/
|
||||||
|
public static boolean isLogin() {
|
||||||
|
return StpUtil.isLogin();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 退出登录
|
||||||
|
*/
|
||||||
|
public static void logout() {
|
||||||
|
StpUtil.logout();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ -55,13 +55,11 @@ public class MinioUtil {
|
|||||||
*/
|
*/
|
||||||
public static String upload(MultipartFile file, String bizPath, String customBucket) throws Exception {
|
public static String upload(MultipartFile file, String bizPath, String customBucket) throws Exception {
|
||||||
String fileUrl = "";
|
String fileUrl = "";
|
||||||
//update-begin-author:wangshuai date:20201012 for: 过滤上传文件夹名特殊字符,防止攻击
|
// 业务路径过滤,防止攻击
|
||||||
bizPath = StrAttackFilter.filter(bizPath);
|
bizPath = StrAttackFilter.filter(bizPath);
|
||||||
//update-end-author:wangshuai date:20201012 for: 过滤上传文件夹名特殊字符,防止攻击
|
|
||||||
|
|
||||||
//update-begin-author:liusq date:20210809 for: 过滤上传文件类型
|
// 文件安全校验,防止上传漏洞文件
|
||||||
SsrfFileTypeFilter.checkUploadFileType(file);
|
SsrfFileTypeFilter.checkUploadFileType(file, bizPath);
|
||||||
//update-end-author:liusq date:20210809 for: 过滤上传文件类型
|
|
||||||
|
|
||||||
String newBucket = bucketName;
|
String newBucket = bucketName;
|
||||||
if(oConvertUtils.isNotEmpty(customBucket)){
|
if(oConvertUtils.isNotEmpty(customBucket)){
|
||||||
@ -163,11 +161,10 @@ 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{
|
||||||
//update-begin---author:liusq Date:20220121 for:获取文件外链报错提示method不能为空,导致文件下载和预览失败----
|
// 代码逻辑说明: 获取文件外链报错提示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){
|
||||||
|
|||||||
@ -0,0 +1,78 @@
|
|||||||
|
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,9 +99,8 @@ 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);
|
||||||
//update-begin-author:sccott date:20180815 for:中文作为用户名时,加密的密码windows和linux会得到不同的结果 gitee/issues/IZUD7
|
// 代码逻辑说明: 中文作为用户名时,加密的密码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.SimpleClientHttpRequestFactory;
|
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
|
||||||
import org.springframework.http.converter.StringHttpMessageConverter;
|
import org.springframework.http.converter.StringHttpMessageConverter;
|
||||||
import org.springframework.web.client.RestTemplate;
|
import org.springframework.web.client.RestTemplate;
|
||||||
|
|
||||||
@ -56,12 +56,19 @@ public class RestUtil {
|
|||||||
private final static RestTemplate RT;
|
private final static RestTemplate RT;
|
||||||
|
|
||||||
static {
|
static {
|
||||||
SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
|
// 解决[issues/8859]online表单java增强失效------------
|
||||||
|
// 使用 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)
|
||||||
RT.getMessageConverters().set(1, new StringHttpMessageConverter(StandardCharsets.UTF_8));
|
for (int i = 0; i < RT.getMessageConverters().size(); i++) {
|
||||||
|
if (RT.getMessageConverters().get(i) instanceof StringHttpMessageConverter) {
|
||||||
|
RT.getMessageConverters().set(i, new StringHttpMessageConverter(StandardCharsets.UTF_8));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static RestTemplate getRestTemplate() {
|
public static RestTemplate getRestTemplate() {
|
||||||
@ -216,6 +223,16 @@ 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.info(" 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);
|
||||||
@ -250,12 +267,18 @@ public class RestUtil {
|
|||||||
// 创建自定义RestTemplate(如果需要设置超时)
|
// 创建自定义RestTemplate(如果需要设置超时)
|
||||||
RestTemplate restTemplate = RT;
|
RestTemplate restTemplate = RT;
|
||||||
if (timeout > 0) {
|
if (timeout > 0) {
|
||||||
SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
|
// 代码逻辑说明: [issues/8859]online表单java增强失效------------
|
||||||
|
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)
|
||||||
restTemplate.getMessageConverters().set(1, new StringHttpMessageConverter(StandardCharsets.UTF_8));
|
for (int i = 0; i < restTemplate.getMessageConverters().size(); i++) {
|
||||||
|
if (restTemplate.getMessageConverters().get(i) instanceof StringHttpMessageConverter) {
|
||||||
|
restTemplate.getMessageConverters().set(i, new StringHttpMessageConverter(StandardCharsets.UTF_8));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 请求体
|
// 请求体
|
||||||
@ -273,11 +296,21 @@ 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.info(" 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,37 +0,0 @@
|
|||||||
package org.jeecg.common.util;
|
|
||||||
|
|
||||||
import org.apache.shiro.SecurityUtils;
|
|
||||||
import org.apache.shiro.mgt.SecurityManager;
|
|
||||||
import org.apache.shiro.subject.Subject;
|
|
||||||
import org.apache.shiro.util.ThreadContext;
|
|
||||||
|
|
||||||
import java.util.concurrent.*;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @date 2025-09-04
|
|
||||||
* @author scott
|
|
||||||
*
|
|
||||||
* @Description: 支持shiro的API,获取当前登录人方法的线程池
|
|
||||||
*/
|
|
||||||
public class ShiroThreadPoolExecutor extends ThreadPoolExecutor {
|
|
||||||
|
|
||||||
public ShiroThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
|
|
||||||
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void execute(Runnable command) {
|
|
||||||
Subject subject = SecurityUtils.getSubject();
|
|
||||||
SecurityManager securityManager = SecurityUtils.getSecurityManager();
|
|
||||||
super.execute(() -> {
|
|
||||||
try {
|
|
||||||
ThreadContext.bind(securityManager);
|
|
||||||
ThreadContext.bind(subject);
|
|
||||||
command.run();
|
|
||||||
} finally {
|
|
||||||
ThreadContext.unbindSubject();
|
|
||||||
ThreadContext.unbindSecurityManager();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -335,13 +335,12 @@ 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();
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -0,0 +1,184 @@
|
|||||||
|
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]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,5 +1,6 @@
|
|||||||
package org.jeecg.common.util;
|
package org.jeecg.common.util;
|
||||||
|
|
||||||
|
import cn.dev33.satoken.stp.StpUtil;
|
||||||
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.api.CommonAPI;
|
import org.jeecg.common.api.CommonAPI;
|
||||||
@ -87,68 +88,42 @@ public class TokenUtils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 验证Token
|
* 验证Token(已重写为Sa-Token实现)
|
||||||
*/
|
*/
|
||||||
public static boolean verifyToken(HttpServletRequest request, CommonAPI commonApi, RedisUtil redisUtil) {
|
public static boolean verifyToken(HttpServletRequest request, CommonAPI commonApi) {
|
||||||
log.debug(" -- url --" + request.getRequestURL());
|
log.debug(" -- url --" + request.getRequestURL());
|
||||||
String token = getTokenByRequest(request);
|
String token = getTokenByRequest(request);
|
||||||
return TokenUtils.verifyToken(token, commonApi, redisUtil);
|
return TokenUtils.verifyToken(token, commonApi);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 验证Token
|
* 验证Token(已重写为Sa-Token实现)
|
||||||
*/
|
*/
|
||||||
public static boolean verifyToken(String token, CommonAPI commonApi, RedisUtil redisUtil) {
|
public static boolean verifyToken(String token, CommonAPI commonApi) {
|
||||||
if (StringUtils.isBlank(token)) {
|
if (StringUtils.isBlank(token)) {
|
||||||
throw new JeecgBoot401Exception("token不能为空!");
|
throw new JeecgBoot401Exception("token不能为空!");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 解密获得username,用于和数据库进行对比
|
// 使用Sa-Token校验token
|
||||||
String username = JwtUtil.getUsername(token);
|
Object username = StpUtil.getLoginIdByToken(token);
|
||||||
if (username == null) {
|
if (username == null) {
|
||||||
throw new JeecgBoot401Exception("token非法无效!");
|
throw new JeecgBoot401Exception("token非法无效!");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 查询用户信息
|
// 查询用户信息
|
||||||
LoginUser user = TokenUtils.getLoginUser(username, commonApi, redisUtil);
|
LoginUser user = commonApi.getUserByName(username.toString());
|
||||||
//LoginUser user = commonApi.getUserByName(username);
|
|
||||||
if (user == null) {
|
if (user == null) {
|
||||||
throw new JeecgBoot401Exception("用户不存在!");
|
throw new JeecgBoot401Exception("用户不存在!");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 判断用户状态
|
// 判断用户状态
|
||||||
if (user.getStatus() != 1) {
|
if (user.getStatus() != 1) {
|
||||||
throw new JeecgBoot401Exception("账号已被锁定,请联系管理员!");
|
throw new JeecgBoot401Exception("账号已被锁定,请联系管理员!");
|
||||||
}
|
}
|
||||||
// 校验token是否超时失效 & 或者账号密码是否错误
|
|
||||||
if (!jwtTokenRefresh(token, username, user.getPassword(), redisUtil)) {
|
|
||||||
throw new JeecgBoot401Exception(CommonConstant.TOKEN_IS_INVALID_MSG);
|
|
||||||
}
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 刷新token(保证用户在线操作不掉线)
|
|
||||||
* @param token
|
|
||||||
* @param userName
|
|
||||||
* @param passWord
|
|
||||||
* @param redisUtil
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
private static boolean jwtTokenRefresh(String token, String userName, String passWord, RedisUtil redisUtil) {
|
|
||||||
String cacheToken = oConvertUtils.getString(redisUtil.get(CommonConstant.PREFIX_USER_TOKEN + token));
|
|
||||||
if (oConvertUtils.isNotEmpty(cacheToken)) {
|
|
||||||
// 校验token有效性
|
|
||||||
if (!JwtUtil.verify(cacheToken, userName, passWord)) {
|
|
||||||
String newAuthorization = JwtUtil.sign(userName, passWord);
|
|
||||||
// 设置Toekn缓存有效时间
|
|
||||||
redisUtil.set(CommonConstant.PREFIX_USER_TOKEN + token, newAuthorization);
|
|
||||||
redisUtil.expire(CommonConstant.PREFIX_USER_TOKEN + token, JwtUtil.EXPIRE_TIME * 2 / 1000);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取登录用户
|
* 获取登录用户
|
||||||
*
|
*
|
||||||
@ -174,4 +149,5 @@ public class TokenUtils {
|
|||||||
}
|
}
|
||||||
return loginUser;
|
return loginUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -54,11 +54,10 @@ public class FreemarkerParseFactory {
|
|||||||
//classic_compatible设置,解决报空指针错误
|
//classic_compatible设置,解决报空指针错误
|
||||||
SQL_CONFIG.setClassicCompatible(true);
|
SQL_CONFIG.setClassicCompatible(true);
|
||||||
|
|
||||||
//update-begin-author:taoyan date:2022-8-10 for: freemarker模板注入问题 禁止解析ObjectConstructor,Execute和freemarker.template.utility.JythonRuntime。
|
// 解决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。
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -73,13 +72,12 @@ public class FreemarkerParseFactory {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
//update-begin--Author:scott Date:20180320 for:解决问题 - 错误提示sql文件不存在,实际问题是sql freemarker用法错误-----
|
// 代码逻辑说明: 解决问题 - 错误提示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,123 +1,108 @@
|
|||||||
package org.jeecg.common.util.encryption;
|
package org.jeecg.common.util.encryption;
|
||||||
|
|
||||||
import org.apache.shiro.lang.codec.Base64;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
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;
|
||||||
|
import java.util.Base64;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @Description: AES 加密
|
* AES 工具 (兼容历史 NoPadding + 新 PKCS5Padding)
|
||||||
* @author: jeecg-boot
|
|
||||||
* @date: 2022/3/30 11:48
|
|
||||||
*/
|
*/
|
||||||
|
@Slf4j
|
||||||
public class AesEncryptUtil {
|
public class AesEncryptUtil {
|
||||||
|
|
||||||
/**
|
private static final String KEY = EncryptedString.key;
|
||||||
* 使用AES-128-CBC加密模式 key和iv可以相同
|
private static final String IV = EncryptedString.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 {
|
||||||
* @param data 要加密的数据
|
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
|
||||||
* @param key 加密key
|
SecretKeySpec ks = new SecretKeySpec(KEY.getBytes(StandardCharsets.UTF_8), "AES");
|
||||||
* @param iv 加密iv
|
IvParameterSpec ivSpec = new IvParameterSpec(IV.getBytes(StandardCharsets.UTF_8));
|
||||||
* @return 加密的结果
|
cipher.init(Cipher.DECRYPT_MODE, ks, ivSpec);
|
||||||
* @throws Exception
|
byte[] plain = cipher.doFinal(Base64.getDecoder().decode(cipherBase64));
|
||||||
*/
|
return new String(plain, StandardCharsets.UTF_8);
|
||||||
public static String encrypt(String data, String key, String iv) throws Exception {
|
}
|
||||||
try {
|
|
||||||
|
|
||||||
//"算法/模式/补码方式"NoPadding PkcsPadding
|
/* -------- 旧版:CBC + NoPadding (手工补 0) -------- */
|
||||||
|
private static String decryptLegacyNoPadding(String cipherBase64) throws Exception {
|
||||||
|
Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
|
||||||
|
SecretKeySpec ks = new SecretKeySpec(KEY.getBytes(StandardCharsets.UTF_8), "AES");
|
||||||
|
IvParameterSpec ivSpec = new IvParameterSpec(IV.getBytes(StandardCharsets.UTF_8));
|
||||||
|
cipher.init(Cipher.DECRYPT_MODE, ks, ivSpec);
|
||||||
|
byte[] data = cipher.doFinal(Base64.getDecoder().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 = plaintextLength + (blockSize - (plaintextLength % blockSize));
|
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");
|
||||||
SecretKeySpec keyspec = new SecretKeySpec(key.getBytes(), "AES");
|
IvParameterSpec ivspec = new IvParameterSpec(IV.getBytes(StandardCharsets.UTF_8));
|
||||||
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.getEncoder().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 密文测试
|
||||||
* @param data 要解密的数据
|
// String frontCipher = encrypt("sa"); // 仅验证管道是否可用(旧方式)
|
||||||
* @param key 解密key
|
// System.out.println(resolvePassword(frontCipher));
|
||||||
* @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);
|
|
||||||
// }
|
// }
|
||||||
|
}
|
||||||
}
|
|
||||||
@ -2,6 +2,7 @@ package org.jeecg.common.util.filter;
|
|||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.jeecg.common.exception.JeecgBootException;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@ -149,29 +150,38 @@ public class SsrfFileTypeFilter {
|
|||||||
public static void checkDownloadFileType(String filePath) throws IOException {
|
public static void checkDownloadFileType(String filePath) throws IOException {
|
||||||
//文件后缀
|
//文件后缀
|
||||||
String suffix = getFileTypeBySuffix(filePath);
|
String suffix = getFileTypeBySuffix(filePath);
|
||||||
log.info("suffix:{}", suffix);
|
log.debug(" 【文件下载校验】文件后缀 suffix: {}", suffix);
|
||||||
boolean isAllowExtension = FILE_TYPE_WHITE_LIST.contains(suffix.toLowerCase());
|
boolean isAllowExtension = FILE_TYPE_WHITE_LIST.contains(suffix.toLowerCase());
|
||||||
//是否允许下载的文件
|
//是否允许下载的文件
|
||||||
if (!isAllowExtension) {
|
if (!isAllowExtension) {
|
||||||
throw new IOException("下载失败,存在非法文件类型:" + suffix);
|
throw new JeecgBootException("下载失败,存在非法文件类型:" + suffix);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 上传文件类型过滤
|
* 上传文件类型过滤
|
||||||
*
|
*
|
||||||
* @param file
|
* @param file
|
||||||
*/
|
*/
|
||||||
public static void checkUploadFileType(MultipartFile file) throws Exception {
|
public static void checkUploadFileType(MultipartFile file) throws Exception {
|
||||||
//获取文件真是后缀
|
checkUploadFileType(file, null);
|
||||||
String suffix = getFileType(file);
|
}
|
||||||
|
|
||||||
log.info("suffix:{}", suffix);
|
/**
|
||||||
|
* 上传文件类型过滤
|
||||||
|
*
|
||||||
|
* @param file
|
||||||
|
*/
|
||||||
|
public static void checkUploadFileType(MultipartFile file, String customPath) throws Exception {
|
||||||
|
//1. 路径安全校验
|
||||||
|
validatePathSecurity(customPath);
|
||||||
|
//2. 校验文件后缀和头
|
||||||
|
String suffix = getFileType(file, customPath);
|
||||||
|
log.info("【文件上传校验】文件后缀 suffix: {},customPath:{}", suffix, customPath);
|
||||||
boolean isAllowExtension = FILE_TYPE_WHITE_LIST.contains(suffix.toLowerCase());
|
boolean isAllowExtension = FILE_TYPE_WHITE_LIST.contains(suffix.toLowerCase());
|
||||||
//是否允许下载的文件
|
//是否允许下载的文件
|
||||||
if (!isAllowExtension) {
|
if (!isAllowExtension) {
|
||||||
throw new Exception("上传失败,存在非法文件类型:" + suffix);
|
throw new JeecgBootException("上传失败,存在非法文件类型:" + suffix);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -183,8 +193,8 @@ public class SsrfFileTypeFilter {
|
|||||||
* @throws Exception
|
* @throws Exception
|
||||||
*/
|
*/
|
||||||
|
|
||||||
private static String getFileType(MultipartFile file) throws Exception {
|
private static String getFileType(MultipartFile file, String customPath) throws Exception {
|
||||||
//update-begin-author:liusq date:20230404 for: [issue/4672]方法造成的文件被占用,注释掉此方法tomcat就能自动清理掉临时文件
|
// 代码逻辑说明: [issue/4672]方法造成的文件被占用,注释掉此方法tomcat就能自动清理掉临时文件
|
||||||
String fileExtendName = null;
|
String fileExtendName = null;
|
||||||
InputStream is = null;
|
InputStream is = null;
|
||||||
try {
|
try {
|
||||||
@ -203,7 +213,7 @@ public class SsrfFileTypeFilter {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
log.info("-----获取到的指定文件类型------"+fileExtendName);
|
log.debug("-----获取到的指定文件类型------"+fileExtendName);
|
||||||
// 如果不是上述类型,则判断扩展名
|
// 如果不是上述类型,则判断扩展名
|
||||||
if (StringUtils.isBlank(fileExtendName)) {
|
if (StringUtils.isBlank(fileExtendName)) {
|
||||||
String fileName = file.getOriginalFilename();
|
String fileName = file.getOriginalFilename();
|
||||||
@ -214,7 +224,6 @@ public class SsrfFileTypeFilter {
|
|||||||
// 如果有扩展名,则返回扩展名
|
// 如果有扩展名,则返回扩展名
|
||||||
return getFileTypeBySuffix(fileName);
|
return getFileTypeBySuffix(fileName);
|
||||||
}
|
}
|
||||||
log.info("-----最終的文件类型------"+fileExtendName);
|
|
||||||
is.close();
|
is.close();
|
||||||
return fileExtendName;
|
return fileExtendName;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
@ -225,7 +234,6 @@ public class SsrfFileTypeFilter {
|
|||||||
is.close();
|
is.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//update-end-author:liusq date:20230404 for: [issue/4672]方法造成的文件被占用,注释掉此方法tomcat就能自动清理掉临时文件
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -249,4 +257,34 @@ public class SsrfFileTypeFilter {
|
|||||||
}
|
}
|
||||||
return stringBuilder.toString();
|
return stringBuilder.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 路径安全校验
|
||||||
|
*/
|
||||||
|
private static void validatePathSecurity(String customPath) throws JeecgBootException {
|
||||||
|
if (customPath == null || customPath.trim().isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 统一分隔符为 /
|
||||||
|
String normalized = customPath.replace("\\", "/");
|
||||||
|
|
||||||
|
// 1. 防止路径遍历攻击
|
||||||
|
if (normalized.contains("..") || normalized.contains("~")) {
|
||||||
|
throw new JeecgBootException("上传业务路径包含非法字符!");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 限制路径深度
|
||||||
|
int depth = normalized.split("/").length;
|
||||||
|
if (depth > 5) {
|
||||||
|
throw new JeecgBootException("上传业务路径深度超出限制!");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 限制字符集(只允许字母、数字、下划线、横线、斜杠)
|
||||||
|
if (!normalized.matches("^[a-zA-Z0-9/_-]+$")) {
|
||||||
|
throw new JeecgBootException("上传业务路径包含非法字符!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,6 +11,10 @@ import org.jeecg.config.mybatis.MybatisPlusSaasConfig;
|
|||||||
import org.springframework.beans.BeanUtils;
|
import org.springframework.beans.BeanUtils;
|
||||||
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import org.springframework.beans.BeanWrapper;
|
||||||
|
import org.springframework.beans.BeanWrapperImpl;
|
||||||
|
|
||||||
|
import java.beans.PropertyDescriptor;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.UnsupportedEncodingException;
|
import java.io.UnsupportedEncodingException;
|
||||||
@ -23,6 +27,7 @@ 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;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@ -563,10 +568,8 @@ public class oConvertUtils {
|
|||||||
return "";
|
return "";
|
||||||
} else if (!name.contains(SymbolConstant.UNDERLINE)) {
|
} else if (!name.contains(SymbolConstant.UNDERLINE)) {
|
||||||
// 不含下划线,仅将首字母小写
|
// 不含下划线,仅将首字母小写
|
||||||
//update-begin--Author:zhoujf Date:20180503 for:TASK #2500 【代码生成器】代码生成器开发一通用模板生成功能
|
// 代码逻辑说明: 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("_");
|
||||||
@ -611,7 +614,6 @@ 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>
|
||||||
@ -644,7 +646,6 @@ public class oConvertUtils {
|
|||||||
}
|
}
|
||||||
return result.toString();
|
return result.toString();
|
||||||
}
|
}
|
||||||
//update-end--Author:zhoujf Date:20180503 for:TASK #2500 【代码生成器】代码生成器开发一通用模板生成功能
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 将驼峰命名转化成下划线
|
* 将驼峰命名转化成下划线
|
||||||
@ -982,17 +983,18 @@ public class oConvertUtils {
|
|||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 判断 list1中的元素是否在list2中出现
|
* 判断 sourceList中的元素是否在targetList中出现
|
||||||
|
*
|
||||||
* QQYUN-5326【简流】获取组织人员 单/多 筛选条件 没有部门筛选
|
* QQYUN-5326【简流】获取组织人员 单/多 筛选条件 没有部门筛选
|
||||||
* @param list1
|
* @param sourceList 源列表,要检查的元素列表
|
||||||
* @param list2
|
* @param targetList 目标列表,用于匹配的列表
|
||||||
* @return
|
* @return 如果sourceList中有任何元素在targetList中存在则返回true,否则返回false
|
||||||
*/
|
*/
|
||||||
public static boolean isInList(List<String> list1, List<String> list2){
|
public static boolean isInList(List<String> sourceList, List<String> targetList){
|
||||||
for(String str1: list1){
|
for(String sourceItem: sourceList){
|
||||||
boolean flag = false;
|
boolean flag = false;
|
||||||
for(String str2: list2){
|
for(String targetItem: targetList){
|
||||||
if(str1.equals(str2)){
|
if(sourceItem.equals(targetItem)){
|
||||||
flag = true;
|
flag = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -1004,6 +1006,35 @@ 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
|
||||||
@ -1168,5 +1199,37 @@ 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);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -97,9 +97,8 @@ public class OssBootUtil {
|
|||||||
* @return oss 中的相对文件路径
|
* @return oss 中的相对文件路径
|
||||||
*/
|
*/
|
||||||
public static String upload(MultipartFile file, String fileDir,String customBucket) throws Exception {
|
public static String upload(MultipartFile file, String fileDir,String customBucket) throws Exception {
|
||||||
//update-begin-author:liusq date:20210809 for: 过滤上传文件类型
|
// 文件安全校验,防止上传漏洞文件
|
||||||
SsrfFileTypeFilter.checkUploadFileType(file);
|
SsrfFileTypeFilter.checkUploadFileType(file);
|
||||||
//update-end-author:liusq date:20210809 for: 过滤上传文件类型
|
|
||||||
|
|
||||||
String filePath = null;
|
String filePath = null;
|
||||||
initOss(endPoint, accessKeyId, accessKeySecret);
|
initOss(endPoint, accessKeyId, accessKeySecret);
|
||||||
@ -125,9 +124,8 @@ 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)) {
|
||||||
@ -264,9 +262,8 @@ public class OssBootUtil {
|
|||||||
newBucket = bucket;
|
newBucket = bucket;
|
||||||
}
|
}
|
||||||
initOss(endPoint, accessKeyId, accessKeySecret);
|
initOss(endPoint, accessKeyId, accessKeySecret);
|
||||||
//update-begin---author:liusq Date:20220120 for:替换objectName前缀,防止key不一致导致获取不到文件----
|
// 代码逻辑说明: 替换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){
|
||||||
@ -294,9 +291,8 @@ 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{
|
||||||
//update-begin---author:liusq Date:20220120 for:替换objectName前缀,防止key不一致导致获取不到文件----
|
// 代码逻辑说明: 替换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.info(" 获取sql信息 :{} ", list.toString());
|
log.debug(" 获取sql信息 :{} ", list.toString());
|
||||||
boolean flag = checkTableAndFieldsName(list);
|
boolean flag = checkTableAndFieldsName(list);
|
||||||
if(flag == false){
|
if(flag == false){
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@ -60,17 +60,15 @@ public class AutoPoiDictConfig implements AutoPoiDictServiceI {
|
|||||||
|
|
||||||
|
|
||||||
for (DictModel t : dictList) {
|
for (DictModel t : dictList) {
|
||||||
//update-begin---author:liusq Date:20230517 for:[issues/4917]excel 导出异常---
|
// 代码逻辑说明: [issues/4917]excel 导出异常---
|
||||||
if(t!=null && t.getText()!=null && t.getValue()!=null){
|
if(t!=null && t.getText()!=null && t.getValue()!=null){
|
||||||
//update-end---author:liusq Date:20230517 for:[issues/4917]excel 导出异常---
|
// 代码逻辑说明: [issues/I4MBB3]@Excel dicText字段的值有下划线时,导入功能不能正确解析---
|
||||||
//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) {
|
||||||
|
|||||||
@ -1,9 +1,11 @@
|
|||||||
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.beans.factory.config.BeanDefinition;
|
||||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
import org.springframework.context.annotation.Bean;
|
|
||||||
import org.springframework.context.annotation.Role;
|
import org.springframework.context.annotation.Role;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
@ -74,7 +76,35 @@ 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,13 +95,11 @@
|
|||||||
// 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;
|
||||||
// }
|
// }
|
||||||
|
|||||||
@ -12,6 +12,7 @@ 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.OperationCustomizer;
|
||||||
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;
|
||||||
@ -22,18 +23,24 @@ 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<>();
|
||||||
// 定义不需要注入安全要求的路径集合
|
// 定义不需要注入安全要求的路径集合
|
||||||
Set<String> excludedPaths = new HashSet<>(Arrays.asList(
|
private static final Set<String> excludedPaths = new HashSet<>(Arrays.asList(
|
||||||
"/sys/randomImage/{key}",
|
"/sys/randomImage/**",
|
||||||
"/sys/login",
|
"/sys/login",
|
||||||
"/sys/phoneLogin",
|
"/sys/phoneLogin",
|
||||||
"/sys/mLogin",
|
"/sys/mLogin",
|
||||||
@ -43,7 +50,20 @@ 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资源:
|
||||||
@ -97,19 +117,18 @@ public class Swagger3Config implements WebMvcConfigurer {
|
|||||||
|
|
||||||
return fullPath.toString();
|
return fullPath.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private boolean isExcludedPath(String path) {
|
private boolean isExcludedPath(String path) {
|
||||||
return excludedPaths.stream()
|
// 使用缓存避免重复计算
|
||||||
.anyMatch(pattern -> {
|
return EXCLUDED_PATHS_CACHE.computeIfAbsent(path, p -> {
|
||||||
if (pattern.endsWith("/**")) {
|
// 精确匹配
|
||||||
// 处理通配符匹配
|
if (exactPatterns.contains(p)) {
|
||||||
String basePath = pattern.substring(0, pattern.length() - 3);
|
return true;
|
||||||
return path.startsWith(basePath);
|
}
|
||||||
}
|
// 通配符匹配
|
||||||
// 精确匹配
|
return wildcardPatterns.stream().anyMatch(p::startsWith);
|
||||||
return pattern.equals(path);
|
});
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
@ -117,7 +136,7 @@ public class Swagger3Config implements WebMvcConfigurer {
|
|||||||
return new OpenAPI()
|
return new OpenAPI()
|
||||||
.info(new Info()
|
.info(new Info()
|
||||||
.title("JeecgBoot 后台服务API接口文档")
|
.title("JeecgBoot 后台服务API接口文档")
|
||||||
.version("3.8.3")
|
.version("3.9.0")
|
||||||
.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")
|
||||||
|
|||||||
@ -1,19 +0,0 @@
|
|||||||
//package org.jeecg.config;
|
|
||||||
//
|
|
||||||
//import io.undertow.server.DefaultByteBufferPool;
|
|
||||||
//import io.undertow.websockets.jsr.WebSocketDeploymentInfo;
|
|
||||||
//import org.springframework.boot.web.embedded.undertow.UndertowServletWebServerFactory;
|
|
||||||
//import org.springframework.boot.web.server.WebServerFactoryCustomizer;
|
|
||||||
//import org.springframework.stereotype.Component;
|
|
||||||
//
|
|
||||||
//@Component
|
|
||||||
//public class UndertowCustomizer implements WebServerFactoryCustomizer<UndertowServletWebServerFactory> {
|
|
||||||
// @Override
|
|
||||||
// public void customize(UndertowServletWebServerFactory factory) {
|
|
||||||
// factory.addDeploymentInfoCustomizers(deploymentInfo -> {
|
|
||||||
// WebSocketDeploymentInfo webSocketDeploymentInfo = new WebSocketDeploymentInfo();
|
|
||||||
// webSocketDeploymentInfo.setBuffers(new DefaultByteBufferPool(false, 1024));
|
|
||||||
// deploymentInfo.addServletContextAttribute("io.undertow.websockets.jsr.WebSocketDeploymentInfo", webSocketDeploymentInfo);
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
//}
|
|
||||||
@ -11,22 +11,19 @@ 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.prometheusmetrics.PrometheusMeterRegistry;
|
||||||
|
import jakarta.annotation.PostConstruct;
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
import org.springframework.beans.factory.InitializingBean;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.beans.factory.ObjectProvider;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.beans.factory.annotation.Qualifier;
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
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.Jackson2ObjectMapperBuilder;
|
|
||||||
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
|
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
|
||||||
import org.springframework.web.cors.CorsConfiguration;
|
import org.springframework.web.cors.CorsConfiguration;
|
||||||
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
||||||
@ -50,6 +47,7 @@ import java.util.concurrent.TimeUnit;
|
|||||||
* @Author qinfeng
|
* @Author qinfeng
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
@Slf4j
|
||||||
@Configuration
|
@Configuration
|
||||||
public class WebMvcConfiguration implements WebMvcConfigurer {
|
public class WebMvcConfiguration implements WebMvcConfigurer {
|
||||||
|
|
||||||
@ -144,7 +142,6 @@ 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
|
||||||
@ -153,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]系统监控功能优化------------
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 监听应用启动完成事件,确保 PrometheusMeterRegistry 已经初始化
|
* 在Bean初始化完成后立即配置PrometheusMeterRegistry,避免在Meter注册后才配置MeterFilter
|
||||||
* 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
|
||||||
*/
|
*/
|
||||||
@EventListener
|
@PostConstruct
|
||||||
public void onApplicationReady(ApplicationReadyEvent event) {
|
public void initPrometheusMeterRegistry() {
|
||||||
if(null != meterRegistryPostProcessor){
|
// 确保在应用启动早期就配置MeterFilter,避免警告
|
||||||
meterRegistryPostProcessor.postProcessAfterInitialization(prometheusMeterRegistry, "");
|
if (null != meterRegistryPostProcessor && null != prometheusMeterRegistry) {
|
||||||
|
meterRegistryPostProcessor.postProcessAfterInitialization(prometheusMeterRegistry, "prometheusMeterRegistry");
|
||||||
|
log.info("PrometheusMeterRegistry 配置完成");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -24,23 +24,18 @@ public class WebsocketFilter implements Filter {
|
|||||||
|
|
||||||
private static CommonAPI commonApi;
|
private static CommonAPI commonApi;
|
||||||
|
|
||||||
private static RedisUtil redisUtil;
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
|
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
|
||||||
if (commonApi == null) {
|
if (commonApi == null) {
|
||||||
commonApi = SpringContextUtils.getBean(CommonAPI.class);
|
commonApi = SpringContextUtils.getBean(CommonAPI.class);
|
||||||
}
|
}
|
||||||
if (redisUtil == null) {
|
|
||||||
redisUtil = SpringContextUtils.getBean(RedisUtil.class);
|
|
||||||
}
|
|
||||||
HttpServletRequest request = (HttpServletRequest)servletRequest;
|
HttpServletRequest request = (HttpServletRequest)servletRequest;
|
||||||
String token = request.getHeader(TOKEN_KEY);
|
String token = request.getHeader(TOKEN_KEY);
|
||||||
|
|
||||||
log.debug("Websocket连接 Token安全校验,Path = {},token:{}", request.getRequestURI(), token);
|
log.debug("Websocket连接 Token安全校验,Path = {},token:{}", request.getRequestURI(), token);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
TokenUtils.verifyToken(token, commonApi, redisUtil);
|
TokenUtils.verifyToken(token, commonApi);
|
||||||
} catch (Exception exception) {
|
} catch (Exception exception) {
|
||||||
//log.error("Websocket连接 Token安全校验失败,IP:{}, Token:{}, Path = {},异常:{}", oConvertUtils.getIpAddrByRequest(request), token, request.getRequestURI(), exception.getMessage());
|
//log.error("Websocket连接 Token安全校验失败,IP:{}, Token:{}, Path = {},异常:{}", oConvertUtils.getIpAddrByRequest(request), token, request.getRequestURI(), exception.getMessage());
|
||||||
log.debug("Websocket连接 Token安全校验失败,IP:{}, Token:{}, Path = {},异常:{}", oConvertUtils.getIpAddrByRequest(request), token, request.getRequestURI(), exception.getMessage());
|
log.debug("Websocket连接 Token安全校验失败,IP:{}, Token:{}, Path = {},异常:{}", oConvertUtils.getIpAddrByRequest(request), token, request.getRequestURI(), exception.getMessage());
|
||||||
|
|||||||
@ -2,7 +2,7 @@ package org.jeecg.config.firewall.interceptor;
|
|||||||
|
|
||||||
import com.alibaba.fastjson.JSON;
|
import com.alibaba.fastjson.JSON;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.shiro.SecurityUtils;
|
import org.jeecg.common.util.LoginUserUtils;
|
||||||
import org.jeecg.common.api.CommonAPI;
|
import org.jeecg.common.api.CommonAPI;
|
||||||
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;
|
||||||
@ -68,7 +68,7 @@ public class LowCodeModeInterceptor implements HandlerInterceptor {
|
|||||||
if (jeecgBaseConfig.getFirewall()!=null && LowCodeModeInterceptor.LOW_CODE_MODE_PROD.equals(jeecgBaseConfig.getFirewall().getLowCodeMode())) {
|
if (jeecgBaseConfig.getFirewall()!=null && LowCodeModeInterceptor.LOW_CODE_MODE_PROD.equals(jeecgBaseConfig.getFirewall().getLowCodeMode())) {
|
||||||
String requestURI = request.getRequestURI().substring(request.getContextPath().length());
|
String requestURI = request.getRequestURI().substring(request.getContextPath().length());
|
||||||
log.info("低代码模式,拦截请求路径:" + requestURI);
|
log.info("低代码模式,拦截请求路径:" + requestURI);
|
||||||
LoginUser loginUser = (LoginUser) SecurityUtils.getSubject().getPrincipal();
|
LoginUser loginUser = LoginUserUtils.getSessionUser();
|
||||||
Set<String> hasRoles = null;
|
Set<String> hasRoles = null;
|
||||||
if (loginUser == null) {
|
if (loginUser == null) {
|
||||||
loginUser = commonAPI.getUserByName(JwtUtil.getUserNameByToken(SpringContextUtils.getHttpServletRequest()));
|
loginUser = commonAPI.getUserByName(JwtUtil.getUserNameByToken(SpringContextUtils.getHttpServletRequest()));
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import org.apache.ibatis.executor.Executor;
|
|||||||
import org.apache.ibatis.mapping.MappedStatement;
|
import org.apache.ibatis.mapping.MappedStatement;
|
||||||
import org.apache.ibatis.mapping.SqlCommandType;
|
import org.apache.ibatis.mapping.SqlCommandType;
|
||||||
import org.apache.ibatis.plugin.*;
|
import org.apache.ibatis.plugin.*;
|
||||||
import org.apache.shiro.SecurityUtils;
|
import org.jeecg.common.util.LoginUserUtils;
|
||||||
import org.jeecg.common.config.TenantContext;
|
import org.jeecg.common.config.TenantContext;
|
||||||
import org.jeecg.common.constant.TenantConstant;
|
import org.jeecg.common.constant.TenantConstant;
|
||||||
import org.jeecg.common.system.vo.LoginUser;
|
import org.jeecg.common.system.vo.LoginUser;
|
||||||
@ -129,20 +129,18 @@ 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;
|
||||||
//update-begin-author:scott date:20190729 for:批量更新报错issues/IZA3Q--
|
// 代码逻辑说明: 批量更新报错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-
|
|
||||||
|
|
||||||
//update-begin-author:scott date:20190729 for:更新指定字段时报错 issues/#516-
|
// 代码逻辑说明: 更新指定字段时报错 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 {
|
||||||
@ -184,7 +182,6 @@ 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
|
||||||
@ -192,13 +189,12 @@ public class MybatisInterceptor implements Interceptor {
|
|||||||
private LoginUser getLoginUser() {
|
private LoginUser getLoginUser() {
|
||||||
LoginUser sysUser = null;
|
LoginUser sysUser = null;
|
||||||
try {
|
try {
|
||||||
sysUser = SecurityUtils.getSubject().getPrincipal() != null ? (LoginUser) SecurityUtils.getSubject().getPrincipal() : null;
|
sysUser = LoginUserUtils.getSessionUser() != null ? LoginUserUtils.getSessionUser() : null;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
//e.printStackTrace();
|
//e.printStackTrace();
|
||||||
sysUser = null;
|
sysUser = null;
|
||||||
}
|
}
|
||||||
return sysUser;
|
return sysUser;
|
||||||
}
|
}
|
||||||
//update-end--Author:scott Date:20191213 for:关于使用Quzrtz 开启线程任务, #465
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
package org.jeecg.config.shiro;
|
package org.jeecg.config.satoken;
|
||||||
|
|
||||||
import java.lang.annotation.ElementType;
|
import java.lang.annotation.ElementType;
|
||||||
import java.lang.annotation.Retention;
|
import java.lang.annotation.Retention;
|
||||||
@ -16,3 +16,4 @@ import java.lang.annotation.Target;
|
|||||||
@Retention(RetentionPolicy.RUNTIME)
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
public @interface IgnoreAuth {
|
public @interface IgnoreAuth {
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -0,0 +1,420 @@
|
|||||||
|
package org.jeecg.config.satoken;
|
||||||
|
|
||||||
|
import cn.dev33.satoken.context.SaHolder;
|
||||||
|
import cn.dev33.satoken.context.model.SaRequest;
|
||||||
|
import cn.dev33.satoken.exception.NotLoginException;
|
||||||
|
import cn.dev33.satoken.filter.SaServletFilter;
|
||||||
|
import cn.dev33.satoken.interceptor.SaInterceptor;
|
||||||
|
import cn.dev33.satoken.jwt.StpLogicJwtForSimple;
|
||||||
|
import cn.dev33.satoken.router.SaHttpMethod;
|
||||||
|
import cn.dev33.satoken.router.SaRouter;
|
||||||
|
import cn.dev33.satoken.stp.StpLogic;
|
||||||
|
import cn.dev33.satoken.stp.StpUtil;
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
import jakarta.servlet.DispatcherType;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.jeecg.common.api.CommonAPI;
|
||||||
|
import org.jeecg.common.config.TenantContext;
|
||||||
|
import org.jeecg.common.constant.CacheConstant;
|
||||||
|
import org.jeecg.common.constant.CommonConstant;
|
||||||
|
import org.jeecg.common.system.vo.LoginUser;
|
||||||
|
import org.jeecg.common.util.*;
|
||||||
|
import org.jeecg.config.JeecgBaseConfig;
|
||||||
|
import org.jeecg.config.mybatis.MybatisPlusSaasConfig;
|
||||||
|
import org.jeecg.config.satoken.ignore.InMemoryIgnoreAuth;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.beans.factory.config.BeanDefinition;
|
||||||
|
import org.springframework.boot.web.servlet.FilterRegistrationBean;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.context.annotation.Primary;
|
||||||
|
import org.springframework.context.annotation.Role;
|
||||||
|
import org.springframework.core.env.Environment;
|
||||||
|
import org.springframework.http.HttpHeaders;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||||
|
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author: jeecg-boot
|
||||||
|
* @description: Sa-Token 配置类
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Configuration
|
||||||
|
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||||
|
public class SaTokenConfig implements WebMvcConfigurer {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private JeecgBaseConfig jeecgBaseConfig;
|
||||||
|
@Autowired
|
||||||
|
private Environment env;
|
||||||
|
@Autowired
|
||||||
|
private CommonAPI commonAPI;
|
||||||
|
@Autowired
|
||||||
|
private RedisUtil redisUtil;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sa-Token 整合 jwt (Simple 模式)
|
||||||
|
* 使用JWT-Simple模式生成标准JWT格式的token
|
||||||
|
* 并支持从URL参数"token"读取token(兼容原系统)
|
||||||
|
*/
|
||||||
|
@Bean
|
||||||
|
@Primary
|
||||||
|
public StpLogic getStpLogicJwt() {
|
||||||
|
return new StpLogicJwtForSimple() {
|
||||||
|
/**
|
||||||
|
* 获取当前请求的 Token 值
|
||||||
|
* 优先级:Header > URL参数token > URL参数X-Access-Token
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public String getTokenValue() {
|
||||||
|
try {
|
||||||
|
SaRequest request = SaHolder.getRequest();
|
||||||
|
|
||||||
|
// 1. 优先从Header中获取
|
||||||
|
String tokenValue = request.getHeader(getConfigOrGlobal().getTokenName());
|
||||||
|
if (oConvertUtils.isNotEmpty(tokenValue)) {
|
||||||
|
return tokenValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 从URL参数"token"获取(兼容原系统)
|
||||||
|
tokenValue = request.getParam("token");
|
||||||
|
if (oConvertUtils.isNotEmpty(tokenValue)) {
|
||||||
|
return tokenValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 从URL参数"X-Access-Token"获取
|
||||||
|
tokenValue = request.getParam(getConfigOrGlobal().getTokenName());
|
||||||
|
if (oConvertUtils.isNotEmpty(tokenValue)) {
|
||||||
|
return tokenValue;
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.debug("获取token失败: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 如果都没有,使用默认逻辑
|
||||||
|
return super.getTokenValue();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注册 Sa-Token 拦截器,打开注解式鉴权功能
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void addInterceptors(InterceptorRegistry registry) {
|
||||||
|
// 注册 Sa-Token 拦截器,打开注解式鉴权功能
|
||||||
|
registry.addInterceptor(new SaInterceptor()).addPathPatterns("/**");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注册 Sa-Token 全局过滤器
|
||||||
|
*/
|
||||||
|
@Bean
|
||||||
|
public SaServletFilter getSaServletFilter() {
|
||||||
|
return new SaServletFilter()
|
||||||
|
// 指定 [拦截路由] 与 [放行路由]
|
||||||
|
.addInclude("/**")
|
||||||
|
.setExcludeList(getExcludeUrls())
|
||||||
|
// 认证函数: 每次请求执行
|
||||||
|
.setAuth(obj -> {
|
||||||
|
// 检查是否是免认证路径
|
||||||
|
String servletPath = SaHolder.getRequest().getRequestPath();
|
||||||
|
if (InMemoryIgnoreAuth.contains(servletPath)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 校验 token:如果请求中带有 token,先切换到对应的登录会话再校验
|
||||||
|
try {
|
||||||
|
String token = StpUtil.getTokenValue();
|
||||||
|
if (oConvertUtils.isNotEmpty(token)) {
|
||||||
|
// 根据 token 获取 loginId 并切换到对应的登录会话
|
||||||
|
Object loginId = StpUtil.getLoginIdByToken(token);
|
||||||
|
if (loginId != null) {
|
||||||
|
StpUtil.switchTo(loginId);
|
||||||
|
|
||||||
|
// 需要手工自动续签,默认参数auto-renew:true 不好使
|
||||||
|
long activeTimeout = StpUtil.stpLogic.getConfigOrGlobal().getActiveTimeout();
|
||||||
|
if (activeTimeout > 0) {
|
||||||
|
// 获取当前token的活跃剩余时间
|
||||||
|
long tokenActiveTimeout = StpUtil.getTokenActiveTimeout();
|
||||||
|
|
||||||
|
// 如果剩余活跃时间少于总活跃时间的一半,进行续签
|
||||||
|
if (tokenActiveTimeout > 0 && tokenActiveTimeout < (activeTimeout / 2)) {
|
||||||
|
StpUtil.stpLogic.updateLastActiveToNow(token);
|
||||||
|
log.info("【Sa-Token拦截器】Token续签成功,剩余活跃时间: {}秒", tokenActiveTimeout);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
// 如果获取 loginId 失败,说明 token 无效或未登录,让 checkLogin 抛出异常
|
||||||
|
log.debug("切换登录会话失败: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 最终校验登录状态
|
||||||
|
StpUtil.checkLogin();
|
||||||
|
|
||||||
|
// 租户校验逻辑
|
||||||
|
checkTenantAuthorization();
|
||||||
|
})
|
||||||
|
// 异常处理函数:每次认证函数发生异常时执行此函数
|
||||||
|
.setError(e -> {
|
||||||
|
log.warn("Sa-Token 认证失败:用户未登录或token无效");
|
||||||
|
log.warn("请求路径: {}, Method: {},Token: {}", SaHolder.getRequest().getRequestPath(), SaHolder.getRequest().getMethod(), StpUtil.getTokenValue());
|
||||||
|
|
||||||
|
// 返回401状态码
|
||||||
|
SaHolder.getResponse().setStatus(401).setHeader("Content-Type", "application/json;charset=UTF-8");
|
||||||
|
return org.jeecg.common.system.util.JwtUtil.responseErrorJson(401, CommonConstant.TOKEN_IS_INVALID_MSG);
|
||||||
|
})
|
||||||
|
// 前置函数:在每次认证函数之前执行(BeforeAuth 不受 includeList 与 excludeList 的限制,所有请求都会进入)
|
||||||
|
.setBeforeAuth(r -> {
|
||||||
|
// 设置跨域配置
|
||||||
|
Object cloudServer = env.getProperty(CommonConstant.CLOUD_SERVER_KEY);
|
||||||
|
// 如果cloudServer为空 则说明是单体 需要加载跨域配置【微服务跨域切换】
|
||||||
|
if (cloudServer == null) {
|
||||||
|
SaHolder.getResponse()
|
||||||
|
// 允许指定域访问跨域资源
|
||||||
|
.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, SaHolder.getRequest().getHeader(HttpHeaders.ORIGIN))
|
||||||
|
// 允许所有请求方式
|
||||||
|
.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS, "GET, POST, PUT, DELETE, OPTIONS")
|
||||||
|
// 有效时间
|
||||||
|
.setHeader(HttpHeaders.ACCESS_CONTROL_MAX_AGE, "3600")
|
||||||
|
// 允许的header参数
|
||||||
|
.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS, SaHolder.getRequest().getHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS))
|
||||||
|
// 允许携带凭证
|
||||||
|
.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true");
|
||||||
|
}
|
||||||
|
|
||||||
|
// OPTIONS预检请求,直接返回
|
||||||
|
SaRouter.match(SaHttpMethod.OPTIONS).free(r2 -> {
|
||||||
|
SaHolder.getResponse().setStatus(HttpStatus.OK.value());
|
||||||
|
});
|
||||||
|
|
||||||
|
// 设置当前线程上下文的租户ID
|
||||||
|
String tenantId = SaHolder.getRequest().getHeader(CommonConstant.TENANT_ID);
|
||||||
|
TenantContext.setTenant(tenantId);
|
||||||
|
log.debug("===【TenantContext 线程设置】=== 请求路径: {}, 租户ID: {}", SaHolder.getRequest().getRequestPath(), tenantId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* spring过滤装饰器 <br/>
|
||||||
|
* 支持异步请求的过滤器装饰
|
||||||
|
*/
|
||||||
|
@Bean
|
||||||
|
public FilterRegistrationBean<SaServletFilter> saTokenFilterRegistration() {
|
||||||
|
FilterRegistrationBean<SaServletFilter> registration = new FilterRegistrationBean<>();
|
||||||
|
registration.setFilter(getSaServletFilter());
|
||||||
|
registration.setName("SaServletFilter");
|
||||||
|
// 支持异步请求
|
||||||
|
registration.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ASYNC);
|
||||||
|
// 拦截所有请求
|
||||||
|
registration.addUrlPatterns("/*");
|
||||||
|
registration.setOrder(1);
|
||||||
|
registration.setAsyncSupported(true); // 支持异步请求
|
||||||
|
return registration;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取排除URL列表
|
||||||
|
*/
|
||||||
|
private List<String> getExcludeUrls() {
|
||||||
|
List<String> excludeUrls = new ArrayList<>();
|
||||||
|
|
||||||
|
// 支持yml方式,配置拦截排除
|
||||||
|
if (jeecgBaseConfig != null && jeecgBaseConfig.getShiro() != null) {
|
||||||
|
String shiroExcludeUrls = jeecgBaseConfig.getShiro().getExcludeUrls();
|
||||||
|
if (oConvertUtils.isNotEmpty(shiroExcludeUrls)) {
|
||||||
|
String[] permissionUrl = shiroExcludeUrls.split(",");
|
||||||
|
excludeUrls.addAll(Arrays.asList(permissionUrl));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加默认排除路径
|
||||||
|
excludeUrls.addAll(Arrays.asList(
|
||||||
|
"/sys/cas/client/validateLogin", // cas验证登录
|
||||||
|
"/sys/randomImage/**", // 登录验证码接口排除
|
||||||
|
"/sys/checkCaptcha", // 登录验证码接口排除
|
||||||
|
"/sys/smsCheckCaptcha", // 短信次数发送太多验证码排除
|
||||||
|
"/sys/login", // 登录接口排除
|
||||||
|
"/sys/mLogin", // 登录接口排除
|
||||||
|
"/sys/logout", // 登出接口排除
|
||||||
|
"/sys/thirdLogin/**", // 第三方登录
|
||||||
|
"/sys/getEncryptedString", // 获取加密串
|
||||||
|
"/sys/sms", // 短信验证码
|
||||||
|
"/sys/phoneLogin", // 手机登录
|
||||||
|
"/sys/user/checkOnlyUser", // 校验用户是否存在
|
||||||
|
"/sys/user/register", // 用户注册
|
||||||
|
"/sys/user/phoneVerification", // 用户忘记密码验证手机号
|
||||||
|
"/sys/user/passwordChange", // 用户更改密码
|
||||||
|
"/auth/2step-code", // 登录验证码
|
||||||
|
"/sys/common/static/**", // 图片预览 & 下载文件不限制token
|
||||||
|
"/sys/common/pdf/**", // pdf预览
|
||||||
|
"/generic/**", // pdf预览需要文件
|
||||||
|
"/sys/getLoginQrcode/**", // 登录二维码
|
||||||
|
"/sys/getQrcodeToken/**", // 监听扫码
|
||||||
|
"/sys/checkAuth", // 授权接口排除
|
||||||
|
"/openapi/call/**", // 开放平台接口排除
|
||||||
|
|
||||||
|
// 排除静态资源后缀
|
||||||
|
"/",
|
||||||
|
"/doc.html",
|
||||||
|
"**/*.js",
|
||||||
|
"**/*.css",
|
||||||
|
"**/*.html",
|
||||||
|
"**/*.svg",
|
||||||
|
"**/*.pdf",
|
||||||
|
"**/*.jpg",
|
||||||
|
"**/*.png",
|
||||||
|
"**/*.gif",
|
||||||
|
"**/*.ico",
|
||||||
|
"**/*.ttf",
|
||||||
|
"**/*.woff",
|
||||||
|
"**/*.woff2",
|
||||||
|
"**/*.glb",
|
||||||
|
"**/*.wasm",
|
||||||
|
"**/*.js.map",
|
||||||
|
"**/*.css.map",
|
||||||
|
|
||||||
|
"/druid/**",
|
||||||
|
"/swagger-ui.html",
|
||||||
|
"/swagger*/**",
|
||||||
|
"/webjars/**",
|
||||||
|
"/v3/**",
|
||||||
|
|
||||||
|
// 排除消息通告查看详情页面(用于第三方APP)
|
||||||
|
"/sys/annountCement/show/**",
|
||||||
|
|
||||||
|
// 积木报表和积木BI排除
|
||||||
|
"/jmreport/**",
|
||||||
|
"/drag/lib/**",
|
||||||
|
"/drag/list/**",
|
||||||
|
"/drag/favicon.ico",
|
||||||
|
"/drag/view",
|
||||||
|
"/drag/page/queryById",
|
||||||
|
"/drag/page/addVisitsNumber",
|
||||||
|
"/drag/page/queryTemplateList",
|
||||||
|
"/drag/share/view/**",
|
||||||
|
"/drag/onlDragDatasetHead/getAllChartData",
|
||||||
|
"/drag/onlDragDatasetHead/getTotalData",
|
||||||
|
"/drag/onlDragDatasetHead/getMapDataByCode",
|
||||||
|
"/drag/onlDragDatasetHead/getTotalDataByCompId",
|
||||||
|
"/drag/mock/json/**",
|
||||||
|
"/drag/onlDragDatasetHead/getDictByCodes",
|
||||||
|
"/drag/onlDragDatasetHead/queryAllById",
|
||||||
|
"/jimubi/view",
|
||||||
|
"/jimubi/share/view/**",
|
||||||
|
|
||||||
|
// 大屏模板例子
|
||||||
|
"/test/bigScreen/**",
|
||||||
|
"/bigscreen/template1/**",
|
||||||
|
"/bigscreen/template2/**",
|
||||||
|
|
||||||
|
// websocket排除
|
||||||
|
"/websocket/**", // 系统通知和公告
|
||||||
|
"/newsWebsocket/**", // CMS模块
|
||||||
|
"/vxeSocket/**", // JVxeTable无痕刷新示例
|
||||||
|
"/dragChannelSocket/**", // 仪表盘(按钮通信)
|
||||||
|
|
||||||
|
// App vue3版本查询版本接口
|
||||||
|
"/sys/version/app3version",
|
||||||
|
|
||||||
|
// 测试模块排除
|
||||||
|
"/test/seata/**",
|
||||||
|
|
||||||
|
// 错误路径排除
|
||||||
|
"/error",
|
||||||
|
|
||||||
|
// 企业微信证书排除
|
||||||
|
"/WW_verify*"
|
||||||
|
));
|
||||||
|
|
||||||
|
return excludeUrls;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验用户的tenant_id和前端传过来的是否一致
|
||||||
|
*
|
||||||
|
* <p>实现逻辑:
|
||||||
|
* <ul>
|
||||||
|
* <li>1. 获取当前登录用户信息</li>
|
||||||
|
* <li>2. 检查用户是否配置了租户信息</li>
|
||||||
|
* <li>3. 获取前端请求头中的租户ID</li>
|
||||||
|
* <li>4. 校验用户所属租户中是否包含当前请求的租户ID</li>
|
||||||
|
* <li>5. 如果校验失败,从数据库重新查询用户信息并再次校验</li>
|
||||||
|
* <li>6. 最终校验失败则抛出异常</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* @throws NotLoginException 租户授权变更异常
|
||||||
|
*/
|
||||||
|
private void checkTenantAuthorization() {
|
||||||
|
log.debug("------ 租户校验开始 ------");
|
||||||
|
// 如果未开启租户控制,直接返回
|
||||||
|
if (!MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 获取当前登录用户信息
|
||||||
|
LoginUser loginUser = TokenUtils.getLoginUser(LoginUserUtils.getUsername(), commonAPI, redisUtil);
|
||||||
|
if (loginUser == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String username = loginUser.getUsername();
|
||||||
|
String userTenantIds = loginUser.getRelTenantIds();
|
||||||
|
|
||||||
|
// 如果用户未配置租户信息,直接返回
|
||||||
|
if (oConvertUtils.isEmpty(userTenantIds)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取前端请求头中的租户ID
|
||||||
|
String loginTenantId = TokenUtils.getTenantIdByRequest(SpringContextUtils.getHttpServletRequest());
|
||||||
|
log.info("登录租户:{}", loginTenantId);
|
||||||
|
log.info("用户拥有那些租户:{}", userTenantIds);
|
||||||
|
|
||||||
|
// 登录用户无租户,前端header中租户ID值为 0
|
||||||
|
String str = "0";
|
||||||
|
if (oConvertUtils.isEmpty(loginTenantId) || str.equals(loginTenantId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String[] userTenantIdsArray = userTenantIds.split(",");
|
||||||
|
if (!oConvertUtils.isIn(loginTenantId, userTenantIdsArray)) {
|
||||||
|
boolean isAuthorization = false;
|
||||||
|
|
||||||
|
//========================================================================
|
||||||
|
// 查询用户信息(如果租户不匹配从数据库中重新查询一次用户信息)
|
||||||
|
String loginUserKey = CacheConstant.SYS_USERS_CACHE + "::" + username;
|
||||||
|
redisUtil.del(loginUserKey);
|
||||||
|
|
||||||
|
LoginUser loginUserFromDb = commonAPI.getUserByName(username);
|
||||||
|
LoginUserUtils.setSessionUser(loginUserFromDb);
|
||||||
|
if (loginUserFromDb != null && oConvertUtils.isNotEmpty(loginUserFromDb.getRelTenantIds())) {
|
||||||
|
String[] newArray = loginUserFromDb.getRelTenantIds().split(",");
|
||||||
|
if (oConvertUtils.isIn(loginTenantId, newArray)) {
|
||||||
|
isAuthorization = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//========================================================================
|
||||||
|
|
||||||
|
if (!isAuthorization) {
|
||||||
|
log.info("租户异常——登录租户:{}", loginTenantId);
|
||||||
|
log.info("租户异常——用户拥有租户组:{}", userTenantIds);
|
||||||
|
throw new NotLoginException("登录租户授权变更,请重新登陆!", StpUtil.TYPE, NotLoginException.KICK_OUT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}catch (Exception e) {
|
||||||
|
log.error("租户校验异常:{}", e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ -0,0 +1,174 @@
|
|||||||
|
package org.jeecg.config.satoken;
|
||||||
|
|
||||||
|
import cn.dev33.satoken.dao.SaTokenDao;
|
||||||
|
import cn.dev33.satoken.SaManager;
|
||||||
|
import cn.dev33.satoken.stp.StpInterface;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.jeecg.common.api.CommonAPI;
|
||||||
|
import org.springframework.context.annotation.Lazy;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: Sa-Token 权限认证接口实现(带缓存)
|
||||||
|
*
|
||||||
|
* <p>⚠️ 重要说明:</p>
|
||||||
|
* <ul>
|
||||||
|
* <li><strong>Sa-Token 的 StpInterface 默认不提供缓存能力</strong>,需要自己实现缓存逻辑</li>
|
||||||
|
* <li>本实现采用 <strong>[账号id -> 权限/角色列表]</strong> 缓存模型</li>
|
||||||
|
* <li>缓存键格式:
|
||||||
|
* <ul>
|
||||||
|
* <li>用户权限缓存:satoken:user-permission:{username}</li>
|
||||||
|
* <li>用户角色缓存:satoken:user-role:{username}</li>
|
||||||
|
* </ul>
|
||||||
|
* </li>
|
||||||
|
* <li>缓存过期时间:30天</li>
|
||||||
|
* <li>⚠️ 当修改用户的角色或权限时,需要手动清除缓存</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p>清除缓存示例:</p>
|
||||||
|
* <pre>
|
||||||
|
* // 清除单个用户的权限和角色缓存
|
||||||
|
* StpInterfaceImpl.clearUserCache("admin");
|
||||||
|
*
|
||||||
|
* // 清除多个用户的缓存
|
||||||
|
* StpInterfaceImpl.clearUserCache(Arrays.asList("admin", "user1", "user2"));
|
||||||
|
* </pre>
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
@Slf4j
|
||||||
|
public class StpInterfaceImpl implements StpInterface {
|
||||||
|
|
||||||
|
@Lazy
|
||||||
|
@Resource
|
||||||
|
private CommonAPI commonApi;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 缓存过期时间(秒):30天
|
||||||
|
*/
|
||||||
|
private static final long CACHE_TIMEOUT = 60 * 60 * 24 * 30;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 权限缓存键前缀
|
||||||
|
*/
|
||||||
|
private static final String PERMISSION_CACHE_PREFIX = "satoken:user-permission:";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 角色缓存键前缀
|
||||||
|
*/
|
||||||
|
private static final String ROLE_CACHE_PREFIX = "satoken:user-role:";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 返回一个账号所拥有的权限码集合(带缓存)
|
||||||
|
*
|
||||||
|
* @param loginId 账号id(这里是 username)
|
||||||
|
* @param loginType 账号类型
|
||||||
|
* @return 权限码集合
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public List<String> getPermissionList(Object loginId, String loginType) {
|
||||||
|
String username = loginId.toString();
|
||||||
|
String cacheKey = PERMISSION_CACHE_PREFIX + username;
|
||||||
|
|
||||||
|
SaTokenDao dao = SaManager.getSaTokenDao();
|
||||||
|
|
||||||
|
// 1. 先从缓存获取
|
||||||
|
List<String> permissionList = (List<String>) dao.getObject(cacheKey);
|
||||||
|
|
||||||
|
if (permissionList == null) {
|
||||||
|
// 2. 缓存不存在,从数据库查询
|
||||||
|
log.warn("权限缓存未命中,查询数据库 [ username={} ]", username);
|
||||||
|
|
||||||
|
String userId = commonApi.getUserIdByName(username);
|
||||||
|
if (userId == null) {
|
||||||
|
log.warn("用户不存在: {}", username);
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
Set<String> permissionSet = commonApi.queryUserAuths(userId);
|
||||||
|
permissionList = new ArrayList<>(permissionSet);
|
||||||
|
|
||||||
|
// 3. 将结果缓存起来
|
||||||
|
dao.setObject(cacheKey, permissionList, CACHE_TIMEOUT);
|
||||||
|
log.info("权限已缓存 [ username={}, permissions={} ]", username, permissionList.size());
|
||||||
|
} else {
|
||||||
|
log.debug("权限缓存命中 [ username={}, permissions={} ]", username, permissionList.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
return permissionList;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 返回一个账号所拥有的角色标识集合(带缓存)
|
||||||
|
*
|
||||||
|
* @param loginId 账号id(这里是 username)
|
||||||
|
* @param loginType 账号类型
|
||||||
|
* @return 角色标识集合
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public List<String> getRoleList(Object loginId, String loginType) {
|
||||||
|
String username = loginId.toString();
|
||||||
|
String cacheKey = ROLE_CACHE_PREFIX + username;
|
||||||
|
|
||||||
|
SaTokenDao dao = SaManager.getSaTokenDao();
|
||||||
|
|
||||||
|
// 1. 先从缓存获取
|
||||||
|
List<String> roleList = (List<String>) dao.getObject(cacheKey);
|
||||||
|
|
||||||
|
if (roleList == null) {
|
||||||
|
// 2. 缓存不存在,从数据库查询
|
||||||
|
log.warn("角色缓存未命中,查询数据库 [ username={} ]", username);
|
||||||
|
|
||||||
|
String userId = commonApi.getUserIdByName(username);
|
||||||
|
if (userId == null) {
|
||||||
|
log.warn("用户不存在: {}", username);
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
Set<String> roleSet = commonApi.queryUserRolesById(userId);
|
||||||
|
roleList = new ArrayList<>(roleSet);
|
||||||
|
|
||||||
|
// 3. 将结果缓存起来
|
||||||
|
dao.setObject(cacheKey, roleList, CACHE_TIMEOUT);
|
||||||
|
log.info("角色已缓存 [ username={}, roles={} ]", username, roleList.size());
|
||||||
|
} else {
|
||||||
|
log.debug("角色缓存命中 [ username={}, roles={} ]", username, roleList.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
return roleList;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除单个用户的权限和角色缓存
|
||||||
|
* <p>使用场景:修改用户的角色分配后</p>
|
||||||
|
*
|
||||||
|
* @param username 用户名
|
||||||
|
*/
|
||||||
|
public static void clearUserCache(String username) {
|
||||||
|
SaTokenDao dao = SaManager.getSaTokenDao();
|
||||||
|
dao.deleteObject(PERMISSION_CACHE_PREFIX + username);
|
||||||
|
dao.deleteObject(ROLE_CACHE_PREFIX + username);
|
||||||
|
log.info("已清除用户缓存 [ username={} ]", username);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量清除多个用户的权限和角色缓存
|
||||||
|
* <p>使用场景:修改角色权限后,清除拥有该角色的所有用户的缓存</p>
|
||||||
|
*
|
||||||
|
* @param usernameList 用户名列表
|
||||||
|
*/
|
||||||
|
public static void clearUserCache(List<String> usernameList) {
|
||||||
|
SaTokenDao dao = SaManager.getSaTokenDao();
|
||||||
|
for (String username : usernameList) {
|
||||||
|
dao.deleteObject(PERMISSION_CACHE_PREFIX + username);
|
||||||
|
dao.deleteObject(ROLE_CACHE_PREFIX + username);
|
||||||
|
}
|
||||||
|
log.info("已批量清除用户缓存 [ count={} ]", usernameList.size());
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,104 @@
|
|||||||
|
package org.jeecg.config.satoken.ignore;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.jeecg.config.satoken.IgnoreAuth;
|
||||||
|
import org.springframework.beans.factory.InitializingBean;
|
||||||
|
import org.springframework.context.annotation.Lazy;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.util.CollectionUtils;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
|
||||||
|
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在spring boot初始化时,根据@RestController注解获取当前spring容器中的bean
|
||||||
|
* @author eightmonth
|
||||||
|
* @date 2024/4/18 11:35
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Lazy(false)
|
||||||
|
@Component
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class IgnoreAuthPostProcessor implements InitializingBean {
|
||||||
|
|
||||||
|
private RequestMappingHandlerMapping requestMappingHandlerMapping;
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void afterPropertiesSet() throws Exception {
|
||||||
|
|
||||||
|
long startTime = System.currentTimeMillis();
|
||||||
|
|
||||||
|
List<String> ignoreAuthUrls = new ArrayList<>();
|
||||||
|
|
||||||
|
// 优化:直接从HandlerMethod过滤,避免重复扫描
|
||||||
|
requestMappingHandlerMapping.getHandlerMethods().values().stream()
|
||||||
|
.filter(handlerMethod -> handlerMethod.getMethod().isAnnotationPresent(IgnoreAuth.class))
|
||||||
|
.forEach(handlerMethod -> {
|
||||||
|
Class<?> clazz = handlerMethod.getBeanType();
|
||||||
|
Method method = handlerMethod.getMethod();
|
||||||
|
ignoreAuthUrls.addAll(processIgnoreAuthMethod(clazz, method));
|
||||||
|
});
|
||||||
|
|
||||||
|
log.info("Init Token ignoreAuthUrls Config [ 集合 ] :{}", ignoreAuthUrls);
|
||||||
|
if (!CollectionUtils.isEmpty(ignoreAuthUrls)) {
|
||||||
|
InMemoryIgnoreAuth.set(ignoreAuthUrls);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算方法的耗时
|
||||||
|
long endTime = System.currentTimeMillis();
|
||||||
|
long elapsedTime = endTime - startTime;
|
||||||
|
log.info("Init Token ignoreAuthUrls Config [ 耗时 ] :" + elapsedTime + "ms");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 优化:新方法处理单个@IgnoreAuth方法,减少重复注解检查
|
||||||
|
private List<String> processIgnoreAuthMethod(Class<?> clazz, Method method) {
|
||||||
|
RequestMapping base = clazz.getAnnotation(RequestMapping.class);
|
||||||
|
String[] baseUrl = Objects.nonNull(base) ? base.value() : new String[]{};
|
||||||
|
|
||||||
|
String[] uri = null;
|
||||||
|
if (method.isAnnotationPresent(RequestMapping.class)) {
|
||||||
|
uri = method.getAnnotation(RequestMapping.class).value();
|
||||||
|
} else if (method.isAnnotationPresent(GetMapping.class)) {
|
||||||
|
uri = method.getAnnotation(GetMapping.class).value();
|
||||||
|
} else if (method.isAnnotationPresent(PostMapping.class)) {
|
||||||
|
uri = method.getAnnotation(PostMapping.class).value();
|
||||||
|
} else if (method.isAnnotationPresent(PutMapping.class)) {
|
||||||
|
uri = method.getAnnotation(PutMapping.class).value();
|
||||||
|
} else if (method.isAnnotationPresent(DeleteMapping.class)) {
|
||||||
|
uri = method.getAnnotation(DeleteMapping.class).value();
|
||||||
|
} else if (method.isAnnotationPresent(PatchMapping.class)) {
|
||||||
|
uri = method.getAnnotation(PatchMapping.class).value();
|
||||||
|
}
|
||||||
|
|
||||||
|
return uri != null ? rebuildUrl(baseUrl, uri) : Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> rebuildUrl(String[] bases, String[] uris) {
|
||||||
|
List<String> urls = new ArrayList<>();
|
||||||
|
if (bases.length > 0) {
|
||||||
|
for (String base : bases) {
|
||||||
|
for (String uri : uris) {
|
||||||
|
// 如果uri包含路径占位符, 则需要将其替换为*
|
||||||
|
if (uri.matches(".*\\{.*}.*")) {
|
||||||
|
uri = uri.replaceAll("\\{.*?}", "*");
|
||||||
|
}
|
||||||
|
urls.add(prefix(base) + prefix(uri));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Arrays.stream(uris).forEach(uri -> {
|
||||||
|
urls.add(prefix(uri));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return urls;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String prefix(String seg) {
|
||||||
|
return seg.startsWith("/") ? seg : "/"+seg;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package org.jeecg.config.shiro.ignore;
|
package org.jeecg.config.satoken.ignore;
|
||||||
|
|
||||||
import org.springframework.util.AntPathMatcher;
|
import org.springframework.util.AntPathMatcher;
|
||||||
import org.springframework.util.PathMatcher;
|
import org.springframework.util.PathMatcher;
|
||||||
@ -6,8 +6,8 @@ import java.util.ArrayList;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 使用内存存储通过@IgnoreAuth注解的url,配合JwtFilter进行免登录校验
|
* 使用内存存储通过@IgnoreAuth注解的url,配合Sa-Token进行免登录校验
|
||||||
* PS:无法使用ThreadLocal进行存储,因为ThreadLocal装载时,JwtFilter已经初始化完毕,导致该类获取ThreadLocal为空
|
* PS:无法使用ThreadLocal进行存储,因为ThreadLocal装载时,Filter已经初始化完毕,导致该类获取ThreadLocal为空
|
||||||
* @author eightmonth
|
* @author eightmonth
|
||||||
* @date 2024/4/18 15:02
|
* @date 2024/4/18 15:02
|
||||||
*/
|
*/
|
||||||
@ -15,6 +15,7 @@ public class InMemoryIgnoreAuth {
|
|||||||
private static final List<String> IGNORE_AUTH_LIST = new ArrayList<>();
|
private static final List<String> IGNORE_AUTH_LIST = new ArrayList<>();
|
||||||
|
|
||||||
private static PathMatcher MATCHER = new AntPathMatcher();
|
private static PathMatcher MATCHER = new AntPathMatcher();
|
||||||
|
|
||||||
public InMemoryIgnoreAuth() {}
|
public InMemoryIgnoreAuth() {}
|
||||||
|
|
||||||
public static void set(List<String> list) {
|
public static void set(List<String> list) {
|
||||||
@ -31,11 +32,11 @@ public class InMemoryIgnoreAuth {
|
|||||||
|
|
||||||
public static boolean contains(String url) {
|
public static boolean contains(String url) {
|
||||||
for (String ignoreAuth : IGNORE_AUTH_LIST) {
|
for (String ignoreAuth : IGNORE_AUTH_LIST) {
|
||||||
if(MATCHER.match(ignoreAuth,url)){
|
if(MATCHER.match(ignoreAuth, url)){
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1,28 +0,0 @@
|
|||||||
package org.jeecg.config.shiro;
|
|
||||||
|
|
||||||
import org.apache.shiro.authc.AuthenticationToken;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @Author Scott
|
|
||||||
* @create 2018-07-12 15:19
|
|
||||||
* @desc
|
|
||||||
**/
|
|
||||||
public class JwtToken implements AuthenticationToken {
|
|
||||||
|
|
||||||
private static final long serialVersionUID = 1L;
|
|
||||||
private String token;
|
|
||||||
|
|
||||||
public JwtToken(String token) {
|
|
||||||
this.token = token;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Object getPrincipal() {
|
|
||||||
return token;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Object getCredentials() {
|
|
||||||
return token;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,393 +0,0 @@
|
|||||||
package org.jeecg.config.shiro;
|
|
||||||
|
|
||||||
import jakarta.annotation.Resource;
|
|
||||||
import jakarta.servlet.DispatcherType;
|
|
||||||
import jakarta.servlet.Filter;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
|
|
||||||
import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;
|
|
||||||
import org.apache.shiro.mgt.DefaultSubjectDAO;
|
|
||||||
import org.apache.shiro.mgt.SecurityManager;
|
|
||||||
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
|
|
||||||
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
|
|
||||||
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
|
|
||||||
import org.apache.shiro.spring.web.ShiroUrlPathHelper;
|
|
||||||
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
|
|
||||||
import org.crazycake.shiro.*;
|
|
||||||
import org.jeecg.common.constant.CommonConstant;
|
|
||||||
import org.jeecg.common.util.oConvertUtils;
|
|
||||||
import org.jeecg.config.JeecgBaseConfig;
|
|
||||||
import org.jeecg.config.shiro.filters.CustomShiroFilterFactoryBean;
|
|
||||||
import org.jeecg.config.shiro.filters.JwtFilter;
|
|
||||||
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.beans.factory.annotation.Qualifier;
|
|
||||||
import org.springframework.beans.factory.config.BeanDefinition;
|
|
||||||
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
|
|
||||||
import org.springframework.boot.web.servlet.FilterRegistrationBean;
|
|
||||||
import org.springframework.context.annotation.*;
|
|
||||||
import org.springframework.core.env.Environment;
|
|
||||||
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
|
|
||||||
import org.springframework.util.CollectionUtils;
|
|
||||||
import org.springframework.util.StringUtils;
|
|
||||||
import org.springframework.web.filter.DelegatingFilterProxy;
|
|
||||||
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
|
|
||||||
import redis.clients.jedis.HostAndPort;
|
|
||||||
import redis.clients.jedis.JedisCluster;
|
|
||||||
|
|
||||||
import java.util.*;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @author: Scott
|
|
||||||
* @date: 2018/2/7
|
|
||||||
* @description: shiro 配置类
|
|
||||||
*/
|
|
||||||
|
|
||||||
@Slf4j
|
|
||||||
@Configuration
|
|
||||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
|
||||||
public class ShiroConfig {
|
|
||||||
|
|
||||||
@Resource
|
|
||||||
private LettuceConnectionFactory lettuceConnectionFactory;
|
|
||||||
@Autowired
|
|
||||||
private Environment env;
|
|
||||||
@Resource
|
|
||||||
private JeecgBaseConfig jeecgBaseConfig;
|
|
||||||
@Autowired(required = false)
|
|
||||||
private RedisProperties redisProperties;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Filter Chain定义说明
|
|
||||||
*
|
|
||||||
* 1、一个URL可以配置多个Filter,使用逗号分隔
|
|
||||||
* 2、当设置多个过滤器时,全部验证通过,才视为通过
|
|
||||||
* 3、部分过滤器可指定参数,如perms,roles
|
|
||||||
*/
|
|
||||||
@Bean("shiroFilterFactoryBean")
|
|
||||||
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
|
|
||||||
CustomShiroFilterFactoryBean shiroFilterFactoryBean = new CustomShiroFilterFactoryBean();
|
|
||||||
shiroFilterFactoryBean.setSecurityManager(securityManager);
|
|
||||||
// 拦截器
|
|
||||||
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
|
|
||||||
|
|
||||||
//支持yml方式,配置拦截排除
|
|
||||||
if(jeecgBaseConfig!=null && jeecgBaseConfig.getShiro()!=null){
|
|
||||||
String shiroExcludeUrls = jeecgBaseConfig.getShiro().getExcludeUrls();
|
|
||||||
if(oConvertUtils.isNotEmpty(shiroExcludeUrls)){
|
|
||||||
String[] permissionUrl = shiroExcludeUrls.split(",");
|
|
||||||
for(String url : permissionUrl){
|
|
||||||
filterChainDefinitionMap.put(url,"anon");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 配置不会被拦截的链接 顺序判断
|
|
||||||
filterChainDefinitionMap.put("/sys/cas/client/validateLogin", "anon"); //cas验证登录
|
|
||||||
filterChainDefinitionMap.put("/sys/randomImage/**", "anon"); //登录验证码接口排除
|
|
||||||
filterChainDefinitionMap.put("/sys/checkCaptcha", "anon"); //登录验证码接口排除
|
|
||||||
filterChainDefinitionMap.put("/sys/smsCheckCaptcha", "anon"); //短信次数发送太多验证码排除
|
|
||||||
filterChainDefinitionMap.put("/sys/login", "anon"); //登录接口排除
|
|
||||||
filterChainDefinitionMap.put("/sys/mLogin", "anon"); //登录接口排除
|
|
||||||
filterChainDefinitionMap.put("/sys/logout", "anon"); //登出接口排除
|
|
||||||
filterChainDefinitionMap.put("/sys/thirdLogin/**", "anon"); //第三方登录
|
|
||||||
filterChainDefinitionMap.put("/sys/getEncryptedString", "anon"); //获取加密串
|
|
||||||
filterChainDefinitionMap.put("/sys/sms", "anon");//短信验证码
|
|
||||||
filterChainDefinitionMap.put("/sys/phoneLogin", "anon");//手机登录
|
|
||||||
filterChainDefinitionMap.put("/sys/user/checkOnlyUser", "anon");//校验用户是否存在
|
|
||||||
filterChainDefinitionMap.put("/sys/user/register", "anon");//用户注册
|
|
||||||
filterChainDefinitionMap.put("/sys/user/phoneVerification", "anon");//用户忘记密码验证手机号
|
|
||||||
filterChainDefinitionMap.put("/sys/user/passwordChange", "anon");//用户更改密码
|
|
||||||
filterChainDefinitionMap.put("/auth/2step-code", "anon");//登录验证码
|
|
||||||
filterChainDefinitionMap.put("/sys/common/static/**", "anon");//图片预览 &下载文件不限制token
|
|
||||||
filterChainDefinitionMap.put("/sys/common/pdf/**", "anon");//pdf预览
|
|
||||||
|
|
||||||
//filterChainDefinitionMap.put("/sys/common/view/**", "anon");//图片预览不限制token
|
|
||||||
//filterChainDefinitionMap.put("/sys/common/download/**", "anon");//文件下载不限制token
|
|
||||||
filterChainDefinitionMap.put("/generic/**", "anon");//pdf预览需要文件
|
|
||||||
|
|
||||||
filterChainDefinitionMap.put("/sys/getLoginQrcode/**", "anon"); //登录二维码
|
|
||||||
filterChainDefinitionMap.put("/sys/getQrcodeToken/**", "anon"); //监听扫码
|
|
||||||
filterChainDefinitionMap.put("/sys/checkAuth", "anon"); //授权接口排除
|
|
||||||
filterChainDefinitionMap.put("/openapi/call/**", "anon"); // 开放平台接口排除
|
|
||||||
|
|
||||||
//update-begin--Author:scott Date:20221116 for:排除静态资源后缀
|
|
||||||
filterChainDefinitionMap.put("/", "anon");
|
|
||||||
filterChainDefinitionMap.put("/doc.html", "anon");
|
|
||||||
filterChainDefinitionMap.put("/**/*.js", "anon");
|
|
||||||
filterChainDefinitionMap.put("/**/*.css", "anon");
|
|
||||||
filterChainDefinitionMap.put("/**/*.html", "anon");
|
|
||||||
filterChainDefinitionMap.put("/**/*.svg", "anon");
|
|
||||||
filterChainDefinitionMap.put("/**/*.pdf", "anon");
|
|
||||||
filterChainDefinitionMap.put("/**/*.jpg", "anon");
|
|
||||||
filterChainDefinitionMap.put("/**/*.png", "anon");
|
|
||||||
filterChainDefinitionMap.put("/**/*.gif", "anon");
|
|
||||||
filterChainDefinitionMap.put("/**/*.ico", "anon");
|
|
||||||
filterChainDefinitionMap.put("/**/*.ttf", "anon");
|
|
||||||
filterChainDefinitionMap.put("/**/*.woff", "anon");
|
|
||||||
filterChainDefinitionMap.put("/**/*.woff2", "anon");
|
|
||||||
|
|
||||||
filterChainDefinitionMap.put("/**/*.glb", "anon");
|
|
||||||
filterChainDefinitionMap.put("/**/*.wasm", "anon");
|
|
||||||
//update-end--Author:scott Date:20221116 for:排除静态资源后缀
|
|
||||||
|
|
||||||
filterChainDefinitionMap.put("/druid/**", "anon");
|
|
||||||
filterChainDefinitionMap.put("/swagger-ui.html", "anon");
|
|
||||||
filterChainDefinitionMap.put("/swagger**/**", "anon");
|
|
||||||
filterChainDefinitionMap.put("/webjars/**", "anon");
|
|
||||||
filterChainDefinitionMap.put("/v3/**", "anon");
|
|
||||||
|
|
||||||
// update-begin--Author:sunjianlei Date:20210510 for:排除消息通告查看详情页面(用于第三方APP)
|
|
||||||
filterChainDefinitionMap.put("/sys/annountCement/show/**", "anon");
|
|
||||||
// update-end--Author:sunjianlei Date:20210510 for:排除消息通告查看详情页面(用于第三方APP)
|
|
||||||
|
|
||||||
//积木报表排除
|
|
||||||
filterChainDefinitionMap.put("/jmreport/**", "anon");
|
|
||||||
filterChainDefinitionMap.put("/**/*.js.map", "anon");
|
|
||||||
filterChainDefinitionMap.put("/**/*.css.map", "anon");
|
|
||||||
|
|
||||||
//积木BI大屏和仪表盘排除
|
|
||||||
filterChainDefinitionMap.put("/drag/view", "anon");
|
|
||||||
filterChainDefinitionMap.put("/drag/page/queryById", "anon");
|
|
||||||
filterChainDefinitionMap.put("/drag/page/addVisitsNumber", "anon");
|
|
||||||
filterChainDefinitionMap.put("/drag/page/queryTemplateList", "anon");
|
|
||||||
filterChainDefinitionMap.put("/drag/share/view/**", "anon");
|
|
||||||
filterChainDefinitionMap.put("/drag/onlDragDatasetHead/getAllChartData", "anon");
|
|
||||||
filterChainDefinitionMap.put("/drag/onlDragDatasetHead/getTotalData", "anon");
|
|
||||||
filterChainDefinitionMap.put("/drag/onlDragDatasetHead/getMapDataByCode", "anon");
|
|
||||||
filterChainDefinitionMap.put("/drag/onlDragDatasetHead/getTotalDataByCompId", "anon");
|
|
||||||
filterChainDefinitionMap.put("/drag/mock/json/**", "anon");
|
|
||||||
filterChainDefinitionMap.put("/drag/onlDragDatasetHead/getDictByCodes", "anon");
|
|
||||||
|
|
||||||
filterChainDefinitionMap.put("/jimubi/view", "anon");
|
|
||||||
filterChainDefinitionMap.put("/jimubi/share/view/**", "anon");
|
|
||||||
|
|
||||||
//大屏模板例子
|
|
||||||
filterChainDefinitionMap.put("/test/bigScreen/**", "anon");
|
|
||||||
filterChainDefinitionMap.put("/bigscreen/template1/**", "anon");
|
|
||||||
filterChainDefinitionMap.put("/bigscreen/template2/**", "anon");
|
|
||||||
//filterChainDefinitionMap.put("/test/jeecgDemo/rabbitMqClientTest/**", "anon"); //MQ测试
|
|
||||||
//filterChainDefinitionMap.put("/test/jeecgDemo/html", "anon"); //模板页面
|
|
||||||
//filterChainDefinitionMap.put("/test/jeecgDemo/redis/**", "anon"); //redis测试
|
|
||||||
|
|
||||||
//websocket排除
|
|
||||||
filterChainDefinitionMap.put("/websocket/**", "anon");//系统通知和公告
|
|
||||||
filterChainDefinitionMap.put("/newsWebsocket/**", "anon");//CMS模块
|
|
||||||
filterChainDefinitionMap.put("/vxeSocket/**", "anon");//JVxeTable无痕刷新示例
|
|
||||||
//App vue3版本查询版本接口
|
|
||||||
filterChainDefinitionMap.put("/sys/version/app3version", "anon");
|
|
||||||
//仪表盘(按钮通信)
|
|
||||||
filterChainDefinitionMap.put("/dragChannelSocket/**","anon");
|
|
||||||
//App vue3版本查询版本接口
|
|
||||||
filterChainDefinitionMap.put("/sys/version/app3version", "anon");
|
|
||||||
|
|
||||||
//性能监控——安全隐患泄露TOEKN(durid连接池也有)
|
|
||||||
//filterChainDefinitionMap.put("/actuator/**", "anon");
|
|
||||||
//测试模块排除
|
|
||||||
filterChainDefinitionMap.put("/test/seata/**", "anon");
|
|
||||||
|
|
||||||
//错误路径排除
|
|
||||||
filterChainDefinitionMap.put("/error", "anon");
|
|
||||||
// 企业微信证书排除
|
|
||||||
filterChainDefinitionMap.put("/WW_verify*", "anon");
|
|
||||||
|
|
||||||
// 添加自己的过滤器并且取名为jwt
|
|
||||||
Map<String, Filter> filterMap = new HashMap<String, Filter>(1);
|
|
||||||
//如果cloudServer为空 则说明是单体 需要加载跨域配置【微服务跨域切换】
|
|
||||||
Object cloudServer = env.getProperty(CommonConstant.CLOUD_SERVER_KEY);
|
|
||||||
filterMap.put("jwt", new JwtFilter(cloudServer==null));
|
|
||||||
shiroFilterFactoryBean.setFilters(filterMap);
|
|
||||||
// <!-- 过滤链定义,从上向下顺序执行,一般将/**放在最为下边
|
|
||||||
filterChainDefinitionMap.put("/**", "jwt");
|
|
||||||
|
|
||||||
// 未授权界面返回JSON
|
|
||||||
shiroFilterFactoryBean.setUnauthorizedUrl("/sys/common/403");
|
|
||||||
shiroFilterFactoryBean.setLoginUrl("/sys/common/403");
|
|
||||||
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
|
|
||||||
return shiroFilterFactoryBean;
|
|
||||||
}
|
|
||||||
|
|
||||||
//update-begin---author:chenrui ---date:20240126 for:【QQYUN-7932】AI助手------------
|
|
||||||
|
|
||||||
/**
|
|
||||||
* spring过滤装饰器 <br/>
|
|
||||||
* 因为shiro的filter不支持异步请求,导致所有的异步请求都会报错. <br/>
|
|
||||||
* 所以需要用spring的FilterRegistrationBean再代理一下shiro的filter.为他扩展异步支持. <br/>
|
|
||||||
* 后续所有异步的接口都需要再这里增加registration.addUrlPatterns("/xxx/xxx");
|
|
||||||
* @return
|
|
||||||
* @author chenrui
|
|
||||||
* @date 2024/12/3 19:49
|
|
||||||
*/
|
|
||||||
@Bean
|
|
||||||
public FilterRegistrationBean shiroFilterRegistration() {
|
|
||||||
FilterRegistrationBean registration = new FilterRegistrationBean();
|
|
||||||
registration.setFilter(new DelegatingFilterProxy("shiroFilterFactoryBean"));
|
|
||||||
registration.setEnabled(true);
|
|
||||||
//update-begin---author:chenrui ---date:20241202 for:[issues/7491]运行时间好长,效率慢 ------------
|
|
||||||
registration.addUrlPatterns("/test/ai/chat/send");
|
|
||||||
//update-end---author:chenrui ---date:20241202 for:[issues/7491]运行时间好长,效率慢 ------------
|
|
||||||
registration.addUrlPatterns("/airag/flow/run");
|
|
||||||
registration.addUrlPatterns("/airag/flow/debug");
|
|
||||||
registration.addUrlPatterns("/airag/chat/send");
|
|
||||||
registration.addUrlPatterns("/airag/app/debug");
|
|
||||||
registration.addUrlPatterns("/airag/app/prompt/generate");
|
|
||||||
registration.addUrlPatterns("/airag/chat/receive/**");
|
|
||||||
//支持异步
|
|
||||||
registration.setAsyncSupported(true);
|
|
||||||
registration.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ASYNC);
|
|
||||||
return registration;
|
|
||||||
}
|
|
||||||
//update-end---author:chenrui ---date:20240126 for:【QQYUN-7932】AI助手------------
|
|
||||||
|
|
||||||
@Bean("securityManager")
|
|
||||||
public DefaultWebSecurityManager securityManager(ShiroRealm myRealm) {
|
|
||||||
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
|
|
||||||
securityManager.setRealm(myRealm);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* 关闭shiro自带的session,详情见文档
|
|
||||||
* http://shiro.apache.org/session-management.html#SessionManagement-
|
|
||||||
* StatelessApplications%28Sessionless%29
|
|
||||||
*/
|
|
||||||
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
|
|
||||||
DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
|
|
||||||
defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
|
|
||||||
subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
|
|
||||||
securityManager.setSubjectDAO(subjectDAO);
|
|
||||||
//自定义缓存实现,使用redis
|
|
||||||
securityManager.setCacheManager(redisCacheManager());
|
|
||||||
return securityManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 下面的代码是添加注解支持
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
@Bean
|
|
||||||
@DependsOn("lifecycleBeanPostProcessor")
|
|
||||||
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
|
|
||||||
DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
|
|
||||||
defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
|
|
||||||
/**
|
|
||||||
* 解决重复代理问题 github#994
|
|
||||||
* 添加前缀判断 不匹配 任何Advisor
|
|
||||||
*/
|
|
||||||
defaultAdvisorAutoProxyCreator.setUsePrefix(true);
|
|
||||||
defaultAdvisorAutoProxyCreator.setAdvisorBeanNamePrefix("_no_advisor");
|
|
||||||
return defaultAdvisorAutoProxyCreator;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
public static LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
|
|
||||||
return new LifecycleBeanPostProcessor();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
|
|
||||||
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
|
|
||||||
advisor.setSecurityManager(securityManager);
|
|
||||||
return advisor;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* cacheManager 缓存 redis实现
|
|
||||||
* 使用的是shiro-redis开源插件
|
|
||||||
*
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
public RedisCacheManager redisCacheManager() {
|
|
||||||
log.info("===============(1)创建缓存管理器RedisCacheManager");
|
|
||||||
RedisCacheManager redisCacheManager = new RedisCacheManager();
|
|
||||||
redisCacheManager.setRedisManager(redisManager());
|
|
||||||
//redis中针对不同用户缓存(此处的id需要对应user实体中的id字段,用于唯一标识)
|
|
||||||
redisCacheManager.setPrincipalIdFieldName("id");
|
|
||||||
//用户权限信息缓存时间
|
|
||||||
redisCacheManager.setExpire(200000);
|
|
||||||
return redisCacheManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* RedisConfig在项目starter项目中
|
|
||||||
* jeecg-boot-starter-github\jeecg-boot-common\src\main\java\org\jeecg\common\modules\redis\config\RedisConfig.java
|
|
||||||
*
|
|
||||||
* 配置shiro redisManager
|
|
||||||
* 使用的是shiro-redis开源插件
|
|
||||||
*
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
@Bean
|
|
||||||
public IRedisManager redisManager() {
|
|
||||||
log.info("===============(2)创建RedisManager,连接Redis..");
|
|
||||||
IRedisManager manager;
|
|
||||||
// sentinel cluster redis(【issues/5569】shiro集成 redis 不支持 sentinel 方式部署的redis集群 #5569)
|
|
||||||
if (Objects.nonNull(redisProperties)
|
|
||||||
&& Objects.nonNull(redisProperties.getSentinel())
|
|
||||||
&& !CollectionUtils.isEmpty(redisProperties.getSentinel().getNodes())) {
|
|
||||||
RedisSentinelManager sentinelManager = new RedisSentinelManager();
|
|
||||||
sentinelManager.setMasterName(redisProperties.getSentinel().getMaster());
|
|
||||||
sentinelManager.setHost(String.join(",", redisProperties.getSentinel().getNodes()));
|
|
||||||
sentinelManager.setPassword(redisProperties.getPassword());
|
|
||||||
sentinelManager.setDatabase(redisProperties.getDatabase());
|
|
||||||
|
|
||||||
return sentinelManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
// redis 单机支持,在集群为空,或者集群无机器时候使用 add by jzyadmin@163.com
|
|
||||||
if (lettuceConnectionFactory.getClusterConfiguration() == null || lettuceConnectionFactory.getClusterConfiguration().getClusterNodes().isEmpty()) {
|
|
||||||
RedisManager redisManager = new RedisManager();
|
|
||||||
redisManager.setHost(lettuceConnectionFactory.getHostName() + ":" + lettuceConnectionFactory.getPort());
|
|
||||||
//(lettuceConnectionFactory.getPort());
|
|
||||||
redisManager.setDatabase(lettuceConnectionFactory.getDatabase());
|
|
||||||
redisManager.setTimeout(0);
|
|
||||||
if (!StringUtils.isEmpty(lettuceConnectionFactory.getPassword())) {
|
|
||||||
redisManager.setPassword(lettuceConnectionFactory.getPassword());
|
|
||||||
}
|
|
||||||
manager = redisManager;
|
|
||||||
}else{
|
|
||||||
// redis集群支持,优先使用集群配置
|
|
||||||
RedisClusterManager redisManager = new RedisClusterManager();
|
|
||||||
Set<HostAndPort> portSet = new HashSet<>();
|
|
||||||
lettuceConnectionFactory.getClusterConfiguration().getClusterNodes().forEach(node -> portSet.add(new HostAndPort(node.getHost() , node.getPort())));
|
|
||||||
//update-begin--Author:scott Date:20210531 for:修改集群模式下未设置redis密码的bug issues/I3QNIC
|
|
||||||
if (oConvertUtils.isNotEmpty(lettuceConnectionFactory.getPassword())) {
|
|
||||||
JedisCluster jedisCluster = new JedisCluster(portSet, 2000, 2000, 5,
|
|
||||||
lettuceConnectionFactory.getPassword(), new GenericObjectPoolConfig());
|
|
||||||
redisManager.setPassword(lettuceConnectionFactory.getPassword());
|
|
||||||
redisManager.setJedisCluster(jedisCluster);
|
|
||||||
} else {
|
|
||||||
JedisCluster jedisCluster = new JedisCluster(portSet);
|
|
||||||
redisManager.setJedisCluster(jedisCluster);
|
|
||||||
}
|
|
||||||
//update-end--Author:scott Date:20210531 for:修改集群模式下未设置redis密码的bug issues/I3QNIC
|
|
||||||
manager = redisManager;
|
|
||||||
}
|
|
||||||
return manager;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 解决 ShiroRequestMappingConfig 获取 requestMappingHandlerMapping Bean 冲突
|
|
||||||
* spring-boot-autoconfigure:3.4.5 和 spring-boot-actuator-autoconfigure:3.4.5
|
|
||||||
*/
|
|
||||||
@Primary
|
|
||||||
@Bean
|
|
||||||
public RequestMappingHandlerMapping overridedRequestMappingHandlerMapping() {
|
|
||||||
RequestMappingHandlerMapping mapping = new RequestMappingHandlerMapping();
|
|
||||||
mapping.setUrlPathHelper(new ShiroUrlPathHelper());
|
|
||||||
return mapping;
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<String> rebuildUrl(String[] bases, String[] uris) {
|
|
||||||
List<String> urls = new ArrayList<>();
|
|
||||||
for (String base : bases) {
|
|
||||||
for (String uri : uris) {
|
|
||||||
urls.add(prefix(base)+prefix(uri));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return urls;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String prefix(String seg) {
|
|
||||||
return seg.startsWith("/") ? seg : "/"+seg;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,237 +0,0 @@
|
|||||||
package org.jeecg.config.shiro;
|
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.apache.shiro.authc.AuthenticationException;
|
|
||||||
import org.apache.shiro.authc.AuthenticationInfo;
|
|
||||||
import org.apache.shiro.authc.AuthenticationToken;
|
|
||||||
import org.apache.shiro.authc.SimpleAuthenticationInfo;
|
|
||||||
import org.apache.shiro.authz.AuthorizationInfo;
|
|
||||||
import org.apache.shiro.authz.SimpleAuthorizationInfo;
|
|
||||||
import org.apache.shiro.realm.AuthorizingRealm;
|
|
||||||
import org.apache.shiro.subject.PrincipalCollection;
|
|
||||||
import org.jeecg.common.api.CommonAPI;
|
|
||||||
import org.jeecg.common.config.TenantContext;
|
|
||||||
import org.jeecg.common.constant.CacheConstant;
|
|
||||||
import org.jeecg.common.constant.CommonConstant;
|
|
||||||
import org.jeecg.common.system.util.JwtUtil;
|
|
||||||
import org.jeecg.common.system.vo.LoginUser;
|
|
||||||
import org.jeecg.common.util.RedisUtil;
|
|
||||||
import org.jeecg.common.util.SpringContextUtils;
|
|
||||||
import org.jeecg.common.util.TokenUtils;
|
|
||||||
import org.jeecg.common.util.oConvertUtils;
|
|
||||||
import org.jeecg.config.mybatis.MybatisPlusSaasConfig;
|
|
||||||
import org.springframework.beans.factory.config.BeanDefinition;
|
|
||||||
import org.springframework.context.annotation.Lazy;
|
|
||||||
import org.springframework.context.annotation.Role;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
import jakarta.annotation.Resource;
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @Description: 用户登录鉴权和获取用户授权
|
|
||||||
* @Author: Scott
|
|
||||||
* @Date: 2019-4-23 8:13
|
|
||||||
* @Version: 1.1
|
|
||||||
*/
|
|
||||||
@Component
|
|
||||||
@Slf4j
|
|
||||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
|
||||||
public class ShiroRealm extends AuthorizingRealm {
|
|
||||||
@Lazy
|
|
||||||
@Resource
|
|
||||||
private CommonAPI commonApi;
|
|
||||||
|
|
||||||
@Lazy
|
|
||||||
@Resource
|
|
||||||
private RedisUtil redisUtil;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 必须重写此方法,不然Shiro会报错
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public boolean supports(AuthenticationToken token) {
|
|
||||||
return token instanceof JwtToken;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 权限信息认证(包括角色以及权限)是用户访问controller的时候才进行验证(redis存储的此处权限信息)
|
|
||||||
* 触发检测用户权限时才会调用此方法,例如checkRole,checkPermission
|
|
||||||
*
|
|
||||||
* @param principals 身份信息
|
|
||||||
* @return AuthorizationInfo 权限信息
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
|
|
||||||
log.debug("===============Shiro权限认证开始============ [ roles、permissions]==========");
|
|
||||||
String username = null;
|
|
||||||
String userId = null;
|
|
||||||
if (principals != null) {
|
|
||||||
LoginUser sysUser = (LoginUser) principals.getPrimaryPrincipal();
|
|
||||||
username = sysUser.getUsername();
|
|
||||||
userId = sysUser.getId();
|
|
||||||
}
|
|
||||||
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
|
|
||||||
|
|
||||||
// 设置用户拥有的角色集合,比如“admin,test”
|
|
||||||
Set<String> roleSet = commonApi.queryUserRolesById(userId);
|
|
||||||
//System.out.println(roleSet.toString());
|
|
||||||
info.setRoles(roleSet);
|
|
||||||
|
|
||||||
// 设置用户拥有的权限集合,比如“sys:role:add,sys:user:add”
|
|
||||||
Set<String> permissionSet = commonApi.queryUserAuths(userId);
|
|
||||||
info.addStringPermissions(permissionSet);
|
|
||||||
//System.out.println(permissionSet);
|
|
||||||
log.info("===============Shiro权限认证成功==============");
|
|
||||||
return info;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 用户信息认证是在用户进行登录的时候进行验证(不存redis)
|
|
||||||
* 也就是说验证用户输入的账号和密码是否正确,错误抛出异常
|
|
||||||
*
|
|
||||||
* @param auth 用户登录的账号密码信息
|
|
||||||
* @return 返回封装了用户信息的 AuthenticationInfo 实例
|
|
||||||
* @throws AuthenticationException
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException {
|
|
||||||
log.debug("===============Shiro身份认证开始============doGetAuthenticationInfo==========");
|
|
||||||
String token = (String) auth.getCredentials();
|
|
||||||
if (token == null) {
|
|
||||||
HttpServletRequest req = SpringContextUtils.getHttpServletRequest();
|
|
||||||
log.info("————————身份认证失败——————————IP地址: "+ oConvertUtils.getIpAddrByRequest(req) +",URL:"+req.getRequestURI());
|
|
||||||
throw new AuthenticationException("token为空!");
|
|
||||||
}
|
|
||||||
// 校验token有效性
|
|
||||||
LoginUser loginUser = null;
|
|
||||||
try {
|
|
||||||
loginUser = this.checkUserTokenIsEffect(token);
|
|
||||||
} catch (AuthenticationException e) {
|
|
||||||
log.error("—————校验 check token 失败——————————"+ e.getMessage(), e);
|
|
||||||
JwtUtil.responseError(SpringContextUtils.getHttpServletResponse(),401,e.getMessage());
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return new SimpleAuthenticationInfo(loginUser, token, getName());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 校验token的有效性
|
|
||||||
*
|
|
||||||
* @param token
|
|
||||||
*/
|
|
||||||
public LoginUser checkUserTokenIsEffect(String token) throws AuthenticationException {
|
|
||||||
// 解密获得username,用于和数据库进行对比
|
|
||||||
String username = JwtUtil.getUsername(token);
|
|
||||||
if (username == null) {
|
|
||||||
throw new AuthenticationException("Token非法无效!");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 查询用户信息
|
|
||||||
log.debug("———校验token是否有效————checkUserTokenIsEffect——————— "+ token);
|
|
||||||
LoginUser loginUser = TokenUtils.getLoginUser(username, commonApi, redisUtil);
|
|
||||||
//LoginUser loginUser = commonApi.getUserByName(username);
|
|
||||||
if (loginUser == null) {
|
|
||||||
throw new AuthenticationException("用户不存在!");
|
|
||||||
}
|
|
||||||
// 判断用户状态
|
|
||||||
if (loginUser.getStatus() != 1) {
|
|
||||||
throw new AuthenticationException("账号已被锁定,请联系管理员!");
|
|
||||||
}
|
|
||||||
// 校验token是否超时失效 & 或者账号密码是否错误
|
|
||||||
if (!jwtTokenRefresh(token, username, loginUser.getPassword())) {
|
|
||||||
throw new AuthenticationException(CommonConstant.TOKEN_IS_INVALID_MSG);
|
|
||||||
}
|
|
||||||
//update-begin-author:taoyan date:20210609 for:校验用户的tenant_id和前端传过来的是否一致
|
|
||||||
String userTenantIds = loginUser.getRelTenantIds();
|
|
||||||
if(MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL && oConvertUtils.isNotEmpty(userTenantIds)){
|
|
||||||
String contextTenantId = TenantContext.getTenant();
|
|
||||||
log.debug("登录租户:" + contextTenantId);
|
|
||||||
log.debug("用户拥有那些租户:" + userTenantIds);
|
|
||||||
//登录用户无租户,前端header中租户ID值为 0
|
|
||||||
String str ="0";
|
|
||||||
if(oConvertUtils.isNotEmpty(contextTenantId) && !str.equals(contextTenantId)){
|
|
||||||
//update-begin-author:taoyan date:20211227 for: /issues/I4O14W 用户租户信息变更判断漏洞
|
|
||||||
String[] arr = userTenantIds.split(",");
|
|
||||||
if(!oConvertUtils.isIn(contextTenantId, arr)){
|
|
||||||
boolean isAuthorization = false;
|
|
||||||
//========================================================================
|
|
||||||
// 查询用户信息(如果租户不匹配从数据库中重新查询一次用户信息)
|
|
||||||
String loginUserKey = CacheConstant.SYS_USERS_CACHE + "::" + username;
|
|
||||||
redisUtil.del(loginUserKey);
|
|
||||||
LoginUser loginUserFromDb = commonApi.getUserByName(username);
|
|
||||||
if (oConvertUtils.isNotEmpty(loginUserFromDb.getRelTenantIds())) {
|
|
||||||
String[] newArray = loginUserFromDb.getRelTenantIds().split(",");
|
|
||||||
if (oConvertUtils.isIn(contextTenantId, newArray)) {
|
|
||||||
isAuthorization = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//========================================================================
|
|
||||||
|
|
||||||
//*********************************************
|
|
||||||
if(!isAuthorization){
|
|
||||||
log.info("租户异常——登录租户:" + contextTenantId);
|
|
||||||
log.info("租户异常——用户拥有租户组:" + userTenantIds);
|
|
||||||
throw new AuthenticationException("登录租户授权变更,请重新登陆!");
|
|
||||||
}
|
|
||||||
//*********************************************
|
|
||||||
}
|
|
||||||
//update-end-author:taoyan date:20211227 for: /issues/I4O14W 用户租户信息变更判断漏洞
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//update-end-author:taoyan date:20210609 for:校验用户的tenant_id和前端传过来的是否一致
|
|
||||||
return loginUser;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* JWTToken刷新生命周期 (实现: 用户在线操作不掉线功能)
|
|
||||||
* 1、登录成功后将用户的JWT生成的Token作为k、v存储到cache缓存里面(这时候k、v值一样),缓存有效期设置为Jwt有效时间的2倍
|
|
||||||
* 2、当该用户再次请求时,通过JWTFilter层层校验之后会进入到doGetAuthenticationInfo进行身份验证
|
|
||||||
* 3、当该用户这次请求jwt生成的token值已经超时,但该token对应cache中的k还是存在,则表示该用户一直在操作只是JWT的token失效了,程序会给token对应的k映射的v值重新生成JWTToken并覆盖v值,该缓存生命周期重新计算
|
|
||||||
* 4、当该用户这次请求jwt在生成的token值已经超时,并在cache中不存在对应的k,则表示该用户账户空闲超时,返回用户信息已失效,请重新登录。
|
|
||||||
* 注意: 前端请求Header中设置Authorization保持不变,校验有效性以缓存中的token为准。
|
|
||||||
* 用户过期时间 = Jwt有效时间 * 2。
|
|
||||||
*
|
|
||||||
* @param userName
|
|
||||||
* @param passWord
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
public boolean jwtTokenRefresh(String token, String userName, String passWord) {
|
|
||||||
String cacheToken = String.valueOf(redisUtil.get(CommonConstant.PREFIX_USER_TOKEN + token));
|
|
||||||
if (oConvertUtils.isNotEmpty(cacheToken)) {
|
|
||||||
// 校验token有效性
|
|
||||||
if (!JwtUtil.verify(cacheToken, userName, passWord)) {
|
|
||||||
String newAuthorization = JwtUtil.sign(userName, passWord);
|
|
||||||
// 设置超时时间
|
|
||||||
redisUtil.set(CommonConstant.PREFIX_USER_TOKEN + token, newAuthorization);
|
|
||||||
redisUtil.expire(CommonConstant.PREFIX_USER_TOKEN + token, JwtUtil.EXPIRE_TIME *2 / 1000);
|
|
||||||
log.debug("——————————用户在线操作,更新token保证不掉线—————————jwtTokenRefresh——————— "+ token);
|
|
||||||
}
|
|
||||||
//update-begin--Author:scott Date:20191005 for:解决每次请求,都重写redis中 token缓存问题
|
|
||||||
// else {
|
|
||||||
// // 设置超时时间
|
|
||||||
// redisUtil.set(CommonConstant.PREFIX_USER_TOKEN + token, cacheToken);
|
|
||||||
// redisUtil.expire(CommonConstant.PREFIX_USER_TOKEN + token, JwtUtil.EXPIRE_TIME / 1000);
|
|
||||||
// }
|
|
||||||
//update-end--Author:scott Date:20191005 for:解决每次请求,都重写redis中 token缓存问题
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
//redis中不存在此TOEKN,说明token非法返回false
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 清除当前用户的权限认证缓存
|
|
||||||
*
|
|
||||||
* @param principals 权限信息
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void clearCache(PrincipalCollection principals) {
|
|
||||||
super.clearCache(principals);
|
|
||||||
//update-begin---author:scott ---date::2024-06-18 for:【TV360X-1320】分配权限必须退出重新登录才生效,造成很多用户困扰---
|
|
||||||
super.clearCachedAuthorizationInfo(principals);
|
|
||||||
//update-end---author:scott ---date::2024-06-18 for:【TV360X-1320】分配权限必须退出重新登录才生效,造成很多用户困扰---
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,77 +0,0 @@
|
|||||||
package org.jeecg.config.shiro.filters;
|
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
|
|
||||||
import org.apache.shiro.web.filter.InvalidRequestFilter;
|
|
||||||
import org.apache.shiro.web.filter.mgt.DefaultFilter;
|
|
||||||
import org.apache.shiro.web.filter.mgt.FilterChainManager;
|
|
||||||
import org.apache.shiro.web.filter.mgt.FilterChainResolver;
|
|
||||||
import org.apache.shiro.web.filter.mgt.PathMatchingFilterChainResolver;
|
|
||||||
import org.apache.shiro.web.mgt.WebSecurityManager;
|
|
||||||
import org.apache.shiro.web.servlet.AbstractShiroFilter;
|
|
||||||
import org.apache.shiro.mgt.SecurityManager;
|
|
||||||
import org.springframework.beans.factory.BeanInitializationException;
|
|
||||||
|
|
||||||
import jakarta.servlet.Filter;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 自定义ShiroFilterFactoryBean解决资源中文路径问题
|
|
||||||
* @author: jeecg-boot
|
|
||||||
*/
|
|
||||||
@Slf4j
|
|
||||||
public class CustomShiroFilterFactoryBean extends ShiroFilterFactoryBean {
|
|
||||||
@Override
|
|
||||||
public Class getObjectType() {
|
|
||||||
return MySpringShiroFilter.class;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected AbstractShiroFilter createInstance() throws Exception {
|
|
||||||
|
|
||||||
SecurityManager securityManager = getSecurityManager();
|
|
||||||
if (securityManager == null) {
|
|
||||||
String msg = "SecurityManager property must be set.";
|
|
||||||
throw new BeanInitializationException(msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!(securityManager instanceof WebSecurityManager)) {
|
|
||||||
String msg = "The security manager does not implement the WebSecurityManager interface.";
|
|
||||||
throw new BeanInitializationException(msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
FilterChainManager manager = createFilterChainManager();
|
|
||||||
//Expose the constructed FilterChainManager by first wrapping it in a
|
|
||||||
// FilterChainResolver implementation. The AbstractShiroFilter implementations
|
|
||||||
// do not know about FilterChainManagers - only resolvers:
|
|
||||||
PathMatchingFilterChainResolver chainResolver = new PathMatchingFilterChainResolver();
|
|
||||||
chainResolver.setFilterChainManager(manager);
|
|
||||||
|
|
||||||
Map<String, Filter> filterMap = manager.getFilters();
|
|
||||||
Filter invalidRequestFilter = filterMap.get(DefaultFilter.invalidRequest.name());
|
|
||||||
if (invalidRequestFilter instanceof InvalidRequestFilter) {
|
|
||||||
//此处是关键,设置false跳过URL携带中文400,servletPath中文校验bug
|
|
||||||
((InvalidRequestFilter) invalidRequestFilter).setBlockNonAscii(false);
|
|
||||||
}
|
|
||||||
//Now create a concrete ShiroFilter instance and apply the acquired SecurityManager and built
|
|
||||||
//FilterChainResolver. It doesn't matter that the instance is an anonymous inner class
|
|
||||||
//here - we're just using it because it is a concrete AbstractShiroFilter instance that accepts
|
|
||||||
//injection of the SecurityManager and FilterChainResolver:
|
|
||||||
return new MySpringShiroFilter((WebSecurityManager) securityManager, chainResolver);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final class MySpringShiroFilter extends AbstractShiroFilter {
|
|
||||||
protected MySpringShiroFilter(WebSecurityManager webSecurityManager, FilterChainResolver resolver) {
|
|
||||||
if (webSecurityManager == null) {
|
|
||||||
throw new IllegalArgumentException("WebSecurityManager property cannot be null.");
|
|
||||||
} else {
|
|
||||||
this.setSecurityManager(webSecurityManager);
|
|
||||||
if (resolver != null) {
|
|
||||||
this.setFilterChainResolver(resolver);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,130 +0,0 @@
|
|||||||
package org.jeecg.config.shiro.filters;
|
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.apache.commons.lang.StringUtils;
|
|
||||||
import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter;
|
|
||||||
import org.jeecg.common.config.TenantContext;
|
|
||||||
import org.jeecg.common.constant.CommonConstant;
|
|
||||||
import org.jeecg.common.system.util.JwtUtil;
|
|
||||||
import org.jeecg.common.util.oConvertUtils;
|
|
||||||
import org.jeecg.config.shiro.JwtToken;
|
|
||||||
import org.jeecg.config.shiro.ignore.InMemoryIgnoreAuth;
|
|
||||||
import org.springframework.http.HttpHeaders;
|
|
||||||
import org.springframework.http.HttpStatus;
|
|
||||||
import org.springframework.web.bind.annotation.RequestMethod;
|
|
||||||
|
|
||||||
import jakarta.servlet.ServletRequest;
|
|
||||||
import jakarta.servlet.ServletResponse;
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @Description: 鉴权登录拦截器
|
|
||||||
* @Author: Scott
|
|
||||||
* @Date: 2018/10/7
|
|
||||||
**/
|
|
||||||
@Slf4j
|
|
||||||
public class JwtFilter extends BasicHttpAuthenticationFilter {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 默认开启跨域设置(使用单体)
|
|
||||||
* 微服务情况下,此属性设置为false
|
|
||||||
*/
|
|
||||||
private boolean allowOrigin = true;
|
|
||||||
|
|
||||||
public JwtFilter(){}
|
|
||||||
public JwtFilter(boolean allowOrigin){
|
|
||||||
this.allowOrigin = allowOrigin;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 执行登录认证
|
|
||||||
*
|
|
||||||
* @param request
|
|
||||||
* @param response
|
|
||||||
* @param mappedValue
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
|
|
||||||
try {
|
|
||||||
// 判断当前路径是不是注解了@IngoreAuth路径,如果是,则放开验证
|
|
||||||
if (InMemoryIgnoreAuth.contains(((HttpServletRequest) request).getServletPath())) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
executeLogin(request, response);
|
|
||||||
return true;
|
|
||||||
} catch (Exception e) {
|
|
||||||
JwtUtil.responseError((HttpServletResponse)response,401,CommonConstant.TOKEN_IS_INVALID_MSG);
|
|
||||||
return false;
|
|
||||||
//throw new AuthenticationException("Token失效,请重新登录", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
|
|
||||||
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
|
|
||||||
String token = httpServletRequest.getHeader(CommonConstant.X_ACCESS_TOKEN);
|
|
||||||
// update-begin--Author:lvdandan Date:20210105 for:JT-355 OA聊天添加token验证,获取token参数
|
|
||||||
if (oConvertUtils.isEmpty(token)) {
|
|
||||||
token = httpServletRequest.getParameter("token");
|
|
||||||
}
|
|
||||||
// update-end--Author:lvdandan Date:20210105 for:JT-355 OA聊天添加token验证,获取token参数
|
|
||||||
|
|
||||||
JwtToken jwtToken = new JwtToken(token);
|
|
||||||
// 提交给realm进行登入,如果错误他会抛出异常并被捕获
|
|
||||||
getSubject(request, response).login(jwtToken);
|
|
||||||
// 如果没有抛出异常则代表登入成功,返回true
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 对跨域提供支持
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
|
|
||||||
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
|
|
||||||
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
|
|
||||||
if(allowOrigin){
|
|
||||||
httpServletResponse.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, httpServletRequest.getHeader(HttpHeaders.ORIGIN));
|
|
||||||
// 允许客户端请求方法
|
|
||||||
httpServletResponse.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS, "GET,POST,OPTIONS,PUT,DELETE");
|
|
||||||
// 允许客户端提交的Header
|
|
||||||
String requestHeaders = httpServletRequest.getHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS);
|
|
||||||
if (StringUtils.isNotEmpty(requestHeaders)) {
|
|
||||||
httpServletResponse.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS, requestHeaders);
|
|
||||||
}
|
|
||||||
// 允许客户端携带凭证信息(是否允许发送Cookie)
|
|
||||||
httpServletResponse.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true");
|
|
||||||
}
|
|
||||||
// 跨域时会首先发送一个option请求,这里我们给option请求直接返回正常状态
|
|
||||||
if (RequestMethod.OPTIONS.name().equalsIgnoreCase(httpServletRequest.getMethod())) {
|
|
||||||
httpServletResponse.setStatus(HttpStatus.OK.value());
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
//update-begin-author:taoyan date:20200708 for:多租户用到
|
|
||||||
String tenantId = httpServletRequest.getHeader(CommonConstant.TENANT_ID);
|
|
||||||
TenantContext.setTenant(tenantId);
|
|
||||||
//update-end-author:taoyan date:20200708 for:多租户用到
|
|
||||||
|
|
||||||
return super.preHandle(request, response);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* JwtFilter中ThreadLocal需要及时清除 #3634
|
|
||||||
*
|
|
||||||
* @param request
|
|
||||||
* @param response
|
|
||||||
* @param exception
|
|
||||||
* @throws Exception
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void afterCompletion(ServletRequest request, ServletResponse response, Exception exception) throws Exception {
|
|
||||||
//log.info("------清空线程中多租户的ID={}------",TenantContext.getTenant());
|
|
||||||
TenantContext.clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,67 +0,0 @@
|
|||||||
package org.jeecg.config.shiro.filters;
|
|
||||||
|
|
||||||
import jakarta.servlet.ServletRequest;
|
|
||||||
import jakarta.servlet.ServletResponse;
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
|
||||||
import org.apache.shiro.subject.Subject;
|
|
||||||
import org.apache.shiro.web.filter.AccessControlFilter;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @Author Scott
|
|
||||||
* @create 2019-02-01 15:56
|
|
||||||
* @desc 鉴权请求URL访问权限拦截器
|
|
||||||
*/
|
|
||||||
@Slf4j
|
|
||||||
public class ResourceCheckFilter extends AccessControlFilter {
|
|
||||||
|
|
||||||
private String errorUrl;
|
|
||||||
|
|
||||||
public String getErrorUrl() {
|
|
||||||
return errorUrl;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setErrorUrl(String errorUrl) {
|
|
||||||
this.errorUrl = errorUrl;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 表示是否允许访问 ,如果允许访问返回true,否则false;
|
|
||||||
*
|
|
||||||
* @param servletRequest
|
|
||||||
* @param servletResponse
|
|
||||||
* @param o 表示写在拦截器中括号里面的字符串 mappedValue 就是 [urls] 配置中拦截器参数部分
|
|
||||||
* @return
|
|
||||||
* @throws Exception
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
protected boolean isAccessAllowed(ServletRequest servletRequest, ServletResponse servletResponse, Object o) throws Exception {
|
|
||||||
Subject subject = getSubject(servletRequest, servletResponse);
|
|
||||||
String url = getPathWithinApplication(servletRequest);
|
|
||||||
log.info("当前用户正在访问的 url => " + url);
|
|
||||||
return subject.isPermitted(url);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* onAccessDenied:表示当访问拒绝时是否已经处理了; 如果返回 true 表示需要继续处理; 如果返回 false
|
|
||||||
* 表示该拦截器实例已经处理了,将直接返回即可。
|
|
||||||
*
|
|
||||||
* @param servletRequest
|
|
||||||
* @param servletResponse
|
|
||||||
* @return
|
|
||||||
* @throws Exception
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
|
|
||||||
log.info("当 isAccessAllowed 返回 false 的时候,才会执行 method onAccessDenied ");
|
|
||||||
|
|
||||||
HttpServletRequest request = (HttpServletRequest) servletRequest;
|
|
||||||
HttpServletResponse response = (HttpServletResponse) servletResponse;
|
|
||||||
response.sendRedirect(request.getContextPath() + this.errorUrl);
|
|
||||||
|
|
||||||
// 返回 false 表示已经处理,例如页面跳转啥的,表示不在走以下的拦截器了(如果还有配置的话)
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,112 +0,0 @@
|
|||||||
package org.jeecg.config.shiro.ignore;
|
|
||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.jeecg.config.shiro.IgnoreAuth;
|
|
||||||
import org.springframework.beans.factory.InitializingBean;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
import org.springframework.util.CollectionUtils;
|
|
||||||
import org.springframework.web.bind.annotation.*;
|
|
||||||
import org.springframework.web.method.HandlerMethod;
|
|
||||||
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
|
|
||||||
|
|
||||||
import java.lang.reflect.Method;
|
|
||||||
import java.util.*;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 在spring boot初始化时,根据@RestController注解获取当前spring容器中的bean
|
|
||||||
* @author eightmonth
|
|
||||||
* @date 2024/4/18 11:35
|
|
||||||
*/
|
|
||||||
@Slf4j
|
|
||||||
@Component
|
|
||||||
@AllArgsConstructor
|
|
||||||
public class IgnoreAuthPostProcessor implements InitializingBean {
|
|
||||||
|
|
||||||
private RequestMappingHandlerMapping requestMappingHandlerMapping;
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void afterPropertiesSet() throws Exception {
|
|
||||||
|
|
||||||
long startTime = System.currentTimeMillis();
|
|
||||||
|
|
||||||
List<String> ignoreAuthUrls = new ArrayList<>();
|
|
||||||
Set<Class<?>> restControllers = requestMappingHandlerMapping.getHandlerMethods().values().stream().map(HandlerMethod::getBeanType).collect(Collectors.toSet());
|
|
||||||
for (Class<?> restController : restControllers) {
|
|
||||||
ignoreAuthUrls.addAll(postProcessRestController(restController));
|
|
||||||
}
|
|
||||||
|
|
||||||
log.info("Init Token ignoreAuthUrls Config [ 集合 ] :{}", ignoreAuthUrls);
|
|
||||||
if (!CollectionUtils.isEmpty(ignoreAuthUrls)) {
|
|
||||||
InMemoryIgnoreAuth.set(ignoreAuthUrls);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 计算方法的耗时
|
|
||||||
long endTime = System.currentTimeMillis();
|
|
||||||
long elapsedTime = endTime - startTime;
|
|
||||||
log.info("Init Token ignoreAuthUrls Config [ 耗时 ] :" + elapsedTime + "毫秒");
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<String> postProcessRestController(Class<?> clazz) {
|
|
||||||
List<String> ignoreAuthUrls = new ArrayList<>();
|
|
||||||
RequestMapping base = clazz.getAnnotation(RequestMapping.class);
|
|
||||||
String[] baseUrl = Objects.nonNull(base) ? base.value() : new String[]{};
|
|
||||||
Method[] methods = clazz.getDeclaredMethods();
|
|
||||||
|
|
||||||
for (Method method : methods) {
|
|
||||||
if (method.isAnnotationPresent(IgnoreAuth.class) && method.isAnnotationPresent(RequestMapping.class)) {
|
|
||||||
RequestMapping requestMapping = method.getAnnotation(RequestMapping.class);
|
|
||||||
String[] uri = requestMapping.value();
|
|
||||||
ignoreAuthUrls.addAll(rebuildUrl(baseUrl, uri));
|
|
||||||
} else if (method.isAnnotationPresent(IgnoreAuth.class) && method.isAnnotationPresent(GetMapping.class)) {
|
|
||||||
GetMapping requestMapping = method.getAnnotation(GetMapping.class);
|
|
||||||
String[] uri = requestMapping.value();
|
|
||||||
ignoreAuthUrls.addAll(rebuildUrl(baseUrl, uri));
|
|
||||||
} else if (method.isAnnotationPresent(IgnoreAuth.class) && method.isAnnotationPresent(PostMapping.class)) {
|
|
||||||
PostMapping requestMapping = method.getAnnotation(PostMapping.class);
|
|
||||||
String[] uri = requestMapping.value();
|
|
||||||
ignoreAuthUrls.addAll(rebuildUrl(baseUrl, uri));
|
|
||||||
} else if (method.isAnnotationPresent(IgnoreAuth.class) && method.isAnnotationPresent(PutMapping.class)) {
|
|
||||||
PutMapping requestMapping = method.getAnnotation(PutMapping.class);
|
|
||||||
String[] uri = requestMapping.value();
|
|
||||||
ignoreAuthUrls.addAll(rebuildUrl(baseUrl, uri));
|
|
||||||
} else if (method.isAnnotationPresent(IgnoreAuth.class) && method.isAnnotationPresent(DeleteMapping.class)) {
|
|
||||||
DeleteMapping requestMapping = method.getAnnotation(DeleteMapping.class);
|
|
||||||
String[] uri = requestMapping.value();
|
|
||||||
ignoreAuthUrls.addAll(rebuildUrl(baseUrl, uri));
|
|
||||||
} else if (method.isAnnotationPresent(IgnoreAuth.class) && method.isAnnotationPresent(PatchMapping.class)) {
|
|
||||||
PatchMapping requestMapping = method.getAnnotation(PatchMapping.class);
|
|
||||||
String[] uri = requestMapping.value();
|
|
||||||
ignoreAuthUrls.addAll(rebuildUrl(baseUrl, uri));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ignoreAuthUrls;
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<String> rebuildUrl(String[] bases, String[] uris) {
|
|
||||||
List<String> urls = new ArrayList<>();
|
|
||||||
if (bases.length > 0) {
|
|
||||||
for (String base : bases) {
|
|
||||||
for (String uri : uris) {
|
|
||||||
// 如果uri包含路径占位符, 则需要将其替换为*
|
|
||||||
if (uri.matches(".*\\{.*}.*")) {
|
|
||||||
uri = uri.replaceAll("\\{.*?}", "*");
|
|
||||||
}
|
|
||||||
urls.add(prefix(base) + prefix(uri));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Arrays.stream(uris).forEach(uri -> {
|
|
||||||
urls.add(prefix(uri));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return urls;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String prefix(String seg) {
|
|
||||||
return seg.startsWith("/") ? seg : "/"+seg;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -41,7 +41,7 @@ public class SignAuthConfiguration implements WebMvcConfigurer {
|
|||||||
registry.addInterceptor(signAuthInterceptor()).addPathPatterns(signUrlsArray);
|
registry.addInterceptor(signAuthInterceptor()).addPathPatterns(signUrlsArray);
|
||||||
}
|
}
|
||||||
|
|
||||||
//update-begin-author:taoyan date:20220427 for: issues/I53J5E post请求X_SIGN签名拦截校验后报错, request body 为空
|
// 代码逻辑说明: issues/I53J5E post请求X_SIGN签名拦截校验后报错, request body 为空
|
||||||
@Bean
|
@Bean
|
||||||
public RequestBodyReserveFilter requestBodyReserveFilter(){
|
public RequestBodyReserveFilter requestBodyReserveFilter(){
|
||||||
return new RequestBodyReserveFilter();
|
return new RequestBodyReserveFilter();
|
||||||
@ -66,6 +66,5 @@ public class SignAuthConfiguration implements WebMvcConfigurer {
|
|||||||
registration.addUrlPatterns(signUrlsArray);
|
registration.addUrlPatterns(signUrlsArray);
|
||||||
return registration;
|
return registration;
|
||||||
}
|
}
|
||||||
//update-end-author:taoyan date:20220427 for: issues/I53J5E post请求X_SIGN签名拦截校验后报错, request body 为空
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -33,7 +33,7 @@ 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("Sign Interceptor request URI = " + request.getRequestURI());
|
log.debug("Sign Interceptor request URI = " + request.getRequestURI());
|
||||||
HttpServletRequest requestWrapper = new BodyReaderHttpServletRequestWrapper(request);
|
HttpServletRequest requestWrapper = new BodyReaderHttpServletRequestWrapper(request);
|
||||||
//获取全部参数(包括URL和body上的)
|
//获取全部参数(包括URL和body上的)
|
||||||
SortedMap<String, String> allParams = HttpUtils.getAllParams(requestWrapper);
|
SortedMap<String, String> allParams = HttpUtils.getAllParams(requestWrapper);
|
||||||
@ -79,7 +79,7 @@ public class SignAuthInterceptor implements HandlerInterceptor {
|
|||||||
log.debug("Sign 签名通过!Header Sign : {}",headerSign);
|
log.debug("Sign 签名通过!Header Sign : {}",headerSign);
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
log.info("sign allParams: {}", allParams);
|
log.debug("sign allParams: {}", allParams);
|
||||||
log.error("request URI = " + request.getRequestURI());
|
log.error("request URI = " + request.getRequestURI());
|
||||||
log.error("Sign 签名校验失败!Header Sign : {}",headerSign);
|
log.error("Sign 签名校验失败!Header Sign : {}",headerSign);
|
||||||
//校验失败返回前端
|
//校验失败返回前端
|
||||||
|
|||||||
@ -41,19 +41,19 @@ public class HttpUtils {
|
|||||||
// 获取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.info(" pathVariable: {}",pathVariable);
|
log.debug(" 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.info("存在%情况下,执行两次解码 — pathVariable decode: {}",deString);
|
log.debug("存在%情况下,执行两次解码 — pathVariable decode: {}",deString);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
//e.printStackTrace();
|
//e.printStackTrace();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
log.info(" pathVariable decode: {}",deString);
|
log.debug(" pathVariable decode: {}",deString);
|
||||||
result.put(SignUtil.X_PATH_VARIABLE, deString);
|
result.put(SignUtil.X_PATH_VARIABLE, deString);
|
||||||
}
|
}
|
||||||
// 获取URL上的参数
|
// 获取URL上的参数
|
||||||
@ -91,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.info(" pathVariable: {}",pathVariable);
|
log.debug(" 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.info("存在%情况下,执行两次解码 — pathVariable decode: {}",deString);
|
log.debug("存在%情况下,执行两次解码 — pathVariable decode: {}",deString);
|
||||||
}
|
}
|
||||||
log.info(" pathVariable decode: {}",deString);
|
log.debug(" pathVariable decode: {}",deString);
|
||||||
result.put(SignUtil.X_PATH_VARIABLE, deString);
|
result.put(SignUtil.X_PATH_VARIABLE, deString);
|
||||||
}
|
}
|
||||||
// 获取URL上的参数
|
// 获取URL上的参数
|
||||||
@ -174,11 +174,10 @@ 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("=");
|
||||||
//update-begin---author:chenrui ---date:20240222 for:[issues/5879]数据查询传ds=“”造成的异常------------
|
// 代码逻辑说明: [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;
|
||||||
}
|
}
|
||||||
@ -202,11 +201,10 @@ 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("=");
|
||||||
//update-begin---author:chenrui ---date:20240222 for:[issues/5879]数据查询传ds=“”造成的异常------------
|
// 代码逻辑说明: [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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -34,7 +34,7 @@ public class SignUtil {
|
|||||||
}
|
}
|
||||||
// 把参数加密
|
// 把参数加密
|
||||||
String paramsSign = getParamsSign(params);
|
String paramsSign = getParamsSign(params);
|
||||||
log.info("Param Sign : {}", paramsSign);
|
log.debug("Param Sign : {}", paramsSign);
|
||||||
return !StringUtils.isEmpty(paramsSign) && headerSign.equals(paramsSign);
|
return !StringUtils.isEmpty(paramsSign) && headerSign.equals(paramsSign);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -47,7 +47,7 @@ public class SignUtil {
|
|||||||
//去掉 Url 里的时间戳
|
//去掉 Url 里的时间戳
|
||||||
params.remove("_t");
|
params.remove("_t");
|
||||||
String paramsJsonStr = JSONObject.toJSONString(params);
|
String paramsJsonStr = JSONObject.toJSONString(params);
|
||||||
log.info("Param paramsJsonStr : {}", paramsJsonStr);
|
log.debug("Param paramsJsonStr : {}", paramsJsonStr);
|
||||||
//设置签名秘钥
|
//设置签名秘钥
|
||||||
JeecgBaseConfig jeecgBaseConfig = SpringContextUtils.getBean(JeecgBaseConfig.class);
|
JeecgBaseConfig jeecgBaseConfig = SpringContextUtils.getBean(JeecgBaseConfig.class);
|
||||||
String signatureSecret = jeecgBaseConfig.getSignatureSecret();
|
String signatureSecret = jeecgBaseConfig.getSignatureSecret();
|
||||||
|
|||||||
@ -0,0 +1,38 @@
|
|||||||
|
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,11 +19,38 @@ public class Firewall {
|
|||||||
* 低代码模式(dev:开发模式,prod:发布模式——关闭所有在线开发配置能力)
|
* 低代码模式(dev:开发模式,prod:发布模式——关闭所有在线开发配置能力)
|
||||||
*/
|
*/
|
||||||
private String lowCodeMode;
|
private String lowCodeMode;
|
||||||
|
/**
|
||||||
|
* 是否开启默认密码登录提醒(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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,11 @@
|
|||||||
|
package org.jeecg.config.vo;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class JeecgMinio {
|
||||||
|
|
||||||
|
private String minio_url;
|
||||||
|
private String bucketName;
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
package org.jeecg.config.vo;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class JeecgOSS {
|
||||||
|
|
||||||
|
private String endpoint;
|
||||||
|
private String bucketName;
|
||||||
|
|
||||||
|
}
|
||||||
@ -2,7 +2,7 @@ package org.jeecg.modules.base.service.impl;
|
|||||||
|
|
||||||
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
|
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.shiro.SecurityUtils;
|
import org.jeecg.common.util.LoginUserUtils;
|
||||||
import org.jeecg.common.api.dto.LogDTO;
|
import org.jeecg.common.api.dto.LogDTO;
|
||||||
import org.jeecg.common.constant.enums.ClientTerminalTypeEnum;
|
import org.jeecg.common.constant.enums.ClientTerminalTypeEnum;
|
||||||
import org.jeecg.common.util.BrowserUtils;
|
import org.jeecg.common.util.BrowserUtils;
|
||||||
@ -74,7 +74,7 @@ public class BaseCommonServiceImpl implements BaseCommonService {
|
|||||||
//获取登录用户信息
|
//获取登录用户信息
|
||||||
if(user==null){
|
if(user==null){
|
||||||
try {
|
try {
|
||||||
user = (LoginUser) SecurityUtils.getSubject().getPrincipal();
|
user = LoginUserUtils.getSessionUser();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
//e.printStackTrace();
|
//e.printStackTrace();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1 @@
|
|||||||
|
org.jeecg.config.DruidWallConfigRegister
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 0 B After Width: | Height: | Size: 487 B |
@ -6,7 +6,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>org.jeecgframework.boot3</groupId>
|
<groupId>org.jeecgframework.boot3</groupId>
|
||||||
<artifactId>jeecg-boot-module</artifactId>
|
<artifactId>jeecg-boot-module</artifactId>
|
||||||
<version>3.8.3</version>
|
<version>3.9.0</version>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
<artifactId>jeecg-boot-module-airag</artifactId>
|
<artifactId>jeecg-boot-module-airag</artifactId>
|
||||||
@ -31,10 +31,30 @@
|
|||||||
</repositories>
|
</repositories>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
<langchain4j.version>0.35.0</langchain4j.version>
|
<kotlin.version>2.2.0</kotlin.version>
|
||||||
|
<liteflow.version>2.15.0</liteflow.version>
|
||||||
<apache-tika.version>2.9.1</apache-tika.version>
|
<apache-tika.version>2.9.1</apache-tika.version>
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
|
<dependencyManagement>
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>dev.langchain4j</groupId>
|
||||||
|
<artifactId>langchain4j-bom</artifactId>
|
||||||
|
<version>1.3.0</version>
|
||||||
|
<type>pom</type>
|
||||||
|
<scope>import</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>dev.langchain4j</groupId>
|
||||||
|
<artifactId>langchain4j-community-bom</artifactId>
|
||||||
|
<version>1.3.0-beta9</version>
|
||||||
|
<type>pom</type>
|
||||||
|
<scope>import</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
</dependencyManagement>
|
||||||
|
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<!-- system单体 api-->
|
<!-- system单体 api-->
|
||||||
<dependency>
|
<dependency>
|
||||||
@ -55,10 +75,24 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.jeecgframework.boot3</groupId>
|
<groupId>org.jeecgframework.boot3</groupId>
|
||||||
<artifactId>jeecg-aiflow</artifactId>
|
<artifactId>jeecg-aiflow</artifactId>
|
||||||
<version>1.2.0</version>
|
<version>3.9.0.1</version>
|
||||||
|
<exclusions>
|
||||||
|
<exclusion>
|
||||||
|
<groupId>commons-io</groupId>
|
||||||
|
<artifactId>commons-io</artifactId>
|
||||||
|
</exclusion>
|
||||||
|
<exclusion>
|
||||||
|
<groupId>commons-beanutils</groupId>
|
||||||
|
<artifactId>commons-beanutils</artifactId>
|
||||||
|
</exclusion>
|
||||||
|
<exclusion>
|
||||||
|
<groupId>com.yomahub</groupId>
|
||||||
|
<artifactId>liteflow-script-python</artifactId>
|
||||||
|
</exclusion>
|
||||||
|
</exclusions>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- beigin 这两个依赖太多每个包50M左右,如果你发布需要使用,请把<scope>provided</scope>删掉 -->
|
<!-- begin 注意:这几个依赖体积较大,每个约50MB。若发布时需要使用,请将 <scope>provided</scope> 删除 -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.jetbrains.kotlin</groupId>
|
<groupId>org.jetbrains.kotlin</groupId>
|
||||||
<artifactId>kotlin-scripting-jsr223</artifactId>
|
<artifactId>kotlin-scripting-jsr223</artifactId>
|
||||||
@ -71,7 +105,13 @@
|
|||||||
<version>${liteflow.version}</version>
|
<version>${liteflow.version}</version>
|
||||||
<scope>provided</scope>
|
<scope>provided</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
<!-- end 这两个依赖太多每个包50M左右,如果你发布需要使用,请把<scope>provided</scope>删掉 -->
|
<dependency>
|
||||||
|
<groupId>com.yomahub</groupId>
|
||||||
|
<artifactId>liteflow-script-python</artifactId>
|
||||||
|
<version>${liteflow.version}</version>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
<!-- end 注意:这几个依赖体积较大,每个约50MB。若发布时需要使用,请将 <scope>provided</scope> 删除 -->
|
||||||
|
|
||||||
<!-- aiflow 脚本依赖 -->
|
<!-- aiflow 脚本依赖 -->
|
||||||
<dependency>
|
<dependency>
|
||||||
@ -109,13 +149,15 @@
|
|||||||
<!-- langChain4j model support -->
|
<!-- langChain4j model support -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>dev.langchain4j</groupId>
|
<groupId>dev.langchain4j</groupId>
|
||||||
<artifactId>langchain4j-ollama</artifactId>
|
<artifactId>langchain4j-open-ai</artifactId>
|
||||||
<version>${langchain4j.version}</version>
|
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>dev.langchain4j</groupId>
|
<groupId>dev.langchain4j</groupId>
|
||||||
<artifactId>langchain4j-zhipu-ai</artifactId>
|
<artifactId>langchain4j-ollama</artifactId>
|
||||||
<version>${langchain4j.version}</version>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>dev.langchain4j</groupId>
|
||||||
|
<artifactId>langchain4j-community-zhipu-ai</artifactId>
|
||||||
<exclusions>
|
<exclusions>
|
||||||
<exclusion>
|
<exclusion>
|
||||||
<artifactId>checker-qual</artifactId>
|
<artifactId>checker-qual</artifactId>
|
||||||
@ -129,13 +171,11 @@
|
|||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>dev.langchain4j</groupId>
|
<groupId>dev.langchain4j</groupId>
|
||||||
<artifactId>langchain4j-qianfan</artifactId>
|
<artifactId>langchain4j-community-qianfan</artifactId>
|
||||||
<version>${langchain4j.version}</version>
|
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>dev.langchain4j</groupId>
|
<groupId>dev.langchain4j</groupId>
|
||||||
<artifactId>langchain4j-dashscope</artifactId>
|
<artifactId>langchain4j-community-dashscope</artifactId>
|
||||||
<version>${langchain4j.version}</version>
|
|
||||||
<exclusions>
|
<exclusions>
|
||||||
<exclusion>
|
<exclusion>
|
||||||
<groupId>org.slf4j</groupId>
|
<groupId>org.slf4j</groupId>
|
||||||
@ -147,11 +187,15 @@
|
|||||||
</exclusion>
|
</exclusion>
|
||||||
</exclusions>
|
</exclusions>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>dev.langchain4j</groupId>
|
||||||
|
<artifactId>langchain4j-anthropic</artifactId>
|
||||||
|
</dependency>
|
||||||
<!-- langChain4j vextor support -->
|
<!-- langChain4j vextor support -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.jeecgframework</groupId>
|
<groupId>org.jeecgframework</groupId>
|
||||||
<artifactId>langchain4j-pgvector</artifactId>
|
<artifactId>langchain4j-pgvector</artifactId>
|
||||||
<version>${langchain4j.version}</version>
|
<version>1.3.0-beta9</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<!-- langChain4j Document Parser -->
|
<!-- langChain4j Document Parser -->
|
||||||
<dependency>
|
<dependency>
|
||||||
|
|||||||
@ -38,4 +38,10 @@ public class AiAppConsts {
|
|||||||
*/
|
*/
|
||||||
public static final String APP_TYPE_CHAT_FLOW = "chatFLow";
|
public static final String APP_TYPE_CHAT_FLOW = "chatFLow";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 应用元数据:流程输入参数
|
||||||
|
* for [issues/8545]新建AI应用的时候只能选择没有自定义参数的AI流程
|
||||||
|
*/
|
||||||
|
public static final String APP_METADATA_FLOW_INPUTS = "flowInputs";
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,17 +1,17 @@
|
|||||||
package org.jeecg.modules.airag.app.controller;
|
package org.jeecg.modules.airag.app.controller;
|
||||||
|
|
||||||
|
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
|
||||||
import org.jeecg.common.api.vo.Result;
|
import org.jeecg.common.api.vo.Result;
|
||||||
import org.jeecg.common.system.base.controller.JeecgController;
|
import org.jeecg.common.system.base.controller.JeecgController;
|
||||||
import org.jeecg.common.system.query.QueryGenerator;
|
import org.jeecg.common.system.query.QueryGenerator;
|
||||||
import org.jeecg.common.util.AssertUtils;
|
import org.jeecg.common.util.AssertUtils;
|
||||||
import org.jeecg.common.util.TokenUtils;
|
import org.jeecg.common.util.TokenUtils;
|
||||||
import org.jeecg.config.mybatis.MybatisPlusSaasConfig;
|
import org.jeecg.config.mybatis.MybatisPlusSaasConfig;
|
||||||
import org.jeecg.config.shiro.IgnoreAuth;
|
import org.jeecg.config.satoken.IgnoreAuth;
|
||||||
import org.jeecg.modules.airag.app.consts.AiAppConsts;
|
import org.jeecg.modules.airag.app.consts.AiAppConsts;
|
||||||
import org.jeecg.modules.airag.app.entity.AiragApp;
|
import org.jeecg.modules.airag.app.entity.AiragApp;
|
||||||
import org.jeecg.modules.airag.app.service.IAiragAppService;
|
import org.jeecg.modules.airag.app.service.IAiragAppService;
|
||||||
@ -67,7 +67,7 @@ public class AiragAppController extends JeecgController<AiragApp, IAiragAppServi
|
|||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
@RequestMapping(value = "/edit", method = {RequestMethod.PUT, RequestMethod.POST})
|
@RequestMapping(value = "/edit", method = {RequestMethod.PUT, RequestMethod.POST})
|
||||||
@RequiresPermissions("airag:app:edit")
|
@SaCheckPermission("airag:app:edit")
|
||||||
public Result<String> edit(@RequestBody AiragApp airagApp) {
|
public Result<String> edit(@RequestBody AiragApp airagApp) {
|
||||||
AssertUtils.assertNotEmpty("参数异常", airagApp);
|
AssertUtils.assertNotEmpty("参数异常", airagApp);
|
||||||
AssertUtils.assertNotEmpty("请输入应用名称", airagApp.getName());
|
AssertUtils.assertNotEmpty("请输入应用名称", airagApp.getName());
|
||||||
@ -106,7 +106,7 @@ public class AiragAppController extends JeecgController<AiragApp, IAiragAppServi
|
|||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
@DeleteMapping(value = "/delete")
|
@DeleteMapping(value = "/delete")
|
||||||
@RequiresPermissions("airag:app:delete")
|
@SaCheckPermission("airag:app:delete")
|
||||||
public Result<String> delete(HttpServletRequest request,@RequestParam(name = "id", required = true) String id) {
|
public Result<String> delete(HttpServletRequest request,@RequestParam(name = "id", required = true) String id) {
|
||||||
//update-begin---author:chenrui ---date:20250606 for:[issues/8337]关于ai工作列表的数据权限问题 #8337------------
|
//update-begin---author:chenrui ---date:20250606 for:[issues/8337]关于ai工作列表的数据权限问题 #8337------------
|
||||||
//如果是saas隔离的情况下,判断当前租户id是否是当前租户下的
|
//如果是saas隔离的情况下,判断当前租户id是否是当前租户下的
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import lombok.extern.slf4j.Slf4j;
|
|||||||
import org.jeecg.common.api.vo.Result;
|
import org.jeecg.common.api.vo.Result;
|
||||||
import org.jeecg.common.constant.CommonConstant;
|
import org.jeecg.common.constant.CommonConstant;
|
||||||
import org.jeecg.common.util.CommonUtils;
|
import org.jeecg.common.util.CommonUtils;
|
||||||
import org.jeecg.config.shiro.IgnoreAuth;
|
import org.jeecg.config.satoken.IgnoreAuth;
|
||||||
import org.jeecg.modules.airag.app.service.IAiragChatService;
|
import org.jeecg.modules.airag.app.service.IAiragChatService;
|
||||||
import org.jeecg.modules.airag.app.vo.ChatConversation;
|
import org.jeecg.modules.airag.app.vo.ChatConversation;
|
||||||
import org.jeecg.modules.airag.app.vo.ChatSendParams;
|
import org.jeecg.modules.airag.app.vo.ChatSendParams;
|
||||||
|
|||||||
@ -167,6 +167,12 @@ public class AiragApp implements Serializable {
|
|||||||
@Schema(description = "元数据")
|
@Schema(description = "元数据")
|
||||||
private java.lang.String metadata;
|
private java.lang.String metadata;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 插件 [{pluginId: '123213', pluginName: 'xxxx', category: 'mcp'}]
|
||||||
|
*/
|
||||||
|
@Schema(description = "插件")
|
||||||
|
private java.lang.String plugins;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 知识库ids
|
* 知识库ids
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -71,7 +71,7 @@ public class AiragAppServiceImpl extends ServiceImpl<AiragAppMapper, AiragApp> i
|
|||||||
AtomicBoolean isThinking = new AtomicBoolean(false);
|
AtomicBoolean isThinking = new AtomicBoolean(false);
|
||||||
String requestId = UUIDGenerator.generate();
|
String requestId = UUIDGenerator.generate();
|
||||||
// ai聊天响应逻辑
|
// ai聊天响应逻辑
|
||||||
tokenStream.onNext((String resMessage) -> {
|
tokenStream.onPartialResponse((String resMessage) -> {
|
||||||
// 兼容推理模型
|
// 兼容推理模型
|
||||||
if ("<think>".equals(resMessage)) {
|
if ("<think>".equals(resMessage)) {
|
||||||
isThinking.set(true);
|
isThinking.set(true);
|
||||||
@ -99,9 +99,9 @@ public class AiragAppServiceImpl extends ServiceImpl<AiragAppMapper, AiragApp> i
|
|||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.onComplete((responseMessage) -> {
|
.onCompleteResponse((responseMessage) -> {
|
||||||
// 记录ai的回复
|
// 记录ai的回复
|
||||||
AiMessage aiMessage = responseMessage.content();
|
AiMessage aiMessage = responseMessage.aiMessage();
|
||||||
FinishReason finishReason = responseMessage.finishReason();
|
FinishReason finishReason = responseMessage.finishReason();
|
||||||
String respText = aiMessage.text();
|
String respText = aiMessage.text();
|
||||||
if (FinishReason.STOP.equals(finishReason) || null == finishReason) {
|
if (FinishReason.STOP.equals(finishReason) || null == finishReason) {
|
||||||
@ -114,9 +114,6 @@ public class AiragAppServiceImpl extends ServiceImpl<AiragAppMapper, AiragApp> i
|
|||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
}
|
}
|
||||||
closeSSE(emitter, eventData);
|
closeSSE(emitter, eventData);
|
||||||
} else if (FinishReason.TOOL_EXECUTION.equals(finishReason)) {
|
|
||||||
// 需要执行工具
|
|
||||||
// TODO author: chenrui for: date:2025/3/7
|
|
||||||
} else {
|
} else {
|
||||||
// 异常结束
|
// 异常结束
|
||||||
log.error("调用模型异常:" + respText);
|
log.error("调用模型异常:" + respText);
|
||||||
|
|||||||
@ -1,10 +1,13 @@
|
|||||||
package org.jeecg.modules.airag.app.service.impl;
|
package org.jeecg.modules.airag.app.service.impl;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson.JSONArray;
|
||||||
import com.alibaba.fastjson.JSONObject;
|
import com.alibaba.fastjson.JSONObject;
|
||||||
|
import dev.langchain4j.agent.tool.ToolExecutionRequest;
|
||||||
import dev.langchain4j.data.image.Image;
|
import dev.langchain4j.data.image.Image;
|
||||||
import dev.langchain4j.data.message.*;
|
import dev.langchain4j.data.message.*;
|
||||||
import dev.langchain4j.model.output.FinishReason;
|
import dev.langchain4j.model.output.FinishReason;
|
||||||
import dev.langchain4j.service.TokenStream;
|
import dev.langchain4j.service.TokenStream;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.jeecg.common.api.vo.Result;
|
import org.jeecg.common.api.vo.Result;
|
||||||
import org.jeecg.common.exception.JeecgBootBizTipException;
|
import org.jeecg.common.exception.JeecgBootBizTipException;
|
||||||
@ -23,16 +26,19 @@ import org.jeecg.modules.airag.common.consts.AiragConsts;
|
|||||||
import org.jeecg.modules.airag.common.handler.AIChatParams;
|
import org.jeecg.modules.airag.common.handler.AIChatParams;
|
||||||
import org.jeecg.modules.airag.common.handler.IAIChatHandler;
|
import org.jeecg.modules.airag.common.handler.IAIChatHandler;
|
||||||
import org.jeecg.modules.airag.common.utils.AiragLocalCache;
|
import org.jeecg.modules.airag.common.utils.AiragLocalCache;
|
||||||
|
import org.jeecg.modules.airag.common.vo.LlmPlugin;
|
||||||
import org.jeecg.modules.airag.common.vo.MessageHistory;
|
import org.jeecg.modules.airag.common.vo.MessageHistory;
|
||||||
import org.jeecg.modules.airag.common.vo.event.EventData;
|
import org.jeecg.modules.airag.common.vo.event.EventData;
|
||||||
import org.jeecg.modules.airag.common.vo.event.EventFlowData;
|
import org.jeecg.modules.airag.common.vo.event.EventFlowData;
|
||||||
import org.jeecg.modules.airag.common.vo.event.EventMessageData;
|
import org.jeecg.modules.airag.common.vo.event.EventMessageData;
|
||||||
import org.jeecg.modules.airag.flow.consts.FlowConsts;
|
import org.jeecg.modules.airag.flow.consts.FlowConsts;
|
||||||
|
import org.jeecg.modules.airag.flow.entity.AiragFlow;
|
||||||
import org.jeecg.modules.airag.flow.service.IAiragFlowService;
|
import org.jeecg.modules.airag.flow.service.IAiragFlowService;
|
||||||
import org.jeecg.modules.airag.flow.vo.api.FlowRunParams;
|
import org.jeecg.modules.airag.flow.vo.api.FlowRunParams;
|
||||||
import org.jeecg.modules.airag.llm.entity.AiragModel;
|
import org.jeecg.modules.airag.llm.entity.AiragModel;
|
||||||
import org.jeecg.modules.airag.llm.handler.AIChatHandler;
|
import org.jeecg.modules.airag.llm.handler.AIChatHandler;
|
||||||
import org.jeecg.modules.airag.llm.handler.JeecgToolsProvider;
|
import org.jeecg.modules.airag.llm.handler.JeecgToolsProvider;
|
||||||
|
import org.jeecg.modules.airag.llm.mapper.AiragModelMapper;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.data.redis.core.BoundValueOperations;
|
import org.springframework.data.redis.core.BoundValueOperations;
|
||||||
@ -40,7 +46,6 @@ import org.springframework.data.redis.core.RedisTemplate;
|
|||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||||
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.concurrent.*;
|
import java.util.concurrent.*;
|
||||||
@ -78,6 +83,9 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
|||||||
@Autowired
|
@Autowired
|
||||||
JeecgToolsProvider jeecgToolsProvider;
|
JeecgToolsProvider jeecgToolsProvider;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
AiragModelMapper airagModelMapper;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 重新接收消息
|
* 重新接收消息
|
||||||
*/
|
*/
|
||||||
@ -102,6 +110,12 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
|||||||
if (oConvertUtils.isEmpty(chatConversation.getTitle())) {
|
if (oConvertUtils.isEmpty(chatConversation.getTitle())) {
|
||||||
chatConversation.setTitle(userMessage.length() > 5 ? userMessage.substring(0, 5) : userMessage);
|
chatConversation.setTitle(userMessage.length() > 5 ? userMessage.substring(0, 5) : userMessage);
|
||||||
}
|
}
|
||||||
|
//update-begin---author:chenrui ---date:20251106 for:[issues/8545]新建AI应用的时候只能选择没有自定义参数的AI流程------------
|
||||||
|
// 保存工作流入参配置(如果有)
|
||||||
|
if (oConvertUtils.isObjectNotEmpty(chatSendParams.getFlowInputs())) {
|
||||||
|
chatConversation.setFlowInputs(chatSendParams.getFlowInputs());
|
||||||
|
}
|
||||||
|
//update-end---author:chenrui ---date:20251106 for:[issues/8545]新建AI应用的时候只能选择没有自定义参数的AI流程------------
|
||||||
// 发送消息
|
// 发送消息
|
||||||
return doChat(chatConversation, topicId, chatSendParams);
|
return doChat(chatConversation, topicId, chatSendParams);
|
||||||
}
|
}
|
||||||
@ -117,6 +131,12 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
|||||||
AiragApp app = appDebugParams.getApp();
|
AiragApp app = appDebugParams.getApp();
|
||||||
app.setId("__DEBUG_APP");
|
app.setId("__DEBUG_APP");
|
||||||
ChatConversation chatConversation = getOrCreateChatConversation(app, topicId);
|
ChatConversation chatConversation = getOrCreateChatConversation(app, topicId);
|
||||||
|
//update-begin---author:chenrui ---date:20251106 for:[issues/8545]新建AI应用的时候只能选择没有自定义参数的AI流程------------
|
||||||
|
// 保存工作流入参配置(如果有)
|
||||||
|
if (oConvertUtils.isObjectNotEmpty(appDebugParams.getFlowInputs())) {
|
||||||
|
chatConversation.setFlowInputs(appDebugParams.getFlowInputs());
|
||||||
|
}
|
||||||
|
//update-end---author:chenrui ---date:20251106 for:[issues/8545]新建AI应用的时候只能选择没有自定义参数的AI流程------------
|
||||||
// 发送消息
|
// 发送消息
|
||||||
SseEmitter emitter = doChat(chatConversation, topicId, appDebugParams);
|
SseEmitter emitter = doChat(chatConversation, topicId, appDebugParams);
|
||||||
//保存会话
|
//保存会话
|
||||||
@ -237,7 +257,33 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
|||||||
if (oConvertUtils.isObjectEmpty(chatConversation)) {
|
if (oConvertUtils.isObjectEmpty(chatConversation)) {
|
||||||
return Result.ok(Collections.emptyList());
|
return Result.ok(Collections.emptyList());
|
||||||
}
|
}
|
||||||
return Result.ok(chatConversation.getMessages());
|
//update-begin---author:chenrui ---date:20251106 for:[issues/8545]新建AI应用的时候只能选择没有自定义参数的AI流程------------
|
||||||
|
// 返回消息列表和会话设置信息
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
// 过滤掉工具调用相关的消息(前端不需要展示)
|
||||||
|
List<MessageHistory> messages = chatConversation.getMessages();
|
||||||
|
if (oConvertUtils.isObjectNotEmpty(messages)) {
|
||||||
|
messages = messages.stream()
|
||||||
|
.filter(msg -> !AiragConsts.MESSAGE_ROLE_TOOL.equals(msg.getRole()))
|
||||||
|
.map(msg -> {
|
||||||
|
// 克隆消息对象,移除工具执行请求信息(前端不需要)
|
||||||
|
MessageHistory displayMsg = MessageHistory.builder()
|
||||||
|
.conversationId(msg.getConversationId())
|
||||||
|
.topicId(msg.getTopicId())
|
||||||
|
.role(msg.getRole())
|
||||||
|
.content(msg.getContent())
|
||||||
|
.images(msg.getImages())
|
||||||
|
.datetime(msg.getDatetime())
|
||||||
|
.build();
|
||||||
|
// 不设置toolExecutionRequests和toolExecutionResult
|
||||||
|
return displayMsg;
|
||||||
|
})
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
result.put("messages", messages);
|
||||||
|
result.put("flowInputs", chatConversation.getFlowInputs());
|
||||||
|
return Result.ok(result);
|
||||||
|
//update-end---author:chenrui ---date:20251106 for:[issues/8545]新建AI应用的时候只能选择没有自定义参数的AI流程------------
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -258,6 +304,51 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
|||||||
@Override
|
@Override
|
||||||
public Result<?> initChat(String appId) {
|
public Result<?> initChat(String appId) {
|
||||||
AiragApp app = airagAppMapper.getByIdIgnoreTenant(appId);
|
AiragApp app = airagAppMapper.getByIdIgnoreTenant(appId);
|
||||||
|
//update-begin---author:chenrui ---date:20251106 for:[issues/8545]新建AI应用的时候只能选择没有自定义参数的AI流程------------
|
||||||
|
if(AiAppConsts.APP_TYPE_CHAT_FLOW.equalsIgnoreCase(app.getType())) {
|
||||||
|
AiragFlow flow = airagFlowService.getById(app.getFlowId());
|
||||||
|
String flowMetadata = flow.getMetadata();
|
||||||
|
if(oConvertUtils.isNotEmpty(flowMetadata)) {
|
||||||
|
JSONObject flowMetadataJson = JSONObject.parseObject(flowMetadata);
|
||||||
|
JSONArray flowMetadataInputs = flowMetadataJson.getJSONArray(FlowConsts.FLOW_METADATA_INPUTS);
|
||||||
|
if(oConvertUtils.isObjectNotEmpty(flowMetadataInputs)) {
|
||||||
|
String appMetadataStr = app.getMetadata();
|
||||||
|
JSONObject appMetadataJson;
|
||||||
|
if(oConvertUtils.isEmpty(appMetadataStr)){
|
||||||
|
appMetadataJson = new JSONObject();
|
||||||
|
} else {
|
||||||
|
appMetadataJson = JSONObject.parseObject(appMetadataStr);
|
||||||
|
}
|
||||||
|
appMetadataJson.put(AiAppConsts.APP_METADATA_FLOW_INPUTS, flowMetadataInputs);
|
||||||
|
app.setMetadata(appMetadataJson.toJSONString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//update-end---author:chenrui ---date:20251106 for:[issues/8545]新建AI应用的时候只能选择没有自定义参数的AI流程------------
|
||||||
|
|
||||||
|
//update-begin---author:chenrui ---date:202501XX for:在initChat接口中返回模型供应商信息,避免前端多次调用模型查询接口------------
|
||||||
|
// 如果应用有模型ID,查询模型信息并将供应商、类型、名称等信息添加到metadata中
|
||||||
|
if (oConvertUtils.isNotEmpty(app.getModelId())) {
|
||||||
|
AiragModel model = airagModelMapper.getByIdIgnoreTenant(app.getModelId());
|
||||||
|
if (model != null) {
|
||||||
|
String appMetadataStr = app.getMetadata();
|
||||||
|
JSONObject appMetadataJson;
|
||||||
|
if(oConvertUtils.isEmpty(appMetadataStr)){
|
||||||
|
appMetadataJson = new JSONObject();
|
||||||
|
} else {
|
||||||
|
appMetadataJson = JSONObject.parseObject(appMetadataStr);
|
||||||
|
}
|
||||||
|
// 将模型信息添加到metadata中
|
||||||
|
JSONObject modelInfo = new JSONObject();
|
||||||
|
modelInfo.put("provider", model.getProvider());
|
||||||
|
modelInfo.put("modelType", model.getModelType());
|
||||||
|
modelInfo.put("modelName", model.getModelName());
|
||||||
|
appMetadataJson.put("modelInfo", modelInfo);
|
||||||
|
app.setMetadata(appMetadataJson.toJSONString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//update-end---author:chenrui ---date:202501XX for:在initChat接口中返回模型供应商信息,避免前端多次调用模型查询接口------------
|
||||||
|
|
||||||
return Result.ok(app);
|
return Result.ok(app);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -541,7 +632,30 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
|||||||
chatMessage = UserMessage.from(contents);
|
chatMessage = UserMessage.from(contents);
|
||||||
break;
|
break;
|
||||||
case AiragConsts.MESSAGE_ROLE_AI:
|
case AiragConsts.MESSAGE_ROLE_AI:
|
||||||
chatMessage = new AiMessage(history.getContent());
|
// 重建AI消息,包括工具执行请求
|
||||||
|
if (oConvertUtils.isObjectNotEmpty(history.getToolExecutionRequests())) {
|
||||||
|
// 有工具执行请求,重建带工具调用的AiMessage
|
||||||
|
List<ToolExecutionRequest> toolRequests = history.getToolExecutionRequests().stream()
|
||||||
|
.map(toolReq -> ToolExecutionRequest.builder()
|
||||||
|
.id(toolReq.getId())
|
||||||
|
.name(toolReq.getName())
|
||||||
|
.arguments(toolReq.getArguments())
|
||||||
|
.build())
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
chatMessage = AiMessage.from(history.getContent(), toolRequests);
|
||||||
|
} else {
|
||||||
|
chatMessage = new AiMessage(history.getContent());
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case AiragConsts.MESSAGE_ROLE_TOOL:
|
||||||
|
// 重建工具执行结果消息
|
||||||
|
// 需要重建ToolExecutionRequest,第一个参数是request对象,第二个参数是result字符串
|
||||||
|
ToolExecutionRequest recreatedRequest = ToolExecutionRequest.builder()
|
||||||
|
.id(history.getContent()) // content字段存储的是工具执行的id
|
||||||
|
.name("unknown") // 工具名称在重建时不重要,因为主要用于AI理解结果
|
||||||
|
.arguments("{}")
|
||||||
|
.build();
|
||||||
|
chatMessage = ToolExecutionResultMessage.from(recreatedRequest, history.getToolExecutionResult());
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (null == chatMessage) {
|
if (null == chatMessage) {
|
||||||
@ -599,7 +713,26 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
|||||||
historyMessage.setImages(images);
|
historyMessage.setImages(images);
|
||||||
} else if (message.type().equals(ChatMessageType.AI)) {
|
} else if (message.type().equals(ChatMessageType.AI)) {
|
||||||
historyMessage.setRole(AiragConsts.MESSAGE_ROLE_AI);
|
historyMessage.setRole(AiragConsts.MESSAGE_ROLE_AI);
|
||||||
historyMessage.setContent(((AiMessage) message).text());
|
AiMessage aiMessage = (AiMessage) message;
|
||||||
|
historyMessage.setContent(aiMessage.text());
|
||||||
|
// 处理工具执行请求
|
||||||
|
if (oConvertUtils.isObjectNotEmpty(aiMessage.toolExecutionRequests())) {
|
||||||
|
List<MessageHistory.ToolExecutionRequestHistory> toolRequests = new ArrayList<>();
|
||||||
|
for (ToolExecutionRequest request : aiMessage.toolExecutionRequests()) {
|
||||||
|
toolRequests.add(MessageHistory.ToolExecutionRequestHistory.from(
|
||||||
|
request.id(),
|
||||||
|
request.name(),
|
||||||
|
request.arguments()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
historyMessage.setToolExecutionRequests(toolRequests);
|
||||||
|
}
|
||||||
|
} else if (message.type().equals(ChatMessageType.TOOL_EXECUTION_RESULT)) {
|
||||||
|
// 工具执行结果消息
|
||||||
|
historyMessage.setRole(AiragConsts.MESSAGE_ROLE_TOOL);
|
||||||
|
ToolExecutionResultMessage toolMessage = (ToolExecutionResultMessage) message;
|
||||||
|
historyMessage.setContent(toolMessage.id());
|
||||||
|
historyMessage.setToolExecutionResult(toolMessage.text());
|
||||||
}
|
}
|
||||||
histories.add(historyMessage);
|
histories.add(historyMessage);
|
||||||
chatConversation.setMessages(histories);
|
chatConversation.setMessages(histories);
|
||||||
@ -648,11 +781,15 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
|||||||
sendWithFlow(requestId, aiApp.getFlowId(), chatConversation, topicId, messages, sendParams);
|
sendWithFlow(requestId, aiApp.getFlowId(), chatConversation, topicId, messages, sendParams);
|
||||||
} else {
|
} else {
|
||||||
// AI应用-聊天助手(ChatAssistant):从应用信息组装模型和提示词
|
// AI应用-聊天助手(ChatAssistant):从应用信息组装模型和提示词
|
||||||
sendWithAppChat(requestId, messages, chatConversation, topicId);
|
sendWithAppChat(requestId, messages, chatConversation, topicId, sendParams);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 发消息
|
// 发消息
|
||||||
sendWithDefault(requestId, chatConversation, topicId, null, messages, null);
|
AIChatParams aiChatParams = new AIChatParams();
|
||||||
|
if (oConvertUtils.isObjectNotEmpty(sendParams.getEnableSearch())) {
|
||||||
|
aiChatParams.setEnableSearch(sendParams.getEnableSearch());
|
||||||
|
}
|
||||||
|
sendWithDefault(requestId, chatConversation, topicId, null, messages, aiChatParams);
|
||||||
}
|
}
|
||||||
// 发送就绪消息
|
// 发送就绪消息
|
||||||
EventData eventRequestId = new EventData(requestId, null, EventData.EVENT_INIT_REQUEST_ID, chatConversation.getId(), topicId);
|
EventData eventRequestId = new EventData(requestId, null, EventData.EVENT_INIT_REQUEST_ID, chatConversation.getId(), topicId);
|
||||||
@ -698,6 +835,14 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
|||||||
flowInputParams.put(FlowConsts.FLOW_INPUT_PARAM_HISTORY, histories);
|
flowInputParams.put(FlowConsts.FLOW_INPUT_PARAM_HISTORY, histories);
|
||||||
flowInputParams.put(FlowConsts.FLOW_INPUT_PARAM_QUESTION, sendParams.getContent());
|
flowInputParams.put(FlowConsts.FLOW_INPUT_PARAM_QUESTION, sendParams.getContent());
|
||||||
flowInputParams.put(FlowConsts.FLOW_INPUT_PARAM_IMAGES, sendParams.getImages());
|
flowInputParams.put(FlowConsts.FLOW_INPUT_PARAM_IMAGES, sendParams.getImages());
|
||||||
|
|
||||||
|
//update-begin---author:chenrui ---date:20251106 for:[issues/8545]新建AI应用的时候只能选择没有自定义参数的AI流程------------
|
||||||
|
// 添加工作流的额外参数(从conversation的flowInputs中读取)
|
||||||
|
if (oConvertUtils.isObjectNotEmpty(chatConversation.getFlowInputs())) {
|
||||||
|
flowInputParams.putAll(chatConversation.getFlowInputs());
|
||||||
|
}
|
||||||
|
//update-end---author:chenrui ---date:20251106 for:[issues/8545]新建AI应用的时候只能选择没有自定义参数的AI流程------------
|
||||||
|
|
||||||
flowRunParams.setInputParams(flowInputParams);
|
flowRunParams.setInputParams(flowInputParams);
|
||||||
HttpServletRequest httpRequest = SpringContextUtils.getHttpServletRequest();
|
HttpServletRequest httpRequest = SpringContextUtils.getHttpServletRequest();
|
||||||
flowRunParams.setHttpRequest(httpRequest);
|
flowRunParams.setHttpRequest(httpRequest);
|
||||||
@ -762,11 +907,12 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
|||||||
* @param messages
|
* @param messages
|
||||||
* @param chatConversation
|
* @param chatConversation
|
||||||
* @param topicId
|
* @param topicId
|
||||||
|
* @param sendParams
|
||||||
* @return
|
* @return
|
||||||
* @author chenrui
|
* @author chenrui
|
||||||
* @date 2025/2/28 10:41
|
* @date 2025/2/28 10:41
|
||||||
*/
|
*/
|
||||||
private void sendWithAppChat(String requestId, List<ChatMessage> messages, ChatConversation chatConversation, String topicId) {
|
private void sendWithAppChat(String requestId, List<ChatMessage> messages, ChatConversation chatConversation, String topicId, ChatSendParams sendParams) {
|
||||||
AiragApp aiApp = chatConversation.getApp();
|
AiragApp aiApp = chatConversation.getApp();
|
||||||
String modelId = aiApp.getModelId();
|
String modelId = aiApp.getModelId();
|
||||||
AssertUtils.assertNotEmpty("请先选择模型", modelId);
|
AssertUtils.assertNotEmpty("请先选择模型", modelId);
|
||||||
@ -799,6 +945,31 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AI应用插件(支持MCP和自定义插件)
|
||||||
|
String plugins = aiApp.getPlugins();
|
||||||
|
if (oConvertUtils.isNotEmpty(plugins)) {
|
||||||
|
List<String> pluginIds = new ArrayList<>();
|
||||||
|
JSONArray pluginArray = JSONArray.parseArray(plugins);
|
||||||
|
pluginArray.stream().filter(Objects::nonNull)
|
||||||
|
.map(o -> JSONObject.parseObject(o.toString(), LlmPlugin.class))
|
||||||
|
.forEach(plugin -> {
|
||||||
|
// 支持MCP和插件类型
|
||||||
|
if (plugin.getCategory().equals(AiragConsts.PLUGIN_CATEGORY_MCP)
|
||||||
|
|| plugin.getCategory().equals(AiragConsts.PLUGIN_CATEGORY_PLUGIN)) {
|
||||||
|
pluginIds.add(plugin.getPluginId());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (oConvertUtils.isNotEmpty(pluginIds)) {
|
||||||
|
aiChatParams.setPluginIds(pluginIds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置网络搜索参数(如果前端传递了)
|
||||||
|
if (sendParams != null && oConvertUtils.isObjectNotEmpty(sendParams.getEnableSearch())) {
|
||||||
|
aiChatParams.setEnableSearch(sendParams.getEnableSearch());
|
||||||
|
}
|
||||||
|
|
||||||
// 打印流程耗时日志
|
// 打印流程耗时日志
|
||||||
printChatDuration(requestId, "构造应用自定义参数完成");
|
printChatDuration(requestId, "构造应用自定义参数完成");
|
||||||
// 发消息
|
// 发消息
|
||||||
@ -828,6 +999,8 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
|||||||
}
|
}
|
||||||
aiChatParams.setKnowIds(chatConversation.getApp().getKnowIds());
|
aiChatParams.setKnowIds(chatConversation.getApp().getKnowIds());
|
||||||
aiChatParams.setMaxMsgNumber(oConvertUtils.getInt(chatConversation.getApp().getMsgNum(), 5));
|
aiChatParams.setMaxMsgNumber(oConvertUtils.getInt(chatConversation.getApp().getMsgNum(), 5));
|
||||||
|
aiChatParams.setCurrentHttpRequest(SpringContextUtils.getHttpServletRequest());
|
||||||
|
aiChatParams.setReturnThinking(true);
|
||||||
HttpServletRequest httpRequest = SpringContextUtils.getHttpServletRequest();
|
HttpServletRequest httpRequest = SpringContextUtils.getHttpServletRequest();
|
||||||
TokenStream chatStream;
|
TokenStream chatStream;
|
||||||
try {
|
try {
|
||||||
@ -860,21 +1033,14 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
|||||||
*/
|
*/
|
||||||
AtomicBoolean isThinking = new AtomicBoolean(false);
|
AtomicBoolean isThinking = new AtomicBoolean(false);
|
||||||
// ai聊天响应逻辑
|
// ai聊天响应逻辑
|
||||||
chatStream.onNext((String resMessage) -> {
|
chatStream.onPartialResponse((String resMessage) -> {
|
||||||
// 兼容推理模型
|
//update-begin---author:wangshuai---date:2025-11-07---for:[issues/8506]/[issues/8260]/[issues/8166]新增推理模型的支持---
|
||||||
if ("<think>".equals(resMessage)) {
|
if(isThinking.get()){
|
||||||
isThinking.set(true);
|
//思考过程结束
|
||||||
resMessage = "> ";
|
this.sendThinkEnd(requestId, chatConversation, topicId);
|
||||||
}
|
|
||||||
if ("</think>".equals(resMessage)) {
|
|
||||||
isThinking.set(false);
|
isThinking.set(false);
|
||||||
resMessage = "\n\n";
|
|
||||||
}
|
|
||||||
if (isThinking.get()) {
|
|
||||||
if (null != resMessage && resMessage.contains("\n")) {
|
|
||||||
resMessage = "\n> ";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
//update-end---author:wangshuai---date:2025-11-07---for:[issues/8506]/[issues/8260]/[issues/8166]新增推理模型的支持---
|
||||||
EventData eventData = new EventData(requestId, null, EventData.EVENT_MESSAGE, chatConversation.getId(), topicId);
|
EventData eventData = new EventData(requestId, null, EventData.EVENT_MESSAGE, chatConversation.getId(), topicId);
|
||||||
EventMessageData messageEventData = EventMessageData.builder().message(resMessage).build();
|
EventMessageData messageEventData = EventMessageData.builder().message(resMessage).build();
|
||||||
eventData.setData(messageEventData);
|
eventData.setData(messageEventData);
|
||||||
@ -886,12 +1052,54 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
sendMessage2Client(emitter, eventData);
|
sendMessage2Client(emitter, eventData);
|
||||||
}).onComplete((responseMessage) -> {
|
}).onToolExecuted((toolExecution) -> {
|
||||||
|
// 打印工具执行结果
|
||||||
|
log.debug("[AI应用]工具执行结果: toolName={}, toolId={}, result={}",
|
||||||
|
toolExecution.request().name(),
|
||||||
|
toolExecution.request().id(),
|
||||||
|
toolExecution.result());
|
||||||
|
// 将工具执行结果存储到消息历史中
|
||||||
|
ToolExecutionResultMessage toolResultMessage = ToolExecutionResultMessage.from(
|
||||||
|
toolExecution.request(),
|
||||||
|
toolExecution.result()
|
||||||
|
);
|
||||||
|
appendMessage(messages, toolResultMessage, chatConversation, topicId);
|
||||||
|
}).onIntermediateResponse((chatResponse) -> {
|
||||||
|
// 中间响应:包含tool_calls的AI消息
|
||||||
|
AiMessage aiMessage = chatResponse.aiMessage();
|
||||||
|
if (aiMessage != null && oConvertUtils.isObjectNotEmpty(aiMessage.toolExecutionRequests())) {
|
||||||
|
// 保存包含工具调用请求的AI消息
|
||||||
|
log.debug("[AI应用]保存包含工具调用的AI消息: toolCallsCount={}", aiMessage.toolExecutionRequests().size());
|
||||||
|
appendMessage(messages, aiMessage, chatConversation, topicId);
|
||||||
|
}
|
||||||
|
}).onPartialThinking((partialThinking) -> {
|
||||||
|
try {
|
||||||
|
if (oConvertUtils.isEmpty(partialThinking)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
isThinking.set(true);
|
||||||
|
String text = partialThinking.text();
|
||||||
|
// 构造事件数据(EVENT_THINKING 以便前端统一处理)
|
||||||
|
EventData thinkingEvent = new EventData(requestId, null, EventData.EVENT_THINKING, chatConversation.getId(), topicId);
|
||||||
|
thinkingEvent.setData(EventMessageData.builder().message(text).build());
|
||||||
|
thinkingEvent.setRequestId(requestId);
|
||||||
|
// 获取当前缓存的 emitter
|
||||||
|
SseEmitter emitter = AiragLocalCache.get(AiragConsts.CACHE_TYPE_SSE, requestId);
|
||||||
|
if (null == emitter) {
|
||||||
|
log.warn("[AI应用]思考过程发送失败,SSE 已关闭: {}", requestId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 发送给客户端并缓存历史
|
||||||
|
sendMessage2Client(emitter, thinkingEvent);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("发送思考过程异常", e);
|
||||||
|
}
|
||||||
|
}).onCompleteResponse((responseMessage) -> {
|
||||||
// 打印流程耗时日志
|
// 打印流程耗时日志
|
||||||
printChatDuration(requestId, "LLM输出消息完成");
|
printChatDuration(requestId, "LLM输出消息完成");
|
||||||
AiragLocalCache.remove(AiragConsts.CACHE_TYPE_SSE_SEND_TIME, requestId);
|
AiragLocalCache.remove(AiragConsts.CACHE_TYPE_SSE_SEND_TIME, requestId);
|
||||||
// 记录ai的回复
|
// 记录ai的回复
|
||||||
AiMessage aiMessage = responseMessage.content();
|
AiMessage aiMessage = responseMessage.aiMessage();
|
||||||
FinishReason finishReason = responseMessage.finishReason();
|
FinishReason finishReason = responseMessage.finishReason();
|
||||||
String respText = aiMessage.text();
|
String respText = aiMessage.text();
|
||||||
// sse
|
// sse
|
||||||
@ -907,9 +1115,6 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
|||||||
// 保存会话
|
// 保存会话
|
||||||
saveChatConversation(chatConversation, false, httpRequest);
|
saveChatConversation(chatConversation, false, httpRequest);
|
||||||
closeSSE(emitter, eventData);
|
closeSSE(emitter, eventData);
|
||||||
} else if (FinishReason.TOOL_EXECUTION.equals(finishReason)) {
|
|
||||||
// 需要执行工具
|
|
||||||
// TODO author: chenrui for: date:2025/3/7
|
|
||||||
} else if (FinishReason.LENGTH.equals(finishReason)) {
|
} else if (FinishReason.LENGTH.equals(finishReason)) {
|
||||||
// 上下文长度超过限制
|
// 上下文长度超过限制
|
||||||
log.error("调用模型异常:上下文长度超过限制:{}", responseMessage.tokenUsage());
|
log.error("调用模型异常:上下文长度超过限制:{}", responseMessage.tokenUsage());
|
||||||
@ -966,6 +1171,26 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
|||||||
}).start();
|
}).start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送思考过程结束
|
||||||
|
*
|
||||||
|
* @param requestId
|
||||||
|
* @param chatConversation
|
||||||
|
* @param topicId
|
||||||
|
*/
|
||||||
|
private void sendThinkEnd(String requestId, ChatConversation chatConversation, String topicId) {
|
||||||
|
EventData eventData = new EventData(requestId, null, EventData.EVENT_THINKING_END, chatConversation.getId(), topicId);
|
||||||
|
EventMessageData messageEventData = EventMessageData.builder().message("").build();
|
||||||
|
eventData.setData(messageEventData);
|
||||||
|
eventData.setRequestId(requestId);
|
||||||
|
SseEmitter emitter = AiragLocalCache.get(AiragConsts.CACHE_TYPE_SSE, requestId);
|
||||||
|
if (null == emitter) {
|
||||||
|
log.warn("[AI应用]接收LLM返回会话已关闭");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
sendMessage2Client(emitter, eventData);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 发送消息到客户端
|
* 发送消息到客户端
|
||||||
*
|
*
|
||||||
@ -1081,7 +1306,7 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
|||||||
} else {
|
} else {
|
||||||
token = TokenUtils.getTokenByRequest();
|
token = TokenUtils.getTokenByRequest();
|
||||||
}
|
}
|
||||||
if (TokenUtils.verifyToken(token, sysBaseApi, redisUtil)) {
|
if (TokenUtils.verifyToken(token, sysBaseApi)) {
|
||||||
return JwtUtil.getUsername(token);
|
return JwtUtil.getUsername(token);
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import org.jeecg.modules.airag.common.vo.MessageHistory;
|
|||||||
|
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @Description: 聊天会话
|
* @Description: 聊天会话
|
||||||
@ -39,4 +40,11 @@ public class ChatConversation {
|
|||||||
* 创建时间
|
* 创建时间
|
||||||
*/
|
*/
|
||||||
private Date createTime;
|
private Date createTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 流程入参配置(工作流的额外参数设置)
|
||||||
|
* key: 参数field, value: 参数值
|
||||||
|
* for [issues/8545]新建AI应用的时候只能选择没有自定义参数的AI流程
|
||||||
|
*/
|
||||||
|
private Map<String, Object> flowInputs;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import lombok.Data;
|
|||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @Description: 发送消息的入参
|
* @Description: 发送消息的入参
|
||||||
@ -46,4 +47,16 @@ public class ChatSendParams {
|
|||||||
*/
|
*/
|
||||||
private List<String> images;
|
private List<String> images;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工作流额外入参配置
|
||||||
|
* key: 参数field, value: 参数值
|
||||||
|
* for [issues/8545]新建AI应用的时候只能选择没有自定义参数的AI流程
|
||||||
|
*/
|
||||||
|
private Map<String, Object> flowInputs;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否开启网络搜索(仅千问模型支持)
|
||||||
|
*/
|
||||||
|
private Boolean enableSearch;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user