mirror of
https://github.com/jeecgboot/JeecgBoot.git
synced 2025-12-08 17:12:28 +08:00
Compare commits
117 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 10bf5cf127 | |||
| 6cc74699aa | |||
| 0e8fdc2c5a | |||
| bd6051d972 | |||
| 9cfb9dd2b7 | |||
| 87b41ab36f | |||
| 378f42bcb7 | |||
| 6838888f5f | |||
| bb8281107e | |||
| e9d44bfc80 | |||
| 8057172f8c | |||
| c6dd83a262 | |||
| 7469474124 | |||
| dc9f36905d | |||
| 30486c5358 | |||
| 5b7a90c8bc | |||
| ac40596ad6 | |||
| 6e0edc093b | |||
| 2831996dd4 | |||
| 130793fffe | |||
| 5cabfe1655 | |||
| e99e29a523 | |||
| d64e8b0adb | |||
| 4108ba54c3 | |||
| e8ad887096 | |||
| 6c24b3cf30 | |||
| 208861f808 | |||
| f7fe1dfc58 | |||
| e2805050cf | |||
| b34bb84fc9 | |||
| 11beca16da | |||
| b0c9a2fd9e | |||
| 6b4e8695ae | |||
| e8ca74f2ab | |||
| eabf98533a | |||
| 7848881dcb | |||
| 553bee57f1 | |||
| b751570789 | |||
| e45d29c4ff | |||
| 8c8a448a71 | |||
| 0148f45979 | |||
| 3d414aaec8 | |||
| 7a4c66af57 | |||
| 0fda30b5d2 | |||
| c8e33d43cb | |||
| 6c6af870ae | |||
| 79107599a6 | |||
| b6e8b37aee | |||
| 195ddd0421 | |||
| 9ddad931ff | |||
| fc05fe1aff | |||
| 7e325f68ca | |||
| f74da23d06 | |||
| e279915ba2 | |||
| bb6f077a95 | |||
| 7a031e6135 | |||
| 157877f9a6 | |||
| 25a71fa66c | |||
| fdc02fa68a | |||
| ec3c34969a | |||
| 5a215525d5 | |||
| 450b93d916 | |||
| 2d62bad2a9 | |||
| 0b10096f1c | |||
| 431ddb8fcb | |||
| ddf0f61ae5 | |||
| 4042579167 | |||
| bd5fda5968 | |||
| fdbd9c30ac | |||
| b8b4d3f29d | |||
| 78212aa7c0 | |||
| 6dc3c6af2a | |||
| b7a6812140 | |||
| 7efc51e30e | |||
| bd83b994bc | |||
| 8fb81f331c | |||
| fb188a83a1 | |||
| 9aea5de668 | |||
| 8979dd7ae9 | |||
| a56bd05389 | |||
| 9cf3328ea4 | |||
| 37c593e1d4 | |||
| 49ba40e98a | |||
| 0e184eaa64 | |||
| 86a3ed9dae | |||
| 94bff11eb1 | |||
| 590d73dfe3 | |||
| fe9630d15c | |||
| 77ae25b86a | |||
| 3b34276cf8 | |||
| cffba084fc | |||
| d1589acc41 | |||
| 0d18e536f0 | |||
| 21392c44f8 | |||
| 2730d8e06f | |||
| 0002606d41 | |||
| 8bd19484ee | |||
| 5d2db92613 | |||
| c1b39d21dd | |||
| e83c9b8190 | |||
| 9bd03f467d | |||
| e032591366 | |||
| de767e07b4 | |||
| b77d3e36ab | |||
| 7885aaed3b | |||
| 6f4c2eb77c | |||
| 3f0597a0f6 | |||
| 04a3764f00 | |||
| 68464109de | |||
| 79866c5823 | |||
| d64b8ecaef | |||
| ec9f2b146a | |||
| 69fd2888a1 | |||
| cb1d8e3527 | |||
| a89b299a4b | |||
| 7a15bfc161 | |||
| 65f7eb9542 |
33
README-AI.md
33
README-AI.md
@ -16,6 +16,19 @@ JeecgBoot平台的AIGC功能模块,是一套类似`Dify`的`AIGC应用开发
|
||||
[](https://www.bilibili.com/video/BV1zmd7YFE4w)
|
||||
|
||||
|
||||
##### 功能大模块
|
||||
|
||||
- AI应用开发平台
|
||||
- AI知识库系统
|
||||
- AI大模型管理
|
||||
- AI流程编排
|
||||
- AI对话支持图片
|
||||
- AI对话助手(智能问答)
|
||||
- AI建表(Online表单)
|
||||
- AI写文章(CMS)
|
||||
- AI表单字段建议(表单设计器)
|
||||
|
||||
|
||||
|
||||
#### Dify `VS` JEECG AI
|
||||
|
||||
@ -25,7 +38,7 @@ JeecgBoot平台的AIGC功能模块,是一套类似`Dify`的`AIGC应用开发
|
||||
> - 对话回复格式美观:
|
||||
> 在对话过程中,JEECG AI能够保持回复内容的原格式,也不丢失图片,使得输出的文章更加美观,不会出现格式错乱的情况,还支持图片的渲染。
|
||||
> - PDF文档导入与格式转换:
|
||||
> JEECG AI在处理PDF文档时,能够更好地保持原始格式和图片,确保转换后的内容与原始文档一致。这哥功能在许多AI产品中表现不佳,而JEECG AI在这方面做出了显著的优化
|
||||
> JEECG AI在处理PDF文档时,能够更好地保持原始格式和图片,确保转换后的内容与原始文档一致。这个功能在许多AI产品中表现不佳,而JEECG AI在这方面做出了显著的优化
|
||||
|
||||
|
||||
| 功能 | Dify | Jeecg AI |
|
||||
@ -44,10 +57,10 @@ JeecgBoot平台的AIGC功能模块,是一套类似`Dify`的`AIGC应用开发
|
||||
|
||||
|
||||
|
||||
### 安装向量库 pgvector
|
||||
|
||||
- https://help.jeecg.com/aigc/config
|
||||
### 技术文档
|
||||
|
||||
- [AIGC开发文档](https://help.jeecg.com/aigc)
|
||||
- [安装向量库 pgvector](https://help.jeecg.com/aigc/config)
|
||||
|
||||
|
||||
|
||||
@ -63,11 +76,17 @@ JeecgBoot平台的AIGC功能模块,是一套类似`Dify`的`AIGC应用开发
|
||||
|
||||
|
||||
|
||||
## 产品体验
|
||||
|
||||
- 使用手册:https://help.jeecg.com/aigc
|
||||
- 演示地址:https://boot3.jeecg.com
|
||||
#### 在线体验
|
||||
|
||||
- JeecgBoot演示: https://boot3.jeecg.com
|
||||
- 敲敲云在线搭建AI知识库:https://app.qiaoqiaoyun.com
|
||||
|
||||
|
||||
## 技术交流
|
||||
|
||||
- 开发文档:https://help.jeecg.com/aigc
|
||||
- QQ群:964611995、716488839(满)
|
||||
|
||||
|
||||
## 功能列表
|
||||
|
||||
@ -7,12 +7,12 @@
|
||||
JEECG BOOT AI Low Code Platform
|
||||
===============
|
||||
|
||||
Current version: 3.8.0 (Release date: 2025-04-18)
|
||||
Current version: 3.8.1 (Release date: 2025-06-30)
|
||||
|
||||
|
||||
[](https://github.com/zhangdaiscott/jeecg-boot/blob/master/LICENSE)
|
||||
[](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)
|
||||
|
||||
@ -81,7 +81,7 @@ Technical documentation
|
||||
- Demo : [OnlineDemo](http://boot3.jeecg.com) | [APP](http://jeecg.com/appIndex)
|
||||
- Doc: [DocumentCenter](http://help.jeecg.com) | [AI Config](https://help.jeecg.com/java/ai/aichat)
|
||||
- Newbie guide: [Quick start](http://www.jeecg.com/doc/quickstart) | [Q&A ](http://www.jeecg.com/doc/qa) | [1 minute experience](https://my.oschina.net/jeecg/blog/3083313)
|
||||
- QQ group : ⑩716488839、⑨808791225
|
||||
- QQ group : 964611995、⑩716488839(满)、⑨808791225(满)
|
||||
|
||||
|
||||
|
||||
|
||||
124
README-Enterprise.md
Normal file
124
README-Enterprise.md
Normal file
@ -0,0 +1,124 @@
|
||||
|
||||
JeecgBoot低代码平台(商业版介绍)
|
||||
===============
|
||||
|
||||
|
||||
|
||||
项目介绍
|
||||
-----------------------------------
|
||||
|
||||
<h3 align="center">企业级AI低代码平台</h3>
|
||||
|
||||
|
||||
JeecgBoot是一款集成AI应用的,基于BPM流程的低代码平台,旨在帮助企业快速实现低代码开发和构建个性化AI应用!前后端分离架构Ant Design&Vue3,SpringBoot,SpringCloud Alibaba,Mybatis-plus,Shiro。强大的代码生成器让前后端代码一键生成,无需写任何代码! 引领AI低代码开发模式: AI生成->OnlineCoding-> 代码生成-> 手工MERGE, 帮助Java项目解决80%的重复工作,让开发更多关注业务,提高效率、节省成本,同时又不失灵活性!低代码能力:Online表单、表单设计、流程设计、Online报表、大屏/仪表盘设计、报表设计; AI应用平台功能:AI知识库问答、AI模型管理、AI流程编排、AI聊天等,支持含ChatGPT、DeepSeek、Ollama等多种AI大模型
|
||||
|
||||
JeecgBoot 提供了一系列 `低代码能力`,实现`真正的零代码`在线开发:Online表单开发、Online报表、复杂报表设计、打印设计、在线图表设计、仪表盘设计、大屏设计、移动图表能力、表单设计器、在线设计流程、流程自动化配置、插件能力(可插拔)
|
||||
|
||||
`AI赋能低代码:` 目前提供了AI应用、AI模型管理、AI流程编排、AI对话助手,AI建表、AI写文章、AI知识库问答、AI字段建议等功能;支持各种AI大模型ChatGPT、DeepSeek、Ollama、智普、千问等.
|
||||
|
||||
`JEECG宗旨是:` 简单功能由OnlineCoding配置实现,做到`零代码开发`;复杂功能由代码生成器生成进行手工Merge 实现`低代码开发`,既保证了`智能`又兼顾`灵活`;实现了低代码开发的同时又支持灵活编码,解决了当前低代码产品普遍不灵活的弊端!
|
||||
|
||||
`JEECG业务流程:` 采用工作流来实现、扩展出任务接口,供开发编写业务逻辑,表单提供多种解决方案: 表单设计器、online配置表单、编码表单。同时实现了流程与表单的分离设计(松耦合)、并支持任务节点灵活配置,既保证了公司流程的保密性,又减少了开发人员的工作量。
|
||||
|
||||
|
||||
|
||||
#### JeecgBoot商业版与同类产品区别
|
||||
-----------------------------------
|
||||
|
||||
- 灵活性:jeecgboot基于开源技术栈,设计初考虑到可插拔性和集成灵活性,确保平台的智能性与灵活性,避免因平台过于庞大而导致的扩展困难。
|
||||
- 流程管理:支持一个表单挂接多个流程,同时一个流程可以连接多个表单,增强了流程的灵活性和复杂性管理。
|
||||
- 符合中国国情的流程:针对中国市场的特定需求,jeecgboot能够实现各种符合中国国情的业务流程。
|
||||
- 强大的表单设计器:jeecgboot的表单设计器与敲敲云共享,具备高质量和智能化的特点,能够满足零代码应用的需求,业内同类产品中不多见。
|
||||
- 报表功能:自主研发的报表工具,拥有独立知识产权,功能上比业内老牌产品如帆软更智能,操作简便。
|
||||
- BI产品整合:提供大屏、仪表盘、门户等功能,完美解决这些需求,并支持移动面板的设计与渲染。
|
||||
- 自主研发的模块:jeecgboot的所有模块均为自主研发,具有独立的知识产权。
|
||||
- 颗粒度和功能细致:在功能细致度和颗粒度上,jeecgboot远超同类产品,尤其在零代码能力方面表现突出。
|
||||
- 零代码应用管理:最新版支持与敲敲云的零代码应用管理能力的集成,使得jeecgboot既具备低代码,又具备零代码的应用能力,业内独一无二。
|
||||
- 强大的代码生成器:作为开源代码生成器的先锋,jeecgboot在代码生成的智能化和在线低代码与代码生成的结合方面,优势明显。
|
||||
- 精细化权限管理:提供行级和列级的数据权限控制,满足企业在ERP和OA领域对权限管理的严格需求。
|
||||
- 多平台支持的APP:目前采用uniapp3实现,支持小程序、H5、App及鸿蒙、鸿蒙Next、Electron桌面应用等多种终端。
|
||||
|
||||
> 综上所述,jeecgboot不仅在功能上具备丰富性和灵活性,还在技术架构、权限管理和用户体验等方面展现出明显的优势,是一个综合性能强大的低代码平台。
|
||||
|
||||
|
||||
|
||||
商业版演示
|
||||
-----------------------------------
|
||||
|
||||
JeecgBoot vs 敲敲云
|
||||
> - JeecgBoot是低代码产品拥有系列低代码能力,比如流程设计、表单设计、大屏设计,代码生成器,适合半开发模式(开发+低代码结合),也可以集成零代码应用管理模块.
|
||||
> - 敲敲云是零代码产品,完全不写代码,通过配置搭建业务系统,其在jeecgboot基础上研发而成,删除了online、代码生成、OA等需要编码功能,只保留应用管理功能和聊天、日程、文件三个OA组件.
|
||||
|
||||
|
||||
- JeecgBoot低代码: https://boot3.jeecg.com
|
||||
- 敲敲云零代码:https://app.qiaoqiaoyun.com
|
||||
- APP演示(多端): http://jeecg.com/appIndex
|
||||
|
||||
|
||||
### 流程视频介绍
|
||||
|
||||
[](https://www.bilibili.com/video/BV1Nk4y1o7Qc)
|
||||
|
||||
|
||||
|
||||
### 商业版功能简述
|
||||
|
||||
> 详细的功能介绍,[请联系官方](https://jeecg.com/vip)
|
||||
|
||||
```
|
||||
│─更多商业功能
|
||||
│ ├─流程设计器
|
||||
│ ├─简流设计器(类钉钉版)
|
||||
│ ├─门户设计(NEW)
|
||||
│ ├─表单设计器
|
||||
│ ├─大屏设计器
|
||||
│ └─我的任务
|
||||
│ └─历史流程
|
||||
│ └─历史流程
|
||||
│ └─流程实例管理
|
||||
│ └─流程监听管理
|
||||
│ └─流程表达式
|
||||
│ └─我发起的流程
|
||||
│ └─我的抄送
|
||||
│ └─流程委派、抄送、跳转
|
||||
│ └─OA办公组件
|
||||
│ └─零代码应用管理(无需编码,在线搭建应用系统)
|
||||
│ ├─积木报表企业版(含jimureport、jimubi)
|
||||
│ ├─AI流程设计器源码
|
||||
│ ├─Online全模块功能和源码
|
||||
│ ├─AI写文章(CMS)
|
||||
│ ├─AI表单字段建议(表单设计器)
|
||||
│ ├─OA办公协同组件
|
||||
│ ├─在线聊天功能
|
||||
│ ├─设计表单移动适配
|
||||
│ ├─设计表单支持外部填报
|
||||
│ ├─设计表单AI字段建议
|
||||
│ ├─设计表单视图功能(支持多种类型含日历、表格、看板、甘特图)
|
||||
│ └─。。。
|
||||
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
##### 流程设计
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

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

|
||||
|
||||

|
||||
|
||||

|
||||
384
README.md
384
README.md
@ -2,12 +2,12 @@
|
||||
JeecgBoot AI低代码平台
|
||||
===============
|
||||
|
||||
当前最新版本: 3.8.0(发布日期:2025-04-18)
|
||||
当前最新版本: 3.8.1(发布日期:2025-06-30)
|
||||
|
||||
|
||||
[](https://github.com/jeecgboot/JeecgBoot/blob/master/LICENSE)
|
||||
[](http://guojusoft.com)
|
||||
[](https://github.com/jeecgboot/JeecgBoot)
|
||||
[](https://github.com/jeecgboot/JeecgBoot)
|
||||
[](https://github.com/jeecgboot/JeecgBoot)
|
||||
[](https://github.com/jeecgboot/JeecgBoot)
|
||||
|
||||
@ -16,54 +16,49 @@ JeecgBoot AI低代码平台
|
||||
项目介绍
|
||||
-----------------------------------
|
||||
|
||||
<h3 align="center">Java AI Low Code Platform</h3>
|
||||
<h3 align="center">企业级AI低代码平台</h3>
|
||||
|
||||
JeecgBoot是一款基于AIGC和低代码引擎的AI低代码平台,旨在帮助开发者快速实现低代码开发和构建、部署个性化的 AI 应用。
|
||||
前后端分离架构Ant Design&Vue3,SpringBoot,SpringCloud Alibaba,Mybatis-plus,Shiro,强大的代码生成器让前后端代码一键生成,无需写任何代码!
|
||||
成套AI大模型功能: AI模型管理、AI应用、知识库、AI流程编排、AI对话助手等;
|
||||
引领AI低代码开发模式: AIGC生成->OnlineCoding-> 代码生成-> 手工MERGE, 帮助Java项目解决80%的重复工作,让开发更多关注业务,快速提高效率 节省成本,同时又不失灵活性!
|
||||
JeecgBoot是一款企业级低代码平台集成了AI应用平台功能,旨在帮助开发者快速实现低代码开发和构建、部署个性化的 AI 应用。
|
||||
前后端分离架构Ant Design4、Vue3,SpringBoot,SpringCloud Alibaba,Mybatis-plus,Shiro/SpringAuthorizationServer,强大的代码生成器让前后端代码一键生成,无需写任何代码;提供强大的报表和大屏工具,满足企业级数据产品需求!
|
||||
引领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、智普、千问等.
|
||||
|
||||
`AI赋能低代码:` 目前提供了AI应用、AI模型管理、AI流程编排、AI对话助手,AI建表、AI写文章、AI知识库问答、AI字段建议等功能;支持各种AI大模型ChatGPT、DeepSeek、Ollama、智普、千问等.
|
||||
|
||||
`JEECG宗旨是:` 简单功能由OnlineCoding配置实现,做到`零代码开发`;复杂功能由代码生成器生成进行手工Merge 实现`低代码开发`,既保证了`智能`又兼顾`灵活`;实现了低代码开发的同时又支持灵活编码,解决了当前低代码产品普遍不灵活的弊端!
|
||||
`JEECG宗旨是:` 简单功能由OnlineCoding零代码搭建,做到`零代码开发`;复杂功能由代码生成器生成进行手工Merge 实现`低代码开发`,既保证了`智能`又兼顾`灵活`,解决了当前低代码产品普遍不灵活的弊端!
|
||||
|
||||
`JEECG业务流程:` 采用工作流来实现、扩展出任务接口,供开发编写业务逻辑,表单提供多种解决方案: 表单设计器、online配置表单、编码表单。同时实现了流程与表单的分离设计(松耦合)、并支持任务节点灵活配置,既保证了公司流程的保密性,又减少了开发人员的工作量。
|
||||
|
||||
|
||||
|
||||
### 视频介绍
|
||||
|
||||
[](https://www.bilibili.com/video/BV1Nk4y1o7Qc)
|
||||
|
||||
|
||||
适用项目
|
||||
-----------------------------------
|
||||
JeecgBoot AI低代码平台,可以应用在任何J2EE项目的开发中,支持信创国产化(默认适配达梦和人大金仓)。尤其适合SAAS项目、企业信息管理系统(MIS)、内部办公系统(OA)、企业资源计划系统(ERP)、客户关系管理系统(CRM)等,其半智能手工Merge的开发方式,可以显著提高开发效率70%以上,极大降低开发成本。
|
||||
JeecgBoot低代码平台,可以应用在任何J2EE项目的开发中,支持信创国产化。尤其适合SAAS项目、企业信息管理系统(MIS)、内部办公系统(OA)、企业资源计划系统(ERP)、客户关系管理系统(CRM)、AI知识库等,其半智能手工Merge的开发方式,可以显著提高开发效率70%以上,极大降低开发成本。
|
||||
又是一个全栈式 AI 开发平台,快速帮助企业构建和部署个性化的 AI 应用。
|
||||
|
||||
**信创兼容说明**
|
||||
- 操作系统:国产麒麟、银河麒麟等国产系统几乎都是基于 Linux 内核,因此它们具有良好的兼容性。
|
||||
- 数据库:达梦、人大金仓、TiDB
|
||||
- 中间件:东方通 TongWeb、TongRDS,宝兰德 AppServer、CacheDB, [信创配置文档](https://help.jeecg.com/java/tongweb-deploy/)
|
||||
|
||||
项目说明
|
||||
|
||||
版本说明
|
||||
-----------------------------------
|
||||
|
||||
| 项目名 | 说明 |
|
||||
|--------------------|------------------------|
|
||||
| `jeecg-boot` | 后端源码JAVA(SpringBoot微服务架构) |
|
||||
| `jeecgboot-vue3` | 前端源码VUE3(vue3+vite6+ts最新技术栈) |
|
||||
| `JeecgUniapp` | [配套APP框架](https://github.com/jeecgboot/JeecgUniapp) 适配多个终端,支持APP、小程序、H5 |
|
||||
|下载 | JDK17 + SpringBoot2.7 | JDK17 + SpringBoot3.3 + Shiro |JDK17 + SpringBoot3.3+ SpringAuthorizationServer |
|
||||
|------|----------------------------------------------------|-----------------------------------------------------------------------------|--------------------------------------------|
|
||||
| Github | [`master`](https://github.com/jeecgboot/JeecgBoot) | [`springboot3`](https://github.com/jeecgboot/JeecgBoot/tree/springboot3) 分支 | [`springboot3_sas`](https://github.com/jeecgboot/JeecgBoot/tree/springboot3_sas) 分支 |
|
||||
| Gitee | [`master`](https://gitee.com/jeecg/JeecgBoot) | [`springboot3`](https://gitee.com/jeecg/JeecgBoot/tree/springboot3/) 分支 | [`springboot3_sas`](https://gitee.com/jeecg/JeecgBoot/tree/springboot3_sas) 分支 |
|
||||
|
||||
|
||||
- `jeecg-boot` 是后端JAVA源码项目(支持单体和微服务切换).
|
||||
- `jeecgboot-vue3` 是前端VUE3源码项目(vue3+vite6+ts最新技术栈).
|
||||
- `JeecgUniapp` 是[配套APP框架](https://github.com/jeecgboot/JeecgUniapp) 适配多个终端,支持APP、小程序、H5、鸿蒙、鸿蒙Next.
|
||||
- 参考 [文档](https://help.jeecg.com/ui/2dev/mini) 可以删除不需要的demo,制作一个精简版本
|
||||
|
||||
技术文档
|
||||
-----------------------------------
|
||||
|
||||
- 官方网站: [http://www.jeecg.com](http://www.jeecg.com)
|
||||
- 在线演示 : [平台演示](http://boot3.jeecg.com) | [APP演示](http://jeecg.com/appIndex) | [体验低代码](https://jeecg.blog.csdn.net/article/details/106079007) | [体验零代码](https://app.qiaoqiaoyun.com/myapps/index)
|
||||
- 开发文档: [文档中心](https://help.jeecg.com) | [AIGC大模块](https://help.jeecg.com/aigc)
|
||||
- 新手指南: [快速入门](http://www.jeecg.com/doc/quickstart) | [入门视频](http://jeecg.com/doc/video) | [如何反馈问题](https://github.com/jeecgboot/JeecgBoot/issues/new?template=bug_report.md)
|
||||
- QQ交流群 : ⑩716488839、⑨808791225(满)、其他(满)
|
||||
|
||||
|
||||
|
||||
@ -75,63 +70,83 @@ JeecgBoot AI低代码平台,可以应用在任何J2EE项目的开发中,支
|
||||
|
||||
|
||||
|
||||
AIGC应用平台介绍
|
||||
技术文档
|
||||
-----------------------------------
|
||||
|
||||
JeecgBoot 平台的AIGC功能模块,是一套类似`Dify`的`AIGC应用开发平台`+`知识库问答`,是一款基于LLM大语言模型AI应用平台和 RAG 的知识库问答系统。
|
||||
其直观的界面结合了 AI 流程编排、RAG 管道、知识库管理、模型管理、对接向量库、实时运行可观察等,让您可以快速从原型到生产,拥有AI服务能力。
|
||||
|
||||
> JDK说明:AI流程编排引擎暂时不支持jdk21,所以目前只能使用jdk8或者jdk17启动项目。
|
||||
|
||||
- [AIGC专题介绍页](README-AI.md)
|
||||
- [AIGC开发文档](https://help.jeecg.com/aigc)
|
||||
- [配置向量库PGVector](https://help.jeecg.com/aigc/config)
|
||||
- 官方网站: [http://www.jeecg.com](http://www.jeecg.com)
|
||||
- 入门指南: [快速入门](http://www.jeecg.com/doc/quickstart) | [开发文档](https://help.jeecg.com) | [AI应用使用手册](https://help.jeecg.com/aigc)
|
||||
- 技术支持: [反馈问题](https://github.com/jeecgboot/JeecgBoot/issues/new?template=bug_report.md) | [视频教程](http://jeecg.com/doc/video) | [低代码体验一分钟](https://jeecg.blog.csdn.net/article/details/106079007)
|
||||
- QQ交流群 : 964611995、⑩716488839(满)、⑨808791225(满)、其他(满)
|
||||
|
||||
|
||||
##### AI视频介绍
|
||||
AI应用平台功能介绍
|
||||
-----------------------------------
|
||||
|
||||
[](https://www.bilibili.com/video/BV1zmd7YFE4w)
|
||||
JeecgBoot平台提供了一套完善的AI应用管理系统模块,是一套类似`Dify`的`AIGC应用开发平台`+`知识库问答`,是一款基于LLM大语言模型AI应用平台和 RAG 的知识库问答系统。
|
||||
其直观的界面结合了 AI 流程编排、RAG 管道、知识库管理、模型管理、对接向量库、实时运行可观察等,让您可以快速从原型到生产,拥有AI服务能力。 [详细专题介绍,请点击查看](README-AI.md)
|
||||
|
||||
|
||||
##### Dify `VS` JEECG AI
|
||||
|
||||
> JEECG AI与Dify相比,在多个方面展现出显著的优势,特别是在文档处理、格式和图片保持方面。以下是一些具体的优点:
|
||||
> - Markdown文档库导入:
|
||||
> JEECG AI允许用户直接导入整个Markdown文档库,这不仅保留markdown格式,还支持图片的导入,确保文档内容的完整性和可视化效果。
|
||||
> - 对话回复格式美观:
|
||||
> 在对话过程中,JEECG AI能够保持回复内容的原格式,也不丢失图片,使得输出的文章更加美观,不会出现格式错乱的情况,还支持图片的渲染。
|
||||
> - PDF文档导入与格式转换:
|
||||
> JEECG AI在处理PDF文档时,能够更好地保持原始格式和图片,确保转换后的内容与原始文档一致。这哥功能在许多AI产品中表现不佳,而JEECG AI在这方面做出了显著的优化
|
||||
|
||||
##### 功能大模块
|
||||
|
||||
- AI应用开发平台
|
||||
- AI知识库系统
|
||||
- AI大模型管理
|
||||
- AI流程编排
|
||||
- AI对话支持图片
|
||||
- AI对话助手(智能问答)
|
||||
- AI建表(Online表单)
|
||||
- AI写文章(CMS)
|
||||
- AI表单字段建议(表单设计器)
|
||||
|
||||
##### AI大模型支持
|
||||
|
||||
| AI大模型 | 支持 |
|
||||
| --- | --- |
|
||||
| DeepSeek | √ |
|
||||
| ChatGTP | √ |
|
||||
| Qwq | √ |
|
||||
| 智库 | √ |
|
||||
| Ollama本地模型 | √ |
|
||||
| 等等。。 | √ |
|
||||
|
||||
为什么选择JeecgBoot?
|
||||
-----------------------------------
|
||||
- 1.采用最新主流前后分离框架(Spring Boot + MyBatis + 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.提供丰富的示例代码,涵盖了常用的业务场景,便于学习和参考。
|
||||
|
||||
|
||||
|
||||
技术架构:
|
||||
-----------------------------------
|
||||
|
||||
#### 前端
|
||||
|
||||
- 前端环境要求:Node.js要求`Node 20+` 版本以上、pnpm 要求`9+` 版本以上
|
||||
- 依赖管理: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
|
||||
|
||||
|
||||
#### 后端
|
||||
|
||||
- IDE建议: IDEA (必须安装lombok插件 )
|
||||
@ -148,28 +163,9 @@ JeecgBoot 平台的AIGC功能模块,是一套类似`Dify`的`AIGC应用开发
|
||||
- 日志打印:logback
|
||||
- 缓存:Redis
|
||||
- 其他:autopoi, fastjson,poi,Swagger-ui,quartz, lombok(简化代码)等。
|
||||
- 默认数据库脚本:MySQL5.7+
|
||||
- [其他数据库,需要自己转](https://my.oschina.net/jeecg/blog/4905722)
|
||||
- 默认提供MySQL5.7+数据库脚本
|
||||
|
||||
|
||||
#### 前端
|
||||
|
||||
- 前端IDE建议:WebStorm、Vscode
|
||||
- 采用 Vue3.0+TypeScript+Vite6+Ant-Design-Vue等新技术方案,包括二次封装组件、utils、hooks、动态菜单、权限校验、按钮级别权限控制等功能
|
||||
- 最新技术栈:Vue3.0 + TypeScript + Vite6 + ant-design-vue4 + pinia + echarts + unocss + vxe-table + qiankun + es6
|
||||
- 依赖管理:node、npm、pnpm
|
||||
|
||||
|
||||
#### 前端环境要求
|
||||
|
||||
* 本地环境安装 `Node.js 、npm 、pnpm`
|
||||
* pnpm 要求`9+` 版本以上
|
||||
* Node.js 版本建议`v20.15.0`,要求`Node 20+` 版本以上
|
||||
|
||||
` ( 因为Vite6 需要 Node.js 18 / 20+ )`
|
||||
|
||||
|
||||
#### 平台支持数据库
|
||||
#### 数据库支持
|
||||
|
||||
> jeecgboot平台支持以下数据库,默认我们只提供mysql脚本,其他数据库可以参考[转库文档](https://my.oschina.net/jeecg/blog/4905722)自己转。
|
||||
|
||||
@ -184,11 +180,17 @@ JeecgBoot 平台的AIGC功能模块,是一套类似`Dify`的`AIGC应用开发
|
||||
| 人大金仓 | √ |
|
||||
| TiDB | √ |
|
||||
| kingbase8 | √ |
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## 微服务解决方案
|
||||
|
||||
> 微服务方式快速启动
|
||||
> - [单体快速切换微服务](https://help.jeecg.com/java/springcloud/switchcloud/monomer)
|
||||
> - [Docker一键启动微服务前后端](https://help.jeecg.com/java/docker/quickcloud)
|
||||
|
||||
|
||||
|
||||
- 1、服务注册和发现 Nacos √
|
||||
- 2、统一配置中心 Nacos √
|
||||
@ -207,62 +209,23 @@ JeecgBoot 平台的AIGC功能模块,是一套类似`Dify`的`AIGC应用开发
|
||||
- 15、CAS 单点登录 √
|
||||
- 16、路由限流 √
|
||||
|
||||
|
||||
#### 微服务方式启动
|
||||
|
||||
- [单体快速切换微服务](https://help.jeecg.com/java/springcloud/switchcloud/monomer)
|
||||
- [Docker一键启动微服务前后端](https://help.jeecg.com/java/docker/quickcloud)
|
||||
|
||||
|
||||
#### 微服务架构图
|
||||

|
||||
|
||||
|
||||
|
||||
为什么选择JeecgBoot?
|
||||
|
||||
|
||||
开源版与企业版区别?
|
||||
-----------------------------------
|
||||
* 1.采用最新主流前后分离框架(Springboot+Mybatis+antd+vue3),容易上手; 代码生成器依赖性低,灵活的扩展能力,可快速实现二次开发;
|
||||
* 2.支持微服务SpringCloud Alibaba(Nacos、Gateway、Sentinel、Skywalking),提供切换机制支持单体和微服务自由切换
|
||||
* 3.开发效率高,采用代码生成器,单表、树列表、一对多、一对一等数据模型,增删改查功能一键生成,菜单配置直接使用;引入AI能力,支持自动建表等功能;
|
||||
* 4.代码生成器提供强大模板机制,支持自定义模板,目前提供四套风格模板(单表两套、树模型一套、一对多三套)
|
||||
* 5.代码生成器非常智能,在线业务建模、在线配置、所见即所得支持23种类控件,一键生成前后端代码,大幅度提升开发效率,不再为重复工作发愁。
|
||||
* 6.AI能力:目前JeecgBoot支持AI大模型chatgpt和deepseek,现在最新版默认使用deepseek,速度更快质量更高。目前提供了AI对话助手、AI建表、AI报表等功能。
|
||||
* 6.低代码能力:Online在线表单(无需编码,通过在线配置表单,实现表单的增删改查,支持单表、树、一对多、一对一等模型,实现人人皆可编码)
|
||||
* 7.低代码能力:Online在线报表、Online在线图表(无需编码,通过在线配置方式,实现数据报表和图形报表,可以快速抽取数据,减轻开发压力,实现人人皆可编码)
|
||||
* 9.封装完善的用户、角色、菜单、组织机构、数据字典、在线定时任务等基础功能,支持访问授权、按钮权限、数据权限等功能
|
||||
* 10.常用共通封装,各种工具类(定时任务,短信接口,邮件发送,Excel导入导出等),基本满足80%项目需求
|
||||
* 11.简易Excel导入导出,支持单表导出和一对多表模式导出,生成的代码自带导入导出功能
|
||||
* 12.集成简易报表工具,图像报表和数据导出非常方便,可极其方便的生成图形报表、pdf、excel、word等报表;
|
||||
* 13.采用前后分离技术,页面UI风格精美,针对常用组件做了封装:时间、行表格控件、截取显示控件、报表组件,编辑器等等
|
||||
* 14.查询过滤器:查询功能自动生成,后台动态拼SQL追加查询条件;支持多种匹配方式(全匹配/模糊查询/包含查询/不匹配查询);
|
||||
* 15.数据权限(精细化数据权限控制,控制到行级,列表级,表单字段级,实现不同人看不同数据,不同人对同一个页面操作不同字段
|
||||
* 16.页面校验自动生成(必须输入、数字校验、金额校验、时间空间等);
|
||||
* 17.支持SAAS服务模式,提供SaaS多租户架构方案。
|
||||
* 18.分布式文件服务,集成minio、阿里OSS等优秀的第三方,提供便捷的文件上传与管理,同时也支持本地存储。
|
||||
* 19.主流数据库兼容,一套代码完全兼容Mysql、Postgresql、Oracle、Sqlserver、MariaDB、达梦、人大金仓等主流数据库。
|
||||
* 20.集成工作流flowable,并实现了只需在页面配置流程转向,可极大的简化bpm工作流的开发;用bpm的流程设计器画出了流程走向,一个工作流基本就完成了,只需写很少量的java代码;
|
||||
* 21.低代码能力:在线流程设计,采用开源flowable流程引擎,实现在线画流程,自定义表单,表单挂靠,业务流转
|
||||
* 22.多数据源:及其简易的使用方式,在线配置数据源配置,便捷的从其他数据抓取数据;
|
||||
* 23.提供单点登录CAS集成方案,项目中已经提供完善的对接代码
|
||||
* 24.低代码能力:表单设计器,支持用户自定义表单布局,支持单表,一对多表单、支持select、radio、checkbox、textarea、date、popup、列表、宏等控件
|
||||
* 25.专业接口对接机制,统一采用restful接口方式,集成swagger-ui在线接口文档,Jwt token安全验证,方便客户端对接
|
||||
* 26.接口安全机制,可细化控制接口授权,非常简便实现不同客户端只看自己数据等控制
|
||||
* 27.高级组合查询功能,在线配置支持主子表关联查询,可保存查询历史
|
||||
* 28.提供各种系统监控,实时跟踪系统运行情况(监控 Redis、Tomcat、jvm、服务器信息、请求追踪、SQL监控)
|
||||
* 29.消息中心(支持短信、邮件、微信推送等等)
|
||||
* 30.集成Websocket消息通知机制
|
||||
* 31.移动自适应效果优秀,提供APP发布方案:
|
||||
* 32.支持多语言,提供国际化方案;
|
||||
* 33.数据变更记录日志,可记录数据每次变更内容,通过版本对比功能查看历史变化
|
||||
* 34.平台UI强大,实现了移动自适应
|
||||
* 35.平台首页风格,提供多种组合模式,支持自定义风格
|
||||
* 36.提供简单易用的打印插件,支持谷歌、火狐、IE11+ 等各种浏览器
|
||||
* 37.示例代码丰富,提供很多学习案例参考
|
||||
* 38.采用maven分模块开发方式
|
||||
* 39.支持菜单动态路由
|
||||
* 40.权限控制采用 RBAC(Role-Based Access Control,基于角色的访问控制)
|
||||
* 41.提供新行编辑表格JVXETable,轻松满足各种复杂ERP布局,拥有更高的性能、更灵活的扩展、更强大的功能
|
||||
* 42.提供仪表盘设计器,类大屏设计支持移动端,免费的数据可视化设计工具,支持丰富的数据源连接,能够通过拖拉拽方式快速制作图表和门户设计;目前支持多种图表类型:柱形图、折线图、散点图、饼图、环形图、面积图、漏斗图、进度图、仪表盘、雷达图、地图等等;
|
||||
|
||||
- JeecgBoot开源版采用 [Apache-2.0 license](LICENSE) 开源协议,允许商用使用,不会造成侵权行为,允许基于本平台软件开展业务系统开发(在任何情况下,您不得使用本软件开发可能被认为与本软件竞争的软件).
|
||||
- 商业版与开源版主要区别在于商业版提供了技术支持 和 更多的企业级功能(例如:Online图表、流程监控、流程设计、流程审批、表单设计器、表单视图、积木报表企业版、OA办公、商业APP、零代码应用、Online模块源码等功能). [更多商业功能介绍,点击查看](README-Enterprise.md)
|
||||
- JeecgBoot未来发展方向是:零代码平台的建设,也就是团队的另外一款产品 [敲敲云零代码](https://www.qiaoqiaoyun.com) ,无需编码即可通过拖拽快速搭建企业级应用,与JeecgBoot低代码平台形成互补,满足从简单业务到复杂系统的全场景开发需求,目前已经上线,[欢迎注册体验](https://app.qiaoqiaoyun.com)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
### Jeecg Boot 产品功能蓝图
|
||||
@ -271,42 +234,9 @@ JeecgBoot 平台的AIGC功能模块,是一套类似`Dify`的`AIGC应用开发
|
||||
|
||||
|
||||
|
||||
### 分支说明
|
||||
|
||||
> 主干master更稳定,如果你对最新技术栈无要求,建议采用主干
|
||||
|
||||
#### springboot3分支
|
||||
- 源码地址:https://github.com/jeecgboot/JeecgBoot/tree/springboot3
|
||||
- 架构说明:升级Spring Boot3 & JDK 17 + Undertow + springdoc + fastjson2
|
||||
|
||||
#### springboot3_sas分支
|
||||
- 源码地址:https://github.com/jeecgboot/JeecgBoot/tree/springboot3_sas
|
||||
- 架构说明:在springboot3分支基础上,采用SpringAuthorizationServer替换Shiro
|
||||
|
||||
|
||||
|
||||
### 功能模块
|
||||
### 开源版功能清单
|
||||
```
|
||||
├─AI开发
|
||||
│ ├─支持AI大模型ChatGPT和DeepSeek
|
||||
│ ├─AI对话助手
|
||||
│ ├─AI建表
|
||||
│ ├─AI写文章
|
||||
│ ├─AI流程编排
|
||||
│ ├─AI知识库问答系统
|
||||
│ ├─AI应用开发平台
|
||||
│ ├─AI聊天窗口支持嵌入第三方
|
||||
├─Online在线开发(低代码)
|
||||
│ ├─Online在线表单
|
||||
│ ├─Online代码生成器
|
||||
│ ├─Online在线报表
|
||||
│ ├─仪表盘设计器
|
||||
│ ├─系统编码规则
|
||||
│ ├─系统校验规则
|
||||
├─积木报表设计器
|
||||
│ ├─打印设计器
|
||||
│ ├─数据报表设计
|
||||
│ ├─图形报表设计(支持echart)
|
||||
├─系统管理
|
||||
│ ├─用户管理
|
||||
│ ├─角色管理
|
||||
@ -322,6 +252,29 @@ JeecgBoot 平台的AIGC功能模块,是一套类似`Dify`的`AIGC应用开发
|
||||
│ └─通讯录
|
||||
│ ├─多数据源管理
|
||||
│ └─多租户管理(租户管理、租户角色、我的租户)
|
||||
├─Online在线开发(低代码)
|
||||
│ ├─Online在线表单
|
||||
│ ├─Online代码生成器
|
||||
│ ├─Online在线报表
|
||||
│ ├─仪表盘设计器
|
||||
│ ├─系统编码规则
|
||||
│ ├─系统校验规则
|
||||
├─AI应用平台
|
||||
│ ├─AI知识库问答系统
|
||||
│ ├─AI大模型管理
|
||||
│ ├─AI流程编排
|
||||
│ ├─AI流程设计器
|
||||
│ ├─AI对话支持图片
|
||||
│ ├─AI对话助手(智能问答)
|
||||
│ ├─AI建表(Online表单)
|
||||
│ ├─AI聊天窗口支持嵌入第三方
|
||||
│ ├─AI聊天窗口支持移动端
|
||||
│ ├─支持常见大模型ChatGPT和DeepSeek、ollama等等
|
||||
│ ├─AI OCR示例
|
||||
├─积木报表设计器
|
||||
│ ├─打印设计器
|
||||
│ ├─数据报表设计
|
||||
│ ├─图形报表设计(支持echart)
|
||||
├─消息中心
|
||||
│ ├─消息管理
|
||||
│ ├─模板管理
|
||||
@ -333,7 +286,9 @@ JeecgBoot 平台的AIGC功能模块,是一套类似`Dify`的`AIGC应用开发
|
||||
│ ├─高级查询器(弹窗自动组合查询条件)
|
||||
│ ├─Excel导入导出工具集成(支持单表,一对多 导入导出)
|
||||
│ ├─平台移动自适应支持
|
||||
│ ├─提供新版uniapp3的代码生成器模板
|
||||
├─系统监控
|
||||
│ ├─基于AK和SK认证鉴权OpenAPI功能
|
||||
│ ├─Gateway路由网关
|
||||
│ ├─性能扫描监控
|
||||
│ │ ├─监控 Redis
|
||||
@ -413,22 +368,10 @@ JeecgBoot 平台的AIGC功能模块,是一套类似`Dify`的`AIGC应用开发
|
||||
│ ├─提供单点登录CAS集成方案
|
||||
│ ├─提供APP发布方案
|
||||
│ ├─集成Websocket消息通知机制
|
||||
│─更多商业功能
|
||||
│ ├─流程设计器
|
||||
│ ├─表单设计器
|
||||
│ ├─大屏设计器
|
||||
│ └─我的任务
|
||||
│ └─历史流程
|
||||
│ └─历史流程
|
||||
│ └─流程实例管理
|
||||
│ └─流程监听管理
|
||||
│ └─流程表达式
|
||||
│ └─我发起的流程
|
||||
│ └─我的抄送
|
||||
│ └─流程委派、抄送、跳转
|
||||
│ └─OA办公组件
|
||||
│ └─。。。
|
||||
|
||||
│ ├─支持electron桌面应用打包(支持windows、linux、macOS三大平台)
|
||||
│ ├─docker容器支持
|
||||
│ ├─提供移动APP框架及源码(Uniapp3版本)支持H5、小程序、APP、鸿蒙Next
|
||||
│ ├─提供移动APP低代码设计(Online表单、仪表盘)
|
||||
```
|
||||
|
||||
|
||||
@ -436,21 +379,6 @@ JeecgBoot 平台的AIGC功能模块,是一套类似`Dify`的`AIGC应用开发
|
||||
### 系统效果
|
||||
|
||||
|
||||
##### AI功能
|
||||
|
||||
AI聊天助手
|
||||
|
||||

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

|
||||
|
||||

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

|
||||
|
||||
|
||||
##### PC端
|
||||
@ -472,6 +400,22 @@ AI写文章
|
||||

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

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

|
||||
|
||||

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

|
||||
|
||||
|
||||
##### 仪表盘设计器
|
||||
|
||||
@ -538,28 +482,6 @@ AI写文章
|
||||

|
||||
|
||||
|
||||
##### 流程设计
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

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

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@ -2,12 +2,12 @@
|
||||
JeecgBoot 低代码开发平台
|
||||
===============
|
||||
|
||||
当前最新版本: 3.8.0(发布日期:2025-04-21)
|
||||
当前最新版本: 3.8.1(发布日期:2025-06-30)
|
||||
|
||||
|
||||
[](https://github.com/zhangdaiscott/jeecg-boot/blob/master/LICENSE)
|
||||
[](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)
|
||||
|
||||
@ -35,7 +35,7 @@ JeecgBoot 是一款基于代码生成器的`低代码开发平台`!前后端
|
||||
|
||||
- 官方网站: [http://www.jeecg.com](http://www.jeecg.com)
|
||||
- 新手指南: [快速入门](http://www.jeecg.com/doc/quickstart)
|
||||
- QQ交流群 : ⑩716488839、⑨808791225、其他(满)
|
||||
- QQ交流群 : 964611995、⑩716488839(满)、⑨808791225(满)、其他(满)
|
||||
- 在线演示 : [在线演示](http://boot3.jeecg.com) | [APP演示](http://jeecg.com/appIndex)
|
||||
> 演示系统的登录账号密码,请点击 [获取账号密码](http://jeecg.com/doc/demo) 获取
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<groupId>org.jeecgframework.boot</groupId>
|
||||
<artifactId>jeecg-boot-parent</artifactId>
|
||||
<version>3.8.0</version>
|
||||
<version>3.8.1</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<artifactId>jeecg-boot-base-core</artifactId>
|
||||
@ -111,6 +111,11 @@
|
||||
<artifactId>mybatis-plus-boot-starter</artifactId>
|
||||
<version>${mybatis-plus.version}</version>
|
||||
</dependency>
|
||||
<!-- minidao -->
|
||||
<dependency>
|
||||
<groupId>org.jeecgframework</groupId>
|
||||
<artifactId>minidao-spring-boot-starter</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- druid -->
|
||||
<dependency>
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
package org.jeecg.common.api.vo;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import org.jeecg.common.constant.CommonConstant;
|
||||
|
||||
@ -9,14 +9,14 @@ import org.apache.commons.lang3.StringUtils;
|
||||
public enum DySmsEnum {
|
||||
|
||||
/**登录短信模板编码*/
|
||||
LOGIN_TEMPLATE_CODE("SMS_175435174","敲敲云","code"),
|
||||
LOGIN_TEMPLATE_CODE("SMS_175435174","敲敲云","code"),
|
||||
/**忘记密码短信模板编码*/
|
||||
FORGET_PASSWORD_TEMPLATE_CODE("SMS_175435174","敲敲云","code"),
|
||||
/**修改密码短信模板编码*/
|
||||
CHANGE_PASSWORD_TEMPLATE_CODE("SMS_465391221","敲敲云","code"),
|
||||
/**注册账号短信模板编码*/
|
||||
REGISTER_TEMPLATE_CODE("SMS_175430166","敲敲云","code");
|
||||
|
||||
FORGET_PASSWORD_TEMPLATE_CODE("SMS_175435174","敲敲云","code"),
|
||||
/**修改密码短信模板编码*/
|
||||
CHANGE_PASSWORD_TEMPLATE_CODE("SMS_465391221","敲敲云","code"),
|
||||
/**注册账号短信模板编码*/
|
||||
REGISTER_TEMPLATE_CODE("SMS_175430166","敲敲云","code");
|
||||
|
||||
/**
|
||||
* 短信模板编码
|
||||
*/
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
package org.jeecg.common.exception;
|
||||
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import io.undertow.server.RequestTooBigException;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.exception.ExceptionUtils;
|
||||
import org.apache.shiro.SecurityUtils;
|
||||
@ -30,6 +31,7 @@ import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
import org.springframework.web.multipart.MaxUploadSizeExceededException;
|
||||
import org.springframework.web.multipart.MultipartException;
|
||||
import org.springframework.web.servlet.NoHandlerFoundException;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
@ -56,7 +58,7 @@ public class JeecgBootExceptionHandler {
|
||||
addSysLog(e);
|
||||
return Result.error("校验失败!" + e.getBindingResult().getAllErrors().stream().map(ObjectError::getDefaultMessage).collect(Collectors.joining(",")));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 处理自定义异常
|
||||
*/
|
||||
@ -166,6 +168,27 @@ public class JeecgBootExceptionHandler {
|
||||
return Result.error("文件大小超出10MB限制, 请压缩或降低文件质量! ");
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理文件过大异常.
|
||||
* jdk17中的MultipartException异常类已经被拆分成了MultipartException和MaxUploadSizeExceededException
|
||||
* for [QQYUN-11716]上传大图片失败没有精确提示
|
||||
* @param e
|
||||
* @return
|
||||
* @author chenrui
|
||||
* @date 2025/4/8 16:13
|
||||
*/
|
||||
@ExceptionHandler(MultipartException.class)
|
||||
public Result<?> handleMaxUploadSizeExceededException(MultipartException e) {
|
||||
Throwable cause = e.getCause();
|
||||
if (cause instanceof IllegalStateException && cause.getCause() instanceof RequestTooBigException) {
|
||||
log.error("文件大小超出限制: {}", cause.getMessage(), e);
|
||||
addSysLog(e);
|
||||
return Result.error("文件大小超出限制, 请压缩或降低文件质量!");
|
||||
} else {
|
||||
return handleException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@ExceptionHandler(DataIntegrityViolationException.class)
|
||||
public Result<?> handleDataIntegrityViolationException(DataIntegrityViolationException e) {
|
||||
log.error(e.getMessage(), e);
|
||||
@ -221,11 +244,16 @@ public class JeecgBootExceptionHandler {
|
||||
} catch (NullPointerException | BeansException ignored) {
|
||||
}
|
||||
if (null != request) {
|
||||
//update-begin---author:chenrui ---date:20250408 for:[QQYUN-11716]上传大图片失败没有精确提示------------
|
||||
//请求的参数
|
||||
Map<String, String[]> parameterMap = request.getParameterMap();
|
||||
if(!CollectionUtils.isEmpty(parameterMap)){
|
||||
log.setMethod(oConvertUtils.mapToString(request.getParameterMap()));
|
||||
if (!isTooBigException(e)) {
|
||||
// 文件上传过大异常时不能获取参数,否则会报错
|
||||
Map<String, String[]> parameterMap = request.getParameterMap();
|
||||
if(!CollectionUtils.isEmpty(parameterMap)) {
|
||||
log.setMethod(oConvertUtils.mapToString(request.getParameterMap()));
|
||||
}
|
||||
}
|
||||
//update-end---author:chenrui ---date:20250408 for:[QQYUN-11716]上传大图片失败没有精确提示------------
|
||||
// 请求地址
|
||||
log.setRequestUrl(request.getRequestURI());
|
||||
//设置IP地址
|
||||
@ -251,4 +279,26 @@ public class JeecgBootExceptionHandler {
|
||||
}
|
||||
//update-end---author:chenrui ---date:20240423 for:[QQYUN-8732]把错误的日志都抓取了 方便后续处理,单独弄个日志类型------------
|
||||
|
||||
/**
|
||||
* 是否文件过大异常
|
||||
* for [QQYUN-11716]上传大图片失败没有精确提示
|
||||
* @param e
|
||||
* @return
|
||||
* @author chenrui
|
||||
* @date 2025/4/8 20:21
|
||||
*/
|
||||
private static boolean isTooBigException(Throwable e) {
|
||||
boolean isTooBigException = false;
|
||||
if(e instanceof MultipartException){
|
||||
Throwable cause = e.getCause();
|
||||
if (cause instanceof IllegalStateException && cause.getCause() instanceof RequestTooBigException){
|
||||
isTooBigException = true;
|
||||
}
|
||||
}
|
||||
if(e instanceof MaxUploadSizeExceededException){
|
||||
isTooBigException = true;
|
||||
}
|
||||
return isTooBigException;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -2,7 +2,6 @@ package org.jeecg.common.system.base.entity;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import org.jeecgframework.poi.excel.annotation.Excel;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
|
||||
@ -10,6 +9,7 @@ import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
@ -16,7 +16,13 @@ import java.util.regex.Pattern;
|
||||
* @author zhoujf
|
||||
*/
|
||||
@Slf4j
|
||||
public class SqlInjectionUtil {
|
||||
public class SqlInjectionUtil {
|
||||
|
||||
/**
|
||||
* sql注入黑名单数据库名
|
||||
*/
|
||||
public final static String XSS_STR_TABLE = "peformance_schema|information_schema";
|
||||
|
||||
/**
|
||||
* 默认—sql注入关键词
|
||||
*/
|
||||
@ -167,7 +173,28 @@ public class SqlInjectionUtil {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 判断是否存在SQL注入关键词字符串
|
||||
*
|
||||
* @param keyword
|
||||
* @return
|
||||
*/
|
||||
@SuppressWarnings("AlibabaUndefineMagicConstant")
|
||||
private static boolean isExistSqlInjectTableKeyword(String sql, String keyword) {
|
||||
// 需要匹配的,sql注入关键词
|
||||
String[] matchingTexts = new String[]{"`" + keyword, "(" + keyword, "(`" + keyword};
|
||||
for (String matchingText : matchingTexts) {
|
||||
String[] checkTexts = new String[]{" " + matchingText, "from" + matchingText};
|
||||
for (String checkText : checkTexts) {
|
||||
if (sql.contains(checkText)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* sql注入过滤处理,遇到注入关键字抛异常
|
||||
*
|
||||
@ -208,6 +235,14 @@ public class SqlInjectionUtil {
|
||||
throw new JeecgSqlInjectionException(SqlInjectionUtil.SQL_INJECTION_TIP + value);
|
||||
}
|
||||
}
|
||||
String[] xssTableArr = XSS_STR_TABLE.split("\\|");
|
||||
for (String xssTableStr : xssTableArr) {
|
||||
if (isExistSqlInjectTableKeyword(value, xssTableStr)) {
|
||||
log.error(SqlInjectionUtil.SQL_INJECTION_KEYWORD_TIP, xssTableStr);
|
||||
log.error(SqlInjectionUtil.SQL_INJECTION_TIP_VARIABLE, value);
|
||||
throw new JeecgSqlInjectionException(SqlInjectionUtil.SQL_INJECTION_TIP + value);
|
||||
}
|
||||
}
|
||||
|
||||
// 三、SQL注入检测存在绕过风险 (正则校验)
|
||||
for (String regularOriginal : XSS_REGULAR_STR_ARRAY) {
|
||||
@ -244,6 +279,14 @@ public class SqlInjectionUtil {
|
||||
throw new JeecgSqlInjectionException(SqlInjectionUtil.SQL_INJECTION_TIP + value);
|
||||
}
|
||||
}
|
||||
String[] xssTableArr = XSS_STR_TABLE.split("\\|");
|
||||
for (String xssTableStr : xssTableArr) {
|
||||
if (isExistSqlInjectTableKeyword(value, xssTableStr)) {
|
||||
log.error(SqlInjectionUtil.SQL_INJECTION_KEYWORD_TIP, xssTableStr);
|
||||
log.error(SqlInjectionUtil.SQL_INJECTION_TIP_VARIABLE, value);
|
||||
throw new JeecgSqlInjectionException(SqlInjectionUtil.SQL_INJECTION_TIP + value);
|
||||
}
|
||||
}
|
||||
|
||||
// 三、SQL注入检测存在绕过风险 (正则校验)
|
||||
for (String regularOriginal : XSS_REGULAR_STR_ARRAY) {
|
||||
|
||||
@ -68,6 +68,13 @@ public class DbTypeUtils {
|
||||
return dbTypeIf(dbType, DbType.ORACLE, DbType.ORACLE_12C, DbType.DM);
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否是达梦
|
||||
*/
|
||||
public static boolean dbTypeIsDm(DbType dbType) {
|
||||
return dbTypeIf(dbType, DbType.DM);
|
||||
}
|
||||
|
||||
public static boolean dbTypeIsSqlServer(DbType dbType) {
|
||||
return dbTypeIf(dbType, DbType.SQL_SERVER, DbType.SQL_SERVER2005);
|
||||
}
|
||||
|
||||
@ -7,6 +7,7 @@ import org.apache.commons.io.IOUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.jeecg.common.constant.CommonConstant;
|
||||
import org.jeecg.common.constant.SymbolConstant;
|
||||
import org.jeecg.config.mybatis.MybatisPlusSaasConfig;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
@ -1133,5 +1134,14 @@ public class oConvertUtils {
|
||||
public static <T> boolean isIn(T obj, T... objs) {
|
||||
return isIn(obj, objs);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断租户ID是否有效
|
||||
* @param tenantId
|
||||
* @return
|
||||
*/
|
||||
public static boolean isEffectiveTenant(String tenantId) {
|
||||
return MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL && isNotEmpty(tenantId) && !("0").equals(tenantId);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -3,6 +3,8 @@ package org.jeecg.common.util.security;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.jeecg.common.exception.JeecgSqlInjectionException;
|
||||
import org.jeecg.common.util.SqlInjectionUtil;
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.regex.Matcher;
|
||||
@ -66,6 +68,8 @@ public abstract class AbstractQueryBlackListHandler {
|
||||
if(flag == false){
|
||||
return false;
|
||||
}
|
||||
Set<String> xssTableSet = new HashSet<>(Arrays.asList(SqlInjectionUtil.XSS_STR_TABLE.split("\\|")));
|
||||
|
||||
for (QueryTable table : list) {
|
||||
String name = table.getName();
|
||||
String fieldRule = ruleMap.get(name);
|
||||
@ -81,6 +85,16 @@ public abstract class AbstractQueryBlackListHandler {
|
||||
}
|
||||
|
||||
}
|
||||
// 判断是否调用了黑名单数据库
|
||||
String dbName = table.getDbName();
|
||||
if (oConvertUtils.isNotEmpty(dbName)) {
|
||||
dbName = dbName.toLowerCase().trim();
|
||||
if (xssTableSet.contains(dbName)) {
|
||||
flag = false;
|
||||
log.warn("sql黑名单校验,数据库【" + dbName + "】禁止查询");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 返回黑名单校验结果(不合法直接抛出异常)
|
||||
@ -135,6 +149,8 @@ public abstract class AbstractQueryBlackListHandler {
|
||||
* 查询的表的信息
|
||||
*/
|
||||
protected class QueryTable {
|
||||
//数据库名
|
||||
private String dbName;
|
||||
//表名
|
||||
private String name;
|
||||
//表的别名
|
||||
@ -158,6 +174,14 @@ public abstract class AbstractQueryBlackListHandler {
|
||||
this.fields.add(field);
|
||||
}
|
||||
|
||||
public String getDbName() {
|
||||
return dbName;
|
||||
}
|
||||
|
||||
public void setDbName(String dbName) {
|
||||
this.dbName = dbName;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@ -1,255 +1,255 @@
|
||||
package org.jeecg.common.util.sqlparse;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import net.sf.jsqlparser.JSQLParserException;
|
||||
import net.sf.jsqlparser.expression.*;
|
||||
import net.sf.jsqlparser.parser.CCJSqlParserManager;
|
||||
import net.sf.jsqlparser.schema.Column;
|
||||
import net.sf.jsqlparser.schema.Table;
|
||||
import net.sf.jsqlparser.statement.Statement;
|
||||
import net.sf.jsqlparser.statement.select.*;
|
||||
import org.jeecg.common.exception.JeecgBootException;
|
||||
import org.jeecg.common.util.sqlparse.vo.SelectSqlInfo;
|
||||
|
||||
import java.io.StringReader;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 解析所有表名和字段的类
|
||||
*/
|
||||
@Slf4j
|
||||
public class JSqlParserAllTableManager {
|
||||
|
||||
private final String sql;
|
||||
private final Map<String, SelectSqlInfo> allTableMap = new HashMap<>();
|
||||
/**
|
||||
* 别名对应实际表名
|
||||
*/
|
||||
private final Map<String, String> tableAliasMap = new HashMap<>();
|
||||
|
||||
/**
|
||||
* 解析后的sql
|
||||
*/
|
||||
private String parsedSql = null;
|
||||
|
||||
JSqlParserAllTableManager(String selectSql) {
|
||||
this.sql = selectSql;
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始解析
|
||||
*
|
||||
* @return
|
||||
* @throws JSQLParserException
|
||||
*/
|
||||
public Map<String, SelectSqlInfo> parse() throws JSQLParserException {
|
||||
// 1. 创建解析器
|
||||
CCJSqlParserManager mgr = new CCJSqlParserManager();
|
||||
// 2. 使用解析器解析sql生成具有层次结构的java类
|
||||
Statement stmt = mgr.parse(new StringReader(this.sql));
|
||||
if (stmt instanceof Select) {
|
||||
Select selectStatement = (Select) stmt;
|
||||
SelectBody selectBody = selectStatement.getSelectBody();
|
||||
this.parsedSql = selectBody.toString();
|
||||
// 3. 解析select查询sql的信息
|
||||
if (selectBody instanceof PlainSelect) {
|
||||
PlainSelect plainSelect = (PlainSelect) selectBody;
|
||||
// 4. 合并 fromItems
|
||||
List<FromItem> fromItems = new ArrayList<>();
|
||||
fromItems.add(plainSelect.getFromItem());
|
||||
// 4.1 处理join的表
|
||||
List<Join> joins = plainSelect.getJoins();
|
||||
if (joins != null) {
|
||||
joins.forEach(join -> fromItems.add(join.getRightItem()));
|
||||
}
|
||||
// 5. 处理 fromItems
|
||||
for (FromItem fromItem : fromItems) {
|
||||
// 5.1 通过表名的方式from
|
||||
if (fromItem instanceof Table) {
|
||||
this.addSqlInfoByTable((Table) fromItem);
|
||||
}
|
||||
// 5.2 通过子查询的方式from
|
||||
else if (fromItem instanceof SubSelect) {
|
||||
this.handleSubSelect((SubSelect) fromItem);
|
||||
}
|
||||
}
|
||||
// 6. 解析 selectFields
|
||||
List<SelectItem> selectItems = plainSelect.getSelectItems();
|
||||
for (SelectItem selectItem : selectItems) {
|
||||
// 6.1 查询的是全部字段
|
||||
if (selectItem instanceof AllColumns) {
|
||||
// 当 selectItem 为 AllColumns 时,fromItem 必定为 Table
|
||||
String tableName = plainSelect.getFromItem(Table.class).getName();
|
||||
// 此处必定不为空,因为在解析 fromItem 时,已经将表名添加到 allTableMap 中
|
||||
SelectSqlInfo sqlInfo = this.allTableMap.get(tableName);
|
||||
assert sqlInfo != null;
|
||||
// 设置为查询全部字段
|
||||
sqlInfo.setSelectAll(true);
|
||||
sqlInfo.setSelectFields(null);
|
||||
sqlInfo.setRealSelectFields(null);
|
||||
}
|
||||
// 6.2 查询的是带表别名( u.* )的全部字段
|
||||
else if (selectItem instanceof AllTableColumns) {
|
||||
AllTableColumns allTableColumns = (AllTableColumns) selectItem;
|
||||
String aliasName = allTableColumns.getTable().getName();
|
||||
// 通过别名获取表名
|
||||
String tableName = this.tableAliasMap.get(aliasName);
|
||||
if (tableName == null) {
|
||||
tableName = aliasName;
|
||||
}
|
||||
SelectSqlInfo sqlInfo = this.allTableMap.get(tableName);
|
||||
// 如果此处为空,则说明该字段是通过子查询获取的,所以可以不处理,只有实际表才需要处理
|
||||
if (sqlInfo != null) {
|
||||
// 设置为查询全部字段
|
||||
sqlInfo.setSelectAll(true);
|
||||
sqlInfo.setSelectFields(null);
|
||||
sqlInfo.setRealSelectFields(null);
|
||||
}
|
||||
}
|
||||
// 6.3 各种字段表达式处理
|
||||
else if (selectItem instanceof SelectExpressionItem) {
|
||||
SelectExpressionItem selectExpressionItem = (SelectExpressionItem) selectItem;
|
||||
Expression expression = selectExpressionItem.getExpression();
|
||||
Alias alias = selectExpressionItem.getAlias();
|
||||
this.handleExpression(expression, alias, plainSelect.getFromItem());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.warn("暂时尚未处理该类型的 SelectBody: {}", selectBody.getClass().getName());
|
||||
throw new JeecgBootException("暂时尚未处理该类型的 SelectBody");
|
||||
}
|
||||
} else {
|
||||
// 非 select 查询sql,不做处理
|
||||
throw new JeecgBootException("非 select 查询sql,不做处理");
|
||||
}
|
||||
return this.allTableMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理子查询
|
||||
*
|
||||
* @param subSelect
|
||||
*/
|
||||
private void handleSubSelect(SubSelect subSelect) {
|
||||
try {
|
||||
String subSelectSql = subSelect.getSelectBody().toString();
|
||||
// 递归调用解析
|
||||
Map<String, SelectSqlInfo> map = JSqlParserUtils.parseAllSelectTable(subSelectSql);
|
||||
if (map != null) {
|
||||
this.assignMap(map);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("解析子查询出错", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理查询字段表达式
|
||||
*
|
||||
* @param expression
|
||||
*/
|
||||
private void handleExpression(Expression expression, Alias alias, FromItem fromItem) {
|
||||
// 处理函数式字段 CONCAT(name,'(',age,')')
|
||||
if (expression instanceof Function) {
|
||||
Function functionExp = (Function) expression;
|
||||
List<Expression> expressions = functionExp.getParameters().getExpressions();
|
||||
for (Expression expItem : expressions) {
|
||||
this.handleExpression(expItem, null, fromItem);
|
||||
}
|
||||
return;
|
||||
}
|
||||
// 处理字段上的子查询
|
||||
if (expression instanceof SubSelect) {
|
||||
this.handleSubSelect((SubSelect) expression);
|
||||
return;
|
||||
}
|
||||
// 不处理字面量
|
||||
if (expression instanceof StringValue ||
|
||||
expression instanceof NullValue ||
|
||||
expression instanceof LongValue ||
|
||||
expression instanceof DoubleValue ||
|
||||
expression instanceof HexValue ||
|
||||
expression instanceof DateValue ||
|
||||
expression instanceof TimestampValue ||
|
||||
expression instanceof TimeValue
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 处理字段
|
||||
if (expression instanceof Column) {
|
||||
Column column = (Column) expression;
|
||||
// 查询字段名
|
||||
String fieldName = column.getColumnName();
|
||||
String aliasName = fieldName;
|
||||
if (alias != null) {
|
||||
aliasName = alias.getName();
|
||||
}
|
||||
String tableName;
|
||||
if (column.getTable() != null) {
|
||||
// 通过列的表名获取 sqlInfo
|
||||
// 例如 user.name,这里的 tableName 就是 user
|
||||
tableName = column.getTable().getName();
|
||||
// 有可能是别名,需要转换为真实表名
|
||||
if (this.tableAliasMap.get(tableName) != null) {
|
||||
tableName = this.tableAliasMap.get(tableName);
|
||||
}
|
||||
} else {
|
||||
// 当column的table为空时,说明是 fromItem 中的字段
|
||||
tableName = ((Table) fromItem).getName();
|
||||
}
|
||||
SelectSqlInfo $sqlInfo = this.allTableMap.get(tableName);
|
||||
if ($sqlInfo != null) {
|
||||
$sqlInfo.addSelectField(aliasName, fieldName);
|
||||
} else {
|
||||
log.warn("发生意外情况,未找到表名为 {} 的 SelectSqlInfo", tableName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据表名添加sqlInfo
|
||||
*
|
||||
* @param table
|
||||
*/
|
||||
private void addSqlInfoByTable(Table table) {
|
||||
String tableName = table.getName();
|
||||
// 解析 aliasName
|
||||
if (table.getAlias() != null) {
|
||||
this.tableAliasMap.put(table.getAlias().getName(), tableName);
|
||||
}
|
||||
SelectSqlInfo sqlInfo = new SelectSqlInfo(this.parsedSql);
|
||||
sqlInfo.setFromTableName(table.getName());
|
||||
this.allTableMap.put(sqlInfo.getFromTableName(), sqlInfo);
|
||||
}
|
||||
|
||||
/**
|
||||
* 合并map
|
||||
*
|
||||
* @param source
|
||||
*/
|
||||
private void assignMap(Map<String, SelectSqlInfo> source) {
|
||||
for (Map.Entry<String, SelectSqlInfo> entry : source.entrySet()) {
|
||||
SelectSqlInfo sqlInfo = this.allTableMap.get(entry.getKey());
|
||||
if (sqlInfo == null) {
|
||||
this.allTableMap.put(entry.getKey(), entry.getValue());
|
||||
} else {
|
||||
// 合并
|
||||
if (sqlInfo.getSelectFields() == null) {
|
||||
sqlInfo.setSelectFields(entry.getValue().getSelectFields());
|
||||
} else {
|
||||
sqlInfo.getSelectFields().addAll(entry.getValue().getSelectFields());
|
||||
}
|
||||
if (sqlInfo.getRealSelectFields() == null) {
|
||||
sqlInfo.setRealSelectFields(entry.getValue().getRealSelectFields());
|
||||
} else {
|
||||
sqlInfo.getRealSelectFields().addAll(entry.getValue().getRealSelectFields());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
//package org.jeecg.common.util.sqlparse;
|
||||
//
|
||||
//import lombok.extern.slf4j.Slf4j;
|
||||
//import net.sf.jsqlparser.JSQLParserException;
|
||||
//import net.sf.jsqlparser.expression.*;
|
||||
//import net.sf.jsqlparser.parser.CCJSqlParserManager;
|
||||
//import net.sf.jsqlparser.schema.Column;
|
||||
//import net.sf.jsqlparser.schema.Table;
|
||||
//import net.sf.jsqlparser.statement.Statement;
|
||||
//import net.sf.jsqlparser.statement.select.*;
|
||||
//import org.jeecg.common.exception.JeecgBootException;
|
||||
//import org.jeecg.common.util.sqlparse.vo.SelectSqlInfo;
|
||||
//
|
||||
//import java.io.StringReader;
|
||||
//import java.util.ArrayList;
|
||||
//import java.util.HashMap;
|
||||
//import java.util.List;
|
||||
//import java.util.Map;
|
||||
//
|
||||
///**
|
||||
// * 解析所有表名和字段的类
|
||||
// */
|
||||
//@Slf4j
|
||||
//public class JSqlParserAllTableManager {
|
||||
//
|
||||
// private final String sql;
|
||||
// private final Map<String, SelectSqlInfo> allTableMap = new HashMap<>();
|
||||
// /**
|
||||
// * 别名对应实际表名
|
||||
// */
|
||||
// private final Map<String, String> tableAliasMap = new HashMap<>();
|
||||
//
|
||||
// /**
|
||||
// * 解析后的sql
|
||||
// */
|
||||
// private String parsedSql = null;
|
||||
//
|
||||
// JSqlParserAllTableManager(String selectSql) {
|
||||
// this.sql = selectSql;
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * 开始解析
|
||||
// *
|
||||
// * @return
|
||||
// * @throws JSQLParserException
|
||||
// */
|
||||
// public Map<String, SelectSqlInfo> parse() throws JSQLParserException {
|
||||
// // 1. 创建解析器
|
||||
// CCJSqlParserManager mgr = new CCJSqlParserManager();
|
||||
// // 2. 使用解析器解析sql生成具有层次结构的java类
|
||||
// Statement stmt = mgr.parse(new StringReader(this.sql));
|
||||
// if (stmt instanceof Select) {
|
||||
// Select selectStatement = (Select) stmt;
|
||||
// SelectBody selectBody = selectStatement.getSelectBody();
|
||||
// this.parsedSql = selectBody.toString();
|
||||
// // 3. 解析select查询sql的信息
|
||||
// if (selectBody instanceof PlainSelect) {
|
||||
// PlainSelect plainSelect = (PlainSelect) selectBody;
|
||||
// // 4. 合并 fromItems
|
||||
// List<FromItem> fromItems = new ArrayList<>();
|
||||
// fromItems.add(plainSelect.getFromItem());
|
||||
// // 4.1 处理join的表
|
||||
// List<Join> joins = plainSelect.getJoins();
|
||||
// if (joins != null) {
|
||||
// joins.forEach(join -> fromItems.add(join.getRightItem()));
|
||||
// }
|
||||
// // 5. 处理 fromItems
|
||||
// for (FromItem fromItem : fromItems) {
|
||||
// // 5.1 通过表名的方式from
|
||||
// if (fromItem instanceof Table) {
|
||||
// this.addSqlInfoByTable((Table) fromItem);
|
||||
// }
|
||||
// // 5.2 通过子查询的方式from
|
||||
// else if (fromItem instanceof SubSelect) {
|
||||
// this.handleSubSelect((SubSelect) fromItem);
|
||||
// }
|
||||
// }
|
||||
// // 6. 解析 selectFields
|
||||
// List<SelectItem> selectItems = plainSelect.getSelectItems();
|
||||
// for (SelectItem selectItem : selectItems) {
|
||||
// // 6.1 查询的是全部字段
|
||||
// if (selectItem instanceof AllColumns) {
|
||||
// // 当 selectItem 为 AllColumns 时,fromItem 必定为 Table
|
||||
// String tableName = plainSelect.getFromItem(Table.class).getName();
|
||||
// // 此处必定不为空,因为在解析 fromItem 时,已经将表名添加到 allTableMap 中
|
||||
// SelectSqlInfo sqlInfo = this.allTableMap.get(tableName);
|
||||
// assert sqlInfo != null;
|
||||
// // 设置为查询全部字段
|
||||
// sqlInfo.setSelectAll(true);
|
||||
// sqlInfo.setSelectFields(null);
|
||||
// sqlInfo.setRealSelectFields(null);
|
||||
// }
|
||||
// // 6.2 查询的是带表别名( u.* )的全部字段
|
||||
// else if (selectItem instanceof AllTableColumns) {
|
||||
// AllTableColumns allTableColumns = (AllTableColumns) selectItem;
|
||||
// String aliasName = allTableColumns.getTable().getName();
|
||||
// // 通过别名获取表名
|
||||
// String tableName = this.tableAliasMap.get(aliasName);
|
||||
// if (tableName == null) {
|
||||
// tableName = aliasName;
|
||||
// }
|
||||
// SelectSqlInfo sqlInfo = this.allTableMap.get(tableName);
|
||||
// // 如果此处为空,则说明该字段是通过子查询获取的,所以可以不处理,只有实际表才需要处理
|
||||
// if (sqlInfo != null) {
|
||||
// // 设置为查询全部字段
|
||||
// sqlInfo.setSelectAll(true);
|
||||
// sqlInfo.setSelectFields(null);
|
||||
// sqlInfo.setRealSelectFields(null);
|
||||
// }
|
||||
// }
|
||||
// // 6.3 各种字段表达式处理
|
||||
// else if (selectItem instanceof SelectExpressionItem) {
|
||||
// SelectExpressionItem selectExpressionItem = (SelectExpressionItem) selectItem;
|
||||
// Expression expression = selectExpressionItem.getExpression();
|
||||
// Alias alias = selectExpressionItem.getAlias();
|
||||
// this.handleExpression(expression, alias, plainSelect.getFromItem());
|
||||
// }
|
||||
// }
|
||||
// } else {
|
||||
// log.warn("暂时尚未处理该类型的 SelectBody: {}", selectBody.getClass().getName());
|
||||
// throw new JeecgBootException("暂时尚未处理该类型的 SelectBody");
|
||||
// }
|
||||
// } else {
|
||||
// // 非 select 查询sql,不做处理
|
||||
// throw new JeecgBootException("非 select 查询sql,不做处理");
|
||||
// }
|
||||
// return this.allTableMap;
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * 处理子查询
|
||||
// *
|
||||
// * @param subSelect
|
||||
// */
|
||||
// private void handleSubSelect(SubSelect subSelect) {
|
||||
// try {
|
||||
// String subSelectSql = subSelect.getSelectBody().toString();
|
||||
// // 递归调用解析
|
||||
// Map<String, SelectSqlInfo> map = JSqlParserUtils.parseAllSelectTable(subSelectSql);
|
||||
// if (map != null) {
|
||||
// this.assignMap(map);
|
||||
// }
|
||||
// } catch (Exception e) {
|
||||
// log.error("解析子查询出错", e);
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * 处理查询字段表达式
|
||||
// *
|
||||
// * @param expression
|
||||
// */
|
||||
// private void handleExpression(Expression expression, Alias alias, FromItem fromItem) {
|
||||
// // 处理函数式字段 CONCAT(name,'(',age,')')
|
||||
// if (expression instanceof Function) {
|
||||
// Function functionExp = (Function) expression;
|
||||
// List<Expression> expressions = functionExp.getParameters().getExpressions();
|
||||
// for (Expression expItem : expressions) {
|
||||
// this.handleExpression(expItem, null, fromItem);
|
||||
// }
|
||||
// return;
|
||||
// }
|
||||
// // 处理字段上的子查询
|
||||
// if (expression instanceof SubSelect) {
|
||||
// this.handleSubSelect((SubSelect) expression);
|
||||
// return;
|
||||
// }
|
||||
// // 不处理字面量
|
||||
// if (expression instanceof StringValue ||
|
||||
// expression instanceof NullValue ||
|
||||
// expression instanceof LongValue ||
|
||||
// expression instanceof DoubleValue ||
|
||||
// expression instanceof HexValue ||
|
||||
// expression instanceof DateValue ||
|
||||
// expression instanceof TimestampValue ||
|
||||
// expression instanceof TimeValue
|
||||
// ) {
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// // 处理字段
|
||||
// if (expression instanceof Column) {
|
||||
// Column column = (Column) expression;
|
||||
// // 查询字段名
|
||||
// String fieldName = column.getColumnName();
|
||||
// String aliasName = fieldName;
|
||||
// if (alias != null) {
|
||||
// aliasName = alias.getName();
|
||||
// }
|
||||
// String tableName;
|
||||
// if (column.getTable() != null) {
|
||||
// // 通过列的表名获取 sqlInfo
|
||||
// // 例如 user.name,这里的 tableName 就是 user
|
||||
// tableName = column.getTable().getName();
|
||||
// // 有可能是别名,需要转换为真实表名
|
||||
// if (this.tableAliasMap.get(tableName) != null) {
|
||||
// tableName = this.tableAliasMap.get(tableName);
|
||||
// }
|
||||
// } else {
|
||||
// // 当column的table为空时,说明是 fromItem 中的字段
|
||||
// tableName = ((Table) fromItem).getName();
|
||||
// }
|
||||
// SelectSqlInfo $sqlInfo = this.allTableMap.get(tableName);
|
||||
// if ($sqlInfo != null) {
|
||||
// $sqlInfo.addSelectField(aliasName, fieldName);
|
||||
// } else {
|
||||
// log.warn("发生意外情况,未找到表名为 {} 的 SelectSqlInfo", tableName);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * 根据表名添加sqlInfo
|
||||
// *
|
||||
// * @param table
|
||||
// */
|
||||
// private void addSqlInfoByTable(Table table) {
|
||||
// String tableName = table.getName();
|
||||
// // 解析 aliasName
|
||||
// if (table.getAlias() != null) {
|
||||
// this.tableAliasMap.put(table.getAlias().getName(), tableName);
|
||||
// }
|
||||
// SelectSqlInfo sqlInfo = new SelectSqlInfo(this.parsedSql);
|
||||
// sqlInfo.setFromTableName(table.getName());
|
||||
// this.allTableMap.put(sqlInfo.getFromTableName(), sqlInfo);
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * 合并map
|
||||
// *
|
||||
// * @param source
|
||||
// */
|
||||
// private void assignMap(Map<String, SelectSqlInfo> source) {
|
||||
// for (Map.Entry<String, SelectSqlInfo> entry : source.entrySet()) {
|
||||
// SelectSqlInfo sqlInfo = this.allTableMap.get(entry.getKey());
|
||||
// if (sqlInfo == null) {
|
||||
// this.allTableMap.put(entry.getKey(), entry.getValue());
|
||||
// } else {
|
||||
// // 合并
|
||||
// if (sqlInfo.getSelectFields() == null) {
|
||||
// sqlInfo.setSelectFields(entry.getValue().getSelectFields());
|
||||
// } else {
|
||||
// sqlInfo.getSelectFields().addAll(entry.getValue().getSelectFields());
|
||||
// }
|
||||
// if (sqlInfo.getRealSelectFields() == null) {
|
||||
// sqlInfo.setRealSelectFields(entry.getValue().getRealSelectFields());
|
||||
// } else {
|
||||
// sqlInfo.getRealSelectFields().addAll(entry.getValue().getRealSelectFields());
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
//}
|
||||
|
||||
@ -1,190 +1,190 @@
|
||||
package org.jeecg.common.util.sqlparse;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import net.sf.jsqlparser.JSQLParserException;
|
||||
import net.sf.jsqlparser.expression.*;
|
||||
import net.sf.jsqlparser.parser.CCJSqlParserManager;
|
||||
import net.sf.jsqlparser.schema.Column;
|
||||
import net.sf.jsqlparser.schema.Table;
|
||||
import net.sf.jsqlparser.statement.Statement;
|
||||
import net.sf.jsqlparser.statement.select.*;
|
||||
import org.jeecg.common.exception.JeecgBootException;
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
import org.jeecg.common.util.sqlparse.vo.SelectSqlInfo;
|
||||
|
||||
import java.io.StringReader;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Slf4j
|
||||
public class JSqlParserUtils {
|
||||
|
||||
/**
|
||||
* 解析 查询(select)sql的信息,
|
||||
* 此方法会展开所有子查询到一个map里,
|
||||
* key只存真实的表名,如果查询的没有真实的表名,则会被忽略。
|
||||
* value只存真实的字段名,如果查询的没有真实的字段名,则会被忽略。
|
||||
* <p>
|
||||
* 例如:SELECT a.*,d.age,(SELECT count(1) FROM sys_depart) AS count FROM (SELECT username AS foo, realname FROM sys_user) a, demo d
|
||||
* 解析后的结果为:{sys_user=[username, realname], demo=[age], sys_depart=[]}
|
||||
*
|
||||
* @param selectSql
|
||||
* @return
|
||||
*/
|
||||
public static Map<String, SelectSqlInfo> parseAllSelectTable(String selectSql) throws JSQLParserException {
|
||||
if (oConvertUtils.isEmpty(selectSql)) {
|
||||
return null;
|
||||
}
|
||||
// log.info("解析查询Sql:{}", selectSql);
|
||||
JSqlParserAllTableManager allTableManager = new JSqlParserAllTableManager(selectSql);
|
||||
return allTableManager.parse();
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 查询(select)sql的信息,子查询嵌套
|
||||
*
|
||||
* @param selectSql
|
||||
* @return
|
||||
*/
|
||||
public static SelectSqlInfo parseSelectSqlInfo(String selectSql) throws JSQLParserException {
|
||||
if (oConvertUtils.isEmpty(selectSql)) {
|
||||
return null;
|
||||
}
|
||||
// log.info("解析查询Sql:{}", selectSql);
|
||||
// 使用 JSqlParer 解析sql
|
||||
// 1、创建解析器
|
||||
CCJSqlParserManager mgr = new CCJSqlParserManager();
|
||||
// 2、使用解析器解析sql生成具有层次结构的java类
|
||||
Statement stmt = mgr.parse(new StringReader(selectSql));
|
||||
if (stmt instanceof Select) {
|
||||
Select selectStatement = (Select) stmt;
|
||||
// 3、解析select查询sql的信息
|
||||
return JSqlParserUtils.parseBySelectBody(selectStatement.getSelectBody());
|
||||
} else {
|
||||
// 非 select 查询sql,不做处理
|
||||
throw new JeecgBootException("非 select 查询sql,不做处理");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 select 查询sql的信息
|
||||
*
|
||||
* @param selectBody
|
||||
* @return
|
||||
*/
|
||||
private static SelectSqlInfo parseBySelectBody(SelectBody selectBody) {
|
||||
// 判断是否使用了union等操作
|
||||
if (selectBody instanceof SetOperationList) {
|
||||
// 如果使用了union等操作,则只解析第一个查询
|
||||
List<SelectBody> selectBodyList = ((SetOperationList) selectBody).getSelects();
|
||||
return JSqlParserUtils.parseBySelectBody(selectBodyList.get(0));
|
||||
}
|
||||
// 简单的select查询
|
||||
if (selectBody instanceof PlainSelect) {
|
||||
SelectSqlInfo sqlInfo = new SelectSqlInfo(selectBody);
|
||||
PlainSelect plainSelect = (PlainSelect) selectBody;
|
||||
FromItem fromItem = plainSelect.getFromItem();
|
||||
// 解析 aliasName
|
||||
if (fromItem.getAlias() != null) {
|
||||
sqlInfo.setFromTableAliasName(fromItem.getAlias().getName());
|
||||
}
|
||||
// 解析 表名
|
||||
if (fromItem instanceof Table) {
|
||||
// 通过表名的方式from
|
||||
Table fromTable = (Table) fromItem;
|
||||
sqlInfo.setFromTableName(fromTable.getName());
|
||||
} else if (fromItem instanceof SubSelect) {
|
||||
// 通过子查询的方式from
|
||||
SubSelect fromSubSelect = (SubSelect) fromItem;
|
||||
SelectSqlInfo subSqlInfo = JSqlParserUtils.parseBySelectBody(fromSubSelect.getSelectBody());
|
||||
sqlInfo.setFromSubSelect(subSqlInfo);
|
||||
}
|
||||
// 解析 selectFields
|
||||
List<SelectItem> selectItems = plainSelect.getSelectItems();
|
||||
for (SelectItem selectItem : selectItems) {
|
||||
if (selectItem instanceof AllColumns || selectItem instanceof AllTableColumns) {
|
||||
// 全部字段
|
||||
sqlInfo.setSelectAll(true);
|
||||
sqlInfo.setSelectFields(null);
|
||||
sqlInfo.setRealSelectFields(null);
|
||||
break;
|
||||
} else if (selectItem instanceof SelectExpressionItem) {
|
||||
// 获取单个查询字段名
|
||||
SelectExpressionItem selectExpressionItem = (SelectExpressionItem) selectItem;
|
||||
Expression expression = selectExpressionItem.getExpression();
|
||||
Alias alias = selectExpressionItem.getAlias();
|
||||
JSqlParserUtils.handleExpression(sqlInfo, expression, alias);
|
||||
}
|
||||
}
|
||||
return sqlInfo;
|
||||
} else {
|
||||
log.warn("暂时尚未处理该类型的 SelectBody: {}", selectBody.getClass().getName());
|
||||
throw new JeecgBootException("暂时尚未处理该类型的 SelectBody");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理查询字段表达式
|
||||
*
|
||||
* @param sqlInfo
|
||||
* @param expression
|
||||
* @param alias 是否有别名,无传null
|
||||
*/
|
||||
private static void handleExpression(SelectSqlInfo sqlInfo, Expression expression, Alias alias) {
|
||||
// 处理函数式字段 CONCAT(name,'(',age,')')
|
||||
if (expression instanceof Function) {
|
||||
JSqlParserUtils.handleFunctionExpression((Function) expression, sqlInfo);
|
||||
return;
|
||||
}
|
||||
// 处理字段上的子查询
|
||||
if (expression instanceof SubSelect) {
|
||||
SubSelect subSelect = (SubSelect) expression;
|
||||
SelectSqlInfo subSqlInfo = JSqlParserUtils.parseBySelectBody(subSelect.getSelectBody());
|
||||
// 注:字段上的子查询,必须只查询一个字段,否则会报错,所以可以放心合并
|
||||
sqlInfo.getSelectFields().addAll(subSqlInfo.getSelectFields());
|
||||
sqlInfo.getRealSelectFields().addAll(subSqlInfo.getAllRealSelectFields());
|
||||
return;
|
||||
}
|
||||
// 不处理字面量
|
||||
if (expression instanceof StringValue ||
|
||||
expression instanceof NullValue ||
|
||||
expression instanceof LongValue ||
|
||||
expression instanceof DoubleValue ||
|
||||
expression instanceof HexValue ||
|
||||
expression instanceof DateValue ||
|
||||
expression instanceof TimestampValue ||
|
||||
expression instanceof TimeValue
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 查询字段名
|
||||
String selectField = expression.toString();
|
||||
// 实际查询字段名
|
||||
String realSelectField = selectField;
|
||||
// 判断是否有别名
|
||||
if (alias != null) {
|
||||
selectField = alias.getName();
|
||||
}
|
||||
// 获取真实字段名
|
||||
if (expression instanceof Column) {
|
||||
Column column = (Column) expression;
|
||||
realSelectField = column.getColumnName();
|
||||
}
|
||||
sqlInfo.addSelectField(selectField, realSelectField);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理函数式字段
|
||||
*
|
||||
* @param functionExp
|
||||
* @param sqlInfo
|
||||
*/
|
||||
private static void handleFunctionExpression(Function functionExp, SelectSqlInfo sqlInfo) {
|
||||
List<Expression> expressions = functionExp.getParameters().getExpressions();
|
||||
for (Expression expression : expressions) {
|
||||
JSqlParserUtils.handleExpression(sqlInfo, expression, null);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
//package org.jeecg.common.util.sqlparse;
|
||||
//
|
||||
//import lombok.extern.slf4j.Slf4j;
|
||||
//import net.sf.jsqlparser.JSQLParserException;
|
||||
//import net.sf.jsqlparser.expression.*;
|
||||
//import net.sf.jsqlparser.parser.CCJSqlParserManager;
|
||||
//import net.sf.jsqlparser.schema.Column;
|
||||
//import net.sf.jsqlparser.schema.Table;
|
||||
//import net.sf.jsqlparser.statement.Statement;
|
||||
//import net.sf.jsqlparser.statement.select.*;
|
||||
//import org.jeecg.common.exception.JeecgBootException;
|
||||
//import org.jeecg.common.util.oConvertUtils;
|
||||
//import org.jeecg.common.util.sqlparse.vo.SelectSqlInfo;
|
||||
//
|
||||
//import java.io.StringReader;
|
||||
//import java.util.List;
|
||||
//import java.util.Map;
|
||||
//
|
||||
//@Slf4j
|
||||
//public class JSqlParserUtils {
|
||||
//
|
||||
// /**
|
||||
// * 解析 查询(select)sql的信息,
|
||||
// * 此方法会展开所有子查询到一个map里,
|
||||
// * key只存真实的表名,如果查询的没有真实的表名,则会被忽略。
|
||||
// * value只存真实的字段名,如果查询的没有真实的字段名,则会被忽略。
|
||||
// * <p>
|
||||
// * 例如:SELECT a.*,d.age,(SELECT count(1) FROM sys_depart) AS count FROM (SELECT username AS foo, realname FROM sys_user) a, demo d
|
||||
// * 解析后的结果为:{sys_user=[username, realname], demo=[age], sys_depart=[]}
|
||||
// *
|
||||
// * @param selectSql
|
||||
// * @return
|
||||
// */
|
||||
// public static Map<String, SelectSqlInfo> parseAllSelectTable(String selectSql) throws JSQLParserException {
|
||||
// if (oConvertUtils.isEmpty(selectSql)) {
|
||||
// return null;
|
||||
// }
|
||||
// // log.info("解析查询Sql:{}", selectSql);
|
||||
// JSqlParserAllTableManager allTableManager = new JSqlParserAllTableManager(selectSql);
|
||||
// return allTableManager.parse();
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * 解析 查询(select)sql的信息,子查询嵌套
|
||||
// *
|
||||
// * @param selectSql
|
||||
// * @return
|
||||
// */
|
||||
// public static SelectSqlInfo parseSelectSqlInfo(String selectSql) throws JSQLParserException {
|
||||
// if (oConvertUtils.isEmpty(selectSql)) {
|
||||
// return null;
|
||||
// }
|
||||
// // log.info("解析查询Sql:{}", selectSql);
|
||||
// // 使用 JSqlParer 解析sql
|
||||
// // 1、创建解析器
|
||||
// CCJSqlParserManager mgr = new CCJSqlParserManager();
|
||||
// // 2、使用解析器解析sql生成具有层次结构的java类
|
||||
// Statement stmt = mgr.parse(new StringReader(selectSql));
|
||||
// if (stmt instanceof Select) {
|
||||
// Select selectStatement = (Select) stmt;
|
||||
// // 3、解析select查询sql的信息
|
||||
// return JSqlParserUtils.parseBySelectBody(selectStatement.getSelectBody());
|
||||
// } else {
|
||||
// // 非 select 查询sql,不做处理
|
||||
// throw new JeecgBootException("非 select 查询sql,不做处理");
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * 解析 select 查询sql的信息
|
||||
// *
|
||||
// * @param selectBody
|
||||
// * @return
|
||||
// */
|
||||
// private static SelectSqlInfo parseBySelectBody(SelectBody selectBody) {
|
||||
// // 判断是否使用了union等操作
|
||||
// if (selectBody instanceof SetOperationList) {
|
||||
// // 如果使用了union等操作,则只解析第一个查询
|
||||
// List<SelectBody> selectBodyList = ((SetOperationList) selectBody).getSelects();
|
||||
// return JSqlParserUtils.parseBySelectBody(selectBodyList.get(0));
|
||||
// }
|
||||
// // 简单的select查询
|
||||
// if (selectBody instanceof PlainSelect) {
|
||||
// SelectSqlInfo sqlInfo = new SelectSqlInfo(selectBody);
|
||||
// PlainSelect plainSelect = (PlainSelect) selectBody;
|
||||
// FromItem fromItem = plainSelect.getFromItem();
|
||||
// // 解析 aliasName
|
||||
// if (fromItem.getAlias() != null) {
|
||||
// sqlInfo.setFromTableAliasName(fromItem.getAlias().getName());
|
||||
// }
|
||||
// // 解析 表名
|
||||
// if (fromItem instanceof Table) {
|
||||
// // 通过表名的方式from
|
||||
// Table fromTable = (Table) fromItem;
|
||||
// sqlInfo.setFromTableName(fromTable.getName());
|
||||
// } else if (fromItem instanceof SubSelect) {
|
||||
// // 通过子查询的方式from
|
||||
// SubSelect fromSubSelect = (SubSelect) fromItem;
|
||||
// SelectSqlInfo subSqlInfo = JSqlParserUtils.parseBySelectBody(fromSubSelect.getSelectBody());
|
||||
// sqlInfo.setFromSubSelect(subSqlInfo);
|
||||
// }
|
||||
// // 解析 selectFields
|
||||
// List<SelectItem> selectItems = plainSelect.getSelectItems();
|
||||
// for (SelectItem selectItem : selectItems) {
|
||||
// if (selectItem instanceof AllColumns || selectItem instanceof AllTableColumns) {
|
||||
// // 全部字段
|
||||
// sqlInfo.setSelectAll(true);
|
||||
// sqlInfo.setSelectFields(null);
|
||||
// sqlInfo.setRealSelectFields(null);
|
||||
// break;
|
||||
// } else if (selectItem instanceof SelectExpressionItem) {
|
||||
// // 获取单个查询字段名
|
||||
// SelectExpressionItem selectExpressionItem = (SelectExpressionItem) selectItem;
|
||||
// Expression expression = selectExpressionItem.getExpression();
|
||||
// Alias alias = selectExpressionItem.getAlias();
|
||||
// JSqlParserUtils.handleExpression(sqlInfo, expression, alias);
|
||||
// }
|
||||
// }
|
||||
// return sqlInfo;
|
||||
// } else {
|
||||
// log.warn("暂时尚未处理该类型的 SelectBody: {}", selectBody.getClass().getName());
|
||||
// throw new JeecgBootException("暂时尚未处理该类型的 SelectBody");
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * 处理查询字段表达式
|
||||
// *
|
||||
// * @param sqlInfo
|
||||
// * @param expression
|
||||
// * @param alias 是否有别名,无传null
|
||||
// */
|
||||
// private static void handleExpression(SelectSqlInfo sqlInfo, Expression expression, Alias alias) {
|
||||
// // 处理函数式字段 CONCAT(name,'(',age,')')
|
||||
// if (expression instanceof Function) {
|
||||
// JSqlParserUtils.handleFunctionExpression((Function) expression, sqlInfo);
|
||||
// return;
|
||||
// }
|
||||
// // 处理字段上的子查询
|
||||
// if (expression instanceof SubSelect) {
|
||||
// SubSelect subSelect = (SubSelect) expression;
|
||||
// SelectSqlInfo subSqlInfo = JSqlParserUtils.parseBySelectBody(subSelect.getSelectBody());
|
||||
// // 注:字段上的子查询,必须只查询一个字段,否则会报错,所以可以放心合并
|
||||
// sqlInfo.getSelectFields().addAll(subSqlInfo.getSelectFields());
|
||||
// sqlInfo.getRealSelectFields().addAll(subSqlInfo.getAllRealSelectFields());
|
||||
// return;
|
||||
// }
|
||||
// // 不处理字面量
|
||||
// if (expression instanceof StringValue ||
|
||||
// expression instanceof NullValue ||
|
||||
// expression instanceof LongValue ||
|
||||
// expression instanceof DoubleValue ||
|
||||
// expression instanceof HexValue ||
|
||||
// expression instanceof DateValue ||
|
||||
// expression instanceof TimestampValue ||
|
||||
// expression instanceof TimeValue
|
||||
// ) {
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// // 查询字段名
|
||||
// String selectField = expression.toString();
|
||||
// // 实际查询字段名
|
||||
// String realSelectField = selectField;
|
||||
// // 判断是否有别名
|
||||
// if (alias != null) {
|
||||
// selectField = alias.getName();
|
||||
// }
|
||||
// // 获取真实字段名
|
||||
// if (expression instanceof Column) {
|
||||
// Column column = (Column) expression;
|
||||
// realSelectField = column.getColumnName();
|
||||
// }
|
||||
// sqlInfo.addSelectField(selectField, realSelectField);
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * 处理函数式字段
|
||||
// *
|
||||
// * @param functionExp
|
||||
// * @param sqlInfo
|
||||
// */
|
||||
// private static void handleFunctionExpression(Function functionExp, SelectSqlInfo sqlInfo) {
|
||||
// List<Expression> expressions = functionExp.getParameters().getExpressions();
|
||||
// for (Expression expression : expressions) {
|
||||
// JSqlParserUtils.handleExpression(sqlInfo, expression, null);
|
||||
// }
|
||||
// }
|
||||
//
|
||||
//}
|
||||
|
||||
@ -1,101 +1,101 @@
|
||||
package org.jeecg.common.util.sqlparse.vo;
|
||||
|
||||
import lombok.Data;
|
||||
import net.sf.jsqlparser.statement.select.SelectBody;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* select 查询 sql 的信息
|
||||
*/
|
||||
@Data
|
||||
public class SelectSqlInfo {
|
||||
|
||||
/**
|
||||
* 查询的表名,如果是子查询,则此处为null
|
||||
*/
|
||||
private String fromTableName;
|
||||
/**
|
||||
* 表别名
|
||||
*/
|
||||
private String fromTableAliasName;
|
||||
/**
|
||||
* 通过子查询获取的表信息,例如:select name from (select * from user) u
|
||||
* 如果不是子查询,则为null
|
||||
*/
|
||||
private SelectSqlInfo fromSubSelect;
|
||||
/**
|
||||
* 查询的字段集合,如果是 * 则为null,如果设了别名则为别名
|
||||
*/
|
||||
private Set<String> selectFields;
|
||||
/**
|
||||
* 真实的查询字段集合,如果是 * 则为null,如果设了别名则为原始字段名
|
||||
*/
|
||||
private Set<String> realSelectFields;
|
||||
/**
|
||||
* 是否是查询所有字段
|
||||
*/
|
||||
private boolean selectAll;
|
||||
|
||||
/**
|
||||
* 解析之后的 SQL (关键字都是大写)
|
||||
*/
|
||||
private final String parsedSql;
|
||||
|
||||
public SelectSqlInfo(String parsedSql) {
|
||||
this.parsedSql = parsedSql;
|
||||
}
|
||||
|
||||
public SelectSqlInfo(SelectBody selectBody) {
|
||||
this.parsedSql = selectBody.toString();
|
||||
}
|
||||
|
||||
public void addSelectField(String selectField, String realSelectField) {
|
||||
if (this.selectFields == null) {
|
||||
this.selectFields = new HashSet<>();
|
||||
}
|
||||
if (this.realSelectFields == null) {
|
||||
this.realSelectFields = new HashSet<>();
|
||||
}
|
||||
this.selectFields.add(selectField);
|
||||
this.realSelectFields.add(realSelectField);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有字段,包括子查询里的。
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public Set<String> getAllRealSelectFields() {
|
||||
Set<String> fields = new HashSet<>();
|
||||
// 递归获取所有字段,起个直观的方法名为:
|
||||
this.recursiveGetAllFields(this, fields);
|
||||
return fields;
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归获取所有字段
|
||||
*/
|
||||
private void recursiveGetAllFields(SelectSqlInfo sqlInfo, Set<String> fields) {
|
||||
if (!sqlInfo.isSelectAll() && sqlInfo.getRealSelectFields() != null) {
|
||||
fields.addAll(sqlInfo.getRealSelectFields());
|
||||
}
|
||||
if (sqlInfo.getFromSubSelect() != null) {
|
||||
recursiveGetAllFields(sqlInfo.getFromSubSelect(), fields);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "SelectSqlInfo{" +
|
||||
"fromTableName='" + fromTableName + '\'' +
|
||||
", fromSubSelect=" + fromSubSelect +
|
||||
", aliasName='" + fromTableAliasName + '\'' +
|
||||
", selectFields=" + selectFields +
|
||||
", realSelectFields=" + realSelectFields +
|
||||
", selectAll=" + selectAll +
|
||||
"}";
|
||||
}
|
||||
|
||||
}
|
||||
//package org.jeecg.common.util.sqlparse.vo;
|
||||
//
|
||||
//import lombok.Data;
|
||||
//import net.sf.jsqlparser.statement.select.SelectBody;
|
||||
//
|
||||
//import java.util.HashSet;
|
||||
//import java.util.Set;
|
||||
//
|
||||
///**
|
||||
// * select 查询 sql 的信息
|
||||
// */
|
||||
//@Data
|
||||
//public class SelectSqlInfo {
|
||||
//
|
||||
// /**
|
||||
// * 查询的表名,如果是子查询,则此处为null
|
||||
// */
|
||||
// private String fromTableName;
|
||||
// /**
|
||||
// * 表别名
|
||||
// */
|
||||
// private String fromTableAliasName;
|
||||
// /**
|
||||
// * 通过子查询获取的表信息,例如:select name from (select * from user) u
|
||||
// * 如果不是子查询,则为null
|
||||
// */
|
||||
// private SelectSqlInfo fromSubSelect;
|
||||
// /**
|
||||
// * 查询的字段集合,如果是 * 则为null,如果设了别名则为别名
|
||||
// */
|
||||
// private Set<String> selectFields;
|
||||
// /**
|
||||
// * 真实的查询字段集合,如果是 * 则为null,如果设了别名则为原始字段名
|
||||
// */
|
||||
// private Set<String> realSelectFields;
|
||||
// /**
|
||||
// * 是否是查询所有字段
|
||||
// */
|
||||
// private boolean selectAll;
|
||||
//
|
||||
// /**
|
||||
// * 解析之后的 SQL (关键字都是大写)
|
||||
// */
|
||||
// private final String parsedSql;
|
||||
//
|
||||
// public SelectSqlInfo(String parsedSql) {
|
||||
// this.parsedSql = parsedSql;
|
||||
// }
|
||||
//
|
||||
// public SelectSqlInfo(SelectBody selectBody) {
|
||||
// this.parsedSql = selectBody.toString();
|
||||
// }
|
||||
//
|
||||
// public void addSelectField(String selectField, String realSelectField) {
|
||||
// if (this.selectFields == null) {
|
||||
// this.selectFields = new HashSet<>();
|
||||
// }
|
||||
// if (this.realSelectFields == null) {
|
||||
// this.realSelectFields = new HashSet<>();
|
||||
// }
|
||||
// this.selectFields.add(selectField);
|
||||
// this.realSelectFields.add(realSelectField);
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * 获取所有字段,包括子查询里的。
|
||||
// *
|
||||
// * @return
|
||||
// */
|
||||
// public Set<String> getAllRealSelectFields() {
|
||||
// Set<String> fields = new HashSet<>();
|
||||
// // 递归获取所有字段,起个直观的方法名为:
|
||||
// this.recursiveGetAllFields(this, fields);
|
||||
// return fields;
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * 递归获取所有字段
|
||||
// */
|
||||
// private void recursiveGetAllFields(SelectSqlInfo sqlInfo, Set<String> fields) {
|
||||
// if (!sqlInfo.isSelectAll() && sqlInfo.getRealSelectFields() != null) {
|
||||
// fields.addAll(sqlInfo.getRealSelectFields());
|
||||
// }
|
||||
// if (sqlInfo.getFromSubSelect() != null) {
|
||||
// recursiveGetAllFields(sqlInfo.getFromSubSelect(), fields);
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// @Override
|
||||
// public String toString() {
|
||||
// return "SelectSqlInfo{" +
|
||||
// "fromTableName='" + fromTableName + '\'' +
|
||||
// ", fromSubSelect=" + fromSubSelect +
|
||||
// ", aliasName='" + fromTableAliasName + '\'' +
|
||||
// ", selectFields=" + selectFields +
|
||||
// ", realSelectFields=" + realSelectFields +
|
||||
// ", selectAll=" + selectAll +
|
||||
// "}";
|
||||
// }
|
||||
//
|
||||
//}
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
//package org.jeecg.config;
|
||||
//
|
||||
//
|
||||
//import com.github.xiaoymin.knife4j.spring.annotations.EnableKnife4j;
|
||||
//import io.swagger.v3.oas.annotations.Operation;
|
||||
//import org.jeecg.common.constant.CommonConstant;
|
||||
//import org.jeecg.config.mybatis.MybatisPlusSaasConfig;
|
||||
//import org.springframework.beans.BeansException;
|
||||
//import org.springframework.beans.factory.config.BeanPostProcessor;
|
||||
//import org.springframework.context.annotation.Bean;
|
||||
@ -18,15 +19,13 @@
|
||||
//import springfox.documentation.builders.ParameterBuilder;
|
||||
//import springfox.documentation.builders.PathSelectors;
|
||||
//import springfox.documentation.builders.RequestHandlerSelectors;
|
||||
//import springfox.documentation.oas.annotations.EnableOpenApi;
|
||||
//import springfox.documentation.schema.ModelRef;
|
||||
//import springfox.documentation.service.*;
|
||||
//import springfox.documentation.spi.DocumentationType;
|
||||
//import springfox.documentation.spi.service.contexts.SecurityContext;
|
||||
//import springfox.documentation.spring.web.plugins.Docket;
|
||||
//import springfox.documentation.spring.web.plugins.WebFluxRequestHandlerProvider;
|
||||
//import springfox.documentation.spring.web.plugins.WebMvcRequestHandlerProvider;
|
||||
//import springfox.documentation.swagger2.annotations.EnableSwagger2;
|
||||
//import springfox.documentation.swagger2.annotations.EnableSwagger2WebMvc;
|
||||
//
|
||||
//import java.lang.reflect.Field;
|
||||
//import java.util.ArrayList;
|
||||
@ -38,8 +37,7 @@
|
||||
// * @Author scott
|
||||
// */
|
||||
//@Configuration
|
||||
//@EnableSwagger2 //开启 Swagger2
|
||||
//@EnableKnife4j //开启 knife4j,可以不写
|
||||
//@EnableSwagger2WebMvc
|
||||
//@Import(BeanValidatorPluginsConfiguration.class)
|
||||
//public class Swagger2Config implements WebMvcConfigurer {
|
||||
//
|
||||
@ -97,6 +95,14 @@
|
||||
// List<Parameter> pars = new ArrayList<>();
|
||||
// tokenPar.name(CommonConstant.X_ACCESS_TOKEN).description("token").modelRef(new ModelRef("string")).parameterType("header").required(false).build();
|
||||
// pars.add(tokenPar.build());
|
||||
// //update-begin-author:liusq---date:2024-08-15--for: 开启多租户时,全局参数增加租户id
|
||||
// if(MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL){
|
||||
// ParameterBuilder tenantPar = new ParameterBuilder();
|
||||
// tenantPar.name(CommonConstant.TENANT_ID).description("租户ID").modelRef(new ModelRef("string")).parameterType("header").required(false).build();
|
||||
// pars.add(tenantPar.build());
|
||||
// }
|
||||
// //update-end-author:liusq---date:2024-08-15--for: 开启多租户时,全局参数增加租户id
|
||||
//
|
||||
// return pars;
|
||||
// }
|
||||
//
|
||||
@ -151,7 +157,7 @@
|
||||
//
|
||||
// @Override
|
||||
// public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
|
||||
// if (bean instanceof WebMvcRequestHandlerProvider || bean instanceof WebFluxRequestHandlerProvider) {
|
||||
// if (bean instanceof WebMvcRequestHandlerProvider) {
|
||||
// customizeSpringfoxHandlerMappings(getHandlerMappings(bean));
|
||||
// }
|
||||
// return bean;
|
||||
|
||||
@ -90,7 +90,7 @@ public class Swagger3Config implements WebMvcConfigurer {
|
||||
return new OpenAPI()
|
||||
.info(new Info()
|
||||
.title("JeecgBoot 后台服务API接口文档")
|
||||
.version("3.8.0")
|
||||
.version("3.8.1")
|
||||
.contact(new Contact().name("北京国炬信息技术有限公司").url("www.jeccg.com").email("jeecgos@163.com"))
|
||||
.description( "后台API接口")
|
||||
.termsOfService("NO terms of service")
|
||||
|
||||
@ -11,17 +11,16 @@ import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
|
||||
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
|
||||
import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;
|
||||
import io.micrometer.prometheus.PrometheusMeterRegistry;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.beans.factory.config.BeanPostProcessor;
|
||||
import org.springframework.boot.actuate.trace.http.InMemoryHttpTraceRepository;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Conditional;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.http.CacheControl;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
|
||||
@ -59,6 +58,14 @@ public class WebMvcConfiguration implements WebMvcConfigurer {
|
||||
@Autowired(required = false)
|
||||
private PrometheusMeterRegistry prometheusMeterRegistry;
|
||||
|
||||
/**
|
||||
* meterRegistryPostProcessor
|
||||
* for [QQYUN-12558]【监控】系统监控的头两个tab不好使,接口404
|
||||
*/
|
||||
@Autowired(required = false)
|
||||
@Qualifier("meterRegistryPostProcessor")
|
||||
private BeanPostProcessor meterRegistryPostProcessor;
|
||||
|
||||
/**
|
||||
* 静态资源的配置 - 使得可以从磁盘中读取 Html、图片、视频、音频等
|
||||
*/
|
||||
@ -147,12 +154,17 @@ public class WebMvcConfiguration implements WebMvcConfigurer {
|
||||
|
||||
|
||||
/**
|
||||
* 解决metrics端点不显示jvm信息的问题(zyf)
|
||||
* 监听应用启动完成事件,确保 PrometheusMeterRegistry 已经初始化
|
||||
* for [QQYUN-12558]【监控】系统监控的头两个tab不好使,接口404
|
||||
* @param event
|
||||
* @author chenrui
|
||||
* @date 2025/5/26 16:46
|
||||
*/
|
||||
@Bean
|
||||
@ConditionalOnBean(name = "meterRegistryPostProcessor")
|
||||
InitializingBean forcePrometheusPostProcessor(BeanPostProcessor meterRegistryPostProcessor) {
|
||||
return () -> meterRegistryPostProcessor.postProcessAfterInitialization(prometheusMeterRegistry, "");
|
||||
@EventListener
|
||||
public void onApplicationReady(ApplicationReadyEvent event) {
|
||||
if(null != meterRegistryPostProcessor){
|
||||
meterRegistryPostProcessor.postProcessAfterInitialization(prometheusMeterRegistry, "");
|
||||
}
|
||||
}
|
||||
|
||||
// /**
|
||||
|
||||
@ -8,12 +8,14 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
@Configuration
|
||||
public class LowCodeModeConfiguration implements WebMvcConfigurer {
|
||||
|
||||
public LowCodeModeInterceptor payInterceptor() {
|
||||
return new LowCodeModeInterceptor();
|
||||
private final LowCodeModeInterceptor lowCodeModeInterceptor;
|
||||
|
||||
public LowCodeModeConfiguration(LowCodeModeInterceptor lowCodeModeInterceptor) {
|
||||
this.lowCodeModeInterceptor = lowCodeModeInterceptor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addInterceptors(InterceptorRegistry registry) {
|
||||
registry.addInterceptor(payInterceptor()).addPathPatterns(LowCodeUrlsEnum.getLowCodeInterceptUrls());
|
||||
registry.addInterceptor(lowCodeModeInterceptor).addPathPatterns(LowCodeUrlsEnum.getLowCodeInterceptUrls());
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,15 +6,13 @@ import org.apache.shiro.SecurityUtils;
|
||||
import org.jeecg.common.api.CommonAPI;
|
||||
import org.jeecg.common.api.vo.Result;
|
||||
import org.jeecg.common.constant.CommonConstant;
|
||||
import org.jeecg.common.exception.JeecgBootException;
|
||||
import org.jeecg.common.system.util.JwtUtil;
|
||||
import org.jeecg.common.system.vo.LoginUser;
|
||||
import org.jeecg.common.util.CommonUtils;
|
||||
import org.jeecg.common.util.SpringContextUtils;
|
||||
import org.jeecg.config.JeecgBaseConfig;
|
||||
import org.jeecg.config.firewall.interceptor.enums.LowCodeUrlsEnum;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.util.AntPathMatcher;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.servlet.HandlerInterceptor;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
@ -41,6 +39,7 @@ import java.util.Set;
|
||||
* @date 20230904
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class LowCodeModeInterceptor implements HandlerInterceptor {
|
||||
/**
|
||||
* 低代码开发模式
|
||||
@ -50,7 +49,8 @@ public class LowCodeModeInterceptor implements HandlerInterceptor {
|
||||
|
||||
@Resource
|
||||
private JeecgBaseConfig jeecgBaseConfig;
|
||||
@Autowired
|
||||
|
||||
@Autowired(required = false)
|
||||
private CommonAPI commonAPI;
|
||||
|
||||
/**
|
||||
@ -58,10 +58,15 @@ public class LowCodeModeInterceptor implements HandlerInterceptor {
|
||||
*/
|
||||
@Override
|
||||
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
|
||||
log.info("低代码模式,拦截请求路径:" + request.getRequestURI());
|
||||
|
||||
//1、验证是否开启低代码开发模式控制
|
||||
if (jeecgBaseConfig == null) {
|
||||
jeecgBaseConfig = SpringContextUtils.getBean(JeecgBaseConfig.class);
|
||||
}
|
||||
if (commonAPI == null) {
|
||||
commonAPI = SpringContextUtils.getBean(CommonAPI.class);
|
||||
}
|
||||
|
||||
if (jeecgBaseConfig.getFirewall()!=null && LowCodeModeInterceptor.LOW_CODE_MODE_PROD.equals(jeecgBaseConfig.getFirewall().getLowCodeMode())) {
|
||||
String requestURI = request.getRequestURI().substring(request.getContextPath().length());
|
||||
@ -70,6 +75,9 @@ public class LowCodeModeInterceptor implements HandlerInterceptor {
|
||||
Set<String> hasRoles = null;
|
||||
if (loginUser == null) {
|
||||
loginUser = commonAPI.getUserByName(JwtUtil.getUserNameByToken(SpringContextUtils.getHttpServletRequest()));
|
||||
}
|
||||
|
||||
if (loginUser != null) {
|
||||
//当前登录人拥有的角色
|
||||
hasRoles = commonAPI.queryUserRolesById(loginUser.getId());
|
||||
}
|
||||
|
||||
@ -221,6 +221,7 @@ public class ShiroConfig {
|
||||
registration.addUrlPatterns("/airag/flow/debug");
|
||||
registration.addUrlPatterns("/airag/chat/send");
|
||||
registration.addUrlPatterns("/airag/app/debug");
|
||||
registration.addUrlPatterns("/airag/app/prompt/generate");
|
||||
//支持异步
|
||||
registration.setAsyncSupported(true);
|
||||
registration.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ASYNC);
|
||||
@ -320,7 +321,7 @@ public class ShiroConfig {
|
||||
|
||||
return sentinelManager;
|
||||
}
|
||||
|
||||
|
||||
// redis 单机支持,在集群为空,或者集群无机器时候使用 add by jzyadmin@163.com
|
||||
if (lettuceConnectionFactory.getClusterConfiguration() == null || lettuceConnectionFactory.getClusterConfiguration().getClusterNodes().isEmpty()) {
|
||||
RedisManager redisManager = new RedisManager();
|
||||
|
||||
@ -1,2 +1,3 @@
|
||||
springdoc.auto-tag-classes: false
|
||||
springdoc.packages-to-scan: org.jeecg
|
||||
springdoc.packages-to-scan: org.jeecg
|
||||
springdoc.default-flat-param-object: true
|
||||
@ -1,75 +0,0 @@
|
||||
package org.jeecg.test.sqlinjection;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import net.sf.jsqlparser.JSQLParserException;
|
||||
import org.jeecg.common.util.SqlInjectionUtil;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
|
||||
/**
|
||||
* SQL注入攻击检查测试
|
||||
* @author: liusq
|
||||
* @date: 2023年09月08日
|
||||
*/
|
||||
@Slf4j
|
||||
public class TestInjectWithSqlParser {
|
||||
/**
|
||||
* 注入测试
|
||||
*
|
||||
* @param sql
|
||||
* @return
|
||||
*/
|
||||
private boolean isExistSqlInject(String sql) {
|
||||
try {
|
||||
SqlInjectionUtil.specialFilterContentForOnlineReport(sql);
|
||||
return false;
|
||||
} catch (Exception e) {
|
||||
log.info("===================================================");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void test() throws JSQLParserException {
|
||||
//不存在sql注入
|
||||
assertFalse(isExistSqlInject("select * from fm_time where dept_id=:sqlparamsmap.id and time=:sqlparamsmap.time"));
|
||||
assertFalse(isExistSqlInject("select * from test"));
|
||||
assertFalse(isExistSqlInject("select load_file(\"C:\\\\benben.txt\")"));
|
||||
assertFalse(isExistSqlInject("WITH SUB1 AS (SELECT user FROM t1) SELECT * FROM T2 WHERE id > 123 "));
|
||||
|
||||
//存在sql注入
|
||||
assertTrue(isExistSqlInject("or 1= 1 --"));
|
||||
assertTrue(isExistSqlInject("select * from test where sleep(%23)"));
|
||||
assertTrue(isExistSqlInject("select * from test where id=1 and multipoint((select * from(select * from(select user())a)b));"));
|
||||
assertTrue(isExistSqlInject("select * from users;show databases;"));
|
||||
assertTrue(isExistSqlInject("select * from dc_device where id=1 and length((select group_concat(table_name) from information_schema.tables where table_schema=database()))>13"));
|
||||
assertTrue(isExistSqlInject("update user set name = '123'"));
|
||||
assertTrue(isExistSqlInject("SELECT * FROM users WHERE username = 'admin' AND password = '123456' OR 1=1;--"));
|
||||
assertTrue(isExistSqlInject("select * from users where id=1 and (select count(*) from information_schema.tables where table_schema='数据库名')>4 %23"));
|
||||
assertTrue(isExistSqlInject("select * from dc_device where sleep(5) %23"));
|
||||
assertTrue(isExistSqlInject("select * from dc_device where id in (select id from other)"));
|
||||
assertTrue(isExistSqlInject("select * from dc_device where id in (select id from other)"));
|
||||
assertTrue(isExistSqlInject("select * from dc_device where 2=2.0 or 2 != 4"));
|
||||
assertTrue(isExistSqlInject("select * from dc_device where 1!=2.0"));
|
||||
assertTrue(isExistSqlInject("select * from dc_device where id=floor(2.0)"));
|
||||
assertTrue(isExistSqlInject("select * from dc_device where not true"));
|
||||
assertTrue(isExistSqlInject("select * from dc_device where 1 or id > 0"));
|
||||
assertTrue(isExistSqlInject("select * from dc_device where 'tom' or id > 0"));
|
||||
assertTrue(isExistSqlInject("select * from dc_device where '-2.3' "));
|
||||
assertTrue(isExistSqlInject("select * from dc_device where 2 "));
|
||||
assertTrue(isExistSqlInject("select * from dc_device where (3+2) "));
|
||||
assertTrue(isExistSqlInject("select * from dc_device where -1 IS TRUE"));
|
||||
assertTrue(isExistSqlInject("select * from dc_device where 'hello' is null "));
|
||||
assertTrue(isExistSqlInject("select * from dc_device where '2022-10-31' and id > 0"));
|
||||
assertTrue(isExistSqlInject("select * from dc_device where id > 0 or 1!=2.0 "));
|
||||
assertTrue(isExistSqlInject("select * from dc_device where id > 0 or 1 in (1,3,4) "));
|
||||
assertTrue(isExistSqlInject("select * from dc_device UNION select name from other"));
|
||||
assertTrue(isExistSqlInject("(SELECT 6240 FROM (SELECT(SLEEP(5))and 1=2)vidl)"));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -1,50 +0,0 @@
|
||||
package org.jeecg.test.sqlinjection;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import net.sf.jsqlparser.JSQLParserException;
|
||||
import org.jeecg.common.util.SqlInjectionUtil;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
|
||||
/**
|
||||
* SQL注入攻击检查测试
|
||||
* @author: liusq
|
||||
* @date: 2023年09月08日
|
||||
*/
|
||||
@Slf4j
|
||||
public class TestSqlInjectForDict {
|
||||
/**
|
||||
* 注入测试
|
||||
*
|
||||
* @param sql
|
||||
* @return
|
||||
*/
|
||||
private boolean isExistSqlInject(String sql) {
|
||||
try {
|
||||
SqlInjectionUtil.specialFilterContentForDictSql(sql);
|
||||
return false;
|
||||
} catch (Exception e) {
|
||||
log.info("===================================================");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void test() throws JSQLParserException {
|
||||
//不存在sql注入
|
||||
assertFalse(isExistSqlInject("sys_user,realname,id"));
|
||||
assertFalse(isExistSqlInject("oa_officialdoc_organcode,organ_name,id"));
|
||||
assertFalse(isExistSqlInject("onl_cgform_head where table_type!=3 and copy_type=0,table_txt,table_name"));
|
||||
assertFalse(isExistSqlInject("onl_cgform_head where copy_type = 0,table_txt,table_name"));
|
||||
|
||||
//存在sql注入
|
||||
assertTrue(isExistSqlInject("or 1= 1 --"));
|
||||
assertTrue(isExistSqlInject("select * from test where sleep(%23)"));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -1,60 +0,0 @@
|
||||
package org.jeecg.test.sqlinjection;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import net.sf.jsqlparser.JSQLParserException;
|
||||
import org.jeecg.common.util.SqlInjectionUtil;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
|
||||
/**
|
||||
* SQL注入攻击检查测试
|
||||
* @author: liusq
|
||||
* @date: 2023年09月08日
|
||||
*/
|
||||
@Slf4j
|
||||
public class TestSqlInjectForOnlineReport {
|
||||
/**
|
||||
* 注入测试
|
||||
*
|
||||
* @param sql
|
||||
* @return
|
||||
*/
|
||||
private boolean isExistSqlInject(String sql) {
|
||||
try {
|
||||
SqlInjectionUtil.specialFilterContentForOnlineReport(sql);
|
||||
return false;
|
||||
} catch (Exception e) {
|
||||
log.info("===================================================");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void test() throws JSQLParserException {
|
||||
//不存在sql注入
|
||||
assertFalse(isExistSqlInject("select * from fm_time where dept_id=:sqlparamsmap.id and time=:sqlparamsmap.time"));
|
||||
assertFalse(isExistSqlInject("select * from test"));
|
||||
assertFalse(isExistSqlInject("select load_file(\"C:\\\\benben.txt\")"));
|
||||
assertFalse(isExistSqlInject("select * from dc_device where id in (select id from other)"));
|
||||
assertFalse(isExistSqlInject("select * from dc_device UNION select name from other"));
|
||||
|
||||
//存在sql注入
|
||||
assertTrue(isExistSqlInject("(SELECT 6240 FROM (SELECT(SLEEP(5))and 1=2)vidl)"));
|
||||
assertTrue(isExistSqlInject("or 1= 1 --"));
|
||||
assertTrue(isExistSqlInject("select * from test where sleep(%23)"));
|
||||
assertTrue(isExistSqlInject("select * from test where SLEEP(3)"));
|
||||
assertTrue(isExistSqlInject("select * from test where id=1 and multipoint((select * from(select * from(select user())a)b));"));
|
||||
assertTrue(isExistSqlInject("select * from users;show databases;"));
|
||||
assertTrue(isExistSqlInject("select * from dc_device where id=1 and length((select group_concat(table_name) from information_schema.tables where table_schema=database()))>13"));
|
||||
assertTrue(isExistSqlInject("update user set name = '123'"));
|
||||
assertTrue(isExistSqlInject("SELECT * FROM users WHERE username = 'admin' AND password = '123456' OR 1=1;--"));
|
||||
assertTrue(isExistSqlInject("select * from users where id=1 and (select count(*) from information_schema.tables where table_schema='数据库名')>4 %23"));
|
||||
assertTrue(isExistSqlInject("select * from dc_device where sleep(5) %23"));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -1,103 +0,0 @@
|
||||
package org.jeecg.test.sqlinjection;
|
||||
|
||||
import com.baomidou.mybatisplus.core.toolkit.sql.SqlInjectionUtils;
|
||||
import org.jeecg.common.util.SqlInjectionUtil;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* @Description: SQL注入测试类
|
||||
* @author: scott
|
||||
* @date: 2023年08月14日 9:55
|
||||
*/
|
||||
public class TestSqlInjection {
|
||||
|
||||
|
||||
/**
|
||||
* 表名带别名,同时有html编码字符
|
||||
*/
|
||||
@Test
|
||||
public void testSpecialSQL() {
|
||||
String tableName = "sys_user t";
|
||||
//解决使用参数tableName=sys_user t&复测,漏洞仍然存在
|
||||
if (tableName.contains(" ")) {
|
||||
tableName = tableName.substring(0, tableName.indexOf(" "));
|
||||
}
|
||||
//【issues/4393】 sys_user , (sys_user), sys_user%20, %60sys_user%60
|
||||
String reg = "\\s+|\\(|\\)|`";
|
||||
tableName = tableName.replaceAll(reg, "");
|
||||
System.out.println(tableName);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 测试sql是否含sql注入风险
|
||||
* <p>
|
||||
* mybatis plus的方法
|
||||
*/
|
||||
@Test
|
||||
public void sqlInjectionCheck() {
|
||||
String sql = "select * from sys_user";
|
||||
System.out.println(SqlInjectionUtils.check(sql));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 测试sql是否有SLEEP风险
|
||||
* <p>
|
||||
* mybatisPlus的方法
|
||||
*/
|
||||
@Test
|
||||
public void sqlSleepCheck() {
|
||||
SqlInjectionUtil.checkSqlAnnotation("(SELECT 6240 FROM (SELECT(SLEEP(5))and 1=2)vidl)");
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试sql是否含sql注入风险
|
||||
* <p>
|
||||
* 自定义方法
|
||||
*/
|
||||
@Test
|
||||
public void sqlInjectionCheck2() {
|
||||
String sql = "select * from sys_user";
|
||||
SqlInjectionUtil.specialFilterContentForOnlineReport(sql);
|
||||
}
|
||||
|
||||
/**
|
||||
* 字段定义只能是是字母 数字 下划线的组合(不允许有空格、转义字符串等)
|
||||
* <p>
|
||||
* 判断字段名是否符合规范
|
||||
*/
|
||||
@Test
|
||||
public void testFieldSpecification() {
|
||||
List<String> list = new ArrayList();
|
||||
list.add("Hello World!");
|
||||
list.add("Hello%20World!");
|
||||
list.add("HelloWorld!");
|
||||
list.add("Hello World");
|
||||
list.add("age");
|
||||
list.add("user_name");
|
||||
list.add("user_name%20");
|
||||
list.add("user_name%20 ");
|
||||
|
||||
for (String input : list) {
|
||||
boolean containsSpecialChars = isValidString(input);
|
||||
System.out.println("input:" + input + " ,包含空格和特殊字符: " + containsSpecialChars);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 字段定义只能是是字母 数字 下划线的组合(不允许有空格、转义字符串等)
|
||||
*
|
||||
* @param input
|
||||
* @return
|
||||
*/
|
||||
private static boolean isValidString(String input) {
|
||||
Pattern pattern = Pattern.compile("^[a-zA-Z0-9_]+$");
|
||||
return pattern.matcher(input).matches();
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,109 +0,0 @@
|
||||
package org.jeecg.test.sqlparse;
|
||||
|
||||
import net.sf.jsqlparser.JSQLParserException;
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
import org.jeecg.common.util.sqlparse.JSqlParserUtils;
|
||||
import org.jeecg.common.util.sqlparse.vo.SelectSqlInfo;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 针对 JSqlParserUtils 的单元测试
|
||||
*/
|
||||
public class JSqlParserUtilsTest {
|
||||
|
||||
private static final String[] sqlList = new String[]{
|
||||
"select * from sys_user",
|
||||
"select u.* from sys_user u",
|
||||
"select u.*, c.name from sys_user u, demo c",
|
||||
"select u.age, c.name from sys_user u, demo c",
|
||||
"select sex, age, c.name from sys_user, demo c",
|
||||
// 别名测试
|
||||
"select username as realname from sys_user",
|
||||
"select username as realname, u.realname as aaa, u.id bbb from sys_user u",
|
||||
// 不存在真实地查询字段
|
||||
"select count(1) from sys_user",
|
||||
// 函数式字段
|
||||
"select max(sex), id from sys_user",
|
||||
// 复杂嵌套函数式字段
|
||||
"select CONCAT(CONCAT(' _ ', sex), ' - ' , birthday) as info, id from sys_user",
|
||||
// 更复杂的嵌套函数式字段
|
||||
"select CONCAT(CONCAT(101,'_',NULL, DATE(create_time),'_',sex),' - ',birthday) as info, id from sys_user",
|
||||
// 子查询SQL
|
||||
"select u.name1 as name2 from (select username as name1 from sys_user) u",
|
||||
// 多层嵌套子查询SQL
|
||||
"select u2.name2 as name3 from (select u1.name1 as name2 from (select username as name1 from sys_user) u1) u2",
|
||||
// 字段子查询SQL
|
||||
"select id, (select username as name1 from sys_user u2 where u1.id = u2.id) as name2 from sys_user u1",
|
||||
// 带条件的SQL(不解析where条件里的字段,但不影响解析查询字段)
|
||||
"select username as name1 from sys_user where realname LIKE '%张%'",
|
||||
// 多重复杂关联表查询解析,包含的表为:sys_user, sys_depart, sys_dict_item, demo
|
||||
"" +
|
||||
"SELECT " +
|
||||
" u.*, d.age, sd.item_text AS sex, (SELECT count(sd.id) FROM sys_depart sd) AS count " +
|
||||
"FROM " +
|
||||
" (SELECT sd.username AS foo, sd.realname FROM sys_user sd) u, " +
|
||||
" demo d " +
|
||||
"LEFT JOIN sys_dict_item AS sd ON d.sex = sd.item_value " +
|
||||
"WHERE sd.dict_id = '3d9a351be3436fbefb1307d4cfb49bf2'",
|
||||
};
|
||||
|
||||
@Test
|
||||
public void testParseSelectSql() {
|
||||
System.out.println("-----------------------------------------");
|
||||
for (String sql : sqlList) {
|
||||
System.out.println("待测试的sql:" + sql);
|
||||
try {
|
||||
// 解析所有的表名,key=表名,value=解析后的sql信息
|
||||
Map<String, SelectSqlInfo> parsedMap = JSqlParserUtils.parseAllSelectTable(sql);
|
||||
assert parsedMap != null;
|
||||
for (Map.Entry<String, SelectSqlInfo> entry : parsedMap.entrySet()) {
|
||||
System.out.println("表名:" + entry.getKey());
|
||||
this.printSqlInfo(entry.getValue(), 1);
|
||||
}
|
||||
} catch (JSQLParserException e) {
|
||||
System.out.println("SQL解析出现异常:" + e.getMessage());
|
||||
}
|
||||
System.out.println("-----------------------------------------");
|
||||
}
|
||||
}
|
||||
|
||||
private void printSqlInfo(SelectSqlInfo sqlInfo, int level) {
|
||||
String beforeStr = this.getBeforeStr(level);
|
||||
if (sqlInfo.getFromTableName() == null) {
|
||||
// 子查询
|
||||
System.out.println(beforeStr + "子查询:" + sqlInfo.getFromSubSelect().getParsedSql());
|
||||
this.printSqlInfo(sqlInfo.getFromSubSelect(), level + 1);
|
||||
} else {
|
||||
// 非子查询
|
||||
System.out.println(beforeStr + "查询的表名:" + sqlInfo.getFromTableName());
|
||||
}
|
||||
if (oConvertUtils.isNotEmpty(sqlInfo.getFromTableAliasName())) {
|
||||
System.out.println(beforeStr + "查询的表别名:" + sqlInfo.getFromTableAliasName());
|
||||
}
|
||||
if (sqlInfo.isSelectAll()) {
|
||||
System.out.println(beforeStr + "查询的字段:*");
|
||||
} else {
|
||||
System.out.println(beforeStr + "查询的字段:" + sqlInfo.getSelectFields());
|
||||
System.out.println(beforeStr + "真实的字段:" + sqlInfo.getRealSelectFields());
|
||||
if (sqlInfo.getFromTableName() == null) {
|
||||
System.out.println(beforeStr + "所有的字段(包括子查询):" + sqlInfo.getAllRealSelectFields());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 打印前缀,根据层级来打印
|
||||
private String getBeforeStr(int level) {
|
||||
if (level == 0) {
|
||||
return "";
|
||||
}
|
||||
StringBuilder beforeStr = new StringBuilder();
|
||||
for (int i = 0; i < level; i++) {
|
||||
beforeStr.append(" ");
|
||||
}
|
||||
beforeStr.append("- ");
|
||||
return beforeStr.toString();
|
||||
}
|
||||
|
||||
}
|
||||
@ -6,7 +6,7 @@
|
||||
<parent>
|
||||
<groupId>org.jeecgframework.boot</groupId>
|
||||
<artifactId>jeecg-boot-module</artifactId>
|
||||
<version>3.8.0</version>
|
||||
<version>3.8.1</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>jeecg-boot-module-airag</artifactId>
|
||||
@ -53,12 +53,61 @@
|
||||
<artifactId>jeecg-system-cloud-api</artifactId>
|
||||
</dependency>-->
|
||||
|
||||
<!-- aiflow依赖 -->
|
||||
<dependency>
|
||||
<groupId>org.jeecgframework.boot</groupId>
|
||||
<artifactId>jeecg-aiflow-jdk8</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<artifactId>jeecg-aiflow</artifactId>
|
||||
<version>1.1.1</version>
|
||||
</dependency>
|
||||
|
||||
<!-- beigin 这两个依赖太多每个包50M左右,如果你发布需要使用,请把<scope>provided</scope>删掉 -->
|
||||
<dependency>
|
||||
<groupId>org.jetbrains.kotlin</groupId>
|
||||
<artifactId>kotlin-scripting-jsr223</artifactId>
|
||||
<version>${kotlin.version}</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.yomahub</groupId>
|
||||
<artifactId>liteflow-script-graaljs</artifactId>
|
||||
<version>${liteflow.version}</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<!-- end 这两个依赖太多每个包50M左右,如果你发布需要使用,请把<scope>provided</scope>删掉 -->
|
||||
|
||||
<!-- aiflow 脚本依赖 -->
|
||||
<dependency>
|
||||
<groupId>com.yomahub</groupId>
|
||||
<artifactId>liteflow-script-groovy</artifactId>
|
||||
<version>${liteflow.version}</version>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.yomahub</groupId>
|
||||
<artifactId>liteflow-script-kotlin</artifactId>
|
||||
<version>${liteflow.version}</version>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.jetbrains.kotlin</groupId>
|
||||
<artifactId>kotlin-scripting-jsr223</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.yomahub</groupId>
|
||||
<artifactId>liteflow-script-aviator</artifactId>
|
||||
<version>${liteflow.version}</version>
|
||||
<scope>runtime</scope>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<artifactId>aviator</artifactId>
|
||||
<groupId>com.googlecode.aviator</groupId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<!-- aiflow 脚本依赖 -->
|
||||
|
||||
<!-- langChain4j model support -->
|
||||
<dependency>
|
||||
<groupId>dev.langchain4j</groupId>
|
||||
|
||||
@ -16,6 +16,10 @@ public class AiAppConsts {
|
||||
* 状态:禁用
|
||||
*/
|
||||
public static final String STATUS_DISABLE = "disable";
|
||||
/**
|
||||
* 状态:发布
|
||||
*/
|
||||
public static final String STATUS_RELEASE = "release";
|
||||
|
||||
|
||||
/**
|
||||
|
||||
@ -4,10 +4,13 @@ import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
||||
import org.jeecg.common.api.vo.Result;
|
||||
import org.jeecg.common.system.base.controller.JeecgController;
|
||||
import org.jeecg.common.system.query.QueryGenerator;
|
||||
import org.jeecg.common.util.AssertUtils;
|
||||
import org.jeecg.common.util.TokenUtils;
|
||||
import org.jeecg.config.mybatis.MybatisPlusSaasConfig;
|
||||
import org.jeecg.config.shiro.IgnoreAuth;
|
||||
import org.jeecg.modules.airag.app.consts.AiAppConsts;
|
||||
import org.jeecg.modules.airag.app.entity.AiragApp;
|
||||
@ -19,7 +22,6 @@ import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* @Description: AI应用
|
||||
@ -64,6 +66,7 @@ public class AiragAppController extends JeecgController<AiragApp, IAiragAppServi
|
||||
* @return
|
||||
*/
|
||||
@RequestMapping(value = "/edit", method = {RequestMethod.PUT, RequestMethod.POST})
|
||||
@RequiresPermissions("airag:app:edit")
|
||||
public Result<String> edit(@RequestBody AiragApp airagApp) {
|
||||
AssertUtils.assertNotEmpty("参数异常", airagApp);
|
||||
AssertUtils.assertNotEmpty("请输入应用名称", airagApp.getName());
|
||||
@ -73,6 +76,28 @@ public class AiragAppController extends JeecgController<AiragApp, IAiragAppServi
|
||||
return Result.OK("保存完成!", airagApp.getId());
|
||||
}
|
||||
|
||||
/**
|
||||
* 发布应用
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@RequestMapping(value = "/release", method = RequestMethod.POST)
|
||||
public Result<String> release(@RequestParam(name = "id") String id, @RequestParam(name = "release") Boolean release) {
|
||||
AssertUtils.assertNotEmpty("id必须填写", id);
|
||||
if (release == null) {
|
||||
release = true;
|
||||
}
|
||||
AiragApp airagApp = new AiragApp();
|
||||
airagApp.setId(id);
|
||||
if (release) {
|
||||
airagApp.setStatus(AiAppConsts.STATUS_RELEASE);
|
||||
} else {
|
||||
airagApp.setStatus(AiAppConsts.STATUS_ENABLE);
|
||||
}
|
||||
airagAppService.updateById(airagApp);
|
||||
return Result.OK(release ? "发布成功" : "取消发布成功");
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过id删除
|
||||
*
|
||||
@ -80,23 +105,23 @@ public class AiragAppController extends JeecgController<AiragApp, IAiragAppServi
|
||||
* @return
|
||||
*/
|
||||
@DeleteMapping(value = "/delete")
|
||||
public Result<String> delete(@RequestParam(name = "id", required = true) String id) {
|
||||
@RequiresPermissions("airag:app:delete")
|
||||
public Result<String> delete(HttpServletRequest request,@RequestParam(name = "id", required = true) String id) {
|
||||
//update-begin---author:chenrui ---date:20250606 for:[issues/8337]关于ai工作列表的数据权限问题 #8337------------
|
||||
//如果是saas隔离的情况下,判断当前租户id是否是当前租户下的
|
||||
if (MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL) {
|
||||
AiragApp app = airagAppService.getById(id);
|
||||
//获取当前租户
|
||||
String currentTenantId = TokenUtils.getTenantIdByRequest(request);
|
||||
if (null == app || !app.getTenantId().equals(currentTenantId)) {
|
||||
return Result.error("删除AI应用失败,不能删除其他租户的AI应用!");
|
||||
}
|
||||
}
|
||||
//update-end---author:chenrui ---date:20250606 for:[issues/8337]关于ai工作列表的数据权限问题 #8337------------
|
||||
airagAppService.removeById(id);
|
||||
return Result.OK("删除成功!");
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除
|
||||
*
|
||||
* @param ids
|
||||
* @return
|
||||
*/
|
||||
@DeleteMapping(value = "/deleteBatch")
|
||||
public Result<String> deleteBatch(@RequestParam(name = "ids", required = true) String ids) {
|
||||
this.airagAppService.removeByIds(Arrays.asList(ids.split(",")));
|
||||
return Result.OK("批量删除成功!");
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过id查询
|
||||
*
|
||||
|
||||
@ -2,14 +2,22 @@ package org.jeecg.modules.airag.app.controller;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jeecg.common.api.vo.Result;
|
||||
import org.jeecg.common.constant.CommonConstant;
|
||||
import org.jeecg.common.util.CommonUtils;
|
||||
import org.jeecg.config.shiro.IgnoreAuth;
|
||||
import org.jeecg.modules.airag.app.service.IAiragChatService;
|
||||
import org.jeecg.modules.airag.app.vo.ChatConversation;
|
||||
import org.jeecg.modules.airag.app.vo.ChatSendParams;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import org.springframework.web.multipart.MultipartHttpServletRequest;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
|
||||
/**
|
||||
* airag应用-chat
|
||||
@ -25,6 +33,15 @@ public class AiragChatController {
|
||||
@Autowired
|
||||
IAiragChatService chatService;
|
||||
|
||||
@Value(value = "${jeecg.path.upload}")
|
||||
private String uploadpath;
|
||||
|
||||
/**
|
||||
* 本地:local minio:minio 阿里:alioss
|
||||
*/
|
||||
@Value(value="${jeecg.uploadType}")
|
||||
private String uploadType;
|
||||
|
||||
|
||||
/**
|
||||
* 发送消息
|
||||
@ -59,6 +76,19 @@ public class AiragChatController {
|
||||
return chatService.send(chatSendParams);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有对话
|
||||
*
|
||||
* @return 返回一个Result对象,包含所有对话的信息
|
||||
* @author chenrui
|
||||
* @date 2025/2/25 11:42
|
||||
*/
|
||||
@IgnoreAuth
|
||||
@GetMapping(value = "/init")
|
||||
public Result<?> initChat(@RequestParam(name = "id", required = true) String id) {
|
||||
return chatService.initChat(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有对话
|
||||
*
|
||||
@ -141,4 +171,36 @@ public class AiragChatController {
|
||||
return chatService.stop(requestId);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 上传文件
|
||||
* for [QQYUN-12135]AI聊天,上传图片提示非法token
|
||||
*
|
||||
* @param request
|
||||
* @param response
|
||||
* @return
|
||||
* @throws Exception
|
||||
* @author chenrui
|
||||
* @date 2025/4/25 11:04
|
||||
*/
|
||||
@IgnoreAuth
|
||||
@PostMapping(value = "/upload")
|
||||
public Result<?> upload(HttpServletRequest request, HttpServletResponse response) throws Exception {
|
||||
String bizPath = "airag";
|
||||
|
||||
MultipartHttpServletRequest multipartRequest = (MultipartHttpServletRequest) request;
|
||||
// 获取上传文件对象
|
||||
MultipartFile file = multipartRequest.getFile("file");
|
||||
String savePath;
|
||||
if (CommonConstant.UPLOAD_TYPE_LOCAL.equals(uploadType)) {
|
||||
savePath = CommonUtils.uploadLocal(file, bizPath, uploadpath);
|
||||
} else {
|
||||
savePath = CommonUtils.upload(file, bizPath, uploadType);
|
||||
}
|
||||
Result<?> result = new Result<>();
|
||||
result.setMessage(savePath);
|
||||
result.setSuccess(true);
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -39,12 +39,13 @@ public class AiragApp implements Serializable {
|
||||
*/
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
@Schema(description = "主键")
|
||||
private String id;
|
||||
private java.lang.String id;
|
||||
/**
|
||||
* 创建人
|
||||
*/
|
||||
@Schema(description = "创建人")
|
||||
private String createBy;
|
||||
@Dict(dictTable = "sys_user",dicCode = "username",dicText = "realname")
|
||||
private java.lang.String createBy;
|
||||
/**
|
||||
* 创建日期
|
||||
*/
|
||||
@ -56,7 +57,7 @@ public class AiragApp implements Serializable {
|
||||
* 更新人
|
||||
*/
|
||||
@Schema(description = "更新人")
|
||||
private String updateBy;
|
||||
private java.lang.String updateBy;
|
||||
/**
|
||||
* 更新日期
|
||||
*/
|
||||
@ -68,95 +69,95 @@ public class AiragApp implements Serializable {
|
||||
* 所属部门
|
||||
*/
|
||||
@Schema(description = "所属部门")
|
||||
private String sysOrgCode;
|
||||
private java.lang.String sysOrgCode;
|
||||
/**
|
||||
* 租户id
|
||||
*/
|
||||
@Excel(name = "租户id", width = 15)
|
||||
@Schema(description = "租户id")
|
||||
private String tenantId;
|
||||
private java.lang.String tenantId;
|
||||
/**
|
||||
* 应用名称
|
||||
*/
|
||||
@Excel(name = "应用名称", width = 15)
|
||||
@Schema(description = "应用名称")
|
||||
private String name;
|
||||
private java.lang.String name;
|
||||
/**
|
||||
* 应用描述
|
||||
*/
|
||||
@Excel(name = "应用描述", width = 15)
|
||||
@Schema(description = "应用描述")
|
||||
private String descr;
|
||||
private java.lang.String descr;
|
||||
/**
|
||||
* 应用图标
|
||||
*/
|
||||
@Excel(name = "应用图标", width = 15)
|
||||
@Schema(description = "应用图标")
|
||||
private String icon;
|
||||
private java.lang.String icon;
|
||||
/**
|
||||
* 应用类型
|
||||
*/
|
||||
@Excel(name = "应用类型", width = 15, dicCode = "ai_app_type")
|
||||
@Dict(dicCode = "ai_app_type")
|
||||
@Schema(description = "应用类型")
|
||||
private String type;
|
||||
private java.lang.String type;
|
||||
/**
|
||||
* 开场白
|
||||
*/
|
||||
@Excel(name = "开场白", width = 15)
|
||||
@Schema(description = "开场白")
|
||||
private String prologue;
|
||||
private java.lang.String prologue;
|
||||
/**
|
||||
* 预设问题
|
||||
*/
|
||||
@Excel(name = "预设问题", width = 15)
|
||||
@Schema(description = "预设问题")
|
||||
private String presetQuestion;
|
||||
private java.lang.String presetQuestion;
|
||||
/**
|
||||
* 提示词
|
||||
*/
|
||||
@Excel(name = "提示词", width = 15)
|
||||
@Schema(description = "提示词")
|
||||
private String prompt;
|
||||
private java.lang.String prompt;
|
||||
/**
|
||||
* 模型配置
|
||||
*/
|
||||
@Excel(name = "模型配置", width = 15, dictTable = "airag_model where model_type = 'LLM' ", dicText = "name", dicCode = "id")
|
||||
@Dict(dictTable = "airag_model where model_type = 'LLM' ", dicText = "name", dicCode = "id")
|
||||
@Schema(description = "模型配置")
|
||||
private String modelId;
|
||||
private java.lang.String modelId;
|
||||
/**
|
||||
* 历史消息数
|
||||
*/
|
||||
@Excel(name = "历史消息数", width = 15)
|
||||
@Schema(description = "历史消息数")
|
||||
private Integer msgNum;
|
||||
private java.lang.Integer msgNum;
|
||||
/**
|
||||
* 知识库
|
||||
*/
|
||||
@Excel(name = "知识库", width = 15, dictTable = "airag_knowledge where status = 'enable'", dicText = "name", dicCode = "id")
|
||||
@Dict(dictTable = "airag_knowledge where status = 'enable'", dicText = "name", dicCode = "id")
|
||||
@Schema(description = "知识库")
|
||||
private String knowledgeIds;
|
||||
private java.lang.String knowledgeIds;
|
||||
/**
|
||||
* 流程
|
||||
*/
|
||||
@Excel(name = "流程", width = 15, dictTable = "airag_flow where status = 'enable' ", dicText = "name", dicCode = "id")
|
||||
@Dict(dictTable = "airag_flow where status = 'enable' ", dicText = "name", dicCode = "id")
|
||||
@Schema(description = "流程")
|
||||
private String flowId;
|
||||
private java.lang.String flowId;
|
||||
/**
|
||||
* 快捷指令
|
||||
*/
|
||||
@Excel(name = "快捷指令", width = 15)
|
||||
@Schema(description = "快捷指令")
|
||||
private String quickCommand;
|
||||
private java.lang.String quickCommand;
|
||||
/**
|
||||
* 状态
|
||||
* 状态(enable=启用、disable=禁用、release=发布)
|
||||
*/
|
||||
@Excel(name = "状态", width = 15)
|
||||
@Schema(description = "状态")
|
||||
private String status;
|
||||
private java.lang.String status;
|
||||
|
||||
|
||||
/**
|
||||
@ -164,7 +165,7 @@ public class AiragApp implements Serializable {
|
||||
*/
|
||||
@Excel(name = "元数据", width = 15)
|
||||
@Schema(description = "元数据")
|
||||
private String metadata;
|
||||
private java.lang.String metadata;
|
||||
|
||||
/**
|
||||
* 知识库ids
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
package org.jeecg.modules.airag.app.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.InterceptorIgnore;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.jeecg.modules.airag.app.entity.AiragApp;
|
||||
|
||||
@ -11,4 +12,14 @@ import org.jeecg.modules.airag.app.entity.AiragApp;
|
||||
*/
|
||||
public interface AiragAppMapper extends BaseMapper<AiragApp> {
|
||||
|
||||
/**
|
||||
* 根据ID查询app信息(忽略租户)
|
||||
* @param id
|
||||
* @return
|
||||
* @author chenrui
|
||||
* @date 2025/4/21 16:03
|
||||
*/
|
||||
@InterceptorIgnore(tenantLine = "true")
|
||||
AiragApp getByIdIgnoreTenant(String id);
|
||||
|
||||
}
|
||||
|
||||
@ -2,4 +2,8 @@
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="org.jeecg.modules.airag.app.mapper.AiragAppMapper">
|
||||
|
||||
<select id="getByIdIgnoreTenant" resultType="org.jeecg.modules.airag.app.entity.AiragApp">
|
||||
SELECT * FROM airag_app WHERE id = #{id}
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
@ -92,4 +92,14 @@ public interface IAiragChatService {
|
||||
* @date 2025/3/3 19:49
|
||||
*/
|
||||
Result<?> clearMessage(String conversationId);
|
||||
|
||||
/**
|
||||
* 初始化聊天(忽略租户)
|
||||
* [QQYUN-12113]分享之后的聊天,应用、模型、知识库不根据租户查询
|
||||
* @param appId
|
||||
* @return
|
||||
* @author chenrui
|
||||
* @date 2025/4/21 14:17
|
||||
*/
|
||||
Result<?> initChat(String appId);
|
||||
}
|
||||
|
||||
@ -13,6 +13,7 @@ import org.jeecg.common.system.util.JwtUtil;
|
||||
import org.jeecg.common.util.*;
|
||||
import org.jeecg.modules.airag.app.consts.AiAppConsts;
|
||||
import org.jeecg.modules.airag.app.entity.AiragApp;
|
||||
import org.jeecg.modules.airag.app.mapper.AiragAppMapper;
|
||||
import org.jeecg.modules.airag.app.service.IAiragAppService;
|
||||
import org.jeecg.modules.airag.app.service.IAiragChatService;
|
||||
import org.jeecg.modules.airag.app.vo.AppDebugParams;
|
||||
@ -63,7 +64,7 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
||||
RedisTemplate redisTemplate;
|
||||
|
||||
@Autowired
|
||||
IAiragAppService airagAppService;
|
||||
AiragAppMapper airagAppMapper;
|
||||
|
||||
@Autowired
|
||||
IAiragFlowService airagFlowService;
|
||||
@ -85,7 +86,7 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
||||
// 获取app信息
|
||||
AiragApp app = null;
|
||||
if (oConvertUtils.isNotEmpty(chatSendParams.getAppId())) {
|
||||
app = airagAppService.getById(chatSendParams.getAppId());
|
||||
app = airagAppMapper.getByIdIgnoreTenant(chatSendParams.getAppId());
|
||||
}
|
||||
ChatConversation chatConversation = getOrCreateChatConversation(app, conversationId);
|
||||
// 更新标题
|
||||
@ -146,13 +147,19 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
||||
try {
|
||||
// 发送完成事件
|
||||
emitter.send(SseEmitter.event().data(eventData));
|
||||
} catch (IOException e) {
|
||||
} catch (Exception e) {
|
||||
log.error("终止会话时发生错误", e);
|
||||
try {
|
||||
// 防止异常冒泡
|
||||
emitter.completeWithError(e);
|
||||
} catch (Exception ignore) {}
|
||||
} finally {
|
||||
// 从缓存中移除emitter
|
||||
AiragLocalCache.remove(AiragConsts.CACHE_TYPE_SSE, eventData.getRequestId());
|
||||
// 关闭emitter
|
||||
emitter.complete();
|
||||
try {
|
||||
emitter.complete();
|
||||
} catch (Exception ignore) {}
|
||||
}
|
||||
}
|
||||
|
||||
@ -237,6 +244,12 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<?> initChat(String appId) {
|
||||
AiragApp app = airagAppMapper.getByIdIgnoreTenant(appId);
|
||||
return Result.ok(app);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<?> deleteConversation(String conversationId) {
|
||||
AssertUtils.assertNotEmpty("请选择要删除的会话", conversationId);
|
||||
@ -278,7 +291,7 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
||||
* @author chenrui
|
||||
* @date 2025/2/25 19:27
|
||||
*/
|
||||
private String getConversationCacheKey(String conversationId,HttpServletRequest httpRequest) {
|
||||
private String getConversationCacheKey(String conversationId, HttpServletRequest httpRequest) {
|
||||
if (oConvertUtils.isEmpty(conversationId)) {
|
||||
return null;
|
||||
}
|
||||
@ -376,7 +389,7 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
||||
* @author chenrui
|
||||
* @date 2025/2/25 19:27
|
||||
*/
|
||||
private void saveChatConversation(ChatConversation chatConversation, boolean temp,HttpServletRequest httpRequest) {
|
||||
private void saveChatConversation(ChatConversation chatConversation, boolean temp, HttpServletRequest httpRequest) {
|
||||
if (null == chatConversation) {
|
||||
return;
|
||||
}
|
||||
@ -414,8 +427,7 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
||||
case AiragConsts.MESSAGE_ROLE_USER:
|
||||
List<Content> contents = new ArrayList<>();
|
||||
List<MessageHistory.ImageHistory> images = history.getImages();
|
||||
if (oConvertUtils.isObjectNotEmpty(images)
|
||||
&& !images.isEmpty()) {
|
||||
if (oConvertUtils.isObjectNotEmpty(images) && !images.isEmpty()) {
|
||||
contents.addAll(images.stream().map(imageHistory -> {
|
||||
if (oConvertUtils.isNotEmpty(imageHistory.getUrl())) {
|
||||
return ImageContent.from(imageHistory.getUrl());
|
||||
@ -452,8 +464,7 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
||||
* @author chenrui
|
||||
* @date 2025/2/25 19:05
|
||||
*/
|
||||
private void appendMessage(List<ChatMessage> messages, ChatMessage message, ChatConversation
|
||||
chatConversation, String topicId) {
|
||||
private void appendMessage(List<ChatMessage> messages, ChatMessage message, ChatConversation chatConversation, String topicId) {
|
||||
|
||||
if (message.type().equals(ChatMessageType.SYSTEM)) {
|
||||
// 系统消息,放到消息列表最前面,并且不记录历史
|
||||
@ -467,11 +478,7 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
||||
histories = new ArrayList<>();
|
||||
}
|
||||
// 消息记录
|
||||
MessageHistory historyMessage = MessageHistory.builder()
|
||||
.conversationId(chatConversation.getId())
|
||||
.topicId(topicId)
|
||||
.datetime(DateUtils.now())
|
||||
.build();
|
||||
MessageHistory historyMessage = MessageHistory.builder().conversationId(chatConversation.getId()).topicId(topicId).datetime(DateUtils.now()).build();
|
||||
if (message.type().equals(ChatMessageType.USER)) {
|
||||
historyMessage.setRole(AiragConsts.MESSAGE_ROLE_USER);
|
||||
StringBuilder textContent = new StringBuilder();
|
||||
@ -516,8 +523,21 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
||||
// 每次会话都生成一个新的,用来缓存emitter
|
||||
String requestId = UUIDGenerator.generate();
|
||||
SseEmitter emitter = new SseEmitter(-0L);
|
||||
emitter.onError(throwable -> {
|
||||
log.warn("SEE向客户端发送消息失败: {}", throwable.getMessage());
|
||||
AiragLocalCache.remove(AiragConsts.CACHE_TYPE_SSE, requestId);
|
||||
try {
|
||||
emitter.complete();
|
||||
} catch (Exception ignore) {}
|
||||
});
|
||||
EventData eventRequestId = new EventData(requestId, null, EventData.EVENT_INIT_REQUEST_ID, chatConversation.getId(), topicId);
|
||||
eventRequestId.setData(EventMessageData.builder().message("").build());
|
||||
sendMessage2Client(emitter, eventRequestId);
|
||||
// 缓存emitter
|
||||
AiragLocalCache.put(AiragConsts.CACHE_TYPE_SSE, requestId, emitter);
|
||||
// 缓存开始发送时间
|
||||
log.info("[AI-CHAT]开始发送消息,requestId:{}", requestId);
|
||||
AiragLocalCache.put(AiragConsts.CACHE_TYPE_SSE_SEND_TIME, requestId, System.currentTimeMillis());
|
||||
try {
|
||||
// 组装用户消息
|
||||
UserMessage userMessage = aiChatHandler.buildUserMessage(sendParams.getContent(), sendParams.getImages());
|
||||
@ -589,36 +609,51 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
||||
SseEmitter emitter = AiragLocalCache.get(AiragConsts.CACHE_TYPE_SSE, requestId);
|
||||
flowRunParams.setEventCallback(eventData -> {
|
||||
if (EventData.EVENT_FLOW_FINISHED.equals(eventData.getEvent())) {
|
||||
// 打印耗时日志
|
||||
printChatDuration(requestId, "流程执行完毕");
|
||||
// 已经执行完了,删除时间缓存
|
||||
AiragLocalCache.remove(AiragConsts.CACHE_TYPE_SSE_SEND_TIME, requestId);
|
||||
EventFlowData data = (EventFlowData) eventData.getData();
|
||||
Object outputs = data.getOutputs();
|
||||
if (oConvertUtils.isObjectNotEmpty(outputs)) {
|
||||
AiMessage aiMessage;
|
||||
if (outputs instanceof String) {
|
||||
// 兼容推理模型
|
||||
String messageText = String.valueOf(outputs);
|
||||
messageText = messageText.replaceAll("<think>([\\s\\S]*?)</think>", "> $1");
|
||||
aiMessage = new AiMessage(messageText);
|
||||
} else {
|
||||
aiMessage = new AiMessage(JSONObject.toJSONString(outputs));
|
||||
if(data.isSuccess()) {
|
||||
Object outputs = data.getOutputs();
|
||||
if (oConvertUtils.isObjectNotEmpty(outputs)) {
|
||||
AiMessage aiMessage;
|
||||
if (outputs instanceof String) {
|
||||
// 兼容推理模型
|
||||
String messageText = String.valueOf(outputs);
|
||||
messageText = messageText.replaceAll("<think>([\\s\\S]*?)</think>", "> $1");
|
||||
aiMessage = new AiMessage(messageText);
|
||||
} else {
|
||||
aiMessage = new AiMessage(JSONObject.toJSONString(outputs));
|
||||
}
|
||||
EventData msgEventData = new EventData(requestId, null, EventData.EVENT_MESSAGE, chatConversation.getId(), topicId);
|
||||
EventMessageData messageEventData = EventMessageData.builder().message(aiMessage.text()).build();
|
||||
msgEventData.setData(messageEventData);
|
||||
msgEventData.setRequestId(requestId);
|
||||
sendMessage2Client(emitter, msgEventData);
|
||||
appendMessage(messages, aiMessage, chatConversation, topicId);
|
||||
// 保存会话
|
||||
saveChatConversation(chatConversation, false, httpRequest);
|
||||
}
|
||||
EventData msgEventData = new EventData(requestId, null, EventData.EVENT_MESSAGE, chatConversation.getId(), topicId);
|
||||
EventMessageData messageEventData = EventMessageData.builder()
|
||||
.message(aiMessage.text())
|
||||
.build();
|
||||
msgEventData.setData(messageEventData);
|
||||
try {
|
||||
String eventStr = JSONObject.toJSONString(msgEventData);
|
||||
log.debug("[AI应用]接收FLOW返回消息:{}", eventStr);
|
||||
emitter.send(SseEmitter.event().data(eventStr));
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}else{
|
||||
//update-begin---author:chenrui ---date:20250425 for:[QQYUN-12203]AI 聊天,超时或者服务器报错,给个友好提示------------
|
||||
// 失败
|
||||
String message = data.getMessage();
|
||||
if (message != null && message.contains(FlowConsts.FLOW_ERROR_MSG_LLM_TIMEOUT)) {
|
||||
message = "当前用户较多,排队中,请稍后再试!";
|
||||
EventData errEventData = new EventData(requestId, null, EventData.EVENT_MESSAGE, chatConversation.getId(), topicId);
|
||||
errEventData.setData(EventMessageData.builder().message("\n" + message).build());
|
||||
sendMessage2Client(emitter, errEventData);
|
||||
errEventData = new EventData(requestId, null, EventData.EVENT_MESSAGE_END, chatConversation.getId(), topicId);
|
||||
// 如果是超时,主动关闭SSE,防止流程切面中返回异常消息导致前端不能正常展示上面的{普通消息}.
|
||||
closeSSE(emitter, errEventData);
|
||||
}
|
||||
appendMessage(messages, aiMessage, chatConversation, topicId);
|
||||
// 保存会话
|
||||
saveChatConversation(chatConversation, false, httpRequest);
|
||||
//update-end---author:chenrui ---date:20250425 for:[QQYUN-12203]AI 聊天,超时或者服务器报错,给个友好提示------------
|
||||
}
|
||||
}
|
||||
});
|
||||
// 打印流程耗时日志
|
||||
printChatDuration(requestId, "开始执行流程");
|
||||
airagFlowService.runFlow(flowRunParams);
|
||||
}
|
||||
|
||||
@ -649,24 +684,26 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
||||
String metadataStr = aiApp.getMetadata();
|
||||
if (oConvertUtils.isNotEmpty(metadataStr)) {
|
||||
JSONObject metadata = JSONObject.parseObject(metadataStr);
|
||||
if(oConvertUtils.isNotEmpty(metadata)){
|
||||
if (oConvertUtils.isNotEmpty(metadata)) {
|
||||
if (metadata.containsKey("temperature")) {
|
||||
aiChatParams.setTemperature(metadata.getDouble("temperature"));
|
||||
}
|
||||
if (metadata.containsKey("topP")) {
|
||||
aiChatParams.setTopP(metadata.getDouble("temperature"));
|
||||
aiChatParams.setTopP(metadata.getDouble("topP"));
|
||||
}
|
||||
if (metadata.containsKey("presencePenalty")) {
|
||||
aiChatParams.setPresencePenalty(metadata.getDouble("temperature"));
|
||||
aiChatParams.setPresencePenalty(metadata.getDouble("presencePenalty"));
|
||||
}
|
||||
if (metadata.containsKey("frequencyPenalty")) {
|
||||
aiChatParams.setFrequencyPenalty(metadata.getDouble("temperature"));
|
||||
aiChatParams.setFrequencyPenalty(metadata.getDouble("frequencyPenalty"));
|
||||
}
|
||||
if (metadata.containsKey("maxTokens")) {
|
||||
aiChatParams.setMaxTokens(metadata.getInteger("temperature"));
|
||||
aiChatParams.setMaxTokens(metadata.getInteger("maxTokens"));
|
||||
}
|
||||
}
|
||||
}
|
||||
// 打印流程耗时日志
|
||||
printChatDuration(requestId, "构造应用自定义参数完成");
|
||||
// 发消息
|
||||
sendWithDefault(requestId, chatConversation, topicId, modelId, messages, aiChatParams);
|
||||
}
|
||||
@ -683,10 +720,9 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
||||
* @author chenrui
|
||||
* @date 2025/2/25 19:24
|
||||
*/
|
||||
private void sendWithDefault(String requestId, ChatConversation chatConversation, String topicId, String modelId,
|
||||
List<ChatMessage> messages,AIChatParams aiChatParams) {
|
||||
private void sendWithDefault(String requestId, ChatConversation chatConversation, String topicId, String modelId, List<ChatMessage> messages, AIChatParams aiChatParams) {
|
||||
// 调用ai聊天
|
||||
if(null == aiChatParams){
|
||||
if (null == aiChatParams) {
|
||||
aiChatParams = new AIChatParams();
|
||||
}
|
||||
aiChatParams.setKnowIds(chatConversation.getApp().getKnowIds());
|
||||
@ -694,13 +730,15 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
||||
HttpServletRequest httpRequest = SpringContextUtils.getHttpServletRequest();
|
||||
TokenStream chatStream;
|
||||
try {
|
||||
// 打印流程耗时日志
|
||||
printChatDuration(requestId, "开始向LLM发送消息");
|
||||
if (oConvertUtils.isNotEmpty(modelId)) {
|
||||
chatStream = aiChatHandler.chat(modelId, messages, aiChatParams);
|
||||
} else {
|
||||
chatStream = aiChatHandler.chatByDefaultModel(messages, aiChatParams);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error(e.getMessage(),e);
|
||||
log.error(e.getMessage(), e);
|
||||
throw new JeecgBootBizTipException("调用大模型接口失败:" + e.getMessage());
|
||||
}
|
||||
/**
|
||||
@ -709,91 +747,120 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
||||
AtomicBoolean isThinking = new AtomicBoolean(false);
|
||||
// ai聊天响应逻辑
|
||||
chatStream.onNext((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, chatConversation.getId(), topicId);
|
||||
EventMessageData messageEventData = EventMessageData.builder()
|
||||
.message(resMessage)
|
||||
.build();
|
||||
eventData.setData(messageEventData);
|
||||
// sse
|
||||
SseEmitter emitter = AiragLocalCache.get(AiragConsts.CACHE_TYPE_SSE, requestId);
|
||||
if (null == emitter) {
|
||||
log.warn("[AI应用]接收LLM返回会话已关闭");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
String eventStr = JSONObject.toJSONString(eventData);
|
||||
log.debug("[AI应用]接收LLM返回消息:{}", eventStr);
|
||||
emitter.send(SseEmitter.event().data(eventStr));
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
})
|
||||
.onComplete((responseMessage) -> {
|
||||
// 记录ai的回复
|
||||
AiMessage aiMessage = responseMessage.content();
|
||||
FinishReason finishReason = responseMessage.finishReason();
|
||||
String respText = aiMessage.text();
|
||||
// sse
|
||||
SseEmitter emitter = AiragLocalCache.get(AiragConsts.CACHE_TYPE_SSE, requestId);
|
||||
if (null == emitter) {
|
||||
log.warn("[AI应用]接收LLM返回会话已关闭");
|
||||
return;
|
||||
}
|
||||
if (FinishReason.STOP.equals(finishReason) || null == finishReason) {
|
||||
// 正常结束
|
||||
EventData eventData = new EventData(requestId, null, EventData.EVENT_MESSAGE_END, chatConversation.getId(), topicId);
|
||||
try {
|
||||
log.debug("[AI应用]接收LLM返回消息完成:{}", respText);
|
||||
emitter.send(SseEmitter.event().data(eventData));
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
appendMessage(messages, aiMessage, chatConversation, topicId);
|
||||
// 保存会话
|
||||
saveChatConversation(chatConversation,false,httpRequest);
|
||||
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, chatConversation.getId(), topicId);
|
||||
eventData.setData(EventFlowData.builder().success(false).message(respText).build());
|
||||
closeSSE(emitter, eventData);
|
||||
}
|
||||
})
|
||||
.onError((Throwable error) -> {
|
||||
// sse
|
||||
SseEmitter emitter = AiragLocalCache.get(AiragConsts.CACHE_TYPE_SSE, requestId);
|
||||
if (null == emitter) {
|
||||
log.warn("[AI应用]接收LLM返回会话已关闭");
|
||||
return;
|
||||
}
|
||||
String errMsg = "调用大模型接口失败:" + error.getMessage();
|
||||
log.error(errMsg, error);
|
||||
EventData eventData = new EventData(requestId, null, EventData.EVENT_FLOW_ERROR, chatConversation.getId(), topicId);
|
||||
eventData.setData(EventFlowData.builder().success(false).message(errMsg).build());
|
||||
closeSSE(emitter, eventData);
|
||||
})
|
||||
.start();
|
||||
// 兼容推理模型
|
||||
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, chatConversation.getId(), topicId);
|
||||
EventMessageData messageEventData = EventMessageData.builder().message(resMessage).build();
|
||||
eventData.setData(messageEventData);
|
||||
eventData.setRequestId(requestId);
|
||||
// sse
|
||||
SseEmitter emitter = AiragLocalCache.get(AiragConsts.CACHE_TYPE_SSE, requestId);
|
||||
if (null == emitter) {
|
||||
log.warn("[AI应用]接收LLM返回会话已关闭");
|
||||
return;
|
||||
}
|
||||
sendMessage2Client(emitter, eventData);
|
||||
}).onComplete((responseMessage) -> {
|
||||
// 打印流程耗时日志
|
||||
printChatDuration(requestId, "LLM输出消息完成");
|
||||
AiragLocalCache.remove(AiragConsts.CACHE_TYPE_SSE_SEND_TIME, requestId);
|
||||
// 记录ai的回复
|
||||
AiMessage aiMessage = responseMessage.content();
|
||||
FinishReason finishReason = responseMessage.finishReason();
|
||||
String respText = aiMessage.text();
|
||||
// sse
|
||||
SseEmitter emitter = AiragLocalCache.get(AiragConsts.CACHE_TYPE_SSE, requestId);
|
||||
if (null == emitter) {
|
||||
log.warn("[AI应用]接收LLM返回会话已关闭");
|
||||
return;
|
||||
}
|
||||
if (FinishReason.STOP.equals(finishReason) || null == finishReason) {
|
||||
// 正常结束
|
||||
EventData eventData = new EventData(requestId, null, EventData.EVENT_MESSAGE_END, chatConversation.getId(), topicId);
|
||||
appendMessage(messages, aiMessage, chatConversation, topicId);
|
||||
// 保存会话
|
||||
saveChatConversation(chatConversation, false, httpRequest);
|
||||
closeSSE(emitter, eventData);
|
||||
} else if (FinishReason.TOOL_EXECUTION.equals(finishReason)) {
|
||||
// 需要执行工具
|
||||
// TODO author: chenrui for: date:2025/3/7
|
||||
} else if (FinishReason.LENGTH.equals(finishReason)) {
|
||||
// 上下文长度超过限制
|
||||
log.error("调用模型异常:上下文长度超过限制:{}", responseMessage.tokenUsage());
|
||||
EventData eventData = new EventData(requestId, null, EventData.EVENT_MESSAGE, chatConversation.getId(), topicId);
|
||||
eventData.setData(EventMessageData.builder().message("\n上下文长度超过限制,请调整模型最大Tokens").build());
|
||||
sendMessage2Client(emitter, eventData);
|
||||
eventData = new EventData(requestId, null, EventData.EVENT_MESSAGE_END, chatConversation.getId(), topicId);
|
||||
closeSSE(emitter, eventData);
|
||||
} else {
|
||||
// 异常结束
|
||||
log.error("调用模型异常:" + respText);
|
||||
if (respText.contains("insufficient Balance")) {
|
||||
respText = "大预言模型账号余额不足!";
|
||||
}
|
||||
EventData eventData = new EventData(requestId, null, EventData.EVENT_FLOW_ERROR, chatConversation.getId(), topicId);
|
||||
eventData.setData(EventFlowData.builder().success(false).message(respText).build());
|
||||
closeSSE(emitter, eventData);
|
||||
}
|
||||
}).onError((Throwable error) -> {
|
||||
// 打印流程耗时日志
|
||||
printChatDuration(requestId, "LLM输出消息异常");
|
||||
AiragLocalCache.remove(AiragConsts.CACHE_TYPE_SSE_SEND_TIME, requestId);
|
||||
// sse
|
||||
SseEmitter emitter = AiragLocalCache.get(AiragConsts.CACHE_TYPE_SSE, requestId);
|
||||
if (null == emitter) {
|
||||
log.warn("[AI应用]接收LLM返回会话已关闭{}", requestId);
|
||||
return;
|
||||
}
|
||||
log.error(error.getMessage(), error);
|
||||
String errMsg = error.getMessage();
|
||||
if (errMsg != null && errMsg.contains("timeout")) {
|
||||
//update-begin---author:chenrui ---date:20250425 for:[QQYUN-12203]AI 聊天,超时或者服务器报错,给个友好提示------------
|
||||
errMsg = "当前用户较多,排队中,请稍后再试!";
|
||||
EventData eventData = new EventData(requestId, null, EventData.EVENT_MESSAGE, chatConversation.getId(), topicId);
|
||||
eventData.setData(EventMessageData.builder().message("\n" + errMsg).build());
|
||||
sendMessage2Client(emitter, eventData);
|
||||
eventData = new EventData(requestId, null, EventData.EVENT_MESSAGE_END, chatConversation.getId(), topicId);
|
||||
closeSSE(emitter, eventData);
|
||||
//update-end---author:chenrui ---date:20250425 for:[QQYUN-12203]AI 聊天,超时或者服务器报错,给个友好提示------------
|
||||
} else {
|
||||
errMsg = "调用大模型接口失败:" + errMsg;
|
||||
EventData eventData = new EventData(requestId, null, EventData.EVENT_FLOW_ERROR, chatConversation.getId(), topicId);
|
||||
eventData.setData(EventFlowData.builder().success(false).message(errMsg).build());
|
||||
closeSSE(emitter, eventData);
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送消息到客户端
|
||||
*
|
||||
* @param emitter
|
||||
* @param eventData
|
||||
* @author chenrui
|
||||
* @date 2025/4/22 19:58
|
||||
*/
|
||||
private static void sendMessage2Client(SseEmitter emitter, EventData eventData) {
|
||||
try {
|
||||
log.info("发送消息:{}", eventData.getRequestId());
|
||||
String eventStr = JSONObject.toJSONString(eventData);
|
||||
log.debug("[AI应用]接收LLM返回消息:{}", eventStr);
|
||||
emitter.send(SseEmitter.event().data(eventStr));
|
||||
} catch (IOException e) {
|
||||
log.error("发送消息失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -837,12 +904,7 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
||||
}
|
||||
CompletableFuture.runAsync(() -> {
|
||||
List<ChatMessage> messages = new LinkedList<>();
|
||||
String systemMsgStr = "根据用户的问题,总结会话标题.\n" +
|
||||
"要求如下:\n" +
|
||||
"1. 使用中文回答.\n" +
|
||||
"2. 标题长度控制在5个汉字10个英文字符以内\n" +
|
||||
"3. 直接回复会话标题,不要有其他任何无关描述\n" +
|
||||
"4. 如果无法总结,回复不知道\n";
|
||||
String systemMsgStr = "根据用户的问题,总结会话标题.\n" + "要求如下:\n" + "1. 使用中文回答.\n" + "2. 标题长度控制在5个汉字10个英文字符以内\n" + "3. 直接回复会话标题,不要有其他任何无关描述\n" + "4. 如果无法总结,回复不知道\n";
|
||||
messages.add(new SystemMessage(systemMsgStr));
|
||||
messages.add(new UserMessage(question));
|
||||
String summaryTitle;
|
||||
@ -876,6 +938,7 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
||||
|
||||
/**
|
||||
* 获取用户名
|
||||
*
|
||||
* @param httpRequest
|
||||
* @return
|
||||
* @author chenrui
|
||||
@ -885,9 +948,9 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
||||
try {
|
||||
TokenUtils.getTokenByRequest();
|
||||
String token;
|
||||
if(null != httpRequest){
|
||||
if (null != httpRequest) {
|
||||
token = TokenUtils.getTokenByRequest(httpRequest);
|
||||
}else{
|
||||
} else {
|
||||
token = TokenUtils.getTokenByRequest();
|
||||
}
|
||||
if (TokenUtils.verifyToken(token, sysBaseApi, redisUtil)) {
|
||||
@ -898,4 +961,19 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 打印耗时
|
||||
* @param requestId
|
||||
* @param message
|
||||
* @author chenrui
|
||||
* @date 2025/4/28 15:15
|
||||
*/
|
||||
private static void printChatDuration(String requestId,String message) {
|
||||
Long beginTime = AiragLocalCache.get(AiragConsts.CACHE_TYPE_SSE_SEND_TIME, requestId);
|
||||
if (null != beginTime) {
|
||||
log.info("[AI-CHAT]{},requestId:{},耗时:{}s", message, requestId, (System.currentTimeMillis() - beginTime) / 1000);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,83 @@
|
||||
package org.jeecg.modules.airag.demo;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jeecg.common.exception.JeecgBootBizTipException;
|
||||
import org.jeecg.modules.airag.flow.component.enhance.IAiRagEnhanceJava;
|
||||
import org.jeecgframework.poi.excel.ExcelImportUtil;
|
||||
import org.jeecgframework.poi.excel.entity.ImportParams;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Java增强Demo: Excel数据读取器
|
||||
* for [QQYUN-11718]【AI】积木报表对接AI流程编排接口展示报表
|
||||
* @Author: chenrui
|
||||
* @Date: 2025/4/29 16:51
|
||||
*/
|
||||
@Component("jimuDataReader")
|
||||
@Slf4j
|
||||
public class JimuDataReader implements IAiRagEnhanceJava {
|
||||
|
||||
|
||||
@Override
|
||||
public Map<String, Object> process(Map<String, Object> inputParams) {
|
||||
// inputParams: {"bizData":"/xxxx/xxxx/xxxx/xxxx.xls"}
|
||||
try {
|
||||
String filePath = (String) inputParams.get("bizData");
|
||||
if (filePath == null || filePath.isEmpty()) {
|
||||
throw new IllegalArgumentException("File path is empty");
|
||||
}
|
||||
|
||||
File excelFile = new File(filePath);
|
||||
if (!excelFile.exists() || !excelFile.isFile()) {
|
||||
throw new IllegalArgumentException("File not found: " + filePath);
|
||||
}
|
||||
|
||||
// Since we don't know the target entity class, we'll read the Excel generically
|
||||
return readExcelData(excelFile);
|
||||
} catch (Exception e) {
|
||||
log.error("Error processing Excel file", e);
|
||||
throw new JeecgBootBizTipException("调用java增强失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Excel导入工具方法,基于ExcelImportUtil
|
||||
*
|
||||
* @param file Excel文件
|
||||
* @return Excel读取结果,包含字段和数据
|
||||
* @throws Exception 导入过程中的异常
|
||||
*/
|
||||
public static Map<String, Object> readExcelData(File file) throws Exception {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
|
||||
// 设置导入参数
|
||||
ImportParams params = new ImportParams();
|
||||
params.setTitleRows(0); // 没有标题
|
||||
params.setHeadRows(1); // 第一行是表头
|
||||
|
||||
// 读取Excel数据
|
||||
List<Map<String, Object>> dataList = ExcelImportUtil.importExcel(file, Map.class, params);
|
||||
|
||||
// 如果没有数据,返回空结果
|
||||
if (dataList == null || dataList.isEmpty()) {
|
||||
result.put("fields", new ArrayList<>());
|
||||
result.put("datas", new ArrayList<>());
|
||||
return result;
|
||||
}
|
||||
|
||||
// 从第一行数据中获取字段名
|
||||
List<String> fieldNames = new ArrayList<>(dataList.get(0).keySet());
|
||||
|
||||
result.put("fields", fieldNames);
|
||||
result.put("datas", dataList);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,22 @@
|
||||
package org.jeecg.modules.airag.demo;
|
||||
|
||||
import org.jeecg.modules.airag.flow.component.enhance.IAiRagEnhanceJava;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @Description: Java增强节点示例类
|
||||
* @Author: chenrui
|
||||
* @Date: 2025/3/6 11:42
|
||||
*/
|
||||
@Component("testAiragEnhance")
|
||||
public class TestAiragEnhance implements IAiRagEnhanceJava {
|
||||
@Override
|
||||
public Map<String, Object> process(Map<String, Object> inputParams) {
|
||||
Object arg1 = inputParams.get("arg1");
|
||||
Object arg2 = inputParams.get("arg2");
|
||||
return Collections.singletonMap("result",arg1.toString()+"java拼接"+arg2.toString());
|
||||
}
|
||||
}
|
||||
@ -35,6 +35,11 @@ public class LLMConsts {
|
||||
*/
|
||||
public static final String MODEL_TYPE_LLM = "LLM";
|
||||
|
||||
/**
|
||||
* 向量模型:默认维度
|
||||
*/
|
||||
public static final Integer EMBED_MODEL_DEFAULT_DIMENSION = 1536;
|
||||
|
||||
/**
|
||||
* 知识库:文档状态:草稿
|
||||
*/
|
||||
@ -47,7 +52,10 @@ public class LLMConsts {
|
||||
* 知识库:文档状态:构建完成
|
||||
*/
|
||||
public static final String KNOWLEDGE_DOC_STATUS_COMPLETE = "complete";
|
||||
|
||||
/**
|
||||
* 知识库:文档状态:构建失败
|
||||
*/
|
||||
public static final String KNOWLEDGE_DOC_STATUS_FAILED = "failed";
|
||||
|
||||
/**
|
||||
* 知识库:文档类型:文本
|
||||
|
||||
@ -4,9 +4,12 @@ import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
||||
import org.jeecg.common.api.vo.Result;
|
||||
import org.jeecg.common.system.query.QueryGenerator;
|
||||
import org.jeecg.common.util.AssertUtils;
|
||||
import org.jeecg.common.util.TokenUtils;
|
||||
import org.jeecg.config.mybatis.MybatisPlusSaasConfig;
|
||||
import org.jeecg.modules.airag.common.vo.knowledge.KnowledgeSearchResult;
|
||||
import org.jeecg.modules.airag.llm.consts.LLMConsts;
|
||||
import org.jeecg.modules.airag.llm.entity.AiragKnowledge;
|
||||
@ -74,6 +77,7 @@ public class AiragKnowledgeController {
|
||||
* @date 2025/2/18 17:09
|
||||
*/
|
||||
@PostMapping(value = "/add")
|
||||
@RequiresPermissions("airag:knowledge:add")
|
||||
public Result<String> add(@RequestBody AiragKnowledge airagKnowledge) {
|
||||
airagKnowledge.setStatus(LLMConsts.STATUS_ENABLE);
|
||||
airagKnowledgeService.save(airagKnowledge);
|
||||
@ -90,6 +94,7 @@ public class AiragKnowledgeController {
|
||||
*/
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@RequestMapping(value = "/edit", method = {RequestMethod.PUT, RequestMethod.POST})
|
||||
@RequiresPermissions("airag:knowledge:edit")
|
||||
public Result<String> edit(@RequestBody AiragKnowledge airagKnowledge) {
|
||||
AiragKnowledge airagKnowledgeEntity = airagKnowledgeService.getById(airagKnowledge.getId());
|
||||
if (airagKnowledgeEntity == null) {
|
||||
@ -113,6 +118,7 @@ public class AiragKnowledgeController {
|
||||
* @date 2025/3/12 17:05
|
||||
*/
|
||||
@PutMapping(value = "/rebuild")
|
||||
@RequiresPermissions("airag:knowledge:rebuild")
|
||||
public Result<?> rebuild(@RequestParam("knowIds") String knowIds) {
|
||||
String[] knowIdArr = knowIds.split(",");
|
||||
for (String knowId : knowIdArr) {
|
||||
@ -131,29 +137,24 @@ public class AiragKnowledgeController {
|
||||
*/
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@DeleteMapping(value = "/delete")
|
||||
public Result<String> delete(@RequestParam(name = "id", required = true) String id) {
|
||||
@RequiresPermissions("airag:knowledge:delete")
|
||||
public Result<String> delete(HttpServletRequest request, @RequestParam(name = "id", required = true) String id) {
|
||||
//update-begin---author:chenrui ---date:20250606 for:[issues/8337]关于ai工作列表的数据权限问题 #8337------------
|
||||
//如果是saas隔离的情况下,判断当前租户id是否是当前租户下的
|
||||
if (MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL) {
|
||||
AiragKnowledge know = airagKnowledgeService.getById(id);
|
||||
//获取当前租户
|
||||
String currentTenantId = TokenUtils.getTenantIdByRequest(request);
|
||||
if (null == know || !know.getTenantId().equals(currentTenantId)) {
|
||||
return Result.error("删除AI知识库失败,不能删除其他租户的AI知识库!");
|
||||
}
|
||||
}
|
||||
//update-end---author:chenrui ---date:20250606 for:[issues/8337]关于ai工作列表的数据权限问题 #8337------------
|
||||
airagKnowledgeDocService.removeByKnowIds(Collections.singletonList(id));
|
||||
airagKnowledgeService.removeById(id);
|
||||
return Result.OK("删除成功!");
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除知识库
|
||||
*
|
||||
* @param ids
|
||||
* @return
|
||||
* @author chenrui
|
||||
* @date 2025/2/18 17:09
|
||||
*/
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@DeleteMapping(value = "/deleteBatch")
|
||||
public Result<String> deleteBatch(@RequestParam(name = "ids", required = true) String ids) {
|
||||
List<String> idsList = Arrays.asList(ids.split(","));
|
||||
airagKnowledgeDocService.removeByKnowIds(idsList);
|
||||
airagKnowledgeService.removeByIds(idsList);
|
||||
return Result.OK("批量删除成功!");
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过id查询知识库
|
||||
*
|
||||
@ -203,6 +204,7 @@ public class AiragKnowledgeController {
|
||||
* @date 2025/2/18 15:47
|
||||
*/
|
||||
@PostMapping(value = "/doc/edit")
|
||||
@RequiresPermissions("airag:knowledge:doc:edit")
|
||||
public Result<?> addDocument(@RequestBody AiragKnowledgeDoc airagKnowledgeDoc) {
|
||||
return airagKnowledgeDocService.editDocument(airagKnowledgeDoc);
|
||||
}
|
||||
@ -215,6 +217,7 @@ public class AiragKnowledgeController {
|
||||
* @date 2025/3/20 11:29
|
||||
*/
|
||||
@PostMapping(value = "/doc/import/zip")
|
||||
@RequiresPermissions("airag:knowledge:doc:zip")
|
||||
public Result<?> importDocumentFromZip(@RequestParam(name = "knowId", required = true) String knowId,
|
||||
@RequestParam(name = "file", required = true) MultipartFile file) {
|
||||
return airagKnowledgeDocService.importDocumentFromZip(knowId,file);
|
||||
@ -241,6 +244,7 @@ public class AiragKnowledgeController {
|
||||
* @date 2025/2/18 15:47
|
||||
*/
|
||||
@PutMapping(value = "/doc/rebuild")
|
||||
@RequiresPermissions("airag:knowledge:doc:rebuild")
|
||||
public Result<?> rebuildDocument(@RequestParam("docIds") String docIds) {
|
||||
return airagKnowledgeDocService.rebuildDocument(docIds);
|
||||
}
|
||||
@ -255,12 +259,49 @@ public class AiragKnowledgeController {
|
||||
*/
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@DeleteMapping(value = "/doc/deleteBatch")
|
||||
public Result<String> deleteDocumentBatch(@RequestParam(name = "ids", required = true) String ids) {
|
||||
@RequiresPermissions("airag:knowledge:doc:deleteBatch")
|
||||
public Result<String> deleteDocumentBatch(HttpServletRequest request, @RequestParam(name = "ids", required = true) String ids) {
|
||||
List<String> idsList = Arrays.asList(ids.split(","));
|
||||
//update-begin---author:chenrui ---date:20250606 for:[issues/8337]关于ai工作列表的数据权限问题 #8337------------
|
||||
//如果是saas隔离的情况下,判断当前租户id是否是当前租户下的
|
||||
if (MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL) {
|
||||
List<AiragKnowledgeDoc> docList = airagKnowledgeDocService.listByIds(idsList);
|
||||
//获取当前租户
|
||||
String currentTenantId = TokenUtils.getTenantIdByRequest(request);
|
||||
docList.forEach(airagKnowledgeDoc -> {
|
||||
if (null == airagKnowledgeDoc || !airagKnowledgeDoc.getTenantId().equals(currentTenantId)) {
|
||||
throw new IllegalArgumentException("删除AI知识库文档失败,不能删除其他租户的AI知识库文档!");
|
||||
}
|
||||
});
|
||||
}
|
||||
//update-end---author:chenrui ---date:20250606 for:[issues/8337]关于ai工作列表的数据权限问题 #8337------------
|
||||
airagKnowledgeDocService.removeDocByIds(idsList);
|
||||
return Result.OK("批量删除成功!");
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空知识库文档
|
||||
*
|
||||
* @param
|
||||
* @return
|
||||
*/
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@DeleteMapping(value = "/doc/deleteAll")
|
||||
@RequiresPermissions("airag:knowledge:doc:deleteAll")
|
||||
public Result<?> deleteDocumentAll(HttpServletRequest request, @RequestParam(name = "knowId") String knowId) {
|
||||
//update-begin---author:chenrui ---date:20250606 for:[issues/8337]关于ai工作列表的数据权限问题 #8337------------
|
||||
//如果是saas隔离的情况下,判断当前租户id是否是当前租户下的
|
||||
if (MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL) {
|
||||
AiragKnowledge know = airagKnowledgeService.getById(knowId);
|
||||
//获取当前租户
|
||||
String currentTenantId = TokenUtils.getTenantIdByRequest(request);
|
||||
if (null == know || !know.getTenantId().equals(currentTenantId)) {
|
||||
return Result.error("删除AI知识库失败,不能删除其他租户的AI知识库!");
|
||||
}
|
||||
}
|
||||
//update-end---author:chenrui ---date:20250606 for:[issues/8337]关于ai工作列表的数据权限问题 #8337------------
|
||||
return airagKnowledgeDocService.deleteAllByKnowId(knowId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 命中测试
|
||||
|
||||
@ -3,12 +3,23 @@ package org.jeecg.modules.airag.llm.controller;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import dev.langchain4j.data.message.UserMessage;
|
||||
import dev.langchain4j.model.embedding.EmbeddingModel;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
||||
import org.jeecg.ai.factory.AiModelFactory;
|
||||
import org.jeecg.ai.factory.AiModelOptions;
|
||||
import org.jeecg.common.api.vo.Result;
|
||||
import org.jeecg.common.system.base.controller.JeecgController;
|
||||
import org.jeecg.common.system.query.QueryGenerator;
|
||||
import org.jeecg.common.util.AssertUtils;
|
||||
import org.jeecg.common.util.TokenUtils;
|
||||
import org.jeecg.config.mybatis.MybatisPlusSaasConfig;
|
||||
import org.jeecg.modules.airag.llm.consts.LLMConsts;
|
||||
import org.jeecg.modules.airag.llm.entity.AiragModel;
|
||||
import org.jeecg.modules.airag.llm.handler.AIChatHandler;
|
||||
import org.jeecg.modules.airag.llm.handler.EmbeddingHandler;
|
||||
import org.jeecg.modules.airag.llm.service.IAiragModelService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
@ -17,6 +28,7 @@ import org.springframework.web.servlet.ModelAndView;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
|
||||
/**
|
||||
* @Description: AiRag模型配置
|
||||
@ -32,6 +44,8 @@ public class AiragModelController extends JeecgController<AiragModel, IAiragMode
|
||||
@Autowired
|
||||
private IAiragModelService airagModelService;
|
||||
|
||||
@Autowired
|
||||
AIChatHandler aiChatHandler;
|
||||
|
||||
/**
|
||||
* 分页列表查询
|
||||
@ -57,7 +71,12 @@ public class AiragModelController extends JeecgController<AiragModel, IAiragMode
|
||||
* @return
|
||||
*/
|
||||
@PostMapping(value = "/add")
|
||||
@RequiresPermissions("airag:model:add")
|
||||
public Result<String> add(@RequestBody AiragModel airagModel) {
|
||||
// 验证 模型名称/模型类型/基础模型
|
||||
AssertUtils.assertNotEmpty("模型名称不能为空", airagModel.getName());
|
||||
AssertUtils.assertNotEmpty("模型类型不能为空", airagModel.getModelType());
|
||||
AssertUtils.assertNotEmpty("基础模型不能为空", airagModel.getModelName());
|
||||
airagModelService.save(airagModel);
|
||||
return Result.OK("添加成功!");
|
||||
}
|
||||
@ -69,6 +88,7 @@ public class AiragModelController extends JeecgController<AiragModel, IAiragMode
|
||||
* @return
|
||||
*/
|
||||
@RequestMapping(value = "/edit", method = {RequestMethod.PUT, RequestMethod.POST})
|
||||
@RequiresPermissions("airag:model:edit")
|
||||
public Result<String> edit(@RequestBody AiragModel airagModel) {
|
||||
airagModelService.updateById(airagModel);
|
||||
return Result.OK("编辑成功!");
|
||||
@ -81,23 +101,23 @@ public class AiragModelController extends JeecgController<AiragModel, IAiragMode
|
||||
* @return
|
||||
*/
|
||||
@DeleteMapping(value = "/delete")
|
||||
public Result<String> delete(@RequestParam(name = "id", required = true) String id) {
|
||||
@RequiresPermissions("airag:model:delete")
|
||||
public Result<String> delete(HttpServletRequest request, @RequestParam(name = "id", required = true) String id) {
|
||||
//update-begin---author:chenrui ---date:20250606 for:[issues/8337]关于ai工作列表的数据权限问题 #8337------------
|
||||
//如果是saas隔离的情况下,判断当前租户id是否是当前租户下的
|
||||
if (MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL) {
|
||||
AiragModel model = airagModelService.getById(id);
|
||||
//获取当前租户
|
||||
String currentTenantId = TokenUtils.getTenantIdByRequest(request);
|
||||
if (null == model || !model.getTenantId().equals(currentTenantId)) {
|
||||
return Result.error("删除AI模型失败,不能删除其他租户的AI模型!");
|
||||
}
|
||||
}
|
||||
//update-end---author:chenrui ---date:20250606 for:[issues/8337]关于ai工作列表的数据权限问题 #8337------------
|
||||
airagModelService.removeById(id);
|
||||
return Result.OK("删除成功!");
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除
|
||||
*
|
||||
* @param ids
|
||||
* @return
|
||||
*/
|
||||
@DeleteMapping(value = "/deleteBatch")
|
||||
public Result<String> deleteBatch(@RequestParam(name = "ids", required = true) String ids) {
|
||||
this.airagModelService.removeByIds(Arrays.asList(ids.split(",")));
|
||||
return Result.OK("批量删除成功!");
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过id查询
|
||||
*
|
||||
@ -136,4 +156,25 @@ public class AiragModelController extends JeecgController<AiragModel, IAiragMode
|
||||
return super.importExcel(request, response, AiragModel.class);
|
||||
}
|
||||
|
||||
@PostMapping(value = "/test")
|
||||
public Result<?> test(@RequestBody AiragModel airagModel) {
|
||||
// 验证 模型名称/模型类型/基础模型
|
||||
AssertUtils.assertNotEmpty("模型名称不能为空", airagModel.getName());
|
||||
AssertUtils.assertNotEmpty("模型类型不能为空", airagModel.getModelType());
|
||||
AssertUtils.assertNotEmpty("基础模型不能为空", airagModel.getModelName());
|
||||
try {
|
||||
if(LLMConsts.MODEL_TYPE_LLM.equals(airagModel.getModelType())){
|
||||
aiChatHandler.completions(airagModel, Collections.singletonList(UserMessage.from("test connection")), null);
|
||||
}else{
|
||||
AiModelOptions aiModelOptions = EmbeddingHandler.buildModelOptions(airagModel);
|
||||
EmbeddingModel embeddingModel = AiModelFactory.createEmbeddingModel(aiModelOptions);
|
||||
embeddingModel.embed("test text");
|
||||
}
|
||||
}catch (Exception e){
|
||||
log.error("测试模型连接失败", e);
|
||||
return Result.error("测试模型连接失败" + e.getMessage());
|
||||
}
|
||||
return Result.OK("测试模型连接成功");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -30,13 +30,14 @@ public class AiragKnowledge implements Serializable {
|
||||
*/
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
@Schema(description = "主键")
|
||||
private String id;
|
||||
private java.lang.String id;
|
||||
|
||||
/**
|
||||
* 创建人
|
||||
*/
|
||||
@Schema(description = "创建人")
|
||||
private String createBy;
|
||||
@Dict(dictTable = "sys_user",dicCode = "username",dicText = "realname")
|
||||
private java.lang.String createBy;
|
||||
|
||||
/**
|
||||
* 创建日期
|
||||
@ -50,7 +51,7 @@ public class AiragKnowledge implements Serializable {
|
||||
* 更新人
|
||||
*/
|
||||
@Schema(description = "更新人")
|
||||
private String updateBy;
|
||||
private java.lang.String updateBy;
|
||||
|
||||
/**
|
||||
* 更新日期
|
||||
@ -64,21 +65,21 @@ public class AiragKnowledge implements Serializable {
|
||||
* 所属部门
|
||||
*/
|
||||
@Schema(description = "所属部门")
|
||||
private String sysOrgCode;
|
||||
private java.lang.String sysOrgCode;
|
||||
|
||||
/**
|
||||
* 租户id
|
||||
*/
|
||||
@Excel(name = "租户id", width = 15)
|
||||
@Schema(description = "租户id")
|
||||
private String tenantId;
|
||||
private java.lang.String tenantId;
|
||||
|
||||
/**
|
||||
* 知识库名称
|
||||
*/
|
||||
@Excel(name = "知识库名称", width = 15)
|
||||
@Schema(description = "知识库名称")
|
||||
private String name;
|
||||
private java.lang.String name;
|
||||
|
||||
/**
|
||||
* 向量模型id
|
||||
@ -86,19 +87,19 @@ public class AiragKnowledge implements Serializable {
|
||||
@Excel(name = "向量模型id", width = 15, dictTable = "airag_model where model_type = 'EMBED'", dicText = "name", dicCode = "id")
|
||||
@Dict(dictTable = "airag_model where model_type = 'EMBED'", dicText = "name", dicCode = "id")
|
||||
@Schema(description = "向量模型id")
|
||||
private String embedId;
|
||||
private java.lang.String embedId;
|
||||
|
||||
/**
|
||||
* 描述
|
||||
*/
|
||||
@Excel(name = "描述", width = 15)
|
||||
@Schema(description = "描述")
|
||||
private String descr;
|
||||
private java.lang.String descr;
|
||||
|
||||
/**
|
||||
* 状态
|
||||
*/
|
||||
@Excel(name = "状态", width = 15)
|
||||
@Schema(description = "状态")
|
||||
private String status;
|
||||
private java.lang.String status;
|
||||
}
|
||||
|
||||
@ -3,6 +3,7 @@ package org.jeecg.modules.airag.llm.entity;
|
||||
import java.io.Serializable;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import org.jeecg.common.aspect.annotation.Dict;
|
||||
import org.jeecg.common.constant.ProvinceCityArea;
|
||||
import org.jeecg.common.util.SpringContextUtils;
|
||||
import lombok.Data;
|
||||
@ -40,6 +41,7 @@ public class AiragKnowledgeDoc implements Serializable {
|
||||
* 创建人
|
||||
*/
|
||||
@Schema(description = "创建人")
|
||||
@Dict(dictTable = "sys_user",dicCode = "username",dicText = "realname")
|
||||
private String createBy;
|
||||
|
||||
/**
|
||||
@ -119,9 +121,4 @@ public class AiragKnowledgeDoc implements Serializable {
|
||||
@Schema(description = "状态")
|
||||
private String status;
|
||||
|
||||
/**
|
||||
* 服务器基础路径
|
||||
*/
|
||||
@TableField(exist = false)
|
||||
private String baseUrl;
|
||||
}
|
||||
|
||||
@ -45,6 +45,7 @@ public class AiragModel implements Serializable {
|
||||
* 创建人
|
||||
*/
|
||||
@Schema(description = "创建人")
|
||||
@Dict(dictTable = "sys_user",dicCode = "username",dicText = "realname")
|
||||
private String createBy;
|
||||
/**
|
||||
* 创建日期
|
||||
|
||||
@ -12,7 +12,7 @@ import org.jeecg.modules.airag.common.handler.AIChatParams;
|
||||
import org.jeecg.modules.airag.common.handler.IAIChatHandler;
|
||||
import org.jeecg.modules.airag.llm.consts.LLMConsts;
|
||||
import org.jeecg.modules.airag.llm.entity.AiragModel;
|
||||
import org.jeecg.modules.airag.llm.service.IAiragModelService;
|
||||
import org.jeecg.modules.airag.llm.mapper.AiragModelMapper;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
@ -38,7 +38,7 @@ import java.util.regex.Matcher;
|
||||
public class AIChatHandler implements IAIChatHandler {
|
||||
|
||||
@Autowired
|
||||
IAiragModelService airagModelService;
|
||||
AiragModelMapper airagModelMapper;
|
||||
|
||||
@Autowired
|
||||
EmbeddingHandler embeddingHandler;
|
||||
@ -82,7 +82,7 @@ public class AIChatHandler implements IAIChatHandler {
|
||||
AssertUtils.assertNotEmpty("至少发送一条消息", messages);
|
||||
AssertUtils.assertNotEmpty("请选择模型", modelId);
|
||||
|
||||
AiragModel airagModel = airagModelService.getById(modelId);
|
||||
AiragModel airagModel = airagModelMapper.getByIdIgnoreTenant(modelId);
|
||||
return completions(airagModel, messages, params);
|
||||
}
|
||||
|
||||
@ -96,7 +96,7 @@ public class AIChatHandler implements IAIChatHandler {
|
||||
* @author chenrui
|
||||
* @date 2025/2/24 17:30
|
||||
*/
|
||||
private String completions(AiragModel airagModel, List<ChatMessage> messages, AIChatParams params) {
|
||||
public String completions(AiragModel airagModel, List<ChatMessage> messages, AIChatParams params) {
|
||||
params = mergeParams(airagModel, params);
|
||||
String resp = llmHandler.completions(messages, params);
|
||||
if (resp.contains("</think>")
|
||||
@ -150,7 +150,7 @@ public class AIChatHandler implements IAIChatHandler {
|
||||
AssertUtils.assertNotEmpty("至少发送一条消息", messages);
|
||||
AssertUtils.assertNotEmpty("请选择模型", modelId);
|
||||
|
||||
AiragModel airagModel = airagModelService.getById(modelId);
|
||||
AiragModel airagModel = airagModelMapper.getByIdIgnoreTenant(modelId);
|
||||
return chat(airagModel, messages, params);
|
||||
}
|
||||
|
||||
@ -226,7 +226,7 @@ public class AIChatHandler implements IAIChatHandler {
|
||||
params.setMaxTokens(modelParams.getInteger("maxTokens"));
|
||||
}
|
||||
if (oConvertUtils.isObjectEmpty(params.getTimeout())) {
|
||||
params.setMaxTokens(modelParams.getInteger("timeout"));
|
||||
params.setTimeout(modelParams.getInteger("timeout"));
|
||||
}
|
||||
}
|
||||
|
||||
@ -237,6 +237,16 @@ public class AIChatHandler implements IAIChatHandler {
|
||||
params.setQueryRouter(queryRouter);
|
||||
}
|
||||
|
||||
// 设置确保maxTokens值正确
|
||||
if (oConvertUtils.isObjectNotEmpty(params.getMaxTokens()) && params.getMaxTokens() <= 0) {
|
||||
params.setMaxTokens(null);
|
||||
}
|
||||
|
||||
// 默认超时时间
|
||||
if(oConvertUtils.isObjectEmpty(params.getTimeout())){
|
||||
params.setTimeout(60);
|
||||
}
|
||||
|
||||
return params;
|
||||
}
|
||||
|
||||
|
||||
@ -35,8 +35,9 @@ import org.jeecg.modules.airag.llm.document.TikaDocumentParser;
|
||||
import org.jeecg.modules.airag.llm.entity.AiragKnowledge;
|
||||
import org.jeecg.modules.airag.llm.entity.AiragKnowledgeDoc;
|
||||
import org.jeecg.modules.airag.llm.entity.AiragModel;
|
||||
import org.jeecg.modules.airag.llm.mapper.AiragKnowledgeMapper;
|
||||
import org.jeecg.modules.airag.llm.mapper.AiragModelMapper;
|
||||
import org.jeecg.modules.airag.llm.service.IAiragKnowledgeService;
|
||||
import org.jeecg.modules.airag.llm.service.IAiragModelService;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
@ -69,13 +70,15 @@ public class EmbeddingHandler implements IEmbeddingHandler {
|
||||
EmbedStoreConfigBean embedStoreConfigBean;
|
||||
|
||||
@Autowired
|
||||
@Lazy
|
||||
private IAiragModelService airagModelService;
|
||||
private AiragModelMapper airagModelMapper;
|
||||
|
||||
@Autowired
|
||||
@Lazy
|
||||
private IAiragKnowledgeService airagKnowledgeService;
|
||||
|
||||
@Autowired
|
||||
private AiragKnowledgeMapper airagKnowledgeMapper;
|
||||
|
||||
@Value(value = "${jeecg.path.upload:}")
|
||||
private String uploadpath;
|
||||
|
||||
@ -112,6 +115,13 @@ public class EmbeddingHandler implements IEmbeddingHandler {
|
||||
*/
|
||||
private static final ConcurrentHashMap<String, EmbeddingStore<TextSegment>> EMBED_STORE_CACHE = new ConcurrentHashMap<>();
|
||||
|
||||
|
||||
/**
|
||||
* 正则匹配: md图片
|
||||
* "!\\[(.*?)]\\((.*?)(\\s*=\\d+)?\\)"
|
||||
*/
|
||||
private static final Pattern PATTERN_MD_IMAGE = Pattern.compile("!\\[(.*?)]\\((.*?)\\)");
|
||||
|
||||
/**
|
||||
* 向量化文档
|
||||
*
|
||||
@ -183,6 +193,7 @@ public class EmbeddingHandler implements IEmbeddingHandler {
|
||||
* @author chenrui
|
||||
* @date 2025/2/18 16:52
|
||||
*/
|
||||
@Override
|
||||
public KnowledgeSearchResult embeddingSearch(List<String> knowIds, String queryText, Integer topNumber, Double similarity) {
|
||||
AssertUtils.assertNotEmpty("请选择知识库", knowIds);
|
||||
AssertUtils.assertNotEmpty("请填写查询内容", queryText);
|
||||
@ -223,7 +234,7 @@ public class EmbeddingHandler implements IEmbeddingHandler {
|
||||
*/
|
||||
public List<Map<String, Object>> searchEmbedding(String knowId, String queryText, Integer topNumber, Double similarity) {
|
||||
AssertUtils.assertNotEmpty("请选择知识库", knowId);
|
||||
AiragKnowledge knowledge = airagKnowledgeService.getById(knowId);
|
||||
AiragKnowledge knowledge = airagKnowledgeMapper.getByIdIgnoreTenant(knowId);
|
||||
AssertUtils.assertNotEmpty("知识库不存在", knowledge);
|
||||
AssertUtils.assertNotEmpty("请填写查询内容", queryText);
|
||||
AiragModel model = getEmbedModelData(knowledge.getEmbedId());
|
||||
@ -268,6 +279,7 @@ public class EmbeddingHandler implements IEmbeddingHandler {
|
||||
* @author chenrui
|
||||
* @date 2025/2/20 21:03
|
||||
*/
|
||||
@Override
|
||||
public QueryRouter getQueryRouter(List<String> knowIds, Integer topNumber, Double similarity) {
|
||||
AssertUtils.assertNotEmpty("请选择知识库", knowIds);
|
||||
List<ContentRetriever> retrievers = Lists.newArrayList();
|
||||
@ -275,7 +287,7 @@ public class EmbeddingHandler implements IEmbeddingHandler {
|
||||
if (oConvertUtils.isEmpty(knowId)) {
|
||||
continue;
|
||||
}
|
||||
AiragKnowledge knowledge = airagKnowledgeService.getById(knowId);
|
||||
AiragKnowledge knowledge = airagKnowledgeMapper.getByIdIgnoreTenant(knowId);
|
||||
AssertUtils.assertNotEmpty("知识库不存在", knowledge);
|
||||
AiragModel model = getEmbedModelData(knowledge.getEmbedId());
|
||||
AiModelOptions modelOptions = buildModelOptions(model);
|
||||
@ -345,7 +357,7 @@ public class EmbeddingHandler implements IEmbeddingHandler {
|
||||
*/
|
||||
private AiragModel getEmbedModelData(String modelId) {
|
||||
AssertUtils.assertNotEmpty("向量模型不能为空", modelId);
|
||||
AiragModel model = airagModelService.getById(modelId);
|
||||
AiragModel model = airagModelMapper.getByIdIgnoreTenant(modelId);
|
||||
AssertUtils.assertNotEmpty("向量模型不存在", model);
|
||||
AssertUtils.assertEquals("仅支持向量模型", LLMConsts.MODEL_TYPE_EMBED, model.getModelType());
|
||||
return model;
|
||||
@ -371,6 +383,18 @@ public class EmbeddingHandler implements IEmbeddingHandler {
|
||||
|
||||
AiModelOptions modelOp = buildModelOptions(model);
|
||||
EmbeddingModel embeddingModel = AiModelFactory.createEmbeddingModel(modelOp);
|
||||
|
||||
String tableName = embedStoreConfigBean.getTable();
|
||||
|
||||
// update-begin---author:sunjianlei ---date:20250509 for:【QQYUN-12345】向量模型维度不一致问题
|
||||
// 如果该模型不是默认的向量维度
|
||||
int dimension = embeddingModel.dimension();
|
||||
if (!LLMConsts.EMBED_MODEL_DEFAULT_DIMENSION.equals(dimension)) {
|
||||
// 就加上维度后缀,防止因维度不一致导致保存失败
|
||||
tableName += ("_" + dimension);
|
||||
}
|
||||
// update-end-----author:sunjianlei ---date:20250509 for:【QQYUN-12345】向量模型维度不一致问题
|
||||
|
||||
EmbeddingStore<TextSegment> embeddingStore = PgVectorEmbeddingStore.builder()
|
||||
// Connection and table parameters
|
||||
.host(embedStoreConfigBean.getHost())
|
||||
@ -378,7 +402,7 @@ public class EmbeddingHandler implements IEmbeddingHandler {
|
||||
.database(embedStoreConfigBean.getDatabase())
|
||||
.user(embedStoreConfigBean.getUser())
|
||||
.password(embedStoreConfigBean.getPassword())
|
||||
.table(embedStoreConfigBean.getTable())
|
||||
.table(tableName)
|
||||
// Embedding dimension
|
||||
// Required: Must match the embedding model’s output dimension
|
||||
.dimension(embeddingModel.dimension())
|
||||
@ -449,16 +473,20 @@ public class EmbeddingHandler implements IEmbeddingHandler {
|
||||
String fileType = FilenameUtils.getExtension(docFile.getName());
|
||||
if ("md".contains(fileType)) {
|
||||
// 如果是md文件,查找所有图片语法,如果是本地图片,替换成网络图片
|
||||
String baseUrl = doc.getBaseUrl() + "/sys/common/static/";
|
||||
String baseUrl = "#{domainURL}/sys/common/static/";
|
||||
String sourcePath = metadataJson.getString(LLMConsts.KNOWLEDGE_DOC_METADATA_SOURCES_PATH);
|
||||
if(oConvertUtils.isNotEmpty(sourcePath)) {
|
||||
String escapedPath = uploadpath;
|
||||
if (File.separator.equals("\\")){
|
||||
//update-begin---author:wangshuai---date:2025-06-03---for:【QQYUN-12636】【AI知识库】文档库上传 本地local 文档中的图片不展示---
|
||||
/*if (File.separator.equals("\\")){
|
||||
escapedPath = uploadpath.replace("//", "\\\\");
|
||||
}
|
||||
}*/
|
||||
//update-end---author:wangshuai---date:2025-06-03---for:【QQYUN-12636】【AI知识库】文档库上传 本地local 文档中的图片不展示---
|
||||
sourcePath = sourcePath.replaceFirst("^" + escapedPath, "").replace("\\", "/");
|
||||
baseUrl = baseUrl + sourcePath + "/";
|
||||
StringBuffer sb = replaceImageUrl(content, baseUrl);
|
||||
String docFilePath = metadataJson.getString(LLMConsts.KNOWLEDGE_DOC_METADATA_FILEPATH);
|
||||
docFilePath = FilenameUtils.getPath(docFilePath);
|
||||
docFilePath = docFilePath.replace("\\", "/");
|
||||
StringBuffer sb = replaceImageUrl(content, baseUrl + sourcePath + "/", baseUrl + docFilePath);
|
||||
content = sb.toString();
|
||||
}
|
||||
}
|
||||
@ -469,11 +497,9 @@ public class EmbeddingHandler implements IEmbeddingHandler {
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static StringBuffer replaceImageUrl(String content, String baseUrl) {
|
||||
private static StringBuffer replaceImageUrl(String content, String abstractBaseUrl, String relativeBaseUrl) {
|
||||
// 正则表达式匹配md文件中的图片语法 
|
||||
String mdImagePattern = "!\\[(.*?)]\\((.*?)(\\s*=\\d+)?\\)";
|
||||
Pattern pattern = Pattern.compile(mdImagePattern);
|
||||
Matcher matcher = pattern.matcher(content);
|
||||
Matcher matcher = PATTERN_MD_IMAGE.matcher(content);
|
||||
|
||||
StringBuffer sb = new StringBuffer();
|
||||
while (matcher.find()) {
|
||||
@ -481,7 +507,16 @@ public class EmbeddingHandler implements IEmbeddingHandler {
|
||||
// 检查是否是本地图片路径
|
||||
if (!imageUrl.startsWith("http")) {
|
||||
// 替换成网络图片路径
|
||||
String networkImageUrl = baseUrl + imageUrl;
|
||||
String networkImageUrl = abstractBaseUrl + imageUrl;
|
||||
if(imageUrl.startsWith("/")) {
|
||||
// 绝对路径
|
||||
networkImageUrl = abstractBaseUrl + imageUrl;
|
||||
}else{
|
||||
// 相对路径
|
||||
networkImageUrl = relativeBaseUrl + imageUrl;
|
||||
}
|
||||
// 修改图片路径中//->/,但保留http://和https://
|
||||
networkImageUrl = networkImageUrl.replaceAll("(?<!http:)(?<!https:)//", "/");
|
||||
matcher.appendReplacement(sb, "");
|
||||
} else {
|
||||
matcher.appendReplacement(sb, "");
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
package org.jeecg.modules.airag.llm.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.InterceptorIgnore;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.jeecg.modules.airag.llm.entity.AiragKnowledge;
|
||||
|
||||
@ -11,4 +12,14 @@ import org.jeecg.modules.airag.llm.entity.AiragKnowledge;
|
||||
*/
|
||||
public interface AiragKnowledgeMapper extends BaseMapper<AiragKnowledge> {
|
||||
|
||||
/**
|
||||
* 根据ID查询知识库信息(忽略租户)
|
||||
* for [QQYUN-12113]分享之后的聊天,应用、模型、知识库不根据租户查询
|
||||
* @param id
|
||||
* @return
|
||||
* @author chenrui
|
||||
* @date 2025/4/21 15:24
|
||||
*/
|
||||
@InterceptorIgnore(tenantLine = "true")
|
||||
AiragKnowledge getByIdIgnoreTenant(String id);
|
||||
}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
package org.jeecg.modules.airag.llm.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.InterceptorIgnore;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.jeecg.modules.airag.llm.entity.AiragModel;
|
||||
|
||||
@ -11,4 +12,14 @@ import org.jeecg.modules.airag.llm.entity.AiragModel;
|
||||
*/
|
||||
public interface AiragModelMapper extends BaseMapper<AiragModel> {
|
||||
|
||||
/**
|
||||
* 根据ID查询模型信息(忽略租户)
|
||||
* for [QQYUN-12113]分享之后的聊天,应用、模型、知识库不根据租户查询
|
||||
* @param id
|
||||
* @return
|
||||
* @author chenrui
|
||||
* @date 2025/4/21 15:24
|
||||
*/
|
||||
@InterceptorIgnore(tenantLine = "true")
|
||||
AiragModel getByIdIgnoreTenant(String id);
|
||||
}
|
||||
|
||||
@ -2,4 +2,8 @@
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="org.jeecg.modules.airag.llm.mapper.AiragKnowledgeMapper">
|
||||
|
||||
<select id="getByIdIgnoreTenant" resultType="org.jeecg.modules.airag.llm.entity.AiragKnowledge">
|
||||
SELECT * FROM airag_knowledge WHERE id = #{id}
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
@ -2,4 +2,8 @@
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="org.jeecg.modules.airag.llm.mapper.AiragModelMapper">
|
||||
|
||||
<select id="getByIdIgnoreTenant" resultType="org.jeecg.modules.airag.llm.entity.AiragModel">
|
||||
SELECT * FROM airag_model WHERE id = #{id}
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
@ -67,6 +67,14 @@ public interface IAiragKnowledgeDocService extends IService<AiragKnowledgeDoc> {
|
||||
*/
|
||||
Result<?> removeDocByIds(List<String> docIds);
|
||||
|
||||
/**
|
||||
* 通过知识库id删除所以文档
|
||||
*
|
||||
* @param knowId
|
||||
* @return
|
||||
*/
|
||||
Result<?> deleteAllByKnowId(String knowId);
|
||||
|
||||
/**
|
||||
* 从zip包导入文档
|
||||
* @param knowId
|
||||
|
||||
@ -1,9 +1,12 @@
|
||||
package org.jeecg.modules.airag.llm.service.impl;
|
||||
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
|
||||
import org.apache.commons.compress.archivers.zip.ZipFile;
|
||||
import org.apache.commons.io.FilenameUtils;
|
||||
import org.jeecg.common.api.vo.Result;
|
||||
import org.jeecg.common.config.TenantContext;
|
||||
@ -26,9 +29,12 @@ import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
@ -36,8 +42,6 @@ import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipFile;
|
||||
|
||||
import static org.jeecg.modules.airag.llm.consts.LLMConsts.*;
|
||||
|
||||
@ -79,6 +83,12 @@ public class AiragKnowledgeDocServiceImpl extends ServiceImpl<AiragKnowledgeDocM
|
||||
*/
|
||||
private static final ExecutorService buildDocExecutorService = Executors.newFixedThreadPool(THREAD_POOL_SIZE);
|
||||
|
||||
// 解压文件:单个文件最大150MB
|
||||
private static final long MAX_FILE_SIZE = 150 * 1024 * 1024;
|
||||
// 解压文件:总解压大小1024MB
|
||||
private static final long MAX_TOTAL_SIZE = 1024 * 1024 * 1024;
|
||||
// 解压文件:最多解压10000个Entry
|
||||
private static final int MAX_ENTRY_COUNT = 10000;
|
||||
|
||||
@Transactional(rollbackFor = {Exception.class})
|
||||
@Override
|
||||
@ -122,7 +132,6 @@ public class AiragKnowledgeDocServiceImpl extends ServiceImpl<AiragKnowledgeDocM
|
||||
AssertUtils.assertNotEmpty("文档不存在", docList);
|
||||
|
||||
HttpServletRequest request = SpringContextUtils.getHttpServletRequest();
|
||||
String baseUrl = CommonUtils.getBaseUrl(request);
|
||||
// 检查状态
|
||||
List<AiragKnowledgeDoc> knowledgeDocs = docList.stream()
|
||||
.filter(doc -> {
|
||||
@ -143,7 +152,6 @@ public class AiragKnowledgeDocServiceImpl extends ServiceImpl<AiragKnowledgeDocM
|
||||
})
|
||||
.peek(doc -> {
|
||||
doc.setStatus(KNOWLEDGE_DOC_STATUS_BUILDING);
|
||||
doc.setBaseUrl(baseUrl);
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
if (oConvertUtils.isObjectEmpty(knowledgeDocs)) {
|
||||
@ -174,13 +182,11 @@ public class AiragKnowledgeDocServiceImpl extends ServiceImpl<AiragKnowledgeDocM
|
||||
this.updateById(doc);
|
||||
log.info("重建文档成功, 知识库id: {}, 文档id: {}", knowId, doc.getId());
|
||||
} else {
|
||||
doc.setStatus(KNOWLEDGE_DOC_STATUS_DRAFT);
|
||||
this.updateById(doc);
|
||||
this.handleDocBuildFailed(doc, "向量化失败");
|
||||
log.info("重建文档失败, 知识库id: {}, 文档id: {}", knowId, doc.getId());
|
||||
}
|
||||
}catch (Throwable t){
|
||||
doc.setStatus(KNOWLEDGE_DOC_STATUS_DRAFT);
|
||||
this.updateById(doc);
|
||||
this.handleDocBuildFailed(doc, t.getMessage());
|
||||
log.error("重建文档失败:" + t.getMessage() + ", 知识库id: " + knowId + ", 文档id: " + doc.getId(), t);
|
||||
}
|
||||
//update-end---author:chenrui ---date:20250410 for:[QQYUN-11943]【ai】ai知识库 上传完文档 一直显示构建中?------------
|
||||
@ -190,6 +196,24 @@ public class AiragKnowledgeDocServiceImpl extends ServiceImpl<AiragKnowledgeDocM
|
||||
return Result.ok("操作成功");
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理文档构建失败
|
||||
*/
|
||||
private void handleDocBuildFailed(AiragKnowledgeDoc doc, String failedReason) {
|
||||
doc.setStatus(KNOWLEDGE_DOC_STATUS_FAILED);
|
||||
|
||||
String metadataStr = doc.getMetadata();
|
||||
JSONObject metadata;
|
||||
if (oConvertUtils.isEmpty(metadataStr)) {
|
||||
metadata = new JSONObject();
|
||||
} else {
|
||||
metadata = JSONObject.parseObject(metadataStr);
|
||||
}
|
||||
metadata.put("failedReason", failedReason);
|
||||
doc.setMetadata(metadata.toJSONString());
|
||||
|
||||
this.updateById(doc);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<?> removeByKnowIds(List<String> knowIds) {
|
||||
@ -198,8 +222,16 @@ public class AiragKnowledgeDocServiceImpl extends ServiceImpl<AiragKnowledgeDocM
|
||||
AiragKnowledge airagKnowledge = airagKnowledgeMapper.selectById(knowId);
|
||||
AssertUtils.assertNotEmpty("知识库不存在", airagKnowledge);
|
||||
AssertUtils.assertNotEmpty("请先为知识库配置向量模型库", airagKnowledge.getEmbedId());
|
||||
// 异步删除向量数据
|
||||
final String embedId = airagKnowledge.getEmbedId();
|
||||
final String finalKnowId = knowId;
|
||||
CompletableFuture.runAsync(() -> {
|
||||
try {
|
||||
embeddingHandler.deleteEmbedDocsByKnowId(finalKnowId, embedId);
|
||||
} catch (Throwable ignore) {
|
||||
}
|
||||
});
|
||||
// 删除数据
|
||||
embeddingHandler.deleteEmbedDocsByKnowId(knowId, airagKnowledge.getEmbedId());
|
||||
airagKnowledgeDocMapper.deleteByMainId(knowId);
|
||||
}
|
||||
return Result.OK();
|
||||
@ -223,13 +255,39 @@ public class AiragKnowledgeDocServiceImpl extends ServiceImpl<AiragKnowledgeDocM
|
||||
AiragKnowledge airagKnowledge = airagKnowledgeMapper.selectById(knowId);
|
||||
AssertUtils.assertNotEmpty("知识库不存在", airagKnowledge);
|
||||
AssertUtils.assertNotEmpty("请先为知识库配置向量模型库", airagKnowledge.getEmbedId());
|
||||
// 异步删除向量数据
|
||||
final String embedId = airagKnowledge.getEmbedId();
|
||||
final List<String> docIdsToDelete = new ArrayList<>(groupedDocIds);
|
||||
CompletableFuture.runAsync(() -> {
|
||||
try {
|
||||
embeddingHandler.deleteEmbedDocsByDocIds(docIdsToDelete, embedId);
|
||||
} catch (Throwable ignore) {
|
||||
}
|
||||
});
|
||||
// 删除数据
|
||||
embeddingHandler.deleteEmbedDocsByDocIds(groupedDocIds, airagKnowledge.getEmbedId());
|
||||
airagKnowledgeDocMapper.deleteBatchIds(groupedDocIds);
|
||||
});
|
||||
return Result.ok("success");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<?> deleteAllByKnowId(String knowId) {
|
||||
if (oConvertUtils.isEmpty(knowId)) {
|
||||
return Result.error("知识库id不能为空");
|
||||
}
|
||||
LambdaQueryWrapper<AiragKnowledgeDoc> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(AiragKnowledgeDoc::getKnowledgeId, knowId);
|
||||
//noinspection unchecked
|
||||
wrapper.select(AiragKnowledgeDoc::getId);
|
||||
List<AiragKnowledgeDoc> docList = airagKnowledgeDocMapper.selectList(wrapper);
|
||||
if (docList.isEmpty()) {
|
||||
return Result.ok("暂无文档");
|
||||
}
|
||||
List<String> docIds = docList.stream().map(AiragKnowledgeDoc::getId).collect(Collectors.toList());
|
||||
this.removeDocByIds(docIds);
|
||||
return Result.ok("清空完成");
|
||||
}
|
||||
|
||||
@Transactional(rollbackFor = {java.lang.Exception.class})
|
||||
@Override
|
||||
public Result<?> importDocumentFromZip(String knowId, MultipartFile zipFile) {
|
||||
@ -283,6 +341,7 @@ public class AiragKnowledgeDocServiceImpl extends ServiceImpl<AiragKnowledgeDocM
|
||||
doc.setMetadata(metadata.toJSONString());
|
||||
docList.add(doc);
|
||||
});
|
||||
AssertUtils.assertNotEmpty("压缩包中没有符合要求的文档", docList);
|
||||
// 保存数据
|
||||
this.saveBatch(docList);
|
||||
// 重建文档
|
||||
@ -299,57 +358,113 @@ public class AiragKnowledgeDocServiceImpl extends ServiceImpl<AiragKnowledgeDocM
|
||||
/**
|
||||
* 解压缩文件
|
||||
*
|
||||
* @param zipFilePath
|
||||
* @param destDir
|
||||
* @param afterExtract
|
||||
* @param zipFilePath 压缩文件路径
|
||||
* @param destDir 目标文件夹
|
||||
* @param afterExtract 解压完成后回调
|
||||
* @throws IOException
|
||||
* @author chenrui
|
||||
* @date 2025/3/20 14:37
|
||||
*/
|
||||
public static void unzipFile(String zipFilePath, String destDir, Consumer<File> afterExtract) throws
|
||||
IOException {
|
||||
// 创建目标目录
|
||||
File dir = new File(destDir);
|
||||
if (!dir.exists()) {
|
||||
dir.mkdirs();
|
||||
public static void unzipFile(String zipFilePath, String destDir, Consumer<File> afterExtract) throws IOException {
|
||||
unzipFile(Paths.get(zipFilePath), Paths.get(destDir), afterExtract);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 解压缩文件
|
||||
*
|
||||
* @param zipFilePath 压缩文件路径
|
||||
* @param targetDir 目标文件夹
|
||||
* @param afterExtract 解压完成后回调
|
||||
* @throws IOException
|
||||
* @author chenrui
|
||||
* @date 2025/4/28 17:02
|
||||
*/
|
||||
private static void unzipFile(Path zipFilePath, Path targetDir, Consumer<File> afterExtract) throws IOException {
|
||||
long totalUnzippedSize = 0;
|
||||
int entryCount = 0;
|
||||
|
||||
if (!Files.exists(targetDir)) {
|
||||
Files.createDirectories(targetDir);
|
||||
}
|
||||
|
||||
try (ZipFile zipFile = new ZipFile(zipFilePath)) {
|
||||
Enumeration<? extends ZipEntry> entries = zipFile.entries();
|
||||
byte[] buffer = new byte[1024];
|
||||
try (ZipFile zipFile = new ZipFile(zipFilePath.toFile())) {
|
||||
Enumeration<ZipArchiveEntry> entries = zipFile.getEntries();
|
||||
|
||||
while (entries.hasMoreElements()) {
|
||||
ZipEntry ze = entries.nextElement();
|
||||
File newFile = new File(destDir, ze.getName());
|
||||
|
||||
// 预防 ZIP 路径穿越攻击
|
||||
String canonicalDestDirPath = dir.getCanonicalPath();
|
||||
String canonicalFilePath = newFile.getCanonicalPath();
|
||||
if (!canonicalFilePath.startsWith(canonicalDestDirPath + File.separator)) {
|
||||
throw new IOException("ZIP 路径穿越攻击被阻止: " + ze.getName());
|
||||
ZipArchiveEntry entry = entries.nextElement();
|
||||
entryCount++;
|
||||
if (entryCount > MAX_ENTRY_COUNT) {
|
||||
throw new IOException("解压文件数量超限,可能是zip bomb攻击");
|
||||
}
|
||||
|
||||
if (ze.isDirectory()) {
|
||||
newFile.mkdirs();
|
||||
} else {
|
||||
// 创建父目录
|
||||
new File(newFile.getParent()).mkdirs();
|
||||
Path newPath = safeResolve(targetDir, entry.getName());
|
||||
|
||||
// 读取 ZIP 文件并写入新文件
|
||||
try (InputStream zis = zipFile.getInputStream(ze);
|
||||
FileOutputStream fos = new FileOutputStream(newFile)) {
|
||||
int len;
|
||||
while ((len = zis.read(buffer)) > 0) {
|
||||
fos.write(buffer, 0, len);
|
||||
if (entry.isDirectory()) {
|
||||
Files.createDirectories(newPath);
|
||||
} else {
|
||||
Files.createDirectories(newPath.getParent());
|
||||
try (InputStream is = zipFile.getInputStream(entry);
|
||||
OutputStream os = Files.newOutputStream(newPath)) {
|
||||
|
||||
long bytesCopied = copyLimited(is, os, MAX_FILE_SIZE);
|
||||
totalUnzippedSize += bytesCopied;
|
||||
|
||||
if (totalUnzippedSize > MAX_TOTAL_SIZE) {
|
||||
throw new IOException("解压总大小超限,可能是zip bomb攻击");
|
||||
}
|
||||
}
|
||||
|
||||
// 解压完成后回调
|
||||
if (afterExtract != null) {
|
||||
afterExtract.accept(newFile);
|
||||
afterExtract.accept(newPath.toFile());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 安全解析路径,防止Zip Slip攻击
|
||||
*
|
||||
* @param targetDir
|
||||
* @param entryName
|
||||
* @return
|
||||
* @throws IOException
|
||||
* @author chenrui
|
||||
* @date 2025/4/28 16:46
|
||||
*/
|
||||
private static Path safeResolve(Path targetDir, String entryName) throws IOException {
|
||||
Path resolvedPath = targetDir.resolve(entryName).normalize();
|
||||
if (!resolvedPath.startsWith(targetDir)) {
|
||||
throw new IOException("ZIP 路径穿越攻击被阻止:" + entryName);
|
||||
}
|
||||
return resolvedPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* 复制输入流到输出流,并限制最大字节数
|
||||
*
|
||||
* @param in
|
||||
* @param out
|
||||
* @param maxBytes
|
||||
* @return
|
||||
* @throws IOException
|
||||
* @author chenrui
|
||||
* @date 2025/4/28 17:03
|
||||
*/
|
||||
private static long copyLimited(InputStream in, OutputStream out, long maxBytes) throws IOException {
|
||||
byte[] buffer = new byte[8192];
|
||||
long totalCopied = 0;
|
||||
int bytesRead;
|
||||
while ((bytesRead = in.read(buffer)) != -1) {
|
||||
totalCopied += bytesRead;
|
||||
if (totalCopied > maxBytes) {
|
||||
throw new IOException("单个文件解压超限,可能是zip bomb攻击");
|
||||
}
|
||||
out.write(buffer, 0, bytesRead);
|
||||
}
|
||||
return totalCopied;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -1,33 +0,0 @@
|
||||
package org.jeecg.modules.airag.llm.vo;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 知识库查询返回结果
|
||||
*
|
||||
* @Author: chenrui
|
||||
* @Date: 2025/2/18 17:53
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
public class KnowledgeSearchResult {
|
||||
|
||||
/**
|
||||
* 命中的文档内容
|
||||
*/
|
||||
String data;
|
||||
|
||||
/**
|
||||
* 命中的文档列表
|
||||
*/
|
||||
List<Map<String, Object>> documents;
|
||||
|
||||
public KnowledgeSearchResult(String data, List<Map<String, Object>> documents) {
|
||||
this.data = data;
|
||||
this.documents = documents;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,94 @@
|
||||
package org.jeecg.modules.airag.ocr.controller;
|
||||
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import org.apache.commons.collections.CollectionUtils;
|
||||
import org.jeecg.common.api.vo.Result;
|
||||
import org.jeecg.common.util.RedisUtil;
|
||||
import org.jeecg.modules.airag.ocr.entity.AiOcr;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/airag/ocr")
|
||||
public class AiOcrController {
|
||||
|
||||
@Autowired
|
||||
private RedisUtil redisUtil;
|
||||
|
||||
private static final String AI_OCR_REDIS_KEY = "airag:ocr";
|
||||
|
||||
@GetMapping("/list")
|
||||
public Result<?> list(){
|
||||
Object aiOcr = redisUtil.get(AI_OCR_REDIS_KEY);
|
||||
IPage<AiOcr> page = new Page<>(1,10);
|
||||
if(null != aiOcr){
|
||||
List<AiOcr> aiOcrList = JSONObject.parseArray(aiOcr.toString(), AiOcr.class);
|
||||
page.setRecords(aiOcrList);
|
||||
page.setTotal(aiOcrList.size());
|
||||
page.setPages(aiOcrList.size());
|
||||
}
|
||||
return Result.OK(page);
|
||||
}
|
||||
|
||||
@PostMapping("/add")
|
||||
public Result<String> add(@RequestBody AiOcr aiOcr){
|
||||
Object aiOcrList = redisUtil.get(AI_OCR_REDIS_KEY);
|
||||
aiOcr.setId(UUID.randomUUID().toString().replace("-",""));
|
||||
if(null == aiOcrList){
|
||||
List<AiOcr> list = new ArrayList<>();
|
||||
list.add(aiOcr);
|
||||
redisUtil.set(AI_OCR_REDIS_KEY, JSONObject.toJSONString(list));
|
||||
}else{
|
||||
List<AiOcr> aiOcrs = JSONObject.parseArray(aiOcrList.toString(), AiOcr.class);
|
||||
aiOcrs.add(aiOcr);
|
||||
redisUtil.set(AI_OCR_REDIS_KEY,JSONObject.toJSONString(aiOcrs));
|
||||
}
|
||||
return Result.OK("添加成功");
|
||||
}
|
||||
|
||||
@PutMapping("/edit")
|
||||
public Result<String> updateById(@RequestBody AiOcr aiOcr){
|
||||
Object aiOcrList = redisUtil.get(AI_OCR_REDIS_KEY);
|
||||
if(null != aiOcrList){
|
||||
List<AiOcr> aiOcrs = JSONObject.parseArray(aiOcrList.toString(), AiOcr.class);
|
||||
aiOcrs.forEach(item->{
|
||||
if(item.getId().equals(aiOcr.getId())){
|
||||
BeanUtils.copyProperties(aiOcr,item);
|
||||
}
|
||||
});
|
||||
redisUtil.set(AI_OCR_REDIS_KEY,JSONObject.toJSONString(aiOcrs));
|
||||
}else{
|
||||
return Result.OK("编辑失败,未找到该数据");
|
||||
}
|
||||
return Result.OK("编辑成功");
|
||||
}
|
||||
|
||||
@DeleteMapping("/deleteById")
|
||||
public Result<String> deleteById(@RequestBody AiOcr aiOcr){
|
||||
Object aiOcrObj = redisUtil.get(AI_OCR_REDIS_KEY);
|
||||
if(null != aiOcrObj){
|
||||
List<AiOcr> aiOcrs = JSONObject.parseArray(aiOcrObj.toString(), AiOcr.class);
|
||||
List<AiOcr> aiOcrList = new ArrayList<>();
|
||||
for(AiOcr ocr: aiOcrs){
|
||||
if(!ocr.getId().equals(aiOcr.getId())){
|
||||
aiOcrList.add(ocr);
|
||||
}
|
||||
}
|
||||
if(CollectionUtils.isNotEmpty(aiOcrList)){
|
||||
redisUtil.set(AI_OCR_REDIS_KEY,JSONObject.toJSONString(aiOcrList));
|
||||
}else{
|
||||
redisUtil.removeAll(AI_OCR_REDIS_KEY);
|
||||
}
|
||||
}else{
|
||||
return Result.OK("删除失败,未找到该数据");
|
||||
}
|
||||
return Result.OK("删除成功");
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,29 @@
|
||||
package org.jeecg.modules.airag.ocr.entity;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* @Description: OCR识别实体类
|
||||
*
|
||||
* @author: wangshuai
|
||||
* @date: 2025/4/16 17:01
|
||||
*/
|
||||
@Data
|
||||
public class AiOcr {
|
||||
|
||||
/**
|
||||
* 编号
|
||||
*/
|
||||
private String id;
|
||||
|
||||
/**
|
||||
* 标题
|
||||
*/
|
||||
private String title;
|
||||
|
||||
/**
|
||||
* 提示词
|
||||
*/
|
||||
private String prompt;
|
||||
|
||||
}
|
||||
@ -1,78 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<configuration debug="false">
|
||||
<!--定义日志文件的存储地址 -->
|
||||
<property name="LOG_HOME" value="../logs" />
|
||||
|
||||
<!--<property name="COLOR_PATTERN" value="%black(%contextName-) %red(%d{yyyy-MM-dd HH:mm:ss}) %green([%thread]) %highlight(%-5level) %boldMagenta( %replace(%caller{1}){'\t|Caller.{1}0|\r\n', ''})- %gray(%msg%xEx%n)" />-->
|
||||
<!-- 控制台输出 -->
|
||||
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
|
||||
<!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符
|
||||
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50}:%L - %msg%n</pattern>-->
|
||||
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %highlight(%-5level) %cyan(%logger{50}:%L) - %msg%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<!-- 按照每天生成日志文件 -->
|
||||
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
|
||||
<!--日志文件输出的文件名 -->
|
||||
<FileNamePattern>${LOG_HOME}/jeecgboot-%d{yyyy-MM-dd}.%i.log</FileNamePattern>
|
||||
<!--日志文件保留天数 -->
|
||||
<MaxHistory>30</MaxHistory>
|
||||
<maxFileSize>10MB</maxFileSize>
|
||||
</rollingPolicy>
|
||||
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
|
||||
<!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符 -->
|
||||
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50}:%L - %msg%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<!-- 生成 error html格式日志开始 -->
|
||||
<appender name="HTML" class="ch.qos.logback.core.FileAppender">
|
||||
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
|
||||
<!--设置日志级别,过滤掉info日志,只输入error日志-->
|
||||
<level>ERROR</level>
|
||||
</filter>
|
||||
<encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
|
||||
<layout class="ch.qos.logback.classic.html.HTMLLayout">
|
||||
<pattern>%p%d%msg%M%F{32}%L</pattern>
|
||||
</layout>
|
||||
</encoder>
|
||||
<file>${LOG_HOME}/error-log.html</file>
|
||||
</appender>
|
||||
<!-- 生成 error html格式日志结束 -->
|
||||
|
||||
<!-- 每天生成一个html格式的日志开始 -->
|
||||
<appender name="FILE_HTML" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
|
||||
<!--日志文件输出的文件名 -->
|
||||
<FileNamePattern>${LOG_HOME}/jeecgboot-%d{yyyy-MM-dd}.%i.html</FileNamePattern>
|
||||
<!--日志文件保留天数 -->
|
||||
<MaxHistory>30</MaxHistory>
|
||||
<MaxFileSize>10MB</MaxFileSize>
|
||||
</rollingPolicy>
|
||||
<encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
|
||||
<layout class="ch.qos.logback.classic.html.HTMLLayout">
|
||||
<pattern>%p%d%msg%M%F{32}%L</pattern>
|
||||
</layout>
|
||||
</encoder>
|
||||
</appender>
|
||||
<!-- 每天生成一个html格式的日志结束 -->
|
||||
|
||||
<!--myibatis log configure -->
|
||||
<logger name="com.apache.ibatis" level="TRACE" />
|
||||
<logger name="java.sql.Connection" level="DEBUG" />
|
||||
<logger name="java.sql.Statement" level="DEBUG" />
|
||||
<logger name="java.sql.PreparedStatement" level="DEBUG" />
|
||||
<logger name="logging.level.dev.langchain4j" level="DEBUG" />
|
||||
<logger name="logging.level.dev.ai4j.openai4j" level="DEBUG" />
|
||||
<!-- 日志输出级别 -->
|
||||
<root level="info">
|
||||
<appender-ref ref="STDOUT" />
|
||||
<!-- <appender-ref ref="FILE" />-->
|
||||
<!-- <appender-ref ref="HTML" />-->
|
||||
<!-- <appender-ref ref="FILE_HTML" />-->
|
||||
</root>
|
||||
|
||||
</configuration>
|
||||
@ -1,7 +1,10 @@
|
||||
//package org.jeecg.modules.airag.test;
|
||||
//
|
||||
//import dev.langchain4j.agent.tool.ToolParameters;
|
||||
//import dev.langchain4j.agent.tool.ToolSpecification;
|
||||
//import dev.langchain4j.data.message.*;
|
||||
//import dev.langchain4j.model.chat.ChatLanguageModel;
|
||||
//import dev.langchain4j.model.chat.request.json.JsonObjectSchema;
|
||||
//import dev.langchain4j.model.openai.OpenAiChatModel;
|
||||
//import dev.langchain4j.model.output.Response;
|
||||
//import dev.langchain4j.service.AiServices;
|
||||
@ -12,15 +15,13 @@
|
||||
//import org.jeecg.ai.factory.AiModelOptions;
|
||||
//import org.jeecg.ai.handler.AIParams;
|
||||
//import org.jeecg.ai.handler.LLMHandler;
|
||||
//import org.jeecg.modules.airag.llm.handler.AIChatParams;
|
||||
//import org.junit.Test;
|
||||
//import org.junit.jupiter.api.Test;
|
||||
//
|
||||
//import java.io.IOException;
|
||||
//import java.io.Serial;
|
||||
//import java.nio.file.Files;
|
||||
//import java.nio.file.Paths;
|
||||
//import java.util.ArrayList;
|
||||
//import java.util.Base64;
|
||||
//import java.util.Collections;
|
||||
//import java.util.*;
|
||||
//import java.util.concurrent.ConcurrentHashMap;
|
||||
//import java.util.concurrent.CountDownLatch;
|
||||
//
|
||||
@ -174,4 +175,51 @@
|
||||
// }
|
||||
//
|
||||
//
|
||||
// @Test
|
||||
// public void testFuncCalling() {
|
||||
// String apiKey = "sk-";
|
||||
// String baseUrl = "https://api.v3.cm/v1";
|
||||
// String modelName = "gpt-4o-mini";
|
||||
// double temperature = 0.7;
|
||||
// ChatLanguageModel llmModel = OpenAiChatModel.builder()
|
||||
// .apiKey(apiKey)
|
||||
// .baseUrl(baseUrl)
|
||||
// .modelName(modelName)
|
||||
// .temperature(temperature)
|
||||
// .build();
|
||||
//
|
||||
// List<ToolSpecification> toolSpecifications = new ArrayList<>();
|
||||
// Map<String, Map<String, Object>> properties = new ConcurrentHashMap<>();
|
||||
//
|
||||
// properties.put("location", new HashMap<>() {
|
||||
// @Serial
|
||||
// private static final long serialVersionUID = -8440944665582258534L;
|
||||
// {
|
||||
// put("type", "string");
|
||||
// put("description", "The city and state, e.g. San Francisco, CA");
|
||||
// }
|
||||
// });
|
||||
// ToolSpecification toolSpecification = ToolSpecification.builder()
|
||||
// .name("get_current_weather")
|
||||
// .description("Get the current weather in a given location")
|
||||
// .parameters(ToolParameters.builder()
|
||||
// .properties(properties)
|
||||
// .required(List.of("location"))
|
||||
// .type("string")
|
||||
// .build())
|
||||
// .build();
|
||||
// toolSpecifications.add(toolSpecification);
|
||||
//
|
||||
// List<ChatMessage> messages = new ArrayList<>();
|
||||
// messages.add(UserMessage.from("How is the weather in Beijing today?"));
|
||||
//
|
||||
// Response<AiMessage> resp = llmModel.generate(messages, toolSpecifications);
|
||||
// System.out.println(resp);
|
||||
//
|
||||
// /*Response { content = AiMessage { text = null
|
||||
// toolExecutionRequests = [ToolExecutionRequest { id = "call_hjPaRh9WAHv6Ib7KpgHpp8Tl", name = "get_current_weather", arguments = "{"location":"Beijing, China"}" }] },
|
||||
// tokenUsage = TokenUsage { inputTokenCount = 69, outputTokenCount = 19, totalTokenCount = 88 }, finishReason = TOOL_EXECUTION, metadata = {} }*/
|
||||
// }
|
||||
//
|
||||
//
|
||||
//}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,97 +0,0 @@
|
||||
常见问题
|
||||
1.pnpm安装依赖报错
|
||||
错误: node\_modules\\vite\\node\_modules\\esbuild\\esbuild.exe ENOENT
|
||||
解决方案:https://blog.csdn.net/weixin_41760500/article/details/119885574
命令:node ./node_modules/esbuild/install.js
|
||||
2.pnpm安装后,访问提示缺少依赖
|
||||
解决方案: https://stackoverflow.com/questions/70597494/pnpm-does-not-resolve-dependencies
|
||||
3.pnpm install出现错误
|
||||
错误:ERR\_PNPM\_PEER\_DEP\_ISSUES Unmet peer dependencies
|
||||
http://ms521.cn/index.php/Home/Index/article/aid/271
|
||||
4.项目安装依赖无问题,访问页面报错
|
||||
前端部分报错:
|
||||
[plugin:vite:vue-jsx] Cannot find package 'C:\Users\123\Desktop\JeecgBoot-master\pincone_system\jeecgboot-vue3\node_modules\.pnpm\@vitejs+plugin-vue-jsx@3.1.0_vite@5.4.9_@types+node@20.16.13_less@4.2.0_terser@5.36.0__vue@3.5.12_typescript@4.9.5_\node_modules\@babel\plugin-transform-typescript\lib\index.js' imported from C:\Users\123\Desktop\JeecgBoot-master\pincone_system\jeecgboot-vue3\node_modules\.pnpm\@vitejs+plugin-vue-jsx@3.1.0_vite@5.4.9_@types+node@20.16.13_less@4.2.0_terser@5.36.0__vue@3.5.12_typescript@4.9.5_\node_modules\@vitejs\plugin-vue-jsx\dist\index.cjs Did you mean to import "@babel/plugin-transform-typescript/lib/index.js"? #7396
|
||||
回答: 是因为项目的路径太长导致 ,相关问题Issues,查看相关博客
|
||||
• https://blog.csdn.net/weixin_43235500/article/details/142144989
|
||||
• https://blog.csdn.net/qq_25996219/article/details/140328092
|
||||
5. 通过npm install启动报错
|
||||
建议:请使用pnpm i 可以避免更多问题
|
||||
错误情况:
|
||||
解决:进入提示的路径 \node_modules\vite-plugin-mock\node_modules\esbuild\
执行命令: node install.js
再启动就好了
|
||||
6. 前端刷新进不了登录页面
|
||||
报错props.ts:15 Uncaught (in promise) SyntaxError: Unexpected token '='
错误截图:
|
||||
原因:谷歌浏览器版本过低,升级浏览器
比如这边版本就过低了
|
||||
|
||||
7.表单如何全部禁用
|
||||
加上这个属性就可以了
|
||||
效果
|
||||
8.table列表如何自定义排序
|
||||
defSort: {
|
||||
column: 'id',
|
||||
order: 'desc',
|
||||
},
|
||||
参考示例:
|
||||
|
||||
9.idea编写js时爆红,提示statement expected
|
||||
https://blog.csdn.net/mlsama/article/details/80633009
|
||||
10.抽屉的setDrawerProps不好使(值会还原)
|
||||
|
||||
11. 如何删除不需要的demo,制作一个精简版本
|
||||
精简项目,删除demo等非必须功能
|
||||
12.vue3 暗黑模式下显示不完整
|
||||
错误示例:
|
||||
|
||||
解决方案:
|
||||
在样式中的字体颜色和背景颜色使用@变量名称来代替
|
||||
color: @text-color;
|
||||
background-color: @component-background;
|
||||
|
||||
[info] 通用样式变量名称可以在在目录bulid->vite->plugin->themes.ts中找到,darkModifyVars是重写antd中的样式
|
||||
|
||||
[info]更多样式变量名称请参考目录node_modules/es/style/themes/default.less
|
||||
改造完成之后的效果
|
||||
|
||||
13.操作列“删除按钮”界面布局异常
|
||||
相关issue
|
||||
https://github.com/jeecgboot/jeecgboot-vue3/issues/458
|
||||
问题截图:
|
||||
|
||||
解决方案:找到popConfirm填写属性placement: 'left'
|
||||
placement: 'left',
|
||||
|
||||
效果截图
|
||||
|
||||
14.在centos7中下载依赖pnpm i时 mozjpeg依赖下载不下来
|
||||
https://github.com/jeecgboot/jeecgboot-vue3/issues/433#issuecomment-1510470534
|
||||
15.日期遮挡问题
|
||||
问题截图:
下拉显示组件,页面滚动时,页面出现错位遮挡问题
|
||||
|
||||
解决方案:将组件挂载到父节点上
|
||||
getPopupContainer: (node) => node.parentNode,
|
||||
效果截图
|
||||
|
||||
16.nextTick作用
|
||||
在 Vue 3 中,nextTick 方法用于在 DOM 更新之后执行回调函数。它的作用是在下次 DOM 更新循环结束后执行一些操作,以确保你在操作更新的 DOM 元素时能够获取到最新的结果。
nextTick 方法可以用于以下情况:
|
||||
1 在更新数据后立即操作 DOM 元素。
|
||||
2 在更新组件后执行某些逻辑或触发一些副作用。
|
||||
3 在更新后获取更新后的 DOM 元素的尺寸或位置等信息。
|
||||
使用nextTick 方法有两种方式:
1.使用回调函数:
|
||||
nextTick(()=>
|
||||
//在DOM更新后执行的操作
|
||||
}):
|
||||
2.使用Promise:
|
||||
nextTick().then(()=>
|
||||
//在DOM更新后执行的操作
|
||||
})
|
||||
无论使用哪种方式,传入的回调函数或Promisel回调都会在下一次DOM更新周期之后被调用。这样可以确保在数据变化后,Vue已经完成了相应的DOM更新。
|
||||
需要注意的是,nextTick 方法是异步执行的,因此不能保证回调函数会立即执行。如果需要等待nextTick执行完成,可以使用await关键字或者. then()方法来等待Promise的完成。
|
||||
17. 一个页面多个表格,列的展示会互相影响
|
||||
[info] 相关issue
|
||||
https://github.com/jeecgboot/jeecgboot-vue3/issues/1064
|
||||
[info]问题截图
|
||||
|
||||
[info] 解决方案:在不同的tableProps下设置不同的checkKey即可解决
|
||||
|
||||
tableSetting: { cacheKey: 'depart_user_departInfo' },
|
||||
18. 通过npm install启动报错
|
||||
可以使用这个命令:
|
||||
npm install --ignore-scripts
|
||||
Binary file not shown.
Binary file not shown.
@ -5,7 +5,7 @@
|
||||
<parent>
|
||||
<artifactId>jeecg-boot-module</artifactId>
|
||||
<groupId>org.jeecgframework.boot</groupId>
|
||||
<version>3.8.0</version>
|
||||
<version>3.8.1</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
//package org.jeecg.modules.demo.cloud.controller;
|
||||
//
|
||||
//import com.alibaba.csp.sentinel.annotation.SentinelResource;
|
||||
//import io.swagger.annotations.Api;
|
||||
//import io.swagger.annotations.ApiOperation;
|
||||
//import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
//import io.swagger.v3.oas.annotations.Operation;
|
||||
//import lombok.extern.slf4j.Slf4j;
|
||||
//import org.jeecg.common.api.vo.Result;
|
||||
//import org.jeecg.common.system.api.ISysBaseAPI;
|
||||
@ -18,7 +18,7 @@
|
||||
// *
|
||||
// */
|
||||
//@Slf4j
|
||||
//@Api(tags = "【微服务】单元测试")
|
||||
//@Tag(name = "【微服务】单元测试")
|
||||
//@RestController
|
||||
//@RequestMapping("/test")
|
||||
//public class JcloudDemoFeignController {
|
||||
@ -34,7 +34,7 @@
|
||||
// */
|
||||
// @GetMapping("/callSystem")
|
||||
// //@SentinelResource(value = "remoteDict",fallback = "getDefaultHandler")
|
||||
// @ApiOperation(value = "通过feign调用system服务", notes = "测试jeecg-demo服务,是否通过fegin调用system服务接口")
|
||||
// @Operation(summary = "通过feign调用system服务")
|
||||
// public Result getRemoteDict() {
|
||||
// List<DictModel> list = sysBaseApi.queryAllDict();
|
||||
// return Result.OK(list);
|
||||
@ -48,7 +48,7 @@
|
||||
//// * @return
|
||||
//// */
|
||||
//// @GetMapping("/callErp")
|
||||
//// @ApiOperation(value = "测试feign erp", notes = "测试feign erp")
|
||||
//// @Operation(summary = "测试feign erp")
|
||||
//// public Result callErp() {
|
||||
//// log.info("call erp 服务");
|
||||
//// String res = erpHelloApi.callHello();
|
||||
|
||||
@ -1,95 +0,0 @@
|
||||
package org.jeecg.modules.demo.gpt.controller;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jeecg.chatgpt.service.AiChatService;
|
||||
import org.jeecg.common.api.vo.Result;
|
||||
import org.jeecg.common.aspect.annotation.AutoLog;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
//update-begin---author:chenrui ---date:20240110 for:[QQYUN-5509]AI生成表结构和软文------------
|
||||
|
||||
/**
|
||||
* @Description: chatGpt接口
|
||||
* @Author: chenrui
|
||||
* @Date: 2024/1/9 16:30
|
||||
*/
|
||||
@Tag(name = "AI接口")
|
||||
@RestController
|
||||
@RequestMapping("/test/ai")
|
||||
@Slf4j
|
||||
public class AiController {
|
||||
|
||||
private static final String CACHE_PREFIX = "ai:resp:";
|
||||
|
||||
@Autowired
|
||||
AiChatService aiChatService;
|
||||
|
||||
|
||||
/**
|
||||
* 通过AI生成模块表设计
|
||||
* @param descr 描述
|
||||
* @return
|
||||
* @author chenrui
|
||||
* @date 2024/1/9 20:12
|
||||
*/
|
||||
@AutoLog(value = "通过AI生成模块表设计")
|
||||
@PostMapping(value = "/gen/schema/modules")
|
||||
@Operation(summary = "通过AI生成模块表设计")
|
||||
public Result<String> genSchemaModules(@RequestParam(name = "prompt", required = true) String prompt) {
|
||||
String result = aiChatService.genSchemaModules(prompt);
|
||||
return Result.ok(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过AI生成软文
|
||||
* @param descr 描述
|
||||
* @return
|
||||
* @author chenrui
|
||||
* @date 2024/1/9 20:12
|
||||
*/
|
||||
@AutoLog(value = "通过AI生成软文")
|
||||
@PostMapping(value = "/gen/article")
|
||||
@Operation(summary = "通过AI生成软文")
|
||||
public Result<String> genArticle(@RequestParam(name = "prompt", required = true) String prompt) {
|
||||
String result = aiChatService.genArticleWithMd(prompt);
|
||||
return Result.ok(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 向AI提问
|
||||
* @param message
|
||||
* @return
|
||||
* @author chenrui
|
||||
* @date 2024/1/15 19:11
|
||||
*/
|
||||
@AutoLog(value = "向AI提问")
|
||||
@PostMapping(value = "/completions")
|
||||
@Operation(summary = "向AI提问")
|
||||
public Result<?> completions(@RequestParam(name = "message", required = true) String message) {
|
||||
String result = aiChatService.completions(message);
|
||||
return Result.ok(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 让AI生成图片
|
||||
* @param prompt
|
||||
* @return
|
||||
* @author chenrui
|
||||
* @date 2024/1/15 19:11
|
||||
*/
|
||||
@AutoLog(value = "让AI生成图片")
|
||||
@PostMapping(value = "/gen/image")
|
||||
@Operation(summary = "让AI生成图片")
|
||||
public Result<?> genImage(@RequestParam(name = "prompt", required = true) String prompt) {
|
||||
String result = aiChatService.imageGenerate(prompt);
|
||||
return Result.ok(result);
|
||||
}
|
||||
|
||||
}
|
||||
//update-end---author:chenrui ---date:20240110 for://update-begin---author:chenrui ---date:20240110 for:[QQYUN-5509]AI生成表结构和软文------------------------
|
||||
@ -4,8 +4,6 @@ import com.alibaba.fastjson.JSON;
|
||||
import com.alibaba.fastjson.JSONArray;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
@ -64,7 +62,7 @@ public class JeecgDemoController extends JeecgController<JeecgDemo, IJeecgDemoSe
|
||||
* @param req
|
||||
* @return
|
||||
*/
|
||||
@Operation(summary = "获取所有Demo数据列表")
|
||||
@Operation(summary = "获取Demo数据列表")
|
||||
@GetMapping(value = "/list")
|
||||
@PermissionData(pageComponent = "jeecg/JeecgDemoList")
|
||||
public Result<?> list(JeecgDemo jeecgDemo, @RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo, @RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize,
|
||||
@ -477,7 +475,6 @@ public class JeecgDemoController extends JeecgController<JeecgDemo, IJeecgDemoSe
|
||||
* 测试Mono对象
|
||||
* @return
|
||||
*/
|
||||
@Operation(summary = "Mono测试")
|
||||
@GetMapping(value ="/test")
|
||||
public Mono<String> test() {
|
||||
//解决shiro报错No SecurityManager accessible to the calling code, either bound to the org.apache.shiro
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
package org.jeecg.modules.demo.test.controller;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jeecg.common.api.vo.Result;
|
||||
import org.jeecg.common.aspect.annotation.AutoLog;
|
||||
|
||||
@ -3,7 +3,6 @@ package org.jeecg.modules.demo.test.entity;
|
||||
import java.io.Serializable;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.Version;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import org.jeecg.common.system.base.entity.JeecgEntity;
|
||||
import org.jeecgframework.poi.excel.annotation.Excel;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
@ -11,6 +10,8 @@ import org.springframework.format.annotation.DateTimeFormat;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
<parent>
|
||||
<artifactId>jeecg-boot-parent</artifactId>
|
||||
<groupId>org.jeecgframework.boot</groupId>
|
||||
<version>3.8.0</version>
|
||||
<version>3.8.1</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
<parent>
|
||||
<artifactId>jeecg-system-api</artifactId>
|
||||
<groupId>org.jeecgframework.boot</groupId>
|
||||
<version>3.8.0</version>
|
||||
<version>3.8.1</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
<parent>
|
||||
<artifactId>jeecg-system-api</artifactId>
|
||||
<groupId>org.jeecgframework.boot</groupId>
|
||||
<version>3.8.0</version>
|
||||
<version>3.8.1</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
<parent>
|
||||
<artifactId>jeecg-module-system</artifactId>
|
||||
<groupId>org.jeecgframework.boot</groupId>
|
||||
<version>3.8.0</version>
|
||||
<version>3.8.1</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<groupId>org.jeecgframework.boot</groupId>
|
||||
<artifactId>jeecg-module-system</artifactId>
|
||||
<version>3.8.0</version>
|
||||
<version>3.8.1</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
@ -24,6 +24,12 @@
|
||||
<artifactId>hibernate-re</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- AI大模型管理 -->
|
||||
<dependency>
|
||||
<groupId>org.jeecgframework.boot</groupId>
|
||||
<artifactId>jeecg-boot-module-airag</artifactId>
|
||||
<version>${jeecgboot.version}</version>
|
||||
</dependency>
|
||||
<!-- 企业微信/钉钉 api -->
|
||||
<dependency>
|
||||
<groupId>org.jeecgframework</groupId>
|
||||
|
||||
@ -4,14 +4,14 @@ import lombok.extern.slf4j.Slf4j;
|
||||
import org.jeecg.common.constant.SymbolConstant;
|
||||
import org.jeecg.common.exception.JeecgSqlInjectionException;
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
import org.jeecg.common.util.sqlparse.JSqlParserUtils;
|
||||
import org.jeecg.common.util.sqlparse.vo.SelectSqlInfo;
|
||||
import org.jeecg.config.JeecgBaseConfig;
|
||||
import org.jeecg.config.firewall.SqlInjection.IDictTableWhiteListHandler;
|
||||
import org.jeecg.config.firewall.interceptor.LowCodeModeInterceptor;
|
||||
import org.jeecg.modules.system.entity.SysTableWhiteList;
|
||||
import org.jeecg.modules.system.security.DictQueryBlackListHandler;
|
||||
import org.jeecg.modules.system.service.ISysTableWhiteListService;
|
||||
import org.jeecgframework.minidao.sqlparser.impl.vo.SelectSqlInfo;
|
||||
import org.jeecgframework.minidao.util.MiniDaoUtil;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@ -65,7 +65,7 @@ public class DictTableWhiteListHandlerImpl implements IDictTableWhiteListHandler
|
||||
public boolean isPassBySql(String sql) {
|
||||
Map<String, SelectSqlInfo> parsedMap = null;
|
||||
try {
|
||||
parsedMap = JSqlParserUtils.parseAllSelectTable(sql);
|
||||
parsedMap = MiniDaoUtil.parseAllSelectTable(sql);
|
||||
} catch (Exception e) {
|
||||
log.warn("校验sql语句,解析报错:{}", e.getMessage());
|
||||
}
|
||||
@ -127,7 +127,7 @@ public class DictTableWhiteListHandlerImpl implements IDictTableWhiteListHandler
|
||||
log.info("字典拼接的查询SQL:{}", sql);
|
||||
try {
|
||||
// 进行SQL解析
|
||||
JSqlParserUtils.parseSelectSqlInfo(sql);
|
||||
MiniDaoUtil.parseSelectSqlInfo(sql);
|
||||
} catch (Exception e) {
|
||||
// 如果SQL解析失败,则通过字段名和表名进行校验
|
||||
return checkWhiteList(tableName, new HashSet<>(Arrays.asList(fields)));
|
||||
|
||||
@ -27,6 +27,10 @@ import java.io.UnsupportedEncodingException;
|
||||
import java.net.URLEncoder;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.SynchronousQueue;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* @Description: 邮箱发送信息
|
||||
@ -54,32 +58,37 @@ public class EmailSendMsgHandle implements ISendMsgHandle {
|
||||
* 真实姓名变量
|
||||
*/
|
||||
private static final String realNameExp = "{REALNAME}";
|
||||
|
||||
/**
|
||||
* 线程池用于异步发送消息
|
||||
*/
|
||||
public static ExecutorService cachedThreadPool = new ThreadPoolExecutor(0, 1024, 60L, TimeUnit.SECONDS, new SynchronousQueue<>());
|
||||
|
||||
|
||||
@Override
|
||||
public void sendMsg(String esReceiver, String esTitle, String esContent) {
|
||||
JavaMailSender mailSender = (JavaMailSender) SpringContextUtils.getBean("mailSender");
|
||||
MimeMessage message = mailSender.createMimeMessage();
|
||||
MimeMessageHelper helper = null;
|
||||
//update-begin-author:taoyan date:20200811 for:配置类数据获取
|
||||
if(oConvertUtils.isEmpty(emailFrom)){
|
||||
StaticConfig staticConfig = SpringContextUtils.getBean(StaticConfig.class);
|
||||
setEmailFrom(staticConfig.getEmailFrom());
|
||||
}
|
||||
//update-end-author:taoyan date:20200811 for:配置类数据获取
|
||||
try {
|
||||
helper = new MimeMessageHelper(message, true);
|
||||
// 设置发送方邮箱地址
|
||||
helper.setFrom(emailFrom);
|
||||
helper.setTo(esReceiver);
|
||||
helper.setSubject(esTitle);
|
||||
helper.setText(esContent, true);
|
||||
mailSender.send(message);
|
||||
} catch (MessagingException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
cachedThreadPool.execute(()->{
|
||||
try {
|
||||
log.info("============> 开始邮件发送,接收人:"+esReceiver);
|
||||
MimeMessageHelper helper = new MimeMessageHelper(message, true);
|
||||
// 设置发送方邮箱地址
|
||||
helper.setFrom(emailFrom);
|
||||
helper.setTo(esReceiver);
|
||||
helper.setSubject(esTitle);
|
||||
helper.setText(esContent, true);
|
||||
mailSender.send(message);
|
||||
log.info("============> 邮件发送成功,接收人:"+esReceiver);
|
||||
} catch (MessagingException e) {
|
||||
log.error("============> 邮件发送失败,接收人:"+esReceiver, e.getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -180,24 +189,26 @@ public class EmailSendMsgHandle implements ISendMsgHandle {
|
||||
private void sendEmail(String email, String content, String title){
|
||||
JavaMailSender mailSender = (JavaMailSender) SpringContextUtils.getBean("mailSender");
|
||||
MimeMessage message = mailSender.createMimeMessage();
|
||||
MimeMessageHelper helper = null;
|
||||
if (oConvertUtils.isEmpty(emailFrom)) {
|
||||
StaticConfig staticConfig = SpringContextUtils.getBean(StaticConfig.class);
|
||||
setEmailFrom(staticConfig.getEmailFrom());
|
||||
}
|
||||
try {
|
||||
helper = new MimeMessageHelper(message, true);
|
||||
// 设置发送方邮箱地址
|
||||
helper.setFrom(emailFrom);
|
||||
helper.setTo(email);
|
||||
//设置抄送人
|
||||
helper.setCc(email);
|
||||
helper.setSubject(title);
|
||||
helper.setText(content, true);
|
||||
mailSender.send(message);
|
||||
} catch (MessagingException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
cachedThreadPool.execute(()->{
|
||||
try {
|
||||
MimeMessageHelper helper = new MimeMessageHelper(message, true);
|
||||
// 设置发送方邮箱地址
|
||||
helper.setFrom(emailFrom);
|
||||
helper.setTo(email);
|
||||
//设置抄送人
|
||||
helper.setCc(email);
|
||||
helper.setSubject(title);
|
||||
helper.setText(content, true);
|
||||
mailSender.send(message);
|
||||
log.info("============> 邮件发送成功,接收人:"+email);
|
||||
} catch (MessagingException e) {
|
||||
log.warn("============> 邮件发送失败,接收人:"+email, e.getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
//update-end-author:taoyan date:2023-6-20 for: QQYUN-5557【简流】通知节点 发送邮箱 表单上有一个邮箱字段,流程中,邮件发送节点,邮件接收人 不可选择邮箱
|
||||
|
||||
|
||||
@ -19,6 +19,21 @@ public class CustomInMemoryHttpTraceRepository extends InMemoryHttpTraceReposito
|
||||
return super.findAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* for [issues/8309]系统监控>请求追踪,列表每刷新一下,总数据就减一#8309
|
||||
* @param trace
|
||||
* @author chenrui
|
||||
* @date 2025/6/4 19:38
|
||||
*/
|
||||
@Override
|
||||
public void add(HttpTrace trace) {
|
||||
// 只有当请求不是OPTIONS方法,并且URI不包含httptrace时才记录数据
|
||||
if (!"OPTIONS".equals(trace.getRequest().getMethod()) &&
|
||||
!trace.getRequest().getUri().toString().contains("httptrace")) {
|
||||
super.add(trace);
|
||||
}
|
||||
}
|
||||
|
||||
public List<HttpTrace> findAll(String query) {
|
||||
List<HttpTrace> allTrace = super.findAll();
|
||||
if (null != allTrace && !allTrace.isEmpty()) {
|
||||
|
||||
@ -78,7 +78,9 @@ public class CustomUndertowMetricsHandler {
|
||||
// 获取当前 session,如果不存在则创建
|
||||
Session session = sessionManager.getSession(exchange, sessionConfig);
|
||||
if (session == null) {
|
||||
sessionManager.createSession(exchange, sessionConfig);
|
||||
try {
|
||||
sessionManager.createSession(exchange, sessionConfig);
|
||||
} catch (Exception e) {}
|
||||
}
|
||||
|
||||
// 执行下一个 Handler
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
package org.jeecg.modules.openapi.controller;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
@ -10,14 +12,14 @@ import org.jeecg.common.constant.CommonConstant;
|
||||
import org.jeecg.common.system.base.controller.JeecgController;
|
||||
import org.jeecg.common.system.query.QueryGenerator;
|
||||
import org.jeecg.common.system.util.JwtUtil;
|
||||
import org.jeecg.common.util.RedisUtil;
|
||||
import org.jeecg.common.util.RestUtil;
|
||||
import org.jeecg.modules.openapi.entity.OpenApi;
|
||||
import org.jeecg.modules.openapi.entity.OpenApiAuth;
|
||||
import org.jeecg.modules.openapi.entity.OpenApiHeader;
|
||||
import org.jeecg.modules.openapi.entity.OpenApiParam;
|
||||
import org.jeecg.modules.openapi.generator.PathGenerator;
|
||||
import org.jeecg.modules.openapi.service.OpenApiAuthService;
|
||||
import org.jeecg.modules.openapi.service.OpenApiHeaderService;
|
||||
import org.jeecg.modules.openapi.service.OpenApiParamService;
|
||||
import org.jeecg.modules.openapi.service.OpenApiService;
|
||||
import org.jeecg.modules.openapi.swagger.*;
|
||||
import org.jeecg.modules.system.entity.SysUser;
|
||||
@ -26,13 +28,15 @@ import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.net.URI;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* @date 2024/12/10 9:11
|
||||
@ -44,9 +48,7 @@ public class OpenApiController extends JeecgController<OpenApi, OpenApiService>
|
||||
@Autowired
|
||||
private RestTemplate restTemplate;
|
||||
@Autowired
|
||||
private OpenApiParamService openApiParamService;
|
||||
@Autowired
|
||||
private OpenApiHeaderService openApiHeaderService;
|
||||
private RedisUtil redisUtil;
|
||||
@Autowired
|
||||
private ISysUserService sysUserService;
|
||||
@Autowired
|
||||
@ -67,10 +69,6 @@ public class OpenApiController extends JeecgController<OpenApi, OpenApiService>
|
||||
QueryWrapper<OpenApi> queryWrapper = QueryGenerator.initQueryWrapper(openApi, req.getParameterMap());
|
||||
Page<OpenApi> page = new Page<>(pageNo, pageSize);
|
||||
IPage<OpenApi> pageList = service.page(page, queryWrapper);
|
||||
for (OpenApi api : pageList.getRecords()) {
|
||||
api.setParams(openApiParamService.findByApiId(api.getId()));
|
||||
api.setHeaders(openApiHeaderService.findByApiId(api.getId()));
|
||||
}
|
||||
return Result.ok(pageList);
|
||||
}
|
||||
|
||||
@ -82,16 +80,7 @@ public class OpenApiController extends JeecgController<OpenApi, OpenApiService>
|
||||
*/
|
||||
@PostMapping(value = "/add")
|
||||
public Result<?> add(@RequestBody OpenApi openApi) {
|
||||
if (service.save(openApi)) {
|
||||
if (!CollectionUtils.isEmpty(openApi.getHeaders())) {
|
||||
openApiHeaderService.saveBatch(openApi.getHeaders());
|
||||
}
|
||||
|
||||
if (!CollectionUtils.isEmpty(openApi.getParams())) {
|
||||
openApiParamService.saveBatch(openApi.getParams());
|
||||
}
|
||||
}
|
||||
|
||||
service.save(openApi);
|
||||
return Result.ok("添加成功!");
|
||||
}
|
||||
|
||||
@ -103,18 +92,7 @@ public class OpenApiController extends JeecgController<OpenApi, OpenApiService>
|
||||
*/
|
||||
@PutMapping(value = "/edit")
|
||||
public Result<?> edit(@RequestBody OpenApi openApi) {
|
||||
if (service.updateById(openApi)) {
|
||||
openApiHeaderService.deleteByApiId(openApi.getId());
|
||||
openApiParamService.deleteByApiId(openApi.getId());
|
||||
|
||||
if (!CollectionUtils.isEmpty(openApi.getHeaders())) {
|
||||
openApiHeaderService.saveBatch(openApi.getHeaders());
|
||||
}
|
||||
|
||||
if (!CollectionUtils.isEmpty(openApi.getParams())) {
|
||||
openApiParamService.saveBatch(openApi.getParams());
|
||||
}
|
||||
}
|
||||
service.updateById(openApi);
|
||||
return Result.ok("修改成功!");
|
||||
|
||||
}
|
||||
@ -170,27 +148,72 @@ public class OpenApiController extends JeecgController<OpenApi, OpenApiService>
|
||||
result.put("data", null);
|
||||
return Result.error("失败", result);
|
||||
}
|
||||
List<OpenApiHeader> headers = openApiHeaderService.findByApiId(openApi.getId());
|
||||
HttpHeaders httpHeaders = new HttpHeaders();
|
||||
if (StrUtil.isNotEmpty(openApi.getHeadersJson())) {
|
||||
List<OpenApiHeader> headers = JSON.parseArray(openApi.getHeadersJson(),OpenApiHeader.class);
|
||||
if (headers.size()>0) {
|
||||
for (OpenApiHeader header : headers) {
|
||||
httpHeaders.put(header.getHeaderKey(), Lists.newArrayList(request.getHeader(header.getHeaderKey())));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String url = openApi.getOriginUrl();
|
||||
String method = openApi.getRequestMethod();
|
||||
|
||||
HttpHeaders httpHeaders = new HttpHeaders();
|
||||
for (OpenApiHeader header : headers) {
|
||||
httpHeaders.put(header.getHeaderKey(), Lists.newArrayList(request.getHeader(header.getHeaderKey())));
|
||||
}
|
||||
|
||||
String appkey = request.getHeader("appkey");
|
||||
OpenApiAuth openApiAuth = openApiAuthService.getByAppkey(appkey);
|
||||
SysUser systemUser = sysUserService.getById(openApiAuth.getSystemUserId());
|
||||
String token = JwtUtil.sign(systemUser.getUsername(), systemUser.getPassword());
|
||||
SysUser systemUser = sysUserService.getUserByName(openApiAuth.getCreateBy());
|
||||
String token = this.getToken(systemUser.getUsername(), systemUser.getPassword());
|
||||
httpHeaders.put("X-Access-Token", Lists.newArrayList(token));
|
||||
|
||||
httpHeaders.put("Content-Type",Lists.newArrayList("application/json"));
|
||||
HttpEntity<String> httpEntity = new HttpEntity<>(json, httpHeaders);
|
||||
url = RestUtil.getBaseUrl() + url;
|
||||
UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(url);
|
||||
if (HttpMethod.GET.matches(method)
|
||||
|| HttpMethod.DELETE.matches(method)
|
||||
|| HttpMethod.OPTIONS.matches(method)
|
||||
|| HttpMethod.TRACE.matches(method)) {
|
||||
//拼接参数
|
||||
if (!request.getParameterMap().isEmpty()) {
|
||||
if (StrUtil.isNotEmpty(openApi.getParamsJson())) {
|
||||
List<OpenApiParam> params = JSON.parseArray(openApi.getParamsJson(),OpenApiParam.class);
|
||||
if (params.size()>0) {
|
||||
Map<String, OpenApiParam> openApiParamMap = params.stream().collect(Collectors.toMap(p -> p.getParamKey(), p -> p, (e, r) -> e));
|
||||
request.getParameterMap().forEach((k, v) -> {
|
||||
OpenApiParam openApiParam = openApiParamMap.get(k);
|
||||
if (Objects.nonNull(openApiParam)) {
|
||||
if(v==null&&StrUtil.isNotEmpty(openApiParam.getDefaultValue())){
|
||||
builder.queryParam(openApiParam.getParamKey(), openApiParam.getDefaultValue());
|
||||
}
|
||||
if (v!=null){
|
||||
builder.queryParam(openApiParam.getParamKey(), v);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return restTemplate.exchange(url, HttpMethod.resolve(method), httpEntity, Result.class, request.getParameterMap()).getBody();
|
||||
}
|
||||
}
|
||||
URI targetUrl = builder.build().encode().toUri();
|
||||
return restTemplate.exchange(targetUrl.toString(), Objects.requireNonNull(HttpMethod.resolve(method)), httpEntity, Result.class, request.getParameterMap()).getBody();
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成接口访问令牌 Token
|
||||
*
|
||||
* @param USERNAME
|
||||
* @param PASSWORD
|
||||
* @return
|
||||
*/
|
||||
private String getToken(String USERNAME, String PASSWORD) {
|
||||
String token = JwtUtil.sign(USERNAME, PASSWORD);
|
||||
redisUtil.set(CommonConstant.PREFIX_USER_TOKEN + token, token);
|
||||
redisUtil.expire(CommonConstant.PREFIX_USER_TOKEN + token, 60);
|
||||
return token;
|
||||
}
|
||||
|
||||
|
||||
@GetMapping("/json")
|
||||
public SwaggerModel swaggerModel() {
|
||||
|
||||
@ -230,13 +253,16 @@ public class OpenApiController extends JeecgController<OpenApi, OpenApiService>
|
||||
definition.setType("object");
|
||||
Map<String, SwaggerDefinitionProperties> definitionProperties = new HashMap<>();
|
||||
definition.setProperties(definitionProperties);
|
||||
|
||||
JSONObject jsonObject = JSONObject.parseObject(openApi.getBody());
|
||||
for (Map.Entry<String, Object> properties : jsonObject.entrySet()) {
|
||||
SwaggerDefinitionProperties swaggerDefinitionProperties = new SwaggerDefinitionProperties();
|
||||
swaggerDefinitionProperties.setType("string");
|
||||
swaggerDefinitionProperties.setDescription(properties.getValue()+"");
|
||||
definitionProperties.put(properties.getKey(), swaggerDefinitionProperties);
|
||||
if (openApi.getBody()!=null){
|
||||
JSONObject jsonObject = JSONObject.parseObject(openApi.getBody());
|
||||
if (jsonObject.size()>0){
|
||||
for (Map.Entry<String, Object> properties : jsonObject.entrySet()) {
|
||||
SwaggerDefinitionProperties swaggerDefinitionProperties = new SwaggerDefinitionProperties();
|
||||
swaggerDefinitionProperties.setType("string");
|
||||
swaggerDefinitionProperties.setDescription(properties.getValue()+"");
|
||||
definitionProperties.put(properties.getKey(), swaggerDefinitionProperties);
|
||||
}
|
||||
}
|
||||
}
|
||||
// body的definition构建完成
|
||||
definitions.put(openApi.getRequestUrl()+"Using"+openApi.getRequestMethod()+"body", definition);
|
||||
@ -327,25 +353,28 @@ public class OpenApiController extends JeecgController<OpenApi, OpenApiService>
|
||||
|
||||
private void parameters(SwaggerOperation operation, OpenApi openApi) {
|
||||
List<SwaggerOperationParameter> parameters = new ArrayList<>();
|
||||
|
||||
for (OpenApiParam openApiParam : openApiParamService.findByApiId(openApi.getId())) {
|
||||
SwaggerOperationParameter parameter = new SwaggerOperationParameter();
|
||||
parameter.setIn("path");
|
||||
parameter.setName(openApiParam.getParamKey());
|
||||
parameter.setRequired(openApiParam.getRequired() == 1);
|
||||
parameter.setDescription(openApiParam.getNote());
|
||||
parameters.add(parameter);
|
||||
if (openApi.getParamsJson()!=null) {
|
||||
List<OpenApiParam> openApiParams = JSON.parseArray(openApi.getParamsJson(), OpenApiParam.class);
|
||||
for (OpenApiParam openApiParam : openApiParams) {
|
||||
SwaggerOperationParameter parameter = new SwaggerOperationParameter();
|
||||
parameter.setIn("path");
|
||||
parameter.setName(openApiParam.getParamKey());
|
||||
parameter.setRequired(openApiParam.getRequired() == 1);
|
||||
parameter.setDescription(openApiParam.getNote());
|
||||
parameters.add(parameter);
|
||||
}
|
||||
}
|
||||
|
||||
for (OpenApiHeader openApiHeader : openApiHeaderService.findByApiId(openApi.getId())) {
|
||||
SwaggerOperationParameter parameter = new SwaggerOperationParameter();
|
||||
parameter.setIn("header");
|
||||
parameter.setName(openApiHeader.getHeaderKey());
|
||||
parameter.setRequired(openApiHeader.getRequired() == 1);
|
||||
parameter.setDescription(openApiHeader.getNote());
|
||||
parameters.add(parameter);
|
||||
if (openApi.getHeadersJson()!=null) {
|
||||
List<OpenApiHeader> openApiHeaders = JSON.parseArray(openApi.getHeadersJson(), OpenApiHeader.class);
|
||||
for (OpenApiHeader openApiHeader : openApiHeaders) {
|
||||
SwaggerOperationParameter parameter = new SwaggerOperationParameter();
|
||||
parameter.setIn("header");
|
||||
parameter.setName(openApiHeader.getHeaderKey());
|
||||
parameter.setRequired(openApiHeader.getRequired() == 1);
|
||||
parameter.setDescription(openApiHeader.getNote());
|
||||
parameters.add(parameter);
|
||||
}
|
||||
}
|
||||
|
||||
operation.setParameters(parameters);
|
||||
}
|
||||
|
||||
@ -353,7 +382,7 @@ public class OpenApiController extends JeecgController<OpenApi, OpenApiService>
|
||||
SwaggerInfo info = new SwaggerInfo();
|
||||
|
||||
info.setDescription("OpenAPI 接口列表");
|
||||
info.setVersion("3.7.1");
|
||||
info.setVersion("3.8.1");
|
||||
info.setTitle("OpenAPI 接口列表");
|
||||
info.setTermsOfService("https://jeecg.com");
|
||||
|
||||
|
||||
@ -6,8 +6,8 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import org.jeecg.common.api.vo.Result;
|
||||
import org.jeecg.common.system.base.controller.JeecgController;
|
||||
import org.jeecg.common.system.query.QueryGenerator;
|
||||
import org.jeecg.modules.openapi.entity.OpenApiRecord;
|
||||
import org.jeecg.modules.openapi.service.OpenApiRecordService;
|
||||
import org.jeecg.modules.openapi.entity.OpenApiLog;
|
||||
import org.jeecg.modules.openapi.service.OpenApiLogService;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
@ -18,47 +18,47 @@ import java.util.Arrays;
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/openapi/record")
|
||||
public class OpenApiRecordController extends JeecgController<OpenApiRecord, OpenApiRecordService> {
|
||||
public class OpenApiLogController extends JeecgController<OpenApiLog, OpenApiLogService> {
|
||||
|
||||
/**
|
||||
* 分页列表查询
|
||||
*
|
||||
* @param openApiRecord
|
||||
* @param OpenApiLog
|
||||
* @param pageNo
|
||||
* @param pageSize
|
||||
* @param req
|
||||
* @return
|
||||
*/
|
||||
@GetMapping(value = "/list")
|
||||
public Result<?> queryPageList(OpenApiRecord openApiRecord, @RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo,
|
||||
public Result<?> queryPageList(OpenApiLog OpenApiLog, @RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo,
|
||||
@RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize, HttpServletRequest req) {
|
||||
QueryWrapper<OpenApiRecord> queryWrapper = QueryGenerator.initQueryWrapper(openApiRecord, req.getParameterMap());
|
||||
Page<OpenApiRecord> page = new Page<>(pageNo, pageSize);
|
||||
IPage<OpenApiRecord> pageList = service.page(page, queryWrapper);
|
||||
QueryWrapper<OpenApiLog> queryWrapper = QueryGenerator.initQueryWrapper(OpenApiLog, req.getParameterMap());
|
||||
Page<OpenApiLog> page = new Page<>(pageNo, pageSize);
|
||||
IPage<OpenApiLog> pageList = service.page(page, queryWrapper);
|
||||
return Result.ok(pageList);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加
|
||||
*
|
||||
* @param openApiRecord
|
||||
* @param OpenApiLog
|
||||
* @return
|
||||
*/
|
||||
@PostMapping(value = "/add")
|
||||
public Result<?> add(@RequestBody OpenApiRecord openApiRecord) {
|
||||
service.save(openApiRecord);
|
||||
public Result<?> add(@RequestBody OpenApiLog OpenApiLog) {
|
||||
service.save(OpenApiLog);
|
||||
return Result.ok("添加成功!");
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑
|
||||
*
|
||||
* @param openApiRecord
|
||||
* @param OpenApiLog
|
||||
* @return
|
||||
*/
|
||||
@PutMapping(value = "/edit")
|
||||
public Result<?> edit(@RequestBody OpenApiRecord openApiRecord) {
|
||||
service.updateById(openApiRecord);
|
||||
public Result<?> edit(@RequestBody OpenApiLog OpenApiLog) {
|
||||
service.updateById(OpenApiLog);
|
||||
return Result.ok("修改成功!");
|
||||
|
||||
}
|
||||
@ -96,7 +96,7 @@ public class OpenApiRecordController extends JeecgController<OpenApiRecord, Open
|
||||
*/
|
||||
@GetMapping(value = "/queryById")
|
||||
public Result<?> queryById(@RequestParam(name = "id", required = true) String id) {
|
||||
OpenApiRecord openApiRecord = service.getById(id);
|
||||
return Result.ok(openApiRecord);
|
||||
OpenApiLog OpenApiLog = service.getById(id);
|
||||
return Result.ok(OpenApiLog);
|
||||
}
|
||||
}
|
||||
@ -4,10 +4,7 @@ import org.jeecg.common.api.vo.Result;
|
||||
import org.jeecg.common.system.base.controller.JeecgController;
|
||||
import org.jeecg.modules.openapi.entity.OpenApiPermission;
|
||||
import org.jeecg.modules.openapi.service.OpenApiPermissionService;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/openapi/permission")
|
||||
@ -15,6 +12,11 @@ public class OpenApiPermissionController extends JeecgController<OpenApiPermissi
|
||||
|
||||
@PostMapping("add")
|
||||
public Result add(@RequestBody OpenApiPermission openApiPermission) {
|
||||
return Result.ok(service.save(openApiPermission));
|
||||
service.add(openApiPermission);
|
||||
return Result.ok("保存成功");
|
||||
}
|
||||
@GetMapping("/getOpenApi")
|
||||
public Result<?> getOpenApi( String apiAuthId) {
|
||||
return service.getOpenApi(apiAuthId);
|
||||
}
|
||||
}
|
||||
|
||||
@ -47,18 +47,15 @@ public class OpenApi implements Serializable {
|
||||
* IP 黑名单
|
||||
*/
|
||||
private String blackList;
|
||||
|
||||
/**
|
||||
* 请求头列表
|
||||
* 请求头json
|
||||
*/
|
||||
@TableField(exist = false)
|
||||
private List<OpenApiHeader> headers;
|
||||
|
||||
private String headersJson;
|
||||
/**
|
||||
* 请求参数列表
|
||||
* 请求参数json
|
||||
*/
|
||||
@TableField(exist = false)
|
||||
private List<OpenApiParam> params;
|
||||
private String paramsJson;
|
||||
|
||||
|
||||
/**
|
||||
* 目前仅支持json
|
||||
@ -100,4 +97,9 @@ public class OpenApi implements Serializable {
|
||||
* 更新时间
|
||||
*/
|
||||
private Date updateTime;
|
||||
/**
|
||||
* 历史已选接口
|
||||
*/
|
||||
@TableField(exist = false)
|
||||
private String ifCheckBox = "0";
|
||||
}
|
||||
|
||||
@ -5,6 +5,7 @@ import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.experimental.Accessors;
|
||||
import org.jeecg.common.aspect.annotation.Dict;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
@ -44,6 +45,7 @@ public class OpenApiAuth implements Serializable {
|
||||
/**
|
||||
* 系统用户ID
|
||||
*/
|
||||
@Dict(dictTable = "sys_user",dicCode = "id",dicText = "username")
|
||||
private String systemUserId;
|
||||
|
||||
/**
|
||||
|
||||
@ -18,16 +18,6 @@ import java.io.Serializable;
|
||||
public class OpenApiHeader implements Serializable {
|
||||
private static final long serialVersionUID = 5032708503120184683L;
|
||||
|
||||
/**
|
||||
* id
|
||||
*/
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
private String id;
|
||||
|
||||
/**
|
||||
* 接口ID
|
||||
*/
|
||||
private String apiId;
|
||||
|
||||
/**
|
||||
* key
|
||||
|
||||
@ -16,7 +16,7 @@ import java.util.Date;
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
@Accessors(chain = true)
|
||||
public class OpenApiRecord implements Serializable {
|
||||
public class OpenApiLog implements Serializable {
|
||||
private static final long serialVersionUID = -5870384488947863579L;
|
||||
|
||||
/**
|
||||
@ -17,16 +17,6 @@ import java.io.Serializable;
|
||||
@Accessors(chain = true)
|
||||
public class OpenApiParam implements Serializable {
|
||||
private static final long serialVersionUID = -6174831468578022357L;
|
||||
/**
|
||||
* id
|
||||
*/
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
private String id;
|
||||
|
||||
/**
|
||||
* 接口ID
|
||||
*/
|
||||
private String apiId;
|
||||
|
||||
/**
|
||||
* key
|
||||
|
||||
@ -4,11 +4,11 @@ import lombok.extern.slf4j.Slf4j;
|
||||
import org.jeecg.common.exception.JeecgBootException;
|
||||
import org.jeecg.modules.openapi.entity.OpenApi;
|
||||
import org.jeecg.modules.openapi.entity.OpenApiAuth;
|
||||
import org.jeecg.modules.openapi.entity.OpenApiLog;
|
||||
import org.jeecg.modules.openapi.entity.OpenApiPermission;
|
||||
import org.jeecg.modules.openapi.entity.OpenApiRecord;
|
||||
import org.jeecg.modules.openapi.service.OpenApiAuthService;
|
||||
import org.jeecg.modules.openapi.service.OpenApiLogService;
|
||||
import org.jeecg.modules.openapi.service.OpenApiPermissionService;
|
||||
import org.jeecg.modules.openapi.service.OpenApiRecordService;
|
||||
import org.jeecg.modules.openapi.service.OpenApiService;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.context.WebApplicationContext;
|
||||
@ -27,7 +27,7 @@ import java.util.List;
|
||||
@Slf4j
|
||||
public class ApiAuthFilter implements Filter {
|
||||
|
||||
private OpenApiRecordService openApiRecordService;
|
||||
private OpenApiLogService openApiLogService;
|
||||
private OpenApiAuthService openApiAuthService;
|
||||
private OpenApiPermissionService openApiPermissionService;
|
||||
private OpenApiService openApiService;
|
||||
@ -61,13 +61,13 @@ public class ApiAuthFilter implements Filter {
|
||||
filterChain.doFilter(servletRequest, servletResponse);
|
||||
long endTime = System.currentTimeMillis();
|
||||
|
||||
OpenApiRecord record = new OpenApiRecord();
|
||||
record.setApiId(openApi.getId());
|
||||
record.setCallAuthId(openApiAuth.getId());
|
||||
record.setCallTime(callTime);
|
||||
record.setUsedTime(endTime - startTime);
|
||||
record.setResponseTime(new Date());
|
||||
openApiRecordService.save(record);
|
||||
OpenApiLog openApiLog = new OpenApiLog();
|
||||
openApiLog.setApiId(openApi.getId());
|
||||
openApiLog.setCallAuthId(openApiAuth.getId());
|
||||
openApiLog.setCallTime(callTime);
|
||||
openApiLog.setUsedTime(endTime - startTime);
|
||||
openApiLog.setResponseTime(new Date());
|
||||
openApiLogService.save(openApiLog);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -75,7 +75,7 @@ public class ApiAuthFilter implements Filter {
|
||||
ServletContext servletContext = filterConfig.getServletContext();
|
||||
WebApplicationContext applicationContext = (WebApplicationContext)servletContext.getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE);
|
||||
this.openApiService = applicationContext.getBean(OpenApiService.class);
|
||||
this.openApiRecordService = applicationContext.getBean(OpenApiRecordService.class);
|
||||
this.openApiLogService = applicationContext.getBean(OpenApiLogService.class);
|
||||
this.openApiAuthService = applicationContext.getBean(OpenApiAuthService.class);
|
||||
this.openApiPermissionService = applicationContext.getBean(OpenApiPermissionService.class);
|
||||
}
|
||||
|
||||
@ -1,12 +0,0 @@
|
||||
package org.jeecg.modules.openapi.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.jeecg.modules.openapi.entity.OpenApiHeader;
|
||||
|
||||
/**
|
||||
* @date 2024/12/10 14:47
|
||||
*/
|
||||
@Mapper
|
||||
public interface OpenApiHeaderMapper extends BaseMapper<OpenApiHeader> {
|
||||
}
|
||||
@ -2,11 +2,11 @@ package org.jeecg.modules.openapi.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.jeecg.modules.openapi.entity.OpenApiRecord;
|
||||
import org.jeecg.modules.openapi.entity.OpenApiLog;
|
||||
|
||||
/**
|
||||
* @date 2024/12/10 9:50
|
||||
*/
|
||||
@Mapper
|
||||
public interface OpenApiRecordMapper extends BaseMapper<OpenApiRecord> {
|
||||
public interface OpenApiLogMapper extends BaseMapper<OpenApiLog> {
|
||||
}
|
||||
@ -1,12 +0,0 @@
|
||||
package org.jeecg.modules.openapi.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.jeecg.modules.openapi.entity.OpenApiParam;
|
||||
|
||||
/**
|
||||
* @date 2024/12/10 14:48
|
||||
*/
|
||||
@Mapper
|
||||
public interface OpenApiParamMapper extends BaseMapper<OpenApiParam> {
|
||||
}
|
||||
@ -1,16 +0,0 @@
|
||||
package org.jeecg.modules.openapi.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import org.jeecg.modules.openapi.entity.OpenApiHeader;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @date 2024/12/10 14:48
|
||||
*/
|
||||
public interface OpenApiHeaderService extends IService<OpenApiHeader> {
|
||||
|
||||
boolean deleteByApiId(String apiId);
|
||||
|
||||
List<OpenApiHeader> findByApiId(String apiId);
|
||||
}
|
||||
@ -1,10 +1,10 @@
|
||||
package org.jeecg.modules.openapi.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import org.jeecg.modules.openapi.entity.OpenApiRecord;
|
||||
import org.jeecg.modules.openapi.entity.OpenApiLog;
|
||||
|
||||
/**
|
||||
* @date 2024/12/10 9:51
|
||||
*/
|
||||
public interface OpenApiRecordService extends IService<OpenApiRecord> {
|
||||
public interface OpenApiLogService extends IService<OpenApiLog> {
|
||||
}
|
||||
@ -1,15 +0,0 @@
|
||||
package org.jeecg.modules.openapi.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import org.jeecg.modules.openapi.entity.OpenApiParam;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @date 2024/12/10 14:49
|
||||
*/
|
||||
public interface OpenApiParamService extends IService<OpenApiParam> {
|
||||
boolean deleteByApiId(String apiId);
|
||||
|
||||
List<OpenApiParam> findByApiId(String apiId);
|
||||
}
|
||||
@ -1,6 +1,8 @@
|
||||
package org.jeecg.modules.openapi.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import org.jeecg.common.api.vo.Result;
|
||||
import org.jeecg.modules.openapi.entity.OpenApi;
|
||||
import org.jeecg.modules.openapi.entity.OpenApiPermission;
|
||||
|
||||
import java.util.List;
|
||||
@ -10,4 +12,8 @@ import java.util.List;
|
||||
*/
|
||||
public interface OpenApiPermissionService extends IService<OpenApiPermission> {
|
||||
List<OpenApiPermission> findByAuthId(String authId);
|
||||
|
||||
Result<?> getOpenApi(String apiAuthId);
|
||||
|
||||
void add(OpenApiPermission openApiPermission);
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user