mirror of
https://github.com/jeecgboot/JeecgBoot.git
synced 2026-02-01 16:15:21 +08:00
Compare commits
129 Commits
2298ee3eed
...
v3.9.1
| Author | SHA1 | Date | |
|---|---|---|---|
| 1d4042a0c1 | |||
| 0e96e7395f | |||
| 42844669af | |||
| 01c3ada3da | |||
| 7493b63a5f | |||
| a20cba0adf | |||
| 360f5d779a | |||
| 1936f503df | |||
| fc1d28581c | |||
| faf7ea55a6 | |||
| 981af95b44 | |||
| ce7415b133 | |||
| cc06450008 | |||
| ca7540b9cb | |||
| 97c76675e7 | |||
| d335ba8612 | |||
| 3cd987b2e9 | |||
| 03f27067d4 | |||
| e94cf00ec0 | |||
| c71e6a6d25 | |||
| 030dc503eb | |||
| 918b59120e | |||
| e2402c75b0 | |||
| 3735ca1687 | |||
| be466f0b03 | |||
| 4092eed2a2 | |||
| 901f05ed21 | |||
| 41877a6e8b | |||
| b7a3da89ca | |||
| de4a8ce652 | |||
| e533af285c | |||
| 23dc7b3f03 | |||
| e57aef0708 | |||
| 42087c0bf8 | |||
| 606edcc82f | |||
| 9082e986f1 | |||
| 40cd525bba | |||
| d6b6cf079e | |||
| 1b688e7cd2 | |||
| 58915a6410 | |||
| b67096dc54 | |||
| 67795493bd | |||
| e1c8f00bf2 | |||
| 17a81e89a5 | |||
| bcbf775756 | |||
| 462365890e | |||
| b686f9fbd1 | |||
| 872f84d006 | |||
| 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 | |||
| 7152ae9e49 | |||
| 58b41db786 | |||
| d715c7a0ac | |||
| aca407e1ce | |||
| cfea79a187 | |||
| 7848d1fb33 | |||
| 91fa645878 | |||
| c9fc948658 | |||
| adc191f03e | |||
| b97d041e7f | |||
| 6492f2c99a | |||
| bf32385a06 | |||
| 6ef637c46f | |||
| bc6f336745 | |||
| 0d86df8e9e | |||
| 3db673b67d | |||
| 3ba5395d33 | |||
| e7eed37470 | |||
| 30ac3f7c72 | |||
| 03e6c97d80 | |||
| b9f6f6dc53 | |||
| 107e13c8af | |||
| 0512b41b2b | |||
| d6d880f887 | |||
| b0e974a418 | |||
| 388fa9b8c2 | |||
| bc04bd1433 | |||
| 35aba0784d | |||
| c3822ab702 | |||
| d4487356f0 | |||
| ae4363dc72 | |||
| 3e6c7651ee | |||
| c0ffd14b7a | |||
| 914875d6a1 | |||
| f6f2ef6316 |
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服务能力。
|
||||||
|
|
||||||
@ -109,6 +106,10 @@ JeecgBoot平台的AIGC功能模块,是一套类似`Dify`的`AIGC应用开发
|
|||||||
| ChatGTP | √ |
|
| ChatGTP | √ |
|
||||||
| Qwq | √ |
|
| Qwq | √ |
|
||||||
| 智库 | √ |
|
| 智库 | √ |
|
||||||
|
| claude | √ |
|
||||||
|
| vl模型 | √ |
|
||||||
|
| 千帆大模型 | √ |
|
||||||
|
| 通义千问 | √ |
|
||||||
| Ollama本地搭建大模型 | √ |
|
| Ollama本地搭建大模型 | √ |
|
||||||
| 等等。。 | √ |
|
| 等等。。 | √ |
|
||||||
|
|
||||||
|
|||||||
@ -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字段建议
|
|
||||||
│ ├─设计表单视图功能(支持多种类型含日历、表格、看板、甘特图)
|
|
||||||
│ └─。。。
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
##### 流程设计
|
|
||||||

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

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

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

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

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

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

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

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

|
|
||||||
@ -1,4 +1,4 @@
|
|||||||
|
[中文](./README.md) | English
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@ -7,12 +7,12 @@
|
|||||||
JEECG BOOT AI Low Code Platform
|
JEECG BOOT AI Low Code Platform
|
||||||
===============
|
===============
|
||||||
|
|
||||||
Current version: 3.8.3 (Release date: 2025-10-09)
|
Current version: 3.9.1 (Release date: 2026-01-28)
|
||||||
|
|
||||||
|
|
||||||
[](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)
|
||||||
|
|
||||||
@ -301,6 +301,41 @@ AI APP: https://help.jeecg.com/aigc
|
|||||||
|
|
||||||
|
|
||||||
##### PC
|
##### PC
|
||||||
|
|
||||||
|
##### AI Model and Application Management
|
||||||
|
|
||||||
|

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

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

|
||||||
|
|
||||||
|
AI Workflow Orchestration
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
MCP and Tool Management
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
AI Knowledge Base (Supports various document formats, with excellent markdown compatibility)
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
AI Toolbox
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
AI Chat Assistant
|
||||||
|
|
||||||
|

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

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

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

|

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

|

|
||||||
171
README.md
171
README.md
@ -1,14 +1,15 @@
|
|||||||
|
中文 | [English](./README.en-US.md)
|
||||||
|
|
||||||
JeecgBoot AI低代码平台
|
JeecgBoot AI低代码平台
|
||||||
===============
|
===============
|
||||||
|
|
||||||
当前最新版本: 3.8.3(发布日期:2025-10-09)
|
当前最新版本: 3.9.1(发布日期:2026-01-28)
|
||||||
|
|
||||||
|
|
||||||
[](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 +20,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 +54,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,制作一个精简版本
|
||||||
|
|
||||||
|
|
||||||
@ -80,8 +85,8 @@ JeecgBoot低代码平台兼容所有J2EE项目开发,支持信创国产化,
|
|||||||
技术文档
|
技术文档
|
||||||
-----------------------------------
|
-----------------------------------
|
||||||
|
|
||||||
- 官方网站: [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](http://www.jeecg.com)
|
||||||
- 入门指南: [快速入门](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)
|
||||||
- 技术支持: [反馈问题](https://github.com/jeecgboot/JeecgBoot/issues/new?template=bug_report.md) | [低代码体验一分钟](https://jeecg.blog.csdn.net/article/details/106079007)
|
- 技术支持: [反馈问题](https://github.com/jeecgboot/JeecgBoot/issues/new?template=bug_report.md) | [低代码体验一分钟](https://jeecg.blog.csdn.net/article/details/106079007)
|
||||||
- QQ交流群 : 964611995、⑩716488839(满)、⑨808791225(满)、其他(满)
|
- QQ交流群 : 964611995、⑩716488839(满)、⑨808791225(满)、其他(满)
|
||||||
@ -104,8 +109,11 @@ JeecgBoot平台提供了一套完善的AI应用管理系统模块,是一套类
|
|||||||
|
|
||||||
为什么选择JeecgBoot?
|
为什么选择JeecgBoot?
|
||||||
-----------------------------------
|
-----------------------------------
|
||||||
- 1.采用最新主流前后分离框架(Spring Boot3 + MyBatis + Shiro/SpringAuthorizationServer + Ant Design4 + Vue3),容易上手;代码生成器依赖性低,灵活的扩展能力,可快速实现二次开发。
|
> 界内首款AI低代码开发平台,同时具备AI应用平台和低代码平台,通过AI驱动低代码开发!
|
||||||
- 2.前端大版本换代,最新版采用 Vue3.0 + TypeScript + Vite6 + Ant Design Vue4 等新技术方案。
|
> 开源界"小普元"超越传统商业平台。引领低代码开发模式(OnlineCoding-> 代码生成器 -> 手工MERGE),低代码开发同时又支持灵活编码, 可以帮助解决Java项目70%的重复工作,让开发更多关注业务。既能快速提高开发效率,节省成本,同时又不失灵活性。
|
||||||
|
|
||||||
|
- 1.提供了一套完善的AI应用管理系统模块,是一套类似`Dify`的`AIGC应用开发平台`+`知识库问答`,是一款基于LLM大语言模型AI应用平台和 RAG 的知识库问答系统。 其直观的界面结合了 AI 流程编排、RAG 管道、知识库管理、模型管理、对接向量库、实时运行可观察等,让您可以快速从原型到生产,拥有AI服务能力
|
||||||
|
- 2.采用最新主流前后分离框架(Spring Boot3 + MyBatisPlus + Vue3.0 + TypeScript + Vite6 + Ant Design Vue4 )等新技术方案。便于学习容易上手,代码生成器依赖性低,灵活的扩展能力,可快速实现二次开发。
|
||||||
- 3.支持微服务Spring Cloud Alibaba(Nacos、Gateway、Sentinel、Skywalking),提供简易机制,支持单体和微服务自由切换(这样可以满足各类项目需求)。
|
- 3.支持微服务Spring Cloud Alibaba(Nacos、Gateway、Sentinel、Skywalking),提供简易机制,支持单体和微服务自由切换(这样可以满足各类项目需求)。
|
||||||
- 4.开发效率高,支持在线建表和AI建表,提供强大代码生成器,单表、树列表、一对多、一对一等数据模型,增删改查功能一键生成,菜单配置直接使用。
|
- 4.开发效率高,支持在线建表和AI建表,提供强大代码生成器,单表、树列表、一对多、一对一等数据模型,增删改查功能一键生成,菜单配置直接使用。
|
||||||
- 5.代码生成器提供强大模板机制,支持自定义模板,目前提供四套风格模板(单表两套、树模型一套、一对多三套)。
|
- 5.代码生成器提供强大模板机制,支持自定义模板,目前提供四套风格模板(单表两套、树模型一套、一对多三套)。
|
||||||
@ -151,12 +159,15 @@ 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、动态菜单、权限校验、按钮级别权限控制等功能
|
||||||
@ -219,38 +230,31 @@ JeecgBoot平台提供了一套完善的AI应用管理系统模块,是一套类
|
|||||||
- 15、CAS 单点登录 √
|
- 15、CAS 单点登录 √
|
||||||
- 16、路由限流 √
|
- 16、路由限流 √
|
||||||
|
|
||||||
#### 微服务架构图
|
|
||||||

|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### 功能清单
|
||||||
|
|
||||||
开源版与企业版区别?
|
|
||||||
-----------------------------------
|
|
||||||
|
|
||||||
- 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 产品功能蓝图
|
|
||||||

|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#### 系统功能架构图
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### 开源版功能清单
|
|
||||||
```
|
```
|
||||||
|
├─AI应用平台
|
||||||
|
│ ├─AI模型管理
|
||||||
|
│ ├─AI应用管理
|
||||||
|
│ ├─AI知识库
|
||||||
|
│ ├─AI流程编排
|
||||||
|
│ ├─AI聊天助手(支持图片、文件)
|
||||||
|
│ ├─AI聊天助手支持嵌入第三方、支持移动端
|
||||||
|
│ ├─MCP插件管理
|
||||||
|
│ ├─提示词管理
|
||||||
|
│ ├─AI应用门户(汇总各种AI应用场景)
|
||||||
|
│ ├─支持各种常见模型ChatGPT和DeepSeek、ollama等
|
||||||
|
├─工具箱
|
||||||
|
│ ├─OCR识别
|
||||||
|
│ ├─AI 海报
|
||||||
|
│ ├─AI 写作
|
||||||
|
│ ├─AI 简历
|
||||||
|
├─AI辅助功能
|
||||||
|
│ ├─AI建表(Online表单)
|
||||||
|
│ ├─AI生成报表(Online报表)
|
||||||
|
│ ├─AI生成大屏
|
||||||
├─系统管理
|
├─系统管理
|
||||||
│ ├─用户管理
|
│ ├─用户管理
|
||||||
│ ├─角色管理
|
│ ├─角色管理
|
||||||
@ -277,18 +281,6 @@ JeecgBoot平台提供了一套完善的AI应用管理系统模块,是一套类
|
|||||||
│ ├─系统编码规则
|
│ ├─系统编码规则
|
||||||
│ ├─系统校验规则
|
│ ├─系统校验规则
|
||||||
│ ├─APP版本管理
|
│ ├─APP版本管理
|
||||||
├─AI应用平台
|
|
||||||
│ ├─AI知识库问答系统
|
|
||||||
│ ├─AI大模型管理
|
|
||||||
│ ├─AI流程编排
|
|
||||||
│ ├─AI流程设计器
|
|
||||||
│ ├─AI对话支持图片
|
|
||||||
│ ├─AI对话助手(智能问答)
|
|
||||||
│ ├─AI建表(Online表单)
|
|
||||||
│ ├─AI聊天窗口支持嵌入第三方
|
|
||||||
│ ├─AI聊天窗口支持移动端
|
|
||||||
│ ├─支持常见大模型ChatGPT和DeepSeek、ollama等等
|
|
||||||
│ ├─AI OCR示例
|
|
||||||
├─数据可视化
|
├─数据可视化
|
||||||
│ ├─报表设计器(支持打印设计)
|
│ ├─报表设计器(支持打印设计)
|
||||||
│ ├─大屏设和仪表盘设计
|
│ ├─大屏设和仪表盘设计
|
||||||
@ -399,6 +391,47 @@ JeecgBoot平台提供了一套完善的AI应用管理系统模块,是一套类
|
|||||||
|
|
||||||
### 系统效果
|
### 系统效果
|
||||||
|
|
||||||
|
|
||||||
|
##### AI模型与应用管理
|
||||||
|
|
||||||
|

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

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

|
||||||
|
|
||||||
|
AI流程编排
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
MCP和工具管理
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
AI知识库(支持各种文档格式,尤其markdown适配很好)
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
AI工具箱
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
AI聊天助手
|
||||||
|
|
||||||
|

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

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

|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
AI写文章
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
##### PC端
|
##### PC端
|
||||||

|

|
||||||
|
|
||||||
@ -418,21 +451,6 @@ JeecgBoot平台提供了一套完善的AI应用管理系统模块,是一套类
|
|||||||

|

|
||||||
|
|
||||||
|
|
||||||
##### AI功能
|
|
||||||
|
|
||||||
AI聊天助手
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
AI建表
|
|
||||||
|
|
||||||

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

|
|
||||||
|
|
||||||
AI写文章
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
|
|
||||||
##### 仪表盘设计器
|
##### 仪表盘设计器
|
||||||
@ -505,6 +523,21 @@ AI写文章
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#### 微服务架构图
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### Jeecg Boot 产品功能蓝图
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#### 系统功能架构图
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## 捐赠
|
## 捐赠
|
||||||
|
|
||||||
|
|||||||
@ -1,13 +1,12 @@
|
|||||||
|
|
||||||
JeecgBoot 低代码开发平台
|
JeecgBoot 低代码开发平台
|
||||||
===============
|
===============
|
||||||
|
|
||||||
当前最新版本: 3.8.3(发布日期:2025-09-22)
|
当前最新版本: 3.9.1(发布日期: 2026-01-22)
|
||||||
|
|
||||||
|
|
||||||
[](https://github.com/zhangdaiscott/jeecg-boot/blob/master/LICENSE)
|
[](https://github.com/zhangdaiscott/jeecg-boot/blob/master/LICENSE)
|
||||||
[](http://jeecg.com/aboutusIndex)
|
[](http://jeecg.com/aboutusIndex)
|
||||||
[](https://github.com/zhangdaiscott/jeecg-boot)
|
[](https://github.com/zhangdaiscott/jeecg-boot)
|
||||||
[](https://github.com/zhangdaiscott/jeecg-boot)
|
[](https://github.com/zhangdaiscott/jeecg-boot)
|
||||||
[](https://github.com/zhangdaiscott/jeecg-boot)
|
[](https://github.com/zhangdaiscott/jeecg-boot)
|
||||||
|
|
||||||
@ -16,43 +15,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 +144,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
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
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.1</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,7 +179,6 @@
|
|||||||
<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>
|
||||||
@ -223,6 +228,12 @@
|
|||||||
<artifactId>shiro-core</artifactId>
|
<artifactId>shiro-core</artifactId>
|
||||||
<classifier>jakarta</classifier>
|
<classifier>jakarta</classifier>
|
||||||
<version>${shiro.version}</version>
|
<version>${shiro.version}</version>
|
||||||
|
<exclusions>
|
||||||
|
<exclusion>
|
||||||
|
<groupId>commons-beanutils</groupId>
|
||||||
|
<artifactId>commons-beanutils</artifactId>
|
||||||
|
</exclusion>
|
||||||
|
</exclusions>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.apache.shiro</groupId>
|
<groupId>org.apache.shiro</groupId>
|
||||||
@ -253,13 +264,6 @@
|
|||||||
</exclusions>
|
</exclusions>
|
||||||
</dependency>
|
</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 +295,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>
|
||||||
@ -301,7 +305,7 @@
|
|||||||
<optional>true</optional>
|
<optional>true</optional>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- mini文件存储服务 -->
|
<!-- minio文件存储服务 -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.minio</groupId>
|
<groupId>io.minio</groupId>
|
||||||
<artifactId>minio</artifactId>
|
<artifactId>minio</artifactId>
|
||||||
@ -310,6 +314,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 +379,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>
|
||||||
@ -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流程
|
||||||
|
|||||||
@ -33,4 +33,10 @@ public class AiragFlowDTO implements Serializable {
|
|||||||
* 输入参数
|
* 输入参数
|
||||||
*/
|
*/
|
||||||
private Map<String, Object> inputParams;
|
private Map<String, Object> inputParams;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否流式返回
|
||||||
|
*/
|
||||||
|
private boolean isStream;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
|
}
|
||||||
@ -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++) {
|
||||||
|
|||||||
@ -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]解决分布式下表字典跨库无法查询问题------------
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -91,6 +91,23 @@ public interface CommonConstant {
|
|||||||
public static String PREFIX_USER_SHIRO_CACHE = "shiro:cache:org.jeecg.config.shiro.ShiroRealm.authorizationCache:";
|
public static String PREFIX_USER_SHIRO_CACHE = "shiro:cache:org.jeecg.config.shiro.ShiroRealm.authorizationCache:";
|
||||||
/** 登录用户Token令牌缓存KEY前缀 */
|
/** 登录用户Token令牌缓存KEY前缀 */
|
||||||
String PREFIX_USER_TOKEN = "prefix_user_token:";
|
String PREFIX_USER_TOKEN = "prefix_user_token:";
|
||||||
|
/** 登录用户Token令牌作废提示信息,比如 “不允许同一账号多地同时登录,会往这个变量存提示信息” */
|
||||||
|
String PREFIX_USER_TOKEN_ERROR_MSG = "prefix_user_token:error:msg_";
|
||||||
|
|
||||||
|
/**============================== 【是否允许同一账号多地同时登录】登录客户端类型常量 ==============================*/
|
||||||
|
/** 客户端类型:PC端 */
|
||||||
|
String CLIENT_TYPE_PC = "PC";
|
||||||
|
/** 客户端类型:APP端 */
|
||||||
|
String CLIENT_TYPE_APP = "APP";
|
||||||
|
/** 客户端类型:手机号登录 */
|
||||||
|
String CLIENT_TYPE_PHONE = "PHONE";
|
||||||
|
String PREFIX_USER_TOKEN_PC = "prefix_user_token:single_login:pc:";
|
||||||
|
/** 单点登录:用户在APP端的Token缓存KEY前缀 (username -> token) */
|
||||||
|
String PREFIX_USER_TOKEN_APP = "prefix_user_token:single_login:app:";
|
||||||
|
/** 单点登录:用户在手机号登录的Token缓存KEY前缀 (username -> token) */
|
||||||
|
String PREFIX_USER_TOKEN_PHONE = "prefix_user_token:single_login:phone:";
|
||||||
|
/**============================== 【是否允许同一账号多地同时登录】登录客户端类型常量 ==============================*/
|
||||||
|
|
||||||
// /** Token缓存时间:3600秒即一小时 */
|
// /** Token缓存时间:3600秒即一小时 */
|
||||||
// int TOKEN_EXPIRE_TIME = 3600;
|
// int TOKEN_EXPIRE_TIME = 3600;
|
||||||
|
|
||||||
@ -308,6 +325,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 +612,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 +626,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 +651,6 @@ public interface CommonConstant {
|
|||||||
* 修改手机号
|
* 修改手机号
|
||||||
*/
|
*/
|
||||||
String UPDATE_PHONE = "updatePhone";
|
String UPDATE_PHONE = "updatePhone";
|
||||||
//update-end---author:wangshuai---date:2024-04-07---for:修改手机号常量---
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 修改手机号验证码请求次数超出
|
* 修改手机号验证码请求次数超出
|
||||||
@ -709,4 +726,19 @@ public interface CommonConstant {
|
|||||||
* 部门名称redisKey(全路径)
|
* 部门名称redisKey(全路径)
|
||||||
*/
|
*/
|
||||||
String DEPART_NAME_REDIS_KEY_PRE = "sys:cache:departPathName:";
|
String DEPART_NAME_REDIS_KEY_PRE = "sys:cache:departPathName:";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 默认用户排序值
|
||||||
|
*/
|
||||||
|
Integer DEFAULT_USER_SORT = 1000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送短信方式:腾讯
|
||||||
|
*/
|
||||||
|
String SMS_SEND_TYPE_TENCENT = "tencent";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送短信方式:阿里云
|
||||||
|
*/
|
||||||
|
String SMS_SEND_TYPE_ALI_YUN = "aliyun";
|
||||||
}
|
}
|
||||||
|
|||||||
@ -39,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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -47,4 +47,8 @@ public interface TenantConstant {
|
|||||||
*/
|
*/
|
||||||
String APP_ADMIN = "appAdmin";
|
String APP_ADMIN = "appAdmin";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 增加SignatureCheck注解POST请求的URL
|
||||||
|
*/
|
||||||
|
String[] SIGNATURE_CHECK_POST_URL = { "/sys/tenant/joinTenantByHouseNumber", "/sys/tenant/invitationUser" };
|
||||||
}
|
}
|
||||||
|
|||||||
@ -23,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;
|
||||||
|
|||||||
@ -121,13 +121,12 @@ public class JeecgBootExceptionHandler {
|
|||||||
@ExceptionHandler(Exception.class)
|
@ExceptionHandler(Exception.class)
|
||||||
public Result<?> handleException(Exception e){
|
public Result<?> handleException(Exception e){
|
||||||
log.error(e.getMessage(), e);
|
log.error(e.getMessage(), e);
|
||||||
//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 +223,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 +241,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 +249,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地址
|
||||||
@ -276,7 +272,6 @@ public class JeecgBootExceptionHandler {
|
|||||||
|
|
||||||
baseCommonService.addLog(log);
|
baseCommonService.addLog(log);
|
||||||
}
|
}
|
||||||
//update-end---author:chenrui ---date:20240423 for:[QQYUN-8732]把错误的日志都抓取了 方便后续处理,单独弄个日志类型------------
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 是否文件过大异常
|
* 是否文件过大异常
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
@ -94,14 +98,12 @@ public class JeecgController<T, S extends IService<T>> {
|
|||||||
// Step.2 计算分页sheet数据
|
// Step.2 计算分页sheet数据
|
||||||
double total = service.count();
|
double total = service.count();
|
||||||
int count = (int)Math.ceil(total/pageNum);
|
int count = (int)Math.ceil(total/pageNum);
|
||||||
//update-begin-author:liusq---date:20220629--for: 多sheet导出根据选择导出写法调整 ---
|
|
||||||
// Step.3 过滤选中数据
|
// Step.3 过滤选中数据
|
||||||
String selections = request.getParameter("selections");
|
String selections = request.getParameter("selections");
|
||||||
if (oConvertUtils.isNotEmpty(selections)) {
|
if (oConvertUtils.isNotEmpty(selections)) {
|
||||||
List<String> selectionList = Arrays.asList(selections.split(","));
|
List<String> selectionList = Arrays.asList(selections.split(","));
|
||||||
queryWrapper.in("id",selectionList);
|
queryWrapper.in("id",selectionList);
|
||||||
}
|
}
|
||||||
//update-end-author:liusq---date:20220629--for: 多sheet导出根据选择导出写法调整 ---
|
|
||||||
// Step.4 多sheet处理
|
// Step.4 多sheet处理
|
||||||
List<Map<String, Object>> listMap = new ArrayList<Map<String, Object>>();
|
List<Map<String, Object>> listMap = new ArrayList<Map<String, Object>>();
|
||||||
for (int i = 1; i <=count ; i++) {
|
for (int i = 1; i <=count ; i++) {
|
||||||
@ -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);
|
||||||
|
|||||||
@ -41,8 +41,10 @@ import org.jeecg.common.util.oConvertUtils;
|
|||||||
@Slf4j
|
@Slf4j
|
||||||
public class JwtUtil {
|
public class JwtUtil {
|
||||||
|
|
||||||
/**Token有效期为7天(Token在reids中缓存时间为两倍)*/
|
/**PC端,Token有效期为7天(Token在reids中缓存时间为两倍)*/
|
||||||
public static final long EXPIRE_TIME = (7 * 12) * 60 * 60 * 1000;
|
public static final long EXPIRE_TIME = (7 * 12) * 60 * 60 * 1000L;
|
||||||
|
/**APP端,Token有效期为30天(Token在reids中缓存时间为两倍)*/
|
||||||
|
public static final long APP_EXPIRE_TIME = (30 * 12) * 60 * 60 * 1000L;
|
||||||
static final String WELL_NUMBER = SymbolConstant.WELL_NUMBER + SymbolConstant.LEFT_CURLY_BRACKET;
|
static final String WELL_NUMBER = SymbolConstant.WELL_NUMBER + SymbolConstant.LEFT_CURLY_BRACKET;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -86,7 +88,7 @@ public class JwtUtil {
|
|||||||
DecodedJWT jwt = verifier.verify(token);
|
DecodedJWT jwt = verifier.verify(token);
|
||||||
return true;
|
return true;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error(e.getMessage(), e);
|
log.warn("Token验证失败:" + e.getMessage(),e);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -112,7 +114,9 @@ public class JwtUtil {
|
|||||||
* @param username 用户名
|
* @param username 用户名
|
||||||
* @param secret 用户的密码
|
* @param secret 用户的密码
|
||||||
* @return 加密的token
|
* @return 加密的token
|
||||||
|
* @deprecated 请使用sign(String username, String secret, String clientType)方法代替
|
||||||
*/
|
*/
|
||||||
|
@Deprecated
|
||||||
public static String sign(String username, String secret) {
|
public static String sign(String username, String secret) {
|
||||||
Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
|
Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
|
||||||
Algorithm algorithm = Algorithm.HMAC256(secret);
|
Algorithm algorithm = Algorithm.HMAC256(secret);
|
||||||
@ -121,6 +125,68 @@ public class JwtUtil {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成签名,5min后过期
|
||||||
|
*
|
||||||
|
* @param username 用户名
|
||||||
|
* @param secret 用户的密码
|
||||||
|
* @param expireTime 过期时间
|
||||||
|
* @return 加密的token
|
||||||
|
* @deprecated 请使用sign(String username, String secret, String clientType)方法代替
|
||||||
|
*/
|
||||||
|
@Deprecated
|
||||||
|
public static String sign(String username, String secret, Long expireTime) {
|
||||||
|
Date date = new Date(System.currentTimeMillis() + expireTime);
|
||||||
|
Algorithm algorithm = Algorithm.HMAC256(secret);
|
||||||
|
// 附带username信息
|
||||||
|
return JWT.create().withClaim("username", username).withExpiresAt(date).sign(algorithm);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成签名,根据客户端类型自动选择过期时间
|
||||||
|
* for [JHHB-1030]【鉴权】移动端用户token到期后续期时间变成pc端时长
|
||||||
|
*
|
||||||
|
* @param username 用户名
|
||||||
|
* @param secret 用户的密码
|
||||||
|
* @param clientType 客户端类型(PC或APP)
|
||||||
|
* @return 加密的token
|
||||||
|
*/
|
||||||
|
public static String sign(String username, String secret, String clientType) {
|
||||||
|
// 根据客户端类型选择对应的过期时间
|
||||||
|
long expireTime = CommonConstant.CLIENT_TYPE_APP.equalsIgnoreCase(clientType)
|
||||||
|
? APP_EXPIRE_TIME
|
||||||
|
: EXPIRE_TIME;
|
||||||
|
Date date = new Date(System.currentTimeMillis() + expireTime);
|
||||||
|
Algorithm algorithm = Algorithm.HMAC256(secret);
|
||||||
|
// 附带username和clientType信息
|
||||||
|
return JWT.create()
|
||||||
|
.withClaim("username", username)
|
||||||
|
.withClaim("clientType", clientType)
|
||||||
|
.withExpiresAt(date)
|
||||||
|
.sign(algorithm);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从token中获取客户端类型
|
||||||
|
* for [JHHB-1030]【鉴权】移动端用户token到期后续期时间变成pc端时长
|
||||||
|
*
|
||||||
|
* @param token JWT token
|
||||||
|
* @return 客户端类型,如果不存在则返回PC(兼容旧token)
|
||||||
|
*/
|
||||||
|
public static String getClientType(String token) {
|
||||||
|
try {
|
||||||
|
DecodedJWT jwt = JWT.decode(token);
|
||||||
|
String clientType = jwt.getClaim("clientType").asString();
|
||||||
|
// 如果clientType为空,返回默认值PC(兼容旧token)
|
||||||
|
return oConvertUtils.isNotEmpty(clientType) ? clientType : CommonConstant.CLIENT_TYPE_PC;
|
||||||
|
} catch (JWTDecodeException e) {
|
||||||
|
log.warn("解析token中的clientType失败,使用默认值PC:" + e.getMessage());
|
||||||
|
return CommonConstant.CLIENT_TYPE_PC;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据request中的token获取用户账号
|
* 根据request中的token获取用户账号
|
||||||
*
|
*
|
||||||
@ -200,7 +266,6 @@ public class JwtUtil {
|
|||||||
} else {
|
} else {
|
||||||
key = key;
|
key = key;
|
||||||
}
|
}
|
||||||
//update-begin---author:chenrui ---date:20250107 for:[QQYUN-10785]数据权限,查看自己拥有部门的权限中存在问题 #7288------------
|
|
||||||
// 是否存在字符串标志
|
// 是否存在字符串标志
|
||||||
boolean multiStr;
|
boolean multiStr;
|
||||||
if(oConvertUtils.isNotEmpty(key) && key.trim().matches("^\\[\\w+]$")){
|
if(oConvertUtils.isNotEmpty(key) && key.trim().matches("^\\[\\w+]$")){
|
||||||
@ -209,7 +274,6 @@ public class JwtUtil {
|
|||||||
} else {
|
} else {
|
||||||
multiStr = false;
|
multiStr = false;
|
||||||
}
|
}
|
||||||
//update-end---author:chenrui ---date:20250107 for:[QQYUN-10785]数据权限,查看自己拥有部门的权限中存在问题 #7288------------
|
|
||||||
//替换为当前系统时间(年月日)
|
//替换为当前系统时间(年月日)
|
||||||
if (key.equals(DataBaseConstant.SYS_DATE)|| key.toLowerCase().equals(DataBaseConstant.SYS_DATE_TABLE)) {
|
if (key.equals(DataBaseConstant.SYS_DATE)|| key.toLowerCase().equals(DataBaseConstant.SYS_DATE_TABLE)) {
|
||||||
returnValue = DateUtils.formatDate();
|
returnValue = DateUtils.formatDate();
|
||||||
@ -278,20 +342,17 @@ public class JwtUtil {
|
|||||||
if(user==null){
|
if(user==null){
|
||||||
//TODO 暂时使用用户登录部门,存在逻辑缺陷,不是用户所拥有的部门
|
//TODO 暂时使用用户登录部门,存在逻辑缺陷,不是用户所拥有的部门
|
||||||
returnValue = sysUser.getOrgCode();
|
returnValue = sysUser.getOrgCode();
|
||||||
//update-begin---author:chenrui ---date:20250107 for:[QQYUN-10785]数据权限,查看自己拥有部门的权限中存在问题 #7288------------
|
// 代码逻辑说明: [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------------
|
// 代码逻辑说明: [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------------
|
// 代码逻辑说明: [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 +360,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,7 +374,7 @@ 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);
|
||||||
@ -323,7 +382,6 @@ public class JwtUtil {
|
|||||||
log.warn("获取系统租户异常:" + e.getMessage());
|
log.warn("获取系统租户异常:" + e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//update-end-author:taoyan date:20210330 for:多租户ID作为系统变量
|
|
||||||
if(returnValue!=null){returnValue = returnValue + moshi;}
|
if(returnValue!=null){returnValue = returnValue + moshi;}
|
||||||
return returnValue;
|
return returnValue;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,9 +3,7 @@ package org.jeecg.common.system.util;
|
|||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.jeecg.common.system.annotation.EnumDict;
|
import org.jeecg.common.system.annotation.EnumDict;
|
||||||
import org.jeecg.common.system.vo.DictModel;
|
import org.jeecg.common.system.vo.DictModel;
|
||||||
import org.jeecg.common.util.SpringContextUtils;
|
|
||||||
import org.jeecg.common.util.oConvertUtils;
|
import org.jeecg.common.util.oConvertUtils;
|
||||||
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
|
|
||||||
import org.springframework.core.io.Resource;
|
import org.springframework.core.io.Resource;
|
||||||
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
|
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
|
||||||
import org.springframework.core.io.support.ResourcePatternResolver;
|
import org.springframework.core.io.support.ResourcePatternResolver;
|
||||||
@ -13,6 +11,7 @@ import org.springframework.core.type.classreading.CachingMetadataReaderFactory;
|
|||||||
import org.springframework.core.type.classreading.MetadataReader;
|
import org.springframework.core.type.classreading.MetadataReader;
|
||||||
import org.springframework.core.type.classreading.MetadataReaderFactory;
|
import org.springframework.core.type.classreading.MetadataReaderFactory;
|
||||||
import org.springframework.util.ClassUtils;
|
import org.springframework.util.ClassUtils;
|
||||||
|
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|
||||||
@ -67,13 +66,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 +102,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 +125,7 @@ public class ResourceUtil {
|
|||||||
}
|
}
|
||||||
|
|
||||||
long processEndTime = System.currentTimeMillis();
|
long processEndTime = System.currentTimeMillis();
|
||||||
log.info("【枚举字典加载】处理完成,实际处理 {} 个带注解的枚举类,处理耗时: {}ms", processedCount, processEndTime - processStartTime);
|
log.debug("【枚举字典加载】处理完成,实际处理 {} 个带注解的枚举类,处理耗时: {}ms", processedCount, processEndTime - processStartTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -183,10 +182,10 @@ public class ResourceUtil {
|
|||||||
for (DictModel dm : dictItemList) {
|
for (DictModel dm : dictItemList) {
|
||||||
String value = dm.getValue();
|
String value = dm.getValue();
|
||||||
if (keySet.contains(value)) {
|
if (keySet.contains(value)) {
|
||||||
List<DictModel> list = new ArrayList<>();
|
// 修复bug:获取或创建该dictCode对应的list,而不是每次都创建新的list
|
||||||
|
List<DictModel> list = map.computeIfAbsent(code, k -> new ArrayList<>());
|
||||||
list.add(new DictModel(value, dm.getText()));
|
list.add(new DictModel(value, dm.getText()));
|
||||||
map.put(code, list);
|
//break;
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 数据权限规则问题
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -56,7 +56,9 @@ public class CommonUtils {
|
|||||||
public static String uploadOnlineImage(byte[] data,String basePath,String bizPath,String uploadType){
|
public static String uploadOnlineImage(byte[] data,String basePath,String bizPath,String uploadType){
|
||||||
String dbPath = null;
|
String dbPath = null;
|
||||||
String fileName = "image" + Math.round(Math.random() * 100000000000L);
|
String fileName = "image" + Math.round(Math.random() * 100000000000L);
|
||||||
fileName += "." + PoiPublicUtil.getFileExtendName(data);
|
//update-begin---author:wangshuai---date:2026-01-08---for:【QQYUN-14535】ai生成图片的后缀不一致的,导致不展示---
|
||||||
|
fileName += "." + PoiPublicUtil.getFileExtendName(data).toLowerCase();
|
||||||
|
//update-end---author:wangshuai---date:2026-01-08---for:【QQYUN-14535】ai生成图片的后缀不一致的,导致不展示---
|
||||||
try {
|
try {
|
||||||
if(CommonConstant.UPLOAD_TYPE_LOCAL.equals(uploadType)){
|
if(CommonConstant.UPLOAD_TYPE_LOCAL.equals(uploadType)){
|
||||||
File file = new File(basePath + File.separator + bizPath + File.separator );
|
File file = new File(basePath + File.separator + bizPath + File.separator );
|
||||||
@ -153,9 +155,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()) {
|
||||||
@ -366,7 +368,7 @@ public class CommonUtils {
|
|||||||
}else{
|
}else{
|
||||||
baseDomainPath = scheme + "://" + serverName + ":" + serverPort + contextPath ;
|
baseDomainPath = scheme + "://" + serverName + ":" + serverPort + contextPath ;
|
||||||
}
|
}
|
||||||
log.debug("-----Common getBaseUrl----- : " + baseDomainPath);
|
log.info("-----Common getBaseUrl----- : " + baseDomainPath);
|
||||||
return baseDomainPath;
|
return baseDomainPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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 邮箱发送失败,需要换写法---
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
|
|
||||||
@ -46,7 +46,7 @@ public class RestUtil {
|
|||||||
|
|
||||||
public static String getBaseUrl() {
|
public static String getBaseUrl() {
|
||||||
String basepath = getDomain() + getPath();
|
String basepath = getDomain() + getPath();
|
||||||
log.info(" RestUtil.getBaseUrl: " + basepath);
|
log.debug(" RestUtil.getBaseUrl: " + basepath);
|
||||||
return basepath;
|
return basepath;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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() {
|
||||||
@ -192,7 +199,7 @@ public class RestUtil {
|
|||||||
* @return ResponseEntity<responseType>
|
* @return ResponseEntity<responseType>
|
||||||
*/
|
*/
|
||||||
public static <T> ResponseEntity<T> request(String url, HttpMethod method, HttpHeaders headers, JSONObject variables, Object params, Class<T> responseType) {
|
public static <T> ResponseEntity<T> request(String url, HttpMethod method, HttpHeaders headers, JSONObject variables, Object params, Class<T> responseType) {
|
||||||
log.info(" RestUtil --- request --- url = "+ url);
|
log.debug(" RestUtil --- request --- url = "+ url);
|
||||||
if (StringUtils.isEmpty(url)) {
|
if (StringUtils.isEmpty(url)) {
|
||||||
throw new RuntimeException("url 不能为空");
|
throw new RuntimeException("url 不能为空");
|
||||||
}
|
}
|
||||||
@ -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.debug(" RestUtil --- request --- 修正/设置 Content-Length = " + contentLength + (current!=null?" (原值="+current+")":""));
|
||||||
|
}
|
||||||
|
}
|
||||||
// 发送请求
|
// 发送请求
|
||||||
HttpEntity<String> request = new HttpEntity<>(body, headers);
|
HttpEntity<String> request = new HttpEntity<>(body, headers);
|
||||||
return RT.exchange(url, method, request, responseType);
|
return RT.exchange(url, method, request, responseType);
|
||||||
@ -235,7 +252,7 @@ public class RestUtil {
|
|||||||
*/
|
*/
|
||||||
public static <T> ResponseEntity<T> request(String url, HttpMethod method, HttpHeaders headers,
|
public static <T> ResponseEntity<T> request(String url, HttpMethod method, HttpHeaders headers,
|
||||||
JSONObject variables, Object params, Class<T> responseType, int timeout) {
|
JSONObject variables, Object params, Class<T> responseType, int timeout) {
|
||||||
log.info(" RestUtil --- request --- url = "+ url + ", timeout = " + timeout);
|
log.debug(" RestUtil --- request --- url = "+ url + ", timeout = " + timeout);
|
||||||
|
|
||||||
if (StringUtils.isEmpty(url)) {
|
if (StringUtils.isEmpty(url)) {
|
||||||
throw new RuntimeException("url 不能为空");
|
throw new RuntimeException("url 不能为空");
|
||||||
@ -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.debug(" RestUtil --- request(timeout) --- 修正/设置 Content-Length = " + contentLength + (current!=null?" (原值="+current+")":""));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 发送请求
|
// 发送请求
|
||||||
HttpEntity<String> request = new HttpEntity<>(body, headers);
|
HttpEntity<String> request = new HttpEntity<>(body, headers);
|
||||||
return restTemplate.exchange(url, method, request, responseType);
|
return restTemplate.exchange(url, method, request, responseType);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取JSON请求头
|
* 获取JSON请求头
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -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]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -121,7 +121,9 @@ public class TokenUtils {
|
|||||||
}
|
}
|
||||||
// 校验token是否超时失效 & 或者账号密码是否错误
|
// 校验token是否超时失效 & 或者账号密码是否错误
|
||||||
if (!jwtTokenRefresh(token, username, user.getPassword(), redisUtil)) {
|
if (!jwtTokenRefresh(token, username, user.getPassword(), redisUtil)) {
|
||||||
throw new JeecgBoot401Exception(CommonConstant.TOKEN_IS_INVALID_MSG);
|
// 用户登录Token过期提示信息
|
||||||
|
String userLoginTokenErrorMsg = oConvertUtils.getString(redisUtil.get(CommonConstant.PREFIX_USER_TOKEN_ERROR_MSG + token));
|
||||||
|
throw new JeecgBoot401Exception(oConvertUtils.isEmpty(userLoginTokenErrorMsg)? CommonConstant.TOKEN_IS_INVALID_MSG: userLoginTokenErrorMsg);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -139,10 +141,15 @@ public class TokenUtils {
|
|||||||
if (oConvertUtils.isNotEmpty(cacheToken)) {
|
if (oConvertUtils.isNotEmpty(cacheToken)) {
|
||||||
// 校验token有效性
|
// 校验token有效性
|
||||||
if (!JwtUtil.verify(cacheToken, userName, passWord)) {
|
if (!JwtUtil.verify(cacheToken, userName, passWord)) {
|
||||||
String newAuthorization = JwtUtil.sign(userName, passWord);
|
// 从token中解析客户端类型,保持续期时使用相同的客户端类型
|
||||||
// 设置Toekn缓存有效时间
|
String clientType = JwtUtil.getClientType(token);
|
||||||
|
String newAuthorization = JwtUtil.sign(userName, passWord, clientType);
|
||||||
|
// 根据客户端类型设置对应的缓存有效时间
|
||||||
|
long expireTime = CommonConstant.CLIENT_TYPE_APP.equalsIgnoreCase(clientType)
|
||||||
|
? JwtUtil.APP_EXPIRE_TIME * 2 / 1000
|
||||||
|
: JwtUtil.EXPIRE_TIME * 2 / 1000;
|
||||||
redisUtil.set(CommonConstant.PREFIX_USER_TOKEN + token, newAuthorization);
|
redisUtil.set(CommonConstant.PREFIX_USER_TOKEN + token, newAuthorization);
|
||||||
redisUtil.expire(CommonConstant.PREFIX_USER_TOKEN + token, JwtUtil.EXPIRE_TIME * 2 / 1000);
|
redisUtil.expire(CommonConstant.PREFIX_USER_TOKEN + token, expireTime);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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,107 @@
|
|||||||
package org.jeecg.common.util.encryption;
|
package org.jeecg.common.util.encryption;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.shiro.lang.codec.Base64;
|
import org.apache.shiro.lang.codec.Base64;
|
||||||
|
import org.jeecg.common.util.oConvertUtils;
|
||||||
import javax.crypto.Cipher;
|
import javax.crypto.Cipher;
|
||||||
import javax.crypto.spec.IvParameterSpec;
|
import javax.crypto.spec.IvParameterSpec;
|
||||||
import javax.crypto.spec.SecretKeySpec;
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @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.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.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.encodeToString(encrypted);
|
return Base64.encodeToString(encrypted);
|
||||||
|
}catch(Exception e){
|
||||||
} catch (Exception e) {
|
throw new IllegalStateException("legacy encrypt error", 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,67 @@ 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("上传业务路径包含非法字符!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验文件路径安全性,防止路径遍历攻击
|
||||||
|
* @param filePath 文件路径
|
||||||
|
*/
|
||||||
|
public static void checkPathTraversal(String filePath) {
|
||||||
|
if (StringUtils.isBlank(filePath)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 1. 防止路径遍历:不允许 ..
|
||||||
|
if (filePath.contains("..")) {
|
||||||
|
throw new JeecgBootException("文件路径包含非法字符");
|
||||||
|
}
|
||||||
|
// 2. 防止URL编码绕过:%2e = .
|
||||||
|
String fileLower = filePath.toLowerCase();
|
||||||
|
if (fileLower.contains("%2e")) {
|
||||||
|
throw new JeecgBootException("文件路径包含非法字符");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量校验文件路径安全性(逗号分隔的多个文件路径)
|
||||||
|
* @param files 逗号分隔的文件路径
|
||||||
|
*/
|
||||||
|
public static void checkPathTraversalBatch(String files) {
|
||||||
|
if (StringUtils.isBlank(files)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (String file : files.split(",")) {
|
||||||
|
if (StringUtils.isNotBlank(file)) {
|
||||||
|
checkPathTraversal(file.trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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,58 @@ public class oConvertUtils {
|
|||||||
public static boolean isEffectiveTenant(String tenantId) {
|
public static boolean isEffectiveTenant(String tenantId) {
|
||||||
return MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL && isNotEmpty(tenantId) && !("0").equals(tenantId);
|
return MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL && isNotEmpty(tenantId) && !("0").equals(tenantId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 复制源对象的非空属性到目标对象(同名属性)
|
||||||
|
*
|
||||||
|
* @param source 源对象(页面)
|
||||||
|
* @param target 目标对象(数据库实体)
|
||||||
|
*/
|
||||||
|
public static void copyNonNullFields(Object source, Object target) {
|
||||||
|
if (source == null || target == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 获取源对象的非空属性名数组
|
||||||
|
String[] nullPropertyNames = getNullPropertyNames(source);
|
||||||
|
// 复制:忽略源对象的空属性,仅覆盖目标对象的对应非空属性
|
||||||
|
BeanUtils.copyProperties(source, target, nullPropertyNames);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取源对象中值为 null 的属性名数组
|
||||||
|
*
|
||||||
|
* @param source
|
||||||
|
*/
|
||||||
|
private static String[] getNullPropertyNames(Object source) {
|
||||||
|
BeanWrapper beanWrapper = new BeanWrapperImpl(source);
|
||||||
|
//获取类的属性
|
||||||
|
PropertyDescriptor[] propertyDescriptors = beanWrapper.getPropertyDescriptors();
|
||||||
|
// 过滤出值为 null 的属性名
|
||||||
|
return Stream.of(propertyDescriptors)
|
||||||
|
.map(PropertyDescriptor::getName)
|
||||||
|
.filter(name -> beanWrapper.getPropertyValue(name) == null)
|
||||||
|
.toArray(String[]::new);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* String转换long类型
|
||||||
|
*
|
||||||
|
* @param v
|
||||||
|
* @param def
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public static long getLong(Object v, long def) {
|
||||||
|
if (v == null) {
|
||||||
|
return def;
|
||||||
|
};
|
||||||
|
if (v instanceof Number) {
|
||||||
|
return ((Number) v).longValue();
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return Long.parseLong(v.toString());
|
||||||
|
} catch (Exception e) {
|
||||||
|
return def;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -0,0 +1,24 @@
|
|||||||
|
package org.jeecg.config;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ai配置类,通用的配置可以放到这里面
|
||||||
|
*
|
||||||
|
* @Author: wangshuai
|
||||||
|
* @Date: 2025/12/17 14:00
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Component
|
||||||
|
@ConfigurationProperties(prefix = AiRagConfigBean.PREFIX)
|
||||||
|
public class AiRagConfigBean {
|
||||||
|
public static final String PREFIX = "jeecg.airag";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 敏感节点
|
||||||
|
* stdio mpc命令行功能开启,sql:AI流程SQL节点开启
|
||||||
|
*/
|
||||||
|
private String allowSensitiveNodes = "";
|
||||||
|
}
|
||||||
@ -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) {
|
||||||
|
|||||||
@ -12,7 +12,6 @@ import java.util.HashMap;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author eightmonth@qq.com
|
|
||||||
* 启动程序修改DruidWallConfig配置
|
* 启动程序修改DruidWallConfig配置
|
||||||
* 允许SELECT语句的WHERE子句是一个永真条件
|
* 允许SELECT语句的WHERE子句是一个永真条件
|
||||||
* @author eightmonth
|
* @author eightmonth
|
||||||
|
|||||||
@ -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.1")
|
||||||
.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")
|
||||||
|
|||||||
@ -0,0 +1,32 @@
|
|||||||
|
package org.jeecg.config;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 任务调度器配置
|
||||||
|
* 提供 ThreadPoolTaskScheduler Bean 用于 AI RAG 流程调度等功能
|
||||||
|
* 仅当容器中不存在 ThreadPoolTaskScheduler 时才创建
|
||||||
|
*
|
||||||
|
* @author jeecg
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Configuration
|
||||||
|
public class TaskSchedulerConfig {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@ConditionalOnMissingBean(ThreadPoolTaskScheduler.class)
|
||||||
|
public ThreadPoolTaskScheduler taskScheduler() {
|
||||||
|
log.info("初始化定时任务调度器 ThreadPoolTaskScheduler");
|
||||||
|
|
||||||
|
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
|
||||||
|
scheduler.setPoolSize(10);
|
||||||
|
scheduler.setThreadNamePrefix("airag-scheduler-");
|
||||||
|
scheduler.setWaitForTasksToCompleteOnShutdown(true);
|
||||||
|
scheduler.setAwaitTerminationSeconds(60);
|
||||||
|
return scheduler;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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 配置完成");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
@ -199,6 +196,5 @@ public class MybatisInterceptor implements Interceptor {
|
|||||||
}
|
}
|
||||||
return sysUser;
|
return sysUser;
|
||||||
}
|
}
|
||||||
//update-end--Author:scott Date:20191213 for:关于使用Quzrtz 开启线程任务, #465
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -21,20 +21,23 @@ import org.jeecg.config.shiro.filters.CustomShiroFilterFactoryBean;
|
|||||||
import org.jeecg.config.shiro.filters.JwtFilter;
|
import org.jeecg.config.shiro.filters.JwtFilter;
|
||||||
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
|
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.beans.factory.annotation.Qualifier;
|
|
||||||
import org.springframework.beans.factory.config.BeanDefinition;
|
import org.springframework.beans.factory.config.BeanDefinition;
|
||||||
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
|
|
||||||
import org.springframework.boot.web.servlet.FilterRegistrationBean;
|
import org.springframework.boot.web.servlet.FilterRegistrationBean;
|
||||||
import org.springframework.context.annotation.*;
|
import org.springframework.context.annotation.*;
|
||||||
|
import org.springframework.core.annotation.AnnotationUtils;
|
||||||
import org.springframework.core.env.Environment;
|
import org.springframework.core.env.Environment;
|
||||||
|
import org.springframework.core.type.filter.AnnotationTypeFilter;
|
||||||
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
|
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
|
||||||
|
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
|
||||||
import org.springframework.util.CollectionUtils;
|
import org.springframework.util.CollectionUtils;
|
||||||
import org.springframework.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
import org.springframework.web.filter.DelegatingFilterProxy;
|
import org.springframework.web.filter.DelegatingFilterProxy;
|
||||||
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
|
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
|
||||||
import redis.clients.jedis.HostAndPort;
|
import redis.clients.jedis.HostAndPort;
|
||||||
import redis.clients.jedis.JedisCluster;
|
import redis.clients.jedis.JedisCluster;
|
||||||
|
|
||||||
|
import java.lang.reflect.Method;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -45,7 +48,6 @@ import java.util.*;
|
|||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Configuration
|
@Configuration
|
||||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
|
||||||
public class ShiroConfig {
|
public class ShiroConfig {
|
||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
@ -111,7 +113,7 @@ public class ShiroConfig {
|
|||||||
filterChainDefinitionMap.put("/sys/checkAuth", "anon"); //授权接口排除
|
filterChainDefinitionMap.put("/sys/checkAuth", "anon"); //授权接口排除
|
||||||
filterChainDefinitionMap.put("/openapi/call/**", "anon"); // 开放平台接口排除
|
filterChainDefinitionMap.put("/openapi/call/**", "anon"); // 开放平台接口排除
|
||||||
|
|
||||||
//update-begin--Author:scott Date:20221116 for:排除静态资源后缀
|
// 代码逻辑说明: 排除静态资源后缀
|
||||||
filterChainDefinitionMap.put("/", "anon");
|
filterChainDefinitionMap.put("/", "anon");
|
||||||
filterChainDefinitionMap.put("/doc.html", "anon");
|
filterChainDefinitionMap.put("/doc.html", "anon");
|
||||||
filterChainDefinitionMap.put("/**/*.js", "anon");
|
filterChainDefinitionMap.put("/**/*.js", "anon");
|
||||||
@ -129,7 +131,6 @@ public class ShiroConfig {
|
|||||||
|
|
||||||
filterChainDefinitionMap.put("/**/*.glb", "anon");
|
filterChainDefinitionMap.put("/**/*.glb", "anon");
|
||||||
filterChainDefinitionMap.put("/**/*.wasm", "anon");
|
filterChainDefinitionMap.put("/**/*.wasm", "anon");
|
||||||
//update-end--Author:scott Date:20221116 for:排除静态资源后缀
|
|
||||||
|
|
||||||
filterChainDefinitionMap.put("/druid/**", "anon");
|
filterChainDefinitionMap.put("/druid/**", "anon");
|
||||||
filterChainDefinitionMap.put("/swagger-ui.html", "anon");
|
filterChainDefinitionMap.put("/swagger-ui.html", "anon");
|
||||||
@ -137,9 +138,7 @@ public class ShiroConfig {
|
|||||||
filterChainDefinitionMap.put("/webjars/**", "anon");
|
filterChainDefinitionMap.put("/webjars/**", "anon");
|
||||||
filterChainDefinitionMap.put("/v3/**", "anon");
|
filterChainDefinitionMap.put("/v3/**", "anon");
|
||||||
|
|
||||||
// update-begin--Author:sunjianlei Date:20210510 for:排除消息通告查看详情页面(用于第三方APP)
|
|
||||||
filterChainDefinitionMap.put("/sys/annountCement/show/**", "anon");
|
filterChainDefinitionMap.put("/sys/annountCement/show/**", "anon");
|
||||||
// update-end--Author:sunjianlei Date:20210510 for:排除消息通告查看详情页面(用于第三方APP)
|
|
||||||
|
|
||||||
//积木报表排除
|
//积木报表排除
|
||||||
filterChainDefinitionMap.put("/jmreport/**", "anon");
|
filterChainDefinitionMap.put("/jmreport/**", "anon");
|
||||||
@ -158,7 +157,7 @@ public class ShiroConfig {
|
|||||||
filterChainDefinitionMap.put("/drag/onlDragDatasetHead/getTotalDataByCompId", "anon");
|
filterChainDefinitionMap.put("/drag/onlDragDatasetHead/getTotalDataByCompId", "anon");
|
||||||
filterChainDefinitionMap.put("/drag/mock/json/**", "anon");
|
filterChainDefinitionMap.put("/drag/mock/json/**", "anon");
|
||||||
filterChainDefinitionMap.put("/drag/onlDragDatasetHead/getDictByCodes", "anon");
|
filterChainDefinitionMap.put("/drag/onlDragDatasetHead/getDictByCodes", "anon");
|
||||||
|
filterChainDefinitionMap.put("/drag/onlDragDatasetHead/queryAllById", "anon");
|
||||||
filterChainDefinitionMap.put("/jimubi/view", "anon");
|
filterChainDefinitionMap.put("/jimubi/view", "anon");
|
||||||
filterChainDefinitionMap.put("/jimubi/share/view/**", "anon");
|
filterChainDefinitionMap.put("/jimubi/share/view/**", "anon");
|
||||||
|
|
||||||
@ -191,6 +190,8 @@ public class ShiroConfig {
|
|||||||
// 企业微信证书排除
|
// 企业微信证书排除
|
||||||
filterChainDefinitionMap.put("/WW_verify*", "anon");
|
filterChainDefinitionMap.put("/WW_verify*", "anon");
|
||||||
|
|
||||||
|
filterChainDefinitionMap.put("/openapi/call/**", "anon");
|
||||||
|
|
||||||
// 添加自己的过滤器并且取名为jwt
|
// 添加自己的过滤器并且取名为jwt
|
||||||
Map<String, Filter> filterMap = new HashMap<String, Filter>(1);
|
Map<String, Filter> filterMap = new HashMap<String, Filter>(1);
|
||||||
//如果cloudServer为空 则说明是单体 需要加载跨域配置【微服务跨域切换】
|
//如果cloudServer为空 则说明是单体 需要加载跨域配置【微服务跨域切换】
|
||||||
@ -207,7 +208,6 @@ public class ShiroConfig {
|
|||||||
return shiroFilterFactoryBean;
|
return shiroFilterFactoryBean;
|
||||||
}
|
}
|
||||||
|
|
||||||
//update-begin---author:chenrui ---date:20240126 for:【QQYUN-7932】AI助手------------
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* spring过滤装饰器 <br/>
|
* spring过滤装饰器 <br/>
|
||||||
@ -223,21 +223,24 @@ public class ShiroConfig {
|
|||||||
FilterRegistrationBean registration = new FilterRegistrationBean();
|
FilterRegistrationBean registration = new FilterRegistrationBean();
|
||||||
registration.setFilter(new DelegatingFilterProxy("shiroFilterFactoryBean"));
|
registration.setFilter(new DelegatingFilterProxy("shiroFilterFactoryBean"));
|
||||||
registration.setEnabled(true);
|
registration.setEnabled(true);
|
||||||
//update-begin---author:chenrui ---date:20241202 for:[issues/7491]运行时间好长,效率慢 ------------
|
// 代码逻辑说明: [issues/7491]运行耗时长,效率慢
|
||||||
registration.addUrlPatterns("/test/ai/chat/send");
|
registration.addUrlPatterns("/test/ai/chat/send");
|
||||||
//update-end---author:chenrui ---date:20241202 for:[issues/7491]运行时间好长,效率慢 ------------
|
|
||||||
registration.addUrlPatterns("/airag/flow/run");
|
registration.addUrlPatterns("/airag/flow/run");
|
||||||
registration.addUrlPatterns("/airag/flow/debug");
|
registration.addUrlPatterns("/airag/flow/debug");
|
||||||
registration.addUrlPatterns("/airag/chat/send");
|
registration.addUrlPatterns("/airag/chat/send");
|
||||||
registration.addUrlPatterns("/airag/app/debug");
|
registration.addUrlPatterns("/airag/app/debug");
|
||||||
registration.addUrlPatterns("/airag/app/prompt/generate");
|
registration.addUrlPatterns("/airag/app/prompt/generate");
|
||||||
registration.addUrlPatterns("/airag/chat/receive/**");
|
registration.addUrlPatterns("/airag/chat/receive/**");
|
||||||
|
// 添加SSE接口的异步支持
|
||||||
|
registration.addUrlPatterns("/airag/extData/evaluator/debug");
|
||||||
|
registration.addUrlPatterns("/drag/onlDragDatasetHead/generateChartSse");
|
||||||
|
registration.addUrlPatterns("/drag/onlDragDatasetHead/updateChartOptSse");
|
||||||
|
registration.addUrlPatterns("/drag/onlDragDatasetHead/generateSqlSse");
|
||||||
//支持异步
|
//支持异步
|
||||||
registration.setAsyncSupported(true);
|
registration.setAsyncSupported(true);
|
||||||
registration.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ASYNC);
|
registration.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ASYNC);
|
||||||
return registration;
|
return registration;
|
||||||
}
|
}
|
||||||
//update-end---author:chenrui ---date:20240126 for:【QQYUN-7932】AI助手------------
|
|
||||||
|
|
||||||
@Bean("securityManager")
|
@Bean("securityManager")
|
||||||
public DefaultWebSecurityManager securityManager(ShiroRealm myRealm) {
|
public DefaultWebSecurityManager securityManager(ShiroRealm myRealm) {
|
||||||
@ -358,7 +361,6 @@ public class ShiroConfig {
|
|||||||
JedisCluster jedisCluster = new JedisCluster(portSet);
|
JedisCluster jedisCluster = new JedisCluster(portSet);
|
||||||
redisManager.setJedisCluster(jedisCluster);
|
redisManager.setJedisCluster(jedisCluster);
|
||||||
}
|
}
|
||||||
//update-end--Author:scott Date:20210531 for:修改集群模式下未设置redis密码的bug issues/I3QNIC
|
|
||||||
manager = redisManager;
|
manager = redisManager;
|
||||||
}
|
}
|
||||||
return manager;
|
return manager;
|
||||||
@ -375,7 +377,7 @@ public class ShiroConfig {
|
|||||||
mapping.setUrlPathHelper(new ShiroUrlPathHelper());
|
mapping.setUrlPathHelper(new ShiroUrlPathHelper());
|
||||||
return mapping;
|
return mapping;
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<String> rebuildUrl(String[] bases, String[] uris) {
|
private List<String> rebuildUrl(String[] bases, String[] uris) {
|
||||||
List<String> urls = new ArrayList<>();
|
List<String> urls = new ArrayList<>();
|
||||||
for (String base : bases) {
|
for (String base : bases) {
|
||||||
|
|||||||
@ -83,7 +83,7 @@ public class ShiroRealm extends AuthorizingRealm {
|
|||||||
Set<String> permissionSet = commonApi.queryUserAuths(userId);
|
Set<String> permissionSet = commonApi.queryUserAuths(userId);
|
||||||
info.addStringPermissions(permissionSet);
|
info.addStringPermissions(permissionSet);
|
||||||
//System.out.println(permissionSet);
|
//System.out.println(permissionSet);
|
||||||
log.info("===============Shiro权限认证成功==============");
|
log.debug("===============Shiro权限认证成功==============");
|
||||||
return info;
|
return info;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -110,8 +110,8 @@ public class ShiroRealm extends AuthorizingRealm {
|
|||||||
loginUser = this.checkUserTokenIsEffect(token);
|
loginUser = this.checkUserTokenIsEffect(token);
|
||||||
} catch (AuthenticationException e) {
|
} catch (AuthenticationException e) {
|
||||||
log.error("—————校验 check token 失败——————————"+ e.getMessage(), e);
|
log.error("—————校验 check token 失败——————————"+ e.getMessage(), e);
|
||||||
JwtUtil.responseError(SpringContextUtils.getHttpServletResponse(),401,e.getMessage());
|
// 重新抛出异常,让JwtFilter统一处理,避免返回两次错误响应
|
||||||
return null;
|
throw e;
|
||||||
}
|
}
|
||||||
return new SimpleAuthenticationInfo(loginUser, token, getName());
|
return new SimpleAuthenticationInfo(loginUser, token, getName());
|
||||||
}
|
}
|
||||||
@ -141,9 +141,11 @@ public class ShiroRealm extends AuthorizingRealm {
|
|||||||
}
|
}
|
||||||
// 校验token是否超时失效 & 或者账号密码是否错误
|
// 校验token是否超时失效 & 或者账号密码是否错误
|
||||||
if (!jwtTokenRefresh(token, username, loginUser.getPassword())) {
|
if (!jwtTokenRefresh(token, username, loginUser.getPassword())) {
|
||||||
throw new AuthenticationException(CommonConstant.TOKEN_IS_INVALID_MSG);
|
// 用户登录Token过期提示信息
|
||||||
|
String userLoginTokenErrorMsg = oConvertUtils.getString(redisUtil.get(CommonConstant.PREFIX_USER_TOKEN_ERROR_MSG + token));
|
||||||
|
throw new AuthenticationException(oConvertUtils.isEmpty(userLoginTokenErrorMsg)? CommonConstant.TOKEN_IS_INVALID_MSG: userLoginTokenErrorMsg);
|
||||||
}
|
}
|
||||||
//update-begin-author:taoyan date:20210609 for:校验用户的tenant_id和前端传过来的是否一致
|
// 代码逻辑说明: 校验用户的tenant_id和前端传过来的是否一致
|
||||||
String userTenantIds = loginUser.getRelTenantIds();
|
String userTenantIds = loginUser.getRelTenantIds();
|
||||||
if(MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL && oConvertUtils.isNotEmpty(userTenantIds)){
|
if(MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL && oConvertUtils.isNotEmpty(userTenantIds)){
|
||||||
String contextTenantId = TenantContext.getTenant();
|
String contextTenantId = TenantContext.getTenant();
|
||||||
@ -152,7 +154,7 @@ public class ShiroRealm extends AuthorizingRealm {
|
|||||||
//登录用户无租户,前端header中租户ID值为 0
|
//登录用户无租户,前端header中租户ID值为 0
|
||||||
String str ="0";
|
String str ="0";
|
||||||
if(oConvertUtils.isNotEmpty(contextTenantId) && !str.equals(contextTenantId)){
|
if(oConvertUtils.isNotEmpty(contextTenantId) && !str.equals(contextTenantId)){
|
||||||
//update-begin-author:taoyan date:20211227 for: /issues/I4O14W 用户租户信息变更判断漏洞
|
// 代码逻辑说明: /issues/I4O14W 用户租户信息变更判断漏洞
|
||||||
String[] arr = userTenantIds.split(",");
|
String[] arr = userTenantIds.split(",");
|
||||||
if(!oConvertUtils.isIn(contextTenantId, arr)){
|
if(!oConvertUtils.isIn(contextTenantId, arr)){
|
||||||
boolean isAuthorization = false;
|
boolean isAuthorization = false;
|
||||||
@ -177,10 +179,8 @@ public class ShiroRealm extends AuthorizingRealm {
|
|||||||
}
|
}
|
||||||
//*********************************************
|
//*********************************************
|
||||||
}
|
}
|
||||||
//update-end-author:taoyan date:20211227 for: /issues/I4O14W 用户租户信息变更判断漏洞
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//update-end-author:taoyan date:20210609 for:校验用户的tenant_id和前端传过来的是否一致
|
|
||||||
return loginUser;
|
return loginUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -202,19 +202,22 @@ public class ShiroRealm extends AuthorizingRealm {
|
|||||||
if (oConvertUtils.isNotEmpty(cacheToken)) {
|
if (oConvertUtils.isNotEmpty(cacheToken)) {
|
||||||
// 校验token有效性
|
// 校验token有效性
|
||||||
if (!JwtUtil.verify(cacheToken, userName, passWord)) {
|
if (!JwtUtil.verify(cacheToken, userName, passWord)) {
|
||||||
String newAuthorization = JwtUtil.sign(userName, passWord);
|
// 从token中解析客户端类型,保持续期时使用相同的客户端类型
|
||||||
// 设置超时时间
|
String clientType = JwtUtil.getClientType(token);
|
||||||
|
String newAuthorization = JwtUtil.sign(userName, passWord, clientType);
|
||||||
|
// 根据客户端类型设置对应的缓存有效时间
|
||||||
|
long expireTime = CommonConstant.CLIENT_TYPE_APP.equalsIgnoreCase(clientType)
|
||||||
|
? JwtUtil.APP_EXPIRE_TIME * 2 / 1000
|
||||||
|
: JwtUtil.EXPIRE_TIME * 2 / 1000;
|
||||||
redisUtil.set(CommonConstant.PREFIX_USER_TOKEN + token, newAuthorization);
|
redisUtil.set(CommonConstant.PREFIX_USER_TOKEN + token, newAuthorization);
|
||||||
redisUtil.expire(CommonConstant.PREFIX_USER_TOKEN + token, JwtUtil.EXPIRE_TIME *2 / 1000);
|
redisUtil.expire(CommonConstant.PREFIX_USER_TOKEN + token, expireTime);
|
||||||
log.debug("——————————用户在线操作,更新token保证不掉线—————————jwtTokenRefresh——————— "+ token);
|
log.debug("——————————用户在线操作,更新token保证不掉线—————————jwtTokenRefresh——————— "+ token);
|
||||||
}
|
}
|
||||||
//update-begin--Author:scott Date:20191005 for:解决每次请求,都重写redis中 token缓存问题
|
|
||||||
// else {
|
// else {
|
||||||
// // 设置超时时间
|
// // 设置超时时间
|
||||||
// redisUtil.set(CommonConstant.PREFIX_USER_TOKEN + token, cacheToken);
|
// redisUtil.set(CommonConstant.PREFIX_USER_TOKEN + token, cacheToken);
|
||||||
// redisUtil.expire(CommonConstant.PREFIX_USER_TOKEN + token, JwtUtil.EXPIRE_TIME / 1000);
|
// redisUtil.expire(CommonConstant.PREFIX_USER_TOKEN + token, JwtUtil.EXPIRE_TIME / 1000);
|
||||||
// }
|
// }
|
||||||
//update-end--Author:scott Date:20191005 for:解决每次请求,都重写redis中 token缓存问题
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -230,8 +233,7 @@ public class ShiroRealm extends AuthorizingRealm {
|
|||||||
@Override
|
@Override
|
||||||
public void clearCache(PrincipalCollection principals) {
|
public void clearCache(PrincipalCollection principals) {
|
||||||
super.clearCache(principals);
|
super.clearCache(principals);
|
||||||
//update-begin---author:scott ---date::2024-06-18 for:【TV360X-1320】分配权限必须退出重新登录才生效,造成很多用户困扰---
|
// 代码逻辑说明: 【TV360X-1320】分配权限必须退出重新登录才生效,造成很多用户困扰---
|
||||||
super.clearCachedAuthorizationInfo(principals);
|
super.clearCachedAuthorizationInfo(principals);
|
||||||
//update-end---author:scott ---date::2024-06-18 for:【TV360X-1320】分配权限必须退出重新登录才生效,造成很多用户困扰---
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -56,9 +56,13 @@ public class JwtFilter extends BasicHttpAuthenticationFilter {
|
|||||||
executeLogin(request, response);
|
executeLogin(request, response);
|
||||||
return true;
|
return true;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
JwtUtil.responseError((HttpServletResponse)response,401,CommonConstant.TOKEN_IS_INVALID_MSG);
|
// 使用异常中的具体错误信息,保留"不允许同一账号多地同时登录"等具体提示
|
||||||
|
String errorMsg = e.getMessage();
|
||||||
|
if (oConvertUtils.isEmpty(errorMsg)) {
|
||||||
|
errorMsg = CommonConstant.TOKEN_IS_INVALID_MSG;
|
||||||
|
}
|
||||||
|
JwtUtil.responseError((HttpServletResponse)response, 401, errorMsg);
|
||||||
return false;
|
return false;
|
||||||
//throw new AuthenticationException("Token失效,请重新登录", e);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -69,11 +73,10 @@ public class JwtFilter extends BasicHttpAuthenticationFilter {
|
|||||||
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
|
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
|
||||||
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
|
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
|
||||||
String token = httpServletRequest.getHeader(CommonConstant.X_ACCESS_TOKEN);
|
String token = httpServletRequest.getHeader(CommonConstant.X_ACCESS_TOKEN);
|
||||||
// update-begin--Author:lvdandan Date:20210105 for:JT-355 OA聊天添加token验证,获取token参数
|
// 代码逻辑说明: JT-355 OA聊天添加token验证,获取token参数
|
||||||
if (oConvertUtils.isEmpty(token)) {
|
if (oConvertUtils.isEmpty(token)) {
|
||||||
token = httpServletRequest.getParameter("token");
|
token = httpServletRequest.getParameter("token");
|
||||||
}
|
}
|
||||||
// update-end--Author:lvdandan Date:20210105 for:JT-355 OA聊天添加token验证,获取token参数
|
|
||||||
|
|
||||||
JwtToken jwtToken = new JwtToken(token);
|
JwtToken jwtToken = new JwtToken(token);
|
||||||
// 提交给realm进行登入,如果错误他会抛出异常并被捕获
|
// 提交给realm进行登入,如果错误他会抛出异常并被捕获
|
||||||
@ -106,10 +109,9 @@ public class JwtFilter extends BasicHttpAuthenticationFilter {
|
|||||||
httpServletResponse.setStatus(HttpStatus.OK.value());
|
httpServletResponse.setStatus(HttpStatus.OK.value());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
//update-begin-author:taoyan date:20200708 for:多租户用到
|
// 代码逻辑说明: 多租户用到
|
||||||
String tenantId = httpServletRequest.getHeader(CommonConstant.TENANT_ID);
|
String tenantId = httpServletRequest.getHeader(CommonConstant.TENANT_ID);
|
||||||
TenantContext.setTenant(tenantId);
|
TenantContext.setTenant(tenantId);
|
||||||
//update-end-author:taoyan date:20200708 for:多租户用到
|
|
||||||
|
|
||||||
return super.preHandle(request, response);
|
return super.preHandle(request, response);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import lombok.AllArgsConstructor;
|
|||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.jeecg.config.shiro.IgnoreAuth;
|
import org.jeecg.config.shiro.IgnoreAuth;
|
||||||
import org.springframework.beans.factory.InitializingBean;
|
import org.springframework.beans.factory.InitializingBean;
|
||||||
|
import org.springframework.context.annotation.Lazy;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.util.CollectionUtils;
|
import org.springframework.util.CollectionUtils;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
@ -20,6 +21,7 @@ import java.util.stream.Collectors;
|
|||||||
* @date 2024/4/18 11:35
|
* @date 2024/4/18 11:35
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
|
@Lazy(false)
|
||||||
@Component
|
@Component
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
public class IgnoreAuthPostProcessor implements InitializingBean {
|
public class IgnoreAuthPostProcessor implements InitializingBean {
|
||||||
@ -33,10 +35,15 @@ public class IgnoreAuthPostProcessor implements InitializingBean {
|
|||||||
long startTime = System.currentTimeMillis();
|
long startTime = System.currentTimeMillis();
|
||||||
|
|
||||||
List<String> ignoreAuthUrls = new ArrayList<>();
|
List<String> ignoreAuthUrls = new ArrayList<>();
|
||||||
Set<Class<?>> restControllers = requestMappingHandlerMapping.getHandlerMethods().values().stream().map(HandlerMethod::getBeanType).collect(Collectors.toSet());
|
|
||||||
for (Class<?> restController : restControllers) {
|
// 优化:直接从HandlerMethod过滤,避免重复扫描
|
||||||
ignoreAuthUrls.addAll(postProcessRestController(restController));
|
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);
|
log.info("Init Token ignoreAuthUrls Config [ 集合 ] :{}", ignoreAuthUrls);
|
||||||
if (!CollectionUtils.isEmpty(ignoreAuthUrls)) {
|
if (!CollectionUtils.isEmpty(ignoreAuthUrls)) {
|
||||||
@ -46,44 +53,30 @@ public class IgnoreAuthPostProcessor implements InitializingBean {
|
|||||||
// 计算方法的耗时
|
// 计算方法的耗时
|
||||||
long endTime = System.currentTimeMillis();
|
long endTime = System.currentTimeMillis();
|
||||||
long elapsedTime = endTime - startTime;
|
long elapsedTime = endTime - startTime;
|
||||||
log.info("Init Token ignoreAuthUrls Config [ 耗时 ] :" + elapsedTime + "毫秒");
|
log.info("Init Token ignoreAuthUrls Config [ 耗时 ] :" + elapsedTime + "ms");
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<String> postProcessRestController(Class<?> clazz) {
|
// 优化:新方法处理单个@IgnoreAuth方法,减少重复注解检查
|
||||||
List<String> ignoreAuthUrls = new ArrayList<>();
|
private List<String> processIgnoreAuthMethod(Class<?> clazz, Method method) {
|
||||||
RequestMapping base = clazz.getAnnotation(RequestMapping.class);
|
RequestMapping base = clazz.getAnnotation(RequestMapping.class);
|
||||||
String[] baseUrl = Objects.nonNull(base) ? base.value() : new String[]{};
|
String[] baseUrl = Objects.nonNull(base) ? base.value() : new String[]{};
|
||||||
Method[] methods = clazz.getDeclaredMethods();
|
|
||||||
|
String[] uri = null;
|
||||||
for (Method method : methods) {
|
if (method.isAnnotationPresent(RequestMapping.class)) {
|
||||||
if (method.isAnnotationPresent(IgnoreAuth.class) && method.isAnnotationPresent(RequestMapping.class)) {
|
uri = method.getAnnotation(RequestMapping.class).value();
|
||||||
RequestMapping requestMapping = method.getAnnotation(RequestMapping.class);
|
} else if (method.isAnnotationPresent(GetMapping.class)) {
|
||||||
String[] uri = requestMapping.value();
|
uri = method.getAnnotation(GetMapping.class).value();
|
||||||
ignoreAuthUrls.addAll(rebuildUrl(baseUrl, uri));
|
} else if (method.isAnnotationPresent(PostMapping.class)) {
|
||||||
} else if (method.isAnnotationPresent(IgnoreAuth.class) && method.isAnnotationPresent(GetMapping.class)) {
|
uri = method.getAnnotation(PostMapping.class).value();
|
||||||
GetMapping requestMapping = method.getAnnotation(GetMapping.class);
|
} else if (method.isAnnotationPresent(PutMapping.class)) {
|
||||||
String[] uri = requestMapping.value();
|
uri = method.getAnnotation(PutMapping.class).value();
|
||||||
ignoreAuthUrls.addAll(rebuildUrl(baseUrl, uri));
|
} else if (method.isAnnotationPresent(DeleteMapping.class)) {
|
||||||
} else if (method.isAnnotationPresent(IgnoreAuth.class) && method.isAnnotationPresent(PostMapping.class)) {
|
uri = method.getAnnotation(DeleteMapping.class).value();
|
||||||
PostMapping requestMapping = method.getAnnotation(PostMapping.class);
|
} else if (method.isAnnotationPresent(PatchMapping.class)) {
|
||||||
String[] uri = requestMapping.value();
|
uri = method.getAnnotation(PatchMapping.class).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;
|
return uri != null ? rebuildUrl(baseUrl, uri) : Collections.emptyList();
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<String> rebuildUrl(String[] bases, String[] uris) {
|
private List<String> rebuildUrl(String[] bases, String[] uris) {
|
||||||
|
|||||||
@ -0,0 +1,32 @@
|
|||||||
|
package org.jeecg.config.sign.annotation;
|
||||||
|
|
||||||
|
import java.lang.annotation.ElementType;
|
||||||
|
import java.lang.annotation.Retention;
|
||||||
|
import java.lang.annotation.RetentionPolicy;
|
||||||
|
import java.lang.annotation.Target;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 签名校验注解
|
||||||
|
* 用于方法级别的签名验证,功能等同于yml中的jeecg.signUrls配置
|
||||||
|
* 参考DragSignatureAspect的设计思路,使用AOP切面实现
|
||||||
|
*
|
||||||
|
* @author GitHub Copilot
|
||||||
|
* @since 2025-12-15
|
||||||
|
*/
|
||||||
|
@Target(ElementType.METHOD)
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
public @interface SignatureCheck {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否启用签名校验
|
||||||
|
* @return true-启用(默认), false-禁用
|
||||||
|
*/
|
||||||
|
boolean enabled() default true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 签名校验失败时的错误消息
|
||||||
|
* @return 错误消息
|
||||||
|
*/
|
||||||
|
String errorMessage() default "Sign签名校验失败!";
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,112 @@
|
|||||||
|
package org.jeecg.config.sign.aspect;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.aspectj.lang.JoinPoint;
|
||||||
|
import org.aspectj.lang.annotation.Aspect;
|
||||||
|
import org.aspectj.lang.annotation.Before;
|
||||||
|
import org.aspectj.lang.annotation.Pointcut;
|
||||||
|
import org.aspectj.lang.reflect.MethodSignature;
|
||||||
|
import org.jeecg.config.sign.annotation.SignatureCheck;
|
||||||
|
import org.jeecg.config.sign.interceptor.SignAuthInterceptor;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.web.context.request.RequestContextHolder;
|
||||||
|
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||||
|
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
|
||||||
|
import java.lang.annotation.Annotation;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 基于AOP的签名验证切面
|
||||||
|
* 复用SignAuthInterceptor的成熟签名验证逻辑
|
||||||
|
*
|
||||||
|
* @author GitHub Copilot
|
||||||
|
* @since 2025-12-15
|
||||||
|
*/
|
||||||
|
@Aspect
|
||||||
|
@Slf4j
|
||||||
|
@Component("signatureCheckAspect")
|
||||||
|
public class SignatureCheckAspect {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 复用SignAuthInterceptor的签名验证逻辑
|
||||||
|
*/
|
||||||
|
private final SignAuthInterceptor signAuthInterceptor = new SignAuthInterceptor();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验签切点:拦截所有标记了@SignatureCheck注解的方法
|
||||||
|
*/
|
||||||
|
@Pointcut("@annotation(org.jeecg.config.sign.annotation.SignatureCheck)")
|
||||||
|
private void signatureCheckPointCut() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 开始验签
|
||||||
|
*/
|
||||||
|
@Before("signatureCheckPointCut()")
|
||||||
|
public void doSignatureValidation(JoinPoint point) throws Exception {
|
||||||
|
// 获取方法上的注解
|
||||||
|
MethodSignature signature = (MethodSignature) point.getSignature();
|
||||||
|
Method method = signature.getMethod();
|
||||||
|
SignatureCheck signatureCheck = method.getAnnotation(SignatureCheck.class);
|
||||||
|
|
||||||
|
log.info("AOP签名验证: {}.{}", method.getDeclaringClass().getSimpleName(), method.getName());
|
||||||
|
|
||||||
|
// 如果注解被禁用,直接返回
|
||||||
|
if (!signatureCheck.enabled()) {
|
||||||
|
log.info("签名验证已禁用,跳过");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// update-begin---author:sjlei---date:20260115 for: 查找带有@RequestBody注解的参数,解决签名校验时读取请求体为空的问题
|
||||||
|
Object bodyParam = null;
|
||||||
|
Object[] args = point.getArgs();
|
||||||
|
Annotation[][] parameterAnnotations = method.getParameterAnnotations();
|
||||||
|
for (int i = 0; i < args.length; i++) {
|
||||||
|
Object arg = args[i];
|
||||||
|
Annotation[] annotations = parameterAnnotations[i];
|
||||||
|
boolean hasRequestBodyAnnotation = Arrays.stream(annotations).anyMatch(annotation -> annotation.annotationType().equals(RequestBody.class));
|
||||||
|
if (hasRequestBodyAnnotation) {
|
||||||
|
// 捕获携带@RequestBody注解的参数,供签名校验使用
|
||||||
|
bodyParam = arg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// update-end-----author:sjlei---date:20260115 for: 查找带有@RequestBody注解的参数,解决签名校验时读取请求体为空的问题
|
||||||
|
|
||||||
|
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
||||||
|
if (attributes == null) {
|
||||||
|
log.error("无法获取请求上下文");
|
||||||
|
throw new IllegalArgumentException("无法获取请求上下文");
|
||||||
|
}
|
||||||
|
|
||||||
|
HttpServletRequest request = attributes.getRequest();
|
||||||
|
log.info("X-SIGN: {}, X-TIMESTAMP: {}", request.getHeader("X-SIGN"), request.getHeader("X-TIMESTAMP"));
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 直接调用SignAuthInterceptor的验证逻辑
|
||||||
|
signAuthInterceptor.validateSignature(request, bodyParam);
|
||||||
|
log.info("AOP签名验证通过");
|
||||||
|
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
// 使用注解中配置的错误消息,或者保留原始错误消息
|
||||||
|
String errorMessage = signatureCheck.errorMessage();
|
||||||
|
log.error("AOP签名验证失败: {}", e.getMessage());
|
||||||
|
|
||||||
|
if ("Sign签名校验失败!".equals(errorMessage)) {
|
||||||
|
// 如果是默认错误消息,使用原始的详细错误信息
|
||||||
|
throw e;
|
||||||
|
} else {
|
||||||
|
// 如果是自定义错误消息,使用自定义消息
|
||||||
|
throw new IllegalArgumentException(errorMessage, e);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
// 包装其他异常
|
||||||
|
String errorMessage = signatureCheck.errorMessage();
|
||||||
|
log.error("AOP签名验证异常: {}", e.getMessage());
|
||||||
|
throw new IllegalArgumentException(errorMessage, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,6 +1,7 @@
|
|||||||
package org.jeecg.config.sign.interceptor;
|
package org.jeecg.config.sign.interceptor;
|
||||||
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.jeecg.common.constant.TenantConstant;
|
||||||
import org.jeecg.common.util.PathMatcherUtil;
|
import org.jeecg.common.util.PathMatcherUtil;
|
||||||
import org.jeecg.config.JeecgBaseConfig;
|
import org.jeecg.config.JeecgBaseConfig;
|
||||||
import org.jeecg.config.filter.RequestBodyReserveFilter;
|
import org.jeecg.config.filter.RequestBodyReserveFilter;
|
||||||
@ -41,7 +42,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();
|
||||||
@ -64,8 +65,9 @@ public class SignAuthConfiguration implements WebMvcConfigurer {
|
|||||||
//------------------------------------------------------------
|
//------------------------------------------------------------
|
||||||
// 建议此处只添加post请求地址而不是所有的都需要走过滤器
|
// 建议此处只添加post请求地址而不是所有的都需要走过滤器
|
||||||
registration.addUrlPatterns(signUrlsArray);
|
registration.addUrlPatterns(signUrlsArray);
|
||||||
|
// 增加注解签名请求
|
||||||
|
registration.addUrlPatterns(TenantConstant.SIGNATURE_CHECK_POST_URL);
|
||||||
return registration;
|
return registration;
|
||||||
}
|
}
|
||||||
//update-end-author:taoyan date:20220427 for: issues/I53J5E post请求X_SIGN签名拦截校验后报错, request body 为空
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -33,63 +33,104 @@ 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.info("签名拦截器 Interceptor request URI = " + request.getRequestURI());
|
||||||
HttpServletRequest requestWrapper = new BodyReaderHttpServletRequestWrapper(request);
|
|
||||||
//获取全部参数(包括URL和body上的)
|
|
||||||
SortedMap<String, String> allParams = HttpUtils.getAllParams(requestWrapper);
|
|
||||||
//对参数进行签名验证
|
|
||||||
String headerSign = request.getHeader(CommonConstant.X_SIGN);
|
|
||||||
String xTimestamp = request.getHeader(CommonConstant.X_TIMESTAMP);
|
|
||||||
|
|
||||||
if(oConvertUtils.isEmpty(xTimestamp)){
|
try {
|
||||||
Result<?> result = Result.error("Sign签名校验失败,时间戳为空!");
|
// 调用验证逻辑
|
||||||
log.error("Sign 签名校验失败!Header xTimestamp 为空");
|
validateSignature(request);
|
||||||
//校验失败返回前端
|
|
||||||
response.setCharacterEncoding("UTF-8");
|
|
||||||
response.setContentType("application/json; charset=utf-8");
|
|
||||||
PrintWriter out = response.getWriter();
|
|
||||||
out.print(JSON.toJSON(result));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
//客户端时间
|
|
||||||
Long clientTimestamp = Long.parseLong(xTimestamp);
|
|
||||||
|
|
||||||
int length = 14;
|
|
||||||
int length1000 = 1000;
|
|
||||||
//1.校验签名时间(兼容X_TIMESTAMP的新老格式)
|
|
||||||
if (xTimestamp.length() == length) {
|
|
||||||
//a. X_TIMESTAMP格式是 yyyyMMddHHmmss (例子:20220308152143)
|
|
||||||
if ((DateUtils.getCurrentTimestamp() - clientTimestamp) > MAX_EXPIRE) {
|
|
||||||
log.error("签名验证失败:X-TIMESTAMP已过期,注意系统时间和服务器时间是否有误差!");
|
|
||||||
throw new IllegalArgumentException("签名验证失败:X-TIMESTAMP已过期");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
//b. X_TIMESTAMP格式是 时间戳 (例子:1646552406000)
|
|
||||||
if ((System.currentTimeMillis() - clientTimestamp) > (MAX_EXPIRE * length1000)) {
|
|
||||||
log.error("签名验证失败:X-TIMESTAMP已过期,注意系统时间和服务器时间是否有误差!");
|
|
||||||
throw new IllegalArgumentException("签名验证失败:X-TIMESTAMP已过期");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//2.校验签名
|
|
||||||
boolean isSigned = SignUtil.verifySign(allParams,headerSign);
|
|
||||||
|
|
||||||
if (isSigned) {
|
|
||||||
log.debug("Sign 签名通过!Header Sign : {}",headerSign);
|
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} catch (IllegalArgumentException e) {
|
||||||
log.info("sign allParams: {}", allParams);
|
// 验证失败,返回错误响应
|
||||||
log.error("request URI = " + request.getRequestURI());
|
log.error("Sign 签名校验失败!{}", e.getMessage());
|
||||||
log.error("Sign 签名校验失败!Header Sign : {}",headerSign);
|
|
||||||
//校验失败返回前端
|
|
||||||
response.setCharacterEncoding("UTF-8");
|
response.setCharacterEncoding("UTF-8");
|
||||||
response.setContentType("application/json; charset=utf-8");
|
response.setContentType("application/json; charset=utf-8");
|
||||||
PrintWriter out = response.getWriter();
|
PrintWriter out = response.getWriter();
|
||||||
Result<?> result = Result.error("Sign签名校验失败!");
|
Result<?> result = Result.error(e.getMessage());
|
||||||
out.print(JSON.toJSON(result));
|
out.print(JSON.toJSON(result));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 签名验证核心逻辑
|
||||||
|
* 提取出来供AOP切面复用
|
||||||
|
* @param request HTTP请求
|
||||||
|
* @throws IllegalArgumentException 验证失败时抛出异常
|
||||||
|
*/
|
||||||
|
public void validateSignature(HttpServletRequest request) throws IllegalArgumentException {
|
||||||
|
validateSignature(request, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 签名验证核心逻辑
|
||||||
|
* 提取出来供AOP切面复用
|
||||||
|
* @param request HTTP请求
|
||||||
|
* @throws IllegalArgumentException 验证失败时抛出异常
|
||||||
|
*/
|
||||||
|
public void validateSignature(HttpServletRequest request, Object bodyParam) throws IllegalArgumentException {
|
||||||
|
try {
|
||||||
|
log.debug("开始签名验证: {} {}", request.getMethod(), request.getRequestURI());
|
||||||
|
|
||||||
|
HttpServletRequest requestWrapper = new BodyReaderHttpServletRequestWrapper(request);
|
||||||
|
//获取全部参数(包括URL和body上的)
|
||||||
|
SortedMap<String, String> allParams = HttpUtils.getAllParams(requestWrapper, bodyParam);
|
||||||
|
log.debug("提取参数: {}", allParams);
|
||||||
|
|
||||||
|
//对参数进行签名验证
|
||||||
|
String headerSign = request.getHeader(CommonConstant.X_SIGN);
|
||||||
|
String xTimestamp = request.getHeader(CommonConstant.X_TIMESTAMP);
|
||||||
|
|
||||||
|
if(oConvertUtils.isEmpty(xTimestamp)){
|
||||||
|
log.error("Sign签名校验失败,时间戳为空!");
|
||||||
|
throw new IllegalArgumentException("Sign签名校验失败,请求参数不完整!");
|
||||||
|
}
|
||||||
|
|
||||||
|
//客户端时间
|
||||||
|
Long clientTimestamp = Long.parseLong(xTimestamp);
|
||||||
|
|
||||||
|
int length = 14;
|
||||||
|
int length1000 = 1000;
|
||||||
|
//1.校验签名时间(兼容X_TIMESTAMP的新老格式)
|
||||||
|
if (xTimestamp.length() == length) {
|
||||||
|
//a. X_TIMESTAMP格式是 yyyyMMddHHmmss (例子:20220308152143)
|
||||||
|
long currentTimestamp = DateUtils.getCurrentTimestamp();
|
||||||
|
long timeDiff = currentTimestamp - clientTimestamp;
|
||||||
|
log.debug("时间戳验证(yyyyMMddHHmmss): 时间差{}秒", timeDiff);
|
||||||
|
|
||||||
|
if (timeDiff > MAX_EXPIRE) {
|
||||||
|
log.error("时间戳已过期: {}秒 > {}秒", timeDiff, MAX_EXPIRE);
|
||||||
|
throw new IllegalArgumentException("签名验证失败,请求时效性验证失败!");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
//b. X_TIMESTAMP格式是 时间戳 (例子:1646552406000)
|
||||||
|
long currentTime = System.currentTimeMillis();
|
||||||
|
long timeDiff = currentTime - clientTimestamp;
|
||||||
|
long maxExpireMs = MAX_EXPIRE * length1000;
|
||||||
|
log.debug("时间戳验证(Unix): 时间差{}ms", timeDiff);
|
||||||
|
|
||||||
|
if (timeDiff > maxExpireMs) {
|
||||||
|
log.error("时间戳已过期: {}ms > {}ms", timeDiff, maxExpireMs);
|
||||||
|
throw new IllegalArgumentException("签名验证失败,请求时效性验证失败!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//2.校验签名
|
||||||
|
boolean isSigned = SignUtil.verifySign(allParams,headerSign);
|
||||||
|
|
||||||
|
if (isSigned) {
|
||||||
|
log.debug("签名验证通过");
|
||||||
|
} else {
|
||||||
|
log.error("签名验证失败, 参数: {}", allParams);
|
||||||
|
throw new IllegalArgumentException("Sign签名校验失败!");
|
||||||
|
}
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
// 重新抛出签名验证异常
|
||||||
|
throw e;
|
||||||
|
} catch (Exception e) {
|
||||||
|
// 包装其他异常(如IOException)
|
||||||
|
log.error("签名验证异常: {}", e.getMessage());
|
||||||
|
throw new IllegalArgumentException("Sign签名校验失败:" + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -35,25 +35,25 @@ public class HttpUtils {
|
|||||||
* @date 20210621
|
* @date 20210621
|
||||||
* @param request
|
* @param request
|
||||||
*/
|
*/
|
||||||
public static SortedMap<String, String> getAllParams(HttpServletRequest request) throws IOException {
|
public static SortedMap<String, String> getAllParams(HttpServletRequest request, Object bodyParam) throws IOException {
|
||||||
|
|
||||||
SortedMap<String, String> result = new TreeMap<>();
|
SortedMap<String, String> result = new TreeMap<>();
|
||||||
// 获取URL上最后带逗号的参数变量 sys/dict/getDictItems/sys_user,realname,username
|
// 获取URL上最后带逗号的参数变量 sys/dict/getDictItems/sys_user,realname,username
|
||||||
String pathVariable = request.getRequestURI().substring(request.getRequestURI().lastIndexOf("/") + 1);
|
String pathVariable = request.getRequestURI().substring(request.getRequestURI().lastIndexOf("/") + 1);
|
||||||
if (pathVariable.contains(SymbolConstant.COMMA)) {
|
if (pathVariable.contains(SymbolConstant.COMMA)) {
|
||||||
log.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上的参数
|
||||||
@ -65,7 +65,13 @@ public class HttpUtils {
|
|||||||
Map<String, String> allRequestParam = new HashMap<>(16);
|
Map<String, String> allRequestParam = new HashMap<>(16);
|
||||||
// get请求不需要拿body参数
|
// get请求不需要拿body参数
|
||||||
if (!HttpMethod.GET.name().equals(request.getMethod())) {
|
if (!HttpMethod.GET.name().equals(request.getMethod())) {
|
||||||
allRequestParam = getAllRequestParam(request);
|
if (bodyParam != null) {
|
||||||
|
// update-begin---author:sjlei---date:20260115 for: 解决签名校验时读取请求体为空的问题
|
||||||
|
allRequestParam = JSONObject.parseObject(JSONObject.toJSONString(bodyParam), Map.class);
|
||||||
|
// update-end-----author:sjlei---date:20260115 for: 解决签名校验时读取请求体为空的问题
|
||||||
|
} else {
|
||||||
|
allRequestParam = getAllRequestParam(request);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// 将URL的参数和body参数进行合并
|
// 将URL的参数和body参数进行合并
|
||||||
if (allRequestParam != null) {
|
if (allRequestParam != null) {
|
||||||
@ -91,15 +97,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 +180,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 +207,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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,7 +11,12 @@ import org.springframework.util.DigestUtils;
|
|||||||
import org.springframework.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
import java.io.UnsupportedEncodingException;
|
import java.io.UnsupportedEncodingException;
|
||||||
|
import java.net.URLDecoder;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.SortedMap;
|
import java.util.SortedMap;
|
||||||
|
import java.util.TreeMap;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 签名工具类
|
* 签名工具类
|
||||||
@ -34,7 +39,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,14 +52,9 @@ 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);
|
String signatureSecret = SignUtil.getSignatureSecret();
|
||||||
String signatureSecret = jeecgBaseConfig.getSignatureSecret();
|
|
||||||
String curlyBracket = SymbolConstant.DOLLAR + SymbolConstant.LEFT_CURLY_BRACKET;
|
|
||||||
if(oConvertUtils.isEmpty(signatureSecret) || signatureSecret.contains(curlyBracket)){
|
|
||||||
throw new JeecgBootException("签名密钥 ${jeecg.signatureSecret} 缺少配置 !!");
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
//【issues/I484RW】2.4.6部署后,下拉搜索框提示“sign签名检验失败”
|
//【issues/I484RW】2.4.6部署后,下拉搜索框提示“sign签名检验失败”
|
||||||
return DigestUtils.md5DigestAsHex((paramsJsonStr + signatureSecret).getBytes("UTF-8")).toUpperCase();
|
return DigestUtils.md5DigestAsHex((paramsJsonStr + signatureSecret).getBytes("UTF-8")).toUpperCase();
|
||||||
@ -63,4 +63,129 @@ public class SignUtil {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
/**
|
||||||
|
* 通过前端签名算法生成签名
|
||||||
|
*
|
||||||
|
* @param url 请求的完整URL(包含查询参数)
|
||||||
|
* @param requestParams 使用 @RequestParam 获取的参数集合
|
||||||
|
* @param requestBodyParams 使用 @RequestBody 获取的参数集合
|
||||||
|
* @return 计算得到的签名(大写MD5),若参数不足返回 null
|
||||||
|
*/
|
||||||
|
public static String generateRequestSign(String url, Map<String, Object> requestParams, Map<String, Object> requestBodyParams) {
|
||||||
|
if (oConvertUtils.isEmpty(url)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// 解析URL上的查询参数与路径变量
|
||||||
|
Map<String, String> urlParams = parseQueryString(url);
|
||||||
|
// 合并URL参数与@RequestParam参数,确保数值和布尔类型转换为字符串
|
||||||
|
Map<String, String> mergedParams = mergeObject(urlParams, requestParams);
|
||||||
|
// 按需合并@RequestBody参数
|
||||||
|
if (requestBodyParams != null && !requestBodyParams.isEmpty()) {
|
||||||
|
mergedParams = mergeObject(mergedParams, requestBodyParams);
|
||||||
|
}
|
||||||
|
// 按键名升序排序,保持与前端一致的签名顺序
|
||||||
|
SortedMap<String, String> sortedParams = new TreeMap<>(mergedParams);
|
||||||
|
// 去除时间戳字段,避免参与签名
|
||||||
|
sortedParams.remove("_t");
|
||||||
|
// 序列化为JSON字符串
|
||||||
|
String paramsJsonStr = JSONObject.toJSONString(sortedParams);
|
||||||
|
// 读取签名秘钥
|
||||||
|
String signatureSecret = getSignatureSecret();
|
||||||
|
// 计算MD5摘要并转大写
|
||||||
|
return DigestUtils.md5DigestAsHex((paramsJsonStr + signatureSecret).getBytes(StandardCharsets.UTF_8)).toUpperCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析URL中的查询参数,并处理末尾逗号分隔的路径变量片段。
|
||||||
|
*
|
||||||
|
* @param url 请求的完整URL
|
||||||
|
* @return 解析后的参数映射,数值与布尔类型均转换为字符串
|
||||||
|
*/
|
||||||
|
private static Map<String, String> parseQueryString(String url) {
|
||||||
|
Map<String, String> result = new HashMap<>(16);
|
||||||
|
int fragmentIndex = url.indexOf('#');
|
||||||
|
if (fragmentIndex >= 0) {
|
||||||
|
url = url.substring(0, fragmentIndex);
|
||||||
|
}
|
||||||
|
int questionIndex = url.indexOf('?');
|
||||||
|
String paramString = null;
|
||||||
|
if (questionIndex >= 0 && questionIndex < url.length() - 1) {
|
||||||
|
paramString = url.substring(questionIndex + 1);
|
||||||
|
}
|
||||||
|
// 处理路径变量末尾以逗号分隔的段,例如 /sys/dict/getDictItems/sys_user,realname,username
|
||||||
|
int lastSlashIndex = url.lastIndexOf(SymbolConstant.SINGLE_SLASH);
|
||||||
|
if (lastSlashIndex >= 0 && lastSlashIndex < url.length() - 1) {
|
||||||
|
String lastPathVariable = url.substring(lastSlashIndex + 1);
|
||||||
|
int qIndexInPath = lastPathVariable.indexOf('?');
|
||||||
|
if (qIndexInPath >= 0) {
|
||||||
|
lastPathVariable = lastPathVariable.substring(0, qIndexInPath);
|
||||||
|
}
|
||||||
|
if (lastPathVariable.contains(SymbolConstant.COMMA)) {
|
||||||
|
String decodedPathVariable = URLDecoder.decode(lastPathVariable, StandardCharsets.UTF_8);
|
||||||
|
result.put(X_PATH_VARIABLE, decodedPathVariable);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (oConvertUtils.isNotEmpty(paramString)) {
|
||||||
|
String[] pairs = paramString.split(SymbolConstant.AND);
|
||||||
|
for (String pair : pairs) {
|
||||||
|
int equalIndex = pair.indexOf('=');
|
||||||
|
if (equalIndex > 0 && equalIndex < pair.length() - 1) {
|
||||||
|
String key = pair.substring(0, equalIndex);
|
||||||
|
String value = pair.substring(equalIndex + 1);
|
||||||
|
// 解码并统一类型为字符串
|
||||||
|
String decodedKey = URLDecoder.decode(key, StandardCharsets.UTF_8);
|
||||||
|
String decodedValue = URLDecoder.decode(value, StandardCharsets.UTF_8);
|
||||||
|
result.put(decodedKey, decodedValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 合并两个参数映射,并保证数值与布尔类型统一转为字符串。
|
||||||
|
*
|
||||||
|
* @param target 初始参数映射
|
||||||
|
* @param source 待合并的参数映射
|
||||||
|
* @return 合并后的新映射
|
||||||
|
*/
|
||||||
|
private static Map<String, String> mergeObject(Map<String, String> target, Map<String, Object> source) {
|
||||||
|
Map<String, String> merged = new HashMap<>(16);
|
||||||
|
if (target != null && !target.isEmpty()) {
|
||||||
|
merged.putAll(target);
|
||||||
|
}
|
||||||
|
if (source != null && !source.isEmpty()) {
|
||||||
|
for (Map.Entry<String, Object> entry : source.entrySet()) {
|
||||||
|
String key = entry.getKey();
|
||||||
|
Object value = entry.getValue();
|
||||||
|
if (value instanceof Number) {
|
||||||
|
// 数值类型转字符串,保持前后端一致
|
||||||
|
merged.put(key, String.valueOf(value));
|
||||||
|
} else if (value instanceof Boolean) {
|
||||||
|
// 布尔类型转字符串,保持前后端一致
|
||||||
|
merged.put(key, String.valueOf(value));
|
||||||
|
} else if (value != null) {
|
||||||
|
merged.put(key, String.valueOf(value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return merged;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 读取并校验签名秘钥配置。
|
||||||
|
*
|
||||||
|
* @return 有效的签名秘钥
|
||||||
|
*/
|
||||||
|
private static String getSignatureSecret() {
|
||||||
|
JeecgBaseConfig jeecgBaseConfig = SpringContextUtils.getBean(JeecgBaseConfig.class);
|
||||||
|
String signatureSecret = jeecgBaseConfig.getSignatureSecret();
|
||||||
|
String curlyBracket = SymbolConstant.DOLLAR + SymbolConstant.LEFT_CURLY_BRACKET;
|
||||||
|
if (oConvertUtils.isEmpty(signatureSecret) || signatureSecret.contains(curlyBracket)) {
|
||||||
|
throw new JeecgBootException("签名密钥 ${jeecg.signatureSecret} 缺少配置 !!");
|
||||||
|
}
|
||||||
|
return signatureSecret;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
@ -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,42 @@ public class Firewall {
|
|||||||
* 低代码模式(dev:开发模式,prod:发布模式——关闭所有在线开发配置能力)
|
* 低代码模式(dev:开发模式,prod:发布模式——关闭所有在线开发配置能力)
|
||||||
*/
|
*/
|
||||||
private String lowCodeMode;
|
private String lowCodeMode;
|
||||||
|
/**
|
||||||
|
* 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录)
|
||||||
|
*/
|
||||||
|
private Boolean isConcurrent = true;
|
||||||
|
/**
|
||||||
|
* 是否开启默认密码登录提醒(true 登录后提示必须修改默认密码)
|
||||||
|
*/
|
||||||
|
private Boolean enableDefaultPwdCheck = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否开启登录验证码校验(true 开启;false 关闭并跳过验证码逻辑)
|
||||||
|
*/
|
||||||
|
private Boolean enableLoginCaptcha = true;
|
||||||
|
|
||||||
// /**
|
// /**
|
||||||
// * 表字典安全模式(white:白名单——配置了白名单的表才能通过表字典方式访问,black:黑名单——配置了黑名单的表不允许表字典方式访问)
|
// * 表字典安全模式(white:白名单——配置了白名单的表才能通过表字典方式访问,black:黑名单——配置了黑名单的表不允许表字典方式访问)
|
||||||
// */
|
// */
|
||||||
// private String tableDictMode;
|
// private String tableDictMode;
|
||||||
|
|
||||||
|
|
||||||
|
public Boolean getEnableLoginCaptcha() {
|
||||||
|
return enableLoginCaptcha;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setEnableLoginCaptcha(Boolean enableLoginCaptcha) {
|
||||||
|
this.enableLoginCaptcha = enableLoginCaptcha;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Boolean getEnableDefaultPwdCheck() {
|
||||||
|
return enableDefaultPwdCheck;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setEnableDefaultPwdCheck(Boolean enableDefaultPwdCheck) {
|
||||||
|
this.enableDefaultPwdCheck = enableDefaultPwdCheck;
|
||||||
|
}
|
||||||
|
|
||||||
public Boolean getDataSourceSafe() {
|
public Boolean getDataSourceSafe() {
|
||||||
return dataSourceSafe;
|
return dataSourceSafe;
|
||||||
}
|
}
|
||||||
@ -47,4 +78,12 @@ public class Firewall {
|
|||||||
public void setDisableSelectAll(Boolean disableSelectAll) {
|
public void setDisableSelectAll(Boolean disableSelectAll) {
|
||||||
this.disableSelectAll = disableSelectAll;
|
this.disableSelectAll = disableSelectAll;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Boolean getIsConcurrent() {
|
||||||
|
return isConcurrent;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setIsConcurrent(Boolean isConcurrent) {
|
||||||
|
this.isConcurrent = isConcurrent;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
org.jeecg.config.DruidWallConfigRegister
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 0 B After Width: | Height: | Size: 487 B |
@ -0,0 +1,429 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
MCP Stdio 工具 - 修复编码问题
|
||||||
|
确保所有输出都使用UTF-8编码
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
from typing import Dict, Any
|
||||||
|
import logging
|
||||||
|
|
||||||
|
# 强制使用UTF-8编码
|
||||||
|
if sys.platform == "win32":
|
||||||
|
# Windows需要特殊处理
|
||||||
|
import io
|
||||||
|
|
||||||
|
sys.stdin = io.TextIOWrapper(sys.stdin.buffer, encoding='utf-8')
|
||||||
|
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||||
|
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8')
|
||||||
|
else:
|
||||||
|
# Unix-like系统
|
||||||
|
sys.stdin.reconfigure(encoding='utf-8')
|
||||||
|
sys.stdout.reconfigure(encoding='utf-8')
|
||||||
|
sys.stderr.reconfigure(encoding='utf-8')
|
||||||
|
|
||||||
|
# 设置环境变量
|
||||||
|
os.environ['PYTHONIOENCODING'] = 'utf-8'
|
||||||
|
os.environ['PYTHONUTF8'] = '1'
|
||||||
|
|
||||||
|
# 配置日志
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||||
|
encoding='utf-8'
|
||||||
|
)
|
||||||
|
logger = logging.getLogger("mcp-tool")
|
||||||
|
|
||||||
|
|
||||||
|
class FixedMCPServer:
|
||||||
|
"""修复编码问题的MCP服务器"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.tools = {}
|
||||||
|
self.initialize_tools()
|
||||||
|
|
||||||
|
def initialize_tools(self):
|
||||||
|
"""初始化工具集"""
|
||||||
|
|
||||||
|
# 获取时间
|
||||||
|
self.tools["get_time"] = {
|
||||||
|
"name": "get_time",
|
||||||
|
"description": "获取当前时间",
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"format": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "时间格式",
|
||||||
|
"enum": ["iso", "timestamp", "human", "chinese"],
|
||||||
|
"default": "iso"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# 文本处理工具
|
||||||
|
self.tools["text_process"] = {
|
||||||
|
"name": "text_process",
|
||||||
|
"description": "文本处理工具",
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"text": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "输入文本"
|
||||||
|
},
|
||||||
|
"operation": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "操作类型",
|
||||||
|
"enum": ["length", "upper", "lower", "reverse", "count_words"],
|
||||||
|
"default": "length"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["text"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# 数据格式工具
|
||||||
|
self.tools["format_data"] = {
|
||||||
|
"name": "format_data",
|
||||||
|
"description": "格式化数据",
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"data": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "原始数据"
|
||||||
|
},
|
||||||
|
"format": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "格式类型",
|
||||||
|
"enum": ["json", "yaml", "xml"],
|
||||||
|
"default": "json"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["data"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def handle_request(self, request: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
"""处理请求"""
|
||||||
|
try:
|
||||||
|
method = request.get("method")
|
||||||
|
params = request.get("params", {})
|
||||||
|
|
||||||
|
if method == "tools/list":
|
||||||
|
return self.handle_tools_list()
|
||||||
|
elif method == "tools/call":
|
||||||
|
return self.handle_tool_call(params)
|
||||||
|
elif method == "ping":
|
||||||
|
return {"result": "pong"}
|
||||||
|
else:
|
||||||
|
return self.create_error_response(
|
||||||
|
code=-32601,
|
||||||
|
message="Method not found"
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error handling request: {e}")
|
||||||
|
return self.create_error_response(
|
||||||
|
code=-32603,
|
||||||
|
message=f"Internal error: {str(e)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
def handle_tools_list(self) -> Dict[str, Any]:
|
||||||
|
"""列出所有工具 - 确保返回标准JSON"""
|
||||||
|
return {
|
||||||
|
"result": {
|
||||||
|
"tools": list(self.tools.values())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def handle_tool_call(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
"""调用工具 - 修复响应格式"""
|
||||||
|
name = params.get("name")
|
||||||
|
arguments = params.get("arguments", {})
|
||||||
|
|
||||||
|
if name not in self.tools:
|
||||||
|
return self.create_error_response(
|
||||||
|
code=-32602,
|
||||||
|
message=f"Tool '{name}' not found"
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
if name == "get_time":
|
||||||
|
result = self.execute_get_time(arguments)
|
||||||
|
elif name == "text_process":
|
||||||
|
result = self.execute_text_process(arguments)
|
||||||
|
elif name == "format_data":
|
||||||
|
result = self.execute_format_data(arguments)
|
||||||
|
else:
|
||||||
|
return self.create_error_response(
|
||||||
|
code=-32602,
|
||||||
|
message="Tool not implemented"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 确保返回正确的MCP响应格式
|
||||||
|
return self.create_success_response(result)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Tool execution error: {e}")
|
||||||
|
return self.create_error_response(
|
||||||
|
code=-32603,
|
||||||
|
message=f"Tool execution failed: {str(e)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
def execute_get_time(self, args: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
"""获取时间 - 支持中文"""
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
try:
|
||||||
|
format_type = args.get("format", "iso")
|
||||||
|
now = datetime.now()
|
||||||
|
|
||||||
|
if format_type == "iso":
|
||||||
|
result = now.isoformat()
|
||||||
|
elif format_type == "timestamp":
|
||||||
|
result = now.timestamp()
|
||||||
|
elif format_type == "human":
|
||||||
|
result = now.strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
elif format_type == "chinese":
|
||||||
|
result = now.strftime("%Y年%m月%d日 %H时%M分%S秒")
|
||||||
|
else:
|
||||||
|
result = now.isoformat()
|
||||||
|
logger.info(f"当前系统时间:{result}")
|
||||||
|
return {
|
||||||
|
"status": "success",
|
||||||
|
"format": format_type,
|
||||||
|
"time": result,
|
||||||
|
"timestamp": now.timestamp(),
|
||||||
|
"date": now.strftime("%Y-%m-%d"),
|
||||||
|
"time_12h": now.strftime("%I:%M:%S %p")
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return {
|
||||||
|
"status": "error",
|
||||||
|
"error": str(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
def execute_text_process(self, args: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
"""文本处理"""
|
||||||
|
try:
|
||||||
|
text = args.get("text", "")
|
||||||
|
operation = args.get("operation", "length")
|
||||||
|
|
||||||
|
if operation == "length":
|
||||||
|
result = len(text)
|
||||||
|
result_str = f"文本长度: {result} 个字符"
|
||||||
|
elif operation == "upper":
|
||||||
|
result = text.upper()
|
||||||
|
result_str = f"大写: {result}"
|
||||||
|
elif operation == "lower":
|
||||||
|
result = text.lower()
|
||||||
|
result_str = f"小写: {result}"
|
||||||
|
elif operation == "reverse":
|
||||||
|
result = text[::-1]
|
||||||
|
result_str = f"反转: {result}"
|
||||||
|
elif operation == "count_words":
|
||||||
|
words = len(text.split())
|
||||||
|
result = words
|
||||||
|
result_str = f"单词数: {words}"
|
||||||
|
else:
|
||||||
|
raise ValueError(f"未知操作: {operation}")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"status": "success",
|
||||||
|
"operation": operation,
|
||||||
|
"original_text": text,
|
||||||
|
"result": result,
|
||||||
|
"result_str": result_str,
|
||||||
|
"text_length": len(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return {
|
||||||
|
"status": "error",
|
||||||
|
"error": str(e),
|
||||||
|
"operation": args.get("operation", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
def execute_format_data(self, args: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
"""格式化数据"""
|
||||||
|
try:
|
||||||
|
data_str = args.get("data", "")
|
||||||
|
format_type = args.get("format", "json")
|
||||||
|
|
||||||
|
# 尝试解析为JSON
|
||||||
|
try:
|
||||||
|
data = json.loads(data_str)
|
||||||
|
is_json = True
|
||||||
|
except:
|
||||||
|
data = data_str
|
||||||
|
is_json = False
|
||||||
|
|
||||||
|
if format_type == "json":
|
||||||
|
if is_json:
|
||||||
|
result = json.dumps(data, ensure_ascii=False, indent=2)
|
||||||
|
else:
|
||||||
|
# 如果不是JSON,包装成JSON
|
||||||
|
result = json.dumps({"text": data}, ensure_ascii=False, indent=2)
|
||||||
|
elif format_type == "yaml":
|
||||||
|
import yaml
|
||||||
|
result = yaml.dump(data, allow_unicode=True, default_flow_style=False)
|
||||||
|
elif format_type == "xml":
|
||||||
|
# 简单的XML格式化
|
||||||
|
if isinstance(data, dict):
|
||||||
|
result = "<data>"
|
||||||
|
for k, v in data.items():
|
||||||
|
result += f"\n <{k}>{v}</{k}>"
|
||||||
|
result += "\n</data>"
|
||||||
|
else:
|
||||||
|
result = f"<text>{data}</text>"
|
||||||
|
else:
|
||||||
|
result = str(data)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"status": "success",
|
||||||
|
"format": format_type,
|
||||||
|
"original": data_str,
|
||||||
|
"formatted": result,
|
||||||
|
"length": len(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return {
|
||||||
|
"status": "error",
|
||||||
|
"error": str(e),
|
||||||
|
"format": args.get("format", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
def create_success_response(self, data: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
"""创建成功响应 - 确保符合MCP规范"""
|
||||||
|
# 将数据转换为JSON字符串作为文本内容
|
||||||
|
content_text = json.dumps(data, ensure_ascii=False, indent=2)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"result": {
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"text": content_text
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isError": False
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def create_error_response(self, code: int, message: str) -> Dict[str, Any]:
|
||||||
|
"""创建错误响应"""
|
||||||
|
return {
|
||||||
|
"error": {
|
||||||
|
"code": code,
|
||||||
|
"message": message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def safe_json_dump(data: Dict[str, Any]) -> str:
|
||||||
|
"""安全的JSON序列化,确保UTF-8编码"""
|
||||||
|
try:
|
||||||
|
return json.dumps(data, ensure_ascii=False, separators=(',', ':'))
|
||||||
|
except:
|
||||||
|
# 如果失败,使用ASCII转义
|
||||||
|
return json.dumps(data, ensure_ascii=True, separators=(',', ':'))
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""主函数 - 修复Stdio通信"""
|
||||||
|
logger.info("启动MCP Stdio服务器 (修复编码版)...")
|
||||||
|
|
||||||
|
server = FixedMCPServer()
|
||||||
|
|
||||||
|
# 初始握手消息
|
||||||
|
init_message = {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": 1,
|
||||||
|
"result": {
|
||||||
|
"protocolVersion": "2024-11-05",
|
||||||
|
"capabilities": {
|
||||||
|
"tools": {}
|
||||||
|
},
|
||||||
|
"serverInfo": {
|
||||||
|
"name": "fixed-mcp-server",
|
||||||
|
"version": "1.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# 发送初始化响应
|
||||||
|
try:
|
||||||
|
sys.stdout.write(safe_json_dump(init_message) + "\n")
|
||||||
|
sys.stdout.flush()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"发送初始化消息失败: {e}")
|
||||||
|
return
|
||||||
|
|
||||||
|
logger.info("MCP服务器已初始化")
|
||||||
|
|
||||||
|
# 主循环
|
||||||
|
line_num = 0
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
line = sys.stdin.readline()
|
||||||
|
if not line:
|
||||||
|
logger.info("输入流结束")
|
||||||
|
break
|
||||||
|
|
||||||
|
line = line.strip()
|
||||||
|
line_num += 1
|
||||||
|
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
|
||||||
|
logger.info(f"收到第 {line_num} 行: {line[:100]}...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
request = json.loads(line)
|
||||||
|
logger.info(f"解析请求: {request.get('method', 'unknown')}")
|
||||||
|
|
||||||
|
# 处理请求
|
||||||
|
response = server.handle_request(request)
|
||||||
|
response["jsonrpc"] = "2.0"
|
||||||
|
response["id"] = request.get("id")
|
||||||
|
|
||||||
|
# 发送响应
|
||||||
|
response_json = safe_json_dump(response)
|
||||||
|
sys.stdout.write(response_json + "\n")
|
||||||
|
sys.stdout.flush()
|
||||||
|
|
||||||
|
logger.info(f"发送响应: {response.get('result', response.get('error', {}))}")
|
||||||
|
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
logger.error(f"JSON解析错误: {e}")
|
||||||
|
error_response = {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"error": {
|
||||||
|
"code": -32700,
|
||||||
|
"message": f"Parse error at line {line_num}"
|
||||||
|
},
|
||||||
|
"id": None
|
||||||
|
}
|
||||||
|
sys.stdout.write(safe_json_dump(error_response) + "\n")
|
||||||
|
sys.stdout.flush()
|
||||||
|
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
logger.info("接收到中断信号")
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"未处理的错误: {e}")
|
||||||
|
break
|
||||||
|
|
||||||
|
logger.info("MCP服务器已停止")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Binary file not shown.
@ -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.1</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>
|
||||||
<apache-tika.version>2.9.1</apache-tika.version>
|
<liteflow.version>2.15.0</liteflow.version>
|
||||||
|
<apache-tika.version>3.2.3</apache-tika.version>
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
|
<dependencyManagement>
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>dev.langchain4j</groupId>
|
||||||
|
<artifactId>langchain4j-bom</artifactId>
|
||||||
|
<version>1.9.1</version>
|
||||||
|
<type>pom</type>
|
||||||
|
<scope>import</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>dev.langchain4j</groupId>
|
||||||
|
<artifactId>langchain4j-community-bom</artifactId>
|
||||||
|
<version>1.9.1-beta17</version>
|
||||||
|
<type>pom</type>
|
||||||
|
<scope>import</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
</dependencyManagement>
|
||||||
|
|
||||||
<dependencies>
|
<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.1-beta1</version>
|
||||||
|
<exclusions>
|
||||||
|
<exclusion>
|
||||||
|
<groupId>commons-io</groupId>
|
||||||
|
<artifactId>commons-io</artifactId>
|
||||||
|
</exclusion>
|
||||||
|
<exclusion>
|
||||||
|
<groupId>commons-beanutils</groupId>
|
||||||
|
<artifactId>commons-beanutils</artifactId>
|
||||||
|
</exclusion>
|
||||||
|
<exclusion>
|
||||||
|
<groupId>com.yomahub</groupId>
|
||||||
|
<artifactId>liteflow-script-python</artifactId>
|
||||||
|
</exclusion>
|
||||||
|
</exclusions>
|
||||||
</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,12 +105,18 @@
|
|||||||
<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-groovy</artifactId>
|
||||||
|
<version>${liteflow.version}</version>
|
||||||
|
<scope>runtime</scope>
|
||||||
|
</dependency>
|
||||||
|
<!-- end 注意:这几个依赖体积较大,每个约50MB。若发布时需要使用,请将 <scope>provided</scope> 删除 -->
|
||||||
|
|
||||||
<!-- aiflow 脚本依赖 -->
|
<!-- aiflow 脚本依赖 -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.yomahub</groupId>
|
<groupId>com.yomahub</groupId>
|
||||||
<artifactId>liteflow-script-groovy</artifactId>
|
<artifactId>liteflow-script-python</artifactId>
|
||||||
<version>${liteflow.version}</version>
|
<version>${liteflow.version}</version>
|
||||||
<scope>runtime</scope>
|
<scope>runtime</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
@ -109,13 +149,20 @@
|
|||||||
<!-- 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>
|
||||||
|
<!-- langChain4j mcp support -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>dev.langchain4j</groupId>
|
||||||
|
<artifactId>langchain4j-mcp</artifactId>
|
||||||
</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 +176,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,13 +192,21 @@
|
|||||||
</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>
|
||||||
|
<!-- langChain4j Document Parser 适用于excel、ppt、word -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>dev.langchain4j</groupId>
|
||||||
|
<artifactId>langchain4j-document-parser-apache-poi</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
<!-- langChain4j Document Parser -->
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.apache.tika</groupId>
|
<groupId>org.apache.tika</groupId>
|
||||||
<artifactId>tika-core</artifactId>
|
<artifactId>tika-core</artifactId>
|
||||||
@ -180,7 +233,12 @@
|
|||||||
<artifactId>tika-parser-text-module</artifactId>
|
<artifactId>tika-parser-text-module</artifactId>
|
||||||
<version>${apache-tika.version}</version>
|
<version>${apache-tika.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<!-- word模版引擎 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.deepoove</groupId>
|
||||||
|
<artifactId>poi-tl</artifactId>
|
||||||
|
<version>1.12.2</version>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
</project>
|
</project>
|
||||||
@ -38,4 +38,25 @@ 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";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否开启记忆
|
||||||
|
*/
|
||||||
|
public static final Integer IZ_OPEN_MEMORY = 1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 会话标题最大长度
|
||||||
|
*/
|
||||||
|
public static final int CONVERSATION_MAX_TITLE_LENGTH = 10;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI写作的应用id
|
||||||
|
*/
|
||||||
|
public static final String WRITER_APP_ID = "2010634128233779202";
|
||||||
}
|
}
|
||||||
|
|||||||
@ -104,4 +104,68 @@ public class Prompts {
|
|||||||
" - 反幻觉校验:\"所有数据需标注来源,不确定信息用[需核实]标记\"\n" +
|
" - 反幻觉校验:\"所有数据需标注来源,不确定信息用[需核实]标记\"\n" +
|
||||||
" - 风格校准器:\"对比[目标风格]与生成内容的余弦相似度,低于0.7时启动重写\"\n" +
|
" - 风格校准器:\"对比[目标风格]与生成内容的余弦相似度,低于0.7时启动重写\"\n" +
|
||||||
" - 伦理审查模块:\"自动过滤涉及隐私/偏见/违法内容,替换为[合规表达]\"";
|
" - 伦理审查模块:\"自动过滤涉及隐私/偏见/违法内容,替换为[合规表达]\"";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提示词生成角色及通用要求
|
||||||
|
*/
|
||||||
|
public static final String GENERATE_GUIDE_HEADER = "# 角色\n" +
|
||||||
|
"你是一位AI提示词专家,请根据提供的配置信息,生成针对AI智能体的“使用指南”提示词。\n" +
|
||||||
|
"\n" +
|
||||||
|
"## 通用要求\n" +
|
||||||
|
"1. 生成的内容将作为系统提示词的一部分。\n" +
|
||||||
|
"2. **严禁**包含任何角色设定开场白(如“你是一个...AI助手”、“在对话过程中...”等)。\n" +
|
||||||
|
"3. **只输出提示词内容**,不要包含任何解释、寒暄或Markdown代码块标记。\n" +
|
||||||
|
"4. 语气专业、清晰、指令性强。\n" +
|
||||||
|
"5. 说明内容请使用中文。\n\n";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 变量生成提示词
|
||||||
|
*/
|
||||||
|
public static final String GENERATE_VAR_PART = "## 任务:生成变量使用指南\n" +
|
||||||
|
"### 输入信息\n" +
|
||||||
|
"**变量列表**:\n" +
|
||||||
|
"%s\n" +
|
||||||
|
"### 要求\n" +
|
||||||
|
"1. 请生成一段**变量使用指南**。\n" +
|
||||||
|
"2. **遍历生成**:请遍历【输入信息】中的所有变量,为**每一个**变量生成一条具体的使用指南。\n" +
|
||||||
|
"3. **格式要求**:请仿照以下句式,根据变量的实际含义生成(确保包含{{变量名}}):\n" +
|
||||||
|
" 例如:针对name变量 -> “回复问题时,请称呼你的用户为{{name}}。”\n" +
|
||||||
|
" 例如:针对age变量 -> “用户的年龄是{{age}},请在对话中适时使用。”\n" +
|
||||||
|
" 例如:针对其他变量 -> “用户的[变量描述]是{{[变量名]}},请在对话中适时使用。”\n" +
|
||||||
|
"4. **通用更新指令**:请在变量指南的最后,单独生成一条指令,明确指示AI:“当从用户对话中获取到上述变量(<列出所有变量名,用顿号分隔>)的**新信息**时,**必须立即调用** `update_variable` 工具进行存储。**注意**:调用前请检查上下文,如果已调用过该工具或变量值未改变,**严禁**重复调用。”\n" +
|
||||||
|
"5. **保留原文**:如果输入信息中包含具体的行为指令(如“回复问题时,请称呼你的用户为{{name}}”),请在生成的指南中**直接引用原文**,不要进行改写或格式化,以免改变用户的原意。\n\n";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记忆库生成提示词
|
||||||
|
*/
|
||||||
|
public static final String GENERATE_MEMORY_PART = "## 任务:生成记忆库使用指南\n" +
|
||||||
|
"### 输入信息\n" +
|
||||||
|
"**记忆库描述**:\n" +
|
||||||
|
"%s\n" +
|
||||||
|
"### 要求\n" +
|
||||||
|
"1. 请生成一段**记忆库使用指南**,加入【工具使用强制协议】:\n" +
|
||||||
|
" - **全自动存储(无需用户指令)**:你必须时刻像一个观察者一样分析对话。一旦检测到符合记忆库描述的信息(尤其是:**姓名、职业、年龄**、联系方式、偏好、经历等),**立即**调用 `add_memory` 工具存储。**绝对不要**询问用户是否需要存储,也不要等待用户明确指令。这是你的后台职责。\n" +
|
||||||
|
" - **全自动检索(强制优先)**:\n" +
|
||||||
|
" * **禁止直接反问**:当用户提出依赖个人信息的问题(如“推荐适合我的...”或“我之前说过...”)时,**绝对禁止**直接反问用户“你的爱好是什么?”。\n" +
|
||||||
|
" * **必须先查后答**:你必须**先假设**记忆库中已经有了答案,并**立即调用** `query_memory` 进行验证。只有当工具返回“未找到相关信息”后,你才有资格询问用户。\n" +
|
||||||
|
" * **宁可查空,不可不查**:即使你觉得可能没有记录,也必须先走一遍查询流程。\n" +
|
||||||
|
" - **动态调整**:请根据【输入信息】中提供的**记忆库状态描述**,明确界定哪些信息属于“自动捕获”的范围。\n" +
|
||||||
|
" - **行为准则**:\n" +
|
||||||
|
" * 你的记忆动作应该是**主动且无感**的。用户只负责聊天,你负责记住一切重要细节。\n" +
|
||||||
|
" * **禁止口头空谈**:严禁只回复“我知道了”、“已记住”而实际不调用工具。这是严重错误。\n" +
|
||||||
|
" - **示例演示**:\n" +
|
||||||
|
" * 自动存储(职业):用户说“我是网络工程师” -> (捕捉到职业信息) -> **立即自动调用** `add_memory(content='用户职业是网络工程师')` -> (存储成功) -> 回复“原来是同行,网络工程很有趣...”。\n" +
|
||||||
|
" * 自动查询(场景):用户说“根据我的爱好推荐旅游地点” -> **严禁**直接问“你有什么爱好?” -> **必须立即调用** `query_memory(queryText='用户爱好')` -> (若查到:爬山) -> 回复“既然你喜欢爬山,推荐去黄山...”。\n" +
|
||||||
|
" * 自动查询(常规):用户问“今天吃什么好?” -> (需要了解口味) -> **立即自动调用** `query_memory(queryText='用户饮食偏好')` -> (获取到不吃香菜) -> 回复“推荐一家不放香菜的...”。\n\n";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ai写作提示词
|
||||||
|
*/
|
||||||
|
public static final String AI_WRITER_PROMPT ="请撰写一篇关于 [{}] 的文章。文章的内容格式:{},语气:{},语言:{},长度:{}。";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ai写作回复提示词
|
||||||
|
*/
|
||||||
|
public static final String AI_REPLY_PROMPT = "请针对如下内容:[{}] 做个回复。回复内容参考:[{}], 回复格式:{},语气:{},语言:{},长度:{}。";
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,7 +22,6 @@ import org.springframework.web.bind.annotation.*;
|
|||||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||||
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import java.util.Arrays;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @Description: AI应用
|
* @Description: AI应用
|
||||||
@ -179,4 +178,16 @@ public class AiragAppController extends JeecgController<AiragApp, IAiragAppServi
|
|||||||
return (SseEmitter) airagAppService.generatePrompt(prompt,false);
|
return (SseEmitter) airagAppService.generatePrompt(prompt,false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据应用ID生成变量和记忆提示词 (SSE)
|
||||||
|
* for: 【QQYUN-14479】提示词单独拆分
|
||||||
|
* @param variables
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
@PostMapping(value = "/prompt/generateMemoryByAppId")
|
||||||
|
public SseEmitter generatePromptByAppIdSse(@RequestParam(name = "variables") String variables,
|
||||||
|
@RequestParam(name = "memoryId") String memoryId) {
|
||||||
|
return (SseEmitter) airagAppService.generateMemoryByAppId(variables, memoryId,false);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,6 +8,7 @@ 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.shiro.IgnoreAuth;
|
||||||
import org.jeecg.modules.airag.app.service.IAiragChatService;
|
import org.jeecg.modules.airag.app.service.IAiragChatService;
|
||||||
|
import org.jeecg.modules.airag.app.vo.AiWriteGenerateVo;
|
||||||
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;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
@ -102,6 +103,19 @@ public class AiragChatController {
|
|||||||
return chatService.getConversations(appId);
|
return chatService.getConversations(appId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据类型获取所有对话
|
||||||
|
*
|
||||||
|
* @return 返回一个Result对象,包含所有对话的信息
|
||||||
|
* @author wangshuai
|
||||||
|
* @date 2025/12/11 11:42
|
||||||
|
*/
|
||||||
|
@IgnoreAuth
|
||||||
|
@GetMapping(value = "/getConversationsByType")
|
||||||
|
public Result<?> getConversationsByType(@RequestParam(value = "sessionType") String sessionType) {
|
||||||
|
return chatService.getConversationsByType(sessionType);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 删除会话
|
* 删除会话
|
||||||
*
|
*
|
||||||
@ -113,7 +127,22 @@ public class AiragChatController {
|
|||||||
@IgnoreAuth
|
@IgnoreAuth
|
||||||
@DeleteMapping(value = "/conversation/{id}")
|
@DeleteMapping(value = "/conversation/{id}")
|
||||||
public Result<?> deleteConversation(@PathVariable("id") String id) {
|
public Result<?> deleteConversation(@PathVariable("id") String id) {
|
||||||
return chatService.deleteConversation(id);
|
return chatService.deleteConversation(id,"");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除会话
|
||||||
|
*
|
||||||
|
* @param id
|
||||||
|
* @return
|
||||||
|
* @author wangshuai
|
||||||
|
* @date 2025/12/11 20:00
|
||||||
|
*/
|
||||||
|
@IgnoreAuth
|
||||||
|
@DeleteMapping(value = "/conversation/{id}/{sessionType}")
|
||||||
|
public Result<?> deleteConversationByType(@PathVariable("id") String id,
|
||||||
|
@PathVariable("sessionType") String sessionType) {
|
||||||
|
return chatService.deleteConversation(id,sessionType);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -139,8 +168,9 @@ public class AiragChatController {
|
|||||||
*/
|
*/
|
||||||
@IgnoreAuth
|
@IgnoreAuth
|
||||||
@GetMapping(value = "/messages")
|
@GetMapping(value = "/messages")
|
||||||
public Result<?> getMessages(@RequestParam(value = "conversationId", required = true) String conversationId) {
|
public Result<?> getMessages(@RequestParam(value = "conversationId", required = true) String conversationId,
|
||||||
return chatService.getMessages(conversationId);
|
@RequestParam(value = "sessionType", required = false) String sessionType) {
|
||||||
|
return chatService.getMessages(conversationId, sessionType);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -153,7 +183,21 @@ public class AiragChatController {
|
|||||||
@IgnoreAuth
|
@IgnoreAuth
|
||||||
@GetMapping(value = "/messages/clear/{conversationId}")
|
@GetMapping(value = "/messages/clear/{conversationId}")
|
||||||
public Result<?> clearMessage(@PathVariable(value = "conversationId") String conversationId) {
|
public Result<?> clearMessage(@PathVariable(value = "conversationId") String conversationId) {
|
||||||
return chatService.clearMessage(conversationId);
|
return chatService.clearMessage(conversationId, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清空消息
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* @author wangshuai
|
||||||
|
* @date 2025/12/11 19:06
|
||||||
|
*/
|
||||||
|
@IgnoreAuth
|
||||||
|
@GetMapping(value = "/messages/clear/{conversationId}/{sessionType}")
|
||||||
|
public Result<?> clearMessageByType(@PathVariable(value = "conversationId") String conversationId,
|
||||||
|
@PathVariable(value = "sessionType") String sessionType) {
|
||||||
|
return chatService.clearMessage(conversationId, sessionType);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -217,4 +261,25 @@ public class AiragChatController {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ai海报生成
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
@PostMapping("/genAiPoster")
|
||||||
|
public Result<String> genAiPoster(@RequestBody ChatSendParams chatSendParams){
|
||||||
|
String imageUrl = chatService.genAiPoster(chatSendParams);
|
||||||
|
return Result.OK(imageUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成ai写作
|
||||||
|
*
|
||||||
|
* @param aiWriteGenerateVo
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
@PostMapping("/genAiWriter")
|
||||||
|
public SseEmitter genAiWriter(@RequestBody AiWriteGenerateVo aiWriteGenerateVo){
|
||||||
|
return chatService.genAiWriter(aiWriteGenerateVo);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -167,6 +167,35 @@ 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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否开启记忆(0 不开启,1开启)
|
||||||
|
*/
|
||||||
|
@Schema(description = "是否开启记忆(0 不开启,1开启)")
|
||||||
|
private java.lang.Integer izOpenMemory;
|
||||||
|
/**
|
||||||
|
* 记忆库,知识库的id
|
||||||
|
*/
|
||||||
|
@Schema(description = "记忆库")
|
||||||
|
private java.lang.String memoryId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 变量
|
||||||
|
*/
|
||||||
|
@Schema(description = "变量")
|
||||||
|
private java.lang.String variables;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记忆和变量提示词
|
||||||
|
*/
|
||||||
|
@Schema(description = "记忆和变量提示词")
|
||||||
|
private java.lang.String memoryPrompt;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 知识库ids
|
* 知识库ids
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
package org.jeecg.modules.airag.app.service;
|
package org.jeecg.modules.airag.app.service;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.extension.service.IService;
|
import com.baomidou.mybatisplus.extension.service.IService;
|
||||||
import org.jeecg.common.api.vo.Result;
|
|
||||||
import org.jeecg.modules.airag.app.entity.AiragApp;
|
import org.jeecg.modules.airag.app.entity.AiragApp;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -21,4 +20,14 @@ public interface IAiragAppService extends IService<AiragApp> {
|
|||||||
* @date 2025/3/12 14:45
|
* @date 2025/3/12 14:45
|
||||||
*/
|
*/
|
||||||
Object generatePrompt(String prompt,boolean blocking);
|
Object generatePrompt(String prompt,boolean blocking);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据应用id生成提示词
|
||||||
|
*
|
||||||
|
* @param variables
|
||||||
|
* @param memoryId
|
||||||
|
* @param blocking
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
Object generateMemoryByAppId(String variables, String memoryId, boolean blocking);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
package org.jeecg.modules.airag.app.service;
|
package org.jeecg.modules.airag.app.service;
|
||||||
|
|
||||||
import org.jeecg.common.api.vo.Result;
|
import org.jeecg.common.api.vo.Result;
|
||||||
|
import org.jeecg.modules.airag.app.vo.AiWriteGenerateVo;
|
||||||
import org.jeecg.modules.airag.app.vo.AppDebugParams;
|
import org.jeecg.modules.airag.app.vo.AppDebugParams;
|
||||||
import org.jeecg.modules.airag.app.vo.ChatConversation;
|
import org.jeecg.modules.airag.app.vo.ChatConversation;
|
||||||
import org.jeecg.modules.airag.app.vo.ChatSendParams;
|
import org.jeecg.modules.airag.app.vo.ChatSendParams;
|
||||||
@ -59,21 +60,23 @@ public interface IAiragChatService {
|
|||||||
* 获取对话聊天记录
|
* 获取对话聊天记录
|
||||||
*
|
*
|
||||||
* @param conversationId
|
* @param conversationId
|
||||||
|
* @param sessionType 类型
|
||||||
* @return
|
* @return
|
||||||
* @author chenrui
|
* @author chenrui
|
||||||
* @date 2025/2/26 15:16
|
* @date 2025/2/26 15:16
|
||||||
*/
|
*/
|
||||||
Result<?> getMessages(String conversationId);
|
Result<?> getMessages(String conversationId, String sessionType);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 删除会话
|
* 删除会话
|
||||||
*
|
*
|
||||||
* @param conversationId
|
* @param conversationId
|
||||||
|
* @param sessionType
|
||||||
* @return
|
* @return
|
||||||
* @author chenrui
|
* @author chenrui
|
||||||
* @date 2025/3/3 16:55
|
* @date 2025/3/3 16:55
|
||||||
*/
|
*/
|
||||||
Result<?> deleteConversation(String conversationId);
|
Result<?> deleteConversation(String conversationId, String sessionType);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 更新会话标题
|
* 更新会话标题
|
||||||
@ -87,11 +90,12 @@ public interface IAiragChatService {
|
|||||||
/**
|
/**
|
||||||
* 清空消息
|
* 清空消息
|
||||||
* @param conversationId
|
* @param conversationId
|
||||||
|
* @param sessionType
|
||||||
* @return
|
* @return
|
||||||
* @author chenrui
|
* @author chenrui
|
||||||
* @date 2025/3/3 19:49
|
* @date 2025/3/3 19:49
|
||||||
*/
|
*/
|
||||||
Result<?> clearMessage(String conversationId);
|
Result<?> clearMessage(String conversationId, String sessionType);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 初始化聊天(忽略租户)
|
* 初始化聊天(忽略租户)
|
||||||
@ -111,4 +115,27 @@ public interface IAiragChatService {
|
|||||||
* @date 2025/8/11 17:39
|
* @date 2025/8/11 17:39
|
||||||
*/
|
*/
|
||||||
SseEmitter receiveByRequestId(String requestId);
|
SseEmitter receiveByRequestId(String requestId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据类型获取会话列表
|
||||||
|
*
|
||||||
|
* @param sessionType
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
Result<?> getConversationsByType(String sessionType);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成海报图片
|
||||||
|
* @param chatSendParams
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
String genAiPoster(ChatSendParams chatSendParams);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成ai创作
|
||||||
|
*
|
||||||
|
* @param chatSendParams
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
SseEmitter genAiWriter(AiWriteGenerateVo chatSendParams);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,44 @@
|
|||||||
|
package org.jeecg.modules.airag.app.service;
|
||||||
|
|
||||||
|
import org.jeecg.modules.airag.app.entity.AiragApp;
|
||||||
|
import org.jeecg.modules.airag.common.handler.AIChatParams;
|
||||||
|
|
||||||
|
public interface IAiragVariableService {
|
||||||
|
/**
|
||||||
|
* 更新变量值
|
||||||
|
*
|
||||||
|
* @param userId
|
||||||
|
* @param appId
|
||||||
|
* @param name
|
||||||
|
* @param value
|
||||||
|
*/
|
||||||
|
void updateVariable(String userId, String appId, String name, String value);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 追加提示词
|
||||||
|
*
|
||||||
|
* @param username
|
||||||
|
* @param app
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
String additionalPrompt(String username, AiragApp app);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化变量(仅不存在时设置)
|
||||||
|
*
|
||||||
|
* @param userId
|
||||||
|
* @param appId
|
||||||
|
* @param name
|
||||||
|
* @param defaultValue
|
||||||
|
*/
|
||||||
|
void initVariable(String userId, String appId, String name, String defaultValue);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加变量更新工具
|
||||||
|
*
|
||||||
|
* @param params
|
||||||
|
* @param aiApp
|
||||||
|
* @param username
|
||||||
|
*/
|
||||||
|
void addUpdateVariableTool(AiragApp aiApp, String username, AIChatParams params);
|
||||||
|
}
|
||||||
@ -1,5 +1,6 @@
|
|||||||
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 com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
import dev.langchain4j.data.message.AiMessage;
|
import dev.langchain4j.data.message.AiMessage;
|
||||||
@ -10,12 +11,15 @@ import dev.langchain4j.model.output.FinishReason;
|
|||||||
import dev.langchain4j.service.TokenStream;
|
import dev.langchain4j.service.TokenStream;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.jeecg.common.api.vo.Result;
|
import org.jeecg.common.api.vo.Result;
|
||||||
|
import org.jeecg.common.exception.JeecgBootBizTipException;
|
||||||
import org.jeecg.common.util.AssertUtils;
|
import org.jeecg.common.util.AssertUtils;
|
||||||
import org.jeecg.common.util.UUIDGenerator;
|
import org.jeecg.common.util.UUIDGenerator;
|
||||||
|
import org.jeecg.common.util.oConvertUtils;
|
||||||
import org.jeecg.modules.airag.app.consts.Prompts;
|
import org.jeecg.modules.airag.app.consts.Prompts;
|
||||||
import org.jeecg.modules.airag.app.entity.AiragApp;
|
import org.jeecg.modules.airag.app.entity.AiragApp;
|
||||||
import org.jeecg.modules.airag.app.mapper.AiragAppMapper;
|
import org.jeecg.modules.airag.app.mapper.AiragAppMapper;
|
||||||
import org.jeecg.modules.airag.app.service.IAiragAppService;
|
import org.jeecg.modules.airag.app.service.IAiragAppService;
|
||||||
|
import org.jeecg.modules.airag.app.vo.AppVariableVo;
|
||||||
import org.jeecg.modules.airag.common.consts.AiragConsts;
|
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;
|
||||||
@ -23,6 +27,8 @@ import org.jeecg.modules.airag.common.utils.AiragLocalCache;
|
|||||||
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.llm.entity.AiragKnowledge;
|
||||||
|
import org.jeecg.modules.airag.llm.service.IAiragKnowledgeService;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
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;
|
||||||
@ -31,6 +37,7 @@ import java.io.IOException;
|
|||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @Description: AI应用
|
* @Description: AI应用
|
||||||
@ -45,6 +52,9 @@ public class AiragAppServiceImpl extends ServiceImpl<AiragAppMapper, AiragApp> i
|
|||||||
@Autowired
|
@Autowired
|
||||||
IAIChatHandler aiChatHandler;
|
IAIChatHandler aiChatHandler;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private IAiragKnowledgeService airagKnowledgeService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Object generatePrompt(String prompt, boolean blocking) {
|
public Object generatePrompt(String prompt, boolean blocking) {
|
||||||
AssertUtils.assertNotEmpty("请输入提示词", prompt);
|
AssertUtils.assertNotEmpty("请输入提示词", prompt);
|
||||||
@ -62,84 +72,167 @@ public class AiragAppServiceImpl extends ServiceImpl<AiragAppMapper, AiragApp> i
|
|||||||
}
|
}
|
||||||
return Result.OK("success", promptValue);
|
return Result.OK("success", promptValue);
|
||||||
}else{
|
}else{
|
||||||
SseEmitter emitter = new SseEmitter(-0L);
|
//update-begin---author:wangshuai---date:2026-01-08---for: 将流式输出单独抽出去,变量和记忆也需要---
|
||||||
// 异步运行(流式)
|
return startSseChat(messages, params);
|
||||||
TokenStream tokenStream = aiChatHandler.chatByDefaultModel(messages, params);
|
//update-end---author:wangshuai---date:2026-01-08---for: 将流式输出单独抽出去,变量和记忆也需要---
|
||||||
/**
|
}
|
||||||
* 是否正在思考
|
}
|
||||||
*/
|
|
||||||
AtomicBoolean isThinking = new AtomicBoolean(false);
|
//update-begin---author:wangshuai---date:2026-01-05---for:【QQYUN-14479】增加一个开启记忆的按钮。下面为提示词和记忆,将记忆提示词单独拆分---
|
||||||
String requestId = UUIDGenerator.generate();
|
@Override
|
||||||
// ai聊天响应逻辑
|
public Object generateMemoryByAppId(String variables, String memoryId, boolean blocking) {
|
||||||
tokenStream.onNext((String resMessage) -> {
|
if(oConvertUtils.isEmpty(variables) && oConvertUtils.isEmpty(memoryId)){
|
||||||
// 兼容推理模型
|
throw new JeecgBootBizTipException("请先添加变量或者记忆后再次重试!");
|
||||||
if ("<think>".equals(resMessage)) {
|
}
|
||||||
isThinking.set(true);
|
// 构建变量描述
|
||||||
resMessage = "> ";
|
StringBuilder variablesDesc = new StringBuilder();
|
||||||
}
|
if (oConvertUtils.isNotEmpty(variables)) {
|
||||||
if ("</think>".equals(resMessage)) {
|
List<AppVariableVo> variableList = JSONArray.parseArray(variables, AppVariableVo.class);
|
||||||
isThinking.set(false);
|
if (variableList != null && !variableList.isEmpty()) {
|
||||||
resMessage = "\n\n";
|
for (AppVariableVo var : variableList) {
|
||||||
}
|
if (var.getEnable() != null && !var.getEnable()) {
|
||||||
if (isThinking.get()) {
|
continue;
|
||||||
if (null != resMessage && resMessage.contains("\n")) {
|
}
|
||||||
resMessage = "\n> ";
|
String name = var.getName();
|
||||||
|
if (oConvertUtils.isNotEmpty(var.getAction())) {
|
||||||
|
String action = var.getAction();
|
||||||
|
if (oConvertUtils.isNotEmpty(name)) {
|
||||||
|
try {
|
||||||
|
// 使用正则替换未被{{}}包裹的变量名
|
||||||
|
String regex = "(?<!\\{\\{)\\b" + Pattern.quote(name) + "\\b(?!\\}\\})";
|
||||||
|
action = action.replaceAll(regex, "{{" + name + "}}");
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("变量名替换异常: name={}", name, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
EventData eventData = new EventData(requestId, null, EventData.EVENT_MESSAGE);
|
variablesDesc.append(action).append("\n");
|
||||||
EventMessageData messageEventData = EventMessageData.builder()
|
} else {
|
||||||
.message(resMessage)
|
variablesDesc.append("- {{").append(name).append("}}");
|
||||||
.build();
|
if (oConvertUtils.isNotEmpty(var.getDescription())) {
|
||||||
eventData.setData(messageEventData);
|
variablesDesc.append(": ").append(var.getDescription());
|
||||||
|
}
|
||||||
|
variablesDesc.append("\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建Prompt
|
||||||
|
StringBuilder promptBuilder = new StringBuilder(Prompts.GENERATE_GUIDE_HEADER);
|
||||||
|
if (!variablesDesc.isEmpty()) {
|
||||||
|
promptBuilder.append(String.format(Prompts.GENERATE_VAR_PART, variablesDesc.toString()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建记忆状态描述
|
||||||
|
if (oConvertUtils.isNotEmpty(memoryId)) {
|
||||||
|
String memoryDescr = "";
|
||||||
|
AiragKnowledge memory = airagKnowledgeService.getById(memoryId);
|
||||||
|
if (memory != null && oConvertUtils.isNotEmpty(memory.getDescr())) {
|
||||||
|
memoryDescr += "记忆库描述:" + memory.getDescr();
|
||||||
|
}
|
||||||
|
promptBuilder.append(String.format(Prompts.GENERATE_MEMORY_PART, memoryDescr));
|
||||||
|
}
|
||||||
|
|
||||||
|
String prompt = promptBuilder.toString();
|
||||||
|
|
||||||
|
List<ChatMessage> messages = List.of(new UserMessage(prompt));
|
||||||
|
|
||||||
|
AIChatParams params = new AIChatParams();
|
||||||
|
params.setTemperature(0.7);
|
||||||
|
|
||||||
|
if(blocking){
|
||||||
|
String promptValue = aiChatHandler.completionsByDefaultModel(messages, params);
|
||||||
|
if (promptValue == null || promptValue.isEmpty()) {
|
||||||
|
return Result.error("生成失败");
|
||||||
|
}
|
||||||
|
return Result.OK("success", promptValue);
|
||||||
|
}else{
|
||||||
|
return startSseChat(messages, params);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送聊天
|
||||||
|
* @param messages
|
||||||
|
* @param params
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
private SseEmitter startSseChat(List<ChatMessage> messages, AIChatParams params) {
|
||||||
|
SseEmitter emitter = new SseEmitter(-0L);
|
||||||
|
// 异步运行(流式)
|
||||||
|
TokenStream tokenStream = aiChatHandler.chatByDefaultModel(messages, params);
|
||||||
|
/**
|
||||||
|
* 是否正在思考
|
||||||
|
*/
|
||||||
|
AtomicBoolean isThinking = new AtomicBoolean(false);
|
||||||
|
String requestId = UUIDGenerator.generate();
|
||||||
|
// ai聊天响应逻辑
|
||||||
|
tokenStream.onPartialResponse((String resMessage) -> {
|
||||||
|
// 兼容推理模型
|
||||||
|
if ("<think>".equals(resMessage)) {
|
||||||
|
isThinking.set(true);
|
||||||
|
resMessage = "> ";
|
||||||
|
}
|
||||||
|
if ("</think>".equals(resMessage)) {
|
||||||
|
isThinking.set(false);
|
||||||
|
resMessage = "\n\n";
|
||||||
|
}
|
||||||
|
if (isThinking.get()) {
|
||||||
|
if (null != resMessage && resMessage.contains("\n")) {
|
||||||
|
resMessage = "\n> ";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EventData eventData = new EventData(requestId, null, EventData.EVENT_MESSAGE);
|
||||||
|
EventMessageData messageEventData = EventMessageData.builder()
|
||||||
|
.message(resMessage)
|
||||||
|
.build();
|
||||||
|
eventData.setData(messageEventData);
|
||||||
|
try {
|
||||||
|
String eventStr = JSONObject.toJSONString(eventData);
|
||||||
|
log.debug("[AI应用]接收LLM返回消息:{}", eventStr);
|
||||||
|
emitter.send(SseEmitter.event().data(eventStr));
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.onCompleteResponse((responseMessage) -> {
|
||||||
|
// 记录ai的回复
|
||||||
|
AiMessage aiMessage = responseMessage.aiMessage();
|
||||||
|
FinishReason finishReason = responseMessage.finishReason();
|
||||||
|
String respText = aiMessage.text();
|
||||||
|
if (FinishReason.STOP.equals(finishReason) || null == finishReason) {
|
||||||
|
// 正常结束
|
||||||
|
EventData eventData = new EventData(requestId, null, EventData.EVENT_MESSAGE_END);
|
||||||
try {
|
try {
|
||||||
String eventStr = JSONObject.toJSONString(eventData);
|
log.debug("[AI应用]接收LLM返回消息完成:{}", respText);
|
||||||
log.debug("[AI应用]接收LLM返回消息:{}", eventStr);
|
emitter.send(SseEmitter.event().data(eventData));
|
||||||
emitter.send(SseEmitter.event().data(eventStr));
|
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
}
|
}
|
||||||
})
|
|
||||||
.onComplete((responseMessage) -> {
|
|
||||||
// 记录ai的回复
|
|
||||||
AiMessage aiMessage = responseMessage.content();
|
|
||||||
FinishReason finishReason = responseMessage.finishReason();
|
|
||||||
String respText = aiMessage.text();
|
|
||||||
if (FinishReason.STOP.equals(finishReason) || null == finishReason) {
|
|
||||||
// 正常结束
|
|
||||||
EventData eventData = new EventData(requestId, null, EventData.EVENT_MESSAGE_END);
|
|
||||||
try {
|
|
||||||
log.debug("[AI应用]接收LLM返回消息完成:{}", respText);
|
|
||||||
emitter.send(SseEmitter.event().data(eventData));
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
closeSSE(emitter, eventData);
|
|
||||||
} else if (FinishReason.TOOL_EXECUTION.equals(finishReason)) {
|
|
||||||
// 需要执行工具
|
|
||||||
// TODO author: chenrui for: date:2025/3/7
|
|
||||||
} else {
|
|
||||||
// 异常结束
|
|
||||||
log.error("调用模型异常:" + respText);
|
|
||||||
if (respText.contains("insufficient Balance")) {
|
|
||||||
respText = "大预言模型账号余额不足!";
|
|
||||||
}
|
|
||||||
EventData eventData = new EventData(requestId, null, EventData.EVENT_FLOW_ERROR);
|
|
||||||
eventData.setData(EventFlowData.builder().success(false).message(respText).build());
|
|
||||||
closeSSE(emitter, eventData);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.onError((Throwable error) -> {
|
|
||||||
// sse
|
|
||||||
String errMsg = "调用大模型接口失败:" + error.getMessage();
|
|
||||||
log.error(errMsg, error);
|
|
||||||
EventData eventData = new EventData(requestId, null, EventData.EVENT_FLOW_ERROR);
|
|
||||||
eventData.setData(EventFlowData.builder().success(false).message(errMsg).build());
|
|
||||||
closeSSE(emitter, eventData);
|
closeSSE(emitter, eventData);
|
||||||
})
|
} else {
|
||||||
.start();
|
// 异常结束
|
||||||
return emitter;
|
log.error("调用模型异常:" + respText);
|
||||||
}
|
if (respText.contains("insufficient Balance")) {
|
||||||
|
respText = "大预言模型账号余额不足!";
|
||||||
|
}
|
||||||
|
EventData eventData = new EventData(requestId, null, EventData.EVENT_FLOW_ERROR);
|
||||||
|
eventData.setData(EventFlowData.builder().success(false).message(respText).build());
|
||||||
|
closeSSE(emitter, eventData);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.onError((Throwable error) -> {
|
||||||
|
// sse
|
||||||
|
String errMsg = "调用大模型接口失败:" + error.getMessage();
|
||||||
|
log.error(errMsg, error);
|
||||||
|
EventData eventData = new EventData(requestId, null, EventData.EVENT_FLOW_ERROR);
|
||||||
|
eventData.setData(EventFlowData.builder().success(false).message(errMsg).build());
|
||||||
|
closeSSE(emitter, eventData);
|
||||||
|
})
|
||||||
|
.start();
|
||||||
|
return emitter;
|
||||||
}
|
}
|
||||||
|
//update-end---author:wangshuai---date:2026-01-05---for:【QQYUN-14479】增加一个开启记忆的按钮。下面为提示词和记忆,将记忆提示词单独拆分---
|
||||||
|
|
||||||
private static void closeSSE(SseEmitter emitter, EventData eventData) {
|
private static void closeSSE(SseEmitter emitter, EventData eventData) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,194 @@
|
|||||||
|
package org.jeecg.modules.airag.app.service.impl;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson.JSONArray;
|
||||||
|
import com.alibaba.fastjson.JSONObject;
|
||||||
|
import dev.langchain4j.agent.tool.ToolSpecification;
|
||||||
|
import dev.langchain4j.model.chat.request.json.JsonObjectSchema;
|
||||||
|
import dev.langchain4j.service.tool.ToolExecutor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.jeecg.common.util.SpringContextUtils;
|
||||||
|
import org.jeecg.common.util.oConvertUtils;
|
||||||
|
import org.jeecg.modules.airag.app.consts.AiAppConsts;
|
||||||
|
import org.jeecg.modules.airag.app.entity.AiragApp;
|
||||||
|
import org.jeecg.modules.airag.app.service.IAiragVariableService;
|
||||||
|
import org.jeecg.modules.airag.app.vo.AppVariableVo;
|
||||||
|
import org.jeecg.modules.airag.common.handler.AIChatParams;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.data.redis.core.RedisTemplate;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Description: AI应用变量服务实现
|
||||||
|
* @Author: jeecg-boot
|
||||||
|
* @Date: 2025-02-26
|
||||||
|
* @Version: V1.0
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
@Slf4j
|
||||||
|
public class AiragVariableServiceImpl implements IAiragVariableService {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private RedisTemplate redisTemplate;
|
||||||
|
|
||||||
|
private static final String CACHE_PREFIX = "airag:app:var:";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化变量(仅不存在时设置)
|
||||||
|
*
|
||||||
|
* @param username
|
||||||
|
* @param appId
|
||||||
|
* @param name
|
||||||
|
* @param defaultValue
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void initVariable(String username, String appId, String name, String defaultValue) {
|
||||||
|
if (oConvertUtils.isEmpty(username) || oConvertUtils.isEmpty(appId) || oConvertUtils.isEmpty(name)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String key = CACHE_PREFIX + appId + ":" + username;
|
||||||
|
redisTemplate.opsForHash().putIfAbsent(key, name, defaultValue != null ? defaultValue : "");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 追加提示词
|
||||||
|
*
|
||||||
|
* @param username
|
||||||
|
* @param app
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public String additionalPrompt(String username, AiragApp app) {
|
||||||
|
String memoryPrompt = app.getMemoryPrompt();
|
||||||
|
String prompt = app.getPrompt();
|
||||||
|
|
||||||
|
if (oConvertUtils.isEmpty(memoryPrompt)) {
|
||||||
|
return prompt;
|
||||||
|
}
|
||||||
|
String variablesStr = app.getVariables();
|
||||||
|
if (oConvertUtils.isEmpty(variablesStr)) {
|
||||||
|
return prompt;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<AppVariableVo> variableList = JSONArray.parseArray(variablesStr, AppVariableVo.class);
|
||||||
|
if (variableList == null || variableList.isEmpty()) {
|
||||||
|
return prompt;
|
||||||
|
}
|
||||||
|
|
||||||
|
String key = CACHE_PREFIX + app.getId() + ":" + username;
|
||||||
|
Map<Object, Object> savedValues = redisTemplate.opsForHash().entries(key);
|
||||||
|
|
||||||
|
for (AppVariableVo variable : variableList) {
|
||||||
|
if (variable.getEnable() != null && !variable.getEnable()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String name = variable.getName();
|
||||||
|
String value = variable.getDefaultValue();
|
||||||
|
|
||||||
|
// 优先使用Redis中的值
|
||||||
|
if (savedValues.containsKey(name)) {
|
||||||
|
Object savedVal = savedValues.get(name);
|
||||||
|
if (savedVal != null) {
|
||||||
|
value = String.valueOf(savedVal);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value == null) {
|
||||||
|
value = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 替换 {{name}}
|
||||||
|
memoryPrompt = memoryPrompt.replace("{{" + name + "}}", value);
|
||||||
|
}
|
||||||
|
return prompt + "\n" + memoryPrompt;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新变量值
|
||||||
|
*
|
||||||
|
* @param userId
|
||||||
|
* @param appId
|
||||||
|
* @param name
|
||||||
|
* @param value
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void updateVariable(String userId, String appId, String name, String value) {
|
||||||
|
if (oConvertUtils.isEmpty(userId) || oConvertUtils.isEmpty(appId) || oConvertUtils.isEmpty(name)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String key = CACHE_PREFIX + appId + ":" + userId;
|
||||||
|
redisTemplate.opsForHash().put(key, name, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加变量更新工具
|
||||||
|
*
|
||||||
|
* @param params
|
||||||
|
* @param aiApp
|
||||||
|
* @param username
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void addUpdateVariableTool(AiragApp aiApp, String username, AIChatParams params) {
|
||||||
|
if (params.getTools() == null) {
|
||||||
|
params.setTools(new HashMap<>());
|
||||||
|
}
|
||||||
|
if (!AiAppConsts.IZ_OPEN_MEMORY.equals(aiApp.getIzOpenMemory())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 构建变量描述信息
|
||||||
|
String variablesStr = aiApp.getVariables();
|
||||||
|
List<AppVariableVo> variableList = null;
|
||||||
|
if (oConvertUtils.isNotEmpty(variablesStr)) {
|
||||||
|
variableList = JSONArray.parseArray(variablesStr, AppVariableVo.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
//工具描述
|
||||||
|
StringBuilder descriptionBuilder = new StringBuilder("更新应用变量的值。仅当检测到变量的新值与当前值不一致时调用。如果已调用过或值未变,请勿重复调用。");
|
||||||
|
if (variableList != null && !variableList.isEmpty()) {
|
||||||
|
descriptionBuilder.append("\n\n可用变量列表:");
|
||||||
|
for (AppVariableVo var : variableList) {
|
||||||
|
if (var.getEnable() != null && !var.getEnable()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
descriptionBuilder.append("\n- ").append(var.getName());
|
||||||
|
if (oConvertUtils.isNotEmpty(var.getDescription())) {
|
||||||
|
descriptionBuilder.append(": ").append(var.getDescription());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
descriptionBuilder.append("\n\n注意:variableName必须是上述列表中的名称之一。");
|
||||||
|
}
|
||||||
|
|
||||||
|
//构建更新变量的工具
|
||||||
|
ToolSpecification spec = ToolSpecification.builder()
|
||||||
|
.name("update_variable")
|
||||||
|
.description(descriptionBuilder.toString())
|
||||||
|
.parameters(JsonObjectSchema.builder()
|
||||||
|
.addStringProperty("variableName", "变量名称")
|
||||||
|
.addStringProperty("value", "变量值")
|
||||||
|
.required("variableName", "value")
|
||||||
|
.build())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
//监听工具的调用
|
||||||
|
ToolExecutor executor = (toolExecutionRequest, memoryId) -> {
|
||||||
|
try {
|
||||||
|
JSONObject args = JSONObject.parseObject(toolExecutionRequest.arguments());
|
||||||
|
String name = args.getString("variableName");
|
||||||
|
String value = args.getString("value");
|
||||||
|
IAiragVariableService variableService = SpringContextUtils.getBean(IAiragVariableService.class);
|
||||||
|
//更新变量值
|
||||||
|
variableService.updateVariable(username, aiApp.getId(), name, value);
|
||||||
|
return "变量 " + name + " 已更新为: " + value;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("更新变量失败", e);
|
||||||
|
return "更新变量失败: " + e.getMessage();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
params.getTools().put(spec, executor);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,48 @@
|
|||||||
|
package org.jeecg.modules.airag.app.vo;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Description: ai写作生成实体类
|
||||||
|
*
|
||||||
|
* @author: wangshuai
|
||||||
|
* @date: 2026/1/12 15:59
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class AiWriteGenerateVo {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 写作类型
|
||||||
|
*/
|
||||||
|
private String activeMode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 写作内容提示
|
||||||
|
*/
|
||||||
|
private String prompt;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 原文
|
||||||
|
*/
|
||||||
|
private String originalContent;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 长度
|
||||||
|
*/
|
||||||
|
private String length;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式
|
||||||
|
*/
|
||||||
|
private String format;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 语气
|
||||||
|
*/
|
||||||
|
private String tone;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 语言
|
||||||
|
*/
|
||||||
|
private String language;
|
||||||
|
}
|
||||||
@ -0,0 +1,46 @@
|
|||||||
|
package org.jeecg.modules.airag.app.vo;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Description: 应用变量配置
|
||||||
|
* @Author: jeecg-boot
|
||||||
|
* @Date: 2025-02-26
|
||||||
|
* @Version: V1.0
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class AppVariableVo implements Serializable {
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 变量名
|
||||||
|
*/
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 描述
|
||||||
|
*/
|
||||||
|
private String description;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 默认值
|
||||||
|
*/
|
||||||
|
private String defaultValue;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否启用
|
||||||
|
*/
|
||||||
|
private Boolean enable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 动作
|
||||||
|
*/
|
||||||
|
private String action;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 排序
|
||||||
|
*/
|
||||||
|
private Integer orderNum;
|
||||||
|
}
|
||||||
@ -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,21 @@ public class ChatConversation {
|
|||||||
* 创建时间
|
* 创建时间
|
||||||
*/
|
*/
|
||||||
private Date createTime;
|
private Date createTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 流程入参配置(工作流的额外参数设置)
|
||||||
|
* key: 参数field, value: 参数值
|
||||||
|
* for [issues/8545]新建AI应用的时候只能选择没有自定义参数的AI流程
|
||||||
|
*/
|
||||||
|
private Map<String, Object> flowInputs;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* portal 应用门户
|
||||||
|
*/
|
||||||
|
private String sessionType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否保存会话
|
||||||
|
*/
|
||||||
|
private Boolean izSaveSession;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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,56 @@ public class ChatSendParams {
|
|||||||
*/
|
*/
|
||||||
private List<String> images;
|
private List<String> images;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文件列表
|
||||||
|
*/
|
||||||
|
private List<String> files;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工作流额外入参配置
|
||||||
|
* key: 参数field, value: 参数值
|
||||||
|
* for [issues/8545]新建AI应用的时候只能选择没有自定义参数的AI流程
|
||||||
|
*/
|
||||||
|
private Map<String, Object> flowInputs;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否开启网络搜索(仅千问模型支持)
|
||||||
|
*/
|
||||||
|
private Boolean enableSearch;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否开启深度思考
|
||||||
|
*/
|
||||||
|
private Boolean enableThink;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 会话类型: portal 应用门户
|
||||||
|
*/
|
||||||
|
private String sessionType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否开启生成绘画
|
||||||
|
*/
|
||||||
|
private Boolean enableDraw;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 绘画模型的id
|
||||||
|
*/
|
||||||
|
private String drawModelId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 图片尺寸
|
||||||
|
*/
|
||||||
|
private String imageSize;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 一张图片
|
||||||
|
*/
|
||||||
|
private String imageUrl;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否保存会话
|
||||||
|
*/
|
||||||
|
private Boolean izSaveSession;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
package org.jeecg.modules.airag.demo;
|
package org.jeecg.modules.airag.demo;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.jeecg.modules.airag.flow.component.enhance.IAiRagEnhanceJava;
|
import org.jeecg.modules.airag.flow.component.enhance.IAiRagEnhanceJava;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
@ -11,12 +12,15 @@ import java.util.Map;
|
|||||||
* @Author: chenrui
|
* @Author: chenrui
|
||||||
* @Date: 2025/3/6 11:42
|
* @Date: 2025/3/6 11:42
|
||||||
*/
|
*/
|
||||||
|
@Slf4j
|
||||||
@Component("testAiragEnhance")
|
@Component("testAiragEnhance")
|
||||||
public class TestAiragEnhance implements IAiRagEnhanceJava {
|
public class TestAiragEnhance implements IAiRagEnhanceJava {
|
||||||
@Override
|
@Override
|
||||||
public Map<String, Object> process(Map<String, Object> inputParams) {
|
public Map<String, Object> process(Map<String, Object> inputParams) {
|
||||||
Object arg1 = inputParams.get("arg1");
|
Object arg1 = inputParams.get("arg1");
|
||||||
Object arg2 = inputParams.get("arg2");
|
Object arg2 = inputParams.get("arg2");
|
||||||
|
Object index = inputParams.get("index");
|
||||||
|
log.info("arg1={}, arg2={}, index={}", arg1, arg2, index);
|
||||||
return Collections.singletonMap("result",arg1.toString()+"java拼接"+arg2.toString());
|
return Collections.singletonMap("result",arg1.toString()+"java拼接"+arg2.toString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -0,0 +1,209 @@
|
|||||||
|
package org.jeecg.modules.airag.llm.consts;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Description: 流程插件常量
|
||||||
|
*
|
||||||
|
* @author: wangshuai
|
||||||
|
* @date: 2025/12/23 19:37
|
||||||
|
*/
|
||||||
|
public interface FlowPluginContent {
|
||||||
|
/**
|
||||||
|
* 名称
|
||||||
|
*/
|
||||||
|
String NAME = "name";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 描述
|
||||||
|
*/
|
||||||
|
String DESCRIPTION = "description";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 响应
|
||||||
|
*/
|
||||||
|
String RESPONSES = "responses";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 类型
|
||||||
|
*/
|
||||||
|
String TYPE = "type";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 参数
|
||||||
|
*/
|
||||||
|
String PARAMETERS = "parameters";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否必须
|
||||||
|
*/
|
||||||
|
String REQUIRED = "required";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 默认值
|
||||||
|
*/
|
||||||
|
String DEFAULT_VALUE = "defaultValue";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 路径
|
||||||
|
*/
|
||||||
|
String PATH = "path";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 方法
|
||||||
|
*/
|
||||||
|
String METHOD = "method";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 位置
|
||||||
|
*/
|
||||||
|
String LOCATION = "location";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 认证类型
|
||||||
|
*/
|
||||||
|
String AUTH_TYPE = "authType";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* token参数名称
|
||||||
|
*/
|
||||||
|
String TOKEN_PARAM_NAME = "tokenParamName";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* token参数值
|
||||||
|
*/
|
||||||
|
String TOKEN_PARAM_VALUE = "tokenParamValue";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* token
|
||||||
|
*/
|
||||||
|
String TOKEN = "token";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Path位置
|
||||||
|
*/
|
||||||
|
String LOCATION_PATH = "Path";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Header位置
|
||||||
|
*/
|
||||||
|
String LOCATION_HEADER = "Header";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Query位置
|
||||||
|
*/
|
||||||
|
String LOCATION_QUERY = "Query";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Body位置
|
||||||
|
*/
|
||||||
|
String LOCATION_BODY = "Body";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Form-Data位置
|
||||||
|
*/
|
||||||
|
String LOCATION_FORM_DATA = "Form-Data";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* String类型
|
||||||
|
*/
|
||||||
|
String TYPE_STRING = "String";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* string类型
|
||||||
|
*/
|
||||||
|
String TYPE_STRING_LOWER = "string";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Number类型
|
||||||
|
*/
|
||||||
|
String TYPE_NUMBER = "Number";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* number类型
|
||||||
|
*/
|
||||||
|
String TYPE_NUMBER_LOWER = "number";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Integer类型
|
||||||
|
*/
|
||||||
|
String TYPE_INTEGER = "Integer";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* integer类型
|
||||||
|
*/
|
||||||
|
String TYPE_INTEGER_LOWER = "integer";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Boolean类型
|
||||||
|
*/
|
||||||
|
String TYPE_BOOLEAN = "Boolean";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* boolean类型
|
||||||
|
*/
|
||||||
|
String TYPE_BOOLEAN_LOWER = "boolean";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工具数量
|
||||||
|
*/
|
||||||
|
String TOOL_COUNT = "tool_count";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否启用
|
||||||
|
*/
|
||||||
|
String ENABLED = "enabled";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 输入
|
||||||
|
*/
|
||||||
|
String INPUTS = "inputs";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 输出
|
||||||
|
*/
|
||||||
|
String OUTPUTS = "outputs";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST请求
|
||||||
|
*/
|
||||||
|
String POST = "POST";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* token名称
|
||||||
|
*/
|
||||||
|
String X_ACCESS_TOKEN = "X-Access-Token";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 插件名称
|
||||||
|
*/
|
||||||
|
String PLUGIN_NAME = "流程调用";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 插件描述
|
||||||
|
*/
|
||||||
|
String PLUGIN_DESC = "调用工作流";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 插件请求地址
|
||||||
|
*/
|
||||||
|
String PLUGIN_REQUEST_URL = "/airag/flow/plugin/run/";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记忆库插件名称
|
||||||
|
*/
|
||||||
|
String PLUGIN_MEMORY_NAME = "记忆库";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记忆库插件描述
|
||||||
|
*/
|
||||||
|
String PLUGIN_MEMORY_DESC = "用于记录长期记忆";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加记忆路径
|
||||||
|
*/
|
||||||
|
String PLUGIN_MEMORY_ADD_PATH = "/airag/knowledge/plugin/add";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询记忆路径
|
||||||
|
*/
|
||||||
|
String PLUGIN_MEMORY_QUERY_PATH = "/airag/knowledge/plugin/query";
|
||||||
|
}
|
||||||
@ -1,5 +1,8 @@
|
|||||||
package org.jeecg.modules.airag.llm.consts;
|
package org.jeecg.modules.airag.llm.consts;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -35,6 +38,11 @@ public class LLMConsts {
|
|||||||
*/
|
*/
|
||||||
public static final String MODEL_TYPE_LLM = "LLM";
|
public static final String MODEL_TYPE_LLM = "LLM";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 模型类型: 图像生成
|
||||||
|
*/
|
||||||
|
public static final String MODEL_TYPE_IMAGE = "IMAGE";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 向量模型:默认维度
|
* 向量模型:默认维度
|
||||||
*/
|
*/
|
||||||
@ -80,4 +88,34 @@ public class LLMConsts {
|
|||||||
*/
|
*/
|
||||||
public static final String KNOWLEDGE_DOC_METADATA_SOURCES_PATH = "sourcesPath";
|
public static final String KNOWLEDGE_DOC_METADATA_SOURCES_PATH = "sourcesPath";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DEEPSEEK推理模型
|
||||||
|
*/
|
||||||
|
public static final String DEEPSEEK_REASONER = "deepseek-reasoner";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 知识库类型:知识库
|
||||||
|
*/
|
||||||
|
public static final String KNOWLEDGE_TYPE_KNOWLEDGE = "knowledge";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 知识库类型:记忆库
|
||||||
|
*/
|
||||||
|
public static final String KNOWLEDGE_TYPE_MEMORY = "memory";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 支持文件的后缀
|
||||||
|
*/
|
||||||
|
public static final Set<String> CHAT_FILE_EXT_WHITELIST = new HashSet<>(Arrays.asList("txt", "pdf", "docx", "doc", "pptx", "ppt", "xlsx", "xls", "md"));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文件内容最大长度
|
||||||
|
*/
|
||||||
|
public static final int CHAT_FILE_TEXT_MAX_LENGTH = 20000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 上传文件对打数量
|
||||||
|
*/
|
||||||
|
public static final int CHAT_FILE_MAX_COUNT = 3;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,31 @@
|
|||||||
|
package org.jeecg.modules.airag.llm.controller;
|
||||||
|
|
||||||
|
import org.jeecg.common.airag.api.IAiragBaseApi;
|
||||||
|
import org.jeecg.modules.airag.llm.service.impl.AiragBaseApiImpl;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* airag baseAPI Controller
|
||||||
|
*
|
||||||
|
* @author sjlei
|
||||||
|
* @date 2025-12-30
|
||||||
|
*/
|
||||||
|
@RestController("airagBaseApiController")
|
||||||
|
public class AiragBaseApiController implements IAiragBaseApi {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
AiragBaseApiImpl airagBaseApi;
|
||||||
|
|
||||||
|
@PostMapping("/airag/api/knowledgeWriteTextDocument")
|
||||||
|
public String knowledgeWriteTextDocument(
|
||||||
|
@RequestParam("knowledgeId") String knowledgeId,
|
||||||
|
@RequestParam("title") String title,
|
||||||
|
@RequestParam("content") String content
|
||||||
|
) {
|
||||||
|
return airagBaseApi.knowledgeWriteTextDocument(knowledgeId, title, content);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user