mirror of
https://github.com/jeecgboot/JeecgBoot.git
synced 2026-01-23 11:36:55 +08:00
Compare commits
244 Commits
v3.7.4
...
v3.8.0last
| Author | SHA1 | Date | |
|---|---|---|---|
| 5972c74b43 | |||
| d69cb121fc | |||
| 10a9edd10b | |||
| c71ff3fbcc | |||
| 08612d5bfa | |||
| 2ecce8f02d | |||
| 62937f14fb | |||
| d6ccc4a326 | |||
| 1893108136 | |||
| 7980915bdc | |||
| 550997268b | |||
| 9e7d40a080 | |||
| 2c38db456b | |||
| e52538d304 | |||
| e91cbd5cd8 | |||
| 70cec8b5c6 | |||
| d2365088ce | |||
| a679571a5a | |||
| b9c74e549f | |||
| 81c1724016 | |||
| 56d59eb589 | |||
| a00fcae3a3 | |||
| 9aea5de668 | |||
| 8979dd7ae9 | |||
| a56bd05389 | |||
| 9cf3328ea4 | |||
| 286d10a50f | |||
| 37c593e1d4 | |||
| 68f36cb1e5 | |||
| 78454d3434 | |||
| 56fbc2ed8f | |||
| 197d7adaaf | |||
| 49ba40e98a | |||
| e952518d71 | |||
| 0e184eaa64 | |||
| 86a3ed9dae | |||
| 1e259c805e | |||
| 8a82141c95 | |||
| 94bff11eb1 | |||
| 590d73dfe3 | |||
| fe9630d15c | |||
| 77ae25b86a | |||
| 3b34276cf8 | |||
| cffba084fc | |||
| d1589acc41 | |||
| 888a032266 | |||
| 0d18e536f0 | |||
| 21392c44f8 | |||
| 2730d8e06f | |||
| 0002606d41 | |||
| 8bd19484ee | |||
| 5d2db92613 | |||
| c1b39d21dd | |||
| e83c9b8190 | |||
| 309c76d268 | |||
| 9bd03f467d | |||
| f78eabfc66 | |||
| e032591366 | |||
| de767e07b4 | |||
| b77d3e36ab | |||
| 7885aaed3b | |||
| 6f4c2eb77c | |||
| 3f0597a0f6 | |||
| 04a3764f00 | |||
| 68464109de | |||
| 79866c5823 | |||
| d64b8ecaef | |||
| ec9f2b146a | |||
| 69fd2888a1 | |||
| 748331d649 | |||
| cb1d8e3527 | |||
| a89b299a4b | |||
| 7a15bfc161 | |||
| 65f7eb9542 | |||
| 92808f9164 | |||
| d3ed3f49d3 | |||
| da3d39c59c | |||
| f332758179 | |||
| 61a8904e52 | |||
| b70e709e53 | |||
| b032a415aa | |||
| 81821eeddc | |||
| 5288b1fe73 | |||
| 408f192b37 | |||
| 6a9f188282 | |||
| 168f15e1c2 | |||
| ae814a7e8b | |||
| 2ba17648c4 | |||
| 7160ea32cb | |||
| 19fc610ef5 | |||
| 44146f073e | |||
| 36caab37e2 | |||
| 8abce5ad9c | |||
| 6e721e4120 | |||
| 447e439612 | |||
| 088c79238e | |||
| ad72c807f6 | |||
| db3d95e1a7 | |||
| 1a16b5550f | |||
| c6fe809013 | |||
| 677b57ae09 | |||
| 02f21de8d5 | |||
| f7ca26fff0 | |||
| 6c6aa964e8 | |||
| b0bab050dd | |||
| 87197be8f7 | |||
| 75aa1fe5a0 | |||
| 3e434ce6b4 | |||
| e07508d29f | |||
| e18e980892 | |||
| a2f18fd0d9 | |||
| 9ad7ef5ab4 | |||
| 27a7046465 | |||
| 47a2a6fbac | |||
| 83b1c8692e | |||
| 73a5f64d7e | |||
| 1d18a54b8a | |||
| a8bf090352 | |||
| b6c9f9db68 | |||
| 60eeef14c7 | |||
| 25b30153a0 | |||
| beff2a271e | |||
| e166d916da | |||
| b79ba97614 | |||
| 4f371672d0 | |||
| 80ae183b58 | |||
| b4cac11368 | |||
| 4936e140e9 | |||
| 9fd4c3b3d2 | |||
| 4f2f1d6265 | |||
| b878f6b6be | |||
| 507289ff6c | |||
| a17b403675 | |||
| 632fd72d79 | |||
| 15fc262675 | |||
| 6768d65e1e | |||
| 410ab7bcc3 | |||
| 174f1ae432 | |||
| eef2f7e269 | |||
| 6a0ec66d3d | |||
| 163b0b531f | |||
| d1af49a33f | |||
| 03265691e6 | |||
| de9cc2f30d | |||
| 26887959cd | |||
| 7e15e81218 | |||
| 8b0e0367c7 | |||
| 334f7dbb62 | |||
| e9ddd21286 | |||
| 458526075e | |||
| a1b55f0d40 | |||
| 2f0a3bcd87 | |||
| 30d3a9f17b | |||
| 03739f2837 | |||
| d9e8bd2bc8 | |||
| 81eef5a838 | |||
| f528f72903 | |||
| 918286c144 | |||
| 512234a804 | |||
| cacc59b8fd | |||
| c744633139 | |||
| 0e4d304878 | |||
| 17a8964487 | |||
| 8ac6989d2c | |||
| 402ab0ffc4 | |||
| 7778ede90e | |||
| 06144206df | |||
| 3d3b5850ad | |||
| 816eeb9225 | |||
| 0b42efbbbf | |||
| b8e0d4391d | |||
| 72b34d082b | |||
| 7112649a21 | |||
| fbc312c35d | |||
| b8162a4a6d | |||
| 28404d2fd3 | |||
| c92c9be49a | |||
| 58e85e0569 | |||
| 6fc34d8a39 | |||
| 790df934b5 | |||
| 8aee4011a2 | |||
| 6e0277c60a | |||
| e923654161 | |||
| 06b41ae479 | |||
| 11af85d87a | |||
| 4caff75cce | |||
| 811861a957 | |||
| 24623ba4b0 | |||
| 7c68b46943 | |||
| 7c34161369 | |||
| bc52aa918d | |||
| 9dfdd47b36 | |||
| 272a7540eb | |||
| ad796f079f | |||
| e7e7716d05 | |||
| c5d620d2b2 | |||
| cdea05ebb0 | |||
| ca9a433f3c | |||
| 2be6052cd4 | |||
| 68ed67ee49 | |||
| d5903ba52a | |||
| 3ee635eddf | |||
| 21bc68fb53 | |||
| f532e57862 | |||
| da08adbea1 | |||
| 46e3e62b59 | |||
| 3656264f8a | |||
| 3361d48cd4 | |||
| ed86ea3da1 | |||
| 3deb0e5487 | |||
| 9e4792941e | |||
| b5fd5fe782 | |||
| 33c0104a02 | |||
| 81ed5100af | |||
| 87f9dc0064 | |||
| b311fedc6b | |||
| e321a0405f | |||
| d8bc74794d | |||
| 732f05dc74 | |||
| 6ce92798c6 | |||
| f4454e9348 | |||
| d9134ae0c8 | |||
| 25180e41c8 | |||
| a99e3f2268 | |||
| d27c354bf1 | |||
| d818b1dd9d | |||
| bcdbec0091 | |||
| 098bb12b9e | |||
| 4a6c750b19 | |||
| d396e5304a | |||
| 9bed25be8c | |||
| 7109b42092 | |||
| 1667b14194 | |||
| e9514873d2 | |||
| 0ee090664e | |||
| 4a9eda4ab0 | |||
| 2416c8b251 | |||
| 5b056f9dd6 | |||
| a93998dc56 | |||
| 268c27a782 | |||
| 23ace2712a | |||
| 157feeb925 | |||
| 4e25d4162f | |||
| 47a68f31e1 |
45
README-AI.md
45
README-AI.md
@ -1,21 +1,22 @@
|
|||||||
AIGC应用平台介绍
|
AIGC应用平台介绍
|
||||||
===============
|
===============
|
||||||
|
|
||||||
即将发布:`最新版本 V3.8.0发布,提供Jeecg AIGC 提供AI应用平台+知识库问答`
|
一个全栈式 AI 开发平台,旨在帮助开发者快速构建和部署个性化的 AI 应用。
|
||||||
|
|
||||||
|
> JDK说明:AI流程编排引擎暂时不支持jdk21,所以目前只能使用jdk8或者jdk17启动项目。
|
||||||
|
|
||||||
|
|
||||||
> JeecgBoot 平台的AIGC功能模块,是一套类似`Dify`的`AIGC应用开发平台`+`知识库问答`,是一款基于LLM大语言模型AI应用平台和 RAG 的知识库问答系统。
|
JeecgBoot平台的AIGC功能模块,是一套类似`Dify`的`AIGC应用开发平台`+`知识库问答`,是一款基于LLM大语言模型AI应用平台和 RAG 的知识库问答系统。
|
||||||
其直观的界面结合了 AI 流程编排、RAG 管道、知识库管理、模型管理、对接向量库、实时运行可观察等,让您可以快速从原型到生产,拥有AI服务能力。
|
其直观的界面结合了 AI 流程编排、RAG 管道、知识库管理、模型管理、对接向量库、实时运行可观察等,让您可以快速从原型到生产,拥有AI服务能力。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### AI视频介绍
|
### AI视频介绍
|
||||||
|
|
||||||
[](https://www.bilibili.com/video/BV1zmd7YFE4w)
|
[](https://www.bilibili.com/video/BV1zmd7YFE4w)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#### Dify `VS` JEECG AI
|
#### Dify `VS` JEECG AI
|
||||||
|
|
||||||
> JEECG AI与Dify相比,在多个方面展现出显著的优势,特别是在文档处理、格式和图片保持方面。以下是一些具体的优点:
|
> JEECG AI与Dify相比,在多个方面展现出显著的优势,特别是在文档处理、格式和图片保持方面。以下是一些具体的优点:
|
||||||
@ -24,23 +25,9 @@ AIGC应用平台介绍
|
|||||||
> - 对话回复格式美观:
|
> - 对话回复格式美观:
|
||||||
> 在对话过程中,JEECG AI能够保持回复内容的原格式,也不丢失图片,使得输出的文章更加美观,不会出现格式错乱的情况,还支持图片的渲染。
|
> 在对话过程中,JEECG AI能够保持回复内容的原格式,也不丢失图片,使得输出的文章更加美观,不会出现格式错乱的情况,还支持图片的渲染。
|
||||||
> - PDF文档导入与格式转换:
|
> - PDF文档导入与格式转换:
|
||||||
> JEECG AI在处理PDF文档时,能够更好地保持原始格式和图片,确保转换后的内容与原始文档一致。这哥功能在许多AI产品中表现不佳,而JEECG AI在这方面做出了显著的优化
|
> JEECG AI在处理PDF文档时,能够更好地保持原始格式和图片,确保转换后的内容与原始文档一致。这个功能在许多AI产品中表现不佳,而JEECG AI在这方面做出了显著的优化
|
||||||
|
|
||||||
|
|
||||||
| 功能 | Dify | Jeecg AI |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| AI工作流 | 有 | 有 |
|
|
||||||
| RAG 管道向量搜索 | 有 | 有 |
|
|
||||||
| AI模型管理 | 有 | 有 |
|
|
||||||
| AI应用管理 | 有 | 有 |
|
|
||||||
| AI知识库 | 有 | 有 |
|
|
||||||
| 产品方向 | 一款独立的 LLM 应用开发平台 | 低代码与AIGC应用二者结合的平台 |
|
|
||||||
| 业务集成 | 业务集成能力弱 | 更方便与业务系统集成,调用系统接口和逻辑更加方便 |
|
|
||||||
| AI业务流 | 侧重AI逻辑流程 | AI流程编排作为低代码的业务引擎,用户可以通过AI流程配置各种业务流和AI流程 |
|
|
||||||
| 上传markdown文档库(支持图片) | 不支持 | 支持 |
|
|
||||||
| AI对话支持发图和展示图片 | 支持 | 支持 |
|
|
||||||
| 实现语言 | python + react | JAVA + vue3 |
|
|
||||||
|
|
||||||
| 功能 | Dify | Jeecg AI |
|
| 功能 | Dify | Jeecg AI |
|
||||||
|------------|------------------|-----------------------------------------|
|
|------------|------------------|-----------------------------------------|
|
||||||
| AI工作流 | 有 | 有 |
|
| AI工作流 | 有 | 有 |
|
||||||
@ -57,6 +44,13 @@ AIGC应用平台介绍
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### 安装向量库 pgvector
|
||||||
|
|
||||||
|
- https://help.jeecg.com/aigc/config
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## 功能特点
|
## 功能特点
|
||||||
|
|
||||||
- AI流程: 提供强大的AI流程设计器引擎,支持编排 AI 工作过程,满足复杂业务场景,支持画布上构建和实时运行查看 AI流程运行情况。
|
- AI流程: 提供强大的AI流程设计器引擎,支持编排 AI 工作过程,满足复杂业务场景,支持画布上构建和实时运行查看 AI流程运行情况。
|
||||||
@ -69,10 +63,17 @@ 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群:716488839
|
||||||
|
|
||||||
|
|
||||||
## 功能列表
|
## 功能列表
|
||||||
|
|||||||
@ -7,12 +7,12 @@
|
|||||||
JEECG BOOT AI Low Code Platform
|
JEECG BOOT AI Low Code Platform
|
||||||
===============
|
===============
|
||||||
|
|
||||||
Current version: 3.7.4 (Release date: 2025-04-07)
|
Current version: 3.8.0 (Release date: 2025-04-18)
|
||||||
|
|
||||||
|
|
||||||
[](https://github.com/zhangdaiscott/jeecg-boot/blob/master/LICENSE)
|
[](https://github.com/zhangdaiscott/jeecg-boot/blob/master/LICENSE)
|
||||||
[](http://www.jeecg.com)
|
[](http://www.jeecg.com)
|
||||||
[](https://github.com/zhangdaiscott/jeecg-boot)
|
[](https://github.com/zhangdaiscott/jeecg-boot)
|
||||||
[](https://github.com/zhangdaiscott/jeecg-boot)
|
[](https://github.com/zhangdaiscott/jeecg-boot)
|
||||||
[](https://github.com/zhangdaiscott/jeecg-boot)
|
[](https://github.com/zhangdaiscott/jeecg-boot)
|
||||||
|
|
||||||
@ -21,7 +21,7 @@ Current version: 3.7.4 (Release date: 2025-04-07)
|
|||||||
Project introduction
|
Project introduction
|
||||||
-----------------------------------
|
-----------------------------------
|
||||||
|
|
||||||
<h3 align="center">Java AI Low Code Platform for Enterprise web applications</h3>
|
<h3 align="center">Java AI Low Code Platform</h3>
|
||||||
|
|
||||||
JeecgBoot is a `AI low code platform` based on code `generators`! Front and back end separation architecture SpringBoot2.x, SpringCloud, Ant Design&Vue, Mybatis plus, Shiro, JWT, support for microservices. The powerful code generator makes the front and back end of the code generation, low code development! JeecgBoot leads a new low-code development paradigm (OnlineCoding-> Code Generator -> Manual MERGE) that helps resolve 70% of the duplication in Java projects and makes development more business-focused. Not only can quickly improve efficiency, save research and development costs, but also do not lose flexibility!
|
JeecgBoot is a `AI low code platform` based on code `generators`! Front and back end separation architecture SpringBoot2.x, SpringCloud, Ant Design&Vue, Mybatis plus, Shiro, JWT, support for microservices. The powerful code generator makes the front and back end of the code generation, low code development! JeecgBoot leads a new low-code development paradigm (OnlineCoding-> Code Generator -> Manual MERGE) that helps resolve 70% of the duplication in Java projects and makes development more business-focused. Not only can quickly improve efficiency, save research and development costs, but also do not lose flexibility!
|
||||||
|
|
||||||
|
|||||||
50
README.md
50
README.md
@ -2,12 +2,12 @@
|
|||||||
JeecgBoot AI低代码平台
|
JeecgBoot AI低代码平台
|
||||||
===============
|
===============
|
||||||
|
|
||||||
当前最新版本: 3.7.4(发布日期:2025-04-07)
|
当前最新版本: 3.8.0(发布日期:2025-04-18)
|
||||||
|
|
||||||
|
|
||||||
[](https://github.com/jeecgboot/JeecgBoot/blob/master/LICENSE)
|
[](https://github.com/jeecgboot/JeecgBoot/blob/master/LICENSE)
|
||||||
[](http://guojusoft.com)
|
[](http://guojusoft.com)
|
||||||
[](https://github.com/jeecgboot/JeecgBoot)
|
[](https://github.com/jeecgboot/JeecgBoot)
|
||||||
[](https://github.com/jeecgboot/JeecgBoot)
|
[](https://github.com/jeecgboot/JeecgBoot)
|
||||||
[](https://github.com/jeecgboot/JeecgBoot)
|
[](https://github.com/jeecgboot/JeecgBoot)
|
||||||
|
|
||||||
@ -16,18 +16,22 @@ JeecgBoot AI低代码平台
|
|||||||
项目介绍
|
项目介绍
|
||||||
-----------------------------------
|
-----------------------------------
|
||||||
|
|
||||||
<h3 align="center">Java AI Low Code Platform for Enterprise web applications</h3>
|
<h3 align="center">Java AI Low Code Platform</h3>
|
||||||
|
|
||||||
JeecgBoot 是一款基于`BPM`和`代码生成器`的 AI低代码平台!前后端分离架构 SpringBoot2.x/3.x,SpringCloud,Ant Design Vue3,Mybatis-plus,Shiro,JWT,支持微服务、多租户;支持 AI 大模型 DeepSeek 和 ChatGPT、Ollama本地模型; 强大的代码生成器让前后端代码一键生成,无需写任何代码! JeecgBoot 引领 AI 低代码开发模式(AI生成-> OnlineCoding-> 代码生成器-> 手工MERGE), 帮助解决Java项目80%的重复工作,让开发更多关注业务。既能快速提高效率,节省成本,同时又不失灵活性!AIGC能力:AI对话助手、AI建表、AI写文章、AI流程编排、AI知识库问答等等.
|
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能力` `低代码模块`,实现在线开发`真正的零代码`:Online表单开发、Online报表、报表配置能力、在线图表设计、仪表盘设计、大屏设计、移动配置能力、表单设计器、在线设计流程、流程自动化配置、插件能力(可插拔)、AI对话助手,AI建表、AI写文章、AI流程编排、AI知识库问答、AI赋能低代码等等!
|
|
||||||
|
|
||||||
|
JeecgBoot 提供了一系列 `低代码能力`,实现`真正的零代码`在线开发:Online表单开发、Online报表、复杂报表设计、打印设计、在线图表设计、仪表盘设计、大屏设计、移动图表能力、表单设计器、在线设计流程、流程自动化配置、插件能力(可插拔)
|
||||||
|
|
||||||
|
`AI赋能低代码:` 目前提供了AI应用、AI模型管理、AI流程编排、AI对话助手,AI建表、AI写文章、AI知识库问答、AI字段建议等功能;支持各种AI大模型ChatGPT、DeepSeek、Ollama、智普、千问等.
|
||||||
|
|
||||||
`JEECG宗旨是:` 简单功能由OnlineCoding配置实现,做到`零代码开发`;复杂功能由代码生成器生成进行手工Merge 实现`低代码开发`,既保证了`智能`又兼顾`灵活`;实现了低代码开发的同时又支持灵活编码,解决了当前低代码产品普遍不灵活的弊端!
|
`JEECG宗旨是:` 简单功能由OnlineCoding配置实现,做到`零代码开发`;复杂功能由代码生成器生成进行手工Merge 实现`低代码开发`,既保证了`智能`又兼顾`灵活`;实现了低代码开发的同时又支持灵活编码,解决了当前低代码产品普遍不灵活的弊端!
|
||||||
|
|
||||||
`JEECG业务流程:` 采用工作流来实现、扩展出任务接口,供开发编写业务逻辑,表单提供多种解决方案: 表单设计器、online配置表单、编码表单。同时实现了流程与表单的分离设计(松耦合)、并支持任务节点灵活配置,既保证了公司流程的保密性,又减少了开发人员的工作量。
|
`JEECG业务流程:` 采用工作流来实现、扩展出任务接口,供开发编写业务逻辑,表单提供多种解决方案: 表单设计器、online配置表单、编码表单。同时实现了流程与表单的分离设计(松耦合)、并支持任务节点灵活配置,既保证了公司流程的保密性,又减少了开发人员的工作量。
|
||||||
|
|
||||||
`AI赋能低代码:` 目前JeecgBoot支持AI大模型`ChatGPT`和`DeepSeek`,现在最新版默认使用`DeepSeek`,速度更快质量更高。目前提供了AI对话助手、AI建表、AI报表、AI写文章、AI流程编排、AI知识库问答等功能。
|
|
||||||
|
|
||||||
|
|
||||||
### 视频介绍
|
### 视频介绍
|
||||||
@ -37,7 +41,19 @@ JeecgBoot 提供了一系列 `AI能力` `低代码模块`,实现在线开发`
|
|||||||
|
|
||||||
适用项目
|
适用项目
|
||||||
-----------------------------------
|
-----------------------------------
|
||||||
JeecgBoot AI低代码平台,可以应用在任何J2EE项目的开发中,支持信创国产化(默认适配达梦和人大金仓)。尤其适合SAAS项目、企业信息管理系统(MIS)、内部办公系统(OA)、企业资源计划系统(ERP)、客户关系管理系统(CRM)等,其半智能手工Merge的开发方式,可以显著提高开发效率70%以上,极大降低开发成本。
|
JeecgBoot AI低代码平台,可以应用在任何J2EE项目的开发中,支持信创国产化。尤其适合SAAS项目、企业信息管理系统(MIS)、内部办公系统(OA)、企业资源计划系统(ERP)、客户关系管理系统(CRM)等,其半智能手工Merge的开发方式,可以显著提高开发效率70%以上,极大降低开发成本。
|
||||||
|
又是一个全栈式 AI 开发平台,快速帮助企业构建和部署个性化的 AI 应用。
|
||||||
|
|
||||||
|
|
||||||
|
信创国产化
|
||||||
|
-----------------------------------
|
||||||
|
JeecgBoot 是一个开源低代码开发平台,支持全信创环境。它兼容多种国产操作系统和数据库,包括:
|
||||||
|
|
||||||
|
- 操作系统:国产麒麟、银河麒麟等国产系统几乎都是基于 Linux 内核,因此它们具有良好的兼容性。
|
||||||
|
- 数据库:达梦、人大金仓、TiDB , [转库文档](https://my.oschina.net/jeecg/blog/4905722)
|
||||||
|
- 中间件:东方通 TongWeb、TongRDS,宝兰德 AppServer、CacheDB, [信创配置文档](https://help.jeecg.com/java/tongweb-deploy/)
|
||||||
|
|
||||||
|
通过这些适配,JeecgBoot 为使用国产软件和硬件的用户提供了高效的开发解决方案。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -74,11 +90,14 @@ JeecgBoot AI低代码平台,可以应用在任何J2EE项目的开发中,支
|
|||||||
AIGC应用平台介绍
|
AIGC应用平台介绍
|
||||||
-----------------------------------
|
-----------------------------------
|
||||||
|
|
||||||
> JeecgBoot 平台的AIGC功能模块,是一套类似`Dify`的`AIGC应用开发平台`+`知识库问答`,是一款基于LLM大语言模型AI应用平台和 RAG 的知识库问答系统。
|
JeecgBoot 平台的AIGC功能模块,是一套类似`Dify`的`AIGC应用开发平台`+`知识库问答`,是一款基于LLM大语言模型AI应用平台和 RAG 的知识库问答系统。
|
||||||
其直观的界面结合了 AI 流程编排、RAG 管道、知识库管理、模型管理、对接向量库、实时运行可观察等,让您可以快速从原型到生产,拥有AI服务能力。
|
其直观的界面结合了 AI 流程编排、RAG 管道、知识库管理、模型管理、对接向量库、实时运行可观察等,让您可以快速从原型到生产,拥有AI服务能力。
|
||||||
|
|
||||||
|
> JDK说明:AI流程编排引擎暂时不支持jdk21,所以目前只能使用jdk8或者jdk17启动项目。
|
||||||
|
|
||||||
- [AIGC专题介绍页](README-AI.md)
|
- [AIGC专题介绍页](README-AI.md)
|
||||||
- [AIGC开发文档](https://help.jeecg.com/aigc)
|
- [AIGC开发文档](https://help.jeecg.com/aigc)
|
||||||
|
- [配置向量库PGVector](https://help.jeecg.com/aigc/config)
|
||||||
|
|
||||||
|
|
||||||
##### AI视频介绍
|
##### AI视频介绍
|
||||||
@ -86,6 +105,11 @@ AIGC应用平台介绍
|
|||||||
[](https://www.bilibili.com/video/BV1zmd7YFE4w)
|
[](https://www.bilibili.com/video/BV1zmd7YFE4w)
|
||||||
|
|
||||||
|
|
||||||
|
##### 在线体验
|
||||||
|
|
||||||
|
- JeecgBoot演示: https://boot3.jeecg.com
|
||||||
|
- 敲敲云在线搭建AI知识库:https://app.qiaoqiaoyun.com
|
||||||
|
|
||||||
##### Dify `VS` JEECG AI
|
##### Dify `VS` JEECG AI
|
||||||
|
|
||||||
> JEECG AI与Dify相比,在多个方面展现出显著的优势,特别是在文档处理、格式和图片保持方面。以下是一些具体的优点:
|
> JEECG AI与Dify相比,在多个方面展现出显著的优势,特别是在文档处理、格式和图片保持方面。以下是一些具体的优点:
|
||||||
@ -94,7 +118,7 @@ AIGC应用平台介绍
|
|||||||
> - 对话回复格式美观:
|
> - 对话回复格式美观:
|
||||||
> 在对话过程中,JEECG AI能够保持回复内容的原格式,也不丢失图片,使得输出的文章更加美观,不会出现格式错乱的情况,还支持图片的渲染。
|
> 在对话过程中,JEECG AI能够保持回复内容的原格式,也不丢失图片,使得输出的文章更加美观,不会出现格式错乱的情况,还支持图片的渲染。
|
||||||
> - PDF文档导入与格式转换:
|
> - PDF文档导入与格式转换:
|
||||||
> JEECG AI在处理PDF文档时,能够更好地保持原始格式和图片,确保转换后的内容与原始文档一致。这哥功能在许多AI产品中表现不佳,而JEECG AI在这方面做出了显著的优化
|
> JEECG AI在处理PDF文档时,能够更好地保持原始格式和图片,确保转换后的内容与原始文档一致。这个功能在许多AI产品中表现不佳,而JEECG AI在这方面做出了显著的优化
|
||||||
|
|
||||||
##### 功能大模块
|
##### 功能大模块
|
||||||
|
|
||||||
@ -285,10 +309,10 @@ AIGC应用平台介绍
|
|||||||
│ ├─AI对话助手
|
│ ├─AI对话助手
|
||||||
│ ├─AI建表
|
│ ├─AI建表
|
||||||
│ ├─AI写文章
|
│ ├─AI写文章
|
||||||
│ ├─AI流程编排(研发中)
|
│ ├─AI流程编排
|
||||||
│ ├─AI知识库问答系统(研发中)
|
│ ├─AI知识库问答系统
|
||||||
│ ├─AI应用开发平台(研发中)
|
│ ├─AI应用开发平台
|
||||||
│ ├─AI聊天窗口支持嵌入第三方(研发中)
|
│ ├─AI聊天窗口支持嵌入第三方
|
||||||
├─Online在线开发(低代码)
|
├─Online在线开发(低代码)
|
||||||
│ ├─Online在线表单
|
│ ├─Online在线表单
|
||||||
│ ├─Online代码生成器
|
│ ├─Online代码生成器
|
||||||
|
|||||||
@ -2,12 +2,12 @@
|
|||||||
JeecgBoot 低代码开发平台
|
JeecgBoot 低代码开发平台
|
||||||
===============
|
===============
|
||||||
|
|
||||||
当前最新版本: 3.7.4(发布日期:2025-04-10)
|
当前最新版本: 3.8.0(发布日期:2025-05-16)
|
||||||
|
|
||||||
|
|
||||||
[](https://github.com/zhangdaiscott/jeecg-boot/blob/master/LICENSE)
|
[](https://github.com/zhangdaiscott/jeecg-boot/blob/master/LICENSE)
|
||||||
[](http://jeecg.com/aboutusIndex)
|
[](http://jeecg.com/aboutusIndex)
|
||||||
[](https://github.com/zhangdaiscott/jeecg-boot)
|
[](https://github.com/zhangdaiscott/jeecg-boot)
|
||||||
[](https://github.com/zhangdaiscott/jeecg-boot)
|
[](https://github.com/zhangdaiscott/jeecg-boot)
|
||||||
[](https://github.com/zhangdaiscott/jeecg-boot)
|
[](https://github.com/zhangdaiscott/jeecg-boot)
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
170
jeecg-boot/db/基于AK和SK安全鉴权的OpenAPI/升级SQL脚本.sql
Normal file
170
jeecg-boot/db/基于AK和SK安全鉴权的OpenAPI/升级SQL脚本.sql
Normal file
@ -0,0 +1,170 @@
|
|||||||
|
/*
|
||||||
|
Navicat Premium Data Transfer
|
||||||
|
|
||||||
|
Source Server : mysql5.7
|
||||||
|
Source Server Type : MySQL
|
||||||
|
Source Server Version : 50738 (5.7.38)
|
||||||
|
Source Host : 127.0.0.1:3306
|
||||||
|
Source Schema : jeecg-boot
|
||||||
|
|
||||||
|
Target Server Type : MySQL
|
||||||
|
Target Server Version : 50738 (5.7.38)
|
||||||
|
File Encoding : 65001
|
||||||
|
|
||||||
|
Date: 15/05/2025 10:18:36
|
||||||
|
*/
|
||||||
|
|
||||||
|
SET NAMES utf8mb4;
|
||||||
|
SET FOREIGN_KEY_CHECKS = 0;
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Table structure for open_api
|
||||||
|
-- ----------------------------
|
||||||
|
DROP TABLE IF EXISTS `open_api`;
|
||||||
|
CREATE TABLE `open_api` (
|
||||||
|
`id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
|
||||||
|
`name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '接口名称',
|
||||||
|
`request_method` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '请求方法',
|
||||||
|
`request_url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '接口地址',
|
||||||
|
`black_list` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT 'IP 黑名单',
|
||||||
|
`body` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '请求体内容',
|
||||||
|
`origin_url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '原始地址',
|
||||||
|
`status` int(10) NULL DEFAULT NULL COMMENT '状态',
|
||||||
|
`del_flag` int(10) NULL DEFAULT NULL COMMENT '删除标识',
|
||||||
|
`create_by` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '创建人',
|
||||||
|
`create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
|
||||||
|
`update_by` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '修改人',
|
||||||
|
`update_time` datetime NULL DEFAULT NULL COMMENT '修改时间',
|
||||||
|
`headers_json` json NULL COMMENT '请求头json',
|
||||||
|
`params_json` json NULL COMMENT '请求参数json',
|
||||||
|
PRIMARY KEY (`id`) USING BTREE
|
||||||
|
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '接口表' ROW_FORMAT = DYNAMIC;
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Records of open_api
|
||||||
|
-- ----------------------------
|
||||||
|
INSERT INTO `open_api` VALUES ('1922132683346649090', '根据部门查询用户', 'GET', 'TEwcXBlr', NULL, NULL, '/sys/user/queryUserByDepId', 1, 0, 'admin', '2025-05-13 11:31:58', 'admin', '2025-05-15 10:10:01', '[]', '[{\"id\": \"row_24\", \"note\": \"\", \"paramKey\": \"id\", \"required\": \"1\", \"defaultValue\": \"\"}]');
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Table structure for open_api_auth
|
||||||
|
-- ----------------------------
|
||||||
|
DROP TABLE IF EXISTS `open_api_auth`;
|
||||||
|
CREATE TABLE `open_api_auth` (
|
||||||
|
`id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
|
||||||
|
`name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '授权名称',
|
||||||
|
`ak` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT 'AK',
|
||||||
|
`sk` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT 'SK',
|
||||||
|
`create_by` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '创建人',
|
||||||
|
`create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
|
||||||
|
`update_by` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '修改人',
|
||||||
|
`update_time` datetime NULL DEFAULT NULL COMMENT '修改时间',
|
||||||
|
`system_user_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '关联系统用户名',
|
||||||
|
PRIMARY KEY (`id`) USING BTREE
|
||||||
|
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '权限表' ROW_FORMAT = DYNAMIC;
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Records of open_api_auth
|
||||||
|
-- ----------------------------
|
||||||
|
INSERT INTO `open_api_auth` VALUES ('1922164194775056386', 'scott', 'ak-pFjyNHWRsJEFWlu6', '4hV5dBrZtmGAtPdbA5yseaeKRYNpzGsS', 'admin', '2025-05-13 13:37:11', NULL, NULL, 'e9ca23d68d884d4ebb19d07889727dae');
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Table structure for open_api_log
|
||||||
|
-- ----------------------------
|
||||||
|
DROP TABLE IF EXISTS `open_api_log`;
|
||||||
|
CREATE TABLE `open_api_log` (
|
||||||
|
`id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
|
||||||
|
`api_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '接口ID',
|
||||||
|
`call_auth_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '调用ID',
|
||||||
|
`call_time` datetime NULL DEFAULT NULL COMMENT '调用时间',
|
||||||
|
`used_time` bigint(20) NULL DEFAULT NULL COMMENT '耗时',
|
||||||
|
`response_time` datetime NULL DEFAULT NULL COMMENT '响应时间',
|
||||||
|
PRIMARY KEY (`id`) USING BTREE
|
||||||
|
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '调用记录表' ROW_FORMAT = DYNAMIC;
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Records of open_api_log
|
||||||
|
-- ----------------------------
|
||||||
|
INSERT INTO `open_api_log` VALUES ('1922175238557913090', '1922132683346649090', '1922164194775056386', '2025-05-13 14:21:04', 94, '2025-05-13 14:21:04');
|
||||||
|
INSERT INTO `open_api_log` VALUES ('1922175436256432130', '1922132683346649090', '1922164194775056386', '2025-05-13 14:21:51', 38, '2025-05-13 14:21:51');
|
||||||
|
INSERT INTO `open_api_log` VALUES ('1922175487921868802', '1922132683346649090', '1922164194775056386', '2025-05-13 14:22:03', 31, '2025-05-13 14:22:03');
|
||||||
|
INSERT INTO `open_api_log` VALUES ('1922176033789562883', '1922132683346649090', '1922164194775056386', '2025-05-13 14:24:13', 27, '2025-05-13 14:24:13');
|
||||||
|
INSERT INTO `open_api_log` VALUES ('1922176583943835650', '1922132683346649090', '1922164194775056386', '2025-05-13 14:26:25', 39, '2025-05-13 14:26:25');
|
||||||
|
INSERT INTO `open_api_log` VALUES ('1922177249969934337', '1922132683346649090', '1922164194775056386', '2025-05-13 14:28:08', 55250, '2025-05-13 14:29:03');
|
||||||
|
INSERT INTO `open_api_log` VALUES ('1922180212645941249', '1922132683346649090', '1922164194775056386', '2025-05-13 14:40:46', 4162, '2025-05-13 14:40:50');
|
||||||
|
INSERT INTO `open_api_log` VALUES ('1922180441692688385', '1922132683346649090', '1922164194775056386', '2025-05-13 14:41:11', 33346, '2025-05-13 14:41:44');
|
||||||
|
INSERT INTO `open_api_log` VALUES ('1922180521686454273', '1922132683346649090', '1922164194775056386', '2025-05-13 14:42:00', 3570, '2025-05-13 14:42:03');
|
||||||
|
INSERT INTO `open_api_log` VALUES ('1922180965825499138', '1922132683346649090', '1922164194775056386', '2025-05-13 14:42:10', 99211, '2025-05-13 14:43:49');
|
||||||
|
INSERT INTO `open_api_log` VALUES ('1922181034515615746', '1922132683346649090', '1922164194775056386', '2025-05-13 14:43:52', 14005, '2025-05-13 14:44:06');
|
||||||
|
INSERT INTO `open_api_log` VALUES ('1922183171307982850', '1922132683346649090', '1922164194775056386', '2025-05-13 14:52:15', 19834, '2025-05-13 14:52:35');
|
||||||
|
INSERT INTO `open_api_log` VALUES ('1922184177068523521', '1922132683346649090', '1922164194775056386', '2025-05-13 14:56:34', 748, '2025-05-13 14:56:35');
|
||||||
|
INSERT INTO `open_api_log` VALUES ('1922184729043107841', '1922132683346649090', '1922164194775056386', '2025-05-13 14:58:46', 1031, '2025-05-13 14:58:47');
|
||||||
|
INSERT INTO `open_api_log` VALUES ('1922184806453182465', '1922132683346649090', '1922164194775056386', '2025-05-13 14:59:05', 68, '2025-05-13 14:59:05');
|
||||||
|
INSERT INTO `open_api_log` VALUES ('1922184918382379009', '1922132683346649090', '1922164194775056386', '2025-05-13 14:59:10', 22155, '2025-05-13 14:59:32');
|
||||||
|
INSERT INTO `open_api_log` VALUES ('1922185292635844610', '1922132683346649090', '1922164194775056386', '2025-05-13 15:00:55', 6267, '2025-05-13 15:01:01');
|
||||||
|
INSERT INTO `open_api_log` VALUES ('1922186002672791554', '1922132683346649090', '1922164194775056386', '2025-05-13 15:03:23', 27554, '2025-05-13 15:03:50');
|
||||||
|
INSERT INTO `open_api_log` VALUES ('1922187506582425601', '1922132683346649090', '1922164194775056386', '2025-05-13 15:09:45', 3464, '2025-05-13 15:09:49');
|
||||||
|
INSERT INTO `open_api_log` VALUES ('1922187586597163011', '1922132683346649090', '1922164194775056386', '2025-05-13 15:10:08', 82, '2025-05-13 15:10:08');
|
||||||
|
INSERT INTO `open_api_log` VALUES ('1922187924741951490', '1922132683346649090', '1922164194775056386', '2025-05-13 15:10:49', 39590, '2025-05-13 15:11:28');
|
||||||
|
INSERT INTO `open_api_log` VALUES ('1922188138710261761', '1922132683346649090', '1922164194775056386', '2025-05-13 15:12:19', 758, '2025-05-13 15:12:19');
|
||||||
|
INSERT INTO `open_api_log` VALUES ('1922188290661507073', '1922132683346649090', '1922164194775056386', '2025-05-13 15:12:29', 26527, '2025-05-13 15:12:56');
|
||||||
|
INSERT INTO `open_api_log` VALUES ('1922189701755424769', '1922132683346649090', '1922164194775056386', '2025-05-13 15:18:28', 3619, '2025-05-13 15:18:32');
|
||||||
|
INSERT INTO `open_api_log` VALUES ('1922190076784803841', '1922132683346649090', '1922164194775056386', '2025-05-13 15:20:01', 741, '2025-05-13 15:20:02');
|
||||||
|
INSERT INTO `open_api_log` VALUES ('1922836671113101313', '1922132683346649090', '1922164194775056386', '2025-05-15 10:09:21', 186, '2025-05-15 10:09:22');
|
||||||
|
INSERT INTO `open_api_log` VALUES ('1922836856287428610', '1922132683346649090', '1922164194775056386', '2025-05-15 10:10:06', 145, '2025-05-15 10:10:06');
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Table structure for open_api_permission
|
||||||
|
-- ----------------------------
|
||||||
|
DROP TABLE IF EXISTS `open_api_permission`;
|
||||||
|
CREATE TABLE `open_api_permission` (
|
||||||
|
`id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
|
||||||
|
`api_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '接口ID',
|
||||||
|
`api_auth_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '认证ID',
|
||||||
|
`create_by` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '创建人',
|
||||||
|
`create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
|
||||||
|
`update_by` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '更新人',
|
||||||
|
`update_time` datetime NULL DEFAULT NULL COMMENT '更新时间',
|
||||||
|
PRIMARY KEY (`id`) USING BTREE
|
||||||
|
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'openapi授权' ROW_FORMAT = DYNAMIC;
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Records of open_api_permission
|
||||||
|
-- ----------------------------
|
||||||
|
INSERT INTO `open_api_permission` VALUES ('1922164225875820545', '1922132683346649090', '1922164194775056386', 'admin', '2025-05-13 13:37:18', NULL, NULL);
|
||||||
|
|
||||||
|
SET FOREIGN_KEY_CHECKS = 1;
|
||||||
|
|
||||||
|
|
||||||
|
INSERT INTO sys_permission (id, parent_id, name, url, component, is_route, component_name, redirect, menu_type, perms, perms_type, sort_no, always_show, icon, is_leaf, keep_alive, hidden, hide_tab, description, create_by, create_time, update_by, update_time, del_flag, rule_flag, status, internal_or_external) VALUES ('1917957565728198657', '1922109301837606914', '接口文档', '/openapi/SwaggerUI', 'openapi/SwaggerUI', 1, '', null, 1, null, '0', 1, 0, null, 1, 0, 0, 0, null, 'admin', '2025-05-01 23:01:32', 'admin', '2025-05-13 09:59:46', 0, 0, null, 0);
|
||||||
|
INSERT INTO sys_permission (id, parent_id, name, url, component, is_route, component_name, redirect, menu_type, perms, perms_type, sort_no, always_show, icon, is_leaf, keep_alive, hidden, hide_tab, description, create_by, create_time, update_by, update_time, del_flag, rule_flag, status, internal_or_external) VALUES ('1922109301837606914', '', 'OpenApi管理', '/openapi', 'layouts/RouteView', 1, '', null, 0, null, '0', 12.1, 0, 'ant-design:swap-outlined', 0, 0, 0, 0, null, 'admin', '2025-05-13 09:59:03', 'admin', '2025-05-13 10:02:43', 0, 0, null, 0);
|
||||||
|
INSERT INTO sys_permission (id, parent_id, name, url, component, is_route, component_name, redirect, menu_type, perms, perms_type, sort_no, always_show, icon, is_leaf, keep_alive, hidden, hide_tab, description, create_by, create_time, update_by, update_time, del_flag, rule_flag, status, internal_or_external) VALUES ('2025050104193340030', '1922109301837606914', '接口管理', '/openapi/openApiList', 'openapi/OpenApiList', 1, null, null, 1, null, '1', 0, 0, null, 0, 0, 0, 0, null, 'admin', '2025-05-01 16:19:03', 'admin', '2025-05-13 09:59:24', 0, 0, '1', 0);
|
||||||
|
INSERT INTO sys_permission (id, parent_id, name, url, component, is_route, component_name, redirect, menu_type, perms, perms_type, sort_no, always_show, icon, is_leaf, keep_alive, hidden, hide_tab, description, create_by, create_time, update_by, update_time, del_flag, rule_flag, status, internal_or_external) VALUES ('2025050104193350031', '2025050104193340030', '添加接口管理', null, null, 0, null, null, 2, 'openapi:open_api:add', '1', null, 0, null, 1, 0, 0, 0, null, 'admin', '2025-05-01 16:19:03', null, null, 0, 0, '1', 0);
|
||||||
|
INSERT INTO sys_permission (id, parent_id, name, url, component, is_route, component_name, redirect, menu_type, perms, perms_type, sort_no, always_show, icon, is_leaf, keep_alive, hidden, hide_tab, description, create_by, create_time, update_by, update_time, del_flag, rule_flag, status, internal_or_external) VALUES ('2025050104193350032', '2025050104193340030', '编辑接口管理', null, null, 0, null, null, 2, 'openapi:open_api:edit', '1', null, 0, null, 1, 0, 0, 0, null, 'admin', '2025-05-01 16:19:03', null, null, 0, 0, '1', 0);
|
||||||
|
INSERT INTO sys_permission (id, parent_id, name, url, component, is_route, component_name, redirect, menu_type, perms, perms_type, sort_no, always_show, icon, is_leaf, keep_alive, hidden, hide_tab, description, create_by, create_time, update_by, update_time, del_flag, rule_flag, status, internal_or_external) VALUES ('2025050104193350033', '2025050104193340030', '删除接口管理', null, null, 0, null, null, 2, 'openapi:open_api:delete', '1', null, 0, null, 1, 0, 0, 0, null, 'admin', '2025-05-01 16:19:03', null, null, 0, 0, '1', 0);
|
||||||
|
INSERT INTO sys_permission (id, parent_id, name, url, component, is_route, component_name, redirect, menu_type, perms, perms_type, sort_no, always_show, icon, is_leaf, keep_alive, hidden, hide_tab, description, create_by, create_time, update_by, update_time, del_flag, rule_flag, status, internal_or_external) VALUES ('2025050104193350034', '2025050104193340030', '批量删除接口管理', null, null, 0, null, null, 2, 'openapi:open_api:deleteBatch', '1', null, 0, null, 1, 0, 0, 0, null, 'admin', '2025-05-01 16:19:03', null, null, 0, 0, '1', 0);
|
||||||
|
INSERT INTO sys_permission (id, parent_id, name, url, component, is_route, component_name, redirect, menu_type, perms, perms_type, sort_no, always_show, icon, is_leaf, keep_alive, hidden, hide_tab, description, create_by, create_time, update_by, update_time, del_flag, rule_flag, status, internal_or_external) VALUES ('2025050104193350035', '2025050104193340030', '导出excel_接口管理', null, null, 0, null, null, 2, 'openapi:open_api:exportXls', '1', null, 0, null, 1, 0, 0, 0, null, 'admin', '2025-05-01 16:19:03', null, null, 0, 0, '1', 0);
|
||||||
|
INSERT INTO sys_permission (id, parent_id, name, url, component, is_route, component_name, redirect, menu_type, perms, perms_type, sort_no, always_show, icon, is_leaf, keep_alive, hidden, hide_tab, description, create_by, create_time, update_by, update_time, del_flag, rule_flag, status, internal_or_external) VALUES ('2025050104193350036', '2025050104193340030', '导入excel_接口管理', null, null, 0, null, null, 2, 'openapi:open_api:importExcel', '1', null, 0, null, 1, 0, 0, 0, null, 'admin', '2025-05-01 16:19:03', null, null, 0, 0, '1', 0);
|
||||||
|
INSERT INTO sys_permission (id, parent_id, name, url, component, is_route, component_name, redirect, menu_type, perms, perms_type, sort_no, always_show, icon, is_leaf, keep_alive, hidden, hide_tab, description, create_by, create_time, update_by, update_time, del_flag, rule_flag, status, internal_or_external) VALUES ('2025050105554940200', '1922109301837606914', '授权管理', '/openapi/openApiAuthList', 'openapi/OpenApiAuthList', 1, null, null, 1, null, '1', 0, 0, null, 0, 0, 0, 0, null, 'admin', '2025-05-01 17:55:20', 'admin', '2025-05-13 09:59:35', 0, 0, '1', 0);
|
||||||
|
INSERT INTO sys_permission (id, parent_id, name, url, component, is_route, component_name, redirect, menu_type, perms, perms_type, sort_no, always_show, icon, is_leaf, keep_alive, hidden, hide_tab, description, create_by, create_time, update_by, update_time, del_flag, rule_flag, status, internal_or_external) VALUES ('2025050105554940201', '2025050105554940200', '添加授权管理', null, null, 0, null, null, 2, 'openapi:open_api_auth:add', '1', null, 0, null, 1, 0, 0, 0, null, 'admin', '2025-05-01 17:55:20', null, null, 0, 0, '1', 0);
|
||||||
|
INSERT INTO sys_permission (id, parent_id, name, url, component, is_route, component_name, redirect, menu_type, perms, perms_type, sort_no, always_show, icon, is_leaf, keep_alive, hidden, hide_tab, description, create_by, create_time, update_by, update_time, del_flag, rule_flag, status, internal_or_external) VALUES ('2025050105554940202', '2025050105554940200', '编辑授权管理', null, null, 0, null, null, 2, 'openapi:open_api_auth:edit', '1', null, 0, null, 1, 0, 0, 0, null, 'admin', '2025-05-01 17:55:20', null, null, 0, 0, '1', 0);
|
||||||
|
INSERT INTO sys_permission (id, parent_id, name, url, component, is_route, component_name, redirect, menu_type, perms, perms_type, sort_no, always_show, icon, is_leaf, keep_alive, hidden, hide_tab, description, create_by, create_time, update_by, update_time, del_flag, rule_flag, status, internal_or_external) VALUES ('2025050105554940203', '2025050105554940200', '删除授权管理', null, null, 0, null, null, 2, 'openapi:open_api_auth:delete', '1', null, 0, null, 1, 0, 0, 0, null, 'admin', '2025-05-01 17:55:20', null, null, 0, 0, '1', 0);
|
||||||
|
INSERT INTO sys_permission (id, parent_id, name, url, component, is_route, component_name, redirect, menu_type, perms, perms_type, sort_no, always_show, icon, is_leaf, keep_alive, hidden, hide_tab, description, create_by, create_time, update_by, update_time, del_flag, rule_flag, status, internal_or_external) VALUES ('2025050105554940204', '2025050105554940200', '批量删除授权管理', null, null, 0, null, null, 2, 'openapi:open_api_auth:deleteBatch', '1', null, 0, null, 1, 0, 0, 0, null, 'admin', '2025-05-01 17:55:20', null, null, 0, 0, '1', 0);
|
||||||
|
INSERT INTO sys_permission (id, parent_id, name, url, component, is_route, component_name, redirect, menu_type, perms, perms_type, sort_no, always_show, icon, is_leaf, keep_alive, hidden, hide_tab, description, create_by, create_time, update_by, update_time, del_flag, rule_flag, status, internal_or_external) VALUES ('2025050105554940205', '2025050105554940200', '导出excel_授权管理', null, null, 0, null, null, 2, 'openapi:open_api_auth:exportXls', '1', null, 0, null, 1, 0, 0, 0, null, 'admin', '2025-05-01 17:55:20', null, null, 0, 0, '1', 0);
|
||||||
|
INSERT INTO sys_permission (id, parent_id, name, url, component, is_route, component_name, redirect, menu_type, perms, perms_type, sort_no, always_show, icon, is_leaf, keep_alive, hidden, hide_tab, description, create_by, create_time, update_by, update_time, del_flag, rule_flag, status, internal_or_external) VALUES ('2025050105554940206', '2025050105554940200', '导入excel_授权管理', null, null, 0, null, null, 2, 'openapi:open_api_auth:importExcel', '1', null, 0, null, 1, 0, 0, 0, null, 'admin', '2025-05-01 17:55:20', null, null, 0, 0, '1', 0);
|
||||||
|
|
||||||
|
INSERT INTO sys_role_permission (id, role_id, permission_id, data_rule_ids, operate_date, operate_ip) VALUES ('1917957659860963330', 'f6817f48af4fb3af11b9e8bf182f618b', '1917957565728198657', null, '2025-05-01 23:01:55', '0:0:0:0:0:0:0:1');
|
||||||
|
INSERT INTO sys_role_permission (id, role_id, permission_id, data_rule_ids, operate_date, operate_ip) VALUES ('1922109760551858178', 'f6817f48af4fb3af11b9e8bf182f618b', '1922109301837606914', null, '2025-05-13 10:00:53', '0:0:0:0:0:0:0:1');
|
||||||
|
INSERT INTO sys_role_permission (id, role_id, permission_id, data_rule_ids, operate_date, operate_ip) VALUES ('1917857071739539457', 'f6817f48af4fb3af11b9e8bf182f618b', '2025050104193340030', null, '2025-05-01 16:22:13', '0:0:0:0:0:0:0:1');
|
||||||
|
INSERT INTO sys_role_permission (id, role_id, permission_id, data_rule_ids, operate_date, operate_ip) VALUES ('1917857071806648321', 'f6817f48af4fb3af11b9e8bf182f618b', '2025050104193350031', null, '2025-05-01 16:22:13', '0:0:0:0:0:0:0:1');
|
||||||
|
INSERT INTO sys_role_permission (id, role_id, permission_id, data_rule_ids, operate_date, operate_ip) VALUES ('1917857071806648322', 'f6817f48af4fb3af11b9e8bf182f618b', '2025050104193350032', null, '2025-05-01 16:22:13', '0:0:0:0:0:0:0:1');
|
||||||
|
INSERT INTO sys_role_permission (id, role_id, permission_id, data_rule_ids, operate_date, operate_ip) VALUES ('1917857071806648323', 'f6817f48af4fb3af11b9e8bf182f618b', '2025050104193350033', null, '2025-05-01 16:22:13', '0:0:0:0:0:0:0:1');
|
||||||
|
INSERT INTO sys_role_permission (id, role_id, permission_id, data_rule_ids, operate_date, operate_ip) VALUES ('1917857071806648324', 'f6817f48af4fb3af11b9e8bf182f618b', '2025050104193350034', null, '2025-05-01 16:22:13', '0:0:0:0:0:0:0:1');
|
||||||
|
INSERT INTO sys_role_permission (id, role_id, permission_id, data_rule_ids, operate_date, operate_ip) VALUES ('1917857071806648325', 'f6817f48af4fb3af11b9e8bf182f618b', '2025050104193350035', null, '2025-05-01 16:22:13', '0:0:0:0:0:0:0:1');
|
||||||
|
INSERT INTO sys_role_permission (id, role_id, permission_id, data_rule_ids, operate_date, operate_ip) VALUES ('1917857071806648326', 'f6817f48af4fb3af11b9e8bf182f618b', '2025050104193350036', null, '2025-05-01 16:22:13', '0:0:0:0:0:0:0:1');
|
||||||
|
INSERT INTO sys_role_permission (id, role_id, permission_id, data_rule_ids, operate_date, operate_ip) VALUES ('1917881149426864129', 'f6817f48af4fb3af11b9e8bf182f618b', '2025050105554940200', null, '2025-05-01 17:57:53', '0:0:0:0:0:0:0:1');
|
||||||
|
INSERT INTO sys_role_permission (id, role_id, permission_id, data_rule_ids, operate_date, operate_ip) VALUES ('1917881149431058436', 'f6817f48af4fb3af11b9e8bf182f618b', '2025050105554940203', null, '2025-05-01 17:57:53', '0:0:0:0:0:0:0:1');
|
||||||
|
INSERT INTO sys_role_permission (id, role_id, permission_id, data_rule_ids, operate_date, operate_ip) VALUES ('1917881149431058437', 'f6817f48af4fb3af11b9e8bf182f618b', '2025050105554940204', null, '2025-05-01 17:57:53', '0:0:0:0:0:0:0:1');
|
||||||
|
INSERT INTO sys_role_permission (id, role_id, permission_id, data_rule_ids, operate_date, operate_ip) VALUES ('1917881149431058438', 'f6817f48af4fb3af11b9e8bf182f618b', '2025050105554940205', null, '2025-05-01 17:57:53', '0:0:0:0:0:0:0:1');
|
||||||
|
INSERT INTO sys_role_permission (id, role_id, permission_id, data_rule_ids, operate_date, operate_ip) VALUES ('1917881149431058439', 'f6817f48af4fb3af11b9e8bf182f618b', '2025050105554940206', null, '2025-05-01 17:57:53', '0:0:0:0:0:0:0:1');
|
||||||
|
INSERT INTO sys_role_permission (id, role_id, permission_id, data_rule_ids, operate_date, operate_ip) VALUES ('1917957659860963330', 'f6817f48af4fb3af11b9e8bf182f618b', '1917957565728198657', null, '2025-05-01 23:01:55', '0:0:0:0:0:0:0:1');
|
||||||
|
INSERT INTO sys_role_permission (id, role_id, permission_id, data_rule_ids, operate_date, operate_ip) VALUES ('1922109760551858178', 'f6817f48af4fb3af11b9e8bf182f618b', '1922109301837606914', null, '2025-05-13 10:00:53', '0:0:0:0:0:0:0:1');
|
||||||
@ -3,6 +3,7 @@
|
|||||||
> JeecgBoot属于平台级产品,每次升级改动较大,目前做不到平滑升级。
|
> JeecgBoot属于平台级产品,每次升级改动较大,目前做不到平滑升级。
|
||||||
|
|
||||||
### 增量升级方案
|
### 增量升级方案
|
||||||
|
|
||||||
#### 1.代码合并
|
#### 1.代码合并
|
||||||
本地通过svn或git做好主干,在分支上做业务开发,jeecg每次版本发布,可以手工覆盖主干的代码,对比合并代码;
|
本地通过svn或git做好主干,在分支上做业务开发,jeecg每次版本发布,可以手工覆盖主干的代码,对比合并代码;
|
||||||
|
|
||||||
@ -11,5 +12,12 @@
|
|||||||
- 其他库请手工执行SQL, 目录: `jeecg-module-system\jeecg-system-start\src\main\resources\flyway\sql\mysql`
|
- 其他库请手工执行SQL, 目录: `jeecg-module-system\jeecg-system-start\src\main\resources\flyway\sql\mysql`
|
||||||
> 注意: 升级sql只提供mysql版本;如果有权限升级, 还需要手工角色授权,退出重新登录才好使。
|
> 注意: 升级sql只提供mysql版本;如果有权限升级, 还需要手工角色授权,退出重新登录才好使。
|
||||||
|
|
||||||
#### 3.兼容问题
|
#### 3.其他数据库脚本说明
|
||||||
|
原先官方默认提供oracle和SqlServer的脚本,但是维护成本太高,未提供脚本的数据库,可以参考下面的文档自己转
|
||||||
|
https://my.oschina.net/jeecg/blog/4905722
|
||||||
|
(注意:定时任务的表qrtz_*,需要删掉用原始的脚本重新执行一下)
|
||||||
|
quartz-2.2.3-distribution.tar.gz放到百度网盘中,大家自己下载,执行所需数据库脚本
|
||||||
|
https://pan.baidu.com/s/1WrmZdUuAPg3iBwJ-LoHWyg?pwd=8mdz
|
||||||
|
|
||||||
|
#### 4.兼容问题
|
||||||
每次发版,会针对不兼容地方重点说明。
|
每次发版,会针对不兼容地方重点说明。
|
||||||
@ -4,7 +4,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>org.jeecgframework.boot</groupId>
|
<groupId>org.jeecgframework.boot</groupId>
|
||||||
<artifactId>jeecg-boot-parent</artifactId>
|
<artifactId>jeecg-boot-parent</artifactId>
|
||||||
<version>3.7.4</version>
|
<version>3.8.1</version>
|
||||||
</parent>
|
</parent>
|
||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
<artifactId>jeecg-boot-base-core</artifactId>
|
<artifactId>jeecg-boot-base-core</artifactId>
|
||||||
@ -43,7 +43,7 @@
|
|||||||
<!--jeecg-tools-->
|
<!--jeecg-tools-->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.jeecgframework.boot</groupId>
|
<groupId>org.jeecgframework.boot</groupId>
|
||||||
<artifactId>jeecg-boot-common</artifactId>
|
<artifactId>jeecg-boot-common3</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
<!--集成springmvc框架并实现自动配置 -->
|
<!--集成springmvc框架并实现自动配置 -->
|
||||||
<dependency>
|
<dependency>
|
||||||
@ -108,21 +108,32 @@
|
|||||||
<!-- mybatis-plus -->
|
<!-- mybatis-plus -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.baomidou</groupId>
|
<groupId>com.baomidou</groupId>
|
||||||
<artifactId>mybatis-plus-boot-starter</artifactId>
|
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
|
||||||
|
<version>${mybatis-plus.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.baomidou</groupId>
|
||||||
|
<artifactId>mybatis-plus-jsqlparser-4.9</artifactId>
|
||||||
|
<version>${mybatis-plus.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<!-- minidao -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.baomidou</groupId>
|
||||||
|
<artifactId>mybatis-plus-jsqlparser-4.9</artifactId>
|
||||||
<version>${mybatis-plus.version}</version>
|
<version>${mybatis-plus.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- druid -->
|
<!-- druid -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.alibaba</groupId>
|
<groupId>com.alibaba</groupId>
|
||||||
<artifactId>druid-spring-boot-starter</artifactId>
|
<artifactId>druid-spring-boot-3-starter</artifactId>
|
||||||
<version>${druid.version}</version>
|
<version>${druid.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- 动态数据源 -->
|
<!-- 动态数据源 -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.baomidou</groupId>
|
<groupId>com.baomidou</groupId>
|
||||||
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
|
<artifactId>dynamic-datasource-spring-boot3-starter</artifactId>
|
||||||
<version>${dynamic-datasource-spring-boot-starter.version}</version>
|
<version>${dynamic-datasource-spring-boot-starter.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
@ -191,7 +202,50 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.apache.shiro</groupId>
|
<groupId>org.apache.shiro</groupId>
|
||||||
<artifactId>shiro-spring-boot-starter</artifactId>
|
<artifactId>shiro-spring-boot-starter</artifactId>
|
||||||
|
<classifier>jakarta</classifier>
|
||||||
<version>${shiro.version}</version>
|
<version>${shiro.version}</version>
|
||||||
|
<exclusions>
|
||||||
|
<exclusion>
|
||||||
|
<groupId>org.apache.shiro</groupId>
|
||||||
|
<artifactId>shiro-spring</artifactId>
|
||||||
|
</exclusion>
|
||||||
|
</exclusions>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.shiro</groupId>
|
||||||
|
<artifactId>shiro-spring</artifactId>
|
||||||
|
<classifier>jakarta</classifier>
|
||||||
|
<version>${shiro.version}</version>
|
||||||
|
<!-- 排除仍使用了javax.servlet的依赖 -->
|
||||||
|
<exclusions>
|
||||||
|
<exclusion>
|
||||||
|
<groupId>org.apache.shiro</groupId>
|
||||||
|
<artifactId>shiro-core</artifactId>
|
||||||
|
</exclusion>
|
||||||
|
<exclusion>
|
||||||
|
<groupId>org.apache.shiro</groupId>
|
||||||
|
<artifactId>shiro-web</artifactId>
|
||||||
|
</exclusion>
|
||||||
|
</exclusions>
|
||||||
|
</dependency>
|
||||||
|
<!-- 引入适配jakarta的依赖包 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.shiro</groupId>
|
||||||
|
<artifactId>shiro-core</artifactId>
|
||||||
|
<classifier>jakarta</classifier>
|
||||||
|
<version>${shiro.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.shiro</groupId>
|
||||||
|
<artifactId>shiro-web</artifactId>
|
||||||
|
<classifier>jakarta</classifier>
|
||||||
|
<version>${shiro.version}</version>
|
||||||
|
<exclusions>
|
||||||
|
<exclusion>
|
||||||
|
<groupId>org.apache.shiro</groupId>
|
||||||
|
<artifactId>shiro-core</artifactId>
|
||||||
|
</exclusion>
|
||||||
|
</exclusions>
|
||||||
</dependency>
|
</dependency>
|
||||||
<!-- shiro-redis -->
|
<!-- shiro-redis -->
|
||||||
<dependency>
|
<dependency>
|
||||||
@ -210,12 +264,23 @@
|
|||||||
</exclusions>
|
</exclusions>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- knife4j -->
|
|
||||||
|
<!-- <dependency>
|
||||||
|
<groupId>com.github.xiaoymin</groupId>
|
||||||
|
<artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
|
||||||
|
<version>${knife4j-spring-boot-starter.version}</version>
|
||||||
|
</dependency>-->
|
||||||
|
<!-- knife4j 升级springboot3.4.5报错 -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.github.xiaoymin</groupId>
|
<groupId>com.github.xiaoymin</groupId>
|
||||||
<artifactId>knife4j-openapi3-spring-boot-starter</artifactId>
|
<artifactId>knife4j-openapi3-ui</artifactId>
|
||||||
<version>${knife4j-spring-boot-starter.version}</version>
|
<version>${knife4j-spring-boot-starter.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springdoc</groupId>
|
||||||
|
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
|
||||||
|
<version>2.7.0</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<!-- 代码生成器 -->
|
<!-- 代码生成器 -->
|
||||||
<!-- 如下载失败,请参考此文档 https://help.jeecg.com/java/setup/maven.html -->
|
<!-- 如下载失败,请参考此文档 https://help.jeecg.com/java/setup/maven.html -->
|
||||||
@ -237,7 +302,7 @@
|
|||||||
|
|
||||||
<!-- AutoPoi Excel工具类-->
|
<!-- AutoPoi Excel工具类-->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.jeecgframework</groupId>
|
<groupId>org.jeecgframework.boot3</groupId>
|
||||||
<artifactId>autopoi-web</artifactId>
|
<artifactId>autopoi-web</artifactId>
|
||||||
<version>${autopoi-web.version}</version>
|
<version>${autopoi-web.version}</version>
|
||||||
<exclusions>
|
<exclusions>
|
||||||
@ -286,6 +351,16 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.xkcoding.justauth</groupId>
|
<groupId>com.xkcoding.justauth</groupId>
|
||||||
<artifactId>justauth-spring-boot-starter</artifactId>
|
<artifactId>justauth-spring-boot-starter</artifactId>
|
||||||
|
<exclusions>
|
||||||
|
<exclusion>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-autoconfigure</artifactId>
|
||||||
|
</exclusion>
|
||||||
|
<exclusion>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-configuration-processor</artifactId>
|
||||||
|
</exclusion>
|
||||||
|
</exclusions>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.squareup.okhttp3</groupId>
|
<groupId>com.squareup.okhttp3</groupId>
|
||||||
@ -312,7 +387,12 @@
|
|||||||
<!-- chatgpt -->
|
<!-- chatgpt -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.jeecgframework.boot</groupId>
|
<groupId>org.jeecgframework.boot</groupId>
|
||||||
<artifactId>jeecg-boot-starter-chatgpt</artifactId>
|
<artifactId>jeecg-boot-starter3-chatgpt</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<!-- minidao -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.jeecgframework.boot3</groupId>
|
||||||
|
<artifactId>minidao-spring-boot-starter-jsqlparser-4.9</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
</project>
|
</project>
|
||||||
@ -2,7 +2,7 @@ package org.jeecg.common.api.dto;
|
|||||||
|
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
import javax.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -20,14 +20,14 @@ import org.jeecg.common.system.vo.LoginUser;
|
|||||||
import org.jeecg.common.util.IpUtils;
|
import org.jeecg.common.util.IpUtils;
|
||||||
import org.jeecg.common.util.SpringContextUtils;
|
import org.jeecg.common.util.SpringContextUtils;
|
||||||
import org.jeecg.common.util.oConvertUtils;
|
import org.jeecg.common.util.oConvertUtils;
|
||||||
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
|
import org.springframework.core.StandardReflectionParameterNameDiscoverer;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.validation.BindingResult;
|
import org.springframework.validation.BindingResult;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
import javax.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
import javax.servlet.ServletRequest;
|
import jakarta.servlet.ServletRequest;
|
||||||
import javax.servlet.ServletResponse;
|
import jakarta.servlet.ServletResponse;
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
|
||||||
@ -172,7 +172,7 @@ public class AutoLogAspect {
|
|||||||
// 请求的方法参数值
|
// 请求的方法参数值
|
||||||
Object[] args = joinPoint.getArgs();
|
Object[] args = joinPoint.getArgs();
|
||||||
// 请求的方法参数名称
|
// 请求的方法参数名称
|
||||||
LocalVariableTableParameterNameDiscoverer u = new LocalVariableTableParameterNameDiscoverer();
|
StandardReflectionParameterNameDiscoverer u=new StandardReflectionParameterNameDiscoverer();
|
||||||
String[] paramNames = u.getParameterNames(method);
|
String[] paramNames = u.getParameterNames(method);
|
||||||
if (args != null && paramNames != null) {
|
if (args != null && paramNames != null) {
|
||||||
for (int i = 0; i < args.length; i++) {
|
for (int i = 0; i < args.length; i++) {
|
||||||
|
|||||||
@ -21,7 +21,7 @@ import org.springframework.beans.factory.annotation.Autowired;
|
|||||||
import org.springframework.context.annotation.Lazy;
|
import org.springframework.context.annotation.Lazy;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
package org.jeecg.common.exception;
|
package org.jeecg.common.exception;
|
||||||
|
|
||||||
import cn.hutool.core.util.ObjectUtil;
|
import cn.hutool.core.util.ObjectUtil;
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.commons.lang3.exception.ExceptionUtils;
|
import org.apache.commons.lang3.exception.ExceptionUtils;
|
||||||
import org.apache.shiro.SecurityUtils;
|
import org.apache.shiro.SecurityUtils;
|
||||||
@ -32,8 +34,6 @@ import org.springframework.web.bind.annotation.RestControllerAdvice;
|
|||||||
import org.springframework.web.multipart.MaxUploadSizeExceededException;
|
import org.springframework.web.multipart.MaxUploadSizeExceededException;
|
||||||
import org.springframework.web.servlet.NoHandlerFoundException;
|
import org.springframework.web.servlet.NoHandlerFoundException;
|
||||||
|
|
||||||
import javax.annotation.Resource;
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
|||||||
@ -23,9 +23,9 @@ import org.springframework.web.multipart.MultipartFile;
|
|||||||
import org.springframework.web.multipart.MultipartHttpServletRequest;
|
import org.springframework.web.multipart.MultipartHttpServletRequest;
|
||||||
import org.springframework.web.servlet.ModelAndView;
|
import org.springframework.web.servlet.ModelAndView;
|
||||||
|
|
||||||
import javax.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import javax.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|
||||||
|
|||||||
@ -13,6 +13,7 @@ import com.fasterxml.jackson.annotation.JsonFormat;
|
|||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
import lombok.experimental.Accessors;
|
import lombok.experimental.Accessors;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @Description: Entity基类
|
* @Description: Entity基类
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import org.jeecg.common.system.vo.SysUserCacheInfo;
|
|||||||
import org.jeecg.common.util.SpringContextUtils;
|
import org.jeecg.common.util.SpringContextUtils;
|
||||||
import org.springframework.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
|||||||
@ -6,16 +6,18 @@ import com.auth0.jwt.algorithms.Algorithm;
|
|||||||
import com.auth0.jwt.exceptions.JWTDecodeException;
|
import com.auth0.jwt.exceptions.JWTDecodeException;
|
||||||
import com.auth0.jwt.interfaces.DecodedJWT;
|
import com.auth0.jwt.interfaces.DecodedJWT;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.google.common.base.Joiner;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import javax.servlet.ServletResponse;
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import jakarta.servlet.ServletResponse;
|
||||||
import javax.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import javax.servlet.http.HttpSession;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import jakarta.servlet.http.HttpSession;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.shiro.SecurityUtils;
|
import org.apache.shiro.SecurityUtils;
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import java.util.Map;
|
|||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
|
|||||||
@ -19,7 +19,7 @@ import org.springframework.jdbc.datasource.DriverManagerDataSource;
|
|||||||
import org.springframework.util.FileCopyUtils;
|
import org.springframework.util.FileCopyUtils;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import javax.sql.DataSource;
|
import javax.sql.DataSource;
|
||||||
import java.io.ByteArrayInputStream;
|
import java.io.ByteArrayInputStream;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
package org.jeecg.common.util;
|
package org.jeecg.common.util;
|
||||||
|
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.commons.io.FilenameUtils;
|
import org.apache.commons.io.FilenameUtils;
|
||||||
import org.jeecg.common.exception.JeecgBootException;
|
import org.jeecg.common.exception.JeecgBootException;
|
||||||
|
|
||||||
import javax.servlet.http.HttpServletResponse;
|
|
||||||
import java.io.*;
|
import java.io.*;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.net.URLConnection;
|
import java.net.URLConnection;
|
||||||
|
|||||||
@ -4,13 +4,13 @@ import com.alibaba.fastjson.JSON;
|
|||||||
import com.alibaba.fastjson.JSONObject;
|
import com.alibaba.fastjson.JSONObject;
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.jeecg.common.constant.SymbolConstant;
|
import org.jeecg.common.constant.SymbolConstant;
|
||||||
import org.jeecg.common.handler.IFillRuleHandler;
|
import org.jeecg.common.handler.IFillRuleHandler;
|
||||||
import org.jeecg.common.system.query.QueryGenerator;
|
import org.jeecg.common.system.query.QueryGenerator;
|
||||||
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
package org.jeecg.common.util;
|
package org.jeecg.common.util;
|
||||||
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.jeecg.common.constant.CommonConstant;
|
import org.jeecg.common.constant.CommonConstant;
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
package org.jeecg.common.util;
|
package org.jeecg.common.util;
|
||||||
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import javax.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
import org.jeecg.common.constant.CommonConstant;
|
import org.jeecg.common.constant.CommonConstant;
|
||||||
import org.jeecg.common.constant.ServiceNameConstants;
|
import org.jeecg.common.constant.ServiceNameConstants;
|
||||||
|
|||||||
@ -11,7 +11,7 @@ import org.jeecg.common.exception.JeecgBoot401Exception;
|
|||||||
import org.jeecg.common.system.util.JwtUtil;
|
import org.jeecg.common.system.util.JwtUtil;
|
||||||
import org.jeecg.common.system.vo.LoginUser;
|
import org.jeecg.common.system.vo.LoginUser;
|
||||||
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @Author scott
|
* @Author scott
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
package org.jeecg.common.util.encryption;
|
package org.jeecg.common.util.encryption;
|
||||||
|
|
||||||
import org.apache.shiro.codec.Base64;
|
import org.apache.shiro.lang.codec.Base64;
|
||||||
|
|
||||||
import javax.crypto.Cipher;
|
import javax.crypto.Cipher;
|
||||||
import javax.crypto.spec.IvParameterSpec;
|
import javax.crypto.spec.IvParameterSpec;
|
||||||
import javax.crypto.spec.SecretKeySpec;
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import org.jeecg.common.constant.CommonConstant;
|
|||||||
import org.jeecg.common.constant.SymbolConstant;
|
import org.jeecg.common.constant.SymbolConstant;
|
||||||
import org.springframework.beans.BeanUtils;
|
import org.springframework.beans.BeanUtils;
|
||||||
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.UnsupportedEncodingException;
|
import java.io.UnsupportedEncodingException;
|
||||||
|
|||||||
@ -1,33 +0,0 @@
|
|||||||
package org.jeecg.common.util.sqlInjection;
|
|
||||||
|
|
||||||
import net.sf.jsqlparser.parser.CCJSqlParserDefaultVisitor;
|
|
||||||
import net.sf.jsqlparser.parser.SimpleNode;
|
|
||||||
import net.sf.jsqlparser.statement.select.UnionOp;
|
|
||||||
import org.jeecg.common.exception.JeecgSqlInjectionException;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 基于抽象语法树(AST)的注入攻击分析实现
|
|
||||||
*
|
|
||||||
* @author guyadong
|
|
||||||
*/
|
|
||||||
public class InjectionAstNodeVisitor extends CCJSqlParserDefaultVisitor {
|
|
||||||
public InjectionAstNodeVisitor() {
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理禁止联合查询
|
|
||||||
*
|
|
||||||
* @param node
|
|
||||||
* @param data
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public Object visit(SimpleNode node, Object data) {
|
|
||||||
Object value = node.jjtGetValue();
|
|
||||||
if (value instanceof UnionOp) {
|
|
||||||
throw new JeecgSqlInjectionException("DISABLE UNION");
|
|
||||||
}
|
|
||||||
return super.visit(node, data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@ -1,172 +0,0 @@
|
|||||||
package org.jeecg.common.util.sqlInjection;
|
|
||||||
|
|
||||||
|
|
||||||
import net.sf.jsqlparser.expression.BinaryExpression;
|
|
||||||
import net.sf.jsqlparser.expression.Expression;
|
|
||||||
import net.sf.jsqlparser.expression.Function;
|
|
||||||
import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
|
|
||||||
import net.sf.jsqlparser.expression.operators.conditional.OrExpression;
|
|
||||||
import net.sf.jsqlparser.expression.operators.relational.ComparisonOperator;
|
|
||||||
import net.sf.jsqlparser.schema.Column;
|
|
||||||
import net.sf.jsqlparser.statement.select.Join;
|
|
||||||
import net.sf.jsqlparser.statement.select.OrderByElement;
|
|
||||||
import net.sf.jsqlparser.statement.select.PlainSelect;
|
|
||||||
import net.sf.jsqlparser.statement.select.SelectItem;
|
|
||||||
import net.sf.jsqlparser.statement.select.SubSelect;
|
|
||||||
import net.sf.jsqlparser.statement.select.WithItem;
|
|
||||||
import net.sf.jsqlparser.util.TablesNamesFinder;
|
|
||||||
import org.jeecg.common.exception.JeecgSqlInjectionException;
|
|
||||||
import org.jeecg.common.util.sqlInjection.parse.ConstAnalyzer;
|
|
||||||
import org.jeecg.common.util.sqlInjection.parse.ParserSupport;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 基于SQL语法对象的SQL注入攻击分析实现
|
|
||||||
*
|
|
||||||
* @author guyadong
|
|
||||||
*/
|
|
||||||
public class InjectionSyntaxObjectAnalyzer extends TablesNamesFinder {
|
|
||||||
/**
|
|
||||||
* 危险函数名
|
|
||||||
*/
|
|
||||||
private static final String DANGROUS_FUNCTIONS = "(sleep|benchmark|extractvalue|updatexml|ST_LatFromGeoHash|ST_LongFromGeoHash|GTID_SUBSET|GTID_SUBTRACT|floor|ST_Pointfromgeohash"
|
|
||||||
+ "|geometrycollection|multipoint|polygon|multipolygon|linestring|multilinestring)";
|
|
||||||
|
|
||||||
private static ThreadLocal<Boolean> disableSubselect = new ThreadLocal<Boolean>() {
|
|
||||||
@Override
|
|
||||||
protected Boolean initialValue() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
private ConstAnalyzer constAnalyzer = new ConstAnalyzer();
|
|
||||||
|
|
||||||
public InjectionSyntaxObjectAnalyzer() {
|
|
||||||
super();
|
|
||||||
init(true);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void visitBinaryExpression(BinaryExpression binaryExpression) {
|
|
||||||
if (binaryExpression instanceof ComparisonOperator) {
|
|
||||||
if (isConst(binaryExpression.getLeftExpression()) && isConst(binaryExpression.getRightExpression())) {
|
|
||||||
/** 禁用恒等式 */
|
|
||||||
throw new JeecgSqlInjectionException("DISABLE IDENTICAL EQUATION " + binaryExpression);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
super.visitBinaryExpression(binaryExpression);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void visit(AndExpression andExpression) {
|
|
||||||
super.visit(andExpression);
|
|
||||||
checkConstExpress(andExpression.getLeftExpression());
|
|
||||||
checkConstExpress(andExpression.getRightExpression());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void visit(OrExpression orExpression) {
|
|
||||||
super.visit(orExpression);
|
|
||||||
checkConstExpress(orExpression.getLeftExpression());
|
|
||||||
checkConstExpress(orExpression.getRightExpression());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void visit(Function function) {
|
|
||||||
if (function.getName().matches(DANGROUS_FUNCTIONS)) {
|
|
||||||
/** 禁用危险函数 */
|
|
||||||
throw new JeecgSqlInjectionException("DANGROUS FUNCTION: " + function.getName());
|
|
||||||
}
|
|
||||||
super.visit(function);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void visit(WithItem withItem) {
|
|
||||||
try {
|
|
||||||
/** 允许 WITH 语句中的子查询 */
|
|
||||||
disableSubselect.set(false);
|
|
||||||
super.visit(withItem);
|
|
||||||
} finally {
|
|
||||||
disableSubselect.set(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void visit(SubSelect subSelect) {
|
|
||||||
try {
|
|
||||||
/** 允许语句中的子查询 */
|
|
||||||
disableSubselect.set(false);
|
|
||||||
super.visit(subSelect);
|
|
||||||
} finally {
|
|
||||||
disableSubselect.set(true);
|
|
||||||
}
|
|
||||||
// if (disableSubselect.get()) {
|
|
||||||
// // 禁用子查询
|
|
||||||
// throw new JeecgSqlInjectionException("DISABLE subselect " + subSelect);
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void visit(Column tableColumn) {
|
|
||||||
if (ParserSupport.isBoolean(tableColumn)) {
|
|
||||||
throw new JeecgSqlInjectionException("DISABLE CONST BOOL " + tableColumn);
|
|
||||||
}
|
|
||||||
super.visit(tableColumn);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void visit(PlainSelect plainSelect) {
|
|
||||||
if (plainSelect.getSelectItems() != null) {
|
|
||||||
for (SelectItem item : plainSelect.getSelectItems()) {
|
|
||||||
item.accept(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (plainSelect.getFromItem() != null) {
|
|
||||||
plainSelect.getFromItem().accept(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (plainSelect.getJoins() != null) {
|
|
||||||
for (Join join : plainSelect.getJoins()) {
|
|
||||||
join.getRightItem().accept(this);
|
|
||||||
for (Expression e : join.getOnExpressions()) {
|
|
||||||
e.accept(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (plainSelect.getWhere() != null) {
|
|
||||||
plainSelect.getWhere().accept(this);
|
|
||||||
checkConstExpress(plainSelect.getWhere());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (plainSelect.getHaving() != null) {
|
|
||||||
plainSelect.getHaving().accept(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (plainSelect.getOracleHierarchical() != null) {
|
|
||||||
plainSelect.getOracleHierarchical().accept(this);
|
|
||||||
}
|
|
||||||
if (plainSelect.getOrderByElements() != null) {
|
|
||||||
for (OrderByElement orderByElement : plainSelect.getOrderByElements()) {
|
|
||||||
orderByElement.getExpression().accept(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (plainSelect.getGroupBy() != null) {
|
|
||||||
for (Expression expression : plainSelect.getGroupBy().getGroupByExpressionList().getExpressions()) {
|
|
||||||
expression.accept(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isConst(Expression expression) {
|
|
||||||
return constAnalyzer.isConstExpression(expression);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void checkConstExpress(Expression expression) {
|
|
||||||
if (constAnalyzer.isConstExpression(expression)) {
|
|
||||||
/** 禁用常量表达式 */
|
|
||||||
throw new JeecgSqlInjectionException("DISABLE CONST EXPRESSION " + expression);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@ -1,65 +0,0 @@
|
|||||||
package org.jeecg.common.util.sqlInjection;
|
|
||||||
|
|
||||||
import org.jeecg.common.exception.JeecgSqlInjectionException;
|
|
||||||
import org.jeecg.common.util.sqlInjection.parse.ParserSupport;
|
|
||||||
;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* SQL注入攻击分析器
|
|
||||||
*
|
|
||||||
* @author guyadong
|
|
||||||
* 参考:
|
|
||||||
* https://blog.csdn.net/10km/article/details/127767358
|
|
||||||
* https://gitee.com/l0km/sql2java/tree/dev/sql2java-manager/src/main/java/gu/sql2java/parser
|
|
||||||
*/
|
|
||||||
public class SqlInjectionAnalyzer {
|
|
||||||
|
|
||||||
//启用/关闭注入攻击检查
|
|
||||||
private boolean injectCheckEnable = true;
|
|
||||||
//防止SQL注入攻击分析实现
|
|
||||||
private final InjectionSyntaxObjectAnalyzer injectionChecker;
|
|
||||||
private final InjectionAstNodeVisitor injectionVisitor;
|
|
||||||
|
|
||||||
public SqlInjectionAnalyzer() {
|
|
||||||
this.injectionChecker = new InjectionSyntaxObjectAnalyzer();
|
|
||||||
this.injectionVisitor = new InjectionAstNodeVisitor();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 启用/关闭注入攻击检查,默认启动
|
|
||||||
*
|
|
||||||
* @param enable
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
public SqlInjectionAnalyzer injectCheckEnable(boolean enable) {
|
|
||||||
injectCheckEnable = enable;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 对解析后的SQL对象执行注入攻击分析,有注入攻击的危险则抛出异常{@link JeecgSqlInjectionException}
|
|
||||||
*
|
|
||||||
* @param sqlParserInfo
|
|
||||||
* @throws JeecgSqlInjectionException
|
|
||||||
*/
|
|
||||||
public ParserSupport.SqlParserInfo injectAnalyse(ParserSupport.SqlParserInfo sqlParserInfo) throws JeecgSqlInjectionException {
|
|
||||||
if (null != sqlParserInfo && injectCheckEnable) {
|
|
||||||
/** SQL注入攻击检查 */
|
|
||||||
sqlParserInfo.statement.accept(injectionChecker);
|
|
||||||
sqlParserInfo.simpleNode.jjtAccept(injectionVisitor, null);
|
|
||||||
}
|
|
||||||
return sqlParserInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* sql校验
|
|
||||||
*/
|
|
||||||
public static void checkSql(String sql,boolean check){
|
|
||||||
SqlInjectionAnalyzer sqlInjectionAnalyzer = new SqlInjectionAnalyzer();
|
|
||||||
sqlInjectionAnalyzer.injectCheckEnable(check);
|
|
||||||
ParserSupport.SqlParserInfo sqlParserInfo = ParserSupport.parse0(sql, null,null);
|
|
||||||
sqlInjectionAnalyzer.injectAnalyse(sqlParserInfo);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@ -1,569 +0,0 @@
|
|||||||
package org.jeecg.common.util.sqlInjection.parse;
|
|
||||||
|
|
||||||
import net.sf.jsqlparser.expression.*;
|
|
||||||
import net.sf.jsqlparser.expression.operators.arithmetic.Addition;
|
|
||||||
import net.sf.jsqlparser.expression.operators.arithmetic.BitwiseAnd;
|
|
||||||
import net.sf.jsqlparser.expression.operators.arithmetic.BitwiseLeftShift;
|
|
||||||
import net.sf.jsqlparser.expression.operators.arithmetic.BitwiseOr;
|
|
||||||
import net.sf.jsqlparser.expression.operators.arithmetic.BitwiseRightShift;
|
|
||||||
import net.sf.jsqlparser.expression.operators.arithmetic.BitwiseXor;
|
|
||||||
import net.sf.jsqlparser.expression.operators.arithmetic.Concat;
|
|
||||||
import net.sf.jsqlparser.expression.operators.arithmetic.Division;
|
|
||||||
import net.sf.jsqlparser.expression.operators.arithmetic.IntegerDivision;
|
|
||||||
import net.sf.jsqlparser.expression.operators.arithmetic.Modulo;
|
|
||||||
import net.sf.jsqlparser.expression.operators.arithmetic.Multiplication;
|
|
||||||
import net.sf.jsqlparser.expression.operators.arithmetic.Subtraction;
|
|
||||||
import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
|
|
||||||
import net.sf.jsqlparser.expression.operators.conditional.OrExpression;
|
|
||||||
import net.sf.jsqlparser.expression.operators.conditional.XorExpression;
|
|
||||||
import net.sf.jsqlparser.expression.operators.relational.Between;
|
|
||||||
import net.sf.jsqlparser.expression.operators.relational.EqualsTo;
|
|
||||||
import net.sf.jsqlparser.expression.operators.relational.ExistsExpression;
|
|
||||||
import net.sf.jsqlparser.expression.operators.relational.ExpressionList;
|
|
||||||
import net.sf.jsqlparser.expression.operators.relational.FullTextSearch;
|
|
||||||
import net.sf.jsqlparser.expression.operators.relational.GeometryDistance;
|
|
||||||
import net.sf.jsqlparser.expression.operators.relational.GreaterThan;
|
|
||||||
import net.sf.jsqlparser.expression.operators.relational.GreaterThanEquals;
|
|
||||||
import net.sf.jsqlparser.expression.operators.relational.InExpression;
|
|
||||||
import net.sf.jsqlparser.expression.operators.relational.IsBooleanExpression;
|
|
||||||
import net.sf.jsqlparser.expression.operators.relational.IsDistinctExpression;
|
|
||||||
import net.sf.jsqlparser.expression.operators.relational.IsNullExpression;
|
|
||||||
import net.sf.jsqlparser.expression.operators.relational.ItemsListVisitor;
|
|
||||||
import net.sf.jsqlparser.expression.operators.relational.JsonOperator;
|
|
||||||
import net.sf.jsqlparser.expression.operators.relational.LikeExpression;
|
|
||||||
import net.sf.jsqlparser.expression.operators.relational.Matches;
|
|
||||||
import net.sf.jsqlparser.expression.operators.relational.MinorThan;
|
|
||||||
import net.sf.jsqlparser.expression.operators.relational.MinorThanEquals;
|
|
||||||
import net.sf.jsqlparser.expression.operators.relational.MultiExpressionList;
|
|
||||||
import net.sf.jsqlparser.expression.operators.relational.NamedExpressionList;
|
|
||||||
import net.sf.jsqlparser.expression.operators.relational.NotEqualsTo;
|
|
||||||
import net.sf.jsqlparser.expression.operators.relational.RegExpMatchOperator;
|
|
||||||
import net.sf.jsqlparser.expression.operators.relational.RegExpMySQLOperator;
|
|
||||||
import net.sf.jsqlparser.expression.operators.relational.SimilarToExpression;
|
|
||||||
import net.sf.jsqlparser.schema.Column;
|
|
||||||
import net.sf.jsqlparser.statement.select.AllColumns;
|
|
||||||
import net.sf.jsqlparser.statement.select.AllTableColumns;
|
|
||||||
import net.sf.jsqlparser.statement.select.OrderByElement;
|
|
||||||
import net.sf.jsqlparser.statement.select.SubSelect;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 判断表达是否为常量的分析器
|
|
||||||
*
|
|
||||||
* @author guyadong
|
|
||||||
*/
|
|
||||||
public class ConstAnalyzer implements ExpressionVisitor, ItemsListVisitor {
|
|
||||||
|
|
||||||
private static ThreadLocal<Boolean> constFlag = new ThreadLocal<Boolean>() {
|
|
||||||
@Override
|
|
||||||
protected Boolean initialValue() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void visit(NullValue value) {
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void visit(Function function) {
|
|
||||||
constFlag.set(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void visit(SignedExpression expr) {
|
|
||||||
expr.getExpression().accept(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void visit(JdbcParameter parameter) {
|
|
||||||
constFlag.set(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void visit(JdbcNamedParameter parameter) {
|
|
||||||
constFlag.set(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void visit(DoubleValue value) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void visit(LongValue value) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void visit(DateValue value) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void visit(TimeValue value) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void visit(TimestampValue value) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void visit(Parenthesis parenthesis) {
|
|
||||||
parenthesis.getExpression().accept(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void visit(StringValue value) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void visit(Addition expr) {
|
|
||||||
visitBinaryExpression(expr);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void visit(Division expr) {
|
|
||||||
visitBinaryExpression(expr);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void visit(IntegerDivision expr) {
|
|
||||||
visitBinaryExpression(expr);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void visit(Multiplication expr) {
|
|
||||||
visitBinaryExpression(expr);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void visit(Subtraction expr) {
|
|
||||||
visitBinaryExpression(expr);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void visit(AndExpression expr) {
|
|
||||||
visitBinaryExpression(expr);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void visit(OrExpression expr) {
|
|
||||||
visitBinaryExpression(expr);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void visit(XorExpression expr) {
|
|
||||||
visitBinaryExpression(expr);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void visit(Between expr) {
|
|
||||||
expr.getLeftExpression().accept(this);
|
|
||||||
expr.getBetweenExpressionStart().accept(this);
|
|
||||||
expr.getBetweenExpressionEnd().accept(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 用于处理 OverlapsCondition 类型的表达式
|
|
||||||
* @param overlapsCondition
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void visit(OverlapsCondition overlapsCondition) {
|
|
||||||
constFlag.set(false);
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* 用于处理 SafeCastExpression 类型的表达式。
|
|
||||||
* @param safeCastExpression
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void visit(SafeCastExpression safeCastExpression) {
|
|
||||||
constFlag.set(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void visit(EqualsTo expr) {
|
|
||||||
visitBinaryExpression(expr);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void visit(GreaterThan expr) {
|
|
||||||
visitBinaryExpression(expr);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void visit(GreaterThanEquals expr) {
|
|
||||||
visitBinaryExpression(expr);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void visit(InExpression expr) {
|
|
||||||
if (expr.getLeftExpression() != null) {
|
|
||||||
expr.getLeftExpression().accept(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void visit(IsNullExpression expr) {
|
|
||||||
expr.getLeftExpression().accept(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void visit(FullTextSearch expr) {
|
|
||||||
constFlag.set(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void visit(IsBooleanExpression expr) {
|
|
||||||
expr.getLeftExpression().accept(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void visit(LikeExpression expr) {
|
|
||||||
visitBinaryExpression(expr);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void visit(MinorThan expr) {
|
|
||||||
visitBinaryExpression(expr);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void visit(MinorThanEquals expr) {
|
|
||||||
visitBinaryExpression(expr);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void visit(NotEqualsTo expr) {
|
|
||||||
visitBinaryExpression(expr);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void visit(Column column) {
|
|
||||||
if (!ParserSupport.isBoolean(column)) {
|
|
||||||
constFlag.set(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void visit(SubSelect subSelect) {
|
|
||||||
constFlag.set(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void visit(CaseExpression expr) {
|
|
||||||
constFlag.set(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void visit(WhenClause expr) {
|
|
||||||
constFlag.set(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void visit(ExistsExpression expr) {
|
|
||||||
constFlag.set(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void visit(AnyComparisonExpression expr) {
|
|
||||||
constFlag.set(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void visit(Concat expr) {
|
|
||||||
visitBinaryExpression(expr);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void visit(Matches expr) {
|
|
||||||
visitBinaryExpression(expr);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void visit(BitwiseAnd expr) {
|
|
||||||
visitBinaryExpression(expr);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void visit(BitwiseOr expr) {
|
|
||||||
visitBinaryExpression(expr);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void visit(BitwiseXor expr) {
|
|
||||||
visitBinaryExpression(expr);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void visit(CastExpression expr) {
|
|
||||||
expr.getLeftExpression().accept(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void visit(TryCastExpression expr) {
|
|
||||||
constFlag.set(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void visit(Modulo expr) {
|
|
||||||
visitBinaryExpression(expr);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void visit(AnalyticExpression expr) {
|
|
||||||
constFlag.set(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void visit(ExtractExpression expr) {
|
|
||||||
expr.getExpression().accept(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void visit(IntervalExpression expr) {
|
|
||||||
constFlag.set(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void visit(OracleHierarchicalExpression expr) {
|
|
||||||
constFlag.set(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void visit(RegExpMatchOperator expr) {
|
|
||||||
visitBinaryExpression(expr);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void visit(ExpressionList expressionList) {
|
|
||||||
for (Expression expr : expressionList.getExpressions()) {
|
|
||||||
expr.accept(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void visit(NamedExpressionList namedExpressionList) {
|
|
||||||
for (Expression expr : namedExpressionList.getExpressions()) {
|
|
||||||
expr.accept(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void visit(MultiExpressionList multiExprList) {
|
|
||||||
for (ExpressionList list : multiExprList.getExpressionLists()) {
|
|
||||||
visit(list);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void visit(NotExpression notExpr) {
|
|
||||||
notExpr.getExpression().accept(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void visit(BitwiseRightShift expr) {
|
|
||||||
visitBinaryExpression(expr);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void visit(BitwiseLeftShift expr) {
|
|
||||||
visitBinaryExpression(expr);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void visitBinaryExpression(BinaryExpression expr) {
|
|
||||||
expr.getLeftExpression().accept(this);
|
|
||||||
expr.getRightExpression().accept(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void visit(JsonExpression jsonExpr) {
|
|
||||||
jsonExpr.getExpression().accept(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void visit(JsonOperator expr) {
|
|
||||||
visitBinaryExpression(expr);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void visit(RegExpMySQLOperator expr) {
|
|
||||||
visitBinaryExpression(expr);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void visit(UserVariable var) {
|
|
||||||
constFlag.set(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void visit(NumericBind bind) {
|
|
||||||
constFlag.set(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void visit(KeepExpression expr) {
|
|
||||||
for (OrderByElement element : expr.getOrderByElements()) {
|
|
||||||
element.getExpression().accept(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void visit(MySQLGroupConcat groupConcat) {
|
|
||||||
constFlag.set(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void visit(ValueListExpression valueListExpression) {
|
|
||||||
for (Expression expr : valueListExpression.getExpressionList().getExpressions()) {
|
|
||||||
expr.accept(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void visit(AllColumns allColumns) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void visit(AllTableColumns allTableColumns) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void visit(AllValue allValue) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void visit(IsDistinctExpression isDistinctExpression) {
|
|
||||||
visitBinaryExpression(isDistinctExpression);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void visit(RowGetExpression rowGetExpression) {
|
|
||||||
rowGetExpression.getExpression().accept(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void visit(HexValue hexValue) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void visit(OracleHint hint) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void visit(TimeKeyExpression timeKeyExpression) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void visit(DateTimeLiteralExpression literal) {
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void visit(NextValExpression nextVal) {
|
|
||||||
constFlag.set(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void visit(CollateExpression col) {
|
|
||||||
constFlag.set(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void visit(SimilarToExpression expr) {
|
|
||||||
visitBinaryExpression(expr);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void visit(ArrayExpression array) {
|
|
||||||
array.getObjExpression().accept(this);
|
|
||||||
if (array.getIndexExpression() != null) {
|
|
||||||
array.getIndexExpression().accept(this);
|
|
||||||
}
|
|
||||||
if (array.getStartIndexExpression() != null) {
|
|
||||||
array.getStartIndexExpression().accept(this);
|
|
||||||
}
|
|
||||||
if (array.getStopIndexExpression() != null) {
|
|
||||||
array.getStopIndexExpression().accept(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void visit(ArrayConstructor aThis) {
|
|
||||||
for (Expression expression : aThis.getExpressions()) {
|
|
||||||
expression.accept(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void visit(VariableAssignment var) {
|
|
||||||
constFlag.set(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void visit(XMLSerializeExpr expr) {
|
|
||||||
constFlag.set(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void visit(TimezoneExpression expr) {
|
|
||||||
expr.getLeftExpression().accept(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void visit(JsonAggregateFunction expression) {
|
|
||||||
Expression expr = expression.getExpression();
|
|
||||||
if (expr != null) {
|
|
||||||
expr.accept(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
expr = expression.getFilterExpression();
|
|
||||||
if (expr != null) {
|
|
||||||
expr.accept(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void visit(JsonFunction expression) {
|
|
||||||
for (JsonFunctionExpression expr : expression.getExpressions()) {
|
|
||||||
expr.getExpression().accept(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void visit(ConnectByRootOperator connectByRootOperator) {
|
|
||||||
constFlag.set(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void visit(OracleNamedFunctionParameter oracleNamedFunctionParameter) {
|
|
||||||
constFlag.set(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void visit(GeometryDistance geometryDistance) {
|
|
||||||
visitBinaryExpression(geometryDistance);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void visit(RowConstructor rowConstructor) {
|
|
||||||
constFlag.set(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isConstExpression(Expression expression) {
|
|
||||||
if (null != expression) {
|
|
||||||
constFlag.set(true);
|
|
||||||
expression.accept(this);
|
|
||||||
return constFlag.get();
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@ -1,177 +0,0 @@
|
|||||||
package org.jeecg.common.util.sqlInjection.parse;
|
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import net.sf.jsqlparser.JSQLParserException;
|
|
||||||
import net.sf.jsqlparser.parser.*;
|
|
||||||
import net.sf.jsqlparser.schema.Column;
|
|
||||||
import net.sf.jsqlparser.statement.Statement;
|
|
||||||
import net.sf.jsqlparser.statement.select.PlainSelect;
|
|
||||||
import net.sf.jsqlparser.statement.select.Select;
|
|
||||||
import net.sf.jsqlparser.statement.select.SelectBody;
|
|
||||||
|
|
||||||
import static com.google.common.base.Preconditions.checkArgument;
|
|
||||||
import static com.google.common.base.Preconditions.checkNotNull;
|
|
||||||
|
|
||||||
import java.lang.reflect.InvocationTargetException;
|
|
||||||
import java.util.regex.Matcher;
|
|
||||||
import java.util.regex.Pattern;
|
|
||||||
|
|
||||||
import com.google.common.base.Throwables;
|
|
||||||
import org.jeecg.common.exception.JeecgBootException;
|
|
||||||
import org.jeecg.common.exception.JeecgSqlInjectionException;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 解析sql支持
|
|
||||||
*/
|
|
||||||
@Slf4j
|
|
||||||
public class ParserSupport {
|
|
||||||
/**
|
|
||||||
* 解析SELECT SQL语句,解析失败或非SELECT语句则抛出异常
|
|
||||||
*
|
|
||||||
* @param sql
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
public static Select parseSelect(String sql) {
|
|
||||||
Statement stmt;
|
|
||||||
try {
|
|
||||||
stmt = CCJSqlParserUtil.parse(checkNotNull(sql, "sql is null"));
|
|
||||||
} catch (JSQLParserException e) {
|
|
||||||
throw new JeecgBootException(e);
|
|
||||||
}
|
|
||||||
checkArgument(stmt instanceof Select, "%s is not SELECT statment", sql);
|
|
||||||
Select select = (Select) stmt;
|
|
||||||
SelectBody selectBody = select.getSelectBody();
|
|
||||||
// 暂时只支持简单的SELECT xxxx FROM ....语句不支持复杂语句如WITH
|
|
||||||
checkArgument(selectBody instanceof PlainSelect, "ONLY SUPPORT plain select statement %s", sql);
|
|
||||||
return (Select) stmt;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 解析SELECT SQL语句,解析失败或非SELECT语句则
|
|
||||||
*
|
|
||||||
* @param sql
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
public static Select parseSelectUnchecked(String sql) {
|
|
||||||
try {
|
|
||||||
return parseSelect(sql);
|
|
||||||
} catch (Exception e) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 实现SQL语句解析,解析成功则返回解析后的{@link Statement},
|
|
||||||
* 并通过{@code visitor}参数提供基于AST(抽象语法树)的遍历所有节点的能力。
|
|
||||||
*
|
|
||||||
* @param sql SQL语句
|
|
||||||
* @param visitor 遍历所有节点的{@link SimpleNodeVisitor}接口实例,为{@code null}忽略
|
|
||||||
* @param sqlSyntaxNormalizer SQL语句分析转换器,为{@code null}忽略
|
|
||||||
* @throws JSQLParserException 输入的SQL语句有语法错误
|
|
||||||
* @see #parse0(String, CCJSqlParserVisitor, SqlSyntaxNormalizer)
|
|
||||||
*/
|
|
||||||
public static Statement parse(String sql, CCJSqlParserVisitor visitor, SqlSyntaxNormalizer sqlSyntaxNormalizer) throws JSQLParserException {
|
|
||||||
return parse0(sql, visitor, sqlSyntaxNormalizer).statement;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 参照{@link CCJSqlParserUtil#parseAST(String)}和{@link CCJSqlParserUtil#parse(String)}实现SQL语句解析,
|
|
||||||
* 解析成功则返回解析后的{@link SqlParserInfo}对象,
|
|
||||||
* 并通过{@code visitor}参数提供基于AST(抽象语法树)的遍历所有节点的能力。
|
|
||||||
*
|
|
||||||
* @param sql SQL语句
|
|
||||||
* @param visitor 遍历所有节点的{@link SimpleNodeVisitor}接口实例,为{@code null}忽略
|
|
||||||
* @param sqlSyntaxAnalyzer SQL语句分析转换器,为{@code null}忽略
|
|
||||||
* @throws JSQLParserException 输入的SQL语句有语法错误
|
|
||||||
* @see net.sf.jsqlparser.parser.Node#jjtAccept(SimpleNodeVisitor, Object)
|
|
||||||
*/
|
|
||||||
public static SqlParserInfo parse0(String sql, CCJSqlParserVisitor visitor, SqlSyntaxNormalizer sqlSyntaxAnalyzer) throws JeecgSqlInjectionException {
|
|
||||||
|
|
||||||
//检查是否非select开头,暂不支持
|
|
||||||
if(!sql.toLowerCase().trim().startsWith("select ")) {
|
|
||||||
log.warn("传入sql 非select开头,不支持非select开头的语句解析!");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
//检查是否存储过程,暂不支持
|
|
||||||
if(sql.toLowerCase().trim().startsWith("call ")){
|
|
||||||
log.warn("传入call 开头存储过程,不支持存储过程解析!");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
//检查特殊语义的特殊字符,目前检查冒号、$、#三种特殊语义字符
|
|
||||||
String specialCharacters = "[:$#]";
|
|
||||||
Pattern pattern = Pattern.compile(specialCharacters);
|
|
||||||
Matcher matcher = pattern.matcher(sql);
|
|
||||||
if (matcher.find()) {
|
|
||||||
sql = sql.replaceAll("[:$#]", "@");
|
|
||||||
}
|
|
||||||
|
|
||||||
checkArgument(null != sql, "sql is null");
|
|
||||||
boolean allowComplexParsing = CCJSqlParserUtil.getNestingDepth(sql) <= CCJSqlParserUtil.ALLOWED_NESTING_DEPTH;
|
|
||||||
|
|
||||||
CCJSqlParser parser = CCJSqlParserUtil.newParser(sql).withAllowComplexParsing(allowComplexParsing);
|
|
||||||
Statement stmt;
|
|
||||||
try {
|
|
||||||
stmt = parser.Statement();
|
|
||||||
} catch (Exception ex) {
|
|
||||||
log.error("请注意,SQL语法可能存在问题---> {}", ex.getMessage());
|
|
||||||
throw new JeecgSqlInjectionException("请注意,SQL语法可能存在问题:"+sql);
|
|
||||||
}
|
|
||||||
if (null != visitor) {
|
|
||||||
parser.getASTRoot().jjtAccept(visitor, null);
|
|
||||||
}
|
|
||||||
if (null != sqlSyntaxAnalyzer) {
|
|
||||||
stmt.accept(sqlSyntaxAnalyzer.resetChanged());
|
|
||||||
}
|
|
||||||
return new SqlParserInfo(stmt.toString(), stmt, (SimpleNode) parser.getASTRoot());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 调用{@link CCJSqlParser}解析SQL语句部件返回解析生成的对象,如{@code 'ORDER BY id DESC'}
|
|
||||||
*
|
|
||||||
* @param <T>
|
|
||||||
* @param input
|
|
||||||
* @param method 指定调用的{@link CCJSqlParser}解析方法
|
|
||||||
* @param targetType 返回的解析对象类型
|
|
||||||
* @return
|
|
||||||
* @since 3.18.3
|
|
||||||
*/
|
|
||||||
public static <T> T parseComponent(String input, String method, Class<T> targetType) {
|
|
||||||
try {
|
|
||||||
CCJSqlParser parser = new CCJSqlParser(new StringProvider(input));
|
|
||||||
try {
|
|
||||||
return checkNotNull(targetType, "targetType is null").cast(parser.getClass().getMethod(method).invoke(parser));
|
|
||||||
} catch (InvocationTargetException e) {
|
|
||||||
Throwables.throwIfUnchecked(e.getTargetException());
|
|
||||||
throw new RuntimeException(e.getTargetException());
|
|
||||||
}
|
|
||||||
} catch (IllegalAccessException | NoSuchMethodException | SecurityException e) {
|
|
||||||
Throwables.throwIfUnchecked(e);
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 如果{@link Column}没有定义table,且字段名为true/false(不区分大小写)则视为布尔常量
|
|
||||||
*
|
|
||||||
* @param column
|
|
||||||
*/
|
|
||||||
public static boolean isBoolean(Column column) {
|
|
||||||
return null != column && null == column.getTable() &&
|
|
||||||
Pattern.compile("(true|false)", Pattern.CASE_INSENSITIVE).matcher(column.getColumnName()).matches();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class SqlParserInfo {
|
|
||||||
public String nativeSql;
|
|
||||||
public Statement statement;
|
|
||||||
public SimpleNode simpleNode;
|
|
||||||
|
|
||||||
SqlParserInfo(String nativeSql, Statement statement, SimpleNode simpleNode) {
|
|
||||||
this.nativeSql = nativeSql;
|
|
||||||
this.statement = statement;
|
|
||||||
this.simpleNode = simpleNode;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@ -1,37 +0,0 @@
|
|||||||
package org.jeecg.common.util.sqlInjection.parse;
|
|
||||||
|
|
||||||
import net.sf.jsqlparser.util.TablesNamesFinder;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* SQL语句分析转换器基类<br>
|
|
||||||
* 基于SQL语法对象实现对SQL的修改
|
|
||||||
* (暂时用不到)
|
|
||||||
*
|
|
||||||
* @author guyadong
|
|
||||||
* @since 3.17.0
|
|
||||||
*/
|
|
||||||
public class SqlSyntaxNormalizer extends TablesNamesFinder {
|
|
||||||
protected static final ThreadLocal<Boolean> changed = new ThreadLocal<>();
|
|
||||||
|
|
||||||
public SqlSyntaxNormalizer() {
|
|
||||||
super();
|
|
||||||
init(true);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 语句改变返回{@code true},否则返回{@code false}
|
|
||||||
*/
|
|
||||||
public boolean changed() {
|
|
||||||
return Boolean.TRUE.equals(changed.get());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 复位线程局部变量{@link #changed}状态
|
|
||||||
*/
|
|
||||||
public SqlSyntaxNormalizer resetChanged() {
|
|
||||||
changed.remove();
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@ -1,255 +1,255 @@
|
|||||||
package org.jeecg.common.util.sqlparse;
|
//package org.jeecg.common.util.sqlparse;
|
||||||
|
//
|
||||||
import lombok.extern.slf4j.Slf4j;
|
//import lombok.extern.slf4j.Slf4j;
|
||||||
import net.sf.jsqlparser.JSQLParserException;
|
//import net.sf.jsqlparser.JSQLParserException;
|
||||||
import net.sf.jsqlparser.expression.*;
|
//import net.sf.jsqlparser.expression.*;
|
||||||
import net.sf.jsqlparser.parser.CCJSqlParserManager;
|
//import net.sf.jsqlparser.parser.CCJSqlParserManager;
|
||||||
import net.sf.jsqlparser.schema.Column;
|
//import net.sf.jsqlparser.schema.Column;
|
||||||
import net.sf.jsqlparser.schema.Table;
|
//import net.sf.jsqlparser.schema.Table;
|
||||||
import net.sf.jsqlparser.statement.Statement;
|
//import net.sf.jsqlparser.statement.Statement;
|
||||||
import net.sf.jsqlparser.statement.select.*;
|
//import net.sf.jsqlparser.statement.select.*;
|
||||||
import org.jeecg.common.exception.JeecgBootException;
|
//import org.jeecg.common.exception.JeecgBootException;
|
||||||
import org.jeecg.common.util.sqlparse.vo.SelectSqlInfo;
|
//import org.jeecg.common.util.sqlparse.vo.SelectSqlInfo;
|
||||||
|
//
|
||||||
import java.io.StringReader;
|
//import java.io.StringReader;
|
||||||
import java.util.ArrayList;
|
//import java.util.ArrayList;
|
||||||
import java.util.HashMap;
|
//import java.util.HashMap;
|
||||||
import java.util.List;
|
//import java.util.List;
|
||||||
import java.util.Map;
|
//import java.util.Map;
|
||||||
|
//
|
||||||
/**
|
///**
|
||||||
* 解析所有表名和字段的类
|
// * 解析所有表名和字段的类
|
||||||
*/
|
// */
|
||||||
@Slf4j
|
//@Slf4j
|
||||||
public class JSqlParserAllTableManager {
|
//public class JSqlParserAllTableManager {
|
||||||
|
//
|
||||||
private final String sql;
|
// private final String sql;
|
||||||
private final Map<String, SelectSqlInfo> allTableMap = new HashMap<>();
|
// private final Map<String, SelectSqlInfo> allTableMap = new HashMap<>();
|
||||||
/**
|
// /**
|
||||||
* 别名对应实际表名
|
// * 别名对应实际表名
|
||||||
*/
|
// */
|
||||||
private final Map<String, String> tableAliasMap = new HashMap<>();
|
// private final Map<String, String> tableAliasMap = new HashMap<>();
|
||||||
|
//
|
||||||
/**
|
// /**
|
||||||
* 解析后的sql
|
// * 解析后的sql
|
||||||
*/
|
// */
|
||||||
private String parsedSql = null;
|
// private String parsedSql = null;
|
||||||
|
//
|
||||||
JSqlParserAllTableManager(String selectSql) {
|
// JSqlParserAllTableManager(String selectSql) {
|
||||||
this.sql = selectSql;
|
// this.sql = selectSql;
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
/**
|
// /**
|
||||||
* 开始解析
|
// * 开始解析
|
||||||
*
|
// *
|
||||||
* @return
|
// * @return
|
||||||
* @throws JSQLParserException
|
// * @throws JSQLParserException
|
||||||
*/
|
// */
|
||||||
public Map<String, SelectSqlInfo> parse() throws JSQLParserException {
|
// public Map<String, SelectSqlInfo> parse() throws JSQLParserException {
|
||||||
// 1. 创建解析器
|
// // 1. 创建解析器
|
||||||
CCJSqlParserManager mgr = new CCJSqlParserManager();
|
// CCJSqlParserManager mgr = new CCJSqlParserManager();
|
||||||
// 2. 使用解析器解析sql生成具有层次结构的java类
|
// // 2. 使用解析器解析sql生成具有层次结构的java类
|
||||||
Statement stmt = mgr.parse(new StringReader(this.sql));
|
// Statement stmt = mgr.parse(new StringReader(this.sql));
|
||||||
if (stmt instanceof Select) {
|
// if (stmt instanceof Select) {
|
||||||
Select selectStatement = (Select) stmt;
|
// Select selectStatement = (Select) stmt;
|
||||||
SelectBody selectBody = selectStatement.getSelectBody();
|
// SelectBody selectBody = selectStatement.getSelectBody();
|
||||||
this.parsedSql = selectBody.toString();
|
// this.parsedSql = selectBody.toString();
|
||||||
// 3. 解析select查询sql的信息
|
// // 3. 解析select查询sql的信息
|
||||||
if (selectBody instanceof PlainSelect) {
|
// if (selectBody instanceof PlainSelect) {
|
||||||
PlainSelect plainSelect = (PlainSelect) selectBody;
|
// PlainSelect plainSelect = (PlainSelect) selectBody;
|
||||||
// 4. 合并 fromItems
|
// // 4. 合并 fromItems
|
||||||
List<FromItem> fromItems = new ArrayList<>();
|
// List<FromItem> fromItems = new ArrayList<>();
|
||||||
fromItems.add(plainSelect.getFromItem());
|
// fromItems.add(plainSelect.getFromItem());
|
||||||
// 4.1 处理join的表
|
// // 4.1 处理join的表
|
||||||
List<Join> joins = plainSelect.getJoins();
|
// List<Join> joins = plainSelect.getJoins();
|
||||||
if (joins != null) {
|
// if (joins != null) {
|
||||||
joins.forEach(join -> fromItems.add(join.getRightItem()));
|
// joins.forEach(join -> fromItems.add(join.getRightItem()));
|
||||||
}
|
// }
|
||||||
// 5. 处理 fromItems
|
// // 5. 处理 fromItems
|
||||||
for (FromItem fromItem : fromItems) {
|
// for (FromItem fromItem : fromItems) {
|
||||||
// 5.1 通过表名的方式from
|
// // 5.1 通过表名的方式from
|
||||||
if (fromItem instanceof Table) {
|
// if (fromItem instanceof Table) {
|
||||||
this.addSqlInfoByTable((Table) fromItem);
|
// this.addSqlInfoByTable((Table) fromItem);
|
||||||
}
|
// }
|
||||||
// 5.2 通过子查询的方式from
|
// // 5.2 通过子查询的方式from
|
||||||
else if (fromItem instanceof SubSelect) {
|
// else if (fromItem instanceof SubSelect) {
|
||||||
this.handleSubSelect((SubSelect) fromItem);
|
// this.handleSubSelect((SubSelect) fromItem);
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
// 6. 解析 selectFields
|
// // 6. 解析 selectFields
|
||||||
List<SelectItem> selectItems = plainSelect.getSelectItems();
|
// List<SelectItem> selectItems = plainSelect.getSelectItems();
|
||||||
for (SelectItem selectItem : selectItems) {
|
// for (SelectItem selectItem : selectItems) {
|
||||||
// 6.1 查询的是全部字段
|
// // 6.1 查询的是全部字段
|
||||||
if (selectItem instanceof AllColumns) {
|
// if (selectItem instanceof AllColumns) {
|
||||||
// 当 selectItem 为 AllColumns 时,fromItem 必定为 Table
|
// // 当 selectItem 为 AllColumns 时,fromItem 必定为 Table
|
||||||
String tableName = plainSelect.getFromItem(Table.class).getName();
|
// String tableName = plainSelect.getFromItem(Table.class).getName();
|
||||||
// 此处必定不为空,因为在解析 fromItem 时,已经将表名添加到 allTableMap 中
|
// // 此处必定不为空,因为在解析 fromItem 时,已经将表名添加到 allTableMap 中
|
||||||
SelectSqlInfo sqlInfo = this.allTableMap.get(tableName);
|
// SelectSqlInfo sqlInfo = this.allTableMap.get(tableName);
|
||||||
assert sqlInfo != null;
|
// assert sqlInfo != null;
|
||||||
// 设置为查询全部字段
|
// // 设置为查询全部字段
|
||||||
sqlInfo.setSelectAll(true);
|
// sqlInfo.setSelectAll(true);
|
||||||
sqlInfo.setSelectFields(null);
|
// sqlInfo.setSelectFields(null);
|
||||||
sqlInfo.setRealSelectFields(null);
|
// sqlInfo.setRealSelectFields(null);
|
||||||
}
|
// }
|
||||||
// 6.2 查询的是带表别名( u.* )的全部字段
|
// // 6.2 查询的是带表别名( u.* )的全部字段
|
||||||
else if (selectItem instanceof AllTableColumns) {
|
// else if (selectItem instanceof AllTableColumns) {
|
||||||
AllTableColumns allTableColumns = (AllTableColumns) selectItem;
|
// AllTableColumns allTableColumns = (AllTableColumns) selectItem;
|
||||||
String aliasName = allTableColumns.getTable().getName();
|
// String aliasName = allTableColumns.getTable().getName();
|
||||||
// 通过别名获取表名
|
// // 通过别名获取表名
|
||||||
String tableName = this.tableAliasMap.get(aliasName);
|
// String tableName = this.tableAliasMap.get(aliasName);
|
||||||
if (tableName == null) {
|
// if (tableName == null) {
|
||||||
tableName = aliasName;
|
// tableName = aliasName;
|
||||||
}
|
// }
|
||||||
SelectSqlInfo sqlInfo = this.allTableMap.get(tableName);
|
// SelectSqlInfo sqlInfo = this.allTableMap.get(tableName);
|
||||||
// 如果此处为空,则说明该字段是通过子查询获取的,所以可以不处理,只有实际表才需要处理
|
// // 如果此处为空,则说明该字段是通过子查询获取的,所以可以不处理,只有实际表才需要处理
|
||||||
if (sqlInfo != null) {
|
// if (sqlInfo != null) {
|
||||||
// 设置为查询全部字段
|
// // 设置为查询全部字段
|
||||||
sqlInfo.setSelectAll(true);
|
// sqlInfo.setSelectAll(true);
|
||||||
sqlInfo.setSelectFields(null);
|
// sqlInfo.setSelectFields(null);
|
||||||
sqlInfo.setRealSelectFields(null);
|
// sqlInfo.setRealSelectFields(null);
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
// 6.3 各种字段表达式处理
|
// // 6.3 各种字段表达式处理
|
||||||
else if (selectItem instanceof SelectExpressionItem) {
|
// else if (selectItem instanceof SelectExpressionItem) {
|
||||||
SelectExpressionItem selectExpressionItem = (SelectExpressionItem) selectItem;
|
// SelectExpressionItem selectExpressionItem = (SelectExpressionItem) selectItem;
|
||||||
Expression expression = selectExpressionItem.getExpression();
|
// Expression expression = selectExpressionItem.getExpression();
|
||||||
Alias alias = selectExpressionItem.getAlias();
|
// Alias alias = selectExpressionItem.getAlias();
|
||||||
this.handleExpression(expression, alias, plainSelect.getFromItem());
|
// this.handleExpression(expression, alias, plainSelect.getFromItem());
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
} else {
|
// } else {
|
||||||
log.warn("暂时尚未处理该类型的 SelectBody: {}", selectBody.getClass().getName());
|
// log.warn("暂时尚未处理该类型的 SelectBody: {}", selectBody.getClass().getName());
|
||||||
throw new JeecgBootException("暂时尚未处理该类型的 SelectBody");
|
// throw new JeecgBootException("暂时尚未处理该类型的 SelectBody");
|
||||||
}
|
// }
|
||||||
} else {
|
// } else {
|
||||||
// 非 select 查询sql,不做处理
|
// // 非 select 查询sql,不做处理
|
||||||
throw new JeecgBootException("非 select 查询sql,不做处理");
|
// throw new JeecgBootException("非 select 查询sql,不做处理");
|
||||||
}
|
// }
|
||||||
return this.allTableMap;
|
// return this.allTableMap;
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
/**
|
// /**
|
||||||
* 处理子查询
|
// * 处理子查询
|
||||||
*
|
// *
|
||||||
* @param subSelect
|
// * @param subSelect
|
||||||
*/
|
// */
|
||||||
private void handleSubSelect(SubSelect subSelect) {
|
// private void handleSubSelect(SubSelect subSelect) {
|
||||||
try {
|
// try {
|
||||||
String subSelectSql = subSelect.getSelectBody().toString();
|
// String subSelectSql = subSelect.getSelectBody().toString();
|
||||||
// 递归调用解析
|
// // 递归调用解析
|
||||||
Map<String, SelectSqlInfo> map = JSqlParserUtils.parseAllSelectTable(subSelectSql);
|
// Map<String, SelectSqlInfo> map = JSqlParserUtils.parseAllSelectTable(subSelectSql);
|
||||||
if (map != null) {
|
// if (map != null) {
|
||||||
this.assignMap(map);
|
// this.assignMap(map);
|
||||||
}
|
// }
|
||||||
} catch (Exception e) {
|
// } catch (Exception e) {
|
||||||
log.error("解析子查询出错", e);
|
// log.error("解析子查询出错", e);
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
/**
|
// /**
|
||||||
* 处理查询字段表达式
|
// * 处理查询字段表达式
|
||||||
*
|
// *
|
||||||
* @param expression
|
// * @param expression
|
||||||
*/
|
// */
|
||||||
private void handleExpression(Expression expression, Alias alias, FromItem fromItem) {
|
// private void handleExpression(Expression expression, Alias alias, FromItem fromItem) {
|
||||||
// 处理函数式字段 CONCAT(name,'(',age,')')
|
// // 处理函数式字段 CONCAT(name,'(',age,')')
|
||||||
if (expression instanceof Function) {
|
// if (expression instanceof Function) {
|
||||||
Function functionExp = (Function) expression;
|
// Function functionExp = (Function) expression;
|
||||||
List<Expression> expressions = functionExp.getParameters().getExpressions();
|
// List<Expression> expressions = functionExp.getParameters().getExpressions();
|
||||||
for (Expression expItem : expressions) {
|
// for (Expression expItem : expressions) {
|
||||||
this.handleExpression(expItem, null, fromItem);
|
// this.handleExpression(expItem, null, fromItem);
|
||||||
}
|
// }
|
||||||
return;
|
// return;
|
||||||
}
|
// }
|
||||||
// 处理字段上的子查询
|
// // 处理字段上的子查询
|
||||||
if (expression instanceof SubSelect) {
|
// if (expression instanceof SubSelect) {
|
||||||
this.handleSubSelect((SubSelect) expression);
|
// this.handleSubSelect((SubSelect) expression);
|
||||||
return;
|
// return;
|
||||||
}
|
// }
|
||||||
// 不处理字面量
|
// // 不处理字面量
|
||||||
if (expression instanceof StringValue ||
|
// if (expression instanceof StringValue ||
|
||||||
expression instanceof NullValue ||
|
// expression instanceof NullValue ||
|
||||||
expression instanceof LongValue ||
|
// expression instanceof LongValue ||
|
||||||
expression instanceof DoubleValue ||
|
// expression instanceof DoubleValue ||
|
||||||
expression instanceof HexValue ||
|
// expression instanceof HexValue ||
|
||||||
expression instanceof DateValue ||
|
// expression instanceof DateValue ||
|
||||||
expression instanceof TimestampValue ||
|
// expression instanceof TimestampValue ||
|
||||||
expression instanceof TimeValue
|
// expression instanceof TimeValue
|
||||||
) {
|
// ) {
|
||||||
return;
|
// return;
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
// 处理字段
|
// // 处理字段
|
||||||
if (expression instanceof Column) {
|
// if (expression instanceof Column) {
|
||||||
Column column = (Column) expression;
|
// Column column = (Column) expression;
|
||||||
// 查询字段名
|
// // 查询字段名
|
||||||
String fieldName = column.getColumnName();
|
// String fieldName = column.getColumnName();
|
||||||
String aliasName = fieldName;
|
// String aliasName = fieldName;
|
||||||
if (alias != null) {
|
// if (alias != null) {
|
||||||
aliasName = alias.getName();
|
// aliasName = alias.getName();
|
||||||
}
|
// }
|
||||||
String tableName;
|
// String tableName;
|
||||||
if (column.getTable() != null) {
|
// if (column.getTable() != null) {
|
||||||
// 通过列的表名获取 sqlInfo
|
// // 通过列的表名获取 sqlInfo
|
||||||
// 例如 user.name,这里的 tableName 就是 user
|
// // 例如 user.name,这里的 tableName 就是 user
|
||||||
tableName = column.getTable().getName();
|
// tableName = column.getTable().getName();
|
||||||
// 有可能是别名,需要转换为真实表名
|
// // 有可能是别名,需要转换为真实表名
|
||||||
if (this.tableAliasMap.get(tableName) != null) {
|
// if (this.tableAliasMap.get(tableName) != null) {
|
||||||
tableName = this.tableAliasMap.get(tableName);
|
// tableName = this.tableAliasMap.get(tableName);
|
||||||
}
|
// }
|
||||||
} else {
|
// } else {
|
||||||
// 当column的table为空时,说明是 fromItem 中的字段
|
// // 当column的table为空时,说明是 fromItem 中的字段
|
||||||
tableName = ((Table) fromItem).getName();
|
// tableName = ((Table) fromItem).getName();
|
||||||
}
|
// }
|
||||||
SelectSqlInfo $sqlInfo = this.allTableMap.get(tableName);
|
// SelectSqlInfo $sqlInfo = this.allTableMap.get(tableName);
|
||||||
if ($sqlInfo != null) {
|
// if ($sqlInfo != null) {
|
||||||
$sqlInfo.addSelectField(aliasName, fieldName);
|
// $sqlInfo.addSelectField(aliasName, fieldName);
|
||||||
} else {
|
// } else {
|
||||||
log.warn("发生意外情况,未找到表名为 {} 的 SelectSqlInfo", tableName);
|
// log.warn("发生意外情况,未找到表名为 {} 的 SelectSqlInfo", tableName);
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
/**
|
// /**
|
||||||
* 根据表名添加sqlInfo
|
// * 根据表名添加sqlInfo
|
||||||
*
|
// *
|
||||||
* @param table
|
// * @param table
|
||||||
*/
|
// */
|
||||||
private void addSqlInfoByTable(Table table) {
|
// private void addSqlInfoByTable(Table table) {
|
||||||
String tableName = table.getName();
|
// String tableName = table.getName();
|
||||||
// 解析 aliasName
|
// // 解析 aliasName
|
||||||
if (table.getAlias() != null) {
|
// if (table.getAlias() != null) {
|
||||||
this.tableAliasMap.put(table.getAlias().getName(), tableName);
|
// this.tableAliasMap.put(table.getAlias().getName(), tableName);
|
||||||
}
|
// }
|
||||||
SelectSqlInfo sqlInfo = new SelectSqlInfo(this.parsedSql);
|
// SelectSqlInfo sqlInfo = new SelectSqlInfo(this.parsedSql);
|
||||||
sqlInfo.setFromTableName(table.getName());
|
// sqlInfo.setFromTableName(table.getName());
|
||||||
this.allTableMap.put(sqlInfo.getFromTableName(), sqlInfo);
|
// this.allTableMap.put(sqlInfo.getFromTableName(), sqlInfo);
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
/**
|
// /**
|
||||||
* 合并map
|
// * 合并map
|
||||||
*
|
// *
|
||||||
* @param source
|
// * @param source
|
||||||
*/
|
// */
|
||||||
private void assignMap(Map<String, SelectSqlInfo> source) {
|
// private void assignMap(Map<String, SelectSqlInfo> source) {
|
||||||
for (Map.Entry<String, SelectSqlInfo> entry : source.entrySet()) {
|
// for (Map.Entry<String, SelectSqlInfo> entry : source.entrySet()) {
|
||||||
SelectSqlInfo sqlInfo = this.allTableMap.get(entry.getKey());
|
// SelectSqlInfo sqlInfo = this.allTableMap.get(entry.getKey());
|
||||||
if (sqlInfo == null) {
|
// if (sqlInfo == null) {
|
||||||
this.allTableMap.put(entry.getKey(), entry.getValue());
|
// this.allTableMap.put(entry.getKey(), entry.getValue());
|
||||||
} else {
|
// } else {
|
||||||
// 合并
|
// // 合并
|
||||||
if (sqlInfo.getSelectFields() == null) {
|
// if (sqlInfo.getSelectFields() == null) {
|
||||||
sqlInfo.setSelectFields(entry.getValue().getSelectFields());
|
// sqlInfo.setSelectFields(entry.getValue().getSelectFields());
|
||||||
} else {
|
// } else {
|
||||||
sqlInfo.getSelectFields().addAll(entry.getValue().getSelectFields());
|
// sqlInfo.getSelectFields().addAll(entry.getValue().getSelectFields());
|
||||||
}
|
// }
|
||||||
if (sqlInfo.getRealSelectFields() == null) {
|
// if (sqlInfo.getRealSelectFields() == null) {
|
||||||
sqlInfo.setRealSelectFields(entry.getValue().getRealSelectFields());
|
// sqlInfo.setRealSelectFields(entry.getValue().getRealSelectFields());
|
||||||
} else {
|
// } else {
|
||||||
sqlInfo.getRealSelectFields().addAll(entry.getValue().getRealSelectFields());
|
// sqlInfo.getRealSelectFields().addAll(entry.getValue().getRealSelectFields());
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
}
|
//}
|
||||||
|
|||||||
@ -1,190 +1,190 @@
|
|||||||
package org.jeecg.common.util.sqlparse;
|
//package org.jeecg.common.util.sqlparse;
|
||||||
|
//
|
||||||
import lombok.extern.slf4j.Slf4j;
|
//import lombok.extern.slf4j.Slf4j;
|
||||||
import net.sf.jsqlparser.JSQLParserException;
|
//import net.sf.jsqlparser.JSQLParserException;
|
||||||
import net.sf.jsqlparser.expression.*;
|
//import net.sf.jsqlparser.expression.*;
|
||||||
import net.sf.jsqlparser.parser.CCJSqlParserManager;
|
//import net.sf.jsqlparser.parser.CCJSqlParserManager;
|
||||||
import net.sf.jsqlparser.schema.Column;
|
//import net.sf.jsqlparser.schema.Column;
|
||||||
import net.sf.jsqlparser.schema.Table;
|
//import net.sf.jsqlparser.schema.Table;
|
||||||
import net.sf.jsqlparser.statement.Statement;
|
//import net.sf.jsqlparser.statement.Statement;
|
||||||
import net.sf.jsqlparser.statement.select.*;
|
//import net.sf.jsqlparser.statement.select.*;
|
||||||
import org.jeecg.common.exception.JeecgBootException;
|
//import org.jeecg.common.exception.JeecgBootException;
|
||||||
import org.jeecg.common.util.oConvertUtils;
|
//import org.jeecg.common.util.oConvertUtils;
|
||||||
import org.jeecg.common.util.sqlparse.vo.SelectSqlInfo;
|
//import org.jeecg.common.util.sqlparse.vo.SelectSqlInfo;
|
||||||
|
//
|
||||||
import java.io.StringReader;
|
//import java.io.StringReader;
|
||||||
import java.util.List;
|
//import java.util.List;
|
||||||
import java.util.Map;
|
//import java.util.Map;
|
||||||
|
//
|
||||||
@Slf4j
|
//@Slf4j
|
||||||
public class JSqlParserUtils {
|
//public class JSqlParserUtils {
|
||||||
|
//
|
||||||
/**
|
// /**
|
||||||
* 解析 查询(select)sql的信息,
|
// * 解析 查询(select)sql的信息,
|
||||||
* 此方法会展开所有子查询到一个map里,
|
// * 此方法会展开所有子查询到一个map里,
|
||||||
* key只存真实的表名,如果查询的没有真实的表名,则会被忽略。
|
// * key只存真实的表名,如果查询的没有真实的表名,则会被忽略。
|
||||||
* value只存真实的字段名,如果查询的没有真实的字段名,则会被忽略。
|
// * value只存真实的字段名,如果查询的没有真实的字段名,则会被忽略。
|
||||||
* <p>
|
// * <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
|
// * 例如: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=[]}
|
// * 解析后的结果为:{sys_user=[username, realname], demo=[age], sys_depart=[]}
|
||||||
*
|
// *
|
||||||
* @param selectSql
|
// * @param selectSql
|
||||||
* @return
|
// * @return
|
||||||
*/
|
// */
|
||||||
public static Map<String, SelectSqlInfo> parseAllSelectTable(String selectSql) throws JSQLParserException {
|
// public static Map<String, SelectSqlInfo> parseAllSelectTable(String selectSql) throws JSQLParserException {
|
||||||
if (oConvertUtils.isEmpty(selectSql)) {
|
// if (oConvertUtils.isEmpty(selectSql)) {
|
||||||
return null;
|
// return null;
|
||||||
}
|
// }
|
||||||
// log.info("解析查询Sql:{}", selectSql);
|
// // log.info("解析查询Sql:{}", selectSql);
|
||||||
JSqlParserAllTableManager allTableManager = new JSqlParserAllTableManager(selectSql);
|
// JSqlParserAllTableManager allTableManager = new JSqlParserAllTableManager(selectSql);
|
||||||
return allTableManager.parse();
|
// return allTableManager.parse();
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
/**
|
// /**
|
||||||
* 解析 查询(select)sql的信息,子查询嵌套
|
// * 解析 查询(select)sql的信息,子查询嵌套
|
||||||
*
|
// *
|
||||||
* @param selectSql
|
// * @param selectSql
|
||||||
* @return
|
// * @return
|
||||||
*/
|
// */
|
||||||
public static SelectSqlInfo parseSelectSqlInfo(String selectSql) throws JSQLParserException {
|
// public static SelectSqlInfo parseSelectSqlInfo(String selectSql) throws JSQLParserException {
|
||||||
if (oConvertUtils.isEmpty(selectSql)) {
|
// if (oConvertUtils.isEmpty(selectSql)) {
|
||||||
return null;
|
// return null;
|
||||||
}
|
// }
|
||||||
// log.info("解析查询Sql:{}", selectSql);
|
// // log.info("解析查询Sql:{}", selectSql);
|
||||||
// 使用 JSqlParer 解析sql
|
// // 使用 JSqlParer 解析sql
|
||||||
// 1、创建解析器
|
// // 1、创建解析器
|
||||||
CCJSqlParserManager mgr = new CCJSqlParserManager();
|
// CCJSqlParserManager mgr = new CCJSqlParserManager();
|
||||||
// 2、使用解析器解析sql生成具有层次结构的java类
|
// // 2、使用解析器解析sql生成具有层次结构的java类
|
||||||
Statement stmt = mgr.parse(new StringReader(selectSql));
|
// Statement stmt = mgr.parse(new StringReader(selectSql));
|
||||||
if (stmt instanceof Select) {
|
// if (stmt instanceof Select) {
|
||||||
Select selectStatement = (Select) stmt;
|
// Select selectStatement = (Select) stmt;
|
||||||
// 3、解析select查询sql的信息
|
// // 3、解析select查询sql的信息
|
||||||
return JSqlParserUtils.parseBySelectBody(selectStatement.getSelectBody());
|
// return JSqlParserUtils.parseBySelectBody(selectStatement.getSelectBody());
|
||||||
} else {
|
// } else {
|
||||||
// 非 select 查询sql,不做处理
|
// // 非 select 查询sql,不做处理
|
||||||
throw new JeecgBootException("非 select 查询sql,不做处理");
|
// throw new JeecgBootException("非 select 查询sql,不做处理");
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
/**
|
// /**
|
||||||
* 解析 select 查询sql的信息
|
// * 解析 select 查询sql的信息
|
||||||
*
|
// *
|
||||||
* @param selectBody
|
// * @param selectBody
|
||||||
* @return
|
// * @return
|
||||||
*/
|
// */
|
||||||
private static SelectSqlInfo parseBySelectBody(SelectBody selectBody) {
|
// private static SelectSqlInfo parseBySelectBody(SelectBody selectBody) {
|
||||||
// 判断是否使用了union等操作
|
// // 判断是否使用了union等操作
|
||||||
if (selectBody instanceof SetOperationList) {
|
// if (selectBody instanceof SetOperationList) {
|
||||||
// 如果使用了union等操作,则只解析第一个查询
|
// // 如果使用了union等操作,则只解析第一个查询
|
||||||
List<SelectBody> selectBodyList = ((SetOperationList) selectBody).getSelects();
|
// List<SelectBody> selectBodyList = ((SetOperationList) selectBody).getSelects();
|
||||||
return JSqlParserUtils.parseBySelectBody(selectBodyList.get(0));
|
// return JSqlParserUtils.parseBySelectBody(selectBodyList.get(0));
|
||||||
}
|
// }
|
||||||
// 简单的select查询
|
// // 简单的select查询
|
||||||
if (selectBody instanceof PlainSelect) {
|
// if (selectBody instanceof PlainSelect) {
|
||||||
SelectSqlInfo sqlInfo = new SelectSqlInfo(selectBody);
|
// SelectSqlInfo sqlInfo = new SelectSqlInfo(selectBody);
|
||||||
PlainSelect plainSelect = (PlainSelect) selectBody;
|
// PlainSelect plainSelect = (PlainSelect) selectBody;
|
||||||
FromItem fromItem = plainSelect.getFromItem();
|
// FromItem fromItem = plainSelect.getFromItem();
|
||||||
// 解析 aliasName
|
// // 解析 aliasName
|
||||||
if (fromItem.getAlias() != null) {
|
// if (fromItem.getAlias() != null) {
|
||||||
sqlInfo.setFromTableAliasName(fromItem.getAlias().getName());
|
// sqlInfo.setFromTableAliasName(fromItem.getAlias().getName());
|
||||||
}
|
// }
|
||||||
// 解析 表名
|
// // 解析 表名
|
||||||
if (fromItem instanceof Table) {
|
// if (fromItem instanceof Table) {
|
||||||
// 通过表名的方式from
|
// // 通过表名的方式from
|
||||||
Table fromTable = (Table) fromItem;
|
// Table fromTable = (Table) fromItem;
|
||||||
sqlInfo.setFromTableName(fromTable.getName());
|
// sqlInfo.setFromTableName(fromTable.getName());
|
||||||
} else if (fromItem instanceof SubSelect) {
|
// } else if (fromItem instanceof SubSelect) {
|
||||||
// 通过子查询的方式from
|
// // 通过子查询的方式from
|
||||||
SubSelect fromSubSelect = (SubSelect) fromItem;
|
// SubSelect fromSubSelect = (SubSelect) fromItem;
|
||||||
SelectSqlInfo subSqlInfo = JSqlParserUtils.parseBySelectBody(fromSubSelect.getSelectBody());
|
// SelectSqlInfo subSqlInfo = JSqlParserUtils.parseBySelectBody(fromSubSelect.getSelectBody());
|
||||||
sqlInfo.setFromSubSelect(subSqlInfo);
|
// sqlInfo.setFromSubSelect(subSqlInfo);
|
||||||
}
|
// }
|
||||||
// 解析 selectFields
|
// // 解析 selectFields
|
||||||
List<SelectItem> selectItems = plainSelect.getSelectItems();
|
// List<SelectItem> selectItems = plainSelect.getSelectItems();
|
||||||
for (SelectItem selectItem : selectItems) {
|
// for (SelectItem selectItem : selectItems) {
|
||||||
if (selectItem instanceof AllColumns || selectItem instanceof AllTableColumns) {
|
// if (selectItem instanceof AllColumns || selectItem instanceof AllTableColumns) {
|
||||||
// 全部字段
|
// // 全部字段
|
||||||
sqlInfo.setSelectAll(true);
|
// sqlInfo.setSelectAll(true);
|
||||||
sqlInfo.setSelectFields(null);
|
// sqlInfo.setSelectFields(null);
|
||||||
sqlInfo.setRealSelectFields(null);
|
// sqlInfo.setRealSelectFields(null);
|
||||||
break;
|
// break;
|
||||||
} else if (selectItem instanceof SelectExpressionItem) {
|
// } else if (selectItem instanceof SelectExpressionItem) {
|
||||||
// 获取单个查询字段名
|
// // 获取单个查询字段名
|
||||||
SelectExpressionItem selectExpressionItem = (SelectExpressionItem) selectItem;
|
// SelectExpressionItem selectExpressionItem = (SelectExpressionItem) selectItem;
|
||||||
Expression expression = selectExpressionItem.getExpression();
|
// Expression expression = selectExpressionItem.getExpression();
|
||||||
Alias alias = selectExpressionItem.getAlias();
|
// Alias alias = selectExpressionItem.getAlias();
|
||||||
JSqlParserUtils.handleExpression(sqlInfo, expression, alias);
|
// JSqlParserUtils.handleExpression(sqlInfo, expression, alias);
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
return sqlInfo;
|
// return sqlInfo;
|
||||||
} else {
|
// } else {
|
||||||
log.warn("暂时尚未处理该类型的 SelectBody: {}", selectBody.getClass().getName());
|
// log.warn("暂时尚未处理该类型的 SelectBody: {}", selectBody.getClass().getName());
|
||||||
throw new JeecgBootException("暂时尚未处理该类型的 SelectBody");
|
// throw new JeecgBootException("暂时尚未处理该类型的 SelectBody");
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
/**
|
// /**
|
||||||
* 处理查询字段表达式
|
// * 处理查询字段表达式
|
||||||
*
|
// *
|
||||||
* @param sqlInfo
|
// * @param sqlInfo
|
||||||
* @param expression
|
// * @param expression
|
||||||
* @param alias 是否有别名,无传null
|
// * @param alias 是否有别名,无传null
|
||||||
*/
|
// */
|
||||||
private static void handleExpression(SelectSqlInfo sqlInfo, Expression expression, Alias alias) {
|
// private static void handleExpression(SelectSqlInfo sqlInfo, Expression expression, Alias alias) {
|
||||||
// 处理函数式字段 CONCAT(name,'(',age,')')
|
// // 处理函数式字段 CONCAT(name,'(',age,')')
|
||||||
if (expression instanceof Function) {
|
// if (expression instanceof Function) {
|
||||||
JSqlParserUtils.handleFunctionExpression((Function) expression, sqlInfo);
|
// JSqlParserUtils.handleFunctionExpression((Function) expression, sqlInfo);
|
||||||
return;
|
// return;
|
||||||
}
|
// }
|
||||||
// 处理字段上的子查询
|
// // 处理字段上的子查询
|
||||||
if (expression instanceof SubSelect) {
|
// if (expression instanceof SubSelect) {
|
||||||
SubSelect subSelect = (SubSelect) expression;
|
// SubSelect subSelect = (SubSelect) expression;
|
||||||
SelectSqlInfo subSqlInfo = JSqlParserUtils.parseBySelectBody(subSelect.getSelectBody());
|
// SelectSqlInfo subSqlInfo = JSqlParserUtils.parseBySelectBody(subSelect.getSelectBody());
|
||||||
// 注:字段上的子查询,必须只查询一个字段,否则会报错,所以可以放心合并
|
// // 注:字段上的子查询,必须只查询一个字段,否则会报错,所以可以放心合并
|
||||||
sqlInfo.getSelectFields().addAll(subSqlInfo.getSelectFields());
|
// sqlInfo.getSelectFields().addAll(subSqlInfo.getSelectFields());
|
||||||
sqlInfo.getRealSelectFields().addAll(subSqlInfo.getAllRealSelectFields());
|
// sqlInfo.getRealSelectFields().addAll(subSqlInfo.getAllRealSelectFields());
|
||||||
return;
|
// return;
|
||||||
}
|
// }
|
||||||
// 不处理字面量
|
// // 不处理字面量
|
||||||
if (expression instanceof StringValue ||
|
// if (expression instanceof StringValue ||
|
||||||
expression instanceof NullValue ||
|
// expression instanceof NullValue ||
|
||||||
expression instanceof LongValue ||
|
// expression instanceof LongValue ||
|
||||||
expression instanceof DoubleValue ||
|
// expression instanceof DoubleValue ||
|
||||||
expression instanceof HexValue ||
|
// expression instanceof HexValue ||
|
||||||
expression instanceof DateValue ||
|
// expression instanceof DateValue ||
|
||||||
expression instanceof TimestampValue ||
|
// expression instanceof TimestampValue ||
|
||||||
expression instanceof TimeValue
|
// expression instanceof TimeValue
|
||||||
) {
|
// ) {
|
||||||
return;
|
// return;
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
// 查询字段名
|
// // 查询字段名
|
||||||
String selectField = expression.toString();
|
// String selectField = expression.toString();
|
||||||
// 实际查询字段名
|
// // 实际查询字段名
|
||||||
String realSelectField = selectField;
|
// String realSelectField = selectField;
|
||||||
// 判断是否有别名
|
// // 判断是否有别名
|
||||||
if (alias != null) {
|
// if (alias != null) {
|
||||||
selectField = alias.getName();
|
// selectField = alias.getName();
|
||||||
}
|
// }
|
||||||
// 获取真实字段名
|
// // 获取真实字段名
|
||||||
if (expression instanceof Column) {
|
// if (expression instanceof Column) {
|
||||||
Column column = (Column) expression;
|
// Column column = (Column) expression;
|
||||||
realSelectField = column.getColumnName();
|
// realSelectField = column.getColumnName();
|
||||||
}
|
// }
|
||||||
sqlInfo.addSelectField(selectField, realSelectField);
|
// sqlInfo.addSelectField(selectField, realSelectField);
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
/**
|
// /**
|
||||||
* 处理函数式字段
|
// * 处理函数式字段
|
||||||
*
|
// *
|
||||||
* @param functionExp
|
// * @param functionExp
|
||||||
* @param sqlInfo
|
// * @param sqlInfo
|
||||||
*/
|
// */
|
||||||
private static void handleFunctionExpression(Function functionExp, SelectSqlInfo sqlInfo) {
|
// private static void handleFunctionExpression(Function functionExp, SelectSqlInfo sqlInfo) {
|
||||||
List<Expression> expressions = functionExp.getParameters().getExpressions();
|
// List<Expression> expressions = functionExp.getParameters().getExpressions();
|
||||||
for (Expression expression : expressions) {
|
// for (Expression expression : expressions) {
|
||||||
JSqlParserUtils.handleExpression(sqlInfo, expression, null);
|
// JSqlParserUtils.handleExpression(sqlInfo, expression, null);
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
}
|
//}
|
||||||
|
|||||||
@ -1,101 +1,101 @@
|
|||||||
package org.jeecg.common.util.sqlparse.vo;
|
//package org.jeecg.common.util.sqlparse.vo;
|
||||||
|
//
|
||||||
import lombok.Data;
|
//import lombok.Data;
|
||||||
import net.sf.jsqlparser.statement.select.SelectBody;
|
//import net.sf.jsqlparser.statement.select.SelectBody;
|
||||||
|
//
|
||||||
import java.util.HashSet;
|
//import java.util.HashSet;
|
||||||
import java.util.Set;
|
//import java.util.Set;
|
||||||
|
//
|
||||||
/**
|
///**
|
||||||
* select 查询 sql 的信息
|
// * select 查询 sql 的信息
|
||||||
*/
|
// */
|
||||||
@Data
|
//@Data
|
||||||
public class SelectSqlInfo {
|
//public class SelectSqlInfo {
|
||||||
|
//
|
||||||
/**
|
// /**
|
||||||
* 查询的表名,如果是子查询,则此处为null
|
// * 查询的表名,如果是子查询,则此处为null
|
||||||
*/
|
// */
|
||||||
private String fromTableName;
|
// private String fromTableName;
|
||||||
/**
|
// /**
|
||||||
* 表别名
|
// * 表别名
|
||||||
*/
|
// */
|
||||||
private String fromTableAliasName;
|
// private String fromTableAliasName;
|
||||||
/**
|
// /**
|
||||||
* 通过子查询获取的表信息,例如:select name from (select * from user) u
|
// * 通过子查询获取的表信息,例如:select name from (select * from user) u
|
||||||
* 如果不是子查询,则为null
|
// * 如果不是子查询,则为null
|
||||||
*/
|
// */
|
||||||
private SelectSqlInfo fromSubSelect;
|
// private SelectSqlInfo fromSubSelect;
|
||||||
/**
|
// /**
|
||||||
* 查询的字段集合,如果是 * 则为null,如果设了别名则为别名
|
// * 查询的字段集合,如果是 * 则为null,如果设了别名则为别名
|
||||||
*/
|
// */
|
||||||
private Set<String> selectFields;
|
// private Set<String> selectFields;
|
||||||
/**
|
// /**
|
||||||
* 真实的查询字段集合,如果是 * 则为null,如果设了别名则为原始字段名
|
// * 真实的查询字段集合,如果是 * 则为null,如果设了别名则为原始字段名
|
||||||
*/
|
// */
|
||||||
private Set<String> realSelectFields;
|
// private Set<String> realSelectFields;
|
||||||
/**
|
// /**
|
||||||
* 是否是查询所有字段
|
// * 是否是查询所有字段
|
||||||
*/
|
// */
|
||||||
private boolean selectAll;
|
// private boolean selectAll;
|
||||||
|
//
|
||||||
/**
|
// /**
|
||||||
* 解析之后的 SQL (关键字都是大写)
|
// * 解析之后的 SQL (关键字都是大写)
|
||||||
*/
|
// */
|
||||||
private final String parsedSql;
|
// private final String parsedSql;
|
||||||
|
//
|
||||||
public SelectSqlInfo(String parsedSql) {
|
// public SelectSqlInfo(String parsedSql) {
|
||||||
this.parsedSql = parsedSql;
|
// this.parsedSql = parsedSql;
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
public SelectSqlInfo(SelectBody selectBody) {
|
// public SelectSqlInfo(SelectBody selectBody) {
|
||||||
this.parsedSql = selectBody.toString();
|
// this.parsedSql = selectBody.toString();
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
public void addSelectField(String selectField, String realSelectField) {
|
// public void addSelectField(String selectField, String realSelectField) {
|
||||||
if (this.selectFields == null) {
|
// if (this.selectFields == null) {
|
||||||
this.selectFields = new HashSet<>();
|
// this.selectFields = new HashSet<>();
|
||||||
}
|
// }
|
||||||
if (this.realSelectFields == null) {
|
// if (this.realSelectFields == null) {
|
||||||
this.realSelectFields = new HashSet<>();
|
// this.realSelectFields = new HashSet<>();
|
||||||
}
|
// }
|
||||||
this.selectFields.add(selectField);
|
// this.selectFields.add(selectField);
|
||||||
this.realSelectFields.add(realSelectField);
|
// this.realSelectFields.add(realSelectField);
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
/**
|
// /**
|
||||||
* 获取所有字段,包括子查询里的。
|
// * 获取所有字段,包括子查询里的。
|
||||||
*
|
// *
|
||||||
* @return
|
// * @return
|
||||||
*/
|
// */
|
||||||
public Set<String> getAllRealSelectFields() {
|
// public Set<String> getAllRealSelectFields() {
|
||||||
Set<String> fields = new HashSet<>();
|
// Set<String> fields = new HashSet<>();
|
||||||
// 递归获取所有字段,起个直观的方法名为:
|
// // 递归获取所有字段,起个直观的方法名为:
|
||||||
this.recursiveGetAllFields(this, fields);
|
// this.recursiveGetAllFields(this, fields);
|
||||||
return fields;
|
// return fields;
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
/**
|
// /**
|
||||||
* 递归获取所有字段
|
// * 递归获取所有字段
|
||||||
*/
|
// */
|
||||||
private void recursiveGetAllFields(SelectSqlInfo sqlInfo, Set<String> fields) {
|
// private void recursiveGetAllFields(SelectSqlInfo sqlInfo, Set<String> fields) {
|
||||||
if (!sqlInfo.isSelectAll() && sqlInfo.getRealSelectFields() != null) {
|
// if (!sqlInfo.isSelectAll() && sqlInfo.getRealSelectFields() != null) {
|
||||||
fields.addAll(sqlInfo.getRealSelectFields());
|
// fields.addAll(sqlInfo.getRealSelectFields());
|
||||||
}
|
// }
|
||||||
if (sqlInfo.getFromSubSelect() != null) {
|
// if (sqlInfo.getFromSubSelect() != null) {
|
||||||
recursiveGetAllFields(sqlInfo.getFromSubSelect(), fields);
|
// recursiveGetAllFields(sqlInfo.getFromSubSelect(), fields);
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
@Override
|
// @Override
|
||||||
public String toString() {
|
// public String toString() {
|
||||||
return "SelectSqlInfo{" +
|
// return "SelectSqlInfo{" +
|
||||||
"fromTableName='" + fromTableName + '\'' +
|
// "fromTableName='" + fromTableName + '\'' +
|
||||||
", fromSubSelect=" + fromSubSelect +
|
// ", fromSubSelect=" + fromSubSelect +
|
||||||
", aliasName='" + fromTableAliasName + '\'' +
|
// ", aliasName='" + fromTableAliasName + '\'' +
|
||||||
", selectFields=" + selectFields +
|
// ", selectFields=" + selectFields +
|
||||||
", realSelectFields=" + realSelectFields +
|
// ", realSelectFields=" + realSelectFields +
|
||||||
", selectAll=" + selectAll +
|
// ", selectAll=" + selectAll +
|
||||||
"}";
|
// "}";
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
}
|
//}
|
||||||
|
|||||||
@ -3,7 +3,7 @@ package org.jeecg.config;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import javax.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
|
|
||||||
import org.jeecg.common.api.CommonAPI;
|
import org.jeecg.common.api.CommonAPI;
|
||||||
import org.jeecg.common.system.vo.DictModel;
|
import org.jeecg.common.system.vo.DictModel;
|
||||||
|
|||||||
@ -2,7 +2,9 @@ package org.jeecg.config;
|
|||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
import javax.servlet.*;
|
import com.alibaba.druid.spring.boot3.autoconfigure.DruidDataSourceAutoConfigure;
|
||||||
|
import com.alibaba.druid.spring.boot3.autoconfigure.properties.DruidStatProperties;
|
||||||
|
import jakarta.servlet.*;
|
||||||
|
|
||||||
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
|
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
@ -11,8 +13,6 @@ import org.springframework.boot.web.servlet.FilterRegistrationBean;
|
|||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure;
|
|
||||||
import com.alibaba.druid.spring.boot.autoconfigure.properties.DruidStatProperties;
|
|
||||||
import com.alibaba.druid.util.Utils;
|
import com.alibaba.druid.util.Utils;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import java.util.HashMap;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @author eightmonth@qq.com
|
||||||
* 启动程序修改DruidWallConfig配置
|
* 启动程序修改DruidWallConfig配置
|
||||||
* 允许SELECT语句的WHERE子句是一个永真条件
|
* 允许SELECT语句的WHERE子句是一个永真条件
|
||||||
* @author eightmonth
|
* @author eightmonth
|
||||||
|
|||||||
@ -1,7 +1,10 @@
|
|||||||
package org.jeecg.config;
|
package org.jeecg.config;
|
||||||
|
|
||||||
import org.jeecg.config.vo.*;
|
import org.jeecg.config.vo.*;
|
||||||
|
import org.springframework.beans.factory.config.BeanDefinition;
|
||||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Role;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
|
||||||
@ -11,6 +14,7 @@ import org.springframework.stereotype.Component;
|
|||||||
*/
|
*/
|
||||||
@Component("jeecgBaseConfig")
|
@Component("jeecgBaseConfig")
|
||||||
@ConfigurationProperties(prefix = "jeecg")
|
@ConfigurationProperties(prefix = "jeecg")
|
||||||
|
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||||
public class JeecgBaseConfig {
|
public class JeecgBaseConfig {
|
||||||
/**
|
/**
|
||||||
* 签名密钥串(字典等敏感接口)
|
* 签名密钥串(字典等敏感接口)
|
||||||
|
|||||||
@ -90,7 +90,7 @@ public class Swagger3Config implements WebMvcConfigurer {
|
|||||||
return new OpenAPI()
|
return new OpenAPI()
|
||||||
.info(new Info()
|
.info(new Info()
|
||||||
.title("JeecgBoot 后台服务API接口文档")
|
.title("JeecgBoot 后台服务API接口文档")
|
||||||
.version("3.7.4")
|
.version("3.8.0")
|
||||||
.contact(new Contact().name("北京国炬信息技术有限公司").url("www.jeccg.com").email("jeecgos@163.com"))
|
.contact(new Contact().name("北京国炬信息技术有限公司").url("www.jeccg.com").email("jeecgos@163.com"))
|
||||||
.description( "后台API接口")
|
.description( "后台API接口")
|
||||||
.termsOfService("NO terms of service")
|
.termsOfService("NO terms of service")
|
||||||
|
|||||||
@ -0,0 +1,19 @@
|
|||||||
|
package org.jeecg.config;
|
||||||
|
|
||||||
|
import io.undertow.server.DefaultByteBufferPool;
|
||||||
|
import io.undertow.websockets.jsr.WebSocketDeploymentInfo;
|
||||||
|
import org.springframework.boot.web.embedded.undertow.UndertowServletWebServerFactory;
|
||||||
|
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class UndertowCustomizer implements WebServerFactoryCustomizer<UndertowServletWebServerFactory> {
|
||||||
|
@Override
|
||||||
|
public void customize(UndertowServletWebServerFactory factory) {
|
||||||
|
factory.addDeploymentInfoCustomizers(deploymentInfo -> {
|
||||||
|
WebSocketDeploymentInfo webSocketDeploymentInfo = new WebSocketDeploymentInfo();
|
||||||
|
webSocketDeploymentInfo.setBuffers(new DefaultByteBufferPool(false, 1024));
|
||||||
|
deploymentInfo.addServletContextAttribute("io.undertow.websockets.jsr.WebSocketDeploymentInfo", webSocketDeploymentInfo);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -10,20 +10,23 @@ import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
|
|||||||
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
|
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
|
||||||
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
|
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
|
||||||
import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;
|
import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;
|
||||||
import io.micrometer.prometheus.PrometheusMeterRegistry;
|
import io.micrometer.prometheusmetrics.PrometheusMeterRegistry;
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
import org.springframework.beans.factory.InitializingBean;
|
import org.springframework.beans.factory.InitializingBean;
|
||||||
|
import org.springframework.beans.factory.ObjectProvider;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.beans.factory.annotation.Qualifier;
|
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.beans.factory.config.BeanPostProcessor;
|
import org.springframework.beans.factory.config.BeanPostProcessor;
|
||||||
import org.springframework.boot.actuate.trace.http.InMemoryHttpTraceRepository;
|
import org.springframework.boot.actuate.web.exchanges.InMemoryHttpExchangeRepository;
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||||
|
import org.springframework.boot.autoconfigure.jackson.JacksonProperties;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Conditional;
|
import org.springframework.context.annotation.Conditional;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.context.annotation.Primary;
|
import org.springframework.context.annotation.Primary;
|
||||||
import org.springframework.http.CacheControl;
|
import org.springframework.http.CacheControl;
|
||||||
import org.springframework.http.converter.HttpMessageConverter;
|
import org.springframework.http.converter.HttpMessageConverter;
|
||||||
|
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
|
||||||
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
|
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
|
||||||
import org.springframework.web.cors.CorsConfiguration;
|
import org.springframework.web.cors.CorsConfiguration;
|
||||||
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
||||||
@ -33,7 +36,6 @@ import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry
|
|||||||
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
|
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
|
||||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||||
|
|
||||||
import javax.annotation.Resource;
|
|
||||||
import java.text.SimpleDateFormat;
|
import java.text.SimpleDateFormat;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
|||||||
@ -3,8 +3,8 @@ package org.jeecg.config.filter;
|
|||||||
import org.jeecg.common.constant.CommonConstant;
|
import org.jeecg.common.constant.CommonConstant;
|
||||||
import org.jeecg.config.sign.util.BodyReaderHttpServletRequestWrapper;
|
import org.jeecg.config.sign.util.BodyReaderHttpServletRequestWrapper;
|
||||||
|
|
||||||
import javax.servlet.*;
|
import jakarta.servlet.*;
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -7,9 +7,9 @@ import org.jeecg.common.util.SpringContextUtils;
|
|||||||
import org.jeecg.common.util.TokenUtils;
|
import org.jeecg.common.util.TokenUtils;
|
||||||
import org.jeecg.common.util.oConvertUtils;
|
import org.jeecg.common.util.oConvertUtils;
|
||||||
|
|
||||||
import javax.servlet.*;
|
import jakarta.servlet.*;
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import javax.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -6,20 +6,17 @@ import org.apache.shiro.SecurityUtils;
|
|||||||
import org.jeecg.common.api.CommonAPI;
|
import org.jeecg.common.api.CommonAPI;
|
||||||
import org.jeecg.common.api.vo.Result;
|
import org.jeecg.common.api.vo.Result;
|
||||||
import org.jeecg.common.constant.CommonConstant;
|
import org.jeecg.common.constant.CommonConstant;
|
||||||
import org.jeecg.common.exception.JeecgBootException;
|
|
||||||
import org.jeecg.common.system.util.JwtUtil;
|
import org.jeecg.common.system.util.JwtUtil;
|
||||||
import org.jeecg.common.system.vo.LoginUser;
|
import org.jeecg.common.system.vo.LoginUser;
|
||||||
import org.jeecg.common.util.CommonUtils;
|
import org.jeecg.common.util.CommonUtils;
|
||||||
import org.jeecg.common.util.SpringContextUtils;
|
import org.jeecg.common.util.SpringContextUtils;
|
||||||
import org.jeecg.config.JeecgBaseConfig;
|
import org.jeecg.config.JeecgBaseConfig;
|
||||||
import org.jeecg.config.firewall.interceptor.enums.LowCodeUrlsEnum;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.util.AntPathMatcher;
|
|
||||||
import org.springframework.web.servlet.HandlerInterceptor;
|
import org.springframework.web.servlet.HandlerInterceptor;
|
||||||
|
|
||||||
import javax.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import javax.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.PrintWriter;
|
import java.io.PrintWriter;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
@ -70,6 +67,9 @@ public class LowCodeModeInterceptor implements HandlerInterceptor {
|
|||||||
Set<String> hasRoles = null;
|
Set<String> hasRoles = null;
|
||||||
if (loginUser == null) {
|
if (loginUser == null) {
|
||||||
loginUser = commonAPI.getUserByName(JwtUtil.getUserNameByToken(SpringContextUtils.getHttpServletRequest()));
|
loginUser = commonAPI.getUserByName(JwtUtil.getUserNameByToken(SpringContextUtils.getHttpServletRequest()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loginUser != null) {
|
||||||
//当前登录人拥有的角色
|
//当前登录人拥有的角色
|
||||||
hasRoles = commonAPI.queryUserRolesById(loginUser.getId());
|
hasRoles = commonAPI.queryUserRolesById(loginUser.getId());
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,6 +6,8 @@ import java.util.List;
|
|||||||
import cn.hutool.core.util.ObjectUtil;
|
import cn.hutool.core.util.ObjectUtil;
|
||||||
import com.baomidou.mybatisplus.extension.plugins.inner.DynamicTableNameInnerInterceptor;
|
import com.baomidou.mybatisplus.extension.plugins.inner.DynamicTableNameInnerInterceptor;
|
||||||
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
|
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
|
||||||
|
import net.sf.jsqlparser.expression.Expression;
|
||||||
|
import net.sf.jsqlparser.expression.LongValue;
|
||||||
import org.jeecg.common.config.TenantContext;
|
import org.jeecg.common.config.TenantContext;
|
||||||
import org.jeecg.common.constant.CommonConstant;
|
import org.jeecg.common.constant.CommonConstant;
|
||||||
import org.jeecg.common.constant.TenantConstant;
|
import org.jeecg.common.constant.TenantConstant;
|
||||||
@ -21,8 +23,6 @@ import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler;
|
|||||||
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
|
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
|
||||||
import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
|
import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
|
||||||
|
|
||||||
import net.sf.jsqlparser.expression.Expression;
|
|
||||||
import net.sf.jsqlparser.expression.LongValue;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 单数据源配置(jeecg.datasource.open = false时生效)
|
* 单数据源配置(jeecg.datasource.open = false时生效)
|
||||||
|
|||||||
@ -11,7 +11,7 @@ import org.jeecg.common.util.SpringContextUtils;
|
|||||||
import org.jeecg.config.mybatis.ThreadLocalDataHelper;
|
import org.jeecg.config.mybatis.ThreadLocalDataHelper;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -6,8 +6,8 @@ import org.apache.commons.lang3.StringUtils;
|
|||||||
import org.springframework.web.servlet.HandlerInterceptor;
|
import org.springframework.web.servlet.HandlerInterceptor;
|
||||||
import org.springframework.web.servlet.ModelAndView;
|
import org.springframework.web.servlet.ModelAndView;
|
||||||
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import javax.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 动态数据源切换拦截器
|
* 动态数据源切换拦截器
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
package org.jeecg.config.oss;
|
package org.jeecg.config.oss;
|
||||||
|
|
||||||
|
import jakarta.annotation.PostConstruct;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.jeecg.common.constant.CommonConstant;
|
import org.jeecg.common.constant.CommonConstant;
|
||||||
import org.jeecg.common.constant.SymbolConstant;
|
import org.jeecg.common.constant.SymbolConstant;
|
||||||
@ -26,7 +27,7 @@ public class MinioConfig {
|
|||||||
@Value(value = "${jeecg.minio.bucketName}")
|
@Value(value = "${jeecg.minio.bucketName}")
|
||||||
private String bucketName;
|
private String bucketName;
|
||||||
|
|
||||||
@Bean
|
@PostConstruct
|
||||||
public void initMinio(){
|
public void initMinio(){
|
||||||
if(!minioUrl.startsWith(CommonConstant.STR_HTTP)){
|
if(!minioUrl.startsWith(CommonConstant.STR_HTTP)){
|
||||||
minioUrl = "http://" + minioUrl;
|
minioUrl = "http://" + minioUrl;
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
package org.jeecg.config.oss;
|
package org.jeecg.config.oss;
|
||||||
|
|
||||||
|
import jakarta.annotation.PostConstruct;
|
||||||
import org.jeecg.common.util.oss.OssBootUtil;
|
import org.jeecg.common.util.oss.OssBootUtil;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
@ -26,7 +27,7 @@ public class OssConfiguration {
|
|||||||
private String staticDomain;
|
private String staticDomain;
|
||||||
|
|
||||||
|
|
||||||
@Bean
|
@PostConstruct
|
||||||
public void initOssBootConfiguration() {
|
public void initOssBootConfiguration() {
|
||||||
OssBootUtil.setEndPoint(endpoint);
|
OssBootUtil.setEndPoint(endpoint);
|
||||||
OssBootUtil.setAccessKeyId(accessKeyId);
|
OssBootUtil.setAccessKeyId(accessKeyId);
|
||||||
|
|||||||
@ -1,5 +1,8 @@
|
|||||||
package org.jeecg.config.shiro;
|
package org.jeecg.config.shiro;
|
||||||
|
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
import jakarta.servlet.DispatcherType;
|
||||||
|
import jakarta.servlet.Filter;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
|
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
|
||||||
import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;
|
import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;
|
||||||
@ -8,6 +11,7 @@ import org.apache.shiro.mgt.SecurityManager;
|
|||||||
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
|
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
|
||||||
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
|
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
|
||||||
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
|
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
|
||||||
|
import org.apache.shiro.spring.web.ShiroUrlPathHelper;
|
||||||
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
|
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
|
||||||
import org.crazycake.shiro.*;
|
import org.crazycake.shiro.*;
|
||||||
import org.jeecg.common.constant.CommonConstant;
|
import org.jeecg.common.constant.CommonConstant;
|
||||||
@ -17,25 +21,20 @@ import org.jeecg.config.shiro.filters.CustomShiroFilterFactoryBean;
|
|||||||
import org.jeecg.config.shiro.filters.JwtFilter;
|
import org.jeecg.config.shiro.filters.JwtFilter;
|
||||||
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
|
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
import org.springframework.beans.factory.config.BeanDefinition;
|
import org.springframework.beans.factory.config.BeanDefinition;
|
||||||
|
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
|
||||||
import org.springframework.boot.web.servlet.FilterRegistrationBean;
|
import org.springframework.boot.web.servlet.FilterRegistrationBean;
|
||||||
import org.springframework.context.annotation.*;
|
import org.springframework.context.annotation.*;
|
||||||
import org.springframework.core.annotation.AnnotationUtils;
|
|
||||||
import org.springframework.core.env.Environment;
|
import org.springframework.core.env.Environment;
|
||||||
import org.springframework.core.type.filter.AnnotationTypeFilter;
|
|
||||||
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
|
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
|
||||||
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
|
|
||||||
import org.springframework.util.CollectionUtils;
|
import org.springframework.util.CollectionUtils;
|
||||||
import org.springframework.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
import org.springframework.web.bind.annotation.*;
|
|
||||||
import org.springframework.web.filter.DelegatingFilterProxy;
|
import org.springframework.web.filter.DelegatingFilterProxy;
|
||||||
|
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
|
||||||
import redis.clients.jedis.HostAndPort;
|
import redis.clients.jedis.HostAndPort;
|
||||||
import redis.clients.jedis.JedisCluster;
|
import redis.clients.jedis.JedisCluster;
|
||||||
|
|
||||||
import javax.annotation.Resource;
|
|
||||||
import javax.servlet.DispatcherType;
|
|
||||||
import javax.servlet.Filter;
|
|
||||||
import java.lang.reflect.Method;
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -46,6 +45,7 @@ import java.util.*;
|
|||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Configuration
|
@Configuration
|
||||||
|
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||||
public class ShiroConfig {
|
public class ShiroConfig {
|
||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
@ -353,6 +353,18 @@ public class ShiroConfig {
|
|||||||
return manager;
|
return manager;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解决 ShiroRequestMappingConfig 获取 requestMappingHandlerMapping Bean 冲突
|
||||||
|
* spring-boot-autoconfigure:3.4.5 和 spring-boot-actuator-autoconfigure:3.4.5
|
||||||
|
*/
|
||||||
|
@Primary
|
||||||
|
@Bean
|
||||||
|
public RequestMappingHandlerMapping overridedRequestMappingHandlerMapping() {
|
||||||
|
RequestMappingHandlerMapping mapping = new RequestMappingHandlerMapping();
|
||||||
|
mapping.setUrlPathHelper(new ShiroUrlPathHelper());
|
||||||
|
return mapping;
|
||||||
|
}
|
||||||
|
|
||||||
private List<String> rebuildUrl(String[] bases, String[] uris) {
|
private List<String> rebuildUrl(String[] bases, String[] uris) {
|
||||||
List<String> urls = new ArrayList<>();
|
List<String> urls = new ArrayList<>();
|
||||||
for (String base : bases) {
|
for (String base : bases) {
|
||||||
|
|||||||
@ -20,11 +20,13 @@ import org.jeecg.common.util.SpringContextUtils;
|
|||||||
import org.jeecg.common.util.TokenUtils;
|
import org.jeecg.common.util.TokenUtils;
|
||||||
import org.jeecg.common.util.oConvertUtils;
|
import org.jeecg.common.util.oConvertUtils;
|
||||||
import org.jeecg.config.mybatis.MybatisPlusSaasConfig;
|
import org.jeecg.config.mybatis.MybatisPlusSaasConfig;
|
||||||
|
import org.springframework.beans.factory.config.BeanDefinition;
|
||||||
import org.springframework.context.annotation.Lazy;
|
import org.springframework.context.annotation.Lazy;
|
||||||
|
import org.springframework.context.annotation.Role;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
import javax.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -35,6 +37,7 @@ import java.util.Set;
|
|||||||
*/
|
*/
|
||||||
@Component
|
@Component
|
||||||
@Slf4j
|
@Slf4j
|
||||||
|
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||||
public class ShiroRealm extends AuthorizingRealm {
|
public class ShiroRealm extends AuthorizingRealm {
|
||||||
@Lazy
|
@Lazy
|
||||||
@Resource
|
@Resource
|
||||||
|
|||||||
@ -12,7 +12,7 @@ import org.apache.shiro.web.servlet.AbstractShiroFilter;
|
|||||||
import org.apache.shiro.mgt.SecurityManager;
|
import org.apache.shiro.mgt.SecurityManager;
|
||||||
import org.springframework.beans.factory.BeanInitializationException;
|
import org.springframework.beans.factory.BeanInitializationException;
|
||||||
|
|
||||||
import javax.servlet.Filter;
|
import jakarta.servlet.Filter;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -13,10 +13,10 @@ import org.springframework.http.HttpHeaders;
|
|||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.web.bind.annotation.RequestMethod;
|
import org.springframework.web.bind.annotation.RequestMethod;
|
||||||
|
|
||||||
import javax.servlet.ServletRequest;
|
import jakarta.servlet.ServletRequest;
|
||||||
import javax.servlet.ServletResponse;
|
import jakarta.servlet.ServletResponse;
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import javax.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @Description: 鉴权登录拦截器
|
* @Description: 鉴权登录拦截器
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
package org.jeecg.config.shiro.filters;
|
package org.jeecg.config.shiro.filters;
|
||||||
|
|
||||||
import javax.servlet.ServletRequest;
|
import jakarta.servlet.ServletRequest;
|
||||||
import javax.servlet.ServletResponse;
|
import jakarta.servlet.ServletResponse;
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import javax.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import org.apache.shiro.subject.Subject;
|
import org.apache.shiro.subject.Subject;
|
||||||
import org.apache.shiro.web.filter.AccessControlFilter;
|
import org.apache.shiro.web.filter.AccessControlFilter;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|||||||
@ -10,7 +10,7 @@ import org.springframework.context.annotation.Configuration;
|
|||||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||||
|
|
||||||
import javax.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 签名 拦截器配置
|
* 签名 拦截器配置
|
||||||
|
|||||||
@ -4,8 +4,8 @@ package org.jeecg.config.sign.interceptor;
|
|||||||
import java.io.PrintWriter;
|
import java.io.PrintWriter;
|
||||||
import java.util.SortedMap;
|
import java.util.SortedMap;
|
||||||
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import javax.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
import org.jeecg.common.api.vo.Result;
|
import org.jeecg.common.api.vo.Result;
|
||||||
import org.jeecg.common.constant.CommonConstant;
|
import org.jeecg.common.constant.CommonConstant;
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
package org.jeecg.config.sign.util;
|
package org.jeecg.config.sign.util;
|
||||||
|
|
||||||
import javax.servlet.ReadListener;
|
import jakarta.servlet.ReadListener;
|
||||||
import javax.servlet.ServletInputStream;
|
import jakarta.servlet.ServletInputStream;
|
||||||
import javax.servlet.ServletRequest;
|
import jakarta.servlet.ServletRequest;
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import javax.servlet.http.HttpServletRequestWrapper;
|
import jakarta.servlet.http.HttpServletRequestWrapper;
|
||||||
import java.io.*;
|
import java.io.*;
|
||||||
import java.nio.charset.Charset;
|
import java.nio.charset.Charset;
|
||||||
|
|
||||||
|
|||||||
@ -10,7 +10,7 @@ import java.util.Map;
|
|||||||
import java.util.SortedMap;
|
import java.util.SortedMap;
|
||||||
import java.util.TreeMap;
|
import java.util.TreeMap;
|
||||||
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.jeecg.common.constant.SymbolConstant;
|
import org.jeecg.common.constant.SymbolConstant;
|
||||||
|
|||||||
@ -14,8 +14,8 @@ import org.jeecg.common.util.SpringContextUtils;
|
|||||||
import org.jeecg.common.util.oConvertUtils;
|
import org.jeecg.common.util.oConvertUtils;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import javax.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -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();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
178
jeecg-boot/jeecg-boot-module/jeecg-boot-module-airag/pom.xml
Normal file
178
jeecg-boot/jeecg-boot-module/jeecg-boot-module-airag/pom.xml
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
<parent>
|
||||||
|
<groupId>org.jeecgframework.boot</groupId>
|
||||||
|
<artifactId>jeecg-boot-module</artifactId>
|
||||||
|
<version>3.8.1</version>
|
||||||
|
</parent>
|
||||||
|
|
||||||
|
<artifactId>jeecg-boot-module-airag</artifactId>
|
||||||
|
|
||||||
|
<repositories>
|
||||||
|
<repository>
|
||||||
|
<id>aliyun</id>
|
||||||
|
<name>aliyun Repository</name>
|
||||||
|
<url>https://maven.aliyun.com/repository/public</url>
|
||||||
|
<snapshots>
|
||||||
|
<enabled>false</enabled>
|
||||||
|
</snapshots>
|
||||||
|
</repository>
|
||||||
|
<repository>
|
||||||
|
<id>jeecg</id>
|
||||||
|
<name>jeecg Repository</name>
|
||||||
|
<url>https://maven.jeecg.org/nexus/content/repositories/jeecg</url>
|
||||||
|
<snapshots>
|
||||||
|
<enabled>false</enabled>
|
||||||
|
</snapshots>
|
||||||
|
</repository>
|
||||||
|
</repositories>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<kotlin.version>1.6.21</kotlin.version>
|
||||||
|
<liteflow.version>2.12.4.1</liteflow.version>
|
||||||
|
<langchain4j.version>0.35.0</langchain4j.version>
|
||||||
|
<apache-tika.version>2.9.1</apache-tika.version>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<!-- system单体 api-->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.jeecgframework.boot</groupId>
|
||||||
|
<artifactId>jeecg-system-local-api</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<!-- 微服务starter和system微服务 api
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.jeecgframework.boot</groupId>
|
||||||
|
<artifactId>jeecg-boot-starter-cloud</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.jeecgframework.boot</groupId>
|
||||||
|
<artifactId>jeecg-system-cloud-api</artifactId>
|
||||||
|
</dependency>-->
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.jeecgframework.boot3</groupId>
|
||||||
|
<artifactId>jeecg-aiflow</artifactId>
|
||||||
|
<version>1.0.5</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- aiflow 脚本依赖 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.yomahub</groupId>
|
||||||
|
<artifactId>liteflow-script-graaljs</artifactId>
|
||||||
|
<version>${liteflow.version}</version>
|
||||||
|
<scope>runtime</scope>
|
||||||
|
</dependency>
|
||||||
|
<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>
|
||||||
|
<artifactId>langchain4j-ollama</artifactId>
|
||||||
|
<version>${langchain4j.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>dev.langchain4j</groupId>
|
||||||
|
<artifactId>langchain4j-zhipu-ai</artifactId>
|
||||||
|
<version>${langchain4j.version}</version>
|
||||||
|
<exclusions>
|
||||||
|
<exclusion>
|
||||||
|
<artifactId>checker-qual</artifactId>
|
||||||
|
<groupId>org.checkerframework</groupId>
|
||||||
|
</exclusion>
|
||||||
|
<exclusion>
|
||||||
|
<artifactId>guava</artifactId>
|
||||||
|
<groupId>com.google.guava</groupId>
|
||||||
|
</exclusion>
|
||||||
|
</exclusions>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>dev.langchain4j</groupId>
|
||||||
|
<artifactId>langchain4j-qianfan</artifactId>
|
||||||
|
<version>${langchain4j.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>dev.langchain4j</groupId>
|
||||||
|
<artifactId>langchain4j-dashscope</artifactId>
|
||||||
|
<version>${langchain4j.version}</version>
|
||||||
|
<exclusions>
|
||||||
|
<exclusion>
|
||||||
|
<groupId>org.slf4j</groupId>
|
||||||
|
<artifactId>slf4j-simple</artifactId>
|
||||||
|
</exclusion>
|
||||||
|
<exclusion>
|
||||||
|
<artifactId>okio</artifactId>
|
||||||
|
<groupId>com.squareup.okio</groupId>
|
||||||
|
</exclusion>
|
||||||
|
</exclusions>
|
||||||
|
</dependency>
|
||||||
|
<!-- langChain4j vextor support -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>dev.langchain4j</groupId>
|
||||||
|
<artifactId>langchain4j-pgvector</artifactId>
|
||||||
|
<version>${langchain4j.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<!-- langChain4j Document Parser -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.tika</groupId>
|
||||||
|
<artifactId>tika-core</artifactId>
|
||||||
|
<version>${apache-tika.version}</version>
|
||||||
|
<exclusions>
|
||||||
|
<exclusion>
|
||||||
|
<artifactId>commons-io</artifactId>
|
||||||
|
<groupId>commons-io</groupId>
|
||||||
|
</exclusion>
|
||||||
|
</exclusions>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.tika</groupId>
|
||||||
|
<artifactId>tika-parser-html-module</artifactId>
|
||||||
|
<version>${apache-tika.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.tika</groupId>
|
||||||
|
<artifactId>tika-parser-pdf-module</artifactId>
|
||||||
|
<version>${apache-tika.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.tika</groupId>
|
||||||
|
<artifactId>tika-parser-text-module</artifactId>
|
||||||
|
<version>${apache-tika.version}</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
</project>
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
//package org.jeecg;
|
||||||
|
//
|
||||||
|
//import org.springframework.boot.SpringApplication;
|
||||||
|
//import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
//
|
||||||
|
//@SpringBootApplication
|
||||||
|
//public class JeecgAiRagApplication {
|
||||||
|
//
|
||||||
|
// public static void main(String[] args) {
|
||||||
|
// SpringApplication.run(JeecgAiRagApplication.class, args);
|
||||||
|
// }
|
||||||
|
//}
|
||||||
@ -0,0 +1,37 @@
|
|||||||
|
package org.jeecg.modules.airag.app.consts;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI应用常量类
|
||||||
|
*
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2025/2/25 14:52
|
||||||
|
*/
|
||||||
|
public class AiAppConsts {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 状态:启用
|
||||||
|
*/
|
||||||
|
public static final String STATUS_ENABLE = "enable";
|
||||||
|
/**
|
||||||
|
* 状态:禁用
|
||||||
|
*/
|
||||||
|
public static final String STATUS_DISABLE = "disable";
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 默认应用id
|
||||||
|
*/
|
||||||
|
public static final String DEFAULT_APP_ID = "default";
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 应用类型:简单聊天
|
||||||
|
*/
|
||||||
|
public static final String APP_TYPE_CHAT_SIMPLE = "chatSimple";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 应用类型:聊天流(高级编排)
|
||||||
|
*/
|
||||||
|
public static final String APP_TYPE_CHAT_FLOW = "chatFLow";
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,107 @@
|
|||||||
|
package org.jeecg.modules.airag.app.consts;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Description: 提示词常量
|
||||||
|
* @Author: chenrui
|
||||||
|
* @Date: 2025/3/12 15:03
|
||||||
|
*/
|
||||||
|
public class Prompts {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据提示生成智能体提示词
|
||||||
|
*/
|
||||||
|
public static final String GENERATE_LLM_PROMPT = "# 角色\n" +
|
||||||
|
"你是一位专业且高效的AI提示词工程师,擅长根据用户多样化需求自动生成高质量的结构化提示词模板,具备全面而敏锐的分析能力和出色的创造力。\n" +
|
||||||
|
"## 要求:\n" +
|
||||||
|
"1. \"\"\"只输出提示词,不要输出多余解释\"\"\"\n" +
|
||||||
|
"2. \"\"\"不要在前后增加代码块的md语法.\"\"\"\n" +
|
||||||
|
"2. 贴合用户需求,描述智能助手的定位、能力、知识储备\n" +
|
||||||
|
"3. 提示词应清晰、精确、易于理解,在保持质量的同时,尽可能简洁\n" +
|
||||||
|
"4. 严格按照给定的流程和格式执行任务,确保输出规范准确。\n" +
|
||||||
|
"\n" +
|
||||||
|
"## 流程\n" +
|
||||||
|
"### 1: 需求分析\n" +
|
||||||
|
"1. 当用户描述需求时,严格运用SCQA框架确认核心要素,精准分析和联想:\"当前场景(Situation)是什么?主要矛盾(Complication)有哪些?需要解决的关键问题(Question)是?预期达成什么效果(Answer)?\"\n" +
|
||||||
|
"2. 通过5W1H细致分析和联想细节:\"目标受众(Who)?使用场景(Where/When)?具体要实现什么(What)?为什么需要这些特征(Why)?如何量化效果(How)?\"\n" +
|
||||||
|
"\n" +
|
||||||
|
"### 2: 框架选择\n" +
|
||||||
|
"根据需求从给定模板库中匹配最佳提示词类型:\n" +
|
||||||
|
"* 角色扮演型:\n" +
|
||||||
|
"```\n" +
|
||||||
|
"你将扮演一个人物角色<角色名称>,以下是关于这个角色的详细设定,请根据这些信息来构建你的回答。 \n" +
|
||||||
|
"\n" +
|
||||||
|
"**人物基本信息:**\n" +
|
||||||
|
"- 你是:<角色的名称、身份等基本介绍>\n" +
|
||||||
|
"- 人称:第一人称\n" +
|
||||||
|
"- 出身背景与上下文:<交代角色背景信息和上下文>\n" +
|
||||||
|
"**性格特点:**\n" +
|
||||||
|
"- <性格特点描述>\n" +
|
||||||
|
"**语言风格:**\n" +
|
||||||
|
"- <语言风格描述> \n" +
|
||||||
|
"**人际关系:**\n" +
|
||||||
|
"- <人际关系描述>\n" +
|
||||||
|
"**过往经历:**\n" +
|
||||||
|
"- <过往经历描述>\n" +
|
||||||
|
"**经典台词或口头禅:**\n" +
|
||||||
|
"补充信息: 即你可以将动作、神情语气、心理活动、故事背景放在()中来表示,为对话提供补充信息。\n" +
|
||||||
|
"- 台词1:<角色台词示例1> \n" +
|
||||||
|
"- 台词2:<角色台词示例2>\n" +
|
||||||
|
"- ...\n" +
|
||||||
|
"\n" +
|
||||||
|
"要求: \n" +
|
||||||
|
"- 要求1\n" +
|
||||||
|
"- 要求2\n" +
|
||||||
|
"- ... \n" +
|
||||||
|
"```\n" +
|
||||||
|
"* 多步骤型:\n" +
|
||||||
|
"```\n" +
|
||||||
|
"# 角色 \n" +
|
||||||
|
"你是<角色设定(比如:xx领域的专家)>\n" +
|
||||||
|
"你的目标是<希望模型执行什么任务,达成什么目标>\n" +
|
||||||
|
"\n" +
|
||||||
|
"{#以下可以采用先总括,再展开详细说明的方式,描述你希望智能体在每一个步骤如何进行工作,具体的工作步骤数量可以根据实际需求增删#}\n" +
|
||||||
|
"## 工作步骤 \n" +
|
||||||
|
"1. <工作流程1的一句话概括> \n" +
|
||||||
|
"2. <工作流程2的一句话概括> \n" +
|
||||||
|
"3. <工作流程3的一句话概括>\n" +
|
||||||
|
"\n" +
|
||||||
|
"### 第一步 <工作流程1标题> \n" +
|
||||||
|
"<工作流程步骤1的具体工作要求和举例说明,可以分点列出希望在本步骤做哪些事情,需要完成什么阶段性的工作目标>\n" +
|
||||||
|
"### 第二步 <工作流程2标题> \n" +
|
||||||
|
"<工作流程步骤2的具体工作要求和举例说明,可以分点列出希望在本步骤做哪些事情,需要完成什么阶段性的工作目标>\n" +
|
||||||
|
"### 第三步 <工作流程3标题>\n" +
|
||||||
|
"<工作流程步骤3的具体工作要求和举例说明,可以分点列出希望在本步骤做哪些事情,需要完成什么阶段性的工作目标>\n" +
|
||||||
|
"```\n" +
|
||||||
|
"* 限制性模板:\n" +
|
||||||
|
"```\n" +
|
||||||
|
"# 角色:<角色名称>\n" +
|
||||||
|
"<角色概述和主要职责的一句话描述>\n" +
|
||||||
|
"\n" +
|
||||||
|
"## 目标:\n" +
|
||||||
|
"<角色的工作目标,如果有多目标可以分点列出,但建议更聚焦1-2个目标>\n" +
|
||||||
|
"\n" +
|
||||||
|
"## 技能:\n" +
|
||||||
|
"1. <为了实现目标,角色需要具备的技能1>\n" +
|
||||||
|
"2. <为了实现目标,角色需要具备的技能2>\n" +
|
||||||
|
"3. <为了实现目标,角色需要具备的技能3>\n" +
|
||||||
|
"\n" +
|
||||||
|
"## 工作流:\n" +
|
||||||
|
"1. <描述角色工作流程的第一步>\n" +
|
||||||
|
"2. <描述角色工作流程的第二步>\n" +
|
||||||
|
"3. <描述角色工作流程的第三步>\n" +
|
||||||
|
"\n" +
|
||||||
|
"## 输出格式:\n" +
|
||||||
|
"<如果对角色的输出格式有特定要求,可以在这里强调并举例说明想要的输出格式>\n" +
|
||||||
|
"\n" +
|
||||||
|
"## 限制:\n" +
|
||||||
|
"- <描述角色在互动过程中需要遵循的限制条件1>\n" +
|
||||||
|
"- <描述角色在互动过程中需要遵循的限制条件2>\n" +
|
||||||
|
"- <描述角色在互动过程中需要遵循的限制条件3>\n" +
|
||||||
|
"```\n" +
|
||||||
|
"\n" +
|
||||||
|
"### 3: 生成优化\n" +
|
||||||
|
"1. 输出时自动添加三重保障机制:\n" +
|
||||||
|
" - 反幻觉校验:\"所有数据需标注来源,不确定信息用[需核实]标记\"\n" +
|
||||||
|
" - 风格校准器:\"对比[目标风格]与生成内容的余弦相似度,低于0.7时启动重写\"\n" +
|
||||||
|
" - 伦理审查模块:\"自动过滤涉及隐私/偏见/违法内容,替换为[合规表达]\"";
|
||||||
|
}
|
||||||
@ -0,0 +1,156 @@
|
|||||||
|
package org.jeecg.modules.airag.app.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 lombok.extern.slf4j.Slf4j;
|
||||||
|
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.config.shiro.IgnoreAuth;
|
||||||
|
import org.jeecg.modules.airag.app.consts.AiAppConsts;
|
||||||
|
import org.jeecg.modules.airag.app.entity.AiragApp;
|
||||||
|
import org.jeecg.modules.airag.app.service.IAiragAppService;
|
||||||
|
import org.jeecg.modules.airag.app.service.IAiragChatService;
|
||||||
|
import org.jeecg.modules.airag.app.vo.AppDebugParams;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||||
|
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Description: AI应用
|
||||||
|
* @Author: jeecg-boot
|
||||||
|
* @Date: 2025-02-26
|
||||||
|
* @Version: V1.0
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/airag/app")
|
||||||
|
@Slf4j
|
||||||
|
public class AiragAppController extends JeecgController<AiragApp, IAiragAppService> {
|
||||||
|
@Autowired
|
||||||
|
private IAiragAppService airagAppService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private IAiragChatService airagChatService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页列表查询
|
||||||
|
*
|
||||||
|
* @param airagApp
|
||||||
|
* @param pageNo
|
||||||
|
* @param pageSize
|
||||||
|
* @param req
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
@GetMapping(value = "/list")
|
||||||
|
public Result<IPage<AiragApp>> queryPageList(AiragApp airagApp,
|
||||||
|
@RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo,
|
||||||
|
@RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize,
|
||||||
|
HttpServletRequest req) {
|
||||||
|
QueryWrapper<AiragApp> queryWrapper = QueryGenerator.initQueryWrapper(airagApp, req.getParameterMap());
|
||||||
|
Page<AiragApp> page = new Page<AiragApp>(pageNo, pageSize);
|
||||||
|
IPage<AiragApp> pageList = airagAppService.page(page, queryWrapper);
|
||||||
|
return Result.OK(pageList);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 新增或编辑
|
||||||
|
*
|
||||||
|
* @param airagApp
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
@RequestMapping(value = "/edit", method = {RequestMethod.PUT, RequestMethod.POST})
|
||||||
|
public Result<String> edit(@RequestBody AiragApp airagApp) {
|
||||||
|
AssertUtils.assertNotEmpty("参数异常", airagApp);
|
||||||
|
AssertUtils.assertNotEmpty("请输入应用名称", airagApp.getName());
|
||||||
|
AssertUtils.assertNotEmpty("请选择应用类型", airagApp.getType());
|
||||||
|
airagApp.setStatus(AiAppConsts.STATUS_ENABLE);
|
||||||
|
airagAppService.saveOrUpdate(airagApp);
|
||||||
|
return Result.OK("保存完成!", airagApp.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通过id删除
|
||||||
|
*
|
||||||
|
* @param id
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
@DeleteMapping(value = "/delete")
|
||||||
|
public Result<String> delete(@RequestParam(name = "id", required = true) String id) {
|
||||||
|
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查询
|
||||||
|
*
|
||||||
|
* @param id
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
@IgnoreAuth
|
||||||
|
@GetMapping(value = "/queryById")
|
||||||
|
public Result<AiragApp> queryById(@RequestParam(name = "id", required = true) String id) {
|
||||||
|
AiragApp airagApp = airagAppService.getById(id);
|
||||||
|
if (airagApp == null) {
|
||||||
|
return Result.error("未找到对应数据");
|
||||||
|
}
|
||||||
|
return Result.OK(airagApp);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 调试应用
|
||||||
|
*
|
||||||
|
* @param appDebugParams
|
||||||
|
* @return
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2025/2/28 10:49
|
||||||
|
*/
|
||||||
|
@PostMapping(value = "/debug")
|
||||||
|
public SseEmitter debugApp(@RequestBody AppDebugParams appDebugParams) {
|
||||||
|
return airagChatService.debugApp(appDebugParams);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据需求生成提示词
|
||||||
|
*
|
||||||
|
* @param prompt
|
||||||
|
* @return
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2025/3/12 15:30
|
||||||
|
*/
|
||||||
|
@GetMapping(value = "/prompt/generate")
|
||||||
|
public Result<?> generatePrompt(@RequestParam(name = "prompt", required = true) String prompt) {
|
||||||
|
return (Result<?>) airagAppService.generatePrompt(prompt,true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据需求生成提示词
|
||||||
|
*
|
||||||
|
* @param prompt
|
||||||
|
* @return
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2025/3/12 15:30
|
||||||
|
*/
|
||||||
|
@PostMapping(value = "/prompt/generate")
|
||||||
|
public SseEmitter generatePromptSse(@RequestParam(name = "prompt", required = true) String prompt) {
|
||||||
|
return (SseEmitter) airagAppService.generatePrompt(prompt,false);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,144 @@
|
|||||||
|
package org.jeecg.modules.airag.app.controller;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.jeecg.common.api.vo.Result;
|
||||||
|
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.web.bind.annotation.*;
|
||||||
|
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* airag应用-chat
|
||||||
|
*
|
||||||
|
* @Author: chenrui
|
||||||
|
* @Date: 2025-02-25 11:40
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/airag/chat")
|
||||||
|
public class AiragChatController {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
IAiragChatService chatService;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送消息
|
||||||
|
*
|
||||||
|
* @return 返回一个Result对象,表示发送消息的结果
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2025/2/25 11:42
|
||||||
|
*/
|
||||||
|
@IgnoreAuth
|
||||||
|
@PostMapping(value = "/send")
|
||||||
|
public SseEmitter send(@RequestBody ChatSendParams chatSendParams) {
|
||||||
|
return chatService.send(chatSendParams);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送消息 <br/>
|
||||||
|
* 兼容旧版浏览器
|
||||||
|
* @param content
|
||||||
|
* @param conversationId
|
||||||
|
* @param topicId
|
||||||
|
* @param appId
|
||||||
|
* @return
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2025/2/25 18:13
|
||||||
|
*/
|
||||||
|
@GetMapping(value = "/send")
|
||||||
|
public SseEmitter sendByGet(@RequestParam("content") String content,
|
||||||
|
@RequestParam(value = "conversationId", required = false) String conversationId,
|
||||||
|
@RequestParam(value = "topicId", required = false) String topicId,
|
||||||
|
@RequestParam(value = "appId", required = false) String appId) {
|
||||||
|
ChatSendParams chatSendParams = new ChatSendParams(content, conversationId, topicId, appId);
|
||||||
|
return chatService.send(chatSendParams);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有对话
|
||||||
|
*
|
||||||
|
* @return 返回一个Result对象,包含所有对话的信息
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2025/2/25 11:42
|
||||||
|
*/
|
||||||
|
@IgnoreAuth
|
||||||
|
@GetMapping(value = "/conversations")
|
||||||
|
public Result<?> getConversations(@RequestParam(value = "appId", required = false) String appId) {
|
||||||
|
return chatService.getConversations(appId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除会话
|
||||||
|
*
|
||||||
|
* @param id
|
||||||
|
* @return
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2025/3/3 16:55
|
||||||
|
*/
|
||||||
|
@IgnoreAuth
|
||||||
|
@DeleteMapping(value = "/conversation/{id}")
|
||||||
|
public Result<?> deleteConversation(@PathVariable("id") String id) {
|
||||||
|
return chatService.deleteConversation(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新会话标题
|
||||||
|
*
|
||||||
|
* @param updateTitleParams
|
||||||
|
* @return
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2025/3/3 16:55
|
||||||
|
*/
|
||||||
|
@IgnoreAuth
|
||||||
|
@PutMapping(value = "/conversation/update/title")
|
||||||
|
public Result<?> updateConversationTitle(@RequestBody ChatConversation updateTitleParams) {
|
||||||
|
return chatService.updateConversationTitle(updateTitleParams);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取消息
|
||||||
|
*
|
||||||
|
* @return 返回一个Result对象,包含消息的信息
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2025/2/25 11:42
|
||||||
|
*/
|
||||||
|
@IgnoreAuth
|
||||||
|
@GetMapping(value = "/messages")
|
||||||
|
public Result<?> getMessages(@RequestParam(value = "conversationId", required = true) String conversationId) {
|
||||||
|
return chatService.getMessages(conversationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清空消息
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2025/2/25 11:42
|
||||||
|
*/
|
||||||
|
@IgnoreAuth
|
||||||
|
@GetMapping(value = "/messages/clear/{conversationId}")
|
||||||
|
public Result<?> clearMessage(@PathVariable(value = "conversationId") String conversationId) {
|
||||||
|
return chatService.clearMessage(conversationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据请求ID停止某个请求的处理
|
||||||
|
*
|
||||||
|
* @param requestId 请求的唯一标识符,用于识别和停止特定的请求
|
||||||
|
* @return 返回一个Result对象,表示停止请求的结果
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2025/2/25 11:42
|
||||||
|
*/
|
||||||
|
@IgnoreAuth
|
||||||
|
@GetMapping(value = "/stop/{requestId}")
|
||||||
|
public Result<?> stop(@PathVariable(name = "requestId", required = true) String requestId) {
|
||||||
|
return chatService.stop(requestId);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,190 @@
|
|||||||
|
package org.jeecg.modules.airag.app.entity;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableField;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
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;
|
||||||
|
import org.jeecg.common.aspect.annotation.Dict;
|
||||||
|
import org.jeecg.common.util.oConvertUtils;
|
||||||
|
import org.jeecgframework.poi.excel.annotation.Excel;
|
||||||
|
import org.springframework.format.annotation.DateTimeFormat;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Description: AI应用
|
||||||
|
* @Author: jeecg-boot
|
||||||
|
* @Date: 2025-02-26
|
||||||
|
* @Version: V1.0
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@TableName("airag_app")
|
||||||
|
@Accessors(chain = true)
|
||||||
|
@EqualsAndHashCode(callSuper = false)
|
||||||
|
@Schema(description="AI应用")
|
||||||
|
public class AiragApp implements Serializable {
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 主键
|
||||||
|
*/
|
||||||
|
@TableId(type = IdType.ASSIGN_ID)
|
||||||
|
@Schema(description = "主键")
|
||||||
|
private String id;
|
||||||
|
/**
|
||||||
|
* 创建人
|
||||||
|
*/
|
||||||
|
@Schema(description = "创建人")
|
||||||
|
private String createBy;
|
||||||
|
/**
|
||||||
|
* 创建日期
|
||||||
|
*/
|
||||||
|
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
|
||||||
|
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||||
|
@Schema(description = "创建日期")
|
||||||
|
private java.util.Date createTime;
|
||||||
|
/**
|
||||||
|
* 更新人
|
||||||
|
*/
|
||||||
|
@Schema(description = "更新人")
|
||||||
|
private String updateBy;
|
||||||
|
/**
|
||||||
|
* 更新日期
|
||||||
|
*/
|
||||||
|
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
|
||||||
|
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||||
|
@Schema(description = "更新日期")
|
||||||
|
private java.util.Date updateTime;
|
||||||
|
/**
|
||||||
|
* 所属部门
|
||||||
|
*/
|
||||||
|
@Schema(description = "所属部门")
|
||||||
|
private String sysOrgCode;
|
||||||
|
/**
|
||||||
|
* 租户id
|
||||||
|
*/
|
||||||
|
@Excel(name = "租户id", width = 15)
|
||||||
|
@Schema(description = "租户id")
|
||||||
|
private String tenantId;
|
||||||
|
/**
|
||||||
|
* 应用名称
|
||||||
|
*/
|
||||||
|
@Excel(name = "应用名称", width = 15)
|
||||||
|
@Schema(description = "应用名称")
|
||||||
|
private String name;
|
||||||
|
/**
|
||||||
|
* 应用描述
|
||||||
|
*/
|
||||||
|
@Excel(name = "应用描述", width = 15)
|
||||||
|
@Schema(description = "应用描述")
|
||||||
|
private String descr;
|
||||||
|
/**
|
||||||
|
* 应用图标
|
||||||
|
*/
|
||||||
|
@Excel(name = "应用图标", width = 15)
|
||||||
|
@Schema(description = "应用图标")
|
||||||
|
private String icon;
|
||||||
|
/**
|
||||||
|
* 应用类型
|
||||||
|
*/
|
||||||
|
@Excel(name = "应用类型", width = 15, dicCode = "ai_app_type")
|
||||||
|
@Dict(dicCode = "ai_app_type")
|
||||||
|
@Schema(description = "应用类型")
|
||||||
|
private String type;
|
||||||
|
/**
|
||||||
|
* 开场白
|
||||||
|
*/
|
||||||
|
@Excel(name = "开场白", width = 15)
|
||||||
|
@Schema(description = "开场白")
|
||||||
|
private String prologue;
|
||||||
|
/**
|
||||||
|
* 预设问题
|
||||||
|
*/
|
||||||
|
@Excel(name = "预设问题", width = 15)
|
||||||
|
@Schema(description = "预设问题")
|
||||||
|
private String presetQuestion;
|
||||||
|
/**
|
||||||
|
* 提示词
|
||||||
|
*/
|
||||||
|
@Excel(name = "提示词", width = 15)
|
||||||
|
@Schema(description = "提示词")
|
||||||
|
private 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;
|
||||||
|
/**
|
||||||
|
* 历史消息数
|
||||||
|
*/
|
||||||
|
@Excel(name = "历史消息数", width = 15)
|
||||||
|
@Schema(description = "历史消息数")
|
||||||
|
private 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;
|
||||||
|
/**
|
||||||
|
* 流程
|
||||||
|
*/
|
||||||
|
@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;
|
||||||
|
/**
|
||||||
|
* 快捷指令
|
||||||
|
*/
|
||||||
|
@Excel(name = "快捷指令", width = 15)
|
||||||
|
@Schema(description = "快捷指令")
|
||||||
|
private String quickCommand;
|
||||||
|
/**
|
||||||
|
* 状态
|
||||||
|
*/
|
||||||
|
@Excel(name = "状态", width = 15)
|
||||||
|
@Schema(description = "状态")
|
||||||
|
private String status;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 元数据
|
||||||
|
*/
|
||||||
|
@Excel(name = "元数据", width = 15)
|
||||||
|
@Schema(description = "元数据")
|
||||||
|
private String metadata;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 知识库ids
|
||||||
|
*/
|
||||||
|
@TableField(exist = false)
|
||||||
|
private List<String> knowIds;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取知识库id
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2025/2/28 11:45
|
||||||
|
*/
|
||||||
|
public List<String> getKnowIds() {
|
||||||
|
if (oConvertUtils.isNotEmpty(knowledgeIds)) {
|
||||||
|
String[] knowIds = knowledgeIds.split(",");
|
||||||
|
return Arrays.asList(knowIds);
|
||||||
|
} else {
|
||||||
|
return new ArrayList<>(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
package org.jeecg.modules.airag.app.mapper;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import org.jeecg.modules.airag.app.entity.AiragApp;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Description: AI应用
|
||||||
|
* @Author: jeecg-boot
|
||||||
|
* @Date: 2025-02-26
|
||||||
|
* @Version: V1.0
|
||||||
|
*/
|
||||||
|
public interface AiragAppMapper extends BaseMapper<AiragApp> {
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-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">
|
||||||
|
|
||||||
|
</mapper>
|
||||||
@ -0,0 +1,24 @@
|
|||||||
|
package org.jeecg.modules.airag.app.service;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.extension.service.IService;
|
||||||
|
import org.jeecg.common.api.vo.Result;
|
||||||
|
import org.jeecg.modules.airag.app.entity.AiragApp;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Description: AI应用
|
||||||
|
* @Author: jeecg-boot
|
||||||
|
* @Date: 2025-02-26
|
||||||
|
* @Version: V1.0
|
||||||
|
*/
|
||||||
|
public interface IAiragAppService extends IService<AiragApp> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成提示词
|
||||||
|
* @param prompt
|
||||||
|
* @return blocking 是否阻塞
|
||||||
|
* @return
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2025/3/12 14:45
|
||||||
|
*/
|
||||||
|
Object generatePrompt(String prompt,boolean blocking);
|
||||||
|
}
|
||||||
@ -0,0 +1,95 @@
|
|||||||
|
package org.jeecg.modules.airag.app.service;
|
||||||
|
|
||||||
|
import org.jeecg.common.api.vo.Result;
|
||||||
|
import org.jeecg.modules.airag.app.vo.AppDebugParams;
|
||||||
|
import org.jeecg.modules.airag.app.vo.ChatConversation;
|
||||||
|
import org.jeecg.modules.airag.app.vo.ChatSendParams;
|
||||||
|
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ai聊天
|
||||||
|
*
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2025/2/25 13:36
|
||||||
|
*/
|
||||||
|
public interface IAiragChatService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送消息
|
||||||
|
*
|
||||||
|
* @param chatSendParams
|
||||||
|
* @return
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2025/2/25 13:39
|
||||||
|
*/
|
||||||
|
SseEmitter send(ChatSendParams chatSendParams);
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 调试应用
|
||||||
|
*
|
||||||
|
* @param appDebugParams
|
||||||
|
* @return
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2025/2/28 10:49
|
||||||
|
*/
|
||||||
|
SseEmitter debugApp(AppDebugParams appDebugParams);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 停止响应
|
||||||
|
*
|
||||||
|
* @param requestId
|
||||||
|
* @return
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2025/2/25 17:17
|
||||||
|
*/
|
||||||
|
Result<?> stop(String requestId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有对话
|
||||||
|
*
|
||||||
|
* @param appId
|
||||||
|
* @return
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2025/2/26 14:48
|
||||||
|
*/
|
||||||
|
Result<?> getConversations(String appId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取对话聊天记录
|
||||||
|
*
|
||||||
|
* @param conversationId
|
||||||
|
* @return
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2025/2/26 15:16
|
||||||
|
*/
|
||||||
|
Result<?> getMessages(String conversationId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除会话
|
||||||
|
*
|
||||||
|
* @param conversationId
|
||||||
|
* @return
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2025/3/3 16:55
|
||||||
|
*/
|
||||||
|
Result<?> deleteConversation(String conversationId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新会话标题
|
||||||
|
* @param updateTitleParams
|
||||||
|
* @return
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2025/3/3 17:02
|
||||||
|
*/
|
||||||
|
Result<?> updateConversationTitle(ChatConversation updateTitleParams);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清空消息
|
||||||
|
* @param conversationId
|
||||||
|
* @return
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2025/3/3 19:49
|
||||||
|
*/
|
||||||
|
Result<?> clearMessage(String conversationId);
|
||||||
|
}
|
||||||
@ -0,0 +1,157 @@
|
|||||||
|
package org.jeecg.modules.airag.app.service.impl;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson.JSONObject;
|
||||||
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
|
import dev.langchain4j.data.message.AiMessage;
|
||||||
|
import dev.langchain4j.data.message.ChatMessage;
|
||||||
|
import dev.langchain4j.data.message.SystemMessage;
|
||||||
|
import dev.langchain4j.data.message.UserMessage;
|
||||||
|
import dev.langchain4j.model.output.FinishReason;
|
||||||
|
import dev.langchain4j.service.TokenStream;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.jeecg.common.api.vo.Result;
|
||||||
|
import org.jeecg.common.util.AssertUtils;
|
||||||
|
import org.jeecg.common.util.UUIDGenerator;
|
||||||
|
import org.jeecg.modules.airag.app.consts.Prompts;
|
||||||
|
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.common.consts.AiragConsts;
|
||||||
|
import org.jeecg.modules.airag.common.handler.AIChatParams;
|
||||||
|
import org.jeecg.modules.airag.common.handler.IAIChatHandler;
|
||||||
|
import org.jeecg.modules.airag.common.utils.AiragLocalCache;
|
||||||
|
import org.jeecg.modules.airag.common.vo.event.EventData;
|
||||||
|
import org.jeecg.modules.airag.common.vo.event.EventFlowData;
|
||||||
|
import org.jeecg.modules.airag.common.vo.event.EventMessageData;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Description: AI应用
|
||||||
|
* @Author: jeecg-boot
|
||||||
|
* @Date: 2025-02-26
|
||||||
|
* @Version: V1.0
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
public class AiragAppServiceImpl extends ServiceImpl<AiragAppMapper, AiragApp> implements IAiragAppService {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
IAIChatHandler aiChatHandler;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object generatePrompt(String prompt, boolean blocking) {
|
||||||
|
AssertUtils.assertNotEmpty("请输入提示词", prompt);
|
||||||
|
List<ChatMessage> messages = Arrays.asList(new SystemMessage(Prompts.GENERATE_LLM_PROMPT), new UserMessage(prompt));
|
||||||
|
|
||||||
|
AIChatParams params = new AIChatParams();
|
||||||
|
params.setTemperature(0.8);
|
||||||
|
params.setTopP(0.9);
|
||||||
|
params.setPresencePenalty(0.1);
|
||||||
|
params.setFrequencyPenalty(0.1);
|
||||||
|
if(blocking){
|
||||||
|
String promptValue = aiChatHandler.completionsByDefaultModel(messages, params);
|
||||||
|
if (promptValue == null || promptValue.isEmpty()) {
|
||||||
|
return Result.error("生成失败");
|
||||||
|
}
|
||||||
|
return Result.OK("success", promptValue);
|
||||||
|
}else{
|
||||||
|
SseEmitter emitter = new SseEmitter(-0L);
|
||||||
|
// 异步运行(流式)
|
||||||
|
TokenStream tokenStream = aiChatHandler.chatByDefaultModel(messages, params);
|
||||||
|
/**
|
||||||
|
* 是否正在思考
|
||||||
|
*/
|
||||||
|
AtomicBoolean isThinking = new AtomicBoolean(false);
|
||||||
|
String requestId = UUIDGenerator.generate();
|
||||||
|
// ai聊天响应逻辑
|
||||||
|
tokenStream.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);
|
||||||
|
EventMessageData messageEventData = EventMessageData.builder()
|
||||||
|
.message(resMessage)
|
||||||
|
.build();
|
||||||
|
eventData.setData(messageEventData);
|
||||||
|
try {
|
||||||
|
String eventStr = JSONObject.toJSONString(eventData);
|
||||||
|
log.debug("[AI应用]接收LLM返回消息:{}", eventStr);
|
||||||
|
emitter.send(SseEmitter.event().data(eventStr));
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.onComplete((responseMessage) -> {
|
||||||
|
// 记录ai的回复
|
||||||
|
AiMessage aiMessage = responseMessage.content();
|
||||||
|
FinishReason finishReason = responseMessage.finishReason();
|
||||||
|
String respText = aiMessage.text();
|
||||||
|
if (FinishReason.STOP.equals(finishReason) || null == finishReason) {
|
||||||
|
// 正常结束
|
||||||
|
EventData eventData = new EventData(requestId, null, EventData.EVENT_MESSAGE_END);
|
||||||
|
try {
|
||||||
|
log.debug("[AI应用]接收LLM返回消息完成:{}", respText);
|
||||||
|
emitter.send(SseEmitter.event().data(eventData));
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
closeSSE(emitter, eventData);
|
||||||
|
} else if (FinishReason.TOOL_EXECUTION.equals(finishReason)) {
|
||||||
|
// 需要执行工具
|
||||||
|
// TODO author: chenrui for: date:2025/3/7
|
||||||
|
} else {
|
||||||
|
// 异常结束
|
||||||
|
log.error("调用模型异常:" + respText);
|
||||||
|
if (respText.contains("insufficient Balance")) {
|
||||||
|
respText = "大预言模型账号余额不足!";
|
||||||
|
}
|
||||||
|
EventData eventData = new EventData(requestId, null, EventData.EVENT_FLOW_ERROR);
|
||||||
|
eventData.setData(EventFlowData.builder().success(false).message(respText).build());
|
||||||
|
closeSSE(emitter, eventData);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.onError((Throwable error) -> {
|
||||||
|
// sse
|
||||||
|
String errMsg = "调用大模型接口失败:" + error.getMessage();
|
||||||
|
log.error(errMsg, error);
|
||||||
|
EventData eventData = new EventData(requestId, null, EventData.EVENT_FLOW_ERROR);
|
||||||
|
eventData.setData(EventFlowData.builder().success(false).message(errMsg).build());
|
||||||
|
closeSSE(emitter, eventData);
|
||||||
|
})
|
||||||
|
.start();
|
||||||
|
return emitter;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void closeSSE(SseEmitter emitter, EventData eventData) {
|
||||||
|
try {
|
||||||
|
// 发送完成事件
|
||||||
|
emitter.send(SseEmitter.event().data(eventData));
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error("终止会话时发生错误", e);
|
||||||
|
} finally {
|
||||||
|
// 从缓存中移除emitter
|
||||||
|
AiragLocalCache.remove(AiragConsts.CACHE_TYPE_SSE, eventData.getRequestId());
|
||||||
|
// 关闭emitter
|
||||||
|
emitter.complete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,901 @@
|
|||||||
|
package org.jeecg.modules.airag.app.service.impl;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson.JSONObject;
|
||||||
|
import dev.langchain4j.data.image.Image;
|
||||||
|
import dev.langchain4j.data.message.*;
|
||||||
|
import dev.langchain4j.model.output.FinishReason;
|
||||||
|
import dev.langchain4j.service.TokenStream;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.jeecg.common.api.vo.Result;
|
||||||
|
import org.jeecg.common.exception.JeecgBootBizTipException;
|
||||||
|
import org.jeecg.common.system.api.ISysBaseAPI;
|
||||||
|
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.service.IAiragAppService;
|
||||||
|
import org.jeecg.modules.airag.app.service.IAiragChatService;
|
||||||
|
import org.jeecg.modules.airag.app.vo.AppDebugParams;
|
||||||
|
import org.jeecg.modules.airag.app.vo.ChatConversation;
|
||||||
|
import org.jeecg.modules.airag.app.vo.ChatSendParams;
|
||||||
|
import org.jeecg.modules.airag.common.consts.AiragConsts;
|
||||||
|
import org.jeecg.modules.airag.common.handler.AIChatParams;
|
||||||
|
import org.jeecg.modules.airag.common.handler.IAIChatHandler;
|
||||||
|
import org.jeecg.modules.airag.common.utils.AiragLocalCache;
|
||||||
|
import org.jeecg.modules.airag.common.vo.MessageHistory;
|
||||||
|
import org.jeecg.modules.airag.common.vo.event.EventData;
|
||||||
|
import org.jeecg.modules.airag.common.vo.event.EventFlowData;
|
||||||
|
import org.jeecg.modules.airag.common.vo.event.EventMessageData;
|
||||||
|
import org.jeecg.modules.airag.flow.consts.FlowConsts;
|
||||||
|
import org.jeecg.modules.airag.flow.service.IAiragFlowService;
|
||||||
|
import org.jeecg.modules.airag.flow.vo.api.FlowRunParams;
|
||||||
|
import org.jeecg.modules.airag.llm.entity.AiragModel;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.data.redis.core.BoundValueOperations;
|
||||||
|
import org.springframework.data.redis.core.RedisTemplate;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||||
|
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI助手聊天Service
|
||||||
|
*
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2024/1/26 20:07
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
@Slf4j
|
||||||
|
public class AiragChatServiceImpl implements IAiragChatService {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
IAIChatHandler aiChatHandler;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
RedisTemplate redisTemplate;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
IAiragAppService airagAppService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
IAiragFlowService airagFlowService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ISysBaseAPI sysBaseApi;
|
||||||
|
@Autowired
|
||||||
|
private RedisUtil redisUtil;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SseEmitter send(ChatSendParams chatSendParams) {
|
||||||
|
AssertUtils.assertNotEmpty("参数异常", chatSendParams);
|
||||||
|
String userMessage = chatSendParams.getContent();
|
||||||
|
AssertUtils.assertNotEmpty("至少发送一条消息", userMessage);
|
||||||
|
|
||||||
|
// 获取会话信息
|
||||||
|
String conversationId = chatSendParams.getConversationId();
|
||||||
|
String topicId = oConvertUtils.getString(chatSendParams.getTopicId(), UUIDGenerator.generate());
|
||||||
|
// 获取app信息
|
||||||
|
AiragApp app = null;
|
||||||
|
if (oConvertUtils.isNotEmpty(chatSendParams.getAppId())) {
|
||||||
|
app = airagAppService.getById(chatSendParams.getAppId());
|
||||||
|
}
|
||||||
|
ChatConversation chatConversation = getOrCreateChatConversation(app, conversationId);
|
||||||
|
// 更新标题
|
||||||
|
if (oConvertUtils.isEmpty(chatConversation.getTitle())) {
|
||||||
|
chatConversation.setTitle(userMessage.length() > 5 ? userMessage.substring(0, 5) : userMessage);
|
||||||
|
}
|
||||||
|
// 发送消息
|
||||||
|
return doChat(chatConversation, topicId, chatSendParams);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SseEmitter debugApp(AppDebugParams appDebugParams) {
|
||||||
|
AssertUtils.assertNotEmpty("参数异常", appDebugParams);
|
||||||
|
String userMessage = appDebugParams.getContent();
|
||||||
|
AssertUtils.assertNotEmpty("至少发送一条消息", userMessage);
|
||||||
|
AssertUtils.assertNotEmpty("应用信息不能为空", appDebugParams.getApp());
|
||||||
|
// 获取会话信息
|
||||||
|
String topicId = oConvertUtils.getString(appDebugParams.getTopicId(), UUIDGenerator.generate());
|
||||||
|
AiragApp app = appDebugParams.getApp();
|
||||||
|
app.setId("__DEBUG_APP");
|
||||||
|
ChatConversation chatConversation = getOrCreateChatConversation(app, topicId);
|
||||||
|
// 发送消息
|
||||||
|
SseEmitter emitter = doChat(chatConversation, topicId, appDebugParams);
|
||||||
|
//保存会话
|
||||||
|
saveChatConversation(chatConversation, true, null);
|
||||||
|
return emitter;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Result<?> stop(String requestId) {
|
||||||
|
AssertUtils.assertNotEmpty("requestId不能为空", requestId);
|
||||||
|
// 从缓存中获取对应的SseEmitter
|
||||||
|
SseEmitter emitter = AiragLocalCache.get(AiragConsts.CACHE_TYPE_SSE, requestId);
|
||||||
|
if (emitter != null) {
|
||||||
|
closeSSE(emitter, new EventData(requestId, null, EventData.EVENT_MESSAGE_END));
|
||||||
|
return Result.ok("会话已成功终止");
|
||||||
|
} else {
|
||||||
|
return Result.error("未找到对应的会话");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 关闭sse
|
||||||
|
*
|
||||||
|
* @param emitter
|
||||||
|
* @param eventData
|
||||||
|
* @throws IOException
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2025/2/27 15:56
|
||||||
|
*/
|
||||||
|
private static void closeSSE(SseEmitter emitter, EventData eventData) {
|
||||||
|
AssertUtils.assertNotEmpty("请求id不能为空", eventData);
|
||||||
|
if (null == emitter) {
|
||||||
|
log.warn("会话已关闭");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
// 发送完成事件
|
||||||
|
emitter.send(SseEmitter.event().data(eventData));
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error("终止会话时发生错误", e);
|
||||||
|
} finally {
|
||||||
|
// 从缓存中移除emitter
|
||||||
|
AiragLocalCache.remove(AiragConsts.CACHE_TYPE_SSE, eventData.getRequestId());
|
||||||
|
// 关闭emitter
|
||||||
|
emitter.complete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Result<?> getConversations(String appId) {
|
||||||
|
if (oConvertUtils.isEmpty(appId)) {
|
||||||
|
appId = AiAppConsts.DEFAULT_APP_ID;
|
||||||
|
}
|
||||||
|
String key = getConversationDirCacheKey(null);
|
||||||
|
key = key + ":*";
|
||||||
|
List<String> keys = redisUtil.scan(key);
|
||||||
|
// 如果键集合为空,返回空列表
|
||||||
|
if (keys.isEmpty()) {
|
||||||
|
return Result.ok(Collections.emptyList());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 遍历键集合,获取对应的 ChatConversation 对象
|
||||||
|
List<ChatConversation> conversations = new ArrayList<>();
|
||||||
|
for (Object k : keys) {
|
||||||
|
ChatConversation conversation = (ChatConversation) redisTemplate.boundValueOps(k).get();
|
||||||
|
|
||||||
|
if (conversation != null) {
|
||||||
|
AiragApp app = conversation.getApp();
|
||||||
|
if (null == app) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String conversationAppId = app.getId();
|
||||||
|
if (appId.equals(conversationAppId)) {
|
||||||
|
conversation.setApp(null);
|
||||||
|
conversation.setMessages(null);
|
||||||
|
conversations.add(conversation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 对会话列表按创建时间降序排序
|
||||||
|
conversations.sort((o1, o2) -> {
|
||||||
|
Date date1 = o1.getCreateTime();
|
||||||
|
Date date2 = o2.getCreateTime();
|
||||||
|
if (date1 == null && date2 == null) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (date1 == null) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (date2 == null) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return date2.compareTo(date1);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 返回结果
|
||||||
|
return Result.ok(conversations);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Result<?> getMessages(String conversationId) {
|
||||||
|
AssertUtils.assertNotEmpty("请先选择会话", conversationId);
|
||||||
|
String key = getConversationCacheKey(conversationId, null);
|
||||||
|
if (oConvertUtils.isEmpty(key)) {
|
||||||
|
return Result.ok(Collections.emptyList());
|
||||||
|
}
|
||||||
|
ChatConversation chatConversation = (ChatConversation) redisTemplate.boundValueOps(key).get();
|
||||||
|
if (oConvertUtils.isObjectEmpty(chatConversation)) {
|
||||||
|
return Result.ok(Collections.emptyList());
|
||||||
|
}
|
||||||
|
return Result.ok(chatConversation.getMessages());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Result<?> clearMessage(String conversationId) {
|
||||||
|
AssertUtils.assertNotEmpty("请先选择会话", conversationId);
|
||||||
|
String key = getConversationCacheKey(conversationId, null);
|
||||||
|
if (oConvertUtils.isEmpty(key)) {
|
||||||
|
return Result.ok(Collections.emptyList());
|
||||||
|
}
|
||||||
|
ChatConversation chatConversation = (ChatConversation) redisTemplate.boundValueOps(key).get();
|
||||||
|
if (null != chatConversation && oConvertUtils.isObjectNotEmpty(chatConversation.getMessages())) {
|
||||||
|
chatConversation.getMessages().clear();
|
||||||
|
saveChatConversation(chatConversation);
|
||||||
|
}
|
||||||
|
return Result.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Result<?> deleteConversation(String conversationId) {
|
||||||
|
AssertUtils.assertNotEmpty("请选择要删除的会话", conversationId);
|
||||||
|
String key = getConversationCacheKey(conversationId, null);
|
||||||
|
if (oConvertUtils.isNotEmpty(key)) {
|
||||||
|
Boolean delete = redisTemplate.delete(key);
|
||||||
|
if (delete) {
|
||||||
|
return Result.ok();
|
||||||
|
} else {
|
||||||
|
return Result.error("删除会话失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.warn("[ai-chat]删除会话:未找到会话:{}", conversationId);
|
||||||
|
return Result.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Result<?> updateConversationTitle(ChatConversation updateTitleParams) {
|
||||||
|
AssertUtils.assertNotEmpty("请先选择会话", updateTitleParams);
|
||||||
|
AssertUtils.assertNotEmpty("请先选择会话", updateTitleParams.getId());
|
||||||
|
AssertUtils.assertNotEmpty("请输入会话标题", updateTitleParams.getTitle());
|
||||||
|
String key = getConversationCacheKey(updateTitleParams.getId(), null);
|
||||||
|
if (oConvertUtils.isEmpty(key)) {
|
||||||
|
log.warn("[ai-chat]删除会话:未找到会话:{}", updateTitleParams.getId());
|
||||||
|
return Result.ok();
|
||||||
|
}
|
||||||
|
ChatConversation chatConversation = (ChatConversation) redisTemplate.boundValueOps(key).get();
|
||||||
|
chatConversation.setTitle(updateTitleParams.getTitle());
|
||||||
|
saveChatConversation(chatConversation);
|
||||||
|
return Result.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取会话缓存key
|
||||||
|
*
|
||||||
|
* @param conversationId
|
||||||
|
* @param httpRequest
|
||||||
|
* @return
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2025/2/25 19:27
|
||||||
|
*/
|
||||||
|
private String getConversationCacheKey(String conversationId,HttpServletRequest httpRequest) {
|
||||||
|
if (oConvertUtils.isEmpty(conversationId)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String key = getConversationDirCacheKey(httpRequest);
|
||||||
|
key = key + ":" + conversationId;
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前用户会话的缓存目录
|
||||||
|
*
|
||||||
|
* @param httpRequest
|
||||||
|
* @return
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2025/2/26 15:09
|
||||||
|
*/
|
||||||
|
private String getConversationDirCacheKey(HttpServletRequest httpRequest) {
|
||||||
|
String username = getUsername(httpRequest);
|
||||||
|
// 如果用户不存在,获取当前请求的sessionid
|
||||||
|
if (oConvertUtils.isEmpty(username)) {
|
||||||
|
try {
|
||||||
|
if (null == httpRequest) {
|
||||||
|
httpRequest = SpringContextUtils.getHttpServletRequest();
|
||||||
|
}
|
||||||
|
username = httpRequest.getSession().getId();
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("获取当前请求的sessionid失败", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AssertUtils.assertNotEmpty("请先登录", username);
|
||||||
|
return "airag:chat:" + username;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取会话
|
||||||
|
*
|
||||||
|
* @param app
|
||||||
|
* @param conversationId
|
||||||
|
* @return
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2025/2/25 19:19
|
||||||
|
*/
|
||||||
|
@NotNull
|
||||||
|
private ChatConversation getOrCreateChatConversation(AiragApp app, String conversationId) {
|
||||||
|
if (oConvertUtils.isObjectEmpty(app)) {
|
||||||
|
app = new AiragApp();
|
||||||
|
app.setId(AiAppConsts.DEFAULT_APP_ID);
|
||||||
|
}
|
||||||
|
ChatConversation chatConversation = null;
|
||||||
|
String key = getConversationCacheKey(conversationId, null);
|
||||||
|
if (oConvertUtils.isNotEmpty(key)) {
|
||||||
|
chatConversation = (ChatConversation) redisTemplate.boundValueOps(key).get();
|
||||||
|
}
|
||||||
|
if (null == chatConversation) {
|
||||||
|
chatConversation = createConversation(conversationId);
|
||||||
|
}
|
||||||
|
chatConversation.setApp(app);
|
||||||
|
return chatConversation;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建新的会话
|
||||||
|
*
|
||||||
|
* @param conversationId
|
||||||
|
* @return
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2025/2/26 15:53
|
||||||
|
*/
|
||||||
|
@NotNull
|
||||||
|
private ChatConversation createConversation(String conversationId) {
|
||||||
|
// 新会话
|
||||||
|
conversationId = oConvertUtils.getString(conversationId, UUIDGenerator.generate());
|
||||||
|
ChatConversation chatConversation = new ChatConversation();
|
||||||
|
chatConversation.setId(conversationId);
|
||||||
|
chatConversation.setCreateTime(new Date());
|
||||||
|
return chatConversation;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存会话
|
||||||
|
*
|
||||||
|
* @param chatConversation
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2025/2/25 19:27
|
||||||
|
*/
|
||||||
|
private void saveChatConversation(ChatConversation chatConversation) {
|
||||||
|
saveChatConversation(chatConversation, false, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存会话
|
||||||
|
*
|
||||||
|
* @param chatConversation
|
||||||
|
* @param temp 是否临时会话
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2025/2/25 19:27
|
||||||
|
*/
|
||||||
|
private void saveChatConversation(ChatConversation chatConversation, boolean temp,HttpServletRequest httpRequest) {
|
||||||
|
if (null == chatConversation) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String key = getConversationCacheKey(chatConversation.getId(), httpRequest);
|
||||||
|
if (oConvertUtils.isEmpty(key)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
BoundValueOperations chatRedisCacheOp = redisTemplate.boundValueOps(key);
|
||||||
|
chatRedisCacheOp.set(chatConversation);
|
||||||
|
if (temp) {
|
||||||
|
chatRedisCacheOp.expire(3, TimeUnit.HOURS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造消息
|
||||||
|
*
|
||||||
|
* @param conversation
|
||||||
|
* @param topicId
|
||||||
|
* @return
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2025/2/25 15:26
|
||||||
|
*/
|
||||||
|
private List<ChatMessage> collateMessage(ChatConversation conversation, String topicId) {
|
||||||
|
List<MessageHistory> messagesHistory = conversation.getMessages();
|
||||||
|
if (oConvertUtils.isObjectEmpty(messagesHistory)) {
|
||||||
|
return new LinkedList<>();
|
||||||
|
}
|
||||||
|
LinkedList<ChatMessage> chatMessages = new LinkedList<>();
|
||||||
|
for (int i = messagesHistory.size() - 1; i >= 0; i--) {
|
||||||
|
MessageHistory history = messagesHistory.get(i);
|
||||||
|
if (topicId.equals(history.getTopicId())) {
|
||||||
|
ChatMessage chatMessage = null;
|
||||||
|
switch (history.getRole()) {
|
||||||
|
case AiragConsts.MESSAGE_ROLE_USER:
|
||||||
|
List<Content> contents = new ArrayList<>();
|
||||||
|
List<MessageHistory.ImageHistory> images = history.getImages();
|
||||||
|
if (oConvertUtils.isObjectNotEmpty(images)
|
||||||
|
&& !images.isEmpty()) {
|
||||||
|
contents.addAll(images.stream().map(imageHistory -> {
|
||||||
|
if (oConvertUtils.isNotEmpty(imageHistory.getUrl())) {
|
||||||
|
return ImageContent.from(imageHistory.getUrl());
|
||||||
|
} else {
|
||||||
|
return ImageContent.from(imageHistory.getBase64Data(), imageHistory.getMimeType());
|
||||||
|
}
|
||||||
|
}).collect(Collectors.toList()));
|
||||||
|
}
|
||||||
|
contents.add(TextContent.from(history.getContent()));
|
||||||
|
chatMessage = UserMessage.from(contents);
|
||||||
|
break;
|
||||||
|
case AiragConsts.MESSAGE_ROLE_AI:
|
||||||
|
chatMessage = new AiMessage(history.getContent());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (null == chatMessage) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
chatMessages.addFirst(chatMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return chatMessages;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 追加消息
|
||||||
|
*
|
||||||
|
* @param messages
|
||||||
|
* @param message
|
||||||
|
* @param chatConversation
|
||||||
|
* @param topicId
|
||||||
|
* @return
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2025/2/25 19:05
|
||||||
|
*/
|
||||||
|
private void appendMessage(List<ChatMessage> messages, ChatMessage message, ChatConversation
|
||||||
|
chatConversation, String topicId) {
|
||||||
|
|
||||||
|
if (message.type().equals(ChatMessageType.SYSTEM)) {
|
||||||
|
// 系统消息,放到消息列表最前面,并且不记录历史
|
||||||
|
messages.add(0, message);
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
messages.add(message);
|
||||||
|
}
|
||||||
|
List<MessageHistory> histories = chatConversation.getMessages();
|
||||||
|
if (oConvertUtils.isObjectEmpty(histories)) {
|
||||||
|
histories = new ArrayList<>();
|
||||||
|
}
|
||||||
|
// 消息记录
|
||||||
|
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();
|
||||||
|
List<MessageHistory.ImageHistory> images = new ArrayList<>();
|
||||||
|
List<Content> contents = ((UserMessage) message).contents();
|
||||||
|
contents.forEach(content -> {
|
||||||
|
if (content.type().equals(ContentType.IMAGE)) {
|
||||||
|
ImageContent imageContent = (ImageContent) content;
|
||||||
|
Image image = imageContent.image();
|
||||||
|
MessageHistory.ImageHistory imageMessage = MessageHistory.ImageHistory.from(image.url(), image.base64Data(), image.mimeType());
|
||||||
|
images.add(imageMessage);
|
||||||
|
} else if (content.type().equals(ContentType.TEXT)) {
|
||||||
|
textContent.append(((TextContent) content).text()).append("\n");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
historyMessage.setContent(textContent.toString());
|
||||||
|
historyMessage.setImages(images);
|
||||||
|
} else if (message.type().equals(ChatMessageType.AI)) {
|
||||||
|
historyMessage.setRole(AiragConsts.MESSAGE_ROLE_AI);
|
||||||
|
historyMessage.setContent(((AiMessage) message).text());
|
||||||
|
}
|
||||||
|
histories.add(historyMessage);
|
||||||
|
chatConversation.setMessages(histories);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送聊天消息
|
||||||
|
*
|
||||||
|
* @param chatConversation
|
||||||
|
* @param topicId
|
||||||
|
* @param sendParams
|
||||||
|
* @return
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2025/2/28 11:04
|
||||||
|
*/
|
||||||
|
@NotNull
|
||||||
|
private SseEmitter doChat(ChatConversation chatConversation, String topicId, ChatSendParams sendParams) {
|
||||||
|
// 从历史消息中组装本次的消息列表
|
||||||
|
List<ChatMessage> messages = collateMessage(chatConversation, topicId);
|
||||||
|
|
||||||
|
AiragApp aiApp = chatConversation.getApp();
|
||||||
|
// 每次会话都生成一个新的,用来缓存emitter
|
||||||
|
String requestId = UUIDGenerator.generate();
|
||||||
|
SseEmitter emitter = new SseEmitter(-0L);
|
||||||
|
// 缓存emitter
|
||||||
|
AiragLocalCache.put(AiragConsts.CACHE_TYPE_SSE, requestId, emitter);
|
||||||
|
try {
|
||||||
|
// 组装用户消息
|
||||||
|
UserMessage userMessage = aiChatHandler.buildUserMessage(sendParams.getContent(), sendParams.getImages());
|
||||||
|
// 追加消息
|
||||||
|
appendMessage(messages, userMessage, chatConversation, topicId);
|
||||||
|
/* 这里应该是有几种情况:
|
||||||
|
* 1. 非ai应用:获取默认模型->开始聊天
|
||||||
|
* 2. AI应用-聊天助手(ChatAssistant):从应用信息组装模型和提示词->开始聊天
|
||||||
|
* 3. AI应用-聊天流程(ChatFlow):从应用信息获取模型,流程,组装入参->调用工作流
|
||||||
|
*/
|
||||||
|
if (null != aiApp && !AiAppConsts.DEFAULT_APP_ID.equals(aiApp.getId())) {
|
||||||
|
// ai应用:查询应用信息(ChatAssistant,chatflow),模型信息,组装模型-提示词,知识库等
|
||||||
|
if (AiAppConsts.APP_TYPE_CHAT_FLOW.equals(aiApp.getType())) {
|
||||||
|
// ai应用:聊天流程(ChatFlow)
|
||||||
|
sendWithFlow(requestId, aiApp.getFlowId(), chatConversation, topicId, messages, sendParams);
|
||||||
|
} else {
|
||||||
|
// AI应用-聊天助手(ChatAssistant):从应用信息组装模型和提示词
|
||||||
|
sendWithAppChat(requestId, messages, chatConversation, topicId);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 发消息
|
||||||
|
sendWithDefault(requestId, chatConversation, topicId, null, messages, null);
|
||||||
|
}
|
||||||
|
} catch (Throwable e) {
|
||||||
|
log.error(e.getMessage(), e);
|
||||||
|
EventData eventData = new EventData(requestId, null, EventData.EVENT_FLOW_ERROR, chatConversation.getId(), topicId);
|
||||||
|
eventData.setData(EventFlowData.builder().success(false).message(e.getMessage()).build());
|
||||||
|
closeSSE(emitter, eventData);
|
||||||
|
}
|
||||||
|
return emitter;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 运行流程
|
||||||
|
*
|
||||||
|
* @param requestId
|
||||||
|
* @param flowId
|
||||||
|
* @param chatConversation
|
||||||
|
* @param topicId
|
||||||
|
* @param messages
|
||||||
|
* @param sendParams
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2025/2/27 14:55
|
||||||
|
*/
|
||||||
|
private void sendWithFlow(String requestId, String flowId, ChatConversation chatConversation, String topicId, List<ChatMessage> messages, ChatSendParams sendParams) {
|
||||||
|
FlowRunParams flowRunParams = new FlowRunParams();
|
||||||
|
flowRunParams.setRequestId(requestId);
|
||||||
|
flowRunParams.setFlowId(flowId);
|
||||||
|
flowRunParams.setConversationId(chatConversation.getId());
|
||||||
|
flowRunParams.setTopicId(topicId);
|
||||||
|
// 支持流式
|
||||||
|
flowRunParams.setResponseMode(FlowConsts.FLOW_RESPONSE_MODE_STREAMING);
|
||||||
|
Map<String, Object> flowInputParams = new HashMap<>();
|
||||||
|
List<MessageHistory> histories = new ArrayList<>();
|
||||||
|
if (oConvertUtils.isObjectNotEmpty(chatConversation.getMessages())) {
|
||||||
|
// 创建历史消息的副本(不直接操作原来的list)
|
||||||
|
histories.addAll(chatConversation.getMessages());
|
||||||
|
// 移除最后一条历史消息(最后一条是当前发出去的这一条消息)
|
||||||
|
histories.remove(histories.size() - 1);
|
||||||
|
}
|
||||||
|
flowInputParams.put(FlowConsts.FLOW_INPUT_PARAM_HISTORY, histories);
|
||||||
|
flowInputParams.put(FlowConsts.FLOW_INPUT_PARAM_QUESTION, sendParams.getContent());
|
||||||
|
flowInputParams.put(FlowConsts.FLOW_INPUT_PARAM_IMAGES, sendParams.getImages());
|
||||||
|
flowRunParams.setInputParams(flowInputParams);
|
||||||
|
HttpServletRequest httpRequest = SpringContextUtils.getHttpServletRequest();
|
||||||
|
flowRunParams.setHttpRequest(httpRequest);
|
||||||
|
// 流程结束后,记录ai返回并保存会话
|
||||||
|
// sse
|
||||||
|
SseEmitter emitter = AiragLocalCache.get(AiragConsts.CACHE_TYPE_SSE, requestId);
|
||||||
|
flowRunParams.setEventCallback(eventData -> {
|
||||||
|
if (EventData.EVENT_FLOW_FINISHED.equals(eventData.getEvent())) {
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
appendMessage(messages, aiMessage, chatConversation, topicId);
|
||||||
|
// 保存会话
|
||||||
|
saveChatConversation(chatConversation, false, httpRequest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
airagFlowService.runFlow(flowRunParams);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送app聊天
|
||||||
|
*
|
||||||
|
* @param requestId
|
||||||
|
* @param messages
|
||||||
|
* @param chatConversation
|
||||||
|
* @param topicId
|
||||||
|
* @return
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2025/2/28 10:41
|
||||||
|
*/
|
||||||
|
private void sendWithAppChat(String requestId, List<ChatMessage> messages, ChatConversation chatConversation, String topicId) {
|
||||||
|
AiragApp aiApp = chatConversation.getApp();
|
||||||
|
String modelId = aiApp.getModelId();
|
||||||
|
AssertUtils.assertNotEmpty("请先选择模型", modelId);
|
||||||
|
// AI应用提示词
|
||||||
|
String prompt = aiApp.getPrompt();
|
||||||
|
if (oConvertUtils.isNotEmpty(prompt)) {
|
||||||
|
appendMessage(messages, new SystemMessage(prompt), chatConversation, topicId);
|
||||||
|
}
|
||||||
|
|
||||||
|
AIChatParams aiChatParams = new AIChatParams();
|
||||||
|
// AI应用自定义的模型参数
|
||||||
|
String metadataStr = aiApp.getMetadata();
|
||||||
|
if (oConvertUtils.isNotEmpty(metadataStr)) {
|
||||||
|
JSONObject metadata = JSONObject.parseObject(metadataStr);
|
||||||
|
if(oConvertUtils.isNotEmpty(metadata)){
|
||||||
|
if (metadata.containsKey("temperature")) {
|
||||||
|
aiChatParams.setTemperature(metadata.getDouble("temperature"));
|
||||||
|
}
|
||||||
|
if (metadata.containsKey("topP")) {
|
||||||
|
aiChatParams.setTopP(metadata.getDouble("temperature"));
|
||||||
|
}
|
||||||
|
if (metadata.containsKey("presencePenalty")) {
|
||||||
|
aiChatParams.setPresencePenalty(metadata.getDouble("temperature"));
|
||||||
|
}
|
||||||
|
if (metadata.containsKey("frequencyPenalty")) {
|
||||||
|
aiChatParams.setFrequencyPenalty(metadata.getDouble("temperature"));
|
||||||
|
}
|
||||||
|
if (metadata.containsKey("maxTokens")) {
|
||||||
|
aiChatParams.setMaxTokens(metadata.getInteger("temperature"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 发消息
|
||||||
|
sendWithDefault(requestId, chatConversation, topicId, modelId, messages, aiChatParams);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理聊天
|
||||||
|
* 向大模型发送消息并接受响应
|
||||||
|
*
|
||||||
|
* @param chatConversation
|
||||||
|
* @param topicId
|
||||||
|
* @param modelId
|
||||||
|
* @param messages
|
||||||
|
* @return
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2025/2/25 19:24
|
||||||
|
*/
|
||||||
|
private void sendWithDefault(String requestId, ChatConversation chatConversation, String topicId, String modelId,
|
||||||
|
List<ChatMessage> messages,AIChatParams aiChatParams) {
|
||||||
|
// 调用ai聊天
|
||||||
|
if(null == aiChatParams){
|
||||||
|
aiChatParams = new AIChatParams();
|
||||||
|
}
|
||||||
|
aiChatParams.setKnowIds(chatConversation.getApp().getKnowIds());
|
||||||
|
aiChatParams.setMaxMsgNumber(oConvertUtils.getInt(chatConversation.getApp().getMsgNum(), 5));
|
||||||
|
HttpServletRequest httpRequest = SpringContextUtils.getHttpServletRequest();
|
||||||
|
TokenStream chatStream;
|
||||||
|
try {
|
||||||
|
if (oConvertUtils.isNotEmpty(modelId)) {
|
||||||
|
chatStream = aiChatHandler.chat(modelId, messages, aiChatParams);
|
||||||
|
} else {
|
||||||
|
chatStream = aiChatHandler.chatByDefaultModel(messages, aiChatParams);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error(e.getMessage(),e);
|
||||||
|
throw new JeecgBootBizTipException("调用大模型接口失败:" + e.getMessage());
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 是否正在思考
|
||||||
|
*/
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送聊天返回结果
|
||||||
|
*
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2025/2/28 11:05
|
||||||
|
*/
|
||||||
|
private static class ChatResult {
|
||||||
|
public final SseEmitter emitter;
|
||||||
|
public final AiragModel chatModel;
|
||||||
|
|
||||||
|
public ChatResult(SseEmitter emitter, AiragModel chatModel) {
|
||||||
|
this.emitter = emitter;
|
||||||
|
this.chatModel = chatModel;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 总结会话标题
|
||||||
|
* 几个问题: <br/>
|
||||||
|
* 1. 如果在发消息时同步总结会话标题,会导致接口很慢甚至超时.
|
||||||
|
* 2. 但如果异步更新会话标题会导致消息记录丢失(不全)或者标题丢失,需要写很多逻辑去保证最终一致
|
||||||
|
* so 暂时先不用AI更新会话标题. 后期如果需要单独再增加一个接口,由前端调用或者在第一次消息接收完成后再异步更新
|
||||||
|
*
|
||||||
|
* @param chatConversation
|
||||||
|
* @param question
|
||||||
|
* @param modelId
|
||||||
|
* @return
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2025/2/25 17:12
|
||||||
|
*/
|
||||||
|
protected void summaryConversationTitle(ChatConversation chatConversation, String question, String modelId) {
|
||||||
|
if (oConvertUtils.isEmpty(chatConversation.getId())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String key = getConversationCacheKey(chatConversation.getId(), null);
|
||||||
|
if (oConvertUtils.isEmpty(key)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
CompletableFuture.runAsync(() -> {
|
||||||
|
List<ChatMessage> messages = new LinkedList<>();
|
||||||
|
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;
|
||||||
|
try {
|
||||||
|
summaryTitle = aiChatHandler.completions(modelId, messages, null);
|
||||||
|
log.info("总结会话完成{}", summaryTitle);
|
||||||
|
if (summaryTitle.equalsIgnoreCase("不知道")) {
|
||||||
|
summaryTitle = "";
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("AI总结会话失败" + e.getMessage(), e);
|
||||||
|
summaryTitle = "";
|
||||||
|
}
|
||||||
|
// 更新会话标题
|
||||||
|
ChatConversation cachedConversation = (ChatConversation) redisTemplate.boundValueOps(key).get();
|
||||||
|
if (null == cachedConversation) {
|
||||||
|
cachedConversation = chatConversation;
|
||||||
|
}
|
||||||
|
if (oConvertUtils.isEmpty(chatConversation.getTitle())) {
|
||||||
|
// 再次判断标题是否为空,只有标题为空才更新
|
||||||
|
if (oConvertUtils.isNotEmpty(summaryTitle)) {
|
||||||
|
cachedConversation.setTitle(summaryTitle);
|
||||||
|
} else {
|
||||||
|
cachedConversation.setTitle(question.length() > 5 ? question.substring(0, 5) : question);
|
||||||
|
}
|
||||||
|
//保存会话
|
||||||
|
saveChatConversation(cachedConversation);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户名
|
||||||
|
* @param httpRequest
|
||||||
|
* @return
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2025/3/27 15:05
|
||||||
|
*/
|
||||||
|
private String getUsername(HttpServletRequest httpRequest) {
|
||||||
|
try {
|
||||||
|
TokenUtils.getTokenByRequest();
|
||||||
|
String token;
|
||||||
|
if(null != httpRequest){
|
||||||
|
token = TokenUtils.getTokenByRequest(httpRequest);
|
||||||
|
}else{
|
||||||
|
token = TokenUtils.getTokenByRequest();
|
||||||
|
}
|
||||||
|
if (TokenUtils.verifyToken(token, sysBaseApi, redisUtil)) {
|
||||||
|
return JwtUtil.getUsername(token);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,20 @@
|
|||||||
|
package org.jeecg.modules.airag.app.vo;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import org.jeecg.modules.airag.app.entity.AiragApp;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Description: 应用调试入参
|
||||||
|
* @Author: chenrui
|
||||||
|
* @Date: 2025/2/25 11:47
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class AppDebugParams extends ChatSendParams {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 应用信息
|
||||||
|
*/
|
||||||
|
AiragApp app;
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,42 @@
|
|||||||
|
package org.jeecg.modules.airag.app.vo;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import org.jeecg.modules.airag.app.entity.AiragApp;
|
||||||
|
import org.jeecg.modules.airag.common.vo.MessageHistory;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Description: 聊天会话
|
||||||
|
* @Author: chenrui
|
||||||
|
* @Date: 2025/2/25 14:56
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class ChatConversation {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 会话id
|
||||||
|
*/
|
||||||
|
private String id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 会话标题
|
||||||
|
*/
|
||||||
|
private String title;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 消息记录
|
||||||
|
*/
|
||||||
|
private List<MessageHistory> messages;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* app
|
||||||
|
*/
|
||||||
|
private AiragApp app;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建时间
|
||||||
|
*/
|
||||||
|
private Date createTime;
|
||||||
|
}
|
||||||
@ -0,0 +1,49 @@
|
|||||||
|
package org.jeecg.modules.airag.app.vo;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Description: 发送消息的入参
|
||||||
|
* @Author: chenrui
|
||||||
|
* @Date: 2025/2/25 11:47
|
||||||
|
*/
|
||||||
|
@NoArgsConstructor
|
||||||
|
@Data
|
||||||
|
public class ChatSendParams {
|
||||||
|
|
||||||
|
public ChatSendParams(String content, String conversationId, String topicId, String appId) {
|
||||||
|
this.content = content;
|
||||||
|
this.conversationId = conversationId;
|
||||||
|
this.topicId = topicId;
|
||||||
|
this.appId = appId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户输入的聊天内容
|
||||||
|
*/
|
||||||
|
private String content;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 对话会话ID
|
||||||
|
*/
|
||||||
|
private String conversationId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 对话主题ID(用于关联历史记录)
|
||||||
|
*/
|
||||||
|
private String topicId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 应用id
|
||||||
|
*/
|
||||||
|
private String appId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 图片列表
|
||||||
|
*/
|
||||||
|
private List<String> images;
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,47 @@
|
|||||||
|
package org.jeecg.modules.airag.llm.config;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 向量存储库配置
|
||||||
|
*
|
||||||
|
* @Author: chenrui
|
||||||
|
* @Date: 2025/2/18 14:24
|
||||||
|
*/
|
||||||
|
@NoArgsConstructor
|
||||||
|
@Data
|
||||||
|
@Component
|
||||||
|
@ConfigurationProperties(prefix = EmbedStoreConfigBean.PREFIX)
|
||||||
|
public class EmbedStoreConfigBean {
|
||||||
|
public static final String PREFIX = "jeecg.airag.embed-store";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* host
|
||||||
|
*/
|
||||||
|
private String host = "127.0.0.1";
|
||||||
|
/**
|
||||||
|
* 端口
|
||||||
|
*/
|
||||||
|
private int port = 5432;
|
||||||
|
/**
|
||||||
|
* 数据库
|
||||||
|
*/
|
||||||
|
private String database = "postgres";
|
||||||
|
/**
|
||||||
|
* 用户名
|
||||||
|
*/
|
||||||
|
private String user = "postgres";
|
||||||
|
/**
|
||||||
|
* 密码
|
||||||
|
*/
|
||||||
|
private String password = "postgres";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 存储向量的表
|
||||||
|
*/
|
||||||
|
private String table = "embeddings";
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,31 @@
|
|||||||
|
package org.jeecg.modules.airag.llm.config;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 知识库配置
|
||||||
|
*
|
||||||
|
* @Author: chenrui
|
||||||
|
* @Date: 2025-04-01 14:19
|
||||||
|
*/
|
||||||
|
@NoArgsConstructor
|
||||||
|
@Data
|
||||||
|
@Component
|
||||||
|
@ConfigurationProperties(prefix = KnowConfigBean.PREFIX)
|
||||||
|
public class KnowConfigBean {
|
||||||
|
public static final String PREFIX = "jeecg.airag.know";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 开启MinerU解析
|
||||||
|
*/
|
||||||
|
private boolean enableMinerU = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* conda的环境(默认不使用conda)
|
||||||
|
*/
|
||||||
|
private String condaEnv = null;
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,75 @@
|
|||||||
|
package org.jeecg.modules.airag.llm.consts;
|
||||||
|
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Description: airag模型常量类
|
||||||
|
* @Author: chenrui
|
||||||
|
* @Date: 2025/2/12 17:35
|
||||||
|
*/
|
||||||
|
public class LLMConsts {
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 正则表达式:是否是网页
|
||||||
|
*/
|
||||||
|
public static final Pattern WEB_PATTERN = Pattern.compile("^(http|https)://.*");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 状态:启用
|
||||||
|
*/
|
||||||
|
public static final String STATUS_ENABLE = "enable";
|
||||||
|
/**
|
||||||
|
* 状态:禁用
|
||||||
|
*/
|
||||||
|
public static final String STATUS_DISABLE = "disable";
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 模型类型:向量
|
||||||
|
*/
|
||||||
|
public static final String MODEL_TYPE_EMBED = "EMBED";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 模型类型:聊天
|
||||||
|
*/
|
||||||
|
public static final String MODEL_TYPE_LLM = "LLM";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 知识库:文档状态:草稿
|
||||||
|
*/
|
||||||
|
public static final String KNOWLEDGE_DOC_STATUS_DRAFT = "draft";
|
||||||
|
/**
|
||||||
|
* 知识库:文档状态:构建中
|
||||||
|
*/
|
||||||
|
public static final String KNOWLEDGE_DOC_STATUS_BUILDING = "building";
|
||||||
|
/**
|
||||||
|
* 知识库:文档状态:构建完成
|
||||||
|
*/
|
||||||
|
public static final String KNOWLEDGE_DOC_STATUS_COMPLETE = "complete";
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 知识库:文档类型:文本
|
||||||
|
*/
|
||||||
|
public static final String KNOWLEDGE_DOC_TYPE_TEXT = "text";
|
||||||
|
/**
|
||||||
|
* 知识库:文档类型:文件
|
||||||
|
*/
|
||||||
|
public static final String KNOWLEDGE_DOC_TYPE_FILE = "file";
|
||||||
|
/**
|
||||||
|
* 知识库:文档类型:网页
|
||||||
|
*/
|
||||||
|
public static final String KNOWLEDGE_DOC_TYPE_WEB = "web";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 知识库:文档元数据:文件路径
|
||||||
|
*/
|
||||||
|
public static final String KNOWLEDGE_DOC_METADATA_FILEPATH = "filePath";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 知识库:文档元数据:资源路径
|
||||||
|
*/
|
||||||
|
public static final String KNOWLEDGE_DOC_METADATA_SOURCES_PATH = "sourcesPath";
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,320 @@
|
|||||||
|
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 lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.jeecg.common.api.vo.Result;
|
||||||
|
import org.jeecg.common.system.query.QueryGenerator;
|
||||||
|
import org.jeecg.common.util.AssertUtils;
|
||||||
|
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;
|
||||||
|
import org.jeecg.modules.airag.llm.entity.AiragKnowledgeDoc;
|
||||||
|
import org.jeecg.modules.airag.llm.handler.EmbeddingHandler;
|
||||||
|
import org.jeecg.modules.airag.llm.service.IAiragKnowledgeDocService;
|
||||||
|
import org.jeecg.modules.airag.llm.service.IAiragKnowledgeService;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Description: AIRag知识库
|
||||||
|
* @Author: jeecg-boot
|
||||||
|
* @Date: 2025-02-18
|
||||||
|
* @Version: V1.0
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/airag/knowledge")
|
||||||
|
@Slf4j
|
||||||
|
public class AiragKnowledgeController {
|
||||||
|
@Autowired
|
||||||
|
private IAiragKnowledgeService airagKnowledgeService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private IAiragKnowledgeDocService airagKnowledgeDocService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
EmbeddingHandler embeddingHandler;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页列表查询知识库
|
||||||
|
*
|
||||||
|
* @param airagKnowledge
|
||||||
|
* @param pageNo
|
||||||
|
* @param pageSize
|
||||||
|
* @param req
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
@GetMapping(value = "/list")
|
||||||
|
public Result<IPage<AiragKnowledge>> queryPageList(AiragKnowledge airagKnowledge,
|
||||||
|
@RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo,
|
||||||
|
@RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize,
|
||||||
|
HttpServletRequest req) {
|
||||||
|
QueryWrapper<AiragKnowledge> queryWrapper = QueryGenerator.initQueryWrapper(airagKnowledge, req.getParameterMap());
|
||||||
|
Page<AiragKnowledge> page = new Page<AiragKnowledge>(pageNo, pageSize);
|
||||||
|
IPage<AiragKnowledge> pageList = airagKnowledgeService.page(page, queryWrapper);
|
||||||
|
return Result.OK(pageList);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加知识库
|
||||||
|
*
|
||||||
|
* @param airagKnowledge 知识库
|
||||||
|
* @return
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2025/2/18 17:09
|
||||||
|
*/
|
||||||
|
@PostMapping(value = "/add")
|
||||||
|
public Result<String> add(@RequestBody AiragKnowledge airagKnowledge) {
|
||||||
|
airagKnowledge.setStatus(LLMConsts.STATUS_ENABLE);
|
||||||
|
airagKnowledgeService.save(airagKnowledge);
|
||||||
|
return Result.OK("添加成功!");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 编辑知识库
|
||||||
|
*
|
||||||
|
* @param airagKnowledge 知识库
|
||||||
|
* @return
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2025/2/18 17:09
|
||||||
|
*/
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
@RequestMapping(value = "/edit", method = {RequestMethod.PUT, RequestMethod.POST})
|
||||||
|
public Result<String> edit(@RequestBody AiragKnowledge airagKnowledge) {
|
||||||
|
AiragKnowledge airagKnowledgeEntity = airagKnowledgeService.getById(airagKnowledge.getId());
|
||||||
|
if (airagKnowledgeEntity == null) {
|
||||||
|
return Result.error("未找到对应数据");
|
||||||
|
}
|
||||||
|
String oldEmbedId = airagKnowledgeEntity.getEmbedId();
|
||||||
|
airagKnowledgeService.updateById(airagKnowledge);
|
||||||
|
if (!oldEmbedId.equalsIgnoreCase(airagKnowledge.getEmbedId())) {
|
||||||
|
// 更新了模型,重建文档
|
||||||
|
airagKnowledgeDocService.rebuildDocumentByKnowId(airagKnowledge.getId());
|
||||||
|
}
|
||||||
|
return Result.OK("编辑成功!");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重建知识库
|
||||||
|
*
|
||||||
|
* @param knowIds
|
||||||
|
* @return
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2025/3/12 17:05
|
||||||
|
*/
|
||||||
|
@PutMapping(value = "/rebuild")
|
||||||
|
public Result<?> rebuild(@RequestParam("knowIds") String knowIds) {
|
||||||
|
String[] knowIdArr = knowIds.split(",");
|
||||||
|
for (String knowId : knowIdArr) {
|
||||||
|
airagKnowledgeDocService.rebuildDocumentByKnowId(knowId);
|
||||||
|
}
|
||||||
|
return Result.OK("");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通过id删除知识库
|
||||||
|
*
|
||||||
|
* @param id
|
||||||
|
* @return
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2025/2/18 17:09
|
||||||
|
*/
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
@DeleteMapping(value = "/delete")
|
||||||
|
public Result<String> delete(@RequestParam(name = "id", required = true) String id) {
|
||||||
|
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查询知识库
|
||||||
|
*
|
||||||
|
* @param id
|
||||||
|
* @return
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2025/2/18 17:09
|
||||||
|
*/
|
||||||
|
@GetMapping(value = "/queryById")
|
||||||
|
public Result<AiragKnowledge> queryById(@RequestParam(name = "id", required = true) String id) {
|
||||||
|
AiragKnowledge airagKnowledge = airagKnowledgeService.getById(id);
|
||||||
|
if (airagKnowledge == null) {
|
||||||
|
return Result.error("未找到对应数据");
|
||||||
|
}
|
||||||
|
return Result.OK(airagKnowledge);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文档分页查询
|
||||||
|
*
|
||||||
|
* @param airagKnowledgeDoc
|
||||||
|
* @param pageNo
|
||||||
|
* @param pageSize
|
||||||
|
* @param req
|
||||||
|
* @return
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2025/2/18 18:37
|
||||||
|
*/
|
||||||
|
@GetMapping(value = "/doc/list")
|
||||||
|
public Result<IPage<AiragKnowledgeDoc>> queryDocumentPageList(AiragKnowledgeDoc airagKnowledgeDoc,
|
||||||
|
@RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo,
|
||||||
|
@RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize,
|
||||||
|
HttpServletRequest req) {
|
||||||
|
AssertUtils.assertNotEmpty("请先选择知识库", airagKnowledgeDoc.getKnowledgeId());
|
||||||
|
QueryWrapper<AiragKnowledgeDoc> queryWrapper = QueryGenerator.initQueryWrapper(airagKnowledgeDoc, req.getParameterMap());
|
||||||
|
Page<AiragKnowledgeDoc> page = new Page<>(pageNo, pageSize);
|
||||||
|
IPage<AiragKnowledgeDoc> pageList = airagKnowledgeDocService.page(page, queryWrapper);
|
||||||
|
return Result.OK(pageList);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 新增或编辑文档
|
||||||
|
*
|
||||||
|
* @param airagKnowledgeDoc 知识库文档
|
||||||
|
* @return
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2025/2/18 15:47
|
||||||
|
*/
|
||||||
|
@PostMapping(value = "/doc/edit")
|
||||||
|
public Result<?> addDocument(@RequestBody AiragKnowledgeDoc airagKnowledgeDoc) {
|
||||||
|
return airagKnowledgeDocService.editDocument(airagKnowledgeDoc);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从压缩包导入文档
|
||||||
|
* @return
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2025/3/20 11:29
|
||||||
|
*/
|
||||||
|
@PostMapping(value = "/doc/import/zip")
|
||||||
|
public Result<?> importDocumentFromZip(@RequestParam(name = "knowId", required = true) String knowId,
|
||||||
|
@RequestParam(name = "file", required = true) MultipartFile file) {
|
||||||
|
return airagKnowledgeDocService.importDocumentFromZip(knowId,file);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通过文档库查询导入任务列表
|
||||||
|
* @param knowId
|
||||||
|
* @return
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2025/3/20 11:37
|
||||||
|
*/
|
||||||
|
@GetMapping(value = "/doc/import/task/list")
|
||||||
|
public Result<?> importDocumentTaskList(@RequestParam(name = "knowId", required = true) String knowId) {
|
||||||
|
return Result.OK(Collections.emptyList());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重新向量化文档
|
||||||
|
*
|
||||||
|
* @param docIds 文档id集合
|
||||||
|
* @return
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2025/2/18 15:47
|
||||||
|
*/
|
||||||
|
@PutMapping(value = "/doc/rebuild")
|
||||||
|
public Result<?> rebuildDocument(@RequestParam("docIds") String docIds) {
|
||||||
|
return airagKnowledgeDocService.rebuildDocument(docIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量删除文档
|
||||||
|
*
|
||||||
|
* @param ids
|
||||||
|
* @return
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2025/2/18 17:09
|
||||||
|
*/
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
@DeleteMapping(value = "/doc/deleteBatch")
|
||||||
|
public Result<String> deleteDocumentBatch(@RequestParam(name = "ids", required = true) String ids) {
|
||||||
|
List<String> idsList = Arrays.asList(ids.split(","));
|
||||||
|
airagKnowledgeDocService.removeDocByIds(idsList);
|
||||||
|
return Result.OK("批量删除成功!");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 命中测试
|
||||||
|
*
|
||||||
|
* @param knowId 知识库id
|
||||||
|
* @param queryText 查询内容
|
||||||
|
* @param topNumber 最多返回条数
|
||||||
|
* @param similarity 最小分数
|
||||||
|
* @return
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2025/2/18 17:09
|
||||||
|
*/
|
||||||
|
@GetMapping(value = "/embedding/hitTest/{knowId}")
|
||||||
|
public Result<?> hitTest(@PathVariable("knowId") String knowId,
|
||||||
|
@RequestParam(name = "queryText") String queryText,
|
||||||
|
@RequestParam(name = "topNumber") Integer topNumber,
|
||||||
|
@RequestParam(name = "similarity") Double similarity) {
|
||||||
|
List<Map<String, Object>> searchResp = embeddingHandler.searchEmbedding(knowId, queryText, topNumber, similarity);
|
||||||
|
return Result.ok(searchResp);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 向量查询
|
||||||
|
*
|
||||||
|
* @param knowIds 知识库ids
|
||||||
|
* @param queryText 查询内容
|
||||||
|
* @param topNumber 最多返回条数
|
||||||
|
* @param similarity 最小分数
|
||||||
|
* @return
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2025/2/18 17:09
|
||||||
|
*/
|
||||||
|
@GetMapping(value = "/embedding/search")
|
||||||
|
public Result<?> embeddingSearch(@RequestParam("knowIds") List<String> knowIds,
|
||||||
|
@RequestParam(name = "queryText") String queryText,
|
||||||
|
@RequestParam(name = "topNumber", required = false) Integer topNumber,
|
||||||
|
@RequestParam(name = "similarity", required = false) Double similarity) {
|
||||||
|
KnowledgeSearchResult searchResp = embeddingHandler.embeddingSearch(knowIds, queryText, topNumber, similarity);
|
||||||
|
return Result.ok(searchResp);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通过ids批量查询知识库
|
||||||
|
*
|
||||||
|
* @param ids
|
||||||
|
* @return
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2025/2/27 16:44
|
||||||
|
*/
|
||||||
|
@GetMapping(value = "/query/batch/byId")
|
||||||
|
public Result<?> queryBatchByIds(@RequestParam(name = "ids", required = true) String ids) {
|
||||||
|
List<String> idList = Arrays.asList(ids.split(","));
|
||||||
|
List<AiragKnowledge> airagKnowledges = airagKnowledgeService.listByIds(idList);
|
||||||
|
return Result.OK(airagKnowledges);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,139 @@
|
|||||||
|
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 io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
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.airag.llm.entity.AiragModel;
|
||||||
|
import org.jeecg.modules.airag.llm.service.IAiragModelService;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
import org.springframework.web.servlet.ModelAndView;
|
||||||
|
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Description: AiRag模型配置
|
||||||
|
* @Author: jeecg-boot
|
||||||
|
* @Date: 2025-02-14
|
||||||
|
* @Version: V1.0
|
||||||
|
*/
|
||||||
|
@Tag(name = "AiRag模型配置")
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/airag/airagModel")
|
||||||
|
@Slf4j
|
||||||
|
public class AiragModelController extends JeecgController<AiragModel, IAiragModelService> {
|
||||||
|
@Autowired
|
||||||
|
private IAiragModelService airagModelService;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页列表查询
|
||||||
|
*
|
||||||
|
* @param airagModel
|
||||||
|
* @param pageNo
|
||||||
|
* @param pageSize
|
||||||
|
* @param req
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
@GetMapping(value = "/list")
|
||||||
|
public Result<IPage<AiragModel>> queryPageList(AiragModel airagModel, @RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo, @RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize, HttpServletRequest req) {
|
||||||
|
QueryWrapper<AiragModel> queryWrapper = QueryGenerator.initQueryWrapper(airagModel, req.getParameterMap());
|
||||||
|
Page<AiragModel> page = new Page<AiragModel>(pageNo, pageSize);
|
||||||
|
IPage<AiragModel> pageList = airagModelService.page(page, queryWrapper);
|
||||||
|
return Result.OK(pageList);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加
|
||||||
|
*
|
||||||
|
* @param airagModel
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
@PostMapping(value = "/add")
|
||||||
|
public Result<String> add(@RequestBody AiragModel airagModel) {
|
||||||
|
airagModelService.save(airagModel);
|
||||||
|
return Result.OK("添加成功!");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 编辑
|
||||||
|
*
|
||||||
|
* @param airagModel
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
@RequestMapping(value = "/edit", method = {RequestMethod.PUT, RequestMethod.POST})
|
||||||
|
public Result<String> edit(@RequestBody AiragModel airagModel) {
|
||||||
|
airagModelService.updateById(airagModel);
|
||||||
|
return Result.OK("编辑成功!");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通过id删除
|
||||||
|
*
|
||||||
|
* @param id
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
@DeleteMapping(value = "/delete")
|
||||||
|
public Result<String> delete(@RequestParam(name = "id", required = true) String id) {
|
||||||
|
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查询
|
||||||
|
*
|
||||||
|
* @param id
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
@GetMapping(value = "/queryById")
|
||||||
|
public Result<AiragModel> queryById(@RequestParam(name = "id", required = true) String id) {
|
||||||
|
AiragModel airagModel = airagModelService.getById(id);
|
||||||
|
if (airagModel == null) {
|
||||||
|
return Result.error("未找到对应数据");
|
||||||
|
}
|
||||||
|
return Result.OK(airagModel);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出excel
|
||||||
|
*
|
||||||
|
* @param request
|
||||||
|
* @param airagModel
|
||||||
|
*/
|
||||||
|
@RequestMapping(value = "/exportXls")
|
||||||
|
public ModelAndView exportXls(HttpServletRequest request, AiragModel airagModel) {
|
||||||
|
return super.exportXls(request, airagModel, AiragModel.class, "AiRag模型配置");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通过excel导入数据
|
||||||
|
*
|
||||||
|
* @param request
|
||||||
|
* @param response
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
@RequestMapping(value = "/importExcel", method = RequestMethod.POST)
|
||||||
|
public Result<?> importExcel(HttpServletRequest request, HttpServletResponse response) {
|
||||||
|
return super.importExcel(request, response, AiragModel.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,274 @@
|
|||||||
|
//
|
||||||
|
// Source code recreated from a .class file by IntelliJ IDEA
|
||||||
|
// (powered by FernFlower decompiler)
|
||||||
|
//
|
||||||
|
|
||||||
|
package org.jeecg.modules.airag.llm.document;
|
||||||
|
|
||||||
|
import dev.langchain4j.data.document.BlankDocumentException;
|
||||||
|
import dev.langchain4j.data.document.Document;
|
||||||
|
import dev.langchain4j.internal.Utils;
|
||||||
|
import org.apache.commons.io.FilenameUtils;
|
||||||
|
import org.apache.poi.hslf.usermodel.HSLFTextParagraph;
|
||||||
|
import org.apache.poi.hwpf.HWPFDocument;
|
||||||
|
import org.apache.poi.hwpf.extractor.WordExtractor;
|
||||||
|
import org.apache.poi.ss.usermodel.*;
|
||||||
|
import org.apache.poi.xslf.usermodel.XMLSlideShow;
|
||||||
|
import org.apache.poi.xslf.usermodel.XSLFSlide;
|
||||||
|
import org.apache.poi.xslf.usermodel.XSLFTextShape;
|
||||||
|
import org.apache.poi.xwpf.usermodel.XWPFDocument;
|
||||||
|
import org.apache.poi.xwpf.usermodel.XWPFParagraph;
|
||||||
|
import org.apache.tika.Tika;
|
||||||
|
import org.apache.tika.exception.ZeroByteFileException;
|
||||||
|
import org.apache.tika.metadata.Metadata;
|
||||||
|
import org.apache.tika.parser.AutoDetectParser;
|
||||||
|
import org.apache.tika.parser.ParseContext;
|
||||||
|
import org.apache.tika.parser.Parser;
|
||||||
|
import org.apache.tika.sax.BodyContentHandler;
|
||||||
|
import org.jeecg.common.util.AssertUtils;
|
||||||
|
import org.xml.sax.ContentHandler;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* tika文档解析器,重写langchain4j的TikaDocumentParser <br/>
|
||||||
|
* jeecgboot目前不支持poi5.x,所以langchain4j同的方法不能用,自己实现
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2025/3/5 16:19
|
||||||
|
*/
|
||||||
|
public class TikaDocumentParser {
|
||||||
|
private static final Tika tika = new Tika();
|
||||||
|
private static final int NO_WRITE_LIMIT = -1;
|
||||||
|
public static final Supplier<Parser> DEFAULT_PARSER_SUPPLIER = AutoDetectParser::new;
|
||||||
|
public static final Supplier<Metadata> DEFAULT_METADATA_SUPPLIER = Metadata::new;
|
||||||
|
public static final Supplier<ParseContext> DEFAULT_PARSE_CONTEXT_SUPPLIER = ParseContext::new;
|
||||||
|
public static final Supplier<ContentHandler> DEFAULT_CONTENT_HANDLER_SUPPLIER = () -> new BodyContentHandler(-1);
|
||||||
|
private final Supplier<Parser> parserSupplier;
|
||||||
|
private final Supplier<ContentHandler> contentHandlerSupplier;
|
||||||
|
private final Supplier<Metadata> metadataSupplier;
|
||||||
|
private final Supplier<ParseContext> parseContextSupplier;
|
||||||
|
|
||||||
|
public TikaDocumentParser() {
|
||||||
|
this((Supplier) ((Supplier) null), (Supplier) null, (Supplier) null, (Supplier) null);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public TikaDocumentParser(Supplier<Parser> parserSupplier, Supplier<ContentHandler> contentHandlerSupplier, Supplier<Metadata> metadataSupplier, Supplier<ParseContext> parseContextSupplier) {
|
||||||
|
this.parserSupplier = (Supplier) Utils.getOrDefault(parserSupplier, () -> DEFAULT_PARSER_SUPPLIER);
|
||||||
|
this.contentHandlerSupplier = (Supplier) Utils.getOrDefault(contentHandlerSupplier, () -> DEFAULT_CONTENT_HANDLER_SUPPLIER);
|
||||||
|
this.metadataSupplier = (Supplier) Utils.getOrDefault(metadataSupplier, () -> DEFAULT_METADATA_SUPPLIER);
|
||||||
|
this.parseContextSupplier = (Supplier) Utils.getOrDefault(parseContextSupplier, () -> DEFAULT_PARSE_CONTEXT_SUPPLIER);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Document parse(File file) {
|
||||||
|
AssertUtils.assertNotEmpty("请选择文件", file);
|
||||||
|
try {
|
||||||
|
// 用于解析
|
||||||
|
InputStream isForParsing = Files.newInputStream(file.toPath());
|
||||||
|
// 使用 Tika 自动检测 MIME 类型
|
||||||
|
String fileName = file.getName().toLowerCase();
|
||||||
|
if (fileName.endsWith(".txt")
|
||||||
|
|| fileName.endsWith(".md")
|
||||||
|
|| fileName.endsWith(".pdf")) {
|
||||||
|
return extractByTika(isForParsing);
|
||||||
|
} else if (fileName.endsWith(".docx")) {
|
||||||
|
return extractTextFromDocx(isForParsing);
|
||||||
|
} else if (fileName.endsWith(".doc")) {
|
||||||
|
return extractTextFromDoc(isForParsing);
|
||||||
|
} else if (fileName.endsWith(".xlsx")) {
|
||||||
|
return extractTextFromExcel(isForParsing);
|
||||||
|
} else if (fileName.endsWith(".xls")) {
|
||||||
|
return extractTextFromExcel(isForParsing);
|
||||||
|
} else if (fileName.endsWith(".pptx")) {
|
||||||
|
return extractTextFromPptx(isForParsing);
|
||||||
|
} else if (fileName.endsWith(".ppt")) {
|
||||||
|
return extractTextFromPpt(isForParsing);
|
||||||
|
} else {
|
||||||
|
throw new IllegalArgumentException("不支持的文件格式: " + FilenameUtils.getExtension(fileName));
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Document tryExtractDocOrDocx(InputStream inputStream) throws IOException {
|
||||||
|
try {
|
||||||
|
// 先尝试 DOCX(基于 OPC XML 格式)
|
||||||
|
return extractTextFromDocx(inputStream);
|
||||||
|
} catch (Exception e1) {
|
||||||
|
try {
|
||||||
|
// 如果 DOCX 解析失败,则尝试 DOC(基于二进制格式)
|
||||||
|
return extractTextFromDoc(inputStream);
|
||||||
|
} catch (Exception e2) {
|
||||||
|
throw new IOException("无法解析 DOC 或 DOCX 文件", e2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用tika提取文件内容 <br/>
|
||||||
|
* pdf/text/md等文件使用tika提取
|
||||||
|
*
|
||||||
|
* @param inputStream
|
||||||
|
* @return
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2025/3/5 14:41
|
||||||
|
*/
|
||||||
|
private Document extractByTika(InputStream inputStream) {
|
||||||
|
try {
|
||||||
|
Parser parser = (Parser) this.parserSupplier.get();
|
||||||
|
ContentHandler contentHandler = (ContentHandler) this.contentHandlerSupplier.get();
|
||||||
|
Metadata metadata = (Metadata) this.metadataSupplier.get();
|
||||||
|
ParseContext parseContext = (ParseContext) this.parseContextSupplier.get();
|
||||||
|
parser.parse(inputStream, contentHandler, metadata, parseContext);
|
||||||
|
String text = contentHandler.toString();
|
||||||
|
if (Utils.isNullOrBlank(text)) {
|
||||||
|
throw new BlankDocumentException();
|
||||||
|
} else {
|
||||||
|
return Document.from(text);
|
||||||
|
}
|
||||||
|
} catch (BlankDocumentException e) {
|
||||||
|
throw e;
|
||||||
|
} catch (ZeroByteFileException var8) {
|
||||||
|
throw new BlankDocumentException();
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提取docx文件内容
|
||||||
|
*
|
||||||
|
* @param inputStream
|
||||||
|
* @return
|
||||||
|
* @throws IOException
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2025/3/5 14:42
|
||||||
|
* @deprecated 因为jeecg主项目目前不支持poi5.x, 自己实现提取功能.
|
||||||
|
*/
|
||||||
|
@Deprecated
|
||||||
|
private static Document extractTextFromDocx(InputStream inputStream) throws IOException {
|
||||||
|
try (XWPFDocument document = new XWPFDocument(inputStream)) {
|
||||||
|
StringBuilder text = new StringBuilder();
|
||||||
|
for (XWPFParagraph para : document.getParagraphs()) {
|
||||||
|
text.append(para.getText()).append("\n");
|
||||||
|
}
|
||||||
|
return Document.from(text.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提取doc文件内容
|
||||||
|
*
|
||||||
|
* @param inputStream
|
||||||
|
* @return
|
||||||
|
* @throws IOException
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2025/3/5 14:42
|
||||||
|
* @deprecated 因为jeecg主项目目前不支持poi5.x, 自己实现提取功能.
|
||||||
|
*/
|
||||||
|
@Deprecated
|
||||||
|
private static Document extractTextFromDoc(InputStream inputStream) throws IOException {
|
||||||
|
try (HWPFDocument document = new HWPFDocument(inputStream);
|
||||||
|
WordExtractor extractor = new WordExtractor(document)) {
|
||||||
|
return Document.from(extractor.getText());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提取excel文件内容
|
||||||
|
*
|
||||||
|
* @param inputStream
|
||||||
|
* @return
|
||||||
|
* @throws IOException
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2025/3/5 14:43
|
||||||
|
* @deprecated 因为jeecg主项目目前不支持poi5.x, 自己实现提取功能.
|
||||||
|
*/
|
||||||
|
@Deprecated
|
||||||
|
private static Document extractTextFromExcel(InputStream inputStream) throws IOException {
|
||||||
|
try (Workbook workbook = WorkbookFactory.create(inputStream)) {
|
||||||
|
StringBuilder text = new StringBuilder();
|
||||||
|
for (Sheet sheet : workbook) {
|
||||||
|
text.append("Sheet: ").append(sheet.getSheetName()).append("\n");
|
||||||
|
for (Row row : sheet) {
|
||||||
|
for (Cell cell : row) {
|
||||||
|
text.append(cell.toString()).append("\t");
|
||||||
|
}
|
||||||
|
text.append("\n");
|
||||||
|
}
|
||||||
|
text.append("\n");
|
||||||
|
}
|
||||||
|
return Document.from(text.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提取pptx文件内容
|
||||||
|
*
|
||||||
|
* @param inputStream
|
||||||
|
* @return
|
||||||
|
* @throws IOException
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2025/3/5 14:43
|
||||||
|
* @deprecated 因为jeecg主项目目前不支持poi5.x, 自己实现提取功能.
|
||||||
|
*/
|
||||||
|
@Deprecated
|
||||||
|
private static Document extractTextFromPptx(InputStream inputStream) throws IOException {
|
||||||
|
try (XMLSlideShow ppt = new XMLSlideShow(inputStream)) {
|
||||||
|
StringBuilder text = new StringBuilder();
|
||||||
|
for (XSLFSlide slide : ppt.getSlides()) {
|
||||||
|
text.append("Slide ").append(slide.getSlideNumber()).append(":\n");
|
||||||
|
List<XSLFTextShape> shapes = slide.getShapes().stream()
|
||||||
|
.filter(s -> s instanceof XSLFTextShape)
|
||||||
|
.map(s -> (XSLFTextShape) s)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
for (XSLFTextShape shape : shapes) {
|
||||||
|
text.append(shape.getText()).append("\n");
|
||||||
|
}
|
||||||
|
text.append("\n");
|
||||||
|
}
|
||||||
|
return Document.from(text.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提取ppt文件内容
|
||||||
|
*
|
||||||
|
* @param inputStream
|
||||||
|
* @return
|
||||||
|
* @throws IOException
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2025/3/5 14:43
|
||||||
|
* @deprecated 因为jeecg主项目目前不支持poi5.x, 自己实现提取功能.
|
||||||
|
*/
|
||||||
|
@Deprecated
|
||||||
|
private static Document extractTextFromPpt(InputStream inputStream) throws IOException {
|
||||||
|
try (org.apache.poi.hslf.usermodel.HSLFSlideShow ppt = new org.apache.poi.hslf.usermodel.HSLFSlideShow(inputStream)) {
|
||||||
|
StringBuilder text = new StringBuilder();
|
||||||
|
for (org.apache.poi.hslf.usermodel.HSLFSlide slide : ppt.getSlides()) {
|
||||||
|
text.append("Slide ").append(slide.getSlideNumber()).append(":\n");
|
||||||
|
for (List<HSLFTextParagraph> shapes : slide.getTextParagraphs()) {
|
||||||
|
text.append(HSLFTextParagraph.getText(shapes)).append("\n");
|
||||||
|
}
|
||||||
|
text.append("\n");
|
||||||
|
}
|
||||||
|
return Document.from(text.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] toByteArray(InputStream inputStream) throws IOException {
|
||||||
|
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
|
||||||
|
byte[] data = new byte[1024];
|
||||||
|
int nRead;
|
||||||
|
while ((nRead = inputStream.read(data, 0, data.length)) != -1) {
|
||||||
|
buffer.write(data, 0, nRead);
|
||||||
|
}
|
||||||
|
return buffer.toByteArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,104 @@
|
|||||||
|
package org.jeecg.modules.airag.llm.entity;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
import org.jeecg.common.aspect.annotation.Dict;
|
||||||
|
import org.jeecgframework.poi.excel.annotation.Excel;
|
||||||
|
import org.springframework.format.annotation.DateTimeFormat;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Description: AIRag知识库
|
||||||
|
* @Author: jeecg-boot
|
||||||
|
* @Date: 2025-02-18
|
||||||
|
* @Version: V1.0
|
||||||
|
*/
|
||||||
|
@Schema(description="AIRag知识库")
|
||||||
|
@Data
|
||||||
|
@TableName("airag_knowledge")
|
||||||
|
public class AiragKnowledge implements Serializable {
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 主键
|
||||||
|
*/
|
||||||
|
@TableId(type = IdType.ASSIGN_ID)
|
||||||
|
@Schema(description = "主键")
|
||||||
|
private String id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建人
|
||||||
|
*/
|
||||||
|
@Schema(description = "创建人")
|
||||||
|
private String createBy;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建日期
|
||||||
|
*/
|
||||||
|
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
|
||||||
|
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||||
|
@Schema(description = "创建日期")
|
||||||
|
private java.util.Date createTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新人
|
||||||
|
*/
|
||||||
|
@Schema(description = "更新人")
|
||||||
|
private String updateBy;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新日期
|
||||||
|
*/
|
||||||
|
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
|
||||||
|
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||||
|
@Schema(description = "更新日期")
|
||||||
|
private java.util.Date updateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 所属部门
|
||||||
|
*/
|
||||||
|
@Schema(description = "所属部门")
|
||||||
|
private String sysOrgCode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 租户id
|
||||||
|
*/
|
||||||
|
@Excel(name = "租户id", width = 15)
|
||||||
|
@Schema(description = "租户id")
|
||||||
|
private String tenantId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 知识库名称
|
||||||
|
*/
|
||||||
|
@Excel(name = "知识库名称", width = 15)
|
||||||
|
@Schema(description = "知识库名称")
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 向量模型id
|
||||||
|
*/
|
||||||
|
@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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 描述
|
||||||
|
*/
|
||||||
|
@Excel(name = "描述", width = 15)
|
||||||
|
@Schema(description = "描述")
|
||||||
|
private String descr;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 状态
|
||||||
|
*/
|
||||||
|
@Excel(name = "状态", width = 15)
|
||||||
|
@Schema(description = "状态")
|
||||||
|
private String status;
|
||||||
|
}
|
||||||
@ -0,0 +1,127 @@
|
|||||||
|
package org.jeecg.modules.airag.llm.entity;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.*;
|
||||||
|
import org.jeecg.common.constant.ProvinceCityArea;
|
||||||
|
import org.jeecg.common.util.SpringContextUtils;
|
||||||
|
import lombok.Data;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||||
|
import org.springframework.format.annotation.DateTimeFormat;
|
||||||
|
import org.jeecgframework.poi.excel.annotation.Excel;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
|
||||||
|
import java.io.UnsupportedEncodingException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Description: airag知识库文档
|
||||||
|
* @Author: jeecg-boot
|
||||||
|
* @Date: 2025-02-18
|
||||||
|
* @Version: V1.0
|
||||||
|
*/
|
||||||
|
@Schema(description="airag知识库文档")
|
||||||
|
@Data
|
||||||
|
@TableName("airag_knowledge_doc")
|
||||||
|
public class AiragKnowledgeDoc implements Serializable {
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 主键
|
||||||
|
*/
|
||||||
|
@TableId(type = IdType.ASSIGN_ID)
|
||||||
|
@Schema(description = "主键")
|
||||||
|
private String id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建人
|
||||||
|
*/
|
||||||
|
@Schema(description = "创建人")
|
||||||
|
private String createBy;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建日期
|
||||||
|
*/
|
||||||
|
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
|
||||||
|
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||||
|
@Schema(description = "创建日期")
|
||||||
|
private Date createTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新人
|
||||||
|
*/
|
||||||
|
@Schema(description = "更新人")
|
||||||
|
private String updateBy;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新日期
|
||||||
|
*/
|
||||||
|
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
|
||||||
|
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||||
|
@Schema(description = "更新日期")
|
||||||
|
private Date updateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 所属部门
|
||||||
|
*/
|
||||||
|
@Schema(description = "所属部门")
|
||||||
|
private String sysOrgCode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 租户id
|
||||||
|
*/
|
||||||
|
@Excel(name = "租户id", width = 15)
|
||||||
|
@Schema(description = "租户id")
|
||||||
|
private String tenantId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 知识库id
|
||||||
|
*/
|
||||||
|
@Schema(description = "知识库id")
|
||||||
|
private String knowledgeId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标题
|
||||||
|
*/
|
||||||
|
@Excel(name = "标题", width = 15)
|
||||||
|
@Schema(description = "标题")
|
||||||
|
private String title;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 类型
|
||||||
|
*/
|
||||||
|
@Excel(name = "类型", width = 15, dicCode = "know_doc_type")
|
||||||
|
@Schema(description = "类型")
|
||||||
|
private String type;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 内容
|
||||||
|
*/
|
||||||
|
@Excel(name = "内容", width = 15)
|
||||||
|
@Schema(description = "内容")
|
||||||
|
private String content;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 元数据,存储上传文件的存储目录以及网站站点 <br/>
|
||||||
|
* eg. {"filePath":"https://xxxxxx","website":"http://hellp.jeecg.com"}
|
||||||
|
*/
|
||||||
|
@Excel(name = "元数据", width = 15)
|
||||||
|
@Schema(description = "元数据")
|
||||||
|
private String metadata;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 状态
|
||||||
|
*/
|
||||||
|
@Excel(name = "状态", width = 15)
|
||||||
|
@Schema(description = "状态")
|
||||||
|
private String status;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 服务器基础路径
|
||||||
|
*/
|
||||||
|
@TableField(exist = false)
|
||||||
|
private String baseUrl;
|
||||||
|
}
|
||||||
@ -0,0 +1,123 @@
|
|||||||
|
package org.jeecg.modules.airag.llm.entity;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.io.UnsupportedEncodingException;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableLogic;
|
||||||
|
import org.jeecg.common.constant.ProvinceCityArea;
|
||||||
|
import org.jeecg.common.util.SpringContextUtils;
|
||||||
|
import lombok.Data;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||||
|
import org.springframework.format.annotation.DateTimeFormat;
|
||||||
|
import org.jeecgframework.poi.excel.annotation.Excel;
|
||||||
|
import org.jeecg.common.aspect.annotation.Dict;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.experimental.Accessors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Description: AiRag模型配置
|
||||||
|
* @Author: jeecg-boot
|
||||||
|
* @Date: 2025-02-17
|
||||||
|
* @Version: V1.0
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@TableName("airag_model")
|
||||||
|
@Accessors(chain = true)
|
||||||
|
@EqualsAndHashCode(callSuper = false)
|
||||||
|
@Schema(description="AiRag模型配置")
|
||||||
|
public class AiragModel implements Serializable {
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 主键
|
||||||
|
*/
|
||||||
|
@TableId(type = IdType.ASSIGN_ID)
|
||||||
|
@Schema(description = "主键")
|
||||||
|
private String id;
|
||||||
|
/**
|
||||||
|
* 创建人
|
||||||
|
*/
|
||||||
|
@Schema(description = "创建人")
|
||||||
|
private String createBy;
|
||||||
|
/**
|
||||||
|
* 创建日期
|
||||||
|
*/
|
||||||
|
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
|
||||||
|
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||||
|
@Schema(description = "创建日期")
|
||||||
|
private Date createTime;
|
||||||
|
/**
|
||||||
|
* 更新人
|
||||||
|
*/
|
||||||
|
@Schema(description = "更新人")
|
||||||
|
private String updateBy;
|
||||||
|
/**
|
||||||
|
* 更新日期
|
||||||
|
*/
|
||||||
|
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
|
||||||
|
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||||
|
@Schema(description = "更新日期")
|
||||||
|
private Date updateTime;
|
||||||
|
/**
|
||||||
|
* 所属部门
|
||||||
|
*/
|
||||||
|
@Schema(description = "所属部门")
|
||||||
|
private String sysOrgCode;
|
||||||
|
/**
|
||||||
|
* 租户id
|
||||||
|
*/
|
||||||
|
@Excel(name = "租户id", width = 15)
|
||||||
|
@Schema(description = "租户id")
|
||||||
|
private String tenantId;
|
||||||
|
/**
|
||||||
|
* 名称
|
||||||
|
*/
|
||||||
|
@Excel(name = "名称", width = 15)
|
||||||
|
@Schema(description = "名称")
|
||||||
|
private String name;
|
||||||
|
/**
|
||||||
|
* 供应者
|
||||||
|
*/
|
||||||
|
@Excel(name = "供应者", width = 15, dicCode = "model_provider")
|
||||||
|
@Dict(dicCode = "model_provider")
|
||||||
|
@Schema(description = "供应者")
|
||||||
|
private String provider;
|
||||||
|
/**
|
||||||
|
* 模型类型
|
||||||
|
*/
|
||||||
|
@Excel(name = "模型类型", width = 15, dicCode = "model_type")
|
||||||
|
@Dict(dicCode = "model_type")
|
||||||
|
@Schema(description = "模型类型")
|
||||||
|
private String modelType;
|
||||||
|
/**
|
||||||
|
* 模型名称
|
||||||
|
*/
|
||||||
|
@Excel(name = "模型名称", width = 15)
|
||||||
|
@Schema(description = "模型名称")
|
||||||
|
private String modelName;
|
||||||
|
/**
|
||||||
|
* API域名
|
||||||
|
*/
|
||||||
|
@Excel(name = "API域名", width = 15)
|
||||||
|
@Schema(description = "API域名")
|
||||||
|
private String baseUrl;
|
||||||
|
/**
|
||||||
|
* 凭证信息
|
||||||
|
*/
|
||||||
|
@Excel(name = "凭证信息", width = 15)
|
||||||
|
@Schema(description = "凭证信息")
|
||||||
|
private String credential;
|
||||||
|
/**
|
||||||
|
* 模型参数
|
||||||
|
*/
|
||||||
|
@Excel(name = "模型参数", width = 15)
|
||||||
|
@Schema(description = "模型参数")
|
||||||
|
private String modelParams;
|
||||||
|
}
|
||||||
@ -0,0 +1,286 @@
|
|||||||
|
package org.jeecg.modules.airag.llm.handler;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson.JSONObject;
|
||||||
|
import dev.langchain4j.data.message.*;
|
||||||
|
import dev.langchain4j.rag.query.router.QueryRouter;
|
||||||
|
import dev.langchain4j.service.TokenStream;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.jeecg.ai.handler.LLMHandler;
|
||||||
|
import org.jeecg.common.util.AssertUtils;
|
||||||
|
import org.jeecg.common.util.oConvertUtils;
|
||||||
|
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.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Base64;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 大模型聊天工具类
|
||||||
|
*
|
||||||
|
* @Author: chenrui
|
||||||
|
* @Date: 2025/2/18 14:31
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
public class AIChatHandler implements IAIChatHandler {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
IAiragModelService airagModelService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
EmbeddingHandler embeddingHandler;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
LLMHandler llmHandler;
|
||||||
|
|
||||||
|
|
||||||
|
@Value(value = "${jeecg.path.upload:}")
|
||||||
|
private String uploadpath;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 问答
|
||||||
|
*
|
||||||
|
* @param modelId
|
||||||
|
* @param messages
|
||||||
|
* @return
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2025/2/18 21:03
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public String completions(String modelId, List<ChatMessage> messages) {
|
||||||
|
AssertUtils.assertNotEmpty("至少发送一条消息", messages);
|
||||||
|
AssertUtils.assertNotEmpty("请选择模型", modelId);
|
||||||
|
// 整理消息
|
||||||
|
return completions(modelId, messages, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 问答
|
||||||
|
*
|
||||||
|
* @param modelId
|
||||||
|
* @param messages
|
||||||
|
* @param params
|
||||||
|
* @return
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2025/2/18 21:03
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public String completions(String modelId, List<ChatMessage> messages, AIChatParams params) {
|
||||||
|
AssertUtils.assertNotEmpty("至少发送一条消息", messages);
|
||||||
|
AssertUtils.assertNotEmpty("请选择模型", modelId);
|
||||||
|
|
||||||
|
AiragModel airagModel = airagModelService.getById(modelId);
|
||||||
|
return completions(airagModel, messages, params);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 问答
|
||||||
|
*
|
||||||
|
* @param airagModel
|
||||||
|
* @param messages
|
||||||
|
* @param params
|
||||||
|
* @return
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2025/2/24 17:30
|
||||||
|
*/
|
||||||
|
private String completions(AiragModel airagModel, List<ChatMessage> messages, AIChatParams params) {
|
||||||
|
params = mergeParams(airagModel, params);
|
||||||
|
String resp = llmHandler.completions(messages, params);
|
||||||
|
if (resp.contains("</think>")
|
||||||
|
&& (null == params.getNoThinking() || params.getNoThinking())) {
|
||||||
|
String[] thinkSplit = resp.split("</think>");
|
||||||
|
resp = thinkSplit[thinkSplit.length - 1];
|
||||||
|
}
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用默认模型问答
|
||||||
|
*
|
||||||
|
* @param messages
|
||||||
|
* @param params
|
||||||
|
* @return
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2025/3/12 15:13
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public String completionsByDefaultModel(List<ChatMessage> messages, AIChatParams params) {
|
||||||
|
return completions(new AiragModel(), messages, params);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 聊天(流式)
|
||||||
|
*
|
||||||
|
* @param modelId
|
||||||
|
* @param messages
|
||||||
|
* @return
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2025/2/20 21:06
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public TokenStream chat(String modelId, List<ChatMessage> messages) {
|
||||||
|
return chat(modelId, messages, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 聊天(流式)
|
||||||
|
*
|
||||||
|
* @param modelId
|
||||||
|
* @param messages
|
||||||
|
* @param params
|
||||||
|
* @return
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2025/2/18 21:03
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public TokenStream chat(String modelId, List<ChatMessage> messages, AIChatParams params) {
|
||||||
|
AssertUtils.assertNotEmpty("至少发送一条消息", messages);
|
||||||
|
AssertUtils.assertNotEmpty("请选择模型", modelId);
|
||||||
|
|
||||||
|
AiragModel airagModel = airagModelService.getById(modelId);
|
||||||
|
return chat(airagModel, messages, params);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 聊天(流式)
|
||||||
|
*
|
||||||
|
* @param airagModel
|
||||||
|
* @param messages
|
||||||
|
* @param params
|
||||||
|
* @return
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2025/2/24 17:29
|
||||||
|
*/
|
||||||
|
private TokenStream chat(AiragModel airagModel, List<ChatMessage> messages, AIChatParams params) {
|
||||||
|
params = mergeParams(airagModel, params);
|
||||||
|
return llmHandler.chat(messages, params);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用默认模型聊天
|
||||||
|
*
|
||||||
|
* @param messages
|
||||||
|
* @param params
|
||||||
|
* @return
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2025/3/12 15:13
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public TokenStream chatByDefaultModel(List<ChatMessage> messages, AIChatParams params) {
|
||||||
|
return chat(new AiragModel(), messages, params);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 合并 airagmodel和params,params为准
|
||||||
|
*
|
||||||
|
* @param airagModel
|
||||||
|
* @param params
|
||||||
|
* @return
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2025/3/11 17:45
|
||||||
|
*/
|
||||||
|
private AIChatParams mergeParams(AiragModel airagModel, AIChatParams params) {
|
||||||
|
if (null == airagModel) {
|
||||||
|
return params;
|
||||||
|
}
|
||||||
|
if (params == null) {
|
||||||
|
params = new AIChatParams();
|
||||||
|
}
|
||||||
|
|
||||||
|
params.setProvider(airagModel.getProvider());
|
||||||
|
params.setModelName(airagModel.getModelName());
|
||||||
|
params.setBaseUrl(airagModel.getBaseUrl());
|
||||||
|
if (oConvertUtils.isObjectNotEmpty(airagModel.getCredential())) {
|
||||||
|
JSONObject modelCredential = JSONObject.parseObject(airagModel.getCredential());
|
||||||
|
params.setApiKey(oConvertUtils.getString(modelCredential.getString("apiKey"), null));
|
||||||
|
params.setSecretKey(oConvertUtils.getString(modelCredential.getString("secretKey"), null));
|
||||||
|
}
|
||||||
|
if (oConvertUtils.isObjectNotEmpty(airagModel.getModelParams())) {
|
||||||
|
JSONObject modelParams = JSONObject.parseObject(airagModel.getModelParams());
|
||||||
|
if (oConvertUtils.isObjectEmpty(params.getTemperature())) {
|
||||||
|
params.setTemperature(modelParams.getDouble("temperature"));
|
||||||
|
}
|
||||||
|
if (oConvertUtils.isObjectEmpty(params.getTopP())) {
|
||||||
|
params.setTopP(modelParams.getDouble("topP"));
|
||||||
|
}
|
||||||
|
if (oConvertUtils.isObjectEmpty(params.getPresencePenalty())) {
|
||||||
|
params.setPresencePenalty(modelParams.getDouble("presencePenalty"));
|
||||||
|
}
|
||||||
|
if (oConvertUtils.isObjectEmpty(params.getFrequencyPenalty())) {
|
||||||
|
params.setFrequencyPenalty(modelParams.getDouble("frequencyPenalty"));
|
||||||
|
}
|
||||||
|
if (oConvertUtils.isObjectEmpty(params.getMaxTokens())) {
|
||||||
|
params.setMaxTokens(modelParams.getInteger("maxTokens"));
|
||||||
|
}
|
||||||
|
if (oConvertUtils.isObjectEmpty(params.getTimeout())) {
|
||||||
|
params.setMaxTokens(modelParams.getInteger("timeout"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RAG
|
||||||
|
List<String> knowIds = params.getKnowIds();
|
||||||
|
if (oConvertUtils.isObjectNotEmpty(knowIds)) {
|
||||||
|
QueryRouter queryRouter = embeddingHandler.getQueryRouter(knowIds, params.getTopNumber(), params.getSimilarity());
|
||||||
|
params.setQueryRouter(queryRouter);
|
||||||
|
}
|
||||||
|
|
||||||
|
return params;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public UserMessage buildUserMessage(String content, List<String> images) {
|
||||||
|
AssertUtils.assertNotEmpty("请输入消息内容", content);
|
||||||
|
List<Content> contents = new ArrayList<>();
|
||||||
|
contents.add(TextContent.from(content));
|
||||||
|
if (oConvertUtils.isObjectNotEmpty(images)) {
|
||||||
|
// 获取所有图片,将他们转换为ImageContent
|
||||||
|
List<ImageContent> imageContents = buildImageContents(images);
|
||||||
|
contents.addAll(imageContents);
|
||||||
|
}
|
||||||
|
return UserMessage.from(contents);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<ImageContent> buildImageContents(List<String> images) {
|
||||||
|
List<ImageContent> imageContents = new ArrayList<>();
|
||||||
|
for (String imageUrl : images) {
|
||||||
|
Matcher matcher = LLMConsts.WEB_PATTERN.matcher(imageUrl);
|
||||||
|
if (matcher.matches()) {
|
||||||
|
// 来源于网络
|
||||||
|
imageContents.add(ImageContent.from(imageUrl));
|
||||||
|
} else {
|
||||||
|
// 本地文件
|
||||||
|
String filePath = uploadpath + File.separator + imageUrl;
|
||||||
|
// 读取文件并转换为 base64 编码字符串
|
||||||
|
try {
|
||||||
|
Path path = Paths.get(filePath);
|
||||||
|
byte[] fileContent = Files.readAllBytes(path);
|
||||||
|
String base64Data = Base64.getEncoder().encodeToString(fileContent);
|
||||||
|
// 获取文件的 MIME 类型
|
||||||
|
String mimeType = Files.probeContentType(path);
|
||||||
|
// 构建 ImageContent 对象
|
||||||
|
imageContents.add(ImageContent.from(base64Data, mimeType));
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error("读取文件失败: " + filePath, e);
|
||||||
|
throw new RuntimeException("发送消息失败,读取文件异常:" + e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return imageContents;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,139 @@
|
|||||||
|
package org.jeecg.modules.airag.llm.handler;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.commons.lang.ArrayUtils;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Description: 命令行执行工具类
|
||||||
|
* @Author: chenrui
|
||||||
|
* @Date: 2024/4/8 10:11
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
public class CommandExecUtil {
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行命令行
|
||||||
|
*
|
||||||
|
* @param command
|
||||||
|
* @param args
|
||||||
|
* @return
|
||||||
|
* @throws IOException
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2024/4/9 10:59
|
||||||
|
*/
|
||||||
|
public static String execCommand(String command, String[] args) throws IOException {
|
||||||
|
if (null == command || command.isEmpty()) {
|
||||||
|
throw new IllegalArgumentException("命令不能为空");
|
||||||
|
}
|
||||||
|
return execCommand(command.split(" "), args);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行命令行
|
||||||
|
*
|
||||||
|
* @param command 脚本目录
|
||||||
|
* @param args 参数
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2024/4/09 10:30
|
||||||
|
*/
|
||||||
|
public static String execCommand(String[] command, String[] args) throws IOException {
|
||||||
|
|
||||||
|
if (null == command || command.length == 0) {
|
||||||
|
throw new IllegalArgumentException("命令不能为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (null != args && args.length > 0) {
|
||||||
|
command = (String[]) ArrayUtils.addAll(command, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
// windows系统处理文件夹空格问题
|
||||||
|
if (System.getProperty("os.name").toLowerCase().startsWith("windows")) {
|
||||||
|
List<String> commandNew = new ArrayList<>(command.length + 2);
|
||||||
|
commandNew.addAll(Arrays.asList("cmd.exe", "/c"));
|
||||||
|
for (String tempCommand : command) {
|
||||||
|
if (tempCommand.contains(" ")) {
|
||||||
|
tempCommand = "\"" + tempCommand.replaceAll("\"", "'") + "\"";
|
||||||
|
}
|
||||||
|
commandNew.add(tempCommand);
|
||||||
|
}
|
||||||
|
command = commandNew.toArray(new String[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Process process = null;
|
||||||
|
try {
|
||||||
|
log.debug(" =============================== Runtime command Script ===============================" );
|
||||||
|
log.debug(String.join(" ", command));
|
||||||
|
log.debug(" =============================== Runtime command Script =============================== " );
|
||||||
|
process = Runtime.getRuntime().exec(command);
|
||||||
|
try (ByteArrayOutputStream resultOutStream = new ByteArrayOutputStream();
|
||||||
|
InputStream processInStream = new BufferedInputStream(process.getInputStream())) {
|
||||||
|
new Thread(new InputStreamRunnable(process.getErrorStream(), "ErrorStream")).start();
|
||||||
|
int num;
|
||||||
|
byte[] bs = new byte[1024];
|
||||||
|
while ((num = processInStream.read(bs)) != -1) {
|
||||||
|
resultOutStream.write(bs, 0, num);
|
||||||
|
String stepMsg = new String(bs);
|
||||||
|
// log.debug("命令行日志:" + stepMsg);
|
||||||
|
if (stepMsg.contains("input any key to continue...")) {
|
||||||
|
process.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
String result = resultOutStream.toString();
|
||||||
|
log.debug("执行命令完成:" + result);
|
||||||
|
return result;
|
||||||
|
} catch (IOException ex) {
|
||||||
|
throw new RuntimeException(ex);
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error(e.getMessage(), e);
|
||||||
|
throw e;
|
||||||
|
} finally {
|
||||||
|
if (process != null) {
|
||||||
|
process.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* exec 控制台输出获取线程类
|
||||||
|
* 使用单独的线程获取控制台输出,防止输入流阻塞
|
||||||
|
*
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2024/4/09 10:30
|
||||||
|
*/
|
||||||
|
static class InputStreamRunnable implements Runnable {
|
||||||
|
BufferedReader bReader = null;
|
||||||
|
String type = null;
|
||||||
|
|
||||||
|
public InputStreamRunnable(InputStream is, String _type) {
|
||||||
|
try {
|
||||||
|
bReader = new BufferedReader(new InputStreamReader(new BufferedInputStream(is), StandardCharsets.UTF_8));
|
||||||
|
type = _type;
|
||||||
|
} catch (Exception ex) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
public void run() {
|
||||||
|
String line;
|
||||||
|
int lineNum = 0;
|
||||||
|
|
||||||
|
try {
|
||||||
|
while ((line = bReader.readLine()) != null) {
|
||||||
|
lineNum++;
|
||||||
|
// Thread.sleep(200);
|
||||||
|
}
|
||||||
|
bReader.close();
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,582 @@
|
|||||||
|
package org.jeecg.modules.airag.llm.handler;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson.JSONObject;
|
||||||
|
import com.google.common.collect.Lists;
|
||||||
|
import dev.langchain4j.data.document.Document;
|
||||||
|
import dev.langchain4j.data.document.DocumentSplitter;
|
||||||
|
import dev.langchain4j.data.document.Metadata;
|
||||||
|
import dev.langchain4j.data.document.splitter.DocumentSplitters;
|
||||||
|
import dev.langchain4j.data.embedding.Embedding;
|
||||||
|
import dev.langchain4j.data.segment.TextSegment;
|
||||||
|
import dev.langchain4j.model.embedding.EmbeddingModel;
|
||||||
|
import dev.langchain4j.model.openai.OpenAiTokenizer;
|
||||||
|
import dev.langchain4j.rag.content.retriever.ContentRetriever;
|
||||||
|
import dev.langchain4j.rag.content.retriever.EmbeddingStoreContentRetriever;
|
||||||
|
import dev.langchain4j.rag.query.router.DefaultQueryRouter;
|
||||||
|
import dev.langchain4j.rag.query.router.QueryRouter;
|
||||||
|
import dev.langchain4j.store.embedding.EmbeddingMatch;
|
||||||
|
import dev.langchain4j.store.embedding.EmbeddingSearchRequest;
|
||||||
|
import dev.langchain4j.store.embedding.EmbeddingStore;
|
||||||
|
import dev.langchain4j.store.embedding.EmbeddingStoreIngestor;
|
||||||
|
import dev.langchain4j.store.embedding.pgvector.PgVectorEmbeddingStore;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.commons.io.FilenameUtils;
|
||||||
|
import org.apache.tika.parser.AutoDetectParser;
|
||||||
|
import org.jeecg.ai.factory.AiModelFactory;
|
||||||
|
import org.jeecg.ai.factory.AiModelOptions;
|
||||||
|
import org.jeecg.common.exception.JeecgBootException;
|
||||||
|
import org.jeecg.common.util.*;
|
||||||
|
import org.jeecg.modules.airag.common.handler.IEmbeddingHandler;
|
||||||
|
import org.jeecg.modules.airag.common.vo.knowledge.KnowledgeSearchResult;
|
||||||
|
import org.jeecg.modules.airag.llm.config.EmbedStoreConfigBean;
|
||||||
|
import org.jeecg.modules.airag.llm.config.KnowConfigBean;
|
||||||
|
import org.jeecg.modules.airag.llm.consts.LLMConsts;
|
||||||
|
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.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;
|
||||||
|
import org.springframework.context.annotation.Lazy;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import static dev.langchain4j.store.embedding.filter.MetadataFilterBuilder.metadataKey;
|
||||||
|
import static org.jeecg.modules.airag.llm.consts.LLMConsts.KNOWLEDGE_DOC_TYPE_FILE;
|
||||||
|
import static org.jeecg.modules.airag.llm.consts.LLMConsts.KNOWLEDGE_DOC_TYPE_WEB;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 向量工具类
|
||||||
|
*
|
||||||
|
* @Author: chenrui
|
||||||
|
* @Date: 2025/2/18 14:31
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
public class EmbeddingHandler implements IEmbeddingHandler {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
EmbedStoreConfigBean embedStoreConfigBean;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
@Lazy
|
||||||
|
private IAiragModelService airagModelService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
@Lazy
|
||||||
|
private IAiragKnowledgeService airagKnowledgeService;
|
||||||
|
|
||||||
|
@Value(value = "${jeecg.path.upload:}")
|
||||||
|
private String uploadpath;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
KnowConfigBean knowConfigBean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 默认分段长度
|
||||||
|
*/
|
||||||
|
private static final int DEFAULT_SEGMENT_SIZE = 1000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 默认分段重叠长度
|
||||||
|
*/
|
||||||
|
private static final int DEFAULT_OVERLAP_SIZE = 50;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 向量存储元数据:knowledgeId
|
||||||
|
*/
|
||||||
|
public static final String EMBED_STORE_METADATA_KNOWLEDGEID = "knowledgeId";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 向量存储元数据:docId
|
||||||
|
*/
|
||||||
|
public static final String EMBED_STORE_METADATA_DOCID = "docId";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 向量存储元数据:docName
|
||||||
|
*/
|
||||||
|
public static final String EMBED_STORE_METADATA_DOCNAME = "docName";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 向量存储缓存
|
||||||
|
*/
|
||||||
|
private static final ConcurrentHashMap<String, EmbeddingStore<TextSegment>> EMBED_STORE_CACHE = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 向量化文档
|
||||||
|
*
|
||||||
|
* @param knowId
|
||||||
|
* @param doc
|
||||||
|
* @return
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2025/2/18 11:52
|
||||||
|
*/
|
||||||
|
public Map<String, Object> embeddingDocument(String knowId, AiragKnowledgeDoc doc) {
|
||||||
|
AiragKnowledge airagKnowledge = airagKnowledgeService.getById(knowId);
|
||||||
|
AssertUtils.assertNotEmpty("知识库不存在", airagKnowledge);
|
||||||
|
AssertUtils.assertNotEmpty("请先为知识库配置向量模型库", airagKnowledge.getEmbedId());
|
||||||
|
AssertUtils.assertNotEmpty("文档不能为空", doc);
|
||||||
|
// 读取文档
|
||||||
|
String content = doc.getContent();
|
||||||
|
// 向量化并存储
|
||||||
|
if (oConvertUtils.isEmpty(content)) {
|
||||||
|
switch (doc.getType()) {
|
||||||
|
case KNOWLEDGE_DOC_TYPE_FILE:
|
||||||
|
//解析文件
|
||||||
|
if (knowConfigBean.isEnableMinerU()) {
|
||||||
|
parseFileByMinerU(doc);
|
||||||
|
}
|
||||||
|
content = parseFile(doc);
|
||||||
|
break;
|
||||||
|
case KNOWLEDGE_DOC_TYPE_WEB:
|
||||||
|
// TODO author: chenrui for:读取网站内容 date:2025/2/18
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//update-begin---author:chenrui ---date:20250307 for:[QQYUN-11443]【AI】是不是应该把标题也生成到向量库里,标题一般是有意义的------------
|
||||||
|
if (oConvertUtils.isNotEmpty(doc.getTitle())) {
|
||||||
|
content = doc.getTitle() + "\n\n" + content;
|
||||||
|
}
|
||||||
|
//update-end---author:chenrui ---date:20250307 for:[QQYUN-11443]【AI】是不是应该把标题也生成到向量库里,标题一般是有意义的------------
|
||||||
|
|
||||||
|
// 向量化 date:2025/2/18
|
||||||
|
AiragModel model = getEmbedModelData(airagKnowledge.getEmbedId());
|
||||||
|
AiModelOptions modelOp = buildModelOptions(model);
|
||||||
|
EmbeddingModel embeddingModel = AiModelFactory.createEmbeddingModel(modelOp);
|
||||||
|
EmbeddingStore<TextSegment> embeddingStore = getEmbedStore(model);
|
||||||
|
// 删除旧数据
|
||||||
|
embeddingStore.removeAll(metadataKey(EMBED_STORE_METADATA_DOCID).isEqualTo(doc.getId()));
|
||||||
|
// 分段器
|
||||||
|
DocumentSplitter splitter = DocumentSplitters.recursive(DEFAULT_SEGMENT_SIZE, DEFAULT_OVERLAP_SIZE, new OpenAiTokenizer());
|
||||||
|
// 分段并存储
|
||||||
|
EmbeddingStoreIngestor ingestor = EmbeddingStoreIngestor.builder()
|
||||||
|
.documentSplitter(splitter)
|
||||||
|
.embeddingModel(embeddingModel)
|
||||||
|
.embeddingStore(embeddingStore)
|
||||||
|
.build();
|
||||||
|
Metadata metadata = Metadata.metadata(EMBED_STORE_METADATA_DOCID, doc.getId())
|
||||||
|
.put(EMBED_STORE_METADATA_KNOWLEDGEID, doc.getKnowledgeId())
|
||||||
|
.put(EMBED_STORE_METADATA_DOCNAME, FilenameUtils.getName(doc.getTitle()));
|
||||||
|
Document from = Document.from(content, metadata);
|
||||||
|
ingestor.ingest(from);
|
||||||
|
return metadata.toMap();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 向量查询(多知识库)
|
||||||
|
*
|
||||||
|
* @param knowIds
|
||||||
|
* @param queryText
|
||||||
|
* @param topNumber
|
||||||
|
* @param similarity
|
||||||
|
* @return
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2025/2/18 16:52
|
||||||
|
*/
|
||||||
|
public KnowledgeSearchResult embeddingSearch(List<String> knowIds, String queryText, Integer topNumber, Double similarity) {
|
||||||
|
AssertUtils.assertNotEmpty("请选择知识库", knowIds);
|
||||||
|
AssertUtils.assertNotEmpty("请填写查询内容", queryText);
|
||||||
|
|
||||||
|
topNumber = oConvertUtils.getInteger(topNumber, 5);
|
||||||
|
|
||||||
|
//命中的文档列表
|
||||||
|
List<Map<String, Object>> documents = new ArrayList<>(16);
|
||||||
|
for (String knowId : knowIds) {
|
||||||
|
List<Map<String, Object>> searchResp = searchEmbedding(knowId, queryText, topNumber, similarity);
|
||||||
|
if (oConvertUtils.isObjectNotEmpty(searchResp)) {
|
||||||
|
documents.addAll(searchResp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//命中的文档内容
|
||||||
|
StringBuilder data = new StringBuilder();
|
||||||
|
// 对documents按score降序排序并取前topNumber个
|
||||||
|
List<Map<String, Object>> sortedDocuments = documents.stream()
|
||||||
|
.sorted(Comparator.comparingDouble((Map<String, Object> doc) -> (Double) doc.get("score")).reversed())
|
||||||
|
.limit(topNumber)
|
||||||
|
.peek(doc -> data.append(doc.get("content")).append("\n"))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
return new KnowledgeSearchResult(data.toString(), sortedDocuments);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 向量查询
|
||||||
|
*
|
||||||
|
* @param knowId
|
||||||
|
* @param queryText
|
||||||
|
* @param topNumber
|
||||||
|
* @param similarity
|
||||||
|
* @return
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2025/2/18 16:52
|
||||||
|
*/
|
||||||
|
public List<Map<String, Object>> searchEmbedding(String knowId, String queryText, Integer topNumber, Double similarity) {
|
||||||
|
AssertUtils.assertNotEmpty("请选择知识库", knowId);
|
||||||
|
AiragKnowledge knowledge = airagKnowledgeService.getById(knowId);
|
||||||
|
AssertUtils.assertNotEmpty("知识库不存在", knowledge);
|
||||||
|
AssertUtils.assertNotEmpty("请填写查询内容", queryText);
|
||||||
|
AiragModel model = getEmbedModelData(knowledge.getEmbedId());
|
||||||
|
|
||||||
|
AiModelOptions modelOp = buildModelOptions(model);
|
||||||
|
EmbeddingModel embeddingModel = AiModelFactory.createEmbeddingModel(modelOp);
|
||||||
|
Embedding queryEmbedding = embeddingModel.embed(queryText).content();
|
||||||
|
|
||||||
|
topNumber = oConvertUtils.getInteger(topNumber, modelOp.getTopNumber());
|
||||||
|
similarity = oConvertUtils.getDou(similarity, modelOp.getSimilarity());
|
||||||
|
EmbeddingSearchRequest embeddingSearchRequest = EmbeddingSearchRequest.builder()
|
||||||
|
.queryEmbedding(queryEmbedding)
|
||||||
|
.maxResults(topNumber)
|
||||||
|
.minScore(similarity)
|
||||||
|
.filter(metadataKey(EMBED_STORE_METADATA_KNOWLEDGEID).isEqualTo(knowId))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
EmbeddingStore<TextSegment> embeddingStore = getEmbedStore(model);
|
||||||
|
List<EmbeddingMatch<TextSegment>> relevant = embeddingStore.search(embeddingSearchRequest).matches();
|
||||||
|
List<Map<String, Object>> result = new ArrayList<>();
|
||||||
|
if (oConvertUtils.isObjectNotEmpty(relevant)) {
|
||||||
|
result = relevant.stream().map(matchRes -> {
|
||||||
|
Map<String, Object> data = new HashMap<>();
|
||||||
|
data.put("score", matchRes.score());
|
||||||
|
data.put("content", matchRes.embedded().text());
|
||||||
|
Metadata metadata = matchRes.embedded().metadata();
|
||||||
|
data.put("chunk", metadata.getInteger("index"));
|
||||||
|
data.put(EMBED_STORE_METADATA_DOCNAME, metadata.getString(EMBED_STORE_METADATA_DOCNAME));
|
||||||
|
return data;
|
||||||
|
}).collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取向量查询路由
|
||||||
|
*
|
||||||
|
* @param knowIds
|
||||||
|
* @param topNumber
|
||||||
|
* @param similarity
|
||||||
|
* @return
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2025/2/20 21:03
|
||||||
|
*/
|
||||||
|
public QueryRouter getQueryRouter(List<String> knowIds, Integer topNumber, Double similarity) {
|
||||||
|
AssertUtils.assertNotEmpty("请选择知识库", knowIds);
|
||||||
|
List<ContentRetriever> retrievers = Lists.newArrayList();
|
||||||
|
for (String knowId : knowIds) {
|
||||||
|
if (oConvertUtils.isEmpty(knowId)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
AiragKnowledge knowledge = airagKnowledgeService.getById(knowId);
|
||||||
|
AssertUtils.assertNotEmpty("知识库不存在", knowledge);
|
||||||
|
AiragModel model = getEmbedModelData(knowledge.getEmbedId());
|
||||||
|
AiModelOptions modelOptions = buildModelOptions(model);
|
||||||
|
EmbeddingModel embeddingModel = AiModelFactory.createEmbeddingModel(modelOptions);
|
||||||
|
|
||||||
|
EmbeddingStore<TextSegment> embeddingStore = getEmbedStore(model);
|
||||||
|
topNumber = oConvertUtils.getInteger(topNumber, 5);
|
||||||
|
similarity = oConvertUtils.getDou(similarity, 0.75);
|
||||||
|
// 构建一个嵌入存储内容检索器,用于从嵌入存储中检索内容
|
||||||
|
EmbeddingStoreContentRetriever contentRetriever = EmbeddingStoreContentRetriever.builder()
|
||||||
|
.embeddingStore(embeddingStore)
|
||||||
|
.embeddingModel(embeddingModel)
|
||||||
|
.maxResults(topNumber)
|
||||||
|
.minScore(similarity)
|
||||||
|
.filter(metadataKey(EMBED_STORE_METADATA_KNOWLEDGEID).isEqualTo(knowId))
|
||||||
|
.build();
|
||||||
|
retrievers.add(contentRetriever);
|
||||||
|
}
|
||||||
|
if (retrievers.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
return new DefaultQueryRouter(retrievers);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除向量化文档
|
||||||
|
*
|
||||||
|
* @param knowId
|
||||||
|
* @param modelId
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2025/2/18 19:07
|
||||||
|
*/
|
||||||
|
public void deleteEmbedDocsByKnowId(String knowId, String modelId) {
|
||||||
|
AssertUtils.assertNotEmpty("选择知识库", knowId);
|
||||||
|
AiragModel model = getEmbedModelData(modelId);
|
||||||
|
|
||||||
|
EmbeddingStore<TextSegment> embeddingStore = getEmbedStore(model);
|
||||||
|
// 删除数据
|
||||||
|
embeddingStore.removeAll(metadataKey(EMBED_STORE_METADATA_KNOWLEDGEID).isEqualTo(knowId));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除向量化文档
|
||||||
|
*
|
||||||
|
* @param docIds
|
||||||
|
* @param modelId
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2025/2/18 19:07
|
||||||
|
*/
|
||||||
|
public void deleteEmbedDocsByDocIds(List<String> docIds, String modelId) {
|
||||||
|
AssertUtils.assertNotEmpty("选择文档", docIds);
|
||||||
|
AiragModel model = getEmbedModelData(modelId);
|
||||||
|
|
||||||
|
EmbeddingStore<TextSegment> embeddingStore = getEmbedStore(model);
|
||||||
|
// 删除数据
|
||||||
|
embeddingStore.removeAll(metadataKey(EMBED_STORE_METADATA_DOCID).isIn(docIds));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询向量模型数据
|
||||||
|
*
|
||||||
|
* @param modelId
|
||||||
|
* @return
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2025/2/20 20:08
|
||||||
|
*/
|
||||||
|
private AiragModel getEmbedModelData(String modelId) {
|
||||||
|
AssertUtils.assertNotEmpty("向量模型不能为空", modelId);
|
||||||
|
AiragModel model = airagModelService.getById(modelId);
|
||||||
|
AssertUtils.assertNotEmpty("向量模型不存在", model);
|
||||||
|
AssertUtils.assertEquals("仅支持向量模型", LLMConsts.MODEL_TYPE_EMBED, model.getModelType());
|
||||||
|
return model;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取向量存储
|
||||||
|
*
|
||||||
|
* @param model
|
||||||
|
* @return
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2025/2/18 14:56
|
||||||
|
*/
|
||||||
|
private EmbeddingStore<TextSegment> getEmbedStore(AiragModel model) {
|
||||||
|
AssertUtils.assertNotEmpty("未配置模型", model);
|
||||||
|
String modelId = model.getId();
|
||||||
|
String connectionInfo = embedStoreConfigBean.getHost() + embedStoreConfigBean.getPort() + embedStoreConfigBean.getDatabase();
|
||||||
|
String key = modelId + connectionInfo;
|
||||||
|
if (EMBED_STORE_CACHE.containsKey(key)) {
|
||||||
|
return EMBED_STORE_CACHE.get(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
AiModelOptions modelOp = buildModelOptions(model);
|
||||||
|
EmbeddingModel embeddingModel = AiModelFactory.createEmbeddingModel(modelOp);
|
||||||
|
EmbeddingStore<TextSegment> embeddingStore = PgVectorEmbeddingStore.builder()
|
||||||
|
// Connection and table parameters
|
||||||
|
.host(embedStoreConfigBean.getHost())
|
||||||
|
.port(embedStoreConfigBean.getPort())
|
||||||
|
.database(embedStoreConfigBean.getDatabase())
|
||||||
|
.user(embedStoreConfigBean.getUser())
|
||||||
|
.password(embedStoreConfigBean.getPassword())
|
||||||
|
.table(embedStoreConfigBean.getTable())
|
||||||
|
// Embedding dimension
|
||||||
|
// Required: Must match the embedding model’s output dimension
|
||||||
|
.dimension(embeddingModel.dimension())
|
||||||
|
// Indexing and performance options
|
||||||
|
// Enable IVFFlat index
|
||||||
|
.useIndex(true)
|
||||||
|
// Number of lists
|
||||||
|
// for IVFFlat index
|
||||||
|
.indexListSize(100)
|
||||||
|
// Table creation options
|
||||||
|
// Automatically create the table if it doesn’t exist
|
||||||
|
.createTable(true)
|
||||||
|
//Don’t drop the table first (set to true if you want a fresh start)
|
||||||
|
.dropTableFirst(false)
|
||||||
|
.build();
|
||||||
|
EMBED_STORE_CACHE.put(key, embeddingStore);
|
||||||
|
return embeddingStore;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造ModelOptions
|
||||||
|
*
|
||||||
|
* @param model
|
||||||
|
* @return
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2025/3/11 17:45
|
||||||
|
*/
|
||||||
|
public static AiModelOptions buildModelOptions(AiragModel model) {
|
||||||
|
AiModelOptions.AiModelOptionsBuilder modelOpBuilder = AiModelOptions.builder()
|
||||||
|
.provider(model.getProvider())
|
||||||
|
.modelName(model.getModelName())
|
||||||
|
.baseUrl(model.getBaseUrl());
|
||||||
|
if (oConvertUtils.isObjectNotEmpty(model.getCredential())) {
|
||||||
|
JSONObject modelCredential = JSONObject.parseObject(model.getCredential());
|
||||||
|
modelOpBuilder.apiKey(oConvertUtils.getString(modelCredential.getString("apiKey"), null));
|
||||||
|
modelOpBuilder.secretKey(oConvertUtils.getString(modelCredential.getString("secretKey"), null));
|
||||||
|
}
|
||||||
|
modelOpBuilder.topNumber(5);
|
||||||
|
modelOpBuilder.similarity(0.75);
|
||||||
|
return modelOpBuilder.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析文件
|
||||||
|
*
|
||||||
|
* @param doc
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2025/3/5 11:31
|
||||||
|
*/
|
||||||
|
private String parseFile(AiragKnowledgeDoc doc) {
|
||||||
|
String metadata = doc.getMetadata();
|
||||||
|
AssertUtils.assertNotEmpty("请先上传文件", metadata);
|
||||||
|
JSONObject metadataJson = JSONObject.parseObject(metadata);
|
||||||
|
if (!metadataJson.containsKey(LLMConsts.KNOWLEDGE_DOC_METADATA_FILEPATH)) {
|
||||||
|
throw new JeecgBootException("请先上传文件");
|
||||||
|
}
|
||||||
|
String filePath = metadataJson.getString(LLMConsts.KNOWLEDGE_DOC_METADATA_FILEPATH);
|
||||||
|
AssertUtils.assertNotEmpty("请先上传文件", filePath);
|
||||||
|
// 网络资源,先下载到临时目录
|
||||||
|
filePath = ensureFile(filePath);
|
||||||
|
// 提取文档内容
|
||||||
|
File docFile = new File(filePath);
|
||||||
|
if (docFile.exists()) {
|
||||||
|
Document document = new TikaDocumentParser(AutoDetectParser::new, null, null, null).parse(docFile);
|
||||||
|
if (null != document) {
|
||||||
|
String content = document.text();
|
||||||
|
// 判断是否md文档
|
||||||
|
String fileType = FilenameUtils.getExtension(docFile.getName());
|
||||||
|
if ("md".contains(fileType)) {
|
||||||
|
// 如果是md文件,查找所有图片语法,如果是本地图片,替换成网络图片
|
||||||
|
String baseUrl = doc.getBaseUrl() + "/sys/common/static/";
|
||||||
|
String sourcePath = metadataJson.getString(LLMConsts.KNOWLEDGE_DOC_METADATA_SOURCES_PATH);
|
||||||
|
if(oConvertUtils.isNotEmpty(sourcePath)) {
|
||||||
|
String escapedPath = uploadpath;
|
||||||
|
if (File.separator.equals("\\")){
|
||||||
|
escapedPath = uploadpath.replace("//", "\\\\");
|
||||||
|
}
|
||||||
|
sourcePath = sourcePath.replaceFirst("^" + escapedPath, "").replace("\\", "/");
|
||||||
|
baseUrl = baseUrl + sourcePath + "/";
|
||||||
|
StringBuffer sb = replaceImageUrl(content, baseUrl);
|
||||||
|
content = sb.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
private static StringBuffer replaceImageUrl(String content, String baseUrl) {
|
||||||
|
// 正则表达式匹配md文件中的图片语法 
|
||||||
|
String mdImagePattern = "!\\[(.*?)]\\((.*?)(\\s*=\\d+)?\\)";
|
||||||
|
Pattern pattern = Pattern.compile(mdImagePattern);
|
||||||
|
Matcher matcher = pattern.matcher(content);
|
||||||
|
|
||||||
|
StringBuffer sb = new StringBuffer();
|
||||||
|
while (matcher.find()) {
|
||||||
|
String imageUrl = matcher.group(2);
|
||||||
|
// 检查是否是本地图片路径
|
||||||
|
if (!imageUrl.startsWith("http")) {
|
||||||
|
// 替换成网络图片路径
|
||||||
|
String networkImageUrl = baseUrl + imageUrl;
|
||||||
|
matcher.appendReplacement(sb, "");
|
||||||
|
} else {
|
||||||
|
matcher.appendReplacement(sb, "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
matcher.appendTail(sb);
|
||||||
|
return sb;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通过MinerU解析文件
|
||||||
|
*
|
||||||
|
* @param doc
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2025/4/1 17:37
|
||||||
|
*/
|
||||||
|
private void parseFileByMinerU(AiragKnowledgeDoc doc) {
|
||||||
|
String metadata = doc.getMetadata();
|
||||||
|
AssertUtils.assertNotEmpty("请先上传文件", metadata);
|
||||||
|
JSONObject metadataJson = JSONObject.parseObject(metadata);
|
||||||
|
if (!metadataJson.containsKey(LLMConsts.KNOWLEDGE_DOC_METADATA_FILEPATH)) {
|
||||||
|
throw new JeecgBootException("请先上传文件");
|
||||||
|
}
|
||||||
|
String filePath = metadataJson.getString(LLMConsts.KNOWLEDGE_DOC_METADATA_FILEPATH);
|
||||||
|
AssertUtils.assertNotEmpty("请先上传文件", filePath);
|
||||||
|
filePath = ensureFile(filePath);
|
||||||
|
|
||||||
|
File docFile = new File(filePath);
|
||||||
|
String fileType = FilenameUtils.getExtension(filePath);
|
||||||
|
if (!docFile.exists()
|
||||||
|
|| "txt".equalsIgnoreCase(fileType)
|
||||||
|
|| "md".equalsIgnoreCase(fileType)) {
|
||||||
|
return ;
|
||||||
|
}
|
||||||
|
|
||||||
|
String command = "magic-pdf";
|
||||||
|
if (oConvertUtils.isNotEmpty(knowConfigBean.getCondaEnv())) {
|
||||||
|
command = "conda run -n " + knowConfigBean.getCondaEnv() + " " + command;
|
||||||
|
}
|
||||||
|
|
||||||
|
String outputPath = docFile.getParentFile().getAbsolutePath();
|
||||||
|
String[] args = {
|
||||||
|
"-p", docFile.getAbsolutePath(),
|
||||||
|
"-o", outputPath,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
String execLog = CommandExecUtil.execCommand(command, args);
|
||||||
|
log.info("执行命令行:" + command + " args:" + Arrays.toString(args) + "\n log::" + execLog);
|
||||||
|
// 如果成功,替换文件路径和静态资源路径
|
||||||
|
String fileBaseName = FilenameUtils.getBaseName(docFile.getName());
|
||||||
|
String newFileDir = outputPath + File.separator + fileBaseName + File.separator + "auto" + File.separator ;
|
||||||
|
// 先检查文件是否存在,存在才替换
|
||||||
|
File convertedFile = new File(newFileDir + fileBaseName + ".md");
|
||||||
|
if (convertedFile.exists()) {
|
||||||
|
log.info("文件转换成md成功,替换文件路径和静态资源路径");
|
||||||
|
newFileDir = newFileDir.replaceFirst("^" + uploadpath, "");
|
||||||
|
metadataJson.put(LLMConsts.KNOWLEDGE_DOC_METADATA_FILEPATH, newFileDir + fileBaseName + ".md");
|
||||||
|
metadataJson.put(LLMConsts.KNOWLEDGE_DOC_METADATA_SOURCES_PATH, newFileDir);
|
||||||
|
doc.setMetadata(metadataJson.toJSONString());
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error("文件转换md失败,使用传统提取方案{}", e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 确保文件存在
|
||||||
|
* @param filePath
|
||||||
|
* @return
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2025/4/1 17:36
|
||||||
|
*/
|
||||||
|
@NotNull
|
||||||
|
private String ensureFile(String filePath) {
|
||||||
|
// 网络资源,先下载到临时目录
|
||||||
|
Matcher matcher = LLMConsts.WEB_PATTERN.matcher(filePath);
|
||||||
|
if (matcher.matches()) {
|
||||||
|
log.info("网络资源,下载到临时目录:" + filePath);
|
||||||
|
// 准备文件
|
||||||
|
String tempFilePath = uploadpath + File.separator + "tmp" + File.separator + UUIDGenerator.generate() + File.separator;
|
||||||
|
String fileName = filePath;
|
||||||
|
if (fileName.contains("?")) {
|
||||||
|
fileName = fileName.substring(0, fileName.indexOf("?"));
|
||||||
|
}
|
||||||
|
fileName = FilenameUtils.getName(fileName);
|
||||||
|
tempFilePath = tempFilePath + fileName;
|
||||||
|
FileDownloadUtils.download2DiskFromNet(filePath, tempFilePath);
|
||||||
|
filePath = tempFilePath;
|
||||||
|
} else {
|
||||||
|
//本地文件
|
||||||
|
filePath = uploadpath + File.separator + filePath;
|
||||||
|
}
|
||||||
|
return filePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,23 @@
|
|||||||
|
package org.jeecg.modules.airag.llm.mapper;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
import org.jeecg.modules.airag.llm.entity.AiragKnowledgeDoc;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Description: airag知识库文档
|
||||||
|
* @Author: jeecg-boot
|
||||||
|
* @Date: 2025-02-18
|
||||||
|
* @Version: V1.0
|
||||||
|
*/
|
||||||
|
public interface AiragKnowledgeDocMapper extends BaseMapper<AiragKnowledgeDoc> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通过主表id删除子表数据
|
||||||
|
*
|
||||||
|
* @param mainId 主表id
|
||||||
|
* @return boolean
|
||||||
|
*/
|
||||||
|
public boolean deleteByMainId(@Param("mainId") String mainId);
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
package org.jeecg.modules.airag.llm.mapper;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import org.jeecg.modules.airag.llm.entity.AiragKnowledge;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Description: AIRag知识库
|
||||||
|
* @Author: jeecg-boot
|
||||||
|
* @Date: 2025-02-18
|
||||||
|
* @Version: V1.0
|
||||||
|
*/
|
||||||
|
public interface AiragKnowledgeMapper extends BaseMapper<AiragKnowledge> {
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
package org.jeecg.modules.airag.llm.mapper;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import org.jeecg.modules.airag.llm.entity.AiragModel;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Description: AiRag模型配置
|
||||||
|
* @Author: jeecg-boot
|
||||||
|
* @Date: 2025-02-14
|
||||||
|
* @Version: V1.0
|
||||||
|
*/
|
||||||
|
public interface AiragModelMapper extends BaseMapper<AiragModel> {
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-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.AiragKnowledgeDocMapper">
|
||||||
|
|
||||||
|
<delete id="deleteByMainId" parameterType="java.lang.String">
|
||||||
|
DELETE
|
||||||
|
FROM airag_knowledge_doc
|
||||||
|
WHERE knowledge_id = #{mainId}
|
||||||
|
</delete>
|
||||||
|
|
||||||
|
</mapper>
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-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">
|
||||||
|
|
||||||
|
</mapper>
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-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">
|
||||||
|
|
||||||
|
</mapper>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user